Upload canvasRenderer.ts
Browse files- frontend/src/utils/canvasRenderer.ts +192 -45
frontend/src/utils/canvasRenderer.ts
CHANGED
@@ -29,46 +29,103 @@ export async function renderElementToCanvas(
|
|
29 |
element: HTMLElement,
|
30 |
options: RenderOptions = {}
|
31 |
): Promise<HTMLCanvasElement> {
|
32 |
-
const {
|
33 |
-
scale =
|
34 |
-
backgroundColor = null,
|
35 |
-
useCORS = true,
|
36 |
-
timeout =
|
37 |
} = options;
|
38 |
-
|
39 |
-
|
40 |
-
|
41 |
-
|
42 |
-
|
43 |
-
|
44 |
-
|
45 |
-
|
46 |
-
|
47 |
-
|
48 |
-
|
49 |
-
|
50 |
-
|
51 |
-
|
52 |
-
|
53 |
-
|
54 |
-
|
55 |
-
|
56 |
-
|
57 |
-
|
58 |
-
|
59 |
-
|
60 |
-
|
61 |
-
|
62 |
-
|
63 |
-
|
64 |
-
|
65 |
-
|
66 |
-
|
67 |
-
|
68 |
-
|
69 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
70 |
}
|
71 |
-
|
|
|
|
|
|
|
|
|
|
|
72 |
}
|
73 |
|
74 |
/**
|
@@ -81,12 +138,10 @@ export async function renderSVGToCanvas(
|
|
81 |
svgElement: SVGElement,
|
82 |
options: RenderOptions = {}
|
83 |
): Promise<HTMLCanvasElement> {
|
84 |
-
// 对于SVG元素,我们可以直接使用html2canvas
|
85 |
-
// 或者使用传统的SVG序列化方��
|
86 |
const {
|
87 |
scale = 1,
|
88 |
backgroundColor = null,
|
89 |
-
timeout =
|
90 |
} = options;
|
91 |
|
92 |
return new Promise((resolve, reject) => {
|
@@ -95,10 +150,31 @@ export async function renderSVGToCanvas(
|
|
95 |
}, timeout);
|
96 |
|
97 |
try {
|
|
|
|
|
98 |
// 获取SVG尺寸
|
99 |
const rect = svgElement.getBoundingClientRect();
|
100 |
-
|
101 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
102 |
|
103 |
// 创建Canvas
|
104 |
const canvas = document.createElement('canvas');
|
@@ -120,17 +196,84 @@ export async function renderSVGToCanvas(
|
|
120 |
ctx.fillRect(0, 0, width, height);
|
121 |
}
|
122 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
123 |
// 获取SVG的XML字符串
|
124 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
125 |
const svgBlob = new Blob([svgData], { type: 'image/svg+xml;charset=utf-8' });
|
126 |
const url = URL.createObjectURL(svgBlob);
|
127 |
|
128 |
const img = new Image();
|
|
|
129 |
img.onload = () => {
|
130 |
try {
|
|
|
131 |
ctx.drawImage(img, 0, 0, width, height);
|
132 |
URL.revokeObjectURL(url);
|
133 |
clearTimeout(timeoutId);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
134 |
resolve(canvas);
|
135 |
} catch (error) {
|
136 |
URL.revokeObjectURL(url);
|
@@ -139,14 +282,18 @@ export async function renderSVGToCanvas(
|
|
139 |
}
|
140 |
};
|
141 |
|
142 |
-
img.onerror = () => {
|
|
|
143 |
URL.revokeObjectURL(url);
|
144 |
clearTimeout(timeoutId);
|
145 |
reject(new Error('SVG image loading failed'));
|
146 |
};
|
147 |
|
|
|
148 |
img.src = url;
|
|
|
149 |
} catch (error) {
|
|
|
150 |
clearTimeout(timeoutId);
|
151 |
reject(new Error(`SVG serialization failed: ${error}`));
|
152 |
}
|
|
|
29 |
element: HTMLElement,
|
30 |
options: RenderOptions = {}
|
31 |
): Promise<HTMLCanvasElement> {
|
32 |
+
const {
|
33 |
+
scale = 2, // 默认使用更高的缩放比例
|
34 |
+
backgroundColor = null, // 默认透明背景
|
35 |
+
useCORS = true,
|
36 |
+
timeout = 15000 // 增加超时时间
|
37 |
} = options;
|
38 |
+
|
39 |
+
try {
|
40 |
+
// 动态导入html2canvas以减少初始包大小
|
41 |
+
const html2canvas = (await import('html2canvas')).default;
|
42 |
+
|
43 |
+
// 获取元素的实际尺寸
|
44 |
+
const rect = element.getBoundingClientRect();
|
45 |
+
const computedStyle = window.getComputedStyle(element);
|
46 |
+
|
47 |
+
const renderWidth = Math.max(
|
48 |
+
rect.width,
|
49 |
+
element.offsetWidth,
|
50 |
+
parseFloat(computedStyle.width) || 0
|
51 |
+
);
|
52 |
+
const renderHeight = Math.max(
|
53 |
+
rect.height,
|
54 |
+
element.offsetHeight,
|
55 |
+
parseFloat(computedStyle.height) || 0
|
56 |
+
);
|
57 |
+
|
58 |
+
console.log('Canvas rendering dimensions:', {
|
59 |
+
boundingRect: { width: rect.width, height: rect.height },
|
60 |
+
offset: { width: element.offsetWidth, height: element.offsetHeight },
|
61 |
+
computed: { width: computedStyle.width, height: computedStyle.height },
|
62 |
+
final: { width: renderWidth, height: renderHeight }
|
63 |
+
});
|
64 |
+
|
65 |
+
const canvas = await html2canvas(element, {
|
66 |
+
scale,
|
67 |
+
backgroundColor,
|
68 |
+
useCORS,
|
69 |
+
allowTaint: false,
|
70 |
+
foreignObjectRendering: true,
|
71 |
+
logging: false,
|
72 |
+
width: renderWidth || 100,
|
73 |
+
height: renderHeight || 100,
|
74 |
+
windowWidth: window.innerWidth,
|
75 |
+
windowHeight: window.innerHeight,
|
76 |
+
scrollX: 0,
|
77 |
+
scrollY: 0,
|
78 |
+
onclone: (clonedDoc, clonedElement) => {
|
79 |
+
// 在克隆的文档中应用样式修复
|
80 |
+
const targetElement = clonedElement || clonedDoc.querySelector(`[data-element-id="${element.getAttribute('data-element-id')}"]`);
|
81 |
+
if (targetElement) {
|
82 |
+
// 确保SVG元素正确渲染
|
83 |
+
const svgElements = targetElement.querySelectorAll('svg');
|
84 |
+
svgElements.forEach(svg => {
|
85 |
+
// 设置SVG尺寸
|
86 |
+
if (!svg.getAttribute('width') || !svg.getAttribute('height')) {
|
87 |
+
const svgRect = svg.getBoundingClientRect();
|
88 |
+
if (svgRect.width > 0 && svgRect.height > 0) {
|
89 |
+
svg.setAttribute('width', svgRect.width.toString());
|
90 |
+
svg.setAttribute('height', svgRect.height.toString());
|
91 |
+
} else {
|
92 |
+
svg.setAttribute('width', renderWidth.toString());
|
93 |
+
svg.setAttribute('height', renderHeight.toString());
|
94 |
+
}
|
95 |
+
}
|
96 |
+
|
97 |
+
// 确保viewBox存在
|
98 |
+
if (!svg.getAttribute('viewBox')) {
|
99 |
+
const width = svg.getAttribute('width') || renderWidth;
|
100 |
+
const height = svg.getAttribute('height') || renderHeight;
|
101 |
+
svg.setAttribute('viewBox', `0 0 ${width} ${height}`);
|
102 |
+
}
|
103 |
+
|
104 |
+
// 添加必要的命名空间
|
105 |
+
if (!svg.getAttribute('xmlns')) {
|
106 |
+
svg.setAttribute('xmlns', 'http://www.w3.org/2000/svg');
|
107 |
+
}
|
108 |
+
});
|
109 |
+
|
110 |
+
// 确保元素可见
|
111 |
+
const style = targetElement.style;
|
112 |
+
style.visibility = 'visible';
|
113 |
+
style.opacity = '1';
|
114 |
+
style.display = style.display === 'none' ? 'block' : style.display;
|
115 |
+
}
|
116 |
+
}
|
117 |
+
});
|
118 |
+
|
119 |
+
// 验证生成的Canvas
|
120 |
+
if (canvas.width === 0 || canvas.height === 0) {
|
121 |
+
throw new Error('Generated canvas has zero dimensions');
|
122 |
}
|
123 |
+
|
124 |
+
return canvas;
|
125 |
+
} catch (error) {
|
126 |
+
console.error('Canvas rendering failed:', error);
|
127 |
+
throw new Error(`Canvas rendering failed: ${error}`);
|
128 |
+
}
|
129 |
}
|
130 |
|
131 |
/**
|
|
|
138 |
svgElement: SVGElement,
|
139 |
options: RenderOptions = {}
|
140 |
): Promise<HTMLCanvasElement> {
|
|
|
|
|
141 |
const {
|
142 |
scale = 1,
|
143 |
backgroundColor = null,
|
144 |
+
timeout = 10000 // 增加超时时间
|
145 |
} = options;
|
146 |
|
147 |
return new Promise((resolve, reject) => {
|
|
|
150 |
}, timeout);
|
151 |
|
152 |
try {
|
153 |
+
console.log('renderSVGToCanvas: Starting SVG to Canvas conversion');
|
154 |
+
|
155 |
// 获取SVG尺寸
|
156 |
const rect = svgElement.getBoundingClientRect();
|
157 |
+
let width = rect.width || svgElement.clientWidth;
|
158 |
+
let height = rect.height || svgElement.clientHeight;
|
159 |
+
|
160 |
+
// 从SVG属性获取尺寸作为备选
|
161 |
+
if (width <= 0 || height <= 0) {
|
162 |
+
const svgWidth = svgElement.getAttribute('width');
|
163 |
+
const svgHeight = svgElement.getAttribute('height');
|
164 |
+
if (svgWidth && svgHeight) {
|
165 |
+
width = parseFloat(svgWidth);
|
166 |
+
height = parseFloat(svgHeight);
|
167 |
+
}
|
168 |
+
}
|
169 |
+
|
170 |
+
// 使用默认尺寸作为最后备选
|
171 |
+
if (width <= 0 || height <= 0) {
|
172 |
+
width = 300;
|
173 |
+
height = 200;
|
174 |
+
console.warn('renderSVGToCanvas: Using default dimensions due to zero size');
|
175 |
+
}
|
176 |
+
|
177 |
+
console.log('renderSVGToCanvas: SVG dimensions:', { width, height });
|
178 |
|
179 |
// 创建Canvas
|
180 |
const canvas = document.createElement('canvas');
|
|
|
196 |
ctx.fillRect(0, 0, width, height);
|
197 |
}
|
198 |
|
199 |
+
// 克隆并优化SVG元素
|
200 |
+
const clonedSVG = svgElement.cloneNode(true) as SVGElement;
|
201 |
+
|
202 |
+
// 确保SVG有正确的命名空间和属性
|
203 |
+
clonedSVG.setAttribute('xmlns', 'http://www.w3.org/2000/svg');
|
204 |
+
clonedSVG.setAttribute('xmlns:xlink', 'http://www.w3.org/1999/xlink');
|
205 |
+
|
206 |
+
// 设置明确的尺寸
|
207 |
+
if (!clonedSVG.getAttribute('width')) {
|
208 |
+
clonedSVG.setAttribute('width', width.toString());
|
209 |
+
}
|
210 |
+
if (!clonedSVG.getAttribute('height')) {
|
211 |
+
clonedSVG.setAttribute('height', height.toString());
|
212 |
+
}
|
213 |
+
|
214 |
+
// 设置viewBox
|
215 |
+
if (!clonedSVG.getAttribute('viewBox')) {
|
216 |
+
clonedSVG.setAttribute('viewBox', `0 0 ${width} ${height}`);
|
217 |
+
}
|
218 |
+
|
219 |
+
// 移除可能导致问题的属性
|
220 |
+
const problematicAttrs = ['vector-effect'];
|
221 |
+
const removeProblematicAttrs = (element: Element) => {
|
222 |
+
problematicAttrs.forEach(attr => {
|
223 |
+
if (element.hasAttribute(attr)) {
|
224 |
+
element.removeAttribute(attr);
|
225 |
+
}
|
226 |
+
});
|
227 |
+
|
228 |
+
// 递归处理子元素
|
229 |
+
Array.from(element.children).forEach(child => {
|
230 |
+
removeProblematicAttrs(child);
|
231 |
+
});
|
232 |
+
};
|
233 |
+
|
234 |
+
removeProblematicAttrs(clonedSVG);
|
235 |
+
|
236 |
// 获取SVG的XML字符串
|
237 |
+
let svgData = new XMLSerializer().serializeToString(clonedSVG);
|
238 |
+
|
239 |
+
// 清理和优化SVG字符串
|
240 |
+
svgData = svgData.replace(/vector-effect="[^"]*"/g, '');
|
241 |
+
|
242 |
+
// 确保SVG字符串格式正确
|
243 |
+
if (!svgData.includes('xmlns="http://www.w3.org/2000/svg"')) {
|
244 |
+
svgData = svgData.replace('<svg', '<svg xmlns="http://www.w3.org/2000/svg"');
|
245 |
+
}
|
246 |
+
|
247 |
+
console.log('renderSVGToCanvas: SVG data prepared:', {
|
248 |
+
length: svgData.length,
|
249 |
+
hasNamespace: svgData.includes('xmlns="http://www.w3.org/2000/svg"'),
|
250 |
+
preview: svgData.substring(0, 200) + '...'
|
251 |
+
});
|
252 |
+
|
253 |
const svgBlob = new Blob([svgData], { type: 'image/svg+xml;charset=utf-8' });
|
254 |
const url = URL.createObjectURL(svgBlob);
|
255 |
|
256 |
const img = new Image();
|
257 |
+
|
258 |
img.onload = () => {
|
259 |
try {
|
260 |
+
console.log('renderSVGToCanvas: Image loaded successfully, drawing to canvas');
|
261 |
ctx.drawImage(img, 0, 0, width, height);
|
262 |
URL.revokeObjectURL(url);
|
263 |
clearTimeout(timeoutId);
|
264 |
+
|
265 |
+
// 验证Canvas内容
|
266 |
+
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
|
267 |
+
const hasContent = imageData.data.some((value, index) => {
|
268 |
+
// 检查非透明像素 (alpha通道)
|
269 |
+
return index % 4 === 3 && value > 0;
|
270 |
+
});
|
271 |
+
|
272 |
+
if (!hasContent) {
|
273 |
+
console.warn('renderSVGToCanvas: Canvas appears to be empty after drawing');
|
274 |
+
}
|
275 |
+
|
276 |
+
console.log('renderSVGToCanvas: Canvas rendering completed successfully');
|
277 |
resolve(canvas);
|
278 |
} catch (error) {
|
279 |
URL.revokeObjectURL(url);
|
|
|
282 |
}
|
283 |
};
|
284 |
|
285 |
+
img.onerror = (error) => {
|
286 |
+
console.error('renderSVGToCanvas: Image loading failed:', error);
|
287 |
URL.revokeObjectURL(url);
|
288 |
clearTimeout(timeoutId);
|
289 |
reject(new Error('SVG image loading failed'));
|
290 |
};
|
291 |
|
292 |
+
// 设置图片源
|
293 |
img.src = url;
|
294 |
+
|
295 |
} catch (error) {
|
296 |
+
console.error('renderSVGToCanvas: SVG serialization failed:', error);
|
297 |
clearTimeout(timeoutId);
|
298 |
reject(new Error(`SVG serialization failed: ${error}`));
|
299 |
}
|