hpdf
Version:
NodeJS library for generation PDF from HTML
158 lines (142 loc) • 3.98 kB
text/typescript
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;
}
}
}