web_ppt / frontend /src /utils /canvasRenderer.ts
CatPtain's picture
Upload 4 files
f11a4fe verified
/**
* 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<HTMLCanvasElement>
*/
export async function renderElementToCanvas(
element: HTMLElement,
options: RenderOptions = {}
): Promise<HTMLCanvasElement> {
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<HTMLCanvasElement>
*/
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');
// 获取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('<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);
// 验证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<string> Base64数据URL
*/
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}`);
}
}
/**
* 检查浏览器是否支持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
};
}