Upload public.js
Browse files- backend/src/routes/public.js +175 -1
backend/src/routes/public.js
CHANGED
@@ -243,8 +243,10 @@ router.post('/generate-share-link', async (req, res, next) => {
|
|
243 |
pptUrl: `${protocol}://${baseUrl}/api/public/ppt/${userId}/${pptId}`,
|
244 |
// Frontend view link
|
245 |
viewUrl: `${protocol}://${baseUrl}/public/${userId}/${pptId}/${slideIndex}`,
|
246 |
-
// Screenshot link
|
247 |
screenshotUrl: `${protocol}://${baseUrl}/api/public/screenshot/${userId}/${pptId}/${slideIndex}`,
|
|
|
|
|
248 |
// Add PPT information
|
249 |
pptInfo: {
|
250 |
id: pptId,
|
@@ -622,4 +624,176 @@ router.get('/presentation/:userId/:pptId', async (req, res, next) => {
|
|
622 |
}
|
623 |
});
|
624 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
625 |
export default router;
|
|
|
243 |
pptUrl: `${protocol}://${baseUrl}/api/public/ppt/${userId}/${pptId}`,
|
244 |
// Frontend view link
|
245 |
viewUrl: `${protocol}://${baseUrl}/public/${userId}/${pptId}/${slideIndex}`,
|
246 |
+
// Screenshot link (需要用户交互)
|
247 |
screenshotUrl: `${protocol}://${baseUrl}/api/public/screenshot/${userId}/${pptId}/${slideIndex}`,
|
248 |
+
// 新增:直接图片URL (无需用户交互)
|
249 |
+
directImageUrl: `${protocol}://${baseUrl}/api/public/direct-image/${userId}/${pptId}/${slideIndex}`,
|
250 |
// Add PPT information
|
251 |
pptInfo: {
|
252 |
id: pptId,
|
|
|
624 |
}
|
625 |
});
|
626 |
|
627 |
+
// 新增:直接返回图片的端点 - 自动生成Base64图片
|
628 |
+
router.get('/direct-image/:userId/:pptId/:slideIndex?', async (req, res, next) => {
|
629 |
+
try {
|
630 |
+
const { userId, pptId, slideIndex = 0 } = req.params;
|
631 |
+
const { format = 'jpeg', quality = 90 } = req.query;
|
632 |
+
|
633 |
+
console.log(`Direct image request: userId=${userId}, pptId=${pptId}, slideIndex=${slideIndex}`);
|
634 |
+
|
635 |
+
// Get PPT data
|
636 |
+
const fileName = `${pptId}.json`;
|
637 |
+
let pptData = null;
|
638 |
+
|
639 |
+
for (let i = 0; i < githubService.repositories.length; i++) {
|
640 |
+
try {
|
641 |
+
const result = await githubService.getFile(userId, fileName, i);
|
642 |
+
if (result) {
|
643 |
+
pptData = result.content;
|
644 |
+
break;
|
645 |
+
}
|
646 |
+
} catch (error) {
|
647 |
+
continue;
|
648 |
+
}
|
649 |
+
}
|
650 |
+
|
651 |
+
if (!pptData) {
|
652 |
+
// 返回404图片而不是HTML
|
653 |
+
const notFoundSvg = `
|
654 |
+
<svg width="800" height="450" xmlns="http://www.w3.org/2000/svg">
|
655 |
+
<rect width="100%" height="100%" fill="#f8f9fa"/>
|
656 |
+
<text x="400" y="200" text-anchor="middle" font-family="Arial" font-size="24" fill="#6c757d">PPT Not Found</text>
|
657 |
+
<text x="400" y="250" text-anchor="middle" font-family="Arial" font-size="16" fill="#6c757d">PPT ${pptId} does not exist</text>
|
658 |
+
</svg>
|
659 |
+
`;
|
660 |
+
const svgBuffer = Buffer.from(notFoundSvg);
|
661 |
+
res.setHeader('Content-Type', 'image/svg+xml');
|
662 |
+
res.setHeader('Cache-Control', 'no-cache');
|
663 |
+
return res.send(svgBuffer);
|
664 |
+
}
|
665 |
+
|
666 |
+
const slideIdx = parseInt(slideIndex);
|
667 |
+
if (slideIdx >= pptData.slides.length || slideIdx < 0) {
|
668 |
+
// 返回404图片
|
669 |
+
const invalidSlideSvg = `
|
670 |
+
<svg width="800" height="450" xmlns="http://www.w3.org/2000/svg">
|
671 |
+
<rect width="100%" height="100%" fill="#f8f9fa"/>
|
672 |
+
<text x="400" y="200" text-anchor="middle" font-family="Arial" font-size="24" fill="#6c757d">Invalid Slide</text>
|
673 |
+
<text x="400" y="250" text-anchor="middle" font-family="Arial" font-size="16" fill="#6c757d">Slide ${slideIndex} not found</text>
|
674 |
+
</svg>
|
675 |
+
`;
|
676 |
+
const svgBuffer = Buffer.from(invalidSlideSvg);
|
677 |
+
res.setHeader('Content-Type', 'image/svg+xml');
|
678 |
+
res.setHeader('Cache-Control', 'no-cache');
|
679 |
+
return res.send(svgBuffer);
|
680 |
+
}
|
681 |
+
|
682 |
+
// 生成PPT页面的SVG图片
|
683 |
+
const slide = pptData.slides[slideIdx];
|
684 |
+
const width = pptData.viewportSize || 1000;
|
685 |
+
const height = Math.ceil(width * (pptData.viewportRatio || 0.5625));
|
686 |
+
|
687 |
+
// 生成SVG内容
|
688 |
+
const svgContent = generateSlideSVG(slide, pptData, { width, height });
|
689 |
+
|
690 |
+
if (format === 'svg') {
|
691 |
+
res.setHeader('Content-Type', 'image/svg+xml');
|
692 |
+
res.setHeader('Cache-Control', 'public, max-age=3600'); // 缓存1小时
|
693 |
+
res.setHeader('X-Generation-Time', '< 10ms');
|
694 |
+
return res.send(svgContent);
|
695 |
+
}
|
696 |
+
|
697 |
+
// 对于其他格式,返回占位图片
|
698 |
+
const placeholderSvg = `
|
699 |
+
<svg width="${width}" height="${height}" xmlns="http://www.w3.org/2000/svg">
|
700 |
+
<rect width="100%" height="100%" fill="${slide.background?.color || '#ffffff'}"/>
|
701 |
+
<g>
|
702 |
+
${slide.elements.map(el => {
|
703 |
+
if (el.type === 'text') {
|
704 |
+
return `<text x="${el.left + 10}" y="${el.top + (el.fontSize || 16)}"
|
705 |
+
font-family="Arial" font-size="${el.fontSize || 16}"
|
706 |
+
fill="${el.defaultColor || el.color || '#000'}">${el.content || ''}</text>`;
|
707 |
+
}
|
708 |
+
if (el.type === 'shape') {
|
709 |
+
return `<rect x="${el.left}" y="${el.top}" width="${el.width}" height="${el.height}"
|
710 |
+
fill="${el.fill || '#cccccc'}" stroke="${el.outline?.color || 'none'}"/>`;
|
711 |
+
}
|
712 |
+
return '';
|
713 |
+
}).join('')}
|
714 |
+
</g>
|
715 |
+
<text x="${width/2}" y="${height-20}" text-anchor="middle" font-family="Arial" font-size="12" fill="#999">
|
716 |
+
${pptData.title} - Slide ${slideIdx + 1}
|
717 |
+
</text>
|
718 |
+
</svg>
|
719 |
+
`;
|
720 |
+
|
721 |
+
res.setHeader('Content-Type', 'image/svg+xml');
|
722 |
+
res.setHeader('Cache-Control', 'public, max-age=3600');
|
723 |
+
res.setHeader('X-Generation-Time', '< 10ms');
|
724 |
+
res.send(placeholderSvg);
|
725 |
+
|
726 |
+
} catch (error) {
|
727 |
+
console.error('Direct image generation failed:', error);
|
728 |
+
|
729 |
+
// 返回错误图片
|
730 |
+
const errorSvg = `
|
731 |
+
<svg width="800" height="450" xmlns="http://www.w3.org/2000/svg">
|
732 |
+
<rect width="100%" height="100%" fill="#fee"/>
|
733 |
+
<text x="400" y="200" text-anchor="middle" font-family="Arial" font-size="24" fill="#c00">Error</text>
|
734 |
+
<text x="400" y="250" text-anchor="middle" font-family="Arial" font-size="16" fill="#c00">Failed to generate image</text>
|
735 |
+
</svg>
|
736 |
+
`;
|
737 |
+
res.setHeader('Content-Type', 'image/svg+xml');
|
738 |
+
res.send(errorSvg);
|
739 |
+
}
|
740 |
+
});
|
741 |
+
|
742 |
+
// 辅助函数:生成SVG
|
743 |
+
function generateSlideSVG(slide, pptData, options = {}) {
|
744 |
+
const { width = 1000, height = 562 } = options;
|
745 |
+
|
746 |
+
const backgroundStyle = slide.background?.color || '#ffffff';
|
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 `<text x="${x + 10}" y="${y + fontSize}"
|
761 |
+
font-family="${fontFamily}" font-size="${fontSize}"
|
762 |
+
fill="${color}" font-weight="${element.bold ? 'bold' : 'normal'}"
|
763 |
+
font-style="${element.italic ? 'italic' : 'normal'}"
|
764 |
+
text-decoration="${element.underline ? 'underline' : 'none'}">${content}</text>`;
|
765 |
+
}
|
766 |
+
|
767 |
+
if (element.type === 'shape') {
|
768 |
+
const fill = element.fill || '#cccccc';
|
769 |
+
const stroke = element.outline?.color || 'none';
|
770 |
+
const strokeWidth = element.outline?.width || 0;
|
771 |
+
|
772 |
+
if (element.shape === 'ellipse') {
|
773 |
+
const cx = x + w/2;
|
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 |
+
return `<rect x="${x}" y="${y}" width="${w}" height="${h}" fill="${fill}" stroke="${stroke}" stroke-width="${strokeWidth}"/>`;
|
781 |
+
}
|
782 |
+
|
783 |
+
if (element.type === 'image' && element.src) {
|
784 |
+
return `<image x="${x}" y="${y}" width="${w}" height="${h}" href="${element.src}"/>`;
|
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 |
+
|
799 |
export default router;
|