web_ppt / frontend /src /utils /svg2Base64.ts
CatPtain's picture
Upload 4 files
f11a4fe verified
// svg转base64图片,参考:https://github.com/scriptex/svg64
const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/='
const PREFIX = 'data:image/svg+xml;base64,'
const utf8Encode = (string: string) => {
string = string.replace(/\r\n/g, '\n')
let utftext = ''
for (let n = 0; n < string.length; n++) {
const c = string.charCodeAt(n)
if (c < 128) {
utftext += String.fromCharCode(c)
}
else if (c > 127 && c < 2048) {
utftext += String.fromCharCode((c >> 6) | 192)
utftext += String.fromCharCode((c & 63) | 128)
}
else {
utftext += String.fromCharCode((c >> 12) | 224)
utftext += String.fromCharCode(((c >> 6) & 63) | 128)
utftext += String.fromCharCode((c & 63) | 128)
}
}
return utftext
}
const encode = (input: string) => {
let output = ''
let chr1, chr2, chr3, enc1, enc2, enc3, enc4
let i = 0
input = utf8Encode(input)
while (i < input.length) {
chr1 = input.charCodeAt(i++)
chr2 = input.charCodeAt(i++)
chr3 = input.charCodeAt(i++)
enc1 = chr1 >> 2
enc2 = ((chr1 & 3) << 4) | (chr2 >> 4)
enc3 = ((chr2 & 15) << 2) | (chr3 >> 6)
enc4 = chr3 & 63
if (isNaN(chr2)) enc3 = enc4 = 64
else if (isNaN(chr3)) enc4 = 64
output = output + characters.charAt(enc1) + characters.charAt(enc2) + characters.charAt(enc3) + characters.charAt(enc4)
}
return output
}
// 生产环境检测和优化
const isProductionEnvironment = (): boolean => {
return process.env.NODE_ENV === 'production';
};
const isHuggingfaceEnvironment = (): boolean => {
return (
typeof window !== 'undefined' &&
(window.location.hostname.includes('hf.space') ||
window.location.hostname.includes('huggingface.co') ||
isProductionEnvironment())
);
};
// 生产环境性能监控
class SVGRenderPerformanceMonitor {
private static instance: SVGRenderPerformanceMonitor;
private metrics: Map<string, number[]> = new Map();
static getInstance(): SVGRenderPerformanceMonitor {
if (!this.instance) {
this.instance = new SVGRenderPerformanceMonitor();
}
return this.instance;
}
recordMetric(operation: string, duration: number): void {
if (!isProductionEnvironment()) return;
if (!this.metrics.has(operation)) {
this.metrics.set(operation, []);
}
const metrics = this.metrics.get(operation)!;
metrics.push(duration);
// 保持最近100次记录
if (metrics.length > 100) {
metrics.shift();
}
}
getAverageTime(operation: string): number {
const metrics = this.metrics.get(operation);
if (!metrics || metrics.length === 0) return 0;
return metrics.reduce((sum, time) => sum + time, 0) / metrics.length;
}
}
export const svg2Base64 = (element: Element) => {
const startTime = performance.now();
const monitor = SVGRenderPerformanceMonitor.getInstance();
const isHF = isHuggingfaceEnvironment();
const isProd = isProductionEnvironment();
try {
// 生产环境减少日志输出
if (!isProd) {
console.log('svg2Base64: Starting conversion for element:', {
tagName: element.tagName,
className: element.className,
id: element.id,
hasChildren: element.children.length > 0,
isHuggingfaceEnvironment: isHF
});
}
// Huggingface环境使用简化的处理方式
if (isHF) {
console.log('svg2Base64: Using Huggingface optimized processing');
return huggingfaceOptimizedSvg2Base64(element);
}
// 克隆元素以避免修改原始DOM
const clonedElement = element.cloneNode(true) as Element;
console.log('svg2Base64: Element cloned successfully');
// 确保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');
// 确保SVG有明确的尺寸
const rect = element.getBoundingClientRect();
if (!svgElement.getAttribute('width') && rect.width > 0) {
svgElement.setAttribute('width', rect.width.toString());
}
if (!svgElement.getAttribute('height') && rect.height > 0) {
svgElement.setAttribute('height', rect.height.toString());
}
// 确保viewBox存在
if (!svgElement.getAttribute('viewBox')) {
const width = parseFloat(svgElement.getAttribute('width') || '0');
const height = parseFloat(svgElement.getAttribute('height') || '0');
if (width > 0 && height > 0) {
svgElement.setAttribute('viewBox', `0 0 ${width} ${height}`);
}
}
console.log('svg2Base64: Added SVG namespaces and dimensions');
}
// 处理内联样式 - 将计算样式应用到元素
const applyComputedStyles = (elem: Element, originalElem: Element) => {
if (elem.nodeType === Node.ELEMENT_NODE) {
const computedStyles = window.getComputedStyle(originalElem as HTMLElement);
const styleProps = ['fill', 'stroke', 'stroke-width', 'opacity', 'font-family', 'font-size', 'font-weight'];
styleProps.forEach(prop => {
const value = computedStyles.getPropertyValue(prop);
if (value && value !== 'none' && !elem.getAttribute(prop)) {
elem.setAttribute(prop, value);
}
});
// 递归处理子元素
for (let i = 0; i < elem.children.length; i++) {
const child = elem.children[i];
const originalChild = (originalElem as HTMLElement).children[i];
if (originalChild) {
applyComputedStyles(child, originalChild);
}
}
}
};
applyComputedStyles(clonedElement, element);
// 检查元素尺寸 - 使用更宽松的检查逻辑
const rect = element.getBoundingClientRect();
const computedStyle = window.getComputedStyle(element as HTMLElement);
const hasValidDimensions = (
rect.width > 0 || rect.height > 0 ||
parseFloat(computedStyle.width) > 0 || parseFloat(computedStyle.height) > 0 ||
(element as HTMLElement).offsetWidth > 0 || (element as HTMLElement).offsetHeight > 0
);
console.log('svg2Base64: Element dimensions:', {
boundingRect: { width: rect.width, height: rect.height },
computedStyle: { width: computedStyle.width, height: computedStyle.height },
offset: { width: (element as HTMLElement).offsetWidth, height: (element as HTMLElement).offsetHeight },
hasValidDimensions
});
if (!hasValidDimensions) {
console.warn('svg2Base64: Element has no valid dimensions, but continuing with serialization');
}
const XMLS = new XMLSerializer();
let svg = XMLS.serializeToString(clonedElement);
// 清理和优化SVG字符串
svg = svg.replace(/vector-effect="[^"]*"/g, ''); // 移除vector-effect属性
svg = svg.replace(/xmlns="[^"]*"/g, ''); // 移除重复的xmlns
// 确保SVG标签包含正确的命名空间
if (svg.includes('<svg') && !svg.includes('xmlns="http://www.w3.org/2000/svg"')) {
svg = svg.replace('<svg', '<svg xmlns="http://www.w3.org/2000/svg"');
}
// 添加xmlns:xlink命名空间(如果需要)
if (svg.includes('xlink:') && !svg.includes('xmlns:xlink')) {
svg = svg.replace('<svg', '<svg xmlns:xlink="http://www.w3.org/1999/xlink"');
}
// 确保SVG有基本的尺寸信息
if (svg.includes('<svg') && !svg.includes('viewBox=') && !svg.includes('width=')) {
const rect = element.getBoundingClientRect();
if (rect.width > 0 && rect.height > 0) {
svg = svg.replace('<svg', `<svg width="${rect.width}" height="${rect.height}" viewBox="0 0 ${rect.width} ${rect.height}"`);
}
}
console.log('svg2Base64: Serialization result:', {
length: svg.length,
preview: svg.substring(0, 200) + '...',
containsSvgTag: svg.includes('<svg'),
containsContent: svg.length > 50,
hasNamespace: svg.includes('xmlns="http://www.w3.org/2000/svg"')
});
if (!svg || svg.length === 0) {
throw new Error('SVG serialization returned empty string');
}
if (svg.length < 20) {
throw new Error('SVG serialization returned suspiciously short string');
}
const encoded = encode(svg);
if (!encoded) {
throw new Error('Base64 encoding failed');
}
const result = PREFIX + encoded;
console.log('svg2Base64: Encoding successful, result length:', result.length);
// 验证结果
if (result.length < 100) {
throw new Error('Base64 result is suspiciously short');
}
// 记录成功的性能指标
const duration = performance.now() - startTime;
monitor.recordMetric('svg2base64_success', duration);
if (!isProd) {
console.log(`svg2Base64: Conversion completed in ${duration.toFixed(2)}ms`);
}
return result;
} catch (error) {
// 记录失败的性能指标
const duration = performance.now() - startTime;
monitor.recordMetric('svg2base64_error', duration);
if (!isProd) {
console.error('svg2Base64: Conversion failed:', error);
} else {
// 生产环境只记录关键错误信息
console.error('svg2Base64: Conversion failed');
}
// 尝试多种备选方案
const fallbackStrategies = [
// 策略1: 简化的序列化
() => {
console.log('svg2Base64: Attempting simplified serialization');
const rect = element.getBoundingClientRect();
const width = rect.width || 100;
const height = rect.height || 100;
const simplifiedSvg = `<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}" viewBox="0 0 ${width} ${height}">${element.innerHTML}</svg>`;
const base64 = encode(utf8Encode(simplifiedSvg));
return `data:image/svg+xml;base64,${base64}`;
},
// 策略2: 使用outerHTML
() => {
console.log('svg2Base64: Attempting outerHTML serialization');
let svgString = (element as HTMLElement).outerHTML;
if (!svgString.includes('xmlns')) {
svgString = svgString.replace('<svg', '<svg xmlns="http://www.w3.org/2000/svg"');
}
const base64 = encode(utf8Encode(svgString));
return `data:image/svg+xml;base64,${base64}`;
},
// 策略3: 最小化SVG
() => {
console.log('svg2Base64: Attempting minimal SVG creation');
const rect = element.getBoundingClientRect();
const minimalSvg = `<svg xmlns="http://www.w3.org/2000/svg" width="${rect.width || 100}" height="${rect.height || 100}"><rect width="100%" height="100%" fill="#cccccc"/></svg>`;
const base64 = encode(utf8Encode(minimalSvg));
return `data:image/svg+xml;base64,${base64}`;
}
];
for (let i = 0; i < fallbackStrategies.length; i++) {
try {
const result = fallbackStrategies[i]();
console.log(`svg2Base64: Fallback strategy ${i + 1} succeeded`);
return result;
} catch (fallbackError) {
console.warn(`svg2Base64: Fallback strategy ${i + 1} failed:`, fallbackError);
}
}
console.error('svg2Base64: All fallback strategies failed');
throw new Error(`SVG to Base64 conversion failed: ${error}`);
}
}
/**
* Huggingface环境优化的SVG转Base64函数
* 使用更简单、更兼容的方法
*/
const huggingfaceOptimizedSvg2Base64 = (element: Element): string => {
try {
console.log('huggingfaceOptimizedSvg2Base64: Starting optimized conversion');
// 克隆元素
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');
// 获取尺寸
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 removeProblematicAttrs = (elem: Element) => {
const problematicAttrs = ['vector-effect', 'xmlns:xlink'];
problematicAttrs.forEach(attr => {
if (elem.hasAttribute(attr)) {
elem.removeAttribute(attr);
}
});
// 递归处理子元素
Array.from(elem.children).forEach(child => {
removeProblematicAttrs(child);
});
};
removeProblematicAttrs(svgElement);
// 简化序列化
let svgString = svgElement.outerHTML;
// 基本清理
svgString = svgString.replace(/vector-effect="[^"]*"/g, '');
svgString = svgString.replace(/xmlns:xlink="[^"]*"/g, '');
// 确保命名空间正确
if (!svgString.includes('xmlns="http://www.w3.org/2000/svg"')) {
svgString = svgString.replace('<svg', '<svg xmlns="http://www.w3.org/2000/svg"');
}
console.log('huggingfaceOptimizedSvg2Base64: SVG string prepared:', {
length: svgString.length,
hasNamespace: svgString.includes('xmlns="http://www.w3.org/2000/svg"'),
preview: svgString.substring(0, 150) + '...'
});
// 使用btoa进行编码(更兼容)
const base64 = btoa(unescape(encodeURIComponent(svgString)));
const result = PREFIX + base64;
console.log('huggingfaceOptimizedSvg2Base64: Conversion successful, result length:', result.length);
return result;
}
// 非SVG元素的处理
console.warn('huggingfaceOptimizedSvg2Base64: Element is not SVG, using fallback');
const rect = element.getBoundingClientRect();
const width = rect.width || 100;
const height = rect.height || 100;
const fallbackSvg = `<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}" viewBox="0 0 ${width} ${height}">
<foreignObject width="100%" height="100%">
<div xmlns="http://www.w3.org/1999/xhtml">${element.innerHTML}</div>
</foreignObject>
</svg>`;
const base64 = btoa(unescape(encodeURIComponent(fallbackSvg)));
return PREFIX + base64;
} catch (error) {
console.error('huggingfaceOptimizedSvg2Base64: Conversion failed:', error);
// 最终备选方案:创建简单的占位符SVG
const rect = element.getBoundingClientRect();
const width = rect.width || 100;
const height = rect.height || 100;
const placeholderSvg = `<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}" viewBox="0 0 ${width} ${height}">
<rect width="100%" height="100%" fill="#f5f5f5" stroke="#ddd" stroke-width="1"/>
<text x="50%" y="50%" text-anchor="middle" dy="0.3em" font-family="Arial, sans-serif" font-size="14" fill="#999">SVG</text>
</svg>`;
const base64 = btoa(unescape(encodeURIComponent(placeholderSvg)));
console.log('huggingfaceOptimizedSvg2Base64: Using placeholder SVG');
return PREFIX + base64;
}
}