web_ppt / frontend /src /utils /huggingfaceRenderer.ts
CatPtain's picture
Upload 2 files
66b71dc verified
/**
* Huggingface环境专用渲染器
* 解决Huggingface Spaces部署环境下的矢量图形渲染问题
*/
// 检测是否在Huggingface环境中
const isHuggingfaceEnvironment = (): boolean => {
return (
typeof window !== 'undefined' &&
(window.location.hostname.includes('hf.space') ||
window.location.hostname.includes('huggingface.co') ||
process.env.NODE_ENV === 'production')
);
};
// Huggingface环境专用配置
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;
}
// 获取Huggingface环境优化配置
const getHuggingfaceConfig = (): HuggingfaceRenderConfig => {
const isHF = isHuggingfaceEnvironment();
return {
useCORS: false, // Huggingface环境下禁用CORS
allowTaint: true, // 允许污染画布
foreignObjectRendering: false, // 禁用foreignObject渲染
scale: isHF ? 1 : 2, // Huggingface环境使用较低缩放
logging: false, // 生产环境禁用日志
timeout: 30000, // 增加超时时间
backgroundColor: null,
removeContainer: true,
imageTimeout: 15000,
onclone: (clonedDoc: Document, element: HTMLElement) => {
// Huggingface环境特殊处理
if (isHF) {
// 移除可能导致CORS问题的外部资源
const externalImages = clonedDoc.querySelectorAll('img[src^="http"]');
externalImages.forEach(img => {
const imgElement = img as HTMLImageElement;
// 替换为占位符或移除
imgElement.style.display = 'none';
});
// 处理SVG元素
const svgElements = clonedDoc.querySelectorAll('svg');
svgElements.forEach(svg => {
// 确保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());
}
// 设置viewBox
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');
}
});
});
}
}
};
};
/**
* Huggingface环境专用SVG转Base64
* @param element SVG元素
* @returns Promise<string> Base64数据URL
*/
export const huggingfaceSvg2Base64 = async (element: Element): Promise<string> => {
console.log('huggingfaceSvg2Base64: Starting conversion for Huggingface environment');
try {
// 方法1: 直接序列化SVG(最兼容)
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 {
// 方法2: Canvas渲染(备选)
const canvasResult = await canvasSvgRendering(element);
if (canvasResult) {
console.log('huggingfaceSvg2Base64: Canvas rendering succeeded');
return canvasResult;
}
} catch (error) {
console.warn('huggingfaceSvg2Base64: Canvas rendering failed:', error);
}
// 方法3: 最小化SVG(最后备选)
console.log('huggingfaceSvg2Base64: Using minimal SVG fallback');
return createMinimalSvg(element);
};
/**
* 直接SVG序列化方法
*/
const directSvgSerialization = async (element: Element): Promise<string | null> => {
try {
const clonedElement = element.cloneNode(true) as Element;
// 确保是SVG元素
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;
}
};
/**
* Canvas SVG渲染方法
*/
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;
// 创建SVG数据URL
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);
}
});
};
/**
* 创建最小化SVG
*/
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}`;
};
/**
* Huggingface环境专用html2canvas渲染
* @param element 要渲染的元素
* @returns Promise<string> Base64数据URL
*/
export const huggingfaceHtml2Canvas = async (element: HTMLElement): Promise<string> => {
console.log('huggingfaceHtml2Canvas: Starting rendering for Huggingface environment');
const config = getHuggingfaceConfig();
try {
// 动态导入html2canvas
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);
// 备选方案:使用html-to-image
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}`);
}
}
};
/**
* 检测并应用Huggingface环境修复
* @param element 要处理的元素
* @returns Promise<string> Base64数据URL
*/
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'
});
// 检查是否包含SVG元素
const hasSvg = element.tagName.toLowerCase() === 'svg' || element.querySelector('svg');
if (hasSvg && isHF) {
// 对于包含SVG的元素,在Huggingface环境下使用专用方法
const svgElement = element.tagName.toLowerCase() === 'svg' ? element : element.querySelector('svg');
if (svgElement) {
return await huggingfaceSvg2Base64(svgElement);
}
}
// 使用Huggingface优化的html2canvas
return await huggingfaceHtml2Canvas(element);
};
export { isHuggingfaceEnvironment, getHuggingfaceConfig };