app/lib/pdf.js

/**
 * @module pdf
 */
const config = require('../models/config-model').server;

const { timeout } = config.headless;
const puppeteer = require('puppeteer');
const { URL } = require('url');

/**
 * @typedef PdfGetOptions
 * @property { string } [format]
 * @property { string } [margin]
 * @property { string } [landscape]
 * @property { string } [scale]
 */

/**
 * default values for {@link module:pdf~PdfGetOptions|PdfGetOptions}
 *
 * @default
 */
const DEFAULTS = {
    FORMAT: 'A4',
    MARGIN: '0.5in',
    LANDSCAPE: false,
    SCALE: 1,
};

/**
 * Asynchronously gets pdf from url using Puppeteer.
 *
 * @static
 * @param { string } url - URL to load
 * @param {PdfGetOptions} [options] - PDF options
 * @return { Promise } a promise that returns the PDF
 */
async function get(url, options = {}) {
    if (!url) {
        throw new Error('No url provided');
    }

    options.format = options.format || DEFAULTS.FORMAT;
    options.margin = options.margin || DEFAULTS.MARGIN;
    options.landscape = options.landscape || DEFAULTS.LANDSCAPE;
    options.scale = options.scale || DEFAULTS.SCALE;

    const urlObj = new URL(url);
    urlObj.searchParams.append('format', options.format);
    urlObj.searchParams.append('margin', options.margin);
    urlObj.searchParams.append('landscape', options.landscape);
    urlObj.searchParams.append('scale', options.scale);

    const browser = await puppeteer.launch({ headless: true });
    const page = await browser.newPage();

    let pdf;

    try {
        await page
            .goto(urlObj.href, { waitUntil: 'networkidle0', timeout })
            .catch((e) => {
                e.status = /timeout/i.test(e.message) ? 408 : 400;
                throw e;
            });

        /*
         * This works around an issue with puppeteer not printing canvas
         * images that were loaded from a file.
         * It is likely this issue: https://bugs.chromium.org/p/chromium/issues/detail?id=809065
         * (though not WebGL-related as some of the commenters suggest)
         */
        await page.evaluate(() => {
            /* eslint-env browser */
            function canvasToImage(element) {
                const image = document.createElement('img');
                image.src = element.toDataURL();

                ['width', 'height', 'position', 'left', 'top'].forEach(
                    (property) =>
                        (image.style[property] = element.style[property])
                );
                // overriding a general image style
                image.style['max-width'] = '100%';
                image.className = element.className;

                element.parentNode &&
                    element.parentNode.insertBefore(image, element);
                element.parentNode && element.parentNode.removeChild(element);
            }

            document.querySelectorAll('canvas').forEach(canvasToImage);
        });

        pdf = await page.pdf({
            landscape: options.landscape,
            format: options.format,
            margin: {
                top: options.margin,
                left: options.margin,
                right: options.margin,
                bottom: options.margin,
            },
            scale: options.scale,
            printBackground: true,
            timeout,
        });
    } catch (e) {
        e.status = e.status || 400;
        await page.close();
        throw e;
    }

    await page.close();
    await browser.close();

    return pdf;
}

module.exports = { get };