UNPKG

hpdf

Version:

NodeJS library for generation PDF from HTML

158 lines (142 loc) 3.98 kB
import { Readable } from 'stream'; import { createPool, Pool, Options as PoolOptions, Factory as PoolFactory, } from 'generic-pool'; import puppeteer, { Browser, Page, PDFOptions } from 'puppeteer'; interface PageInstance { browser: Browser; page: Page; } class PageFactory implements PoolFactory<PageInstance> { constructor(protected puppeteerArgs?: string[]) {} async create(): Promise<PageInstance> { const browser = await puppeteer.launch({ headless: 'new', args: this.puppeteerArgs || ['--no-sandbox', '--disable-setuid-sandbox'], }); return { browser, page: await browser.newPage(), }; } async destroy(client: PageInstance): Promise<void> { try { if (client.browser.isConnected()) { await client.browser.close(); } } catch { return; } } async validate(client: PageInstance): Promise<boolean> { try { return !!(await client.page.metrics()); } catch { return false; } } } export class PdfGenerator { protected pagesPool: Pool<PageInstance>; /** * @param poolConfig https://github.com/coopernurse/node-pool/blob/1c5cb79dcbea27c4b1839bd75bfc41274adb8b94/lib/PoolOptions.js#L5 * @param puppeteerArgs https://peter.sh/experiments/chromium-command-line-switches/ */ constructor( poolConfig: PoolOptions = { min: 1, max: 10 }, puppeteerArgs?: string[], ) { this.pagesPool = createPool(new PageFactory(puppeteerArgs), { testOnBorrow: true, // Should the pool validate resources before giving them to clients evictionRunIntervalMillis: 5000, ...poolConfig, autostart: true, }); } async stop() { await this.pagesPool.drain(); await this.pagesPool.clear(); } async awaitPool() { return this.pagesPool.ready(); } generatePDF(htmlOrUrl: string | URL): Promise<Buffer>; generatePDF( htmlOrUrl: string | URL, stream: undefined, pdfOptions?: PDFOptions, ): Promise<Buffer>; generatePDF( htmlOrUrl: string | URL, stream: false, pdfOptions?: PDFOptions, ): Promise<Buffer>; generatePDF( htmlOrUrl: string | URL, stream: true, pdfOptions?: PDFOptions, ): Promise<Readable>; public async generatePDF( htmlOrUrl: string | URL, stream = false, pdfOptions?: PDFOptions, ): Promise<Readable | Buffer> { const page = await this.pagesPool.acquire(); try { if (htmlOrUrl instanceof URL) { await page.page.goto(htmlOrUrl.toString(), { waitUntil: 'networkidle0', }); } else { await page.page.setContent(htmlOrUrl, { waitUntil: 'networkidle0' }); } //To reflect CSS used for screens instead of print await page.page.emulateMediaType('print'); } catch (e) { await this.pagesPool.release(page); throw e; } const options: PDFOptions = pdfOptions || { margin: { top: '100px', right: '50px', bottom: '100px', left: '50px' }, format: 'A4', }; if (!stream) { try { const res = await page.page.pdf(options); await this.pagesPool.release(page); return res; } catch (e) { await this.pagesPool.destroy(page); throw e; } } try { let released = false; return (await page.page.createPDFStream(options)) .once('error', () => { if (!released) { released = true; this.pagesPool.destroy(page).catch(() => undefined); } }) .once('close', () => { if (!released) { released = true; this.pagesPool.release(page).catch(() => undefined); } }) .once('end', () => { if (!released) { released = true; this.pagesPool.release(page).catch(() => undefined); } }); } catch (e) { await this.pagesPool.destroy(page).catch(() => undefined); throw e; } } }