Upload useExport.ts
Browse files- frontend/src/hooks/useExport.ts +117 -4
frontend/src/hooks/useExport.ts
CHANGED
|
@@ -14,6 +14,8 @@ import { encrypt } from '@/utils/crypto'
|
|
| 14 |
import { svg2Base64 } from '@/utils/svg2Base64'
|
| 15 |
import { renderElementToBase64, isCanvasRenderSupported, getElementDimensions } from '@/utils/canvasRenderer'
|
| 16 |
import { renderWithHuggingfaceFix, isHuggingfaceEnvironment } from '@/utils/huggingfaceRenderer'
|
|
|
|
|
|
|
| 17 |
import message from '@/utils/message'
|
| 18 |
|
| 19 |
interface ExportImageConfig {
|
|
@@ -37,19 +39,68 @@ export default () => {
|
|
| 37 |
|
| 38 |
const exporting = ref(false)
|
| 39 |
|
| 40 |
-
//
|
| 41 |
const exportImage = (domRef: HTMLElement, format: string, quality: number, ignoreWebfont = true) => {
|
| 42 |
exporting.value = true
|
| 43 |
|
| 44 |
-
//
|
| 45 |
-
const isHF =
|
| 46 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 47 |
|
| 48 |
const foreignObjectSpans = domRef.querySelectorAll('foreignObject [xmlns]')
|
| 49 |
foreignObjectSpans.forEach(spanRef => spanRef.removeAttribute('xmlns'))
|
| 50 |
|
| 51 |
setTimeout(async () => {
|
| 52 |
try {
|
|
|
|
|
|
|
|
|
|
| 53 |
let dataUrl: string
|
| 54 |
|
| 55 |
if (isHF) {
|
|
@@ -509,6 +560,46 @@ export default () => {
|
|
| 509 |
}
|
| 510 |
|
| 511 |
// 格式化元素
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 512 |
const formatElement = (element: any) => {
|
| 513 |
const baseStyle = `
|
| 514 |
left: ${element.left || 0}px;
|
|
@@ -538,6 +629,28 @@ export default () => {
|
|
| 538 |
}
|
| 539 |
|
| 540 |
if (element.type === 'shape') {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 541 |
const shapeStyle = `
|
| 542 |
background: ${element.fill || '#ffffff'};
|
| 543 |
border: ${element.outline?.width || 0}px solid ${element.outline?.color || '#000000'};
|
|
|
|
| 14 |
import { svg2Base64 } from '@/utils/svg2Base64'
|
| 15 |
import { renderElementToBase64, isCanvasRenderSupported, getElementDimensions } from '@/utils/canvasRenderer'
|
| 16 |
import { renderWithHuggingfaceFix, isHuggingfaceEnvironment } from '@/utils/huggingfaceRenderer'
|
| 17 |
+
import { vectorRenderManager, RenderStrategy } from '@/utils/VectorRenderManager'
|
| 18 |
+
import { VECTOR_EXPORT_CONFIG } from '@/config/vectorExportConfig'
|
| 19 |
import message from '@/utils/message'
|
| 20 |
|
| 21 |
interface ExportImageConfig {
|
|
|
|
| 39 |
|
| 40 |
const exporting = ref(false)
|
| 41 |
|
| 42 |
+
// 导出图片(生产环境优化版本)
|
| 43 |
const exportImage = (domRef: HTMLElement, format: string, quality: number, ignoreWebfont = true) => {
|
| 44 |
exporting.value = true
|
| 45 |
|
| 46 |
+
// 环境检测
|
| 47 |
+
const isHF = VECTOR_EXPORT_CONFIG.ENVIRONMENT.isHuggingface()
|
| 48 |
+
const isProd = VECTOR_EXPORT_CONFIG.ENVIRONMENT.isProduction()
|
| 49 |
+
|
| 50 |
+
if (!isProd) {
|
| 51 |
+
console.log('exportImage: Environment check:', { isHuggingface: isHF, format, production: isProd })
|
| 52 |
+
}
|
| 53 |
+
|
| 54 |
+
// 预处理矢量图形元素
|
| 55 |
+
const preprocessVectorElements = async () => {
|
| 56 |
+
const svgElements = domRef.querySelectorAll('svg');
|
| 57 |
+
const vectorShapes = domRef.querySelectorAll('.vector-shape');
|
| 58 |
+
|
| 59 |
+
// 处理SVG元素
|
| 60 |
+
for (const svg of Array.from(svgElements)) {
|
| 61 |
+
try {
|
| 62 |
+
// 移除problematic属性
|
| 63 |
+
svg.removeAttribute('vector-effect');
|
| 64 |
+
const vectorEffectElements = svg.querySelectorAll('[vector-effect]');
|
| 65 |
+
vectorEffectElements.forEach(el => el.removeAttribute('vector-effect'));
|
| 66 |
+
|
| 67 |
+
// 确保命名空间
|
| 68 |
+
if (!svg.hasAttribute('xmlns')) {
|
| 69 |
+
svg.setAttribute('xmlns', 'http://www.w3.org/2000/svg');
|
| 70 |
+
}
|
| 71 |
+
} catch (error) {
|
| 72 |
+
if (!isProd) {
|
| 73 |
+
console.warn('exportImage: SVG preprocessing failed:', error);
|
| 74 |
+
}
|
| 75 |
+
}
|
| 76 |
+
}
|
| 77 |
+
|
| 78 |
+
// 处理矢量形状
|
| 79 |
+
for (const shape of Array.from(vectorShapes)) {
|
| 80 |
+
try {
|
| 81 |
+
const svgChild = shape.querySelector('svg');
|
| 82 |
+
if (svgChild) {
|
| 83 |
+
svgChild.removeAttribute('vector-effect');
|
| 84 |
+
if (!svgChild.hasAttribute('xmlns')) {
|
| 85 |
+
svgChild.setAttribute('xmlns', 'http://www.w3.org/2000/svg');
|
| 86 |
+
}
|
| 87 |
+
}
|
| 88 |
+
} catch (error) {
|
| 89 |
+
if (!isProd) {
|
| 90 |
+
console.warn('exportImage: Vector shape preprocessing failed:', error);
|
| 91 |
+
}
|
| 92 |
+
}
|
| 93 |
+
}
|
| 94 |
+
};
|
| 95 |
|
| 96 |
const foreignObjectSpans = domRef.querySelectorAll('foreignObject [xmlns]')
|
| 97 |
foreignObjectSpans.forEach(spanRef => spanRef.removeAttribute('xmlns'))
|
| 98 |
|
| 99 |
setTimeout(async () => {
|
| 100 |
try {
|
| 101 |
+
// 预处理矢量元素
|
| 102 |
+
await preprocessVectorElements();
|
| 103 |
+
|
| 104 |
let dataUrl: string
|
| 105 |
|
| 106 |
if (isHF) {
|
|
|
|
| 560 |
}
|
| 561 |
|
| 562 |
// 格式化元素
|
| 563 |
+
// 生产环境级别的矢量图形SVG生成函数
|
| 564 |
+
const generateSVGFromShape = (element: any): string => {
|
| 565 |
+
try {
|
| 566 |
+
const { width, height, path, fill, outline, viewBox, opacity = 1 } = element;
|
| 567 |
+
const [vbX, vbY, vbWidth, vbHeight] = viewBox || [0, 0, width, height];
|
| 568 |
+
|
| 569 |
+
// 安全的颜色处理
|
| 570 |
+
const fillColor = fill || '#000000';
|
| 571 |
+
const strokeColor = outline?.color || 'none';
|
| 572 |
+
const strokeWidth = outline?.width || 0;
|
| 573 |
+
|
| 574 |
+
// 构建SVG字符串,确保所有属性都被正确转义
|
| 575 |
+
const svgAttributes = [
|
| 576 |
+
`width="${width}"`,
|
| 577 |
+
`height="${height}"`,
|
| 578 |
+
`viewBox="${vbX} ${vbY} ${vbWidth} ${vbHeight}"`,
|
| 579 |
+
'xmlns="http://www.w3.org/2000/svg"',
|
| 580 |
+
'style="display: block; width: 100%; height: 100%;"'
|
| 581 |
+
].join(' ');
|
| 582 |
+
|
| 583 |
+
const pathAttributes = [
|
| 584 |
+
`d="${path}"`,
|
| 585 |
+
`fill="${fillColor}"`,
|
| 586 |
+
strokeWidth > 0 ? `stroke="${strokeColor}"` : '',
|
| 587 |
+
strokeWidth > 0 ? `stroke-width="${strokeWidth}"` : '',
|
| 588 |
+
opacity < 1 ? `opacity="${opacity}"` : '',
|
| 589 |
+
'vector-effect="non-scaling-stroke"'
|
| 590 |
+
].filter(Boolean).join(' ');
|
| 591 |
+
|
| 592 |
+
return `<svg ${svgAttributes}><path ${pathAttributes} /></svg>`;
|
| 593 |
+
} catch (error) {
|
| 594 |
+
console.error('generateSVGFromShape error:', error);
|
| 595 |
+
// 生产环境降级处理:返回简单的占位符
|
| 596 |
+
return `<svg width="${element.width || 100}" height="${element.height || 100}" xmlns="http://www.w3.org/2000/svg">
|
| 597 |
+
<rect width="100%" height="100%" fill="#f5f5f5" stroke="#ddd" stroke-width="1"/>
|
| 598 |
+
<text x="50%" y="50%" text-anchor="middle" dy="0.3em" font-size="12" fill="#999">Vector</text>
|
| 599 |
+
</svg>`;
|
| 600 |
+
}
|
| 601 |
+
};
|
| 602 |
+
|
| 603 |
const formatElement = (element: any) => {
|
| 604 |
const baseStyle = `
|
| 605 |
left: ${element.left || 0}px;
|
|
|
|
| 629 |
}
|
| 630 |
|
| 631 |
if (element.type === 'shape') {
|
| 632 |
+
// 处理特殊矢量图形(生产环境优化)
|
| 633 |
+
if (element.special && element.path) {
|
| 634 |
+
try {
|
| 635 |
+
const svgContent = generateSVGFromShape(element);
|
| 636 |
+
return `<div class="element shape-element vector-shape" style="${baseStyle}">${svgContent}</div>`;
|
| 637 |
+
} catch (error) {
|
| 638 |
+
// 生产环境降级处理
|
| 639 |
+
if (VECTOR_EXPORT_CONFIG.ERROR_CONFIG.LOGGING.VERBOSE) {
|
| 640 |
+
console.warn('formatElement: Vector shape generation failed, using fallback:', error);
|
| 641 |
+
}
|
| 642 |
+
|
| 643 |
+
// 使用简化的矢量图形表示
|
| 644 |
+
const fallbackSvg = `<svg width="${element.width}" height="${element.height}" xmlns="http://www.w3.org/2000/svg">
|
| 645 |
+
<rect width="100%" height="100%" fill="${element.fill || '#f5f5f5'}" stroke="${element.outline?.color || '#ddd'}" stroke-width="${element.outline?.width || 1}"/>
|
| 646 |
+
<text x="50%" y="50%" text-anchor="middle" dy="0.3em" font-size="12" fill="#999">Vector</text>
|
| 647 |
+
</svg>`;
|
| 648 |
+
|
| 649 |
+
return `<div class="element shape-element vector-shape-fallback" style="${baseStyle}">${fallbackSvg}</div>`;
|
| 650 |
+
}
|
| 651 |
+
}
|
| 652 |
+
|
| 653 |
+
// 处理普通形状
|
| 654 |
const shapeStyle = `
|
| 655 |
background: ${element.fill || '#ffffff'};
|
| 656 |
border: ${element.outline?.width || 0}px solid ${element.outline?.color || '#000000'};
|