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;
|