// svg转base64图片,参考:https://github.com/scriptex/svg64 const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=' const PREFIX = 'data:image/svg+xml;base64,' const utf8Encode = (string: string) => { string = string.replace(/\r\n/g, '\n') let utftext = '' for (let n = 0; n < string.length; n++) { const c = string.charCodeAt(n) if (c < 128) { utftext += String.fromCharCode(c) } else if (c > 127 && c < 2048) { utftext += String.fromCharCode((c >> 6) | 192) utftext += String.fromCharCode((c & 63) | 128) } else { utftext += String.fromCharCode((c >> 12) | 224) utftext += String.fromCharCode(((c >> 6) & 63) | 128) utftext += String.fromCharCode((c & 63) | 128) } } return utftext } const encode = (input: string) => { let output = '' let chr1, chr2, chr3, enc1, enc2, enc3, enc4 let i = 0 input = utf8Encode(input) while (i < input.length) { chr1 = input.charCodeAt(i++) chr2 = input.charCodeAt(i++) chr3 = input.charCodeAt(i++) enc1 = chr1 >> 2 enc2 = ((chr1 & 3) << 4) | (chr2 >> 4) enc3 = ((chr2 & 15) << 2) | (chr3 >> 6) enc4 = chr3 & 63 if (isNaN(chr2)) enc3 = enc4 = 64 else if (isNaN(chr3)) enc4 = 64 output = output + characters.charAt(enc1) + characters.charAt(enc2) + characters.charAt(enc3) + characters.charAt(enc4) } return output } // 生产环境检测和优化 const isProductionEnvironment = (): boolean => { return process.env.NODE_ENV === 'production'; }; const isHuggingfaceEnvironment = (): boolean => { return ( typeof window !== 'undefined' && (window.location.hostname.includes('hf.space') || window.location.hostname.includes('huggingface.co') || isProductionEnvironment()) ); }; // 生产环境性能监控 class SVGRenderPerformanceMonitor { private static instance: SVGRenderPerformanceMonitor; private metrics: Map = new Map(); static getInstance(): SVGRenderPerformanceMonitor { if (!this.instance) { this.instance = new SVGRenderPerformanceMonitor(); } return this.instance; } recordMetric(operation: string, duration: number): void { if (!isProductionEnvironment()) return; if (!this.metrics.has(operation)) { this.metrics.set(operation, []); } const metrics = this.metrics.get(operation)!; metrics.push(duration); // 保持最近100次记录 if (metrics.length > 100) { metrics.shift(); } } getAverageTime(operation: string): number { const metrics = this.metrics.get(operation); if (!metrics || metrics.length === 0) return 0; return metrics.reduce((sum, time) => sum + time, 0) / metrics.length; } } export const svg2Base64 = (element: Element) => { const startTime = performance.now(); const monitor = SVGRenderPerformanceMonitor.getInstance(); const isHF = isHuggingfaceEnvironment(); const isProd = isProductionEnvironment(); try { // 生产环境减少日志输出 if (!isProd) { console.log('svg2Base64: Starting conversion for element:', { tagName: element.tagName, className: element.className, id: element.id, hasChildren: element.children.length > 0, isHuggingfaceEnvironment: isHF }); } // Huggingface环境使用简化的处理方式 if (isHF) { console.log('svg2Base64: Using Huggingface optimized processing'); return huggingfaceOptimizedSvg2Base64(element); } // 克隆元素以避免修改原始DOM const clonedElement = element.cloneNode(true) as Element; console.log('svg2Base64: Element cloned successfully'); // 确保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'); // 确保SVG有明确的尺寸 const rect = element.getBoundingClientRect(); if (!svgElement.getAttribute('width') && rect.width > 0) { svgElement.setAttribute('width', rect.width.toString()); } if (!svgElement.getAttribute('height') && rect.height > 0) { svgElement.setAttribute('height', rect.height.toString()); } // 确保viewBox存在 if (!svgElement.getAttribute('viewBox')) { const width = parseFloat(svgElement.getAttribute('width') || '0'); const height = parseFloat(svgElement.getAttribute('height') || '0'); if (width > 0 && height > 0) { svgElement.setAttribute('viewBox', `0 0 ${width} ${height}`); } } console.log('svg2Base64: Added SVG namespaces and dimensions'); } // 处理内联样式 - 将计算样式应用到元素 const applyComputedStyles = (elem: Element, originalElem: Element) => { if (elem.nodeType === Node.ELEMENT_NODE) { const computedStyles = window.getComputedStyle(originalElem as HTMLElement); const styleProps = ['fill', 'stroke', 'stroke-width', 'opacity', 'font-family', 'font-size', 'font-weight']; styleProps.forEach(prop => { const value = computedStyles.getPropertyValue(prop); if (value && value !== 'none' && !elem.getAttribute(prop)) { elem.setAttribute(prop, value); } }); // 递归处理子元素 for (let i = 0; i < elem.children.length; i++) { const child = elem.children[i]; const originalChild = (originalElem as HTMLElement).children[i]; if (originalChild) { applyComputedStyles(child, originalChild); } } } }; applyComputedStyles(clonedElement, element); // 检查元素尺寸 - 使用更宽松的检查逻辑 const rect = element.getBoundingClientRect(); const computedStyle = window.getComputedStyle(element as HTMLElement); const hasValidDimensions = ( rect.width > 0 || rect.height > 0 || parseFloat(computedStyle.width) > 0 || parseFloat(computedStyle.height) > 0 || (element as HTMLElement).offsetWidth > 0 || (element as HTMLElement).offsetHeight > 0 ); console.log('svg2Base64: Element dimensions:', { boundingRect: { width: rect.width, height: rect.height }, computedStyle: { width: computedStyle.width, height: computedStyle.height }, offset: { width: (element as HTMLElement).offsetWidth, height: (element as HTMLElement).offsetHeight }, hasValidDimensions }); if (!hasValidDimensions) { console.warn('svg2Base64: Element has no valid dimensions, but continuing with serialization'); } const XMLS = new XMLSerializer(); let svg = XMLS.serializeToString(clonedElement); // 清理和优化SVG字符串 svg = svg.replace(/vector-effect="[^"]*"/g, ''); // 移除vector-effect属性 svg = svg.replace(/xmlns="[^"]*"/g, ''); // 移除重复的xmlns // 确保SVG标签包含正确的命名空间 if (svg.includes(' 50, hasNamespace: svg.includes('xmlns="http://www.w3.org/2000/svg"') }); if (!svg || svg.length === 0) { throw new Error('SVG serialization returned empty string'); } if (svg.length < 20) { throw new Error('SVG serialization returned suspiciously short string'); } const encoded = encode(svg); if (!encoded) { throw new Error('Base64 encoding failed'); } const result = PREFIX + encoded; console.log('svg2Base64: Encoding successful, result length:', result.length); // 验证结果 if (result.length < 100) { throw new Error('Base64 result is suspiciously short'); } // 记录成功的性能指标 const duration = performance.now() - startTime; monitor.recordMetric('svg2base64_success', duration); if (!isProd) { console.log(`svg2Base64: Conversion completed in ${duration.toFixed(2)}ms`); } return result; } catch (error) { // 记录失败的性能指标 const duration = performance.now() - startTime; monitor.recordMetric('svg2base64_error', duration); if (!isProd) { console.error('svg2Base64: Conversion failed:', error); } else { // 生产环境只记录关键错误信息 console.error('svg2Base64: Conversion failed'); } // 尝试多种备选方案 const fallbackStrategies = [ // 策略1: 简化的序列化 () => { console.log('svg2Base64: Attempting simplified serialization'); const rect = element.getBoundingClientRect(); const width = rect.width || 100; const height = rect.height || 100; const simplifiedSvg = `${element.innerHTML}`; const base64 = encode(utf8Encode(simplifiedSvg)); return `data:image/svg+xml;base64,${base64}`; }, // 策略2: 使用outerHTML () => { console.log('svg2Base64: Attempting outerHTML serialization'); let svgString = (element as HTMLElement).outerHTML; if (!svgString.includes('xmlns')) { svgString = svgString.replace(' { console.log('svg2Base64: Attempting minimal SVG creation'); const rect = element.getBoundingClientRect(); const minimalSvg = ``; const base64 = encode(utf8Encode(minimalSvg)); return `data:image/svg+xml;base64,${base64}`; } ]; for (let i = 0; i < fallbackStrategies.length; i++) { try { const result = fallbackStrategies[i](); console.log(`svg2Base64: Fallback strategy ${i + 1} succeeded`); return result; } catch (fallbackError) { console.warn(`svg2Base64: Fallback strategy ${i + 1} failed:`, fallbackError); } } console.error('svg2Base64: All fallback strategies failed'); throw new Error(`SVG to Base64 conversion failed: ${error}`); } } /** * Huggingface环境优化的SVG转Base64函数 * 使用更简单、更兼容的方法 */ const huggingfaceOptimizedSvg2Base64 = (element: Element): string => { try { console.log('huggingfaceOptimizedSvg2Base64: Starting optimized conversion'); // 克隆元素 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'); // 获取尺寸 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 removeProblematicAttrs = (elem: Element) => { const problematicAttrs = ['vector-effect', 'xmlns:xlink']; problematicAttrs.forEach(attr => { if (elem.hasAttribute(attr)) { elem.removeAttribute(attr); } }); // 递归处理子元素 Array.from(elem.children).forEach(child => { removeProblematicAttrs(child); }); }; removeProblematicAttrs(svgElement); // 简化序列化 let svgString = svgElement.outerHTML; // 基本清理 svgString = svgString.replace(/vector-effect="[^"]*"/g, ''); svgString = svgString.replace(/xmlns:xlink="[^"]*"/g, ''); // 确保命名空间正确 if (!svgString.includes('xmlns="http://www.w3.org/2000/svg"')) { svgString = svgString.replace('
${element.innerHTML}
`; const base64 = btoa(unescape(encodeURIComponent(fallbackSvg))); return PREFIX + base64; } catch (error) { console.error('huggingfaceOptimizedSvg2Base64: Conversion failed:', error); // 最终备选方案:创建简单的占位符SVG const rect = element.getBoundingClientRect(); const width = rect.width || 100; const height = rect.height || 100; const placeholderSvg = ` SVG `; const base64 = btoa(unescape(encodeURIComponent(placeholderSvg))); console.log('huggingfaceOptimizedSvg2Base64: Using placeholder SVG'); return PREFIX + base64; } }