UNPKG

safeer-pdf-generator

Version:

Framework-agnostic PDF generation library with chunking, merging, S3 upload, and email delivery

253 lines 9.35 kB
import puppeteer from 'puppeteer'; import { promises as fs } from 'fs'; import { BrowserError, PdfGenerationError } from '../core/errors.js'; import { noOpLogger } from '../config/defaults.js'; import { withTimeout } from '../utils/timing.js'; /** * PDF generation engine using Puppeteer */ export class PdfEngine { constructor(puppeteerOptions = {}, pdfOptions = {}, logger = noOpLogger) { this.browser = null; this.logger = logger; this.puppeteerOptions = { headless: true, protocolTimeout: 600000, // 10 minutes args: [ '--no-sandbox', '--disable-setuid-sandbox', '--disable-dev-shm-usage', '--disable-accelerated-2d-canvas', '--disable-gpu', '--no-zygote', '--single-process', '--disable-software-rasterizer', '--disable-extensions', '--font-render-hinting=none', '--no-first-run', '--max-old-space-size=4096', ], ...puppeteerOptions, }; this.pdfOptions = { format: 'A4', landscape: true, printBackground: true, margin: { top: '80px', bottom: '60px', left: '5mm', right: '5mm', }, displayHeaderFooter: false, // Disabled by default to prevent "about:blank" ...pdfOptions, }; } /** * Launch the browser if not already launched */ async launchBrowser() { if (this.browser) { return this.browser; } try { const executablePath = await this.getBrowserPath(); this.logger.debug(`Launching browser with executable: ${executablePath}`); this.browser = await puppeteer.launch({ ...this.puppeteerOptions, executablePath, }); this.logger.info('Browser launched successfully'); return this.browser; } catch (error) { throw new BrowserError('Failed to launch browser', 'launch', error); } } /** * Generate PDF from HTML content */ async generatePdf(html, headerTemplate, footerTemplate, customPdfOptions) { const browser = await this.launchBrowser(); const page = await browser.newPage(); try { // Keep JavaScript enabled for table functionality but disable cache await page.setCacheEnabled(false); this.logger.debug('Setting page content...'); // Set content with timeout await withTimeout(page.setContent(html, { waitUntil: 'domcontentloaded', timeout: 180000, // 3 minutes }), 180000, 'Page content loading timed out'); const pdfOptions = { ...this.pdfOptions, ...customPdfOptions, headerTemplate: headerTemplate || '', footerTemplate: footerTemplate || '', // Disable header/footer if templates are empty to prevent "about:blank" displayHeaderFooter: !!(headerTemplate || footerTemplate), }; this.logger.debug('Generating PDF...'); // Generate PDF with timeout const pdfBuffer = await withTimeout(page.pdf(pdfOptions), 300000, // 5 minutes 'PDF generation timed out'); this.logger.debug(`PDF generated successfully (${pdfBuffer.length} bytes)`); return Buffer.from(pdfBuffer); } catch (error) { throw new PdfGenerationError(`Failed to generate PDF: ${error instanceof Error ? error.message : 'Unknown error'}`, 'pdf-generation', error); } finally { try { await page.close(); } catch (closeError) { this.logger.warn(`Warning: Failed to close page: ${closeError}`); } } } /** * Generate multiple PDF chunks concurrently */ async generateChunks(htmlChunks, maxConcurrency = 2) { const browser = await this.launchBrowser(); const results = new Array(htmlChunks.length); let currentIndex = 0; const processChunk = async () => { while (currentIndex < htmlChunks.length) { const index = currentIndex++; if (index >= htmlChunks.length) return; const { html, header, footer } = htmlChunks[index]; const page = await browser.newPage(); try { // Keep JavaScript enabled for table functionality but disable cache await page.setCacheEnabled(false); this.logger.debug(`Processing chunk ${index + 1}/${htmlChunks.length}`); await withTimeout(page.setContent(html, { waitUntil: 'domcontentloaded', timeout: 180000, }), 180000, `Chunk ${index + 1} content loading timed out`); const pdfBuffer = await withTimeout(page.pdf({ ...this.pdfOptions, headerTemplate: header || '', footerTemplate: footer || '', // Disable header/footer if templates are empty to prevent "about:blank" displayHeaderFooter: !!(header || footer), }), 300000, `Chunk ${index + 1} PDF generation timed out`); results[index] = Buffer.from(pdfBuffer); this.logger.debug(`Chunk ${index + 1} completed (${pdfBuffer.length} bytes)`); } catch (error) { throw new PdfGenerationError(`Failed to process chunk ${index + 1}: ${error instanceof Error ? error.message : 'Unknown error'}`, 'chunk-processing', error); } finally { try { await page.close(); } catch (closeError) { this.logger.warn(`Warning: Failed to close page for chunk ${index + 1}: ${closeError}`); } } } }; // Create worker promises with concurrency limit const workers = Array(Math.min(maxConcurrency, htmlChunks.length)) .fill(null) .map(() => processChunk()); await Promise.all(workers); return results; } /** * Close the browser */ async close() { if (this.browser) { try { await this.browser.close(); this.logger.info('Browser closed successfully'); } catch (error) { this.logger.warn(`Warning: Error closing browser: ${error}`); } finally { this.browser = null; } } } /** * Check if browser is running */ isRunning() { return this.browser !== null && this.browser.connected; } /** * Get browser process ID */ getPid() { return this.browser?.process()?.pid; } /** * Get browser path with fallbacks */ async getBrowserPath() { const possiblePaths = [ this.puppeteerOptions.executablePath, process.env.PUPPETEER_EXECUTABLE_PATH, '/usr/bin/chromium', '/usr/bin/chromium-browser', '/usr/bin/google-chrome-stable', '/usr/bin/google-chrome', ].filter(Boolean); // Check each possible path for (const path of possiblePaths) { try { await fs.access(path); this.logger.debug(`Found browser at: ${path}`); return path; } catch { // Continue to next path } } // Fallback to Puppeteer's default try { const defaultPath = puppeteer.executablePath(); this.logger.debug(`Using Puppeteer default: ${defaultPath}`); return defaultPath; } catch (error) { throw new BrowserError('Chromium not found. Please install Chromium or set PUPPETEER_EXECUTABLE_PATH environment variable.', 'executable-path', error); } } /** * Get browser version info */ async getBrowserVersion() { const browser = await this.launchBrowser(); return browser.version(); } /** * Create a new page with optimized settings */ async createOptimizedPage() { const browser = await this.launchBrowser(); const page = await browser.newPage(); // Apply optimizations - keep JavaScript enabled for table functionality await page.setCacheEnabled(false); // Set viewport for consistent rendering await page.setViewport({ width: 1200, height: 800, deviceScaleFactor: 1, }); return page; } /** * Cleanup resources */ async cleanup() { await this.close(); } } //# sourceMappingURL=PdfEngine.js.map