-
Notifications
You must be signed in to change notification settings - Fork 2.6k
fix: PDF export cross-domain #3945
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -6,7 +6,8 @@ | |
| @date:2025/6/6 11:18 | ||
| @desc: | ||
| """ | ||
| from django.http import HttpResponse | ||
| import requests | ||
| from django.http import HttpResponse, StreamingHttpResponse | ||
| from django.utils.translation import gettext_lazy as _ | ||
| from drf_spectacular.utils import extend_schema | ||
| from rest_framework.parsers import MultiPartParser | ||
|
|
@@ -30,6 +31,39 @@ | |
| from users.serializers.login import CaptchaSerializer | ||
|
|
||
|
|
||
| def stream_image(response): | ||
| """生成器函数,用于流式传输图片数据""" | ||
| for chunk in response.iter_content(chunk_size=4096): | ||
| if chunk: # 过滤掉保持连接的空块 | ||
| yield chunk | ||
|
|
||
|
|
||
| class ResourceProxy(APIView): | ||
| def get(self, request: Request): | ||
| image_url = request.query_params.get("url") | ||
| if not image_url: | ||
| return result.error("Missing 'url' parameter") | ||
| try: | ||
|
|
||
| # 发送GET请求,流式获取图片内容 | ||
| response = requests.get( | ||
| image_url, | ||
| stream=True, # 启用流式响应 | ||
| allow_redirects=True, | ||
| timeout=10 | ||
| ) | ||
| content_type = response.headers.get('Content-Type', '').split(';')[0] | ||
| # 创建Django流式响应 | ||
| django_response = StreamingHttpResponse( | ||
| stream_image(response), # 使用生成器 | ||
| content_type=content_type | ||
| ) | ||
|
|
||
| return django_response | ||
| except Exception as e: | ||
| return result.error(f"Image request failed: {str(e)}") | ||
|
|
||
|
|
||
| class OpenAIView(APIView): | ||
| authentication_classes = [TokenAuth] | ||
|
|
||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There are several potential issues and optimizations in the provided code: Potential Issues:
Optimization Suggestions:
Here's an updated version of the code with some suggested improvements: import functools
# Import necessary Django/Rest Framework libraries
from django.http import HttpResponse, StreamingHttpResponse
from drf_spectacular.utils import extend_schema
from rest_framework.parsers import MultiPartParser
from rest_framework.views import APIView
from rest_framework.request import Request
from core.results import result
from core.authentication import TokenAuth
@extend_schema(responses=result.serializer)
class OpenAIView(APIView):
authentication_classes = [TokenAuth]
@staticmethod
def process_image(url: str) -> HTTPResponse:
# Helper function to fetch and stream the image
async def _fetch_and_stream():
try:
response = await asyncio.sleep(0.1) # Simulate asynchronous behavior
response.raise_for_status() # Raises exception on HTTP error codes
content_type = response.headers['Content-Type']
return StreamingHttpResponse(
content=response.iter_content(chunk_size=4096),
status=response.status_code,
headers={'Content-Type': content_type}
)
except Exception as e:
print(f"Image request failed: {str(e)}")
return None
return _fetch_and_stream()
async def get(self, request: Request):
url_param = request.query_params.get("url", "")
if not url_param:
return result.error("Missing 'url' parameter")
return await self.process_image(url_param)
class ImageStreamer(object):
def __init__(self, response):
self._response = response
def __aiter__(self):
return self
async def __anext__(self):
chunk = await self._response.read(4096)
if not chunk:
raise StopAsyncIteration
return chunkExplanation of Changes:
These changes make the code cleaner, more modular, and better suited for handling larger datasets without consuming excessive resources. |
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -11,6 +11,7 @@ | |
| v-loading="loading" | ||
| style="height: calc(70vh - 150px); overflow-y: auto; display: flex; justify-content: center" | ||
| > | ||
| <div ref="cloneContainerRef" style="width: 100%"></div> | ||
| <div ref="svgContainerRef"></div> | ||
| </div> | ||
| <template #footer> | ||
|
|
@@ -39,42 +40,82 @@ import html2Canvas from 'html2canvas' | |
| import { jsPDF } from 'jspdf' | ||
| const loading = ref<boolean>(false) | ||
| const svgContainerRef = ref() | ||
| const cloneContainerRef = ref() | ||
| const dialogVisible = ref<boolean>(false) | ||
| const open = (element: HTMLElement | null) => { | ||
| dialogVisible.value = true | ||
| loading.value = true | ||
| if (!element) { | ||
| return | ||
| } | ||
| setTimeout(() => { | ||
| nextTick(() => { | ||
| htmlToImage | ||
| .toSvg(element, { pixelRatio: 1, quality: 1 }) | ||
| .then((dataUrl) => { | ||
| return fetch(dataUrl) | ||
| .then((response) => { | ||
| return response.text() | ||
| }) | ||
| .then((text) => { | ||
| const parser = new DOMParser() | ||
| const svgDoc = parser.parseFromString(text, 'image/svg+xml') | ||
| const svgElement = svgDoc.documentElement | ||
| svgContainerRef.value.appendChild(svgElement) | ||
| svgContainerRef.value.style.height = svgElement.scrollHeight + 'px' | ||
| }) | ||
| }) | ||
| .finally(() => { | ||
| loading.value = false | ||
| }) | ||
| const cElement = element.cloneNode(true) as HTMLElement | ||
| const images = cElement.querySelectorAll('img') | ||
| const loadPromises = Array.from(images).map((img) => { | ||
| if (!img.src.startsWith(window.origin) && img.src.startsWith('http')) { | ||
| img.src = `${window.MaxKB.prefix}/api/resource_proxy?url=${encodeURIComponent(img.src)}` | ||
| } | ||
| img.setAttribute('onerror', '') | ||
| return new Promise((resolve) => { | ||
| // 已加载完成的图片直接 resolve | ||
| if (img.complete) { | ||
| resolve({ img, success: img.naturalWidth > 0 }) | ||
| return | ||
| } | ||
|
|
||
| // 未加载完成的图片监听事件 | ||
| img.onload = () => resolve({ img, success: true }) | ||
| img.onerror = () => resolve({ img, success: false }) | ||
| }) | ||
| }, 1) | ||
| }) | ||
| Promise.all(loadPromises).finally(() => { | ||
| setTimeout(() => { | ||
| nextTick(() => { | ||
| cloneContainerRef.value.appendChild(cElement) | ||
| htmlToImage | ||
| .toSvg(cElement, { | ||
| pixelRatio: 1, | ||
| quality: 1, | ||
| onImageErrorHandler: ( | ||
| event: Event | string, | ||
| source?: string, | ||
| lineno?: number, | ||
| colno?: number, | ||
| error?: Error, | ||
| ) => { | ||
| console.log(event, source, lineno, colno, error) | ||
| }, | ||
| }) | ||
| .then((dataUrl) => { | ||
| return fetch(dataUrl) | ||
| .then((response) => { | ||
| return response.text() | ||
| }) | ||
| .then((text) => { | ||
| const parser = new DOMParser() | ||
| const svgDoc = parser.parseFromString(text, 'image/svg+xml') | ||
| cloneContainerRef.value.style.display = 'none' | ||
| const svgElement = svgDoc.documentElement | ||
| svgContainerRef.value.appendChild(svgElement) | ||
| svgContainerRef.value.style.height = svgElement.scrollHeight + 'px' | ||
| }) | ||
| }) | ||
| .finally(() => { | ||
| loading.value = false | ||
| }) | ||
| .catch((e) => { | ||
| loading.value = false | ||
| }) | ||
| }) | ||
| }, 1) | ||
| }) | ||
| } | ||
|
|
||
| const exportPDF = () => { | ||
| loading.value = true | ||
| setTimeout(() => { | ||
| nextTick(() => { | ||
| html2Canvas(svgContainerRef.value, { | ||
| scale: 2, | ||
| logging: false, | ||
| }) | ||
| .then((canvas) => { | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The code looks generally consistent for handling HTML to SVG conversion with image proxying, but there are a few recommendations:
Here's the revised version of the relevant parts with these considerations: import html2Canvas from 'html2canvas'
import { jsPDF } from 'jspdf'
const loading = ref<boolean>(false)
const svgContainerRef = ref<any>()
+ const cloneContainerRef = ref<any>()
...
const open = (element: HTMLElement | null) => {
dialogVisible.value = true
loading.value = true
if (!element) {
return
}
+ nextTick(() => {
+ // Clone the HTML node while processing child nodes
+ let clonedElement;
+ html2Canvas(element, { canvas, scaleFactor: 2 }).then(canvas => {
+ loading.value = false;
+ if ((clonedElement = element.cloneNode())) {
+ const promises = [];
+ clonedElement.childNodes.forEach(node => {
+ if (node.nodeType === Node.ELEMENT_NODE && node.tagName.toLowerCase() === 'image') {
+ promises.push(processImgTagForProxy(node));
+ }
+ });
+ loadPromises.then(allLoaded => {
+ this.createSVGWithClonedDOM(clonedElement);
+ }).catch(err => {
+ console.error('Failed to process image tags:', err);
+ });
+ }
+ });
+ function createSVGWithClonedDOM(targetElm: Element) {
+ // Clear previous data in containers
+ svgContainerRef.value.innerHTML = '';
+ cloneContainerRef.value.innerHTML = '';
+ const tempDoc = document.createElement('template');
+ tempDoc.appendChild(targetElm);
+
+ const parser = new DOMParser();
+ const doc = parser.parseFromString(tempDoc.innerHTML, "image/svg+xml");
+ const svg = doc.documentElement;
+ // Adjust container styling based on svg content size
+ const maxHeight = Math.max(svg.clientHeight * 1.8, window.innerHeight /2);
+ svgContainerRef.value.style.maxHeight=maxHeight + 'px';
+ cloneContainerRef.value.appendChild(svg);
+ // Finalize PDF generation once SVG is appended and styled appropriately
// ...
};Ensure that you handle errors properly by checking returned promises in a more structured way to avoid nested callbacks within async flows. This will help make your application robust against unexpected behavior during asynchronous operations. |
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The provided code has two main issues:
Incorrect Return Type for Non-MultiSelect Case: In the line where
return default_value_listis executed, it should only be returned if_typeis not'MultiSelect'. This ensures that the function behaves correctly when a non-multi-select type is encountered.Redundant List Comprehension in Default MultiSelect Case: The condition
if _type == 'MultiSelect': return default_value_list;does not need to use a list comprehension becausedefault_value_listis already a Python list at this point.Here's the corrected version of the code:
To optimize further, ensure that any operations performed on lists (like
len(), comprehensions, etc.) are efficient, especially on larger data sets. Also, consider adding error handling or raising exceptions in case certain conditions are not met, which can provide clearer feedback about the unexpected behavior of the code under different input scenarios.