Upload public.js
Browse files- backend/src/routes/public.js +125 -125
backend/src/routes/public.js
CHANGED
@@ -6,6 +6,129 @@ import { generateSlideHTML, generateExportPage, exportPPTToJSON, generateHTMLPre
|
|
6 |
|
7 |
const router = express.Router();
|
8 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
9 |
// Generate error page for frontend display
|
10 |
function generateErrorPage(title, message) {
|
11 |
return `
|
@@ -352,13 +475,13 @@ router.get('/screenshot/:userId/:pptId/:slideIndex?', async (req, res, next) =>
|
|
352 |
}
|
353 |
});
|
354 |
|
355 |
-
//
|
356 |
router.get('/image/:userId/:pptId/:slideIndex?', async (req, res, next) => {
|
357 |
try {
|
358 |
const { userId, pptId, slideIndex = 0 } = req.params;
|
359 |
const { format = 'svg', quality = 90, width: requestWidth, height: requestHeight } = req.query;
|
360 |
|
361 |
-
console.log(`🖼️ Image request
|
362 |
|
363 |
// Get PPT data
|
364 |
const fileName = `${pptId}.json`;
|
@@ -827,127 +950,4 @@ router.get('/direct-image/:userId/:pptId/:slideIndex?', async (req, res, next) =
|
|
827 |
}
|
828 |
});
|
829 |
|
830 |
-
// 辅助函数:生成SVG - 改进版,支持更精确的PPT渲染
|
831 |
-
function generateSlideSVG(slide, pptData, options = {}) {
|
832 |
-
const { width = 1000, height = 562 } = options;
|
833 |
-
|
834 |
-
// 处理背景
|
835 |
-
let backgroundStyle = '#ffffff';
|
836 |
-
if (slide.background) {
|
837 |
-
if (slide.background.type === 'solid') {
|
838 |
-
backgroundStyle = slide.background.color || '#ffffff';
|
839 |
-
} else if (slide.background.type === 'gradient' && slide.background.gradient) {
|
840 |
-
// SVG渐变处理
|
841 |
-
const colors = slide.background.gradient.colors || [];
|
842 |
-
if (colors.length > 0) {
|
843 |
-
backgroundStyle = colors[0].color || '#ffffff';
|
844 |
-
}
|
845 |
-
}
|
846 |
-
}
|
847 |
-
|
848 |
-
// 生成渐变定义(如果需要)
|
849 |
-
let gradientDefs = '';
|
850 |
-
if (slide.background?.type === 'gradient' && slide.background.gradient?.colors) {
|
851 |
-
const colors = slide.background.gradient.colors;
|
852 |
-
const gradientId = 'bg-gradient';
|
853 |
-
if (slide.background.gradient.type === 'linear') {
|
854 |
-
gradientDefs = `
|
855 |
-
<defs>
|
856 |
-
<linearGradient id="${gradientId}" x1="0%" y1="0%" x2="100%" y2="0%">
|
857 |
-
${colors.map((color, index) =>
|
858 |
-
`<stop offset="${(index / (colors.length - 1)) * 100}%" style="stop-color:${color.color};stop-opacity:1" />`
|
859 |
-
).join('')}
|
860 |
-
</linearGradient>
|
861 |
-
</defs>
|
862 |
-
`;
|
863 |
-
backgroundStyle = `url(#${gradientId})`;
|
864 |
-
}
|
865 |
-
}
|
866 |
-
|
867 |
-
// 渲染元素
|
868 |
-
const elementsHTML = slide.elements.map(element => {
|
869 |
-
const x = element.left || 0;
|
870 |
-
const y = element.top || 0;
|
871 |
-
const w = element.width || 100;
|
872 |
-
const h = element.height || 100;
|
873 |
-
const rotation = element.rotate || 0;
|
874 |
-
|
875 |
-
// 变换属性
|
876 |
-
const transform = rotation !== 0 ? `transform="rotate(${rotation} ${x + w/2} ${y + h/2})"` : '';
|
877 |
-
|
878 |
-
if (element.type === 'text') {
|
879 |
-
const fontSize = element.fontSize || 16;
|
880 |
-
const fontFamily = element.fontName || 'Arial, sans-serif';
|
881 |
-
const color = element.defaultColor || element.color || '#000000';
|
882 |
-
const fontWeight = element.bold ? 'bold' : 'normal';
|
883 |
-
const fontStyle = element.italic ? 'italic' : 'normal';
|
884 |
-
const textDecoration = element.underline ? 'underline' : 'none';
|
885 |
-
const textAnchor = element.align === 'center' ? 'middle' : element.align === 'right' ? 'end' : 'start';
|
886 |
-
|
887 |
-
// 计算文本位置
|
888 |
-
let textX = x + 10;
|
889 |
-
if (element.align === 'center') textX = x + w/2;
|
890 |
-
else if (element.align === 'right') textX = x + w - 10;
|
891 |
-
|
892 |
-
// 处理多行文本
|
893 |
-
const content = (element.content || '').replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
894 |
-
const lines = content.split('\n');
|
895 |
-
|
896 |
-
return `
|
897 |
-
<g ${transform}>
|
898 |
-
${lines.map((line, index) => `
|
899 |
-
<text x="${textX}" y="${y + fontSize + (index * fontSize * 1.2)}"
|
900 |
-
font-family="${fontFamily}"
|
901 |
-
font-size="${fontSize}"
|
902 |
-
fill="${color}"
|
903 |
-
font-weight="${fontWeight}"
|
904 |
-
font-style="${fontStyle}"
|
905 |
-
text-decoration="${textDecoration}"
|
906 |
-
text-anchor="${textAnchor}">${line}</text>
|
907 |
-
`).join('')}
|
908 |
-
</g>
|
909 |
-
`;
|
910 |
-
}
|
911 |
-
|
912 |
-
if (element.type === 'shape') {
|
913 |
-
const fill = element.fill || '#cccccc';
|
914 |
-
const stroke = element.outline?.color || 'none';
|
915 |
-
const strokeWidth = element.outline?.width || 0;
|
916 |
-
|
917 |
-
if (element.shape === 'ellipse') {
|
918 |
-
const cx = x + w/2;
|
919 |
-
const cy = y + h/2;
|
920 |
-
const rx = w/2;
|
921 |
-
const ry = h/2;
|
922 |
-
return `<ellipse cx="${cx}" cy="${cy}" rx="${rx}" ry="${ry}" fill="${fill}" stroke="${stroke}" stroke-width="${strokeWidth}" ${transform}/>`;
|
923 |
-
}
|
924 |
-
|
925 |
-
// 默认矩形
|
926 |
-
const borderRadius = element.borderRadius || 0;
|
927 |
-
if (borderRadius > 0) {
|
928 |
-
return `<rect x="${x}" y="${y}" width="${w}" height="${h}" rx="${borderRadius}" ry="${borderRadius}" fill="${fill}" stroke="${stroke}" stroke-width="${strokeWidth}" ${transform}/>`;
|
929 |
-
}
|
930 |
-
return `<rect x="${x}" y="${y}" width="${w}" height="${h}" fill="${fill}" stroke="${stroke}" stroke-width="${strokeWidth}" ${transform}/>`;
|
931 |
-
}
|
932 |
-
|
933 |
-
if (element.type === 'image' && element.src) {
|
934 |
-
// 检查是否是base64图片
|
935 |
-
if (element.src.startsWith('data:image/')) {
|
936 |
-
return `<image x="${x}" y="${y}" width="${w}" height="${h}" href="${element.src}" ${transform}/>`;
|
937 |
-
}
|
938 |
-
// 外部图片 - 在SVG中可能有跨域问题,显示占位符
|
939 |
-
return `<rect x="${x}" y="${y}" width="${w}" height="${h}" fill="#f0f0f0" stroke="#ccc" stroke-width="1" ${transform}/>
|
940 |
-
<text x="${x + w/2}" y="${y + h/2}" text-anchor="middle" font-size="12" fill="#666">图片</text>`;
|
941 |
-
}
|
942 |
-
|
943 |
-
return '';
|
944 |
-
}).filter(Boolean).join('');
|
945 |
-
|
946 |
-
return `<svg width="${width}" height="${height}" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
947 |
-
${gradientDefs}
|
948 |
-
<rect width="100%" height="100%" fill="${backgroundStyle}"/>
|
949 |
-
<g>${elementsHTML}</g>
|
950 |
-
</svg>`;
|
951 |
-
}
|
952 |
-
|
953 |
export default router;
|
|
|
6 |
|
7 |
const router = express.Router();
|
8 |
|
9 |
+
// 辅助函数:生成SVG - 改进版,支持更精确的PPT渲染
|
10 |
+
function generateSlideSVG(slide, pptData, options = {}) {
|
11 |
+
const { width = 1000, height = 562 } = options;
|
12 |
+
|
13 |
+
// 处理背景
|
14 |
+
let backgroundStyle = '#ffffff';
|
15 |
+
if (slide.background) {
|
16 |
+
if (slide.background.type === 'solid') {
|
17 |
+
backgroundStyle = slide.background.color || '#ffffff';
|
18 |
+
} else if (slide.background.type === 'gradient' && slide.background.gradient) {
|
19 |
+
// SVG渐变处理
|
20 |
+
const colors = slide.background.gradient.colors || [];
|
21 |
+
if (colors.length > 0) {
|
22 |
+
backgroundStyle = colors[0].color || '#ffffff';
|
23 |
+
}
|
24 |
+
}
|
25 |
+
}
|
26 |
+
|
27 |
+
// 生成渐变定义(如果需要)
|
28 |
+
let gradientDefs = '';
|
29 |
+
if (slide.background?.type === 'gradient' && slide.background.gradient?.colors) {
|
30 |
+
const colors = slide.background.gradient.colors;
|
31 |
+
const gradientId = 'bg-gradient';
|
32 |
+
if (slide.background.gradient.type === 'linear') {
|
33 |
+
gradientDefs = `
|
34 |
+
<defs>
|
35 |
+
<linearGradient id="${gradientId}" x1="0%" y1="0%" x2="100%" y2="0%">
|
36 |
+
${colors.map((color, index) =>
|
37 |
+
`<stop offset="${(index / (colors.length - 1)) * 100}%" style="stop-color:${color.color};stop-opacity:1" />`
|
38 |
+
).join('')}
|
39 |
+
</linearGradient>
|
40 |
+
</defs>
|
41 |
+
`;
|
42 |
+
backgroundStyle = `url(#${gradientId})`;
|
43 |
+
}
|
44 |
+
}
|
45 |
+
|
46 |
+
// 渲染元素
|
47 |
+
const elementsHTML = slide.elements.map(element => {
|
48 |
+
const x = element.left || 0;
|
49 |
+
const y = element.top || 0;
|
50 |
+
const w = element.width || 100;
|
51 |
+
const h = element.height || 100;
|
52 |
+
const rotation = element.rotate || 0;
|
53 |
+
|
54 |
+
// 变换属性
|
55 |
+
const transform = rotation !== 0 ? `transform="rotate(${rotation} ${x + w/2} ${y + h/2})"` : '';
|
56 |
+
|
57 |
+
if (element.type === 'text') {
|
58 |
+
const fontSize = element.fontSize || 16;
|
59 |
+
const fontFamily = element.fontName || 'Arial, sans-serif';
|
60 |
+
const color = element.defaultColor || element.color || '#000000';
|
61 |
+
const fontWeight = element.bold ? 'bold' : 'normal';
|
62 |
+
const fontStyle = element.italic ? 'italic' : 'normal';
|
63 |
+
const textDecoration = element.underline ? 'underline' : 'none';
|
64 |
+
const textAnchor = element.align === 'center' ? 'middle' : element.align === 'right' ? 'end' : 'start';
|
65 |
+
|
66 |
+
// 计算文本位置
|
67 |
+
let textX = x + 10;
|
68 |
+
if (element.align === 'center') textX = x + w/2;
|
69 |
+
else if (element.align === 'right') textX = x + w - 10;
|
70 |
+
|
71 |
+
// 处理多行文本
|
72 |
+
const content = (element.content || '').replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
73 |
+
const lines = content.split('\n');
|
74 |
+
|
75 |
+
return `
|
76 |
+
<g ${transform}>
|
77 |
+
${lines.map((line, index) => `
|
78 |
+
<text x="${textX}" y="${y + fontSize + (index * fontSize * 1.2)}"
|
79 |
+
font-family="${fontFamily}"
|
80 |
+
font-size="${fontSize}"
|
81 |
+
fill="${color}"
|
82 |
+
font-weight="${fontWeight}"
|
83 |
+
font-style="${fontStyle}"
|
84 |
+
text-decoration="${textDecoration}"
|
85 |
+
text-anchor="${textAnchor}">${line}</text>
|
86 |
+
`).join('')}
|
87 |
+
</g>
|
88 |
+
`;
|
89 |
+
}
|
90 |
+
|
91 |
+
if (element.type === 'shape') {
|
92 |
+
const fill = element.fill || '#cccccc';
|
93 |
+
const stroke = element.outline?.color || 'none';
|
94 |
+
const strokeWidth = element.outline?.width || 0;
|
95 |
+
|
96 |
+
if (element.shape === 'ellipse') {
|
97 |
+
const cx = x + w/2;
|
98 |
+
const cy = y + h/2;
|
99 |
+
const rx = w/2;
|
100 |
+
const ry = h/2;
|
101 |
+
return `<ellipse cx="${cx}" cy="${cy}" rx="${rx}" ry="${ry}" fill="${fill}" stroke="${stroke}" stroke-width="${strokeWidth}" ${transform}/>`;
|
102 |
+
}
|
103 |
+
|
104 |
+
// 默认矩形
|
105 |
+
const borderRadius = element.borderRadius || 0;
|
106 |
+
if (borderRadius > 0) {
|
107 |
+
return `<rect x="${x}" y="${y}" width="${w}" height="${h}" rx="${borderRadius}" ry="${borderRadius}" fill="${fill}" stroke="${stroke}" stroke-width="${strokeWidth}" ${transform}/>`;
|
108 |
+
}
|
109 |
+
return `<rect x="${x}" y="${y}" width="${w}" height="${h}" fill="${fill}" stroke="${stroke}" stroke-width="${strokeWidth}" ${transform}/>`;
|
110 |
+
}
|
111 |
+
|
112 |
+
if (element.type === 'image' && element.src) {
|
113 |
+
// 检查是否是base64图片
|
114 |
+
if (element.src.startsWith('data:image/')) {
|
115 |
+
return `<image x="${x}" y="${y}" width="${w}" height="${h}" href="${element.src}" ${transform}/>`;
|
116 |
+
}
|
117 |
+
// 外部图片 - 在SVG中可能有跨域问题,显示占位符
|
118 |
+
return `<rect x="${x}" y="${y}" width="${w}" height="${h}" fill="#f0f0f0" stroke="#ccc" stroke-width="1" ${transform}/>
|
119 |
+
<text x="${x + w/2}" y="${y + h/2}" text-anchor="middle" font-size="12" fill="#666">图片</text>`;
|
120 |
+
}
|
121 |
+
|
122 |
+
return '';
|
123 |
+
}).filter(Boolean).join('');
|
124 |
+
|
125 |
+
return `<svg width="${width}" height="${height}" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
126 |
+
${gradientDefs}
|
127 |
+
<rect width="100%" height="100%" fill="${backgroundStyle}"/>
|
128 |
+
<g>${elementsHTML}</g>
|
129 |
+
</svg>`;
|
130 |
+
}
|
131 |
+
|
132 |
// Generate error page for frontend display
|
133 |
function generateErrorPage(title, message) {
|
134 |
return `
|
|
|
475 |
}
|
476 |
});
|
477 |
|
478 |
+
// 🔥 关键修改:image端点现在直接返回SVG图片,不再返回截图工具页面
|
479 |
router.get('/image/:userId/:pptId/:slideIndex?', async (req, res, next) => {
|
480 |
try {
|
481 |
const { userId, pptId, slideIndex = 0 } = req.params;
|
482 |
const { format = 'svg', quality = 90, width: requestWidth, height: requestHeight } = req.query;
|
483 |
|
484 |
+
console.log(`🖼️ Direct Image request: userId=${userId}, pptId=${pptId}, slideIndex=${slideIndex}, format=${format}`);
|
485 |
|
486 |
// Get PPT data
|
487 |
const fileName = `${pptId}.json`;
|
|
|
950 |
}
|
951 |
});
|
952 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
953 |
export default router;
|