Skip to content
Snippets Groups Projects
printer.js 8.67 KiB
Newer Older
import EventEmitter from "events";
import puppeteer from "puppeteer";
import path from "path";
import fs from "fs";
import { fileURLToPath } from "url";
import { PDFDocument } from "pdf-lib";
import { setTrimBoxes, setMetadata } from "./postprocesser.js";
import { parseOutline, setOutline } from "./outline.js";
const currentPath = fileURLToPath(import.meta.url);
const dir = process.cwd();
const scriptPath = path.resolve(path.dirname(currentPath), "../dist/browser.js");

class Printer extends EventEmitter {
	constructor(options = {}) {
		super();

		this.headless = options.headless !== false;
		this.allowLocal = options.allowLocal;
		this.allowRemote = options.allowRemote;
		this.additionalScripts = options.additionalScripts || [];
		this.allowedPaths = options.allowedPaths || [];
		this.allowedDomains = options.allowedDomains || [];
		this.ignoreHTTPSErrors = options.ignoreHTTPSErrors;
		this.browserWSEndpoint = options.browserEndpoint;
		this.browserArgs = options.browserArgs;
		this.overrideDefaultBackgroundColor = options.overrideDefaultBackgroundColor;
Fred Chasen's avatar
Fred Chasen committed
		this.timeout = options.timeout || 0;
Fred Chasen's avatar
Fred Chasen committed
		this.closeAfter = typeof options.closeAfter !== "undefined" ? options.closeAfter : true;
Fred Chasen's avatar
Fred Chasen committed
		this.emulateMedia = options.emulateMedia || "print";

		this.pages = [];
	}

	async setup() {
		let puppeteerOptions = {
			headless: this.headless,
			args: ["--disable-dev-shm-usage", "--export-tagged-pdf"],
			ignoreHTTPSErrors: this.ignoreHTTPSErrors
		};

		if (this.allowLocal) {
			puppeteerOptions.args.push("--allow-file-access-from-files");
		}

		if (this.browserArgs) {
			puppeteerOptions.args.push(...this.browserArgs);
		}

		if (this.browserWSEndpoint) {
			puppeteerOptions.browserWSEndpoint = this.browserWSEndpoint;
			this.browser = await puppeteer.connect(puppeteerOptions);
		} else {
			this.browser = await puppeteer.launch(puppeteerOptions);
		}

		return this.browser;
	}

	async render(input) {
		let resolver;
		let rendered = new Promise(function(resolve, reject) {
			resolver = resolve;
		});

		if (!this.browser) {
			await this.setup();
		}

Fred Chasen's avatar
Fred Chasen committed
		try {
			const page = await this.browser.newPage();
Fred Chasen's avatar
Fred Chasen committed
			page.setDefaultTimeout(this.timeout);
Fred Chasen's avatar
Fred Chasen committed
			await page.emulateMediaType(this.emulateMedia);
Fred Chasen's avatar
Fred Chasen committed

Fred Chasen's avatar
Fred Chasen committed
			if (this.overrideDefaultBackgroundColor) {
				page._client.send("Emulation.setDefaultBackgroundColorOverride", { color: this.overrideDefaultBackgroundColor });
			}
Fred Chasen's avatar
Fred Chasen committed
			let url, relativePath, html;
			if (typeof input === "string") {
				try {
					new URL(input);
					url = input;
				} catch (error) {
					relativePath = path.resolve(dir, input);

					if (this.browserWSEndpoint) {
						html = fs.readFileSync(relativePath, "utf-8");
					} else {
						url = "file://" + relativePath;
					}
Fred Chasen's avatar
Fred Chasen committed
			} else {
				url = input.url;
				html = input.html;
			}
Fred Chasen's avatar
Fred Chasen committed
			if (this.needsAllowedRules()) {
				await page.setRequestInterception(true);
Fred Chasen's avatar
Fred Chasen committed
				page.on("request", (request) => {
					let uri = new URL(request.url());
					let { host, protocol, pathname } = uri;
					let local = protocol === "file:";
Fred Chasen's avatar
Fred Chasen committed
					if (local && this.withinAllowedPath(pathname) === false) {
						request.abort();
						return;
					}
Fred Chasen's avatar
Fred Chasen committed
					if (local && !this.allowLocal) {
						request.abort();
						return;
					}
Fred Chasen's avatar
Fred Chasen committed
					if (host && this.isAllowedDomain(host) === false) {
						request.abort();
						return;
					}
Fred Chasen's avatar
Fred Chasen committed
					if (host && !this.allowRemote) {
						request.abort();
						return;
Fred Chasen's avatar
Fred Chasen committed

					request.continue();
				});	
Fred Chasen's avatar
Fred Chasen committed
			if (html) {
Fred Chasen's avatar
Fred Chasen committed
				await page.setContent(html);
Fred Chasen's avatar
Fred Chasen committed

				if (url) {
					await page.evaluate((url) => {
						let base = document.querySelector("base");
						if (!base) {
							base = document.createElement("base");
							document.querySelector("head").appendChild(base);
						}
						base.setAttribute("href", url);
					}, url);
				}
Fred Chasen's avatar
Fred Chasen committed
			} else {
Fred Chasen's avatar
Fred Chasen committed
				await page.goto(url);
Fred Chasen's avatar
Fred Chasen committed
			this.content = await page.content();
Fred Chasen's avatar
Fred Chasen committed
			await page.evaluate(() => {
				window.PagedConfig = window.PagedConfig || {};
				window.PagedConfig.auto = false;
			});

			await page.addScriptTag({
Fred Chasen's avatar
Fred Chasen committed
				path: scriptPath
Fred Chasen's avatar
Fred Chasen committed
			for (const script of this.additionalScripts) {
				await page.addScriptTag({
					path: script
				});
			}
Fred Chasen's avatar
Fred Chasen committed
			await page.exposeFunction("onSize", (size) => {
				this.emit("size", size);
			});
Fred Chasen's avatar
Fred Chasen committed
			await page.exposeFunction("onPage", (page) => {
Fred Chasen's avatar
Fred Chasen committed
				this.pages.push(page);
Fred Chasen's avatar
Fred Chasen committed
				this.emit("page", page);
			});
Fred Chasen's avatar
Fred Chasen committed
			await page.exposeFunction("onRendered", (msg, width, height, orientation) => {
				this.emit("rendered", msg, width, height, orientation);
				resolver({msg, width, height, orientation});
			});
Fred Chasen's avatar
Fred Chasen committed
			await page.evaluate(async () => {
				let done;
				window.PagedPolyfill.on("page", (page) => {
					const { id, width, height, startToken, endToken, breakAfter, breakBefore, position } = page;
Fred Chasen's avatar
Fred Chasen committed
					const mediabox = page.element.getBoundingClientRect();
					const cropbox = page.pagebox.getBoundingClientRect();
Fred Chasen's avatar
Fred Chasen committed
					function getPointsValue(value) {
						return (Math.round(CSS.px(value).to("pt").value * 100) / 100);
Fred Chasen's avatar
Fred Chasen committed
					let boxes = {
						media: {
							width: getPointsValue(mediabox.width),
							height: getPointsValue(mediabox.height),
							x: 0,
							y: 0
						},
						crop: {
							width: getPointsValue(cropbox.width),
							height: getPointsValue(cropbox.height),
							x: getPointsValue(cropbox.x) - getPointsValue(mediabox.x),
							y: getPointsValue(cropbox.y) - getPointsValue(mediabox.y)
						}
					};

					window.onPage({ id, width, height, startToken, endToken, breakAfter, breakBefore, position, boxes });
				});

				window.PagedPolyfill.on("size", (size) => {
					window.onSize(size);
				});

				window.PagedPolyfill.on("rendered", (flow) => {
					let msg = "Rendering " + flow.total + " pages took " + flow.performance + " milliseconds.";
					window.onRendered(msg, flow.width, flow.height, flow.orientation);
				});

				if (window.PagedConfig.before) {
					await window.PagedConfig.before();
				}
Fred Chasen's avatar
Fred Chasen committed
				done = await window.PagedPolyfill.preview();
Fred Chasen's avatar
Fred Chasen committed
				if (window.PagedConfig.after) {
					await window.PagedConfig.after(done);
				}
			}).catch((error) => {
				throw error;
Fred Chasen's avatar
Fred Chasen committed
			await page.waitForNetworkIdle({
				timeout: this.timeout
			});

Fred Chasen's avatar
Fred Chasen committed
			await rendered;
Fred Chasen's avatar
Fred Chasen committed
			await page.waitForSelector(".pagedjs_pages");
Fred Chasen's avatar
Fred Chasen committed
			return page;
		} catch (error) {
			this.closeAfter && this.close();
			throw error;
	}

	async pdf(input, options={}) {
		let page = await this.render(input)
			.catch((e) => {
				throw e;
			});

Fred Chasen's avatar
Fred Chasen committed
		try {
			// Get metatags
			const meta = await page.evaluate(() => {
				let meta = {};
				let title = document.querySelector("title");
				if (title) {
					meta.title = title.textContent.trim();
				}
				let lang = document.querySelector("html").getAttribute("lang");
				if (lang) {
					meta.lang = lang;
Fred Chasen's avatar
Fred Chasen committed
				let metaTags = document.querySelectorAll("meta");
				[...metaTags].forEach((tag) => {
					if (tag.name) {
						meta[tag.name] = tag.content;
					}
				});
				return meta;
Fred Chasen's avatar
Fred Chasen committed
			const outline =  await parseOutline(page, options.outlineTags);

			let settings = {
Fred Chasen's avatar
Fred Chasen committed
				timeout: this.timeout,
Fred Chasen's avatar
Fred Chasen committed
				printBackground: true,
				displayHeaderFooter: false,
				preferCSSPageSize: options.width ? false : true,
				width: options.width,
				height: options.height,
				orientation: options.orientation,
				margin: {
					top: 0,
					right: 0,
					bottom: 0,
					left: 0,
				}
			};
Fred Chasen's avatar
Fred Chasen committed
			let pdf = await page.pdf(settings)
				.catch((e) => {
					throw e;
				});
Fred Chasen's avatar
Fred Chasen committed
			page.close();
Fred Chasen's avatar
Fred Chasen committed
			this.emit("postprocessing");
Fred Chasen's avatar
Fred Chasen committed
			let pdfDoc = await PDFDocument.load(pdf);
Fred Chasen's avatar
Fred Chasen committed
			setMetadata(pdfDoc, meta);
			setTrimBoxes(pdfDoc, this.pages);
			setOutline(pdfDoc, outline);
Fred Chasen's avatar
Fred Chasen committed
			pdf = await pdfDoc.save();
Fred Chasen's avatar
Fred Chasen committed
			return pdf;
		} catch (error) {
			this.closeAfter && this.close();
			throw error;
		}
	}

	async html(input, stayopen) {
		let page = await this.render(input);

		let content = await page.content();

Fred Chasen's avatar
Fred Chasen committed
		page.close();
		this.closeAfter && this.close();

		return content;
	}

	async preview(input) {
		let page = await this.render(input);
Fred Chasen's avatar
Fred Chasen committed
		this.closeAfter && this.close();
		return page;
	}

	async close() {
Fred Chasen's avatar
Fred Chasen committed
		return this.browser && this.browser.close();
	}

	needsAllowedRules() {
		if (this.allowedPaths && this.allowedPaths.length !== 0) {
			return true;
		}
		if (this.allowedDomains && this.allowedDomains.length !== 0) {
			return true;
		}
	}

	withinAllowedPath(pathname) {
		if (!this.allowedPaths || this.allowedPaths.length === 0) {
			return true;
		}

		for (let parent of this.allowedPaths) {
			const relative = path.relative(parent, pathname);
			if (relative && !relative.startsWith("..") && !path.isAbsolute(relative)) {
				return true;
			}
		}

		return false;
	}

	isAllowedDomain(domain) {
		if (!this.allowedDomains || this.allowedDomains.length === 0) {
			return true;
		}
		return this.allowedDomains.includes(domain);
	}
export default Printer;