|
|
|
|
|
|
|
|
|
|
|
|
|
const isHuggingfaceEnvironment = (): boolean => { |
|
return ( |
|
typeof window !== 'undefined' && |
|
(window.location.hostname.includes('hf.space') || |
|
window.location.hostname.includes('huggingface.co') || |
|
process.env.NODE_ENV === 'production') |
|
); |
|
}; |
|
|
|
|
|
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; |
|
} |
|
|
|
|
|
const getHuggingfaceConfig = (): HuggingfaceRenderConfig => { |
|
const isHF = isHuggingfaceEnvironment(); |
|
|
|
return { |
|
useCORS: false, |
|
allowTaint: true, |
|
foreignObjectRendering: false, |
|
scale: isHF ? 1 : 2, |
|
logging: false, |
|
timeout: 30000, |
|
backgroundColor: null, |
|
removeContainer: true, |
|
imageTimeout: 15000, |
|
onclone: (clonedDoc: Document, element: HTMLElement) => { |
|
|
|
if (isHF) { |
|
|
|
const externalImages = clonedDoc.querySelectorAll('img[src^="http"]'); |
|
externalImages.forEach(img => { |
|
const imgElement = img as HTMLImageElement; |
|
|
|
imgElement.style.display = 'none'; |
|
}); |
|
|
|
|
|
const svgElements = clonedDoc.querySelectorAll('svg'); |
|
svgElements.forEach(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()); |
|
} |
|
|
|
|
|
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'); |
|
} |
|
}); |
|
}); |
|
} |
|
} |
|
}; |
|
}; |
|
|
|
|
|
|
|
|
|
|
|
|
|
export const huggingfaceSvg2Base64 = async (element: Element): Promise<string> => { |
|
console.log('huggingfaceSvg2Base64: Starting conversion for Huggingface environment'); |
|
|
|
try { |
|
|
|
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 { |
|
|
|
const canvasResult = await canvasSvgRendering(element); |
|
if (canvasResult) { |
|
console.log('huggingfaceSvg2Base64: Canvas rendering succeeded'); |
|
return canvasResult; |
|
} |
|
} catch (error) { |
|
console.warn('huggingfaceSvg2Base64: Canvas rendering failed:', error); |
|
} |
|
|
|
|
|
console.log('huggingfaceSvg2Base64: Using minimal SVG fallback'); |
|
return createMinimalSvg(element); |
|
}; |
|
|
|
|
|
|
|
|
|
const directSvgSerialization = async (element: Element): Promise<string | null> => { |
|
try { |
|
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'); |
|
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; |
|
} |
|
}; |
|
|
|
|
|
|
|
|
|
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; |
|
|
|
|
|
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); |
|
} |
|
}); |
|
}; |
|
|
|
|
|
|
|
|
|
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}`; |
|
}; |
|
|
|
|
|
|
|
|
|
|
|
|
|
export const huggingfaceHtml2Canvas = async (element: HTMLElement): Promise<string> => { |
|
console.log('huggingfaceHtml2Canvas: Starting rendering for Huggingface environment'); |
|
|
|
const config = getHuggingfaceConfig(); |
|
|
|
try { |
|
|
|
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); |
|
|
|
|
|
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}`); |
|
} |
|
} |
|
}; |
|
|
|
|
|
|
|
|
|
|
|
|
|
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' |
|
}); |
|
|
|
|
|
const hasSvg = element.tagName.toLowerCase() === 'svg' || element.querySelector('svg'); |
|
|
|
if (hasSvg && isHF) { |
|
|
|
const svgElement = element.tagName.toLowerCase() === 'svg' ? element : element.querySelector('svg'); |
|
if (svgElement) { |
|
return await huggingfaceSvg2Base64(svgElement); |
|
} |
|
} |
|
|
|
|
|
return await huggingfaceHtml2Canvas(element); |
|
}; |
|
|
|
export { isHuggingfaceEnvironment, getHuggingfaceConfig }; |