pagedjs-cli
Version:
Render Html to PDFs using [Pagedjs](https://gitlab.pagedmedia.org/polyfills/pagedjs) and [Puppeteer](https://github.com/GoogleChrome/puppeteer).
383 lines (305 loc) • 9.01 kB
JavaScript
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;
this.timeout = options.timeout || 0;
this.closeAfter = typeof options.closeAfter !== "undefined" ? options.closeAfter : true;
this.emulateMedia = options.emulateMedia || "print";
this.styles = options.styles;
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();
}
try {
const page = await this.browser.newPage();
page.setDefaultTimeout(this.timeout);
await page.emulateMediaType(this.emulateMedia);
if (this.overrideDefaultBackgroundColor) {
page._client.send("Emulation.setDefaultBackgroundColorOverride", { color: this.overrideDefaultBackgroundColor });
}
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;
}
}
} else {
url = input.url;
html = input.html;
}
if (this.needsAllowedRules()) {
await page.setRequestInterception(true);
page.on("request", (request) => {
let uri = new URL(request.url());
let { host, protocol, pathname } = uri;
let local = protocol === "file:";
if (local && this.withinAllowedPath(pathname) === false) {
request.abort();
return;
}
if (local && !this.allowLocal) {
request.abort();
return;
}
if (host && this.isAllowedDomain(host) === false) {
request.abort();
return;
}
if (host && !this.allowRemote) {
request.abort();
return;
}
request.continue();
});
}
if (html) {
await page.setContent(html);
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);
}
} else {
await page.goto(url);
}
this.content = await page.content();
await page.evaluate(() => {
window.PagedConfig = window.PagedConfig || {};
window.PagedConfig.auto = false;
});
for (const style of this.styles) {
await page.addStyleTag({
path: style
});
}
await page.addScriptTag({
path: scriptPath
});
for (const script of this.additionalScripts) {
await page.addScriptTag({
path: script
});
}
await page.exposeFunction("onSize", (size) => {
this.emit("size", size);
});
await page.exposeFunction("onPage", (page) => {
this.pages.push(page);
this.emit("page", page);
});
await page.exposeFunction("onRendered", (msg, width, height, orientation) => {
this.emit("rendered", msg, width, height, orientation);
resolver({msg, width, height, orientation});
});
await page.evaluate(async () => {
let done;
window.PagedPolyfill.on("page", (page) => {
const { id, width, height, startToken, endToken, breakAfter, breakBefore, position } = page;
const mediabox = page.element.getBoundingClientRect();
const cropbox = page.pagebox.getBoundingClientRect();
function getPointsValue(value) {
return (Math.round(CSS.px(value).to("pt").value * 100) / 100);
}
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();
}
done = await window.PagedPolyfill.preview();
if (window.PagedConfig.after) {
await window.PagedConfig.after(done);
}
}).catch((error) => {
throw error;
});
await page.waitForNetworkIdle({
timeout: this.timeout
});
await rendered;
await page.waitForSelector(".pagedjs_pages");
return page;
} catch (error) {
this.closeAfter && this.close();
throw error;
}
}
async pdf(input, options={}) {
let page = await this.render(input)
.catch((e) => {
throw e;
});
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;
}
let metaTags = document.querySelectorAll("meta");
[...metaTags].forEach((tag) => {
if (tag.name) {
meta[tag.name] = tag.content;
}
});
return meta;
});
const outline = await parseOutline(page, options.outlineTags);
let settings = {
timeout: this.timeout,
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,
}
};
let pdf = await page.pdf(settings)
.catch((e) => {
throw e;
});
page.close();
this.emit("postprocessing");
let pdfDoc = await PDFDocument.load(pdf);
setMetadata(pdfDoc, meta);
setTrimBoxes(pdfDoc, this.pages);
setOutline(pdfDoc, outline);
pdf = await pdfDoc.save();
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();
page.close();
this.closeAfter && this.close();
return content;
}
async preview(input) {
let page = await this.render(input);
this.closeAfter && this.close();
return page;
}
async close() {
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;