Upload public.js
Browse files- backend/src/routes/public.js +82 -16
backend/src/routes/public.js
CHANGED
@@ -739,29 +739,86 @@ router.get('/direct-image/:userId/:pptId/:slideIndex?', async (req, res, next) =
|
|
739 |
}
|
740 |
});
|
741 |
|
742 |
-
// 辅助函数:生成SVG
|
743 |
function generateSlideSVG(slide, pptData, options = {}) {
|
744 |
const { width = 1000, height = 562 } = options;
|
745 |
|
746 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
747 |
|
|
|
748 |
const elementsHTML = slide.elements.map(element => {
|
749 |
const x = element.left || 0;
|
750 |
const y = element.top || 0;
|
751 |
const w = element.width || 100;
|
752 |
const h = element.height || 100;
|
|
|
|
|
|
|
|
|
753 |
|
754 |
if (element.type === 'text') {
|
755 |
const fontSize = element.fontSize || 16;
|
756 |
-
const fontFamily = element.fontName || 'Arial';
|
757 |
const color = element.defaultColor || element.color || '#000000';
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
758 |
const content = (element.content || '').replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
|
759 |
|
760 |
-
return
|
761 |
-
|
762 |
-
|
763 |
-
|
764 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
765 |
}
|
766 |
|
767 |
if (element.type === 'shape') {
|
@@ -774,25 +831,34 @@ function generateSlideSVG(slide, pptData, options = {}) {
|
|
774 |
const cy = y + h/2;
|
775 |
const rx = w/2;
|
776 |
const ry = h/2;
|
777 |
-
return `<ellipse cx="${cx}" cy="${cy}" rx="${rx}" ry="${ry}" fill="${fill}" stroke="${stroke}" stroke-width="${strokeWidth}"/>`;
|
778 |
}
|
779 |
|
780 |
-
|
|
|
|
|
|
|
|
|
|
|
781 |
}
|
782 |
|
783 |
if (element.type === 'image' && element.src) {
|
784 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
785 |
}
|
786 |
|
787 |
return '';
|
788 |
-
}).join('');
|
789 |
|
790 |
-
return `<svg width="${width}" height="${height}" xmlns="http://www.w3.org/2000/svg">
|
|
|
791 |
<rect width="100%" height="100%" fill="${backgroundStyle}"/>
|
792 |
<g>${elementsHTML}</g>
|
793 |
-
<text x="${width-10}" y="${height-10}" text-anchor="end" font-family="Arial" font-size="10" fill="#999" opacity="0.7">
|
794 |
-
${pptData.title || 'PPTist'} - Generated at ${new Date().toISOString()}
|
795 |
-
</text>
|
796 |
</svg>`;
|
797 |
}
|
798 |
|
|
|
739 |
}
|
740 |
});
|
741 |
|
742 |
+
// 辅助函数:生成SVG - 改进版,支持更精确的PPT渲染
|
743 |
function generateSlideSVG(slide, pptData, options = {}) {
|
744 |
const { width = 1000, height = 562 } = options;
|
745 |
|
746 |
+
// 处理背景
|
747 |
+
let backgroundStyle = '#ffffff';
|
748 |
+
if (slide.background) {
|
749 |
+
if (slide.background.type === 'solid') {
|
750 |
+
backgroundStyle = slide.background.color || '#ffffff';
|
751 |
+
} else if (slide.background.type === 'gradient' && slide.background.gradient) {
|
752 |
+
// SVG渐变处理
|
753 |
+
const colors = slide.background.gradient.colors || [];
|
754 |
+
if (colors.length > 0) {
|
755 |
+
backgroundStyle = colors[0].color || '#ffffff';
|
756 |
+
}
|
757 |
+
}
|
758 |
+
}
|
759 |
+
|
760 |
+
// 生成渐变定义(如果需要)
|
761 |
+
let gradientDefs = '';
|
762 |
+
if (slide.background?.type === 'gradient' && slide.background.gradient?.colors) {
|
763 |
+
const colors = slide.background.gradient.colors;
|
764 |
+
const gradientId = 'bg-gradient';
|
765 |
+
if (slide.background.gradient.type === 'linear') {
|
766 |
+
gradientDefs = `
|
767 |
+
<defs>
|
768 |
+
<linearGradient id="${gradientId}" x1="0%" y1="0%" x2="100%" y2="0%">
|
769 |
+
${colors.map((color, index) =>
|
770 |
+
`<stop offset="${(index / (colors.length - 1)) * 100}%" style="stop-color:${color.color};stop-opacity:1" />`
|
771 |
+
).join('')}
|
772 |
+
</linearGradient>
|
773 |
+
</defs>
|
774 |
+
`;
|
775 |
+
backgroundStyle = `url(#${gradientId})`;
|
776 |
+
}
|
777 |
+
}
|
778 |
|
779 |
+
// 渲染元素
|
780 |
const elementsHTML = slide.elements.map(element => {
|
781 |
const x = element.left || 0;
|
782 |
const y = element.top || 0;
|
783 |
const w = element.width || 100;
|
784 |
const h = element.height || 100;
|
785 |
+
const rotation = element.rotate || 0;
|
786 |
+
|
787 |
+
// 变换属性
|
788 |
+
const transform = rotation !== 0 ? `transform="rotate(${rotation} ${x + w/2} ${y + h/2})"` : '';
|
789 |
|
790 |
if (element.type === 'text') {
|
791 |
const fontSize = element.fontSize || 16;
|
792 |
+
const fontFamily = element.fontName || 'Arial, sans-serif';
|
793 |
const color = element.defaultColor || element.color || '#000000';
|
794 |
+
const fontWeight = element.bold ? 'bold' : 'normal';
|
795 |
+
const fontStyle = element.italic ? 'italic' : 'normal';
|
796 |
+
const textDecoration = element.underline ? 'underline' : 'none';
|
797 |
+
const textAnchor = element.align === 'center' ? 'middle' : element.align === 'right' ? 'end' : 'start';
|
798 |
+
|
799 |
+
// 计算文本位置
|
800 |
+
let textX = x + 10;
|
801 |
+
if (element.align === 'center') textX = x + w/2;
|
802 |
+
else if (element.align === 'right') textX = x + w - 10;
|
803 |
+
|
804 |
+
// 处理多行文本
|
805 |
const content = (element.content || '').replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
806 |
+
const lines = content.split('\n');
|
807 |
|
808 |
+
return `
|
809 |
+
<g ${transform}>
|
810 |
+
${lines.map((line, index) => `
|
811 |
+
<text x="${textX}" y="${y + fontSize + (index * fontSize * 1.2)}"
|
812 |
+
font-family="${fontFamily}"
|
813 |
+
font-size="${fontSize}"
|
814 |
+
fill="${color}"
|
815 |
+
font-weight="${fontWeight}"
|
816 |
+
font-style="${fontStyle}"
|
817 |
+
text-decoration="${textDecoration}"
|
818 |
+
text-anchor="${textAnchor}">${line}</text>
|
819 |
+
`).join('')}
|
820 |
+
</g>
|
821 |
+
`;
|
822 |
}
|
823 |
|
824 |
if (element.type === 'shape') {
|
|
|
831 |
const cy = y + h/2;
|
832 |
const rx = w/2;
|
833 |
const ry = h/2;
|
834 |
+
return `<ellipse cx="${cx}" cy="${cy}" rx="${rx}" ry="${ry}" fill="${fill}" stroke="${stroke}" stroke-width="${strokeWidth}" ${transform}/>`;
|
835 |
}
|
836 |
|
837 |
+
// 默认矩形
|
838 |
+
const borderRadius = element.borderRadius || 0;
|
839 |
+
if (borderRadius > 0) {
|
840 |
+
return `<rect x="${x}" y="${y}" width="${w}" height="${h}" rx="${borderRadius}" ry="${borderRadius}" fill="${fill}" stroke="${stroke}" stroke-width="${strokeWidth}" ${transform}/>`;
|
841 |
+
}
|
842 |
+
return `<rect x="${x}" y="${y}" width="${w}" height="${h}" fill="${fill}" stroke="${stroke}" stroke-width="${strokeWidth}" ${transform}/>`;
|
843 |
}
|
844 |
|
845 |
if (element.type === 'image' && element.src) {
|
846 |
+
// 检查是否是base64图片
|
847 |
+
if (element.src.startsWith('data:image/')) {
|
848 |
+
return `<image x="${x}" y="${y}" width="${w}" height="${h}" href="${element.src}" ${transform}/>`;
|
849 |
+
}
|
850 |
+
// 外部图片 - 在SVG中可能有跨域问题,显示占位符
|
851 |
+
return `<rect x="${x}" y="${y}" width="${w}" height="${h}" fill="#f0f0f0" stroke="#ccc" stroke-width="1" ${transform}/>
|
852 |
+
<text x="${x + w/2}" y="${y + h/2}" text-anchor="middle" font-size="12" fill="#666">图片</text>`;
|
853 |
}
|
854 |
|
855 |
return '';
|
856 |
+
}).filter(Boolean).join('');
|
857 |
|
858 |
+
return `<svg width="${width}" height="${height}" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
859 |
+
${gradientDefs}
|
860 |
<rect width="100%" height="100%" fill="${backgroundStyle}"/>
|
861 |
<g>${elementsHTML}</g>
|
|
|
|
|
|
|
862 |
</svg>`;
|
863 |
}
|
864 |
|