safeer-pdf-generator
Version:
Framework-agnostic PDF generation library with chunking, merging, S3 upload, and email delivery
253 lines • 9.35 kB
JavaScript
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