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;
this.closeAfter = typeof options.closeAfter !== "undefined" ? options.closeAfter : true;
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
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();
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;
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);
}
await page.evaluate(() => {
window.PagedConfig = window.PagedConfig || {};
window.PagedConfig.auto = false;
});
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) => {
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);
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
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();
}
if (window.PagedConfig.after) {
await window.PagedConfig.after(done);
}
}).catch((error) => {
throw error;
await page.waitForNetworkIdle({
timeout: this.timeout
});
return page;
} catch (error) {
this.closeAfter && this.close();
}
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 = {
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;
});
setMetadata(pdfDoc, meta);
setTrimBoxes(pdfDoc, this.pages);
setOutline(pdfDoc, outline);
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);
return page;
}
async close() {
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
}
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);
}