File size: 10,717 Bytes
66b71dc
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
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
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
/**
 * Huggingface环境专用渲染器
 * 解决Huggingface Spaces部署环境下的矢量图形渲染问题
 */

// 检测是否在Huggingface环境中
const isHuggingfaceEnvironment = (): boolean => {
  return (
    typeof window !== 'undefined' &&
    (window.location.hostname.includes('hf.space') ||
     window.location.hostname.includes('huggingface.co') ||
     process.env.NODE_ENV === 'production')
  );
};

// Huggingface环境专用配置
interface HuggingfaceRenderConfig {
  useCORS: boolean;
  allowTaint: boolean;
  foreignObjectRendering: boolean;
  scale: number;
  logging: boolean;
  timeout: number;
  backgroundColor: string | null;
  removeContainer: boolean;
  imageTimeout: number;
  onclone?: (clonedDoc: Document, element: HTMLElement) => void;
}

// 获取Huggingface环境优化配置
const getHuggingfaceConfig = (): HuggingfaceRenderConfig => {
  const isHF = isHuggingfaceEnvironment();
  
  return {
    useCORS: false, // Huggingface环境下禁用CORS
    allowTaint: true, // 允许污染画布
    foreignObjectRendering: false, // 禁用foreignObject渲染
    scale: isHF ? 1 : 2, // Huggingface环境使用较低缩放
    logging: false, // 生产环境禁用日志
    timeout: 30000, // 增加超时时间
    backgroundColor: null,
    removeContainer: true,
    imageTimeout: 15000,
    onclone: (clonedDoc: Document, element: HTMLElement) => {
      // Huggingface环境特殊处理
      if (isHF) {
        // 移除可能导致CORS问题的外部资源
        const externalImages = clonedDoc.querySelectorAll('img[src^="http"]');
        externalImages.forEach(img => {
          const imgElement = img as HTMLImageElement;
          // 替换为占位符或移除
          imgElement.style.display = 'none';
        });
        
        // 处理SVG元素
        const svgElements = clonedDoc.querySelectorAll('svg');
        svgElements.forEach(svg => {
          // 确保SVG有正确的命名空间
          svg.setAttribute('xmlns', 'http://www.w3.org/2000/svg');
          
          // 移除可能导致问题的属性
          svg.removeAttribute('vector-effect');
          
          // 确保尺寸明确
          const rect = svg.getBoundingClientRect();
          if (!svg.getAttribute('width') && rect.width > 0) {
            svg.setAttribute('width', rect.width.toString());
          }
          if (!svg.getAttribute('height') && rect.height > 0) {
            svg.setAttribute('height', rect.height.toString());
          }
          
          // 设置viewBox
          if (!svg.getAttribute('viewBox')) {
            const width = svg.getAttribute('width') || '100';
            const height = svg.getAttribute('height') || '100';
            svg.setAttribute('viewBox', `0 0 ${width} ${height}`);
          }
        });
        
        // 内联所有样式
        const allElements = clonedDoc.querySelectorAll('*');
        allElements.forEach(el => {
          const element = el as HTMLElement;
          const computedStyle = window.getComputedStyle(element);
          
          // 关键样式属性
          const importantStyles = [
            'color', 'background-color', 'font-family', 'font-size', 
            'font-weight', 'fill', 'stroke', 'stroke-width', 'opacity'
          ];
          
          importantStyles.forEach(prop => {
            const value = computedStyle.getPropertyValue(prop);
            if (value && value !== 'initial' && value !== 'inherit') {
              element.style.setProperty(prop, value, 'important');
            }
          });
        });
      }
    }
  };
};

/**
 * Huggingface环境专用SVG转Base64
 * @param element SVG元素
 * @returns Promise<string> Base64数据URL
 */
export const huggingfaceSvg2Base64 = async (element: Element): Promise<string> => {
  console.log('huggingfaceSvg2Base64: Starting conversion for Huggingface environment');
  
  try {
    // 方法1: 直接序列化SVG(最兼容)
    const directResult = await directSvgSerialization(element);
    if (directResult) {
      console.log('huggingfaceSvg2Base64: Direct serialization succeeded');
      return directResult;
    }
  } catch (error) {
    console.warn('huggingfaceSvg2Base64: Direct serialization failed:', error);
  }
  
  try {
    // 方法2: Canvas渲染(备选)
    const canvasResult = await canvasSvgRendering(element);
    if (canvasResult) {
      console.log('huggingfaceSvg2Base64: Canvas rendering succeeded');
      return canvasResult;
    }
  } catch (error) {
    console.warn('huggingfaceSvg2Base64: Canvas rendering failed:', error);
  }
  
  // 方法3: 最小化SVG(最后备选)
  console.log('huggingfaceSvg2Base64: Using minimal SVG fallback');
  return createMinimalSvg(element);
};

/**
 * 直接SVG序列化方法
 */
const directSvgSerialization = async (element: Element): Promise<string | null> => {
  try {
    const clonedElement = element.cloneNode(true) as Element;
    
    // 确保是SVG元素
    if (clonedElement.tagName.toLowerCase() === 'svg') {
      const svgElement = clonedElement as SVGElement;
      
      // 设置必要属性
      svgElement.setAttribute('xmlns', 'http://www.w3.org/2000/svg');
      svgElement.setAttribute('xmlns:xlink', 'http://www.w3.org/1999/xlink');
      
      // 获取尺寸
      const rect = element.getBoundingClientRect();
      const width = rect.width || 100;
      const height = rect.height || 100;
      
      svgElement.setAttribute('width', width.toString());
      svgElement.setAttribute('height', height.toString());
      svgElement.setAttribute('viewBox', `0 0 ${width} ${height}`);
      
      // 序列化
      const serializer = new XMLSerializer();
      let svgString = serializer.serializeToString(svgElement);
      
      // 清理
      svgString = svgString.replace(/vector-effect="[^"]*"/g, '');
      
      // 编码
      const base64 = btoa(unescape(encodeURIComponent(svgString)));
      return `data:image/svg+xml;base64,${base64}`;
    }
    
    return null;
  } catch (error) {
    console.error('directSvgSerialization failed:', error);
    return null;
  }
};

/**
 * Canvas SVG渲染方法
 */
const canvasSvgRendering = async (element: Element): Promise<string | null> => {
  return new Promise((resolve) => {
    try {
      const rect = element.getBoundingClientRect();
      const width = rect.width || 100;
      const height = rect.height || 100;
      
      const canvas = document.createElement('canvas');
      const ctx = canvas.getContext('2d');
      
      if (!ctx) {
        resolve(null);
        return;
      }
      
      canvas.width = width;
      canvas.height = height;
      
      // 创建SVG数据URL
      const svgData = new XMLSerializer().serializeToString(element);
      const svgBlob = new Blob([svgData], { type: 'image/svg+xml;charset=utf-8' });
      const url = URL.createObjectURL(svgBlob);
      
      const img = new Image();
      img.onload = () => {
        ctx.drawImage(img, 0, 0, width, height);
        URL.revokeObjectURL(url);
        resolve(canvas.toDataURL('image/png'));
      };
      
      img.onerror = () => {
        URL.revokeObjectURL(url);
        resolve(null);
      };
      
      // 设置超时
      setTimeout(() => {
        URL.revokeObjectURL(url);
        resolve(null);
      }, 5000);
      
      img.src = url;
    } catch (error) {
      console.error('canvasSvgRendering failed:', error);
      resolve(null);
    }
  });
};

/**
 * 创建最小化SVG
 */
const createMinimalSvg = (element: Element): string => {
  const rect = element.getBoundingClientRect();
  const width = rect.width || 100;
  const height = rect.height || 100;
  
  const minimalSvg = `<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}" viewBox="0 0 ${width} ${height}">
    <rect width="100%" height="100%" fill="#f0f0f0" stroke="#ccc" stroke-width="1"/>
    <text x="50%" y="50%" text-anchor="middle" dy="0.3em" font-family="Arial" font-size="12" fill="#666">SVG</text>
  </svg>`;
  
  const base64 = btoa(unescape(encodeURIComponent(minimalSvg)));
  return `data:image/svg+xml;base64,${base64}`;
};

/**
 * Huggingface环境专用html2canvas渲染
 * @param element 要渲染的元素
 * @returns Promise<string> Base64数据URL
 */
export const huggingfaceHtml2Canvas = async (element: HTMLElement): Promise<string> => {
  console.log('huggingfaceHtml2Canvas: Starting rendering for Huggingface environment');
  
  const config = getHuggingfaceConfig();
  
  try {
    // 动态导入html2canvas
    const html2canvas = (await import('html2canvas')).default;
    
    const canvas = await html2canvas(element, config);
    
    if (canvas.width === 0 || canvas.height === 0) {
      throw new Error('Generated canvas has zero dimensions');
    }
    
    const dataUrl = canvas.toDataURL('image/png', 0.9);
    console.log('huggingfaceHtml2Canvas: Rendering completed successfully');
    
    return dataUrl;
  } catch (error) {
    console.error('huggingfaceHtml2Canvas failed:', error);
    
    // 备选方案:使用html-to-image
    try {
      console.log('huggingfaceHtml2Canvas: Trying html-to-image fallback');
      const { toPng } = await import('html-to-image');
      
      const dataUrl = await toPng(element, {
        quality: 0.9,
        pixelRatio: 1,
        backgroundColor: '#ffffff'
      });
      
      console.log('huggingfaceHtml2Canvas: html-to-image fallback succeeded');
      return dataUrl;
    } catch (fallbackError) {
      console.error('huggingfaceHtml2Canvas: All methods failed:', fallbackError);
      throw new Error(`Huggingface rendering failed: ${error}`);
    }
  }
};

/**
 * 检测并应用Huggingface环境修复
 * @param element 要处理的元素
 * @returns Promise<string> Base64数据URL
 */
export const renderWithHuggingfaceFix = async (element: HTMLElement): Promise<string> => {
  const isHF = isHuggingfaceEnvironment();
  
  console.log('renderWithHuggingfaceFix:', {
    isHuggingfaceEnvironment: isHF,
    elementType: element.tagName,
    hostname: typeof window !== 'undefined' ? window.location.hostname : 'unknown'
  });
  
  // 检查是否包含SVG元素
  const hasSvg = element.tagName.toLowerCase() === 'svg' || element.querySelector('svg');
  
  if (hasSvg && isHF) {
    // 对于包含SVG的元素,在Huggingface环境下使用专用方法
    const svgElement = element.tagName.toLowerCase() === 'svg' ? element : element.querySelector('svg');
    if (svgElement) {
      return await huggingfaceSvg2Base64(svgElement);
    }
  }
  
  // 使用Huggingface优化的html2canvas
  return await huggingfaceHtml2Canvas(element);
};

export { isHuggingfaceEnvironment, getHuggingfaceConfig };