CatPtain commited on
Commit
66b71dc
·
verified ·
1 Parent(s): 77be7be

Upload 2 files

Browse files
frontend/src/utils/huggingfaceRenderer.ts ADDED
@@ -0,0 +1,328 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * Huggingface环境专用渲染器
3
+ * 解决Huggingface Spaces部署环境下的矢量图形渲染问题
4
+ */
5
+
6
+ // 检测是否在Huggingface环境中
7
+ const isHuggingfaceEnvironment = (): boolean => {
8
+ return (
9
+ typeof window !== 'undefined' &&
10
+ (window.location.hostname.includes('hf.space') ||
11
+ window.location.hostname.includes('huggingface.co') ||
12
+ process.env.NODE_ENV === 'production')
13
+ );
14
+ };
15
+
16
+ // Huggingface环境专用配置
17
+ interface HuggingfaceRenderConfig {
18
+ useCORS: boolean;
19
+ allowTaint: boolean;
20
+ foreignObjectRendering: boolean;
21
+ scale: number;
22
+ logging: boolean;
23
+ timeout: number;
24
+ backgroundColor: string | null;
25
+ removeContainer: boolean;
26
+ imageTimeout: number;
27
+ onclone?: (clonedDoc: Document, element: HTMLElement) => void;
28
+ }
29
+
30
+ // 获取Huggingface环境优化配置
31
+ const getHuggingfaceConfig = (): HuggingfaceRenderConfig => {
32
+ const isHF = isHuggingfaceEnvironment();
33
+
34
+ return {
35
+ useCORS: false, // Huggingface环境下禁用CORS
36
+ allowTaint: true, // 允许污染画布
37
+ foreignObjectRendering: false, // 禁用foreignObject渲染
38
+ scale: isHF ? 1 : 2, // Huggingface环境使用较低缩放
39
+ logging: false, // 生产环境禁用日志
40
+ timeout: 30000, // 增加超时时间
41
+ backgroundColor: null,
42
+ removeContainer: true,
43
+ imageTimeout: 15000,
44
+ onclone: (clonedDoc: Document, element: HTMLElement) => {
45
+ // Huggingface环境特殊处理
46
+ if (isHF) {
47
+ // 移除可能导致CORS问题的外部资源
48
+ const externalImages = clonedDoc.querySelectorAll('img[src^="http"]');
49
+ externalImages.forEach(img => {
50
+ const imgElement = img as HTMLImageElement;
51
+ // 替换为占位符或移除
52
+ imgElement.style.display = 'none';
53
+ });
54
+
55
+ // 处理SVG元素
56
+ const svgElements = clonedDoc.querySelectorAll('svg');
57
+ svgElements.forEach(svg => {
58
+ // 确保SVG有正确的命名空间
59
+ svg.setAttribute('xmlns', 'http://www.w3.org/2000/svg');
60
+
61
+ // 移除可能导致问题的属性
62
+ svg.removeAttribute('vector-effect');
63
+
64
+ // 确保尺寸明确
65
+ const rect = svg.getBoundingClientRect();
66
+ if (!svg.getAttribute('width') && rect.width > 0) {
67
+ svg.setAttribute('width', rect.width.toString());
68
+ }
69
+ if (!svg.getAttribute('height') && rect.height > 0) {
70
+ svg.setAttribute('height', rect.height.toString());
71
+ }
72
+
73
+ // 设置viewBox
74
+ if (!svg.getAttribute('viewBox')) {
75
+ const width = svg.getAttribute('width') || '100';
76
+ const height = svg.getAttribute('height') || '100';
77
+ svg.setAttribute('viewBox', `0 0 ${width} ${height}`);
78
+ }
79
+ });
80
+
81
+ // 内联所有样式
82
+ const allElements = clonedDoc.querySelectorAll('*');
83
+ allElements.forEach(el => {
84
+ const element = el as HTMLElement;
85
+ const computedStyle = window.getComputedStyle(element);
86
+
87
+ // 关键样式属性
88
+ const importantStyles = [
89
+ 'color', 'background-color', 'font-family', 'font-size',
90
+ 'font-weight', 'fill', 'stroke', 'stroke-width', 'opacity'
91
+ ];
92
+
93
+ importantStyles.forEach(prop => {
94
+ const value = computedStyle.getPropertyValue(prop);
95
+ if (value && value !== 'initial' && value !== 'inherit') {
96
+ element.style.setProperty(prop, value, 'important');
97
+ }
98
+ });
99
+ });
100
+ }
101
+ }
102
+ };
103
+ };
104
+
105
+ /**
106
+ * Huggingface环境专用SVG转Base64
107
+ * @param element SVG元素
108
+ * @returns Promise<string> Base64数据URL
109
+ */
110
+ export const huggingfaceSvg2Base64 = async (element: Element): Promise<string> => {
111
+ console.log('huggingfaceSvg2Base64: Starting conversion for Huggingface environment');
112
+
113
+ try {
114
+ // 方法1: 直接序列化SVG(最兼容)
115
+ const directResult = await directSvgSerialization(element);
116
+ if (directResult) {
117
+ console.log('huggingfaceSvg2Base64: Direct serialization succeeded');
118
+ return directResult;
119
+ }
120
+ } catch (error) {
121
+ console.warn('huggingfaceSvg2Base64: Direct serialization failed:', error);
122
+ }
123
+
124
+ try {
125
+ // 方法2: Canvas渲染(备选)
126
+ const canvasResult = await canvasSvgRendering(element);
127
+ if (canvasResult) {
128
+ console.log('huggingfaceSvg2Base64: Canvas rendering succeeded');
129
+ return canvasResult;
130
+ }
131
+ } catch (error) {
132
+ console.warn('huggingfaceSvg2Base64: Canvas rendering failed:', error);
133
+ }
134
+
135
+ // 方法3: 最小化SVG(最后备选)
136
+ console.log('huggingfaceSvg2Base64: Using minimal SVG fallback');
137
+ return createMinimalSvg(element);
138
+ };
139
+
140
+ /**
141
+ * 直接SVG序列化方法
142
+ */
143
+ const directSvgSerialization = async (element: Element): Promise<string | null> => {
144
+ try {
145
+ const clonedElement = element.cloneNode(true) as Element;
146
+
147
+ // 确保是SVG元素
148
+ if (clonedElement.tagName.toLowerCase() === 'svg') {
149
+ const svgElement = clonedElement as SVGElement;
150
+
151
+ // 设置必要属性
152
+ svgElement.setAttribute('xmlns', 'http://www.w3.org/2000/svg');
153
+ svgElement.setAttribute('xmlns:xlink', 'http://www.w3.org/1999/xlink');
154
+
155
+ // 获取尺寸
156
+ const rect = element.getBoundingClientRect();
157
+ const width = rect.width || 100;
158
+ const height = rect.height || 100;
159
+
160
+ svgElement.setAttribute('width', width.toString());
161
+ svgElement.setAttribute('height', height.toString());
162
+ svgElement.setAttribute('viewBox', `0 0 ${width} ${height}`);
163
+
164
+ // 序列化
165
+ const serializer = new XMLSerializer();
166
+ let svgString = serializer.serializeToString(svgElement);
167
+
168
+ // 清理
169
+ svgString = svgString.replace(/vector-effect="[^"]*"/g, '');
170
+
171
+ // 编码
172
+ const base64 = btoa(unescape(encodeURIComponent(svgString)));
173
+ return `data:image/svg+xml;base64,${base64}`;
174
+ }
175
+
176
+ return null;
177
+ } catch (error) {
178
+ console.error('directSvgSerialization failed:', error);
179
+ return null;
180
+ }
181
+ };
182
+
183
+ /**
184
+ * Canvas SVG渲染方法
185
+ */
186
+ const canvasSvgRendering = async (element: Element): Promise<string | null> => {
187
+ return new Promise((resolve) => {
188
+ try {
189
+ const rect = element.getBoundingClientRect();
190
+ const width = rect.width || 100;
191
+ const height = rect.height || 100;
192
+
193
+ const canvas = document.createElement('canvas');
194
+ const ctx = canvas.getContext('2d');
195
+
196
+ if (!ctx) {
197
+ resolve(null);
198
+ return;
199
+ }
200
+
201
+ canvas.width = width;
202
+ canvas.height = height;
203
+
204
+ // 创建SVG数据URL
205
+ const svgData = new XMLSerializer().serializeToString(element);
206
+ const svgBlob = new Blob([svgData], { type: 'image/svg+xml;charset=utf-8' });
207
+ const url = URL.createObjectURL(svgBlob);
208
+
209
+ const img = new Image();
210
+ img.onload = () => {
211
+ ctx.drawImage(img, 0, 0, width, height);
212
+ URL.revokeObjectURL(url);
213
+ resolve(canvas.toDataURL('image/png'));
214
+ };
215
+
216
+ img.onerror = () => {
217
+ URL.revokeObjectURL(url);
218
+ resolve(null);
219
+ };
220
+
221
+ // 设置超时
222
+ setTimeout(() => {
223
+ URL.revokeObjectURL(url);
224
+ resolve(null);
225
+ }, 5000);
226
+
227
+ img.src = url;
228
+ } catch (error) {
229
+ console.error('canvasSvgRendering failed:', error);
230
+ resolve(null);
231
+ }
232
+ });
233
+ };
234
+
235
+ /**
236
+ * 创建最小化SVG
237
+ */
238
+ const createMinimalSvg = (element: Element): string => {
239
+ const rect = element.getBoundingClientRect();
240
+ const width = rect.width || 100;
241
+ const height = rect.height || 100;
242
+
243
+ const minimalSvg = `<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}" viewBox="0 0 ${width} ${height}">
244
+ <rect width="100%" height="100%" fill="#f0f0f0" stroke="#ccc" stroke-width="1"/>
245
+ <text x="50%" y="50%" text-anchor="middle" dy="0.3em" font-family="Arial" font-size="12" fill="#666">SVG</text>
246
+ </svg>`;
247
+
248
+ const base64 = btoa(unescape(encodeURIComponent(minimalSvg)));
249
+ return `data:image/svg+xml;base64,${base64}`;
250
+ };
251
+
252
+ /**
253
+ * Huggingface环境专用html2canvas渲染
254
+ * @param element 要渲染的元素
255
+ * @returns Promise<string> Base64数据URL
256
+ */
257
+ export const huggingfaceHtml2Canvas = async (element: HTMLElement): Promise<string> => {
258
+ console.log('huggingfaceHtml2Canvas: Starting rendering for Huggingface environment');
259
+
260
+ const config = getHuggingfaceConfig();
261
+
262
+ try {
263
+ // 动态导入html2canvas
264
+ const html2canvas = (await import('html2canvas')).default;
265
+
266
+ const canvas = await html2canvas(element, config);
267
+
268
+ if (canvas.width === 0 || canvas.height === 0) {
269
+ throw new Error('Generated canvas has zero dimensions');
270
+ }
271
+
272
+ const dataUrl = canvas.toDataURL('image/png', 0.9);
273
+ console.log('huggingfaceHtml2Canvas: Rendering completed successfully');
274
+
275
+ return dataUrl;
276
+ } catch (error) {
277
+ console.error('huggingfaceHtml2Canvas failed:', error);
278
+
279
+ // 备选方案:使用html-to-image
280
+ try {
281
+ console.log('huggingfaceHtml2Canvas: Trying html-to-image fallback');
282
+ const { toPng } = await import('html-to-image');
283
+
284
+ const dataUrl = await toPng(element, {
285
+ quality: 0.9,
286
+ pixelRatio: 1,
287
+ backgroundColor: '#ffffff'
288
+ });
289
+
290
+ console.log('huggingfaceHtml2Canvas: html-to-image fallback succeeded');
291
+ return dataUrl;
292
+ } catch (fallbackError) {
293
+ console.error('huggingfaceHtml2Canvas: All methods failed:', fallbackError);
294
+ throw new Error(`Huggingface rendering failed: ${error}`);
295
+ }
296
+ }
297
+ };
298
+
299
+ /**
300
+ * 检测并应用Huggingface环境修复
301
+ * @param element 要处理的元素
302
+ * @returns Promise<string> Base64数据URL
303
+ */
304
+ export const renderWithHuggingfaceFix = async (element: HTMLElement): Promise<string> => {
305
+ const isHF = isHuggingfaceEnvironment();
306
+
307
+ console.log('renderWithHuggingfaceFix:', {
308
+ isHuggingfaceEnvironment: isHF,
309
+ elementType: element.tagName,
310
+ hostname: typeof window !== 'undefined' ? window.location.hostname : 'unknown'
311
+ });
312
+
313
+ // 检查是否包含SVG元素
314
+ const hasSvg = element.tagName.toLowerCase() === 'svg' || element.querySelector('svg');
315
+
316
+ if (hasSvg && isHF) {
317
+ // 对于包含SVG的元素,在Huggingface环境下使用专用方法
318
+ const svgElement = element.tagName.toLowerCase() === 'svg' ? element : element.querySelector('svg');
319
+ if (svgElement) {
320
+ return await huggingfaceSvg2Base64(svgElement);
321
+ }
322
+ }
323
+
324
+ // 使用Huggingface优化的html2canvas
325
+ return await huggingfaceHtml2Canvas(element);
326
+ };
327
+
328
+ export { isHuggingfaceEnvironment, getHuggingfaceConfig };