|
|
|
|
|
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<string, number[]> = 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); |
|
|
|
|
|
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 |
|
}); |
|
} |
|
|
|
|
|
if (isHF) { |
|
console.log('svg2Base64: Using Huggingface optimized processing'); |
|
return huggingfaceOptimizedSvg2Base64(element); |
|
} |
|
|
|
|
|
const clonedElement = element.cloneNode(true) as Element; |
|
console.log('svg2Base64: Element cloned successfully'); |
|
|
|
|
|
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(); |
|
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()); |
|
} |
|
|
|
|
|
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.replace(/vector-effect="[^"]*"/g, ''); |
|
svg = svg.replace(/xmlns="[^"]*"/g, ''); |
|
|
|
|
|
if (svg.includes('<svg') && !svg.includes('xmlns="http://www.w3.org/2000/svg"')) { |
|
svg = svg.replace('<svg', '<svg xmlns="http://www.w3.org/2000/svg"'); |
|
} |
|
|
|
|
|
if (svg.includes('xlink:') && !svg.includes('xmlns:xlink')) { |
|
svg = svg.replace('<svg', '<svg xmlns:xlink="http://www.w3.org/1999/xlink"'); |
|
} |
|
|
|
|
|
if (svg.includes('<svg') && !svg.includes('viewBox=') && !svg.includes('width=')) { |
|
const rect = element.getBoundingClientRect(); |
|
if (rect.width > 0 && rect.height > 0) { |
|
svg = svg.replace('<svg', `<svg width="${rect.width}" height="${rect.height}" viewBox="0 0 ${rect.width} ${rect.height}"`); |
|
} |
|
} |
|
|
|
console.log('svg2Base64: Serialization result:', { |
|
length: svg.length, |
|
preview: svg.substring(0, 200) + '...', |
|
containsSvgTag: svg.includes('<svg'), |
|
containsContent: svg.length > 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 = [ |
|
|
|
() => { |
|
console.log('svg2Base64: Attempting simplified serialization'); |
|
const rect = element.getBoundingClientRect(); |
|
const width = rect.width || 100; |
|
const height = rect.height || 100; |
|
const simplifiedSvg = `<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}" viewBox="0 0 ${width} ${height}">${element.innerHTML}</svg>`; |
|
const base64 = encode(utf8Encode(simplifiedSvg)); |
|
return `data:image/svg+xml;base64,${base64}`; |
|
}, |
|
|
|
|
|
() => { |
|
console.log('svg2Base64: Attempting outerHTML serialization'); |
|
let svgString = (element as HTMLElement).outerHTML; |
|
if (!svgString.includes('xmlns')) { |
|
svgString = svgString.replace('<svg', '<svg xmlns="http://www.w3.org/2000/svg"'); |
|
} |
|
const base64 = encode(utf8Encode(svgString)); |
|
return `data:image/svg+xml;base64,${base64}`; |
|
}, |
|
|
|
|
|
() => { |
|
console.log('svg2Base64: Attempting minimal SVG creation'); |
|
const rect = element.getBoundingClientRect(); |
|
const minimalSvg = `<svg xmlns="http://www.w3.org/2000/svg" width="${rect.width || 100}" height="${rect.height || 100}"><rect width="100%" height="100%" fill="#cccccc"/></svg>`; |
|
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}`); |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
|
|
const huggingfaceOptimizedSvg2Base64 = (element: Element): string => { |
|
try { |
|
console.log('huggingfaceOptimizedSvg2Base64: Starting optimized conversion'); |
|
|
|
|
|
const clonedElement = element.cloneNode(true) as Element; |
|
|
|
|
|
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('<svg', '<svg xmlns="http://www.w3.org/2000/svg"'); |
|
} |
|
|
|
console.log('huggingfaceOptimizedSvg2Base64: SVG string prepared:', { |
|
length: svgString.length, |
|
hasNamespace: svgString.includes('xmlns="http://www.w3.org/2000/svg"'), |
|
preview: svgString.substring(0, 150) + '...' |
|
}); |
|
|
|
|
|
const base64 = btoa(unescape(encodeURIComponent(svgString))); |
|
const result = PREFIX + base64; |
|
|
|
console.log('huggingfaceOptimizedSvg2Base64: Conversion successful, result length:', result.length); |
|
return result; |
|
} |
|
|
|
|
|
console.warn('huggingfaceOptimizedSvg2Base64: Element is not SVG, using fallback'); |
|
const rect = element.getBoundingClientRect(); |
|
const width = rect.width || 100; |
|
const height = rect.height || 100; |
|
|
|
const fallbackSvg = `<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}" viewBox="0 0 ${width} ${height}"> |
|
<foreignObject width="100%" height="100%"> |
|
<div xmlns="http://www.w3.org/1999/xhtml">${element.innerHTML}</div> |
|
</foreignObject> |
|
</svg>`; |
|
|
|
const base64 = btoa(unescape(encodeURIComponent(fallbackSvg))); |
|
return PREFIX + base64; |
|
|
|
} catch (error) { |
|
console.error('huggingfaceOptimizedSvg2Base64: Conversion failed:', error); |
|
|
|
|
|
const rect = element.getBoundingClientRect(); |
|
const width = rect.width || 100; |
|
const height = rect.height || 100; |
|
|
|
const placeholderSvg = `<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}" viewBox="0 0 ${width} ${height}"> |
|
<rect width="100%" height="100%" fill="#f5f5f5" stroke="#ddd" stroke-width="1"/> |
|
<text x="50%" y="50%" text-anchor="middle" dy="0.3em" font-family="Arial, sans-serif" font-size="14" fill="#999">SVG</text> |
|
</svg>`; |
|
|
|
const base64 = btoa(unescape(encodeURIComponent(placeholderSvg))); |
|
console.log('huggingfaceOptimizedSvg2Base64: Using placeholder SVG'); |
|
return PREFIX + base64; |
|
} |
|
} |