ppt-tool/frontend/app/api/export-as-pdf/route.ts
Vadym Samoilenko ff9cdffc32 Phase 5: Fix export, slide edit, static files; add README
- Fix PPTX/PDF export: Puppeteer URL port mismatch (80 → 3000)
- Fix backend export_utils to use NEXT_INTERNAL_URL env var
- Add Chromium to frontend Dockerfile for Docker-based export
- Fix slide edit socket hang up with asyncio.wait_for() timeouts
- Add FastAPI StaticFiles mounts for /static and /app_data
- Add Next.js rewrite for /static/ to proxy to backend
- Show template thumbnail in master decks admin page
- Add error logging to ReviewWorkflow component
- Add Docker env vars for web service (APP_DATA_DIRECTORY, app_data volume)
- Add project README in English

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-27 15:40:36 +00:00

104 lines
3 KiB
TypeScript

import path from "path";
import fs from "fs";
import puppeteer from "puppeteer";
import { sanitizeFilename } from "@/app/(presentation-generator)/utils/others";
import { NextResponse, NextRequest } from "next/server";
export async function POST(req: NextRequest) {
const { id, title } = await req.json();
if (!id) {
return NextResponse.json(
{ error: "Missing Presentation ID" },
{ status: 400 }
);
}
const browser = await puppeteer.launch({
executablePath: process.env.PUPPETEER_EXECUTABLE_PATH,
headless: true,
args: [
"--no-sandbox",
"--disable-setuid-sandbox",
"--disable-dev-shm-usage",
"--disable-gpu",
"--disable-web-security",
"--disable-background-timer-throttling",
"--disable-backgrounding-occluded-windows",
"--disable-renderer-backgrounding",
"--disable-features=TranslateUI",
"--disable-ipc-flooding-protection",
],
});
const page = await browser.newPage();
await page.setViewport({ width: 1280, height: 720 });
page.setDefaultNavigationTimeout(300000);
page.setDefaultTimeout(300000);
const baseUrl = process.env.PUPPETEER_BASE_URL || `http://localhost:${process.env.PORT || 3000}`;
await page.goto(`${baseUrl}/pdf-maker?id=${id}`, {
waitUntil: "networkidle0",
timeout: 300000,
});
await page.waitForFunction('() => document.readyState === "complete"');
try {
await page.waitForFunction(
`
() => {
const allElements = document.querySelectorAll('*');
let loadedElements = 0;
let totalElements = allElements.length;
for (let el of allElements) {
const style = window.getComputedStyle(el);
const isVisible = style.display !== 'none' &&
style.visibility !== 'hidden' &&
style.opacity !== '0';
if (isVisible && el.offsetWidth > 0 && el.offsetHeight > 0) {
loadedElements++;
}
}
return (loadedElements / totalElements) >= 0.99;
}
`,
{ timeout: 300000 }
);
await new Promise((resolve) => setTimeout(resolve, 1000));
} catch (error) {
console.log("Warning: Some content may not have loaded completely:", error);
}
const pdfBuffer = await page.pdf({
width: "1280px",
height: "720px",
printBackground: true,
margin: { top: 0, right: 0, bottom: 0, left: 0 },
});
browser.close();
const sanitizedTitle = sanitizeFilename(title ?? "presentation");
const appDataDirectory = process.env.APP_DATA_DIRECTORY!;
if (!appDataDirectory) {
return NextResponse.json({
error: "App data directory not found",
status: 500,
});
}
const destinationPath = path.join(
appDataDirectory,
"exports",
`${sanitizedTitle}.pdf`
);
await fs.promises.mkdir(path.dirname(destinationPath), { recursive: true });
await fs.promises.writeFile(destinationPath, pdfBuffer);
return NextResponse.json({
success: true,
path: destinationPath,
});
}