@cityssm/pdf-puppeteer
Version:
A simple NPM package to convert html to pdf for Node applications by using Puppeteer
216 lines (175 loc) • 5.88 kB
text/typescript
import { getPaperSize } from '@cityssm/paper-sizes'
import launchPuppeteer, { type puppeteer } from '@cityssm/puppeteer-launch'
import Debug from 'debug'
import exitHook from 'exit-hook'
import { DEBUG_NAMESPACE } from './debug.config.js'
import {
type PDFPuppeteerOptions,
defaultPdfOptions,
defaultPdfPuppeteerOptions,
defaultPuppeteerOptions,
htmlNavigationTimeoutMillis,
urlNavigationTimeoutMillis
} from './defaultOptions.js'
const debug = Debug(`${DEBUG_NAMESPACE}:index`)
let cachedBrowser: puppeteer.Browser | undefined
/**
* Converts HTML or a webpage into HTML using Puppeteer.
* @param html - An HTML string, or a URL.
* @param instancePdfOptions - PDF options for Puppeteer.
* @param instancePdfPuppeteerOptions - pdf-puppeteer options.
* @returns A Buffer of PDF data.
*/
// eslint-disable-next-line complexity
export async function convertHTMLToPDF(
html: string,
instancePdfOptions: puppeteer.PDFOptions = {},
instancePdfPuppeteerOptions: Partial<PDFPuppeteerOptions> = {}
): Promise<Uint8Array> {
if (typeof html !== 'string') {
throw new TypeError(
'Invalid Argument: HTML expected as type of string and received a value of a different type. Check your request body and request headers.'
)
}
const pdfPuppeteerOptions = {
...defaultPdfPuppeteerOptions,
...instancePdfPuppeteerOptions
}
/*
* Initialize browser
*/
const puppeteerOptions = { ...defaultPuppeteerOptions }
puppeteerOptions.browser = pdfPuppeteerOptions.browser ?? 'chrome'
if (pdfPuppeteerOptions.disableSandbox) {
puppeteerOptions.args = ['--no-sandbox', '--disable-setuid-sandbox']
}
let browser: puppeteer.Browser | undefined
let doCloseBrowser = false
let isRunningPdfGeneration = false
try {
let puppeteerLaunchFunction = launchPuppeteer
if (pdfPuppeteerOptions.usePackagePuppeteer) {
const puppeteerPackage = await import('puppeteer')
puppeteerLaunchFunction = puppeteerPackage.launch
}
if (pdfPuppeteerOptions.cacheBrowser) {
cachedBrowser ??= await puppeteerLaunchFunction(puppeteerOptions)
browser = cachedBrowser
} else {
doCloseBrowser = true
browser = await puppeteerLaunchFunction(puppeteerOptions)
}
const browserVersion = await browser.version()
debug(`Browser: ${browserVersion}`)
const browserIsFirefox = browserVersion.toLowerCase().includes('firefox')
/*
* Initialize page
*/
const page = await browser.newPage()
const remoteContent = pdfPuppeteerOptions.remoteContent
if (pdfPuppeteerOptions.htmlIsUrl) {
debug('Loading URL...')
await page.goto(html, {
waitUntil: browserIsFirefox ? 'domcontentloaded' : 'networkidle0',
timeout: urlNavigationTimeoutMillis
})
} else if (remoteContent) {
debug('Loading HTML with remote content...')
await page.goto(
`data:text/html;base64,${Buffer.from(html).toString('base64')}`,
{
waitUntil: browserIsFirefox ? 'domcontentloaded' : 'networkidle0',
timeout: urlNavigationTimeoutMillis
}
)
} else {
debug('Loading HTML...')
await page.setContent(html, {
timeout: htmlNavigationTimeoutMillis
})
}
debug('Content loaded.')
const pdfOptions = { ...defaultPdfOptions, ...instancePdfOptions }
// Fix "format" issue
if (pdfOptions.format !== undefined) {
const size = getPaperSize(pdfOptions.format)
// eslint-disable-next-line sonarjs/different-types-comparison, @typescript-eslint/no-unnecessary-condition
if (size !== undefined) {
delete pdfOptions.format
pdfOptions.width = `${size.width}${size.unit}`
pdfOptions.height = `${size.height}${size.unit}`
}
}
debug('Converting to PDF...')
// eslint-disable-next-line sonarjs/no-dead-store
isRunningPdfGeneration = true
const pdfBuffer = await page.pdf(pdfOptions)
// eslint-disable-next-line sonarjs/no-dead-store
isRunningPdfGeneration = false
debug('PDF conversion done.')
await page.close()
if (!pdfPuppeteerOptions.cacheBrowser || cachedBrowser !== browser) {
await browser.close()
}
return pdfBuffer
} catch (error) {
if (
isRunningPdfGeneration &&
defaultPuppeteerOptions.browser === 'chrome'
) {
if (!doCloseBrowser) {
await closeCachedBrowser()
}
defaultPuppeteerOptions.browser = 'firefox'
debug('Trying again with Firefox.')
return await convertHTMLToPDF(
html,
instancePdfOptions,
instancePdfPuppeteerOptions
)
} else {
// eslint-disable-next-line @typescript-eslint/only-throw-error
throw error
}
} finally {
try {
if (doCloseBrowser && browser !== undefined) {
debug('Closing browser...')
await browser.close()
debug('Browser closed.')
}
} catch {
// ignore
}
}
}
export default convertHTMLToPDF
/**
* Closes the cached browser instance.
*/
export async function closeCachedBrowser(): Promise<void> {
if (cachedBrowser !== undefined) {
try {
await cachedBrowser.close()
} catch {
// ignore
}
cachedBrowser = undefined
}
}
/**
* Checks for any cached browser instance.
* @returns True is a cached browser instance exists.
*/
export function hasCachedBrowser(): boolean {
return cachedBrowser !== undefined
}
export {
type PDFPuppeteerOptions,
defaultPdfOptions,
defaultPdfPuppeteerOptions
} from './defaultOptions.js'
exitHook(() => {
debug('Running exit hook.')
void closeCachedBrowser()
})