import express from 'express'; import githubService from '../services/githubService.js'; import memoryStorageService from '../services/memoryStorageService.js'; import screenshotService from '../services/screenshotService.js'; const router = express.Router(); // 选择存储服务 const getStorageService = () => { // 如果GitHub Token未配置,使用内存存储 if (!process.env.GITHUB_TOKEN) { return memoryStorageService; } return githubService; }; // 生成HTML页面来显示PPT幻灯片 const generateSlideHTML = (pptData, slideIndex, title) => { const slide = pptData.slides[slideIndex]; const theme = pptData.theme || {}; // 精确计算PPT内容的真实尺寸 - 基于填满画布的图像元素推断 const calculatePptDimensions = (slide) => { console.log('开始计算PPT尺寸,当前slide元素数量:', slide.elements?.length || 0); // 1. 优先使用PPT数据中的viewportSize和viewportRatio(编辑器的真实尺寸) if (pptData.viewportSize && pptData.viewportRatio) { const result = { width: Math.ceil(pptData.viewportSize), height: Math.ceil(pptData.viewportSize * pptData.viewportRatio) }; console.log(`使用编辑器真实尺寸: ${result.width}x${result.height} (viewportSize: ${pptData.viewportSize}, viewportRatio: ${pptData.viewportRatio})`); return result; } // 2. 使用PPT数据中的预设尺寸 if (pptData.slideSize && pptData.slideSize.width && pptData.slideSize.height) { const result = { width: Math.ceil(pptData.slideSize.width), height: Math.ceil(pptData.slideSize.height) }; console.log(`使用PPT预设尺寸: ${result.width}x${result.height}`); return result; } // 3. 如果PPT数据中有width和height if (pptData.width && pptData.height) { const result = { width: Math.ceil(pptData.width), height: Math.ceil(pptData.height) }; console.log(`使用PPT根级尺寸: ${result.width}x${result.height}`); return result; } // 4. 使用slide级别的尺寸设置 if (slide.width && slide.height) { const result = { width: Math.ceil(slide.width), height: Math.ceil(slide.height) }; console.log(`使用slide尺寸: ${result.width}x${result.height}`); return result; } // 5. 基于填满画布的图像元素精确推断PPT尺寸 if (slide.elements && slide.elements.length > 0) { // 寻找可能填满画布的图像元素(left=0, top=0 或接近0,且尺寸较大) const candidateImages = slide.elements.filter(element => element.type === 'image' && Math.abs(element.left || 0) <= 10 && // 允许小偏差 Math.abs(element.top || 0) <= 10 && // 允许小偏差 (element.width || 0) >= 800 && // 宽度至少800 (element.height || 0) >= 400 // 高度至少400 ); if (candidateImages.length > 0) { // 使用面积最大的图像作为画布尺寸参考 const referenceImage = candidateImages.reduce((max, current) => { const maxArea = (max.width || 0) * (max.height || 0); const currentArea = (current.width || 0) * (current.height || 0); return currentArea > maxArea ? current : max; }); const result = { width: Math.ceil(referenceImage.width || 1000), height: Math.ceil(referenceImage.height || 562) }; console.log(`基于填满画布的图像推断PPT尺寸: ${result.width}x${result.height} (参考图像: ${referenceImage.id})`); console.log(`参考图像信息: left=${referenceImage.left}, top=${referenceImage.top}, width=${referenceImage.width}, height=${referenceImage.height}`); return result; } // 如果没有找到填满画布的图像,回退到边界计算 let maxRight = 0; let maxBottom = 0; let minLeft = Infinity; let minTop = Infinity; // 计算所有元素的实际边界 slide.elements.forEach(element => { const left = element.left || 0; const top = element.top || 0; const width = element.width || 0; const height = element.height || 0; const elementRight = left + width; const elementBottom = top + height; maxRight = Math.max(maxRight, elementRight); maxBottom = Math.max(maxBottom, elementBottom); minLeft = Math.min(minLeft, left); minTop = Math.min(minTop, top); console.log(`元素 ${element.id}: type=${element.type}, left=${left}, top=${top}, width=${width}, height=${height}, right=${elementRight}, bottom=${elementBottom}`); }); // 重置无限值 if (minLeft === Infinity) minLeft = 0; if (minTop === Infinity) minTop = 0; console.log(`元素边界: minLeft=${minLeft}, minTop=${minTop}, maxRight=${maxRight}, maxBottom=${maxBottom}`); // 根据实际元素分布确定画布尺寸 const canvasLeft = Math.min(0, minLeft); const canvasTop = Math.min(0, minTop); const canvasRight = Math.max(maxRight, 1000); // 最小宽度1000(基于GitHub数据) const canvasBottom = Math.max(maxBottom, 562); // 最小高度562(基于GitHub数据) // 计算最终的画布尺寸 let finalWidth = canvasRight - canvasLeft; let finalHeight = canvasBottom - canvasTop; // 基于从GitHub仓库观察到的实际比例进行智能调整 const currentRatio = finalWidth / finalHeight; console.log(`当前计算比例: ${currentRatio.toFixed(3)}`); // 从GitHub数据分析:1000x562.5 ≈ 1.78 (接近16:9的1.77) const targetRatio = 1000 / 562.5; // ≈ 1.778 // 如果比例接近观察到的标准比例,调整为精确比例 if (Math.abs(currentRatio - targetRatio) < 0.2) { if (finalWidth > finalHeight * targetRatio) { finalHeight = finalWidth / targetRatio; } else { finalWidth = finalHeight * targetRatio; } console.log(`调整为GitHub观察到的标准比例: ${targetRatio.toFixed(3)}`); } // 如果比例接近16:9,调整为标准16:9 else if (Math.abs(currentRatio - 16/9) < 0.1) { if (finalWidth > finalHeight * 16/9) { finalHeight = finalWidth / (16/9); } else { finalWidth = finalHeight * (16/9); } console.log(`调整为16:9比例`); } // 如果比例接近4:3,调整为标准4:3 else if (Math.abs(currentRatio - 4/3) < 0.1) { if (finalWidth > finalHeight * 4/3) { finalHeight = finalWidth / (4/3); } else { finalWidth = finalHeight * (4/3); } console.log(`调整为4:3比例`); } // 确保尺寸为偶数(避免半像素问题) finalWidth = Math.ceil(finalWidth / 2) * 2; finalHeight = Math.ceil(finalHeight / 2) * 2; const result = { width: finalWidth, height: finalHeight }; console.log(`根据元素边界计算最终尺寸: ${result.width}x${result.height}, 比例: ${(result.width/result.height).toFixed(3)}`); return result; } // 6. 如果没有元素,使用从GitHub数据分析得出的标准尺寸 const result = { width: 1000, height: 562 }; // 基于GitHub仓库中观察到的实际数据 console.log(`使用GitHub数据分析的默认尺寸: ${result.width}x${result.height} (1000x562, 比例≈1.78)`); return result; }; const pptDimensions = calculatePptDimensions(slide); // 渲染幻灯片元素 - 使用原始像素值,完全保真 const renderElements = (elements) => { if (!elements || elements.length === 0) return ''; return elements.map(element => { const style = ` position: absolute; left: ${element.left || 0}px; top: ${element.top || 0}px; width: ${element.width || 0}px; height: ${element.height || 0}px; transform: rotate(${element.rotate || 0}deg); z-index: ${element.zIndex || 1}; `; switch (element.type) { case 'text': return `
${element.content || ''}
`; case 'image': return `
`; case 'shape': const shapeStyle = element.fill ? `background-color: ${element.fill};` : ''; const borderStyle = element.outline ? `border: ${element.outline.width || 1}px ${element.outline.style || 'solid'} ${element.outline.color || '#000'};` : ''; return `
`; default: return `
`; } }).join(''); }; return ` ${title} - 第${slideIndex + 1}页
${renderElements(slide.elements)}
`; }; // 公开访问PPT页面 - 返回HTML页面 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`; const storageService = getStorageService(); let pptData = null; // 如果是GitHub服务,尝试所有仓库 if (storageService === githubService && storageService.repositories) { for (let i = 0; i < storageService.repositories.length; i++) { try { const result = await storageService.getFile(userId, fileName, i); if (result) { pptData = result.content; break; } } catch (error) { continue; } } } else { // 内存存储服务 const result = await storageService.getFile(userId, fileName); if (result) { pptData = result.content; } } if (!pptData) { return res.status(404).send(` PPT未找到

PPT未找到

请检查链接是否正确

`); } const slideIdx = querySlideIndex; if (slideIdx >= pptData.slides.length || slideIdx < 0) { return res.status(404).send(` 幻灯片未找到

幻灯片未找到

请检查幻灯片索引是否正确

`); } // 返回HTML页面 const htmlContent = generateSlideHTML(pptData, slideIdx, pptData.title); res.setHeader('Content-Type', 'text/html; charset=utf-8'); res.send(htmlContent); } catch (error) { next(error); } }); // API接口:获取PPT数据和指定幻灯片(JSON格式) router.get('/api-view/:userId/:pptId/:slideIndex?', async (req, res, next) => { try { const { userId, pptId, slideIndex = 0 } = req.params; const fileName = `${pptId}.json`; const storageService = getStorageService(); let pptData = null; // 如果是GitHub服务,尝试所有仓库 if (storageService === githubService && storageService.repositories) { for (let i = 0; i < storageService.repositories.length; i++) { try { const result = await storageService.getFile(userId, fileName, i); if (result) { pptData = result.content; break; } } catch (error) { continue; } } } else { // 内存存储服务 const result = await storageService.getFile(userId, fileName); if (result) { pptData = result.content; } } if (!pptData) { return res.status(404).json({ error: 'PPT not found' }); } const slideIdx = parseInt(slideIndex); if (slideIdx >= pptData.slides.length || slideIdx < 0) { return res.status(404).json({ error: 'Slide not found' }); } // 返回PPT数据和指定幻灯片 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); } }); // 获取完整PPT数据(只读模式) router.get('/ppt/:userId/:pptId', async (req, res, next) => { try { const { userId, pptId } = req.params; const fileName = `${pptId}.json`; const storageService = getStorageService(); let pptData = null; // 如果是GitHub服务,尝试所有仓库 if (storageService === githubService && storageService.repositories) { for (let i = 0; i < storageService.repositories.length; i++) { try { const result = await storageService.getFile(userId, fileName, i); if (result) { pptData = result.content; break; } } catch (error) { continue; } } } else { // 内存存储服务 const result = await storageService.getFile(userId, fileName); if (result) { pptData = result.content; } } if (!pptData) { return res.status(404).json({ error: 'PPT not found' }); } // 返回只读版本的PPT数据 res.json({ ...pptData, isPublicView: true, readOnly: true }); } catch (error) { next(error); } }); // 生成PPT分享链接 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}`); // 验证PPT是否存在 const fileName = `${pptId}.json`; const storageService = getStorageService(); let pptExists = false; let pptData = null; console.log(`Using storage service: ${storageService === githubService ? 'GitHub' : 'Memory'}`); // 如果是GitHub服务,尝试所有仓库 if (storageService === githubService && storageService.repositories) { console.log(`Checking ${storageService.repositories.length} GitHub repositories...`); for (let i = 0; i < storageService.repositories.length; i++) { try { console.log(`Checking repository ${i}: ${storageService.repositories[i]}`); const result = await storageService.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; } } } else { // 内存存储服务 console.log('Checking memory storage...'); try { const result = await storageService.getFile(userId, fileName); if (result) { pptExists = true; pptData = result.content; console.log('PPT found in memory storage'); } } catch (error) { console.log(`PPT not found in memory storage: ${error.message}`); } } if (!pptExists) { console.log('PPT not found in any storage location'); // 额外尝试:如果GitHub失败,检查memory storage作为fallback if (storageService === githubService) { console.log('GitHub lookup failed, trying memory storage fallback...'); try { const memoryResult = await memoryStorageService.getFile(userId, fileName); if (memoryResult) { pptExists = true; pptData = memoryResult.content; console.log('PPT found in memory storage fallback'); } } catch (memoryError) { console.log(`Memory storage fallback also failed: ${memoryError.message}`); } } } if (!pptExists) { return res.status(404).json({ error: 'PPT not found', details: `PPT ${pptId} not found for user ${userId}`, searchedLocations: storageService === githubService ? 'GitHub repositories and memory storage' : 'Memory storage only' }); } const baseUrl = process.env.PUBLIC_URL || req.get('host'); const protocol = process.env.NODE_ENV === 'production' ? 'https' : req.protocol; const shareLinks = { // 单页分享链接 slideUrl: `${protocol}://${baseUrl}/api/public/view/${userId}/${pptId}/${slideIndex}`, // 完整PPT分享链接 pptUrl: `${protocol}://${baseUrl}/api/public/ppt/${userId}/${pptId}`, // 前端查看链接 viewUrl: `${protocol}://${baseUrl}/public/${userId}/${pptId}/${slideIndex}`, // 新增:截图链接 screenshotUrl: `${protocol}://${baseUrl}/api/public/screenshot/${userId}/${pptId}/${slideIndex}`, // 添加PPT信息 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) { console.error('Share link generation error:', error); next(error); } }); // 截图功能 - 返回JPEG图片 router.get('/screenshot/:userId/:pptId/:slideIndex?', async (req, res, next) => { try { console.log('📸 Screenshot request received:', req.params); const { userId, pptId, slideIndex = 0 } = req.params; const slideIdx = parseInt(slideIndex); const fileName = `${pptId}.json`; const storageService = getStorageService(); console.log(`🎯 Generating screenshot for: ${userId}/${pptId}/${slideIdx}`); let pptData = null; // 获取PPT数据(复用现有逻辑) if (storageService === githubService && storageService.repositories) { console.log('📂 Checking GitHub repositories...'); for (let i = 0; i < storageService.repositories.length; i++) { try { const result = await storageService.getFile(userId, fileName, i); if (result) { pptData = result.content; console.log(`✅ PPT data found in repository ${i}`); break; } } catch (error) { console.log(`❌ Repository ${i} check failed:`, error.message); continue; } } } else { console.log('📂 Checking memory storage...'); try { const result = await storageService.getFile(userId, fileName); if (result) { pptData = result.content; console.log('✅ PPT data found in memory storage'); } } catch (error) { console.log('❌ Memory storage check failed:', error.message); } } // 如果GitHub失败,尝试内存存储fallback if (!pptData && storageService === githubService) { console.log('🔄 Trying memory storage fallback...'); try { const memoryResult = await memoryStorageService.getFile(userId, fileName); if (memoryResult) { pptData = memoryResult.content; console.log('✅ PPT data found in memory storage fallback'); } } catch (memoryError) { console.log('❌ Memory storage fallback failed:', memoryError.message); } } if (!pptData) { console.log('❌ PPT not found anywhere'); // 生成"PPT未找到"的错误图片 const errorImage = screenshotService.generateFallbackImage(960, 720, 'PPT未找到'); res.setHeader('Content-Type', 'image/svg+xml'); res.setHeader('Cache-Control', 'no-cache'); return res.send(errorImage); } if (slideIdx >= pptData.slides.length || slideIdx < 0) { console.log(`❌ Slide index out of bounds: ${slideIdx}/${pptData.slides.length}`); // 生成"幻灯片不存在"的错误图片 const errorImage = screenshotService.generateFallbackImage(960, 720, '幻灯片不存在'); res.setHeader('Content-Type', 'image/svg+xml'); res.setHeader('Cache-Control', 'no-cache'); return res.send(errorImage); } console.log('📝 Generating HTML content...'); // 生成HTML内容(复用现有函数) const htmlContent = generateSlideHTML(pptData, slideIdx, pptData.title); console.log('🎯 Calling screenshot service...'); // 生成截图 const screenshot = await screenshotService.generateScreenshot(htmlContent, { format: 'jpeg', quality: 90 }); console.log(`✅ Screenshot generated successfully, size: ${screenshot.length} bytes`); // 检查是否是SVG fallback(通过内容检测) const isSvgFallback = screenshot.toString().includes('