CatPtain's picture
Upload public.js
773dbcc verified
import express from 'express';
import githubService from '../services/githubService.js';
import screenshotService from '../services/screenshotService.js';
// 修正导入路径:从 backend/src 向上两级到 app,再进入 shared
import { generateSlideHTML, generateExportPage, exportPPTToJSON, generateHTMLPresentation } from '../../../shared/export-utils.js';
const router = express.Router();
// 前端导出图片功能已移除,直接使用screenshotService
// 辅助函数:生成SVG - 改进版,支持更精确的PPT渲染
function generateSlideSVG(slide, pptData, options = {}) {
const { width = 1000, height = 562 } = options;
// 处理背景
let backgroundStyle = '#ffffff';
if (slide.background) {
if (slide.background.type === 'solid') {
backgroundStyle = slide.background.color || '#ffffff';
} else if (slide.background.type === 'gradient' && slide.background.gradient) {
// SVG渐变处理
const colors = slide.background.gradient.colors || [];
if (colors.length > 0) {
backgroundStyle = colors[0].color || '#ffffff';
}
}
}
// 生成渐变定义(如果需要)
let gradientDefs = '';
if (slide.background?.type === 'gradient' && slide.background.gradient?.colors) {
const colors = slide.background.gradient.colors;
const gradientId = 'bg-gradient';
if (slide.background.gradient.type === 'linear') {
gradientDefs = `
<defs>
<linearGradient id="${gradientId}" x1="0%" y1="0%" x2="100%" y2="0%">
${colors.map((color, index) =>
`<stop offset="${(index / (colors.length - 1)) * 100}%" style="stop-color:${color.color};stop-opacity:1" />`
).join('')}
</linearGradient>
</defs>
`;
backgroundStyle = `url(#${gradientId})`;
}
}
// 渲染元素
const elementsHTML = slide.elements.map(element => {
const x = element.left || 0;
const y = element.top || 0;
const w = element.width || 100;
const h = element.height || 100;
const rotation = element.rotate || 0;
// 变换属性
const transform = rotation !== 0 ? `transform="rotate(${rotation} ${x + w/2} ${y + h/2})"` : '';
if (element.type === 'text') {
const fontSize = element.fontSize || 16;
const fontFamily = element.fontName || 'Arial, sans-serif';
const color = element.defaultColor || element.color || '#000000';
const fontWeight = element.bold ? 'bold' : 'normal';
const fontStyle = element.italic ? 'italic' : 'normal';
const textDecoration = element.underline ? 'underline' : 'none';
const textAnchor = element.align === 'center' ? 'middle' : element.align === 'right' ? 'end' : 'start';
// 计算文本位置
let textX = x + 10;
if (element.align === 'center') textX = x + w/2;
else if (element.align === 'right') textX = x + w - 10;
// 处理多行文本
const content = (element.content || '').replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
const lines = content.split('\n');
return `
<g ${transform}>
${lines.map((line, index) => `
<text x="${textX}" y="${y + fontSize + (index * fontSize * 1.2)}"
font-family="${fontFamily}"
font-size="${fontSize}"
fill="${color}"
font-weight="${fontWeight}"
font-style="${fontStyle}"
text-decoration="${textDecoration}"
text-anchor="${textAnchor}">${line}</text>
`).join('')}
</g>
`;
}
if (element.type === 'shape') {
const fill = element.fill || '#cccccc';
const stroke = element.outline?.color || 'none';
const strokeWidth = element.outline?.width || 0;
if (element.shape === 'ellipse') {
const cx = x + w/2;
const cy = y + h/2;
const rx = w/2;
const ry = h/2;
return `<ellipse cx="${cx}" cy="${cy}" rx="${rx}" ry="${ry}" fill="${fill}" stroke="${stroke}" stroke-width="${strokeWidth}" ${transform}/>`;
}
// 默认矩形
const borderRadius = element.borderRadius || 0;
if (borderRadius > 0) {
return `<rect x="${x}" y="${y}" width="${w}" height="${h}" rx="${borderRadius}" ry="${borderRadius}" fill="${fill}" stroke="${stroke}" stroke-width="${strokeWidth}" ${transform}/>`;
}
return `<rect x="${x}" y="${y}" width="${w}" height="${h}" fill="${fill}" stroke="${stroke}" stroke-width="${strokeWidth}" ${transform}/>`;
}
if (element.type === 'image' && element.src) {
// 检查是否是base64图片
if (element.src.startsWith('data:image/')) {
return `<image x="${x}" y="${y}" width="${w}" height="${h}" href="${element.src}" ${transform}/>`;
}
// 外部图片 - 在SVG中可能有跨域问题,显示占位符
return `<rect x="${x}" y="${y}" width="${w}" height="${h}" fill="#f0f0f0" stroke="#ccc" stroke-width="1" ${transform}/>
<text x="${x + w/2}" y="${y + h/2}" text-anchor="middle" font-size="12" fill="#666">图片</text>`;
}
return '';
}).filter(Boolean).join('');
return `<svg width="${width}" height="${height}" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
${gradientDefs}
<rect width="100%" height="100%" fill="${backgroundStyle}"/>
<g>${elementsHTML}</g>
</svg>`;
}
// Generate error page for frontend display
function generateErrorPage(title, message) {
return `
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>${title} - PPT导出</title>
<style>
body {
font-family: 'Microsoft YaHei', sans-serif;
margin: 0;
padding: 0;
background: linear-gradient(135deg, #ff6b6b 0%, #feca57 100%);
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
}
.error-container {
background: white;
padding: 40px;
border-radius: 15px;
box-shadow: 0 20px 40px rgba(0,0,0,0.1);
text-align: center;
max-width: 500px;
margin: 20px;
}
.error-icon { font-size: 64px; margin-bottom: 20px; }
.error-title { font-size: 24px; font-weight: bold; color: #333; margin-bottom: 15px; }
.error-message { font-size: 16px; color: #666; line-height: 1.6; margin-bottom: 30px; }
.error-actions { display: flex; gap: 15px; justify-content: center; }
.btn {
padding: 12px 24px;
border: none;
border-radius: 5px;
font-size: 16px;
cursor: pointer;
text-decoration: none;
display: inline-block;
transition: all 0.3s;
}
.btn-primary { background: #4CAF50; color: white; }
.btn-primary:hover { background: #45a049; }
.btn-secondary { background: #f8f9fa; color: #333; border: 1px solid #ddd; }
.btn-secondary:hover { background: #e9ecef; }
</style>
</head>
<body>
<div class="error-container">
<div class="error-icon">❌</div>
<div class="error-title">${title}</div>
<div class="error-message">${message}</div>
<div class="error-actions">
<button class="btn btn-secondary" onclick="history.back()">返回上页</button>
<a href="/" class="btn btn-primary">回到首页</a>
</div>
</div>
</body>
</html>
`;
}
// Publicly access PPT page - return HTML page
router.get('/view/:userId/:pptId/:slideIndex?', async (req, res, next) => {
try {
const { userId, pptId, slideIndex = 0 } = req.params;
const querySlideIndex = req.query.slide ? parseInt(req.query.slide) : parseInt(slideIndex);
const fileName = `${pptId}.json`;
let pptData = null;
// Try all GitHub repositories
for (let i = 0; i < githubService.repositories.length; i++) {
try {
const result = await githubService.getFile(userId, fileName, i);
if (result) {
pptData = result.content;
break;
}
} catch (error) {
continue;
}
}
if (!pptData) {
return res.status(404).send(generateErrorPage('PPT Not Found', 'Please check if the link is correct'));
}
const slideIdx = querySlideIndex;
if (slideIdx >= pptData.slides.length || slideIdx < 0) {
return res.status(404).send(generateErrorPage('Slide Not Found', 'Please check if the slide index is correct'));
}
// 使用共享模块生成HTML
const htmlContent = generateSlideHTML(pptData, slideIdx, { format: 'view' });
res.setHeader('Content-Type', 'text/html; charset=utf-8');
res.send(htmlContent);
} catch (error) {
next(error);
}
});
// API endpoint: Get PPT data and specified slide (JSON format)
router.get('/api-view/:userId/:pptId/:slideIndex?', async (req, res, next) => {
try {
const { userId, pptId, slideIndex = 0 } = req.params;
const fileName = `${pptId}.json`;
let pptData = null;
// Try all GitHub repositories
for (let i = 0; i < githubService.repositories.length; i++) {
try {
const result = await githubService.getFile(userId, fileName, i);
if (result) {
pptData = result.content;
break;
}
} catch (error) {
continue;
}
}
if (!pptData) {
return res.status(404).json({ error: 'PPT not found' });
}
const slideIdx = parseInt(slideIndex);
// 验证 slideIndex 是否为有效数字
if (isNaN(slideIdx)) {
console.log(`❌ Invalid slide index format: ${slideIndex}`);
if (format === 'svg') {
const invalidIndexSvg = `<svg width="800" height="450" xmlns="http://www.w3.org/2000/svg">
<rect width="100%" height="100%" fill="#f8f9fa"/>
<text x="400" y="200" text-anchor="middle" font-family="Arial, sans-serif" font-size="24" fill="#6c757d">Invalid Slide Index</text>
<text x="400" y="250" text-anchor="middle" font-family="Arial, sans-serif" font-size="16" fill="#6c757d">Slide index "${slideIndex}" is not a valid number</text>
<text x="400" y="300" text-anchor="middle" font-family="Arial, sans-serif" font-size="12" fill="#999">${new Date().toISOString()}</text>
</svg>`;
res.setHeader('Content-Type', 'image/svg+xml; charset=utf-8');
return res.send(invalidIndexSvg);
} else {
const errorPage = generateErrorPage('Invalid Slide Index', `Slide index "${slideIndex}" is not a valid number.`);
res.setHeader('Content-Type', 'text/html; charset=utf-8');
return res.status(400).send(errorPage);
}
}
// 安全检查:确保 slides 数组存在
if (!pptData.slides || !Array.isArray(pptData.slides)) {
console.log(`❌ Invalid PPT data: slides array not found`);
if (format === 'svg') {
const invalidDataSvg = `<svg width="800" height="450" xmlns="http://www.w3.org/2000/svg">
<rect width="100%" height="100%" fill="#f8f9fa"/>
<text x="400" y="200" text-anchor="middle" font-family="Arial, sans-serif" font-size="24" fill="#6c757d">Invalid PPT Data</text>
<text x="400" y="250" text-anchor="middle" font-family="Arial, sans-serif" font-size="16" fill="#6c757d">Slides data not found</text>
<text x="400" y="300" text-anchor="middle" font-family="Arial, sans-serif" font-size="12" fill="#999">${new Date().toISOString()}</text>
</svg>`;
res.setHeader('Content-Type', 'image/svg+xml; charset=utf-8');
return res.send(invalidDataSvg);
} else {
const errorPage = generateErrorPage('Invalid PPT Data', 'PPT slides data not found or corrupted.');
res.setHeader('Content-Type', 'text/html; charset=utf-8');
return res.status(400).send(errorPage);
}
}
if (slideIdx >= pptData.slides.length || slideIdx < 0) {
return res.status(404).json({ error: 'Slide not found' });
}
// Return PPT data and specified slide
res.json({
id: pptData.id,
title: pptData.title,
theme: pptData.theme,
currentSlide: pptData.slides[slideIdx],
slideIndex: slideIdx,
totalSlides: pptData.slides.length,
isPublicView: true
});
} catch (error) {
next(error);
}
});
// Get complete PPT data (read-only mode)
router.get('/ppt/:userId/:pptId', async (req, res, next) => {
try {
const { userId, pptId } = req.params;
const fileName = `${pptId}.json`;
let pptData = null;
// Try all GitHub repositories
for (let i = 0; i < githubService.repositories.length; i++) {
try {
const result = await githubService.getFile(userId, fileName, i);
if (result) {
pptData = result.content;
break;
}
} catch (error) {
continue;
}
}
if (!pptData) {
return res.status(404).json({ error: 'PPT not found' });
}
// Return read-only version of PPT data
res.json({
...pptData,
isPublicView: true,
readOnly: true
});
} catch (error) {
next(error);
}
});
// Generate PPT share link
router.post('/generate-share-link', async (req, res, next) => {
try {
const { userId, pptId, slideIndex = 0 } = req.body;
if (!userId || !pptId) {
return res.status(400).json({ error: 'User ID and PPT ID are required' });
}
console.log(`Generating share link for PPT: ${pptId}, User: ${userId}`);
// Verify if PPT exists
const fileName = `${pptId}.json`;
let pptExists = false;
let pptData = null;
console.log(`Checking ${githubService.repositories.length} GitHub repositories...`);
for (let i = 0; i < githubService.repositories.length; i++) {
try {
console.log(`Checking repository ${i}: ${githubService.repositories[i]}`);
const result = await githubService.getFile(userId, fileName, i);
if (result) {
pptExists = true;
pptData = result.content;
console.log(`PPT found in repository ${i}`);
break;
}
} catch (error) {
console.log(`PPT not found in repository ${i}: ${error.message}`);
continue;
}
}
if (!pptExists) {
return res.status(404).json({
error: 'PPT not found',
details: `PPT ${pptId} not found for user ${userId}`,
searchedLocations: 'GitHub repositories'
});
}
const baseUrl = process.env.PUBLIC_URL || req.get('host');
// 修复协议判断:localhost 始终使用 http,生产环境使用 https
const protocol = baseUrl.includes('localhost') ? 'http' : 'https';
const shareLinks = {
// Single page share link
slideUrl: `${protocol}://${baseUrl}/api/public/view/${userId}/${pptId}/${slideIndex}`,
// Complete PPT share link
pptUrl: `${protocol}://${baseUrl}/api/public/ppt/${userId}/${pptId}`,
// Frontend view link
viewUrl: `${protocol}://${baseUrl}/public/${userId}/${pptId}/${slideIndex}`,
// Screenshot link (直接返回图片)
screenshotUrl: `${protocol}://${baseUrl}/api/public/image/${userId}/${pptId}/${slideIndex}?format=jpg&quality=90`,
// 新增:直接图片URL (无需用户交互)
directImageUrl: `${protocol}://${baseUrl}/api/public/direct-image/${userId}/${pptId}/${slideIndex}`,
// Add PPT information
pptInfo: {
id: pptId,
title: pptData?.title || 'Unknown Title',
slideCount: pptData?.slides?.length || 0
}
};
console.log('Share links generated successfully:', shareLinks);
res.json(shareLinks);
} catch (error) {
next(error);
}
});
// Screenshot endpoint - Frontend Export Strategy
router.get('/screenshot/:userId/:pptId/:slideIndex?', async (req, res, next) => {
try {
const { userId, pptId, slideIndex = 0 } = req.params;
const { format = 'jpeg', quality = 90, strategy = 'frontend-first', returnHtml = 'true' } = req.query;
console.log(`Screenshot request: userId=${userId}, pptId=${pptId}, slideIndex=${slideIndex}, strategy=${strategy}, returnHtml=${returnHtml}`);
// Get PPT data
const fileName = `${pptId}.json`;
let pptData = null;
// Try all GitHub repositories
for (let i = 0; i < githubService.repositories.length; i++) {
try {
const result = await githubService.getFile(userId, fileName, i);
if (result) {
pptData = result.content;
break;
}
} catch (error) {
continue;
}
}
if (!pptData) {
return res.status(404).json({ error: 'PPT not found' });
}
// Check if slides array exists
if (!pptData.slides || !Array.isArray(pptData.slides)) {
return res.status(400).json({ error: 'Invalid PPT data: slides not found' });
}
const slideIdx = parseInt(slideIndex);
// 安全检查:确保 slides 数组存在
if (!pptData.slides || !Array.isArray(pptData.slides)) {
console.log(`❌ Invalid PPT data: slides array not found`);
if (format === 'svg') {
const invalidDataSvg = `<svg width="800" height="450" xmlns="http://www.w3.org/2000/svg">
<rect width="100%" height="100%" fill="#f8f9fa"/>
<text x="400" y="200" text-anchor="middle" font-family="Arial, sans-serif" font-size="24" fill="#6c757d">Invalid PPT Data</text>
<text x="400" y="250" text-anchor="middle" font-family="Arial, sans-serif" font-size="16" fill="#6c757d">Slides data not found</text>
<text x="400" y="300" text-anchor="middle" font-family="Arial, sans-serif" font-size="12" fill="#999">${new Date().toISOString()}</text>
</svg>`;
res.setHeader('Content-Type', 'image/svg+xml; charset=utf-8');
return res.send(invalidDataSvg);
} else {
const errorPage = generateErrorPage('Invalid PPT Data', 'PPT slides data not found or corrupted.');
res.setHeader('Content-Type', 'text/html; charset=utf-8');
return res.status(400).send(errorPage);
}
}
if (slideIdx >= pptData.slides.length || slideIdx < 0) {
return res.status(404).json({ error: 'Invalid slide index' });
}
// Frontend Export Strategy - Return HTML page for client-side screenshot generation
if (strategy === 'frontend-first' && returnHtml !== 'false') {
console.log('Using frontend export strategy - returning HTML page');
// 使用共享模块生成导出页面
const htmlPage = generateExportPage(pptData, slideIdx, {
format,
quality: parseInt(quality),
autoDownload: req.query.autoDownload === 'true'
});
res.setHeader('Content-Type', 'text/html; charset=utf-8');
res.setHeader('X-Screenshot-Strategy', 'frontend-export');
res.setHeader('X-Generation-Time', '< 100ms');
return res.send(htmlPage);
}
// Fallback: Backend screenshot (if specifically requested)
console.log('Using backend screenshot for direct image return...');
// 使用共享模块生成HTML用于后端截图
const htmlContent = generateSlideHTML(pptData, slideIdx, { format: 'screenshot' });
try {
const screenshot = await screenshotService.generateScreenshot(htmlContent, {
format,
quality: parseInt(quality),
width: pptData.viewportSize || 1000,
height: Math.ceil((pptData.viewportSize || 1000) * (pptData.viewportRatio || 0.5625))
});
res.setHeader('Content-Type', `image/${format}`);
res.setHeader('X-Screenshot-Type', 'backend-generated');
res.setHeader('X-Generation-Time', '2-5s');
res.send(screenshot);
} catch (error) {
console.error('Backend screenshot failed:', error);
// Generate fallback image
const fallbackImage = screenshotService.generateFallbackImage(
pptData.viewportSize || 1000,
Math.ceil((pptData.viewportSize || 1000) * (pptData.viewportRatio || 0.5625))
);
res.setHeader('Content-Type', `image/${format}`);
res.setHeader('X-Screenshot-Type', 'fallback-generated');
res.setHeader('X-Generation-Time', '< 50ms');
res.send(fallbackImage);
}
} catch (error) {
next(error);
}
});
// 🔥 关键修改:image端点现在支持多种格式,包括SVG和JPG
router.get('/image/:userId/:pptId/:slideIndex?', async (req, res, next) => {
try {
const { userId, pptId, slideIndex = 0 } = req.params;
const { format = 'svg', quality = 90, width: requestWidth, height: requestHeight } = req.query;
console.log(`🔥 Image request: userId=${userId}, pptId=${pptId}, slideIndex=${slideIndex}, format=${format}`);
// Get PPT data
const fileName = `${pptId}.json`;
let pptData = null;
for (let i = 0; i < githubService.repositories.length; i++) {
try {
const result = await githubService.getFile(userId, fileName, i);
if (result) {
pptData = result.content;
console.log(`✅ PPT data found in repository ${i}`);
break;
}
} catch (error) {
continue;
}
}
if (!pptData) {
console.log(`❌ PPT not found: ${pptId}`);
if (format === 'svg') {
const notFoundSvg = `<svg width="800" height="450" xmlns="http://www.w3.org/2000/svg">
<rect width="100%" height="100%" fill="#f8f9fa"/>
<text x="400" y="200" text-anchor="middle" font-family="Arial, sans-serif" font-size="24" fill="#6c757d">PPT Not Found</text>
<text x="400" y="250" text-anchor="middle" font-family="Arial, sans-serif" font-size="16" fill="#6c757d">PPT ${pptId} does not exist</text>
<text x="400" y="300" text-anchor="middle" font-family="Arial, sans-serif" font-size="12" fill="#999">${new Date().toISOString()}</text>
</svg>`;
res.setHeader('Content-Type', 'image/svg+xml; charset=utf-8');
return res.send(notFoundSvg);
} else {
// 对于JPG/PNG格式,返回截图工具页面
const errorPage = generateErrorPage('PPT Not Found', `PPT ${pptId} not found. Please check the PPT ID.`);
res.setHeader('Content-Type', 'text/html; charset=utf-8');
return res.status(404).send(errorPage);
}
}
const slideIdx = parseInt(slideIndex);
// 安全检查:确保 slides 数组存在
if (!pptData.slides || !Array.isArray(pptData.slides)) {
console.log(`❌ Invalid PPT data: slides array not found`);
if (format === 'svg') {
const invalidDataSvg = `<svg width="800" height="450" xmlns="http://www.w3.org/2000/svg">
<rect width="100%" height="100%" fill="#f8f9fa"/>
<text x="400" y="200" text-anchor="middle" font-family="Arial, sans-serif" font-size="24" fill="#6c757d">Invalid PPT Data</text>
<text x="400" y="250" text-anchor="middle" font-family="Arial, sans-serif" font-size="16" fill="#6c757d">Slides data not found</text>
<text x="400" y="300" text-anchor="middle" font-family="Arial, sans-serif" font-size="12" fill="#999">${new Date().toISOString()}</text>
</svg>`;
res.setHeader('Content-Type', 'image/svg+xml; charset=utf-8');
return res.send(invalidDataSvg);
} else {
const errorPage = generateErrorPage('Invalid PPT Data', 'PPT slides data not found or corrupted.');
res.setHeader('Content-Type', 'text/html; charset=utf-8');
return res.status(400).send(errorPage);
}
}
if (slideIdx >= pptData.slides.length || slideIdx < 0) {
console.log(`❌ Invalid slide index: ${slideIndex}`);
if (format === 'svg') {
const invalidSlideSvg = `<svg width="800" height="450" xmlns="http://www.w3.org/2000/svg">
<rect width="100%" height="100%" fill="#f8f9fa"/>
<text x="400" y="200" text-anchor="middle" font-family="Arial, sans-serif" font-size="24" fill="#6c757d">Invalid Slide</text>
<text x="400" y="250" text-anchor="middle" font-family="Arial, sans-serif" font-size="16" fill="#6c757d">Slide ${slideIndex} not found</text>
<text x="400" y="300" text-anchor="middle" font-family="Arial, sans-serif" font-size="12" fill="#999">${new Date().toISOString()}</text>
</svg>`;
res.setHeader('Content-Type', 'image/svg+xml; charset=utf-8');
return res.send(invalidSlideSvg);
} else {
const errorPage = generateErrorPage('Invalid Slide', `Slide ${slideIndex} not found in this PPT.`);
res.setHeader('Content-Type', 'text/html; charset=utf-8');
return res.status(404).send(errorPage);
}
}
// 根据格式处理
if (format === 'svg') {
// SVG格式 - 直接生成并返回
console.log(`✅ Generating SVG for slide ${slideIdx}`);
const defaultWidth = pptData.viewportSize || 1000;
const defaultHeight = Math.ceil(defaultWidth * (pptData.viewportRatio || 0.5625));
const finalWidth = requestWidth ? parseInt(requestWidth) : defaultWidth;
const finalHeight = requestHeight ? parseInt(requestHeight) : defaultHeight;
const slide = pptData.slides[slideIdx];
const svgContent = generateSlideSVG(slide, pptData, {
width: finalWidth,
height: finalHeight
});
console.log(`🎉 SVG generated successfully, returning image data`);
res.setHeader('Content-Type', 'image/svg+xml; charset=utf-8');
res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate');
res.setHeader('Access-Control-Allow-Origin', '*');
res.setHeader('X-Force-Update', 'true');
res.setHeader('X-Generation-Time', '< 10ms');
res.send(svgContent);
} else {
// JPG/PNG格式 - 使用优化的截图方法
console.log(`✅ Generating ${format} image using optimized screenshot method`);
try {
// 使用共享模块生成HTML用于后端截图
const htmlContent = generateSlideHTML(pptData, slideIdx, { format: 'screenshot' });
const screenshot = await screenshotService.generateScreenshot(htmlContent, {
format: format === 'jpg' ? 'jpeg' : format,
quality: parseInt(quality),
width: pptData.viewportSize || 1000,
height: Math.ceil((pptData.viewportSize || 1000) * (pptData.viewportRatio || 0.5625))
});
res.setHeader('Content-Type', `image/${format === 'jpg' ? 'jpeg' : format}`);
res.setHeader('X-Screenshot-Type', 'backend-generated');
res.setHeader('X-Generation-Time', '1-3s'); // 优化后的时间估计
res.setHeader('Cache-Control', 'public, max-age=3600'); // 缓存1小时
res.send(screenshot);
} catch (error) {
console.error(`Backend ${format} generation failed:`, error);
// 生成后备图片
const fallbackImage = screenshotService.generateFallbackImage(
pptData.viewportSize || 1000,
Math.ceil((pptData.viewportSize || 1000) * (pptData.viewportRatio || 0.5625)),
`PPT ${format.toUpperCase()} Image`,
'Image generation failed'
);
res.setHeader('Content-Type', 'image/svg+xml; charset=utf-8');
res.setHeader('X-Screenshot-Type', 'fallback-generated');
res.setHeader('X-Generation-Time', '< 50ms');
res.send(fallbackImage);
}
}
} catch (error) {
console.error('❌ Image generation failed:', error);
// 根据格式返回错误
if (req.query.format === 'svg') {
res.setHeader('Content-Type', 'image/svg+xml; charset=utf-8');
res.setHeader('Access-Control-Allow-Origin', '*');
const errorSvg = `<svg width="800" height="450" xmlns="http://www.w3.org/2000/svg">
<rect width="100%" height="100%" fill="#ffe6e6"/>
<text x="400" y="200" text-anchor="middle" font-family="Arial, sans-serif" font-size="24" fill="#cc0000">Generation Error</text>
<text x="400" y="250" text-anchor="middle" font-family="Arial, sans-serif" font-size="16" fill="#cc0000">Failed to generate image</text>
<text x="400" y="300" text-anchor="middle" font-family="Arial, sans-serif" font-size="12" fill="#999">${new Date().toISOString()}</text>
</svg>`;
res.send(errorSvg);
} else {
const errorPage = generateErrorPage('Generation Error', 'Failed to generate the requested image format.');
res.setHeader('Content-Type', 'text/html; charset=utf-8');
res.status(500).send(errorPage);
}
}
});
// Screenshot data endpoint - For frontend direct rendering
router.get('/screenshot-data/:userId/:pptId/:slideIndex?', async (req, res, next) => {
try {
const { userId, pptId, slideIndex = 0 } = req.params;
const { format = 'jpeg', quality = 90 } = req.query;
console.log(`Screenshot data request: userId=${userId}, pptId=${pptId}, slideIndex=${slideIndex}`);
// Get PPT data
const fileName = `${pptId}.json`;
let pptData = null;
for (let i = 0; i < githubService.repositories.length; i++) {
try {
const result = await githubService.getFile(userId, fileName, i);
if (result) {
pptData = result.content;
break;
}
} catch (error) {
continue;
}
}
if (!pptData) {
return res.status(404).json({ error: 'PPT not found' });
}
// Check if slides array exists
if (!pptData.slides || !Array.isArray(pptData.slides)) {
return res.status(400).json({ error: 'Invalid PPT data: slides not found' });
}
const slideIdx = parseInt(slideIndex);
// 安全检查:确保 slides 数组存在
if (!pptData.slides || !Array.isArray(pptData.slides)) {
console.log(`❌ Invalid PPT data: slides array not found`);
if (format === 'svg') {
const invalidDataSvg = `<svg width="800" height="450" xmlns="http://www.w3.org/2000/svg">
<rect width="100%" height="100%" fill="#f8f9fa"/>
<text x="400" y="200" text-anchor="middle" font-family="Arial, sans-serif" font-size="24" fill="#6c757d">Invalid PPT Data</text>
<text x="400" y="250" text-anchor="middle" font-family="Arial, sans-serif" font-size="16" fill="#6c757d">Slides data not found</text>
<text x="400" y="300" text-anchor="middle" font-family="Arial, sans-serif" font-size="12" fill="#999">${new Date().toISOString()}</text>
</svg>`;
res.setHeader('Content-Type', 'image/svg+xml; charset=utf-8');
return res.send(invalidDataSvg);
} else {
const errorPage = generateErrorPage('Invalid PPT Data', 'PPT slides data not found or corrupted.');
res.setHeader('Content-Type', 'text/html; charset=utf-8');
return res.status(400).send(errorPage);
}
}
if (slideIdx >= pptData.slides.length || slideIdx < 0) {
return res.status(404).json({ error: 'Invalid slide index' });
}
// Return PPT data for frontend rendering
const responseData = {
pptData: {
id: pptData.id,
title: pptData.title,
theme: pptData.theme,
viewportSize: pptData.viewportSize,
viewportRatio: pptData.viewportRatio,
slide: pptData.slides[slideIdx]
},
slideIndex: slideIdx,
totalSlides: pptData.slides.length,
exportConfig: {
format,
quality: parseInt(quality),
width: pptData.viewportSize || 1000,
height: Math.ceil((pptData.viewportSize || 1000) * (pptData.viewportRatio || 0.5625))
},
strategy: 'frontend-direct-rendering',
timestamp: new Date().toISOString()
};
res.setHeader('X-Screenshot-Strategy', 'frontend-data-api');
res.json(responseData);
} catch (error) {
next(error);
}
});
// Screenshot health check
router.get('/screenshot-health', async (req, res, next) => {
try {
const status = {
status: 'healthy',
timestamp: new Date().toISOString(),
environment: {
nodeEnv: process.env.NODE_ENV,
isHuggingFace: process.env.SPACE_ID ? true : false,
platform: process.platform
},
screenshotService: {
available: true,
strategy: 'frontend-first',
backendFallback: false, // Disable backend browsers
screenshotSize: 0
}
};
// Test fallback image generation
try {
const testImage = screenshotService.generateFallbackImage(100, 100);
status.screenshotService.screenshotSize = testImage.length;
status.screenshotService.fallbackAvailable = true;
} catch (error) {
status.screenshotService.fallbackAvailable = false;
status.screenshotService.fallbackError = error.message;
}
res.json(status);
} catch (error) {
res.status(500).json({
status: 'error',
timestamp: new Date().toISOString(),
error: error.message
});
}
});
// 新增:截图工具页面端点(原来的image端点功能)
router.get('/screenshot-tool/:userId/:pptId/:slideIndex?', async (req, res, next) => {
try {
const { userId, pptId, slideIndex = 0 } = req.params;
const { format = 'jpeg', quality = 90 } = req.query;
console.log(`Screenshot tool request: userId=${userId}, pptId=${pptId}, slideIndex=${slideIndex}`);
// Get PPT data
const fileName = `${pptId}.json`;
let pptData = null;
for (let i = 0; i < githubService.repositories.length; i++) {
try {
const result = await githubService.getFile(userId, fileName, i);
if (result) {
pptData = result.content;
console.log(`✅ PPT data found in repository ${i}`);
break;
}
} catch (error) {
continue;
}
}
if (!pptData) {
const errorPage = generateErrorPage('PPT Not Found', `PPT ${pptId} not found for user ${userId}`);
res.setHeader('Content-Type', 'text/html; charset=utf-8');
return res.status(404).send(errorPage);
}
const slideIdx = parseInt(slideIndex);
// 安全检查:确保 slides 数组存在
if (!pptData.slides || !Array.isArray(pptData.slides)) {
console.log(`❌ Invalid PPT data: slides array not found`);
if (format === 'svg') {
const invalidDataSvg = `<svg width="800" height="450" xmlns="http://www.w3.org/2000/svg">
<rect width="100%" height="100%" fill="#f8f9fa"/>
<text x="400" y="200" text-anchor="middle" font-family="Arial, sans-serif" font-size="24" fill="#6c757d">Invalid PPT Data</text>
<text x="400" y="250" text-anchor="middle" font-family="Arial, sans-serif" font-size="16" fill="#6c757d">Slides data not found</text>
<text x="400" y="300" text-anchor="middle" font-family="Arial, sans-serif" font-size="12" fill="#999">${new Date().toISOString()}</text>
</svg>`;
res.setHeader('Content-Type', 'image/svg+xml; charset=utf-8');
return res.send(invalidDataSvg);
} else {
const errorPage = generateErrorPage('Invalid PPT Data', 'PPT slides data not found or corrupted.');
res.setHeader('Content-Type', 'text/html; charset=utf-8');
return res.status(400).send(errorPage);
}
}
if (slideIdx >= pptData.slides.length || slideIdx < 0) {
const errorPage = generateErrorPage('Invalid Slide', `Slide ${slideIndex} not found in PPT`);
res.setHeader('Content-Type', 'text/html; charset=utf-8');
return res.status(404).send(errorPage);
}
// 使用共享模块生成导出页面
const htmlPage = generateExportPage(pptData, slideIdx, {
format,
quality: parseInt(quality),
autoDownload: false // 不自动下载,需要用户手动操作
});
res.setHeader('Content-Type', 'text/html; charset=utf-8');
res.setHeader('X-Screenshot-Strategy', 'frontend-screenshot-tool');
res.setHeader('X-Generation-Time', '< 50ms');
res.send(htmlPage);
} catch (error) {
next(error);
}
});
// 新增:PPT JSON导出端点
router.get('/export-json/:userId/:pptId', async (req, res, next) => {
try {
const { userId, pptId } = req.params;
const fileName = `${pptId}.json`;
let pptData = null;
// Try all GitHub repositories
for (let i = 0; i < githubService.repositories.length; i++) {
try {
const result = await githubService.getFile(userId, fileName, i);
if (result) {
pptData = result.content;
break;
}
} catch (error) {
continue;
}
}
if (!pptData) {
return res.status(404).json({ error: 'PPT not found' });
}
// 使用共享模块导出JSON
const jsonData = exportPPTToJSON(pptData);
res.setHeader('Content-Type', 'application/json; charset=utf-8');
res.setHeader('Content-Disposition', `attachment; filename="${pptData.title || 'presentation'}.json"`);
res.json(jsonData);
} catch (error) {
next(error);
}
});
// 新增:PPT HTML演示文稿导出端点
router.get('/export-html/:userId/:pptId', async (req, res, next) => {
try {
const { userId, pptId } = req.params;
const { interactive = 'true', standalone = 'true', css = 'true' } = req.query;
const fileName = `${pptId}.json`;
let pptData = null;
// Try all GitHub repositories
for (let i = 0; i < githubService.repositories.length; i++) {
try {
const result = await githubService.getFile(userId, fileName, i);
if (result) {
pptData = result.content;
break;
}
} catch (error) {
continue;
}
}
if (!pptData) {
return res.status(404).json({ error: 'PPT not found' });
}
// 使用共享模块生成完整HTML演示文稿
const htmlContent = generateHTMLPresentation(pptData, {
includeInteractivity: interactive === 'true',
standalone: standalone === 'true',
includeCSS: css === 'true'
});
res.setHeader('Content-Type', 'text/html; charset=utf-8');
res.setHeader('Content-Disposition', `attachment; filename="${pptData.title || 'presentation'}.html"`);
res.send(htmlContent);
} catch (error) {
next(error);
}
});
// 新增:PPT完整演示文稿查看端点(在线查看,非下载)
router.get('/presentation/:userId/:pptId', async (req, res, next) => {
try {
const { userId, pptId } = req.params;
const fileName = `${pptId}.json`;
let pptData = null;
// Try all GitHub repositories
for (let i = 0; i < githubService.repositories.length; i++) {
try {
const result = await githubService.getFile(userId, fileName, i);
if (result) {
pptData = result.content;
break;
}
} catch (error) {
continue;
}
}
if (!pptData) {
return res.status(404).send(generateErrorPage('PPT Not Found', 'The presentation you are looking for does not exist.'));
}
// 使用共享模块生成完整HTML演示文稿用于在线查看
const htmlContent = generateHTMLPresentation(pptData, {
includeInteractivity: true,
standalone: true,
includeCSS: true
});
res.setHeader('Content-Type', 'text/html; charset=utf-8');
res.send(htmlContent);
} catch (error) {
next(error);
}
});
// 新增:直接返回图片的端点 - 必须放在其他路由之前避免被覆盖
router.get('/direct-image/:userId/:pptId/:slideIndex?', async (req, res, next) => {
try {
const { userId, pptId, slideIndex = 0 } = req.params;
const { format = 'svg', quality = 90, width: requestWidth, height: requestHeight } = req.query;
console.log(`🖼️ Direct image request: userId=${userId}, pptId=${pptId}, slideIndex=${slideIndex}, format=${format}`);
// Get PPT data
const fileName = `${pptId}.json`;
let pptData = null;
for (let i = 0; i < githubService.repositories.length; i++) {
try {
const result = await githubService.getFile(userId, fileName, i);
if (result) {
pptData = result.content;
console.log(`✅ PPT data found in repository ${i}`);
break;
}
} catch (error) {
continue;
}
}
if (!pptData) {
console.log(`❌ PPT not found: ${pptId}`);
// 返回404图片SVG
const notFoundSvg = `<svg width="800" height="450" xmlns="http://www.w3.org/2000/svg">
<rect width="100%" height="100%" fill="#f8f9fa"/>
<text x="400" y="200" text-anchor="middle" font-family="Arial, sans-serif" font-size="24" fill="#6c757d">PPT Not Found</text>
<text x="400" y="250" text-anchor="middle" font-family="Arial, sans-serif" font-size="16" fill="#6c757d">PPT ${pptId} does not exist</text>
</svg>`;
res.setHeader('Content-Type', 'image/svg+xml; charset=utf-8');
res.setHeader('Cache-Control', 'no-cache');
res.setHeader('Access-Control-Allow-Origin', '*');
return res.send(notFoundSvg);
}
const slideIdx = parseInt(slideIndex);
// 安全检查:确保 slides 数组存在
if (!pptData.slides || !Array.isArray(pptData.slides)) {
console.log(`❌ Invalid PPT data: slides array not found`);
if (format === 'svg') {
const invalidDataSvg = `<svg width="800" height="450" xmlns="http://www.w3.org/2000/svg">
<rect width="100%" height="100%" fill="#f8f9fa"/>
<text x="400" y="200" text-anchor="middle" font-family="Arial, sans-serif" font-size="24" fill="#6c757d">Invalid PPT Data</text>
<text x="400" y="250" text-anchor="middle" font-family="Arial, sans-serif" font-size="16" fill="#6c757d">Slides data not found</text>
<text x="400" y="300" text-anchor="middle" font-family="Arial, sans-serif" font-size="12" fill="#999">${new Date().toISOString()}</text>
</svg>`;
res.setHeader('Content-Type', 'image/svg+xml; charset=utf-8');
return res.send(invalidDataSvg);
} else {
const errorPage = generateErrorPage('Invalid PPT Data', 'PPT slides data not found or corrupted.');
res.setHeader('Content-Type', 'text/html; charset=utf-8');
return res.status(400).send(errorPage);
}
}
if (slideIdx >= pptData.slides.length || slideIdx < 0) {
console.log(`❌ Invalid slide index: ${slideIndex}`);
// 返回404幻灯片SVG
const invalidSlideSvg = `<svg width="800" height="450" xmlns="http://www.w3.org/2000/svg">
<rect width="100%" height="100%" fill="#f8f9fa"/>
<text x="400" y="200" text-anchor="middle" font-family="Arial, sans-serif" font-size="24" fill="#6c757d">Invalid Slide</text>
<text x="400" y="250" text-anchor="middle" font-family="Arial, sans-serif" font-size="16" fill="#6c757d">Slide ${slideIndex} not found</text>
</svg>`;
res.setHeader('Content-Type', 'image/svg+xml; charset=utf-8');
res.setHeader('Cache-Control', 'no-cache');
res.setHeader('Access-Control-Allow-Origin', '*');
return res.send(invalidSlideSvg);
}
console.log(`✅ Generating SVG for slide ${slideIdx}`);
// 计算图片尺寸
const defaultWidth = pptData.viewportSize || 1000;
const defaultHeight = Math.ceil(defaultWidth * (pptData.viewportRatio || 0.5625));
const finalWidth = requestWidth ? parseInt(requestWidth) : defaultWidth;
const finalHeight = requestHeight ? parseInt(requestHeight) : defaultHeight;
// 生成PPT幻灯片的SVG图片
const slide = pptData.slides[slideIdx];
const svgContent = generateSlideSVG(slide, pptData, {
width: finalWidth,
height: finalHeight
});
console.log(`✅ SVG generated successfully, size: ${svgContent.length} characters`);
// 设置正确的响应头,使其可以被其他网站作为图片引用
res.setHeader('Content-Type', 'image/svg+xml; charset=utf-8');
res.setHeader('Cache-Control', 'public, max-age=3600'); // 缓存1小时
res.setHeader('Access-Control-Allow-Origin', '*'); // 允许跨域引用
res.setHeader('X-Generation-Time', '< 10ms');
res.setHeader('X-Content-Source', 'direct-svg-generation');
res.setHeader('Content-Disposition', 'inline'); // 确保浏览器内联显示图片
// 直接返回SVG图片内容
res.send(svgContent);
} catch (error) {
console.error('❌ Direct image generation failed:', error);
// 返回错误图片SVG
const errorSvg = `<svg width="800" height="450" xmlns="http://www.w3.org/2000/svg">
<rect width="100%" height="100%" fill="#ffe6e6"/>
<text x="400" y="200" text-anchor="middle" font-family="Arial, sans-serif" font-size="24" fill="#cc0000">Generation Error</text>
<text x="400" y="250" text-anchor="middle" font-family="Arial, sans-serif" font-size="16" fill="#cc0000">Failed to generate image: ${error.message}</text>
</svg>`;
res.setHeader('Content-Type', 'image/svg+xml; charset=utf-8');
res.setHeader('Cache-Control', 'no-cache');
res.setHeader('Access-Control-Allow-Origin', '*');
res.send(errorSvg);
}
});
// 存储前端生成的图片数据
const imageCache = new Map();
// POST endpoint to receive frontend-generated images
router.post('/save-image/:userId/:pptId/:slideIndex', express.json({ limit: '10mb' }), async (req, res, next) => {
try {
const { userId, pptId, slideIndex } = req.params;
const { imageData, format = 'jpeg', quality = 90 } = req.body;
if (!imageData) {
return res.status(400).json({ error: 'Image data is required' });
}
// 生成唯一的图片ID
const imageId = `${userId}-${pptId}-${slideIndex}-${Date.now()}`;
// 存储图片数据到内存缓存
imageCache.set(imageId, {
data: imageData,
format,
quality,
userId,
pptId,
slideIndex,
timestamp: new Date().toISOString(),
contentType: `image/${format === 'jpg' ? 'jpeg' : format}`
});
// 设置过期时间(1小时后自动清理)
setTimeout(() => {
imageCache.delete(imageId);
}, 3600000);
// 返回图片访问URL
const baseUrl = req.protocol + '://' + req.get('host');
const imageUrl = `${baseUrl}/api/public/cached-image/${imageId}`;
console.log(`✅ Image saved with ID: ${imageId}`);
res.json({
success: true,
imageId,
imageUrl,
expiresAt: new Date(Date.now() + 3600000).toISOString()
});
} catch (error) {
console.error('❌ Failed to save image:', error);
next(error);
}
});
// GET endpoint to serve cached images
router.get('/cached-image/:imageId', async (req, res, next) => {
try {
const { imageId } = req.params;
const cachedImage = imageCache.get(imageId);
if (!cachedImage) {
return res.status(404).json({ error: 'Image not found or expired' });
}
// 设置响应头
res.setHeader('Content-Type', cachedImage.contentType);
res.setHeader('Cache-Control', 'public, max-age=3600');
res.setHeader('Access-Control-Allow-Origin', '*');
res.setHeader('X-Image-Source', 'frontend-generated');
res.setHeader('X-Generation-Time', cachedImage.timestamp);
// 如果是base64数据,需要转换
if (cachedImage.data.startsWith('data:')) {
const base64Data = cachedImage.data.split(',')[1];
const buffer = Buffer.from(base64Data, 'base64');
res.send(buffer);
} else {
res.send(cachedImage.data);
}
} catch (error) {
console.error('❌ Failed to serve cached image:', error);
next(error);
}
});
export default router;