|
import express from 'express';
|
|
import githubService from '../services/githubService.js';
|
|
import screenshotService from '../services/screenshotService.js';
|
|
|
|
import { generateSlideHTML, generateExportPage, exportPPTToJSON, generateHTMLPresentation } from '../../../shared/export-utils.js';
|
|
|
|
const router = express.Router();
|
|
|
|
|
|
|
|
|
|
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) {
|
|
|
|
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, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
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) {
|
|
|
|
if (element.src.startsWith('data:image/')) {
|
|
return `<image x="${x}" y="${y}" width="${w}" height="${h}" href="${element.src}" ${transform}/>`;
|
|
}
|
|
|
|
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>`;
|
|
}
|
|
|
|
|
|
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>
|
|
`;
|
|
}
|
|
|
|
|
|
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;
|
|
|
|
|
|
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'));
|
|
}
|
|
|
|
|
|
const htmlContent = generateSlideHTML(pptData, slideIdx, { format: 'view' });
|
|
res.setHeader('Content-Type', 'text/html; charset=utf-8');
|
|
res.send(htmlContent);
|
|
} catch (error) {
|
|
next(error);
|
|
}
|
|
});
|
|
|
|
|
|
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;
|
|
|
|
|
|
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);
|
|
|
|
|
|
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);
|
|
}
|
|
}
|
|
|
|
|
|
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' });
|
|
}
|
|
|
|
|
|
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);
|
|
}
|
|
});
|
|
|
|
|
|
router.get('/ppt/:userId/:pptId', async (req, res, next) => {
|
|
try {
|
|
const { userId, pptId } = req.params;
|
|
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' });
|
|
}
|
|
|
|
|
|
res.json({
|
|
...pptData,
|
|
isPublicView: true,
|
|
readOnly: true
|
|
});
|
|
} catch (error) {
|
|
next(error);
|
|
}
|
|
});
|
|
|
|
|
|
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}`);
|
|
|
|
|
|
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');
|
|
|
|
const protocol = baseUrl.includes('localhost') ? 'http' : 'https';
|
|
|
|
const shareLinks = {
|
|
|
|
slideUrl: `${protocol}://${baseUrl}/api/public/view/${userId}/${pptId}/${slideIndex}`,
|
|
|
|
pptUrl: `${protocol}://${baseUrl}/api/public/ppt/${userId}/${pptId}`,
|
|
|
|
viewUrl: `${protocol}://${baseUrl}/public/${userId}/${pptId}/${slideIndex}`,
|
|
|
|
screenshotUrl: `${protocol}://${baseUrl}/api/public/image/${userId}/${pptId}/${slideIndex}?format=jpg&quality=90`,
|
|
|
|
directImageUrl: `${protocol}://${baseUrl}/api/public/direct-image/${userId}/${pptId}/${slideIndex}`,
|
|
|
|
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);
|
|
}
|
|
});
|
|
|
|
|
|
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}`);
|
|
|
|
|
|
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' });
|
|
}
|
|
|
|
|
|
if (!pptData.slides || !Array.isArray(pptData.slides)) {
|
|
return res.status(400).json({ error: 'Invalid PPT data: slides not found' });
|
|
}
|
|
|
|
const slideIdx = parseInt(slideIndex);
|
|
|
|
|
|
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' });
|
|
}
|
|
|
|
|
|
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);
|
|
}
|
|
|
|
|
|
console.log('Using backend screenshot for direct image return...');
|
|
|
|
|
|
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);
|
|
|
|
|
|
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);
|
|
}
|
|
});
|
|
|
|
|
|
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}`);
|
|
|
|
|
|
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 {
|
|
|
|
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);
|
|
|
|
|
|
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') {
|
|
|
|
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 {
|
|
|
|
console.log(`✅ Generating ${format} image using optimized screenshot method`);
|
|
|
|
try {
|
|
|
|
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');
|
|
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);
|
|
}
|
|
}
|
|
});
|
|
|
|
|
|
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}`);
|
|
|
|
|
|
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' });
|
|
}
|
|
|
|
|
|
if (!pptData.slides || !Array.isArray(pptData.slides)) {
|
|
return res.status(400).json({ error: 'Invalid PPT data: slides not found' });
|
|
}
|
|
|
|
const slideIdx = parseInt(slideIndex);
|
|
|
|
|
|
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' });
|
|
}
|
|
|
|
|
|
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);
|
|
}
|
|
});
|
|
|
|
|
|
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,
|
|
screenshotSize: 0
|
|
}
|
|
};
|
|
|
|
|
|
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
|
|
});
|
|
}
|
|
});
|
|
|
|
|
|
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}`);
|
|
|
|
|
|
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);
|
|
|
|
|
|
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);
|
|
}
|
|
});
|
|
|
|
|
|
router.get('/export-json/:userId/:pptId', async (req, res, next) => {
|
|
try {
|
|
const { userId, pptId } = req.params;
|
|
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' });
|
|
}
|
|
|
|
|
|
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);
|
|
}
|
|
});
|
|
|
|
|
|
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;
|
|
|
|
|
|
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 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);
|
|
}
|
|
});
|
|
|
|
|
|
router.get('/presentation/:userId/:pptId', async (req, res, next) => {
|
|
try {
|
|
const { userId, pptId } = req.params;
|
|
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).send(generateErrorPage('PPT Not Found', 'The presentation you are looking for does not exist.'));
|
|
}
|
|
|
|
|
|
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}`);
|
|
|
|
|
|
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}`);
|
|
|
|
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);
|
|
|
|
|
|
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}`);
|
|
|
|
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;
|
|
|
|
|
|
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');
|
|
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');
|
|
|
|
|
|
res.send(svgContent);
|
|
|
|
} catch (error) {
|
|
console.error('❌ Direct image generation failed:', error);
|
|
|
|
|
|
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();
|
|
|
|
|
|
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' });
|
|
}
|
|
|
|
|
|
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}`
|
|
});
|
|
|
|
|
|
setTimeout(() => {
|
|
imageCache.delete(imageId);
|
|
}, 3600000);
|
|
|
|
|
|
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);
|
|
}
|
|
});
|
|
|
|
|
|
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);
|
|
|
|
|
|
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; |