/** * exportPdf.ts * Combines all slide PNGs into a single multi-page PDF using pdf-lib. * Each PNG becomes one page at the original 1080x1080 dimensions. * * Approach: PNG → PDF embedding (not HTML→PDF) for pixel-perfect output. * This avoids cross-page layout bleed and ensures each slide is exactly one page. */ import fs from 'fs'; import path from 'path'; import type { SlideContent } from '../schema/carouselSchema'; import type { Theme } from '../render/themes'; import { exportToPngs } from './exportPng'; // Points per pixel at 72 DPI (PDF's native unit) // 1080px at 96 DPI screen = 810pt at 72 DPI // We preserve 1:1 mapping for now (1px = 1pt) which gives ~15" square at 72dpi // For LinkedIn, pixel dimensions are more important than physical size const PX_TO_PT = 1.0; // 1080px slides → 1080pt pages /** * Export all slides to a single PDF file. * First renders PNGs (if not already done), then combines them. * * @param slides - Ordered array of SlideContent * @param theme - Visual theme * @param outputDir - Base output directory * @param existingPngs - Optional: pre-rendered PNG paths to skip re-render * @returns - Absolute path to generated PDF */ export async function exportToPdf( slides: SlideContent[], theme: Theme, outputDir: string, existingPngs?: string[] ): Promise { // Lazy-import pdf-lib let PDFDocument: typeof import('pdf-lib').PDFDocument; let pdfLib: typeof import('pdf-lib'); try { pdfLib = await import('pdf-lib'); PDFDocument = pdfLib.PDFDocument; } catch (err) { throw new Error( 'pdf-lib is not installed. Run: npm install pdf-lib\n' + 'If you cannot install it, PNG exports are still available in slides_preview/.' ); } // Render PNGs if not provided const pngPaths = existingPngs ?? (await exportToPngs(slides, theme, outputDir)); if (pngPaths.length === 0) { throw new Error('No PNG files available to combine into PDF.'); } // Create PDF const pdfDoc = await PDFDocument.create(); for (const pngPath of pngPaths) { const pngBytes = fs.readFileSync(pngPath); const pngImage = await pdfDoc.embedPng(pngBytes); // Use the actual image dimensions for the page const { width, height } = pngImage; const page = pdfDoc.addPage([width * PX_TO_PT, height * PX_TO_PT]); page.drawImage(pngImage, { x: 0, y: 0, width: width * PX_TO_PT, height: height * PX_TO_PT, }); } // Set PDF metadata pdfDoc.setTitle('LinkedIn Career Carousel'); pdfDoc.setAuthor('Generated by job-carousel skill'); pdfDoc.setCreationDate(new Date()); const pdfBytes = await pdfDoc.save(); const pdfPath = path.join(outputDir, 'linkedin_carousel.pdf'); fs.writeFileSync(pdfPath, pdfBytes); console.log(` ✓ PDF exported: linkedin_carousel.pdf (${pngPaths.length} slides)`); return pdfPath; }