/** * 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 Base64数据URL */ export const huggingfaceSvg2Base64 = async (element: Element): Promise => { 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 => { 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 => { 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 `; const base64 = btoa(unescape(encodeURIComponent(minimalSvg))); return `data:image/svg+xml;base64,${base64}`; }; /** * Huggingface环境专用html2canvas渲染 * @param element 要渲染的元素 * @returns Promise Base64数据URL */ export const huggingfaceHtml2Canvas = async (element: HTMLElement): Promise => { 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 Base64数据URL */ export const renderWithHuggingfaceFix = async (element: HTMLElement): Promise => { 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 };