/** * Canvas渲染工具 * 用于将DOM元素渲染为Canvas并转换为Base64图像 */ // Dynamic import for html2canvas to resolve bundling issues 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; } /** * 将DOM元素渲染到Canvas * @param element 要渲染的DOM元素 * @param options 渲染选项 * @returns Promise */ export async function renderElementToCanvas( element: HTMLElement, options: RenderOptions = {} ): Promise { const { scale = 2, // 默认使用更高的缩放比例 backgroundColor = null, // 默认透明背景 useCORS = true, timeout = 15000 // 增加超时时间 } = options; try { // 动态导入html2canvas以减少初始包大小 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) { // 确保SVG元素正确渲染 const svgElements = targetElement.querySelectorAll('svg'); svgElements.forEach(svg => { // 设置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()); } } // 确保viewBox存在 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; } } }); // 验证生成的Canvas 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}`); } } /** * 将SVG元素渲染到Canvas * @param svgElement SVG元素 * @param options 渲染选项 * @returns Promise */ export async function renderSVGToCanvas( svgElement: SVGElement, options: RenderOptions = {} ): Promise { 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'); // 获取SVG尺寸 const rect = svgElement.getBoundingClientRect(); let width = rect.width || svgElement.clientWidth; let height = rect.height || svgElement.clientHeight; // 从SVG属性获取尺寸作为备选 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 }); // 创建Canvas 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); } // 克隆并优化SVG元素 const clonedSVG = svgElement.cloneNode(true) as SVGElement; // 确保SVG有正确的命名空间和属性 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()); } // 设置viewBox 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); // 获取SVG的XML字符串 let svgData = new XMLSerializer().serializeToString(clonedSVG); // 清理和优化SVG字符串 svgData = svgData.replace(/vector-effect="[^"]*"/g, ''); // 确保SVG字符串格式正确 if (!svgData.includes('xmlns="http://www.w3.org/2000/svg"')) { svgData = svgData.replace(' { try { console.log('renderSVGToCanvas: Image loaded successfully, drawing to canvas'); ctx.drawImage(img, 0, 0, width, height); URL.revokeObjectURL(url); clearTimeout(timeoutId); // 验证Canvas内容 const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height); const hasContent = imageData.data.some((value, index) => { // 检查非透明像素 (alpha通道) 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}`)); } }); } /** * 将Canvas转换为Base64数据URL * @param canvas Canvas元素 * @param options 转换选项 * @returns Base64数据URL字符串 */ 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}`); } } /** * 直接将DOM元素渲染为Base64图像 * @param element 要渲染的DOM元素 * @param options 渲染和转换选项 * @returns Promise Base64数据URL */ export async function renderElementToBase64( element: HTMLElement, options: RenderOptions = {} ): Promise { 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}`); } } /** * 检查浏览器是否支持Canvas渲染 * @returns boolean */ 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; } } /** * 获取元素的尺寸信息(生产环境优化版本) * @param element HTML元素 * @returns 元素的各种尺寸信息 */ export function getElementDimensions(element: HTMLElement) { const rect = element.getBoundingClientRect(); const computedStyle = window.getComputedStyle(element); // 特殊处理SVG元素 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 }; }