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 = ` ${colors.map((color, index) => `` ).join('')} `; 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, '&').replace(//g, '>'); const lines = content.split('\n'); return ` ${lines.map((line, index) => ` ${line} `).join('')} `; } 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 ``; } // 默认矩形 const borderRadius = element.borderRadius || 0; if (borderRadius > 0) { return ``; } return ``; } if (element.type === 'image' && element.src) { // 检查是否是base64图片 if (element.src.startsWith('data:image/')) { return ``; } // 外部图片 - 在SVG中可能有跨域问题,显示占位符 return ` 图片`; } return ''; }).filter(Boolean).join(''); return ` ${gradientDefs} ${elementsHTML} `; } // Generate error page for frontend display function generateErrorPage(title, message) { return ` ${title} - PPT导出
${title}
${message}
回到首页
`; } // 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 = ` Invalid Slide Index Slide index "${slideIndex}" is not a valid number ${new Date().toISOString()} `; 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 = ` Invalid PPT Data Slides data not found ${new Date().toISOString()} `; 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 = ` Invalid PPT Data Slides data not found ${new Date().toISOString()} `; 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 = ` PPT Not Found PPT ${pptId} does not exist ${new Date().toISOString()} `; 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 = ` Invalid PPT Data Slides data not found ${new Date().toISOString()} `; 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 = ` Invalid Slide Slide ${slideIndex} not found ${new Date().toISOString()} `; 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 = ` Generation Error Failed to generate image ${new Date().toISOString()} `; 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 = ` Invalid PPT Data Slides data not found ${new Date().toISOString()} `; 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 = ` Invalid PPT Data Slides data not found ${new Date().toISOString()} `; 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 = ` PPT Not Found PPT ${pptId} does not exist `; 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 = ` Invalid PPT Data Slides data not found ${new Date().toISOString()} `; 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 = ` Invalid Slide Slide ${slideIndex} not found `; 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 = ` Generation Error Failed to generate image: ${error.message} `; 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;