|
|
|
|
|
|
|
|
|
|
|
|
|
const getHtml2Canvas = async () => { |
|
const { default: html2canvas } = await import('html2canvas'); |
|
return html2canvas; |
|
}; |
|
|
|
|
|
interface RenderOptions { |
|
scale?: number; |
|
backgroundColor?: string | null; |
|
useCORS?: boolean; |
|
timeout?: number; |
|
format?: 'png' | 'jpeg' | 'webp'; |
|
quality?: number; |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
export async function renderElementToCanvas( |
|
element: HTMLElement, |
|
options: RenderOptions = {} |
|
): Promise<HTMLCanvasElement> { |
|
const { |
|
scale = 2, |
|
backgroundColor = null, |
|
useCORS = true, |
|
timeout = 15000 |
|
} = options; |
|
|
|
try { |
|
|
|
const html2canvas = (await import('html2canvas')).default; |
|
|
|
|
|
const rect = element.getBoundingClientRect(); |
|
const computedStyle = window.getComputedStyle(element); |
|
|
|
const renderWidth = Math.max( |
|
rect.width, |
|
element.offsetWidth, |
|
parseFloat(computedStyle.width) || 0 |
|
); |
|
const renderHeight = Math.max( |
|
rect.height, |
|
element.offsetHeight, |
|
parseFloat(computedStyle.height) || 0 |
|
); |
|
|
|
console.log('Canvas rendering dimensions:', { |
|
boundingRect: { width: rect.width, height: rect.height }, |
|
offset: { width: element.offsetWidth, height: element.offsetHeight }, |
|
computed: { width: computedStyle.width, height: computedStyle.height }, |
|
final: { width: renderWidth, height: renderHeight } |
|
}); |
|
|
|
const canvas = await html2canvas(element, { |
|
scale, |
|
backgroundColor, |
|
useCORS, |
|
allowTaint: false, |
|
foreignObjectRendering: true, |
|
logging: false, |
|
width: renderWidth || 100, |
|
height: renderHeight || 100, |
|
windowWidth: window.innerWidth, |
|
windowHeight: window.innerHeight, |
|
scrollX: 0, |
|
scrollY: 0, |
|
onclone: (clonedDoc, clonedElement) => { |
|
|
|
const targetElement = clonedElement || clonedDoc.querySelector(`[data-element-id="${element.getAttribute('data-element-id')}"]`); |
|
if (targetElement) { |
|
|
|
const svgElements = targetElement.querySelectorAll('svg'); |
|
svgElements.forEach(svg => { |
|
|
|
if (!svg.getAttribute('width') || !svg.getAttribute('height')) { |
|
const svgRect = svg.getBoundingClientRect(); |
|
if (svgRect.width > 0 && svgRect.height > 0) { |
|
svg.setAttribute('width', svgRect.width.toString()); |
|
svg.setAttribute('height', svgRect.height.toString()); |
|
} else { |
|
svg.setAttribute('width', renderWidth.toString()); |
|
svg.setAttribute('height', renderHeight.toString()); |
|
} |
|
} |
|
|
|
|
|
if (!svg.getAttribute('viewBox')) { |
|
const width = svg.getAttribute('width') || renderWidth; |
|
const height = svg.getAttribute('height') || renderHeight; |
|
svg.setAttribute('viewBox', `0 0 ${width} ${height}`); |
|
} |
|
|
|
|
|
if (!svg.getAttribute('xmlns')) { |
|
svg.setAttribute('xmlns', 'http://www.w3.org/2000/svg'); |
|
} |
|
}); |
|
|
|
|
|
const style = targetElement.style; |
|
style.visibility = 'visible'; |
|
style.opacity = '1'; |
|
style.display = style.display === 'none' ? 'block' : style.display; |
|
} |
|
} |
|
}); |
|
|
|
|
|
if (canvas.width === 0 || canvas.height === 0) { |
|
throw new Error('Generated canvas has zero dimensions'); |
|
} |
|
|
|
return canvas; |
|
} catch (error) { |
|
console.error('Canvas rendering failed:', error); |
|
throw new Error(`Canvas rendering failed: ${error}`); |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
export async function renderSVGToCanvas( |
|
svgElement: SVGElement, |
|
options: RenderOptions = {} |
|
): Promise<HTMLCanvasElement> { |
|
const { |
|
scale = 1, |
|
backgroundColor = null, |
|
timeout = 10000 |
|
} = options; |
|
|
|
return new Promise((resolve, reject) => { |
|
const timeoutId = setTimeout(() => { |
|
reject(new Error('SVG Canvas rendering timeout')); |
|
}, timeout); |
|
|
|
try { |
|
console.log('renderSVGToCanvas: Starting SVG to Canvas conversion'); |
|
|
|
|
|
const rect = svgElement.getBoundingClientRect(); |
|
let width = rect.width || svgElement.clientWidth; |
|
let height = rect.height || svgElement.clientHeight; |
|
|
|
|
|
if (width <= 0 || height <= 0) { |
|
const svgWidth = svgElement.getAttribute('width'); |
|
const svgHeight = svgElement.getAttribute('height'); |
|
if (svgWidth && svgHeight) { |
|
width = parseFloat(svgWidth); |
|
height = parseFloat(svgHeight); |
|
} |
|
} |
|
|
|
|
|
if (width <= 0 || height <= 0) { |
|
width = 300; |
|
height = 200; |
|
console.warn('renderSVGToCanvas: Using default dimensions due to zero size'); |
|
} |
|
|
|
console.log('renderSVGToCanvas: SVG dimensions:', { width, height }); |
|
|
|
|
|
const canvas = document.createElement('canvas'); |
|
const ctx = canvas.getContext('2d'); |
|
|
|
if (!ctx) { |
|
clearTimeout(timeoutId); |
|
reject(new Error('Failed to get 2D context')); |
|
return; |
|
} |
|
|
|
canvas.width = width * scale; |
|
canvas.height = height * scale; |
|
ctx.scale(scale, scale); |
|
|
|
|
|
if (backgroundColor) { |
|
ctx.fillStyle = backgroundColor; |
|
ctx.fillRect(0, 0, width, height); |
|
} |
|
|
|
|
|
const clonedSVG = svgElement.cloneNode(true) as SVGElement; |
|
|
|
|
|
clonedSVG.setAttribute('xmlns', 'http://www.w3.org/2000/svg'); |
|
clonedSVG.setAttribute('xmlns:xlink', 'http://www.w3.org/1999/xlink'); |
|
|
|
|
|
if (!clonedSVG.getAttribute('width')) { |
|
clonedSVG.setAttribute('width', width.toString()); |
|
} |
|
if (!clonedSVG.getAttribute('height')) { |
|
clonedSVG.setAttribute('height', height.toString()); |
|
} |
|
|
|
|
|
if (!clonedSVG.getAttribute('viewBox')) { |
|
clonedSVG.setAttribute('viewBox', `0 0 ${width} ${height}`); |
|
} |
|
|
|
|
|
const problematicAttrs = ['vector-effect']; |
|
const removeProblematicAttrs = (element: Element) => { |
|
problematicAttrs.forEach(attr => { |
|
if (element.hasAttribute(attr)) { |
|
element.removeAttribute(attr); |
|
} |
|
}); |
|
|
|
|
|
Array.from(element.children).forEach(child => { |
|
removeProblematicAttrs(child); |
|
}); |
|
}; |
|
|
|
removeProblematicAttrs(clonedSVG); |
|
|
|
|
|
let svgData = new XMLSerializer().serializeToString(clonedSVG); |
|
|
|
|
|
svgData = svgData.replace(/vector-effect="[^"]*"/g, ''); |
|
|
|
|
|
if (!svgData.includes('xmlns="http://www.w3.org/2000/svg"')) { |
|
svgData = svgData.replace('<svg', '<svg xmlns="http://www.w3.org/2000/svg"'); |
|
} |
|
|
|
console.log('renderSVGToCanvas: SVG data prepared:', { |
|
length: svgData.length, |
|
hasNamespace: svgData.includes('xmlns="http://www.w3.org/2000/svg"'), |
|
preview: svgData.substring(0, 200) + '...' |
|
}); |
|
|
|
const svgBlob = new Blob([svgData], { type: 'image/svg+xml;charset=utf-8' }); |
|
const url = URL.createObjectURL(svgBlob); |
|
|
|
const img = new Image(); |
|
|
|
img.onload = () => { |
|
try { |
|
console.log('renderSVGToCanvas: Image loaded successfully, drawing to canvas'); |
|
ctx.drawImage(img, 0, 0, width, height); |
|
URL.revokeObjectURL(url); |
|
clearTimeout(timeoutId); |
|
|
|
|
|
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height); |
|
const hasContent = imageData.data.some((value, index) => { |
|
|
|
return index % 4 === 3 && value > 0; |
|
}); |
|
|
|
if (!hasContent) { |
|
console.warn('renderSVGToCanvas: Canvas appears to be empty after drawing'); |
|
} |
|
|
|
console.log('renderSVGToCanvas: Canvas rendering completed successfully'); |
|
resolve(canvas); |
|
} catch (error) { |
|
URL.revokeObjectURL(url); |
|
clearTimeout(timeoutId); |
|
reject(new Error(`Failed to draw SVG to Canvas: ${error}`)); |
|
} |
|
}; |
|
|
|
img.onerror = (error) => { |
|
console.error('renderSVGToCanvas: Image loading failed:', error); |
|
URL.revokeObjectURL(url); |
|
clearTimeout(timeoutId); |
|
reject(new Error('SVG image loading failed')); |
|
}; |
|
|
|
|
|
img.src = url; |
|
|
|
} catch (error) { |
|
console.error('renderSVGToCanvas: SVG serialization failed:', error); |
|
clearTimeout(timeoutId); |
|
reject(new Error(`SVG serialization failed: ${error}`)); |
|
} |
|
}); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
export function canvasToBase64( |
|
canvas: HTMLCanvasElement, |
|
options: RenderOptions = {} |
|
): string { |
|
const { format = 'png', quality = 0.95 } = options; |
|
|
|
try { |
|
if (format === 'jpeg') { |
|
return canvas.toDataURL('image/jpeg', quality); |
|
} else if (format === 'webp') { |
|
return canvas.toDataURL('image/webp', quality); |
|
} else { |
|
return canvas.toDataURL('image/png'); |
|
} |
|
} catch (error) { |
|
throw new Error(`Canvas to Base64 conversion failed: ${error}`); |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
export async function renderElementToBase64( |
|
element: HTMLElement, |
|
options: RenderOptions = {} |
|
): Promise<string> { |
|
try { |
|
console.log('Starting element to Base64 conversion:', { |
|
element: element.tagName, |
|
className: element.className, |
|
options |
|
}); |
|
|
|
const canvas = await renderElementToCanvas(element, options); |
|
const base64 = canvasToBase64(canvas, options); |
|
|
|
console.log('Element to Base64 conversion completed:', { |
|
canvasSize: `${canvas.width}x${canvas.height}`, |
|
base64Length: base64.length, |
|
preview: base64.substring(0, 100) + '...' |
|
}); |
|
|
|
return base64; |
|
} catch (error) { |
|
console.error('Element to Base64 conversion failed:', error); |
|
throw new Error(`Element rendering failed: ${error}`); |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
|
|
export function isCanvasRenderSupported(): boolean { |
|
try { |
|
const canvas = document.createElement('canvas'); |
|
const ctx = canvas.getContext('2d'); |
|
return !!(ctx && typeof ctx.drawImage === 'function' && typeof window !== 'undefined'); |
|
} catch { |
|
return false; |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
export function getElementDimensions(element: HTMLElement) { |
|
const rect = element.getBoundingClientRect(); |
|
const computedStyle = window.getComputedStyle(element); |
|
|
|
|
|
let svgDimensions = null; |
|
if (element.tagName.toLowerCase() === 'svg') { |
|
const svgElement = element as SVGSVGElement; |
|
const viewBox = svgElement.viewBox.baseVal; |
|
|
|
svgDimensions = { |
|
viewBoxWidth: viewBox.width || 0, |
|
viewBoxHeight: viewBox.height || 0, |
|
svgWidth: svgElement.width.baseVal.value || 0, |
|
svgHeight: svgElement.height.baseVal.value || 0 |
|
}; |
|
} |
|
|
|
|
|
const getValidDimension = (primary: number, ...fallbacks: number[]): number => { |
|
if (primary > 0) return primary; |
|
for (const fallback of fallbacks) { |
|
if (fallback > 0) return fallback; |
|
} |
|
return 0; |
|
}; |
|
|
|
const width = getValidDimension( |
|
rect.width, |
|
element.offsetWidth, |
|
parseFloat(computedStyle.width) || 0, |
|
svgDimensions?.svgWidth || 0, |
|
svgDimensions?.viewBoxWidth || 0 |
|
); |
|
|
|
const height = getValidDimension( |
|
rect.height, |
|
element.offsetHeight, |
|
parseFloat(computedStyle.height) || 0, |
|
svgDimensions?.svgHeight || 0, |
|
svgDimensions?.viewBoxHeight || 0 |
|
); |
|
|
|
return { |
|
width, |
|
height, |
|
clientWidth: element.clientWidth, |
|
clientHeight: element.clientHeight, |
|
offsetWidth: element.offsetWidth, |
|
offsetHeight: element.offsetHeight, |
|
boundingRect: { |
|
width: rect.width, |
|
height: rect.height, |
|
top: rect.top, |
|
left: rect.left |
|
}, |
|
svgDimensions, |
|
isValid: width > 0 && height > 0 |
|
}; |
|
} |