declarative-pdf
Version:
A tool for generating PDF documents from declarative HTML templates
1,098 lines (1,073 loc) • 37.5 kB
JavaScript
"use strict";
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
// src/index.ts
var src_exports = {};
__export(src_exports, {
default: () => DeclarativePDF
});
module.exports = __toCommonJS(src_exports);
var import_pdf_lib3 = require("pdf-lib");
// src/models/document-page.ts
var import_pdf_lib = require("pdf-lib");
// src/models/element.ts
var BodyElement = class {
constructor(opts) {
this.buffer = opts.buffer;
this.pdf = opts.pdf;
this.layout = opts.layout;
}
get x() {
return this.layout.x;
}
get y() {
return this.layout.y;
}
get width() {
return this.layout.width;
}
get height() {
return this.layout.height;
}
async embedPageIdx(targetPage, idx) {
return await targetPage.doc.embedPdf(this.pdf, [idx]);
}
};
var SectionElement = class {
constructor(opts) {
this.buffer = opts.buffer;
this.pdf = opts.pdf;
this.setting = opts.setting;
this.layout = opts.layout;
this.debug = opts.debug;
this._name = `${opts.debug.pageNumber}-${opts.debug.type}.pdf`;
}
get x() {
return this.layout.x;
}
get y() {
return this.layout.y;
}
get width() {
return this.layout.width;
}
get height() {
return this.setting.height;
}
get name() {
return this._name;
}
async embedPage(targetPage) {
if (this.embeddedPage) return this.embeddedPage;
const pages = await targetPage.doc.embedPdf(this.pdf);
this.embeddedPage = pages[0];
return pages[0];
}
};
// src/utils/layout/calculate-page-layout.ts
var getMaxHeight = (els) => {
return els.reduce((x, s) => Math.max(x, s.height ?? 0), 0);
};
function calculatePageLayout(sectionSettings, opts) {
const { pageHeight, pageWidth, bodyHeightMinimumFactor } = opts;
const headerHeight = getMaxHeight(sectionSettings?.headers ?? []);
const footerHeight = getMaxHeight(sectionSettings?.footers ?? []);
const bodyHeight = pageHeight ? pageHeight - headerHeight - footerHeight + 2 : 0;
const backgroundHeight = pageHeight;
const headerY = pageHeight - headerHeight;
const footerY = footerHeight ? -1 : 0;
const bodyY = footerHeight - (pageHeight ? 1 : 0);
const backgroundY = 0;
if (bodyHeight < pageHeight * bodyHeightMinimumFactor) {
throw new Error(
`Header/footer too big. Page height: ${pageHeight}px, header: ${headerHeight}px, footer: ${footerHeight}px, body: ${bodyHeight}px.`
);
}
return {
header: {
width: pageWidth,
height: headerHeight,
x: 0,
y: headerY
},
footer: {
width: pageWidth,
height: footerHeight,
x: 0,
y: footerY
},
body: {
width: pageWidth,
height: bodyHeight,
x: 0,
y: bodyY
},
background: {
width: pageWidth,
height: backgroundHeight,
x: 0,
y: backgroundY
}
};
}
// src/utils/layout/has-page-numbers.ts
function hasPageNumbers(sectionSettings) {
if (!sectionSettings) return false;
for (const elements of Object.values(sectionSettings)) {
for (const el of elements) {
if (el.hasCurrentPageNumber || el.hasTotalPagesNumber) {
return true;
}
}
}
return false;
}
function hasSectionPageNumbers(sectionSettings) {
if (!sectionSettings) return false;
return sectionSettings.some((ss) => ss.hasCurrentPageNumber || ss.hasTotalPagesNumber);
}
// src/utils/layout/create-page-layout-settings.ts
function createPageLayoutSettings(sectionSettings, opts) {
const pageLayout = calculatePageLayout(sectionSettings, opts);
const transparentBg = !!sectionSettings?.backgrounds.length;
const { headers = [], footers = [], backgrounds = [] } = sectionSettings ?? {};
const hasAnySection = !!(headers.length || footers.length || backgrounds.length);
return {
height: opts.pageHeight,
width: opts.pageWidth,
hasPageNumbers: hasPageNumbers(sectionSettings),
hasAnySection,
pageCount: 0,
body: {
...pageLayout.body,
transparentBg
},
header: headers.length ? {
...pageLayout.header,
transparentBg,
hasPageNumbers: hasSectionPageNumbers(headers),
settings: headers
} : void 0,
footer: footers.length ? {
...pageLayout.footer,
transparentBg,
hasPageNumbers: hasSectionPageNumbers(footers),
settings: footers
} : void 0,
background: backgrounds.length ? {
...pageLayout.background,
transparentBg: false,
hasPageNumbers: hasSectionPageNumbers(backgrounds),
settings: backgrounds
} : void 0
};
}
// src/models/document-page.ts
var DocumentPage = class {
constructor(opts) {
this.sectionElements = [];
this.parent = opts.parent;
this.index = opts.index;
this.width = opts.width;
this.height = opts.height;
this.bodyMarginTop = opts.bodyMarginTop;
this.bodyMarginBottom = opts.bodyMarginBottom;
this.hasSections = opts.hasSections;
}
get viewPort() {
return {
width: this.width,
height: this.height
};
}
get html() {
return this.parent.html;
}
/**
* Create the layout and body element.
*
* This method will figure out heights and positions of
* all existing elements on this page and create a body
* element which will, in turn, give us a total page
* count for this document page.
*
* We need to know the number of pages to be able to
* construct other elements that might need to display
* current page / total page number.
*/
async createLayoutAndBody(sectionSettings, logger) {
const layoutOpts = {
pageHeight: this.height,
pageWidth: this.width,
bodyHeightMinimumFactor: this.parent.documentOptions?.bodyHeightMinimumFactor ?? 1 / 3
};
this.layout = createPageLayoutSettings(sectionSettings, layoutOpts);
await this.html.prepareSection({ documentPageIndex: this.index });
logger?.level3().start("[5.3.1] Print body to pdf buffer");
const uint8Array = await this.html.pdf({
width: this.layout.width,
height: this.layout.body.height,
margin: {
top: this.bodyMarginTop,
bottom: this.bodyMarginBottom
},
transparentBg: this.layout.body.transparentBg
});
logger?.level3().end();
const buffer = Buffer.from(uint8Array);
logger?.level3().start("[5.3.2] Load body pdf as PDFDocument");
const pdf = await import_pdf_lib.PDFDocument.load(uint8Array);
logger?.level3().end();
await this.html.resetVisibility();
this.layout.pageCount = pdf.getPageCount();
this.body = new BodyElement({
buffer,
pdf,
layout: this.layout.body
});
}
get previousDocumentPages() {
return this.parent.documentPages.slice(0, this.index);
}
get pageCountOffset() {
return this.previousDocumentPages.reduce((acc, doc) => acc + doc.layout.pageCount, 0);
}
};
// src/consts/paper-size.ts
var PAPER_SIZE = {
a0: { width: 841, height: 1189 },
a1: { width: 594, height: 841 },
a2: { width: 420, height: 594 },
a3: { width: 297, height: 420 },
a4: { width: 210, height: 297 },
a5: { width: 148, height: 210 },
a6: { width: 105, height: 148 },
letter: { width: 216, height: 279 },
legal: { width: 216, height: 356 },
tabloid: { width: 279, height: 432 },
ledger: { width: 432, height: 279 }
};
// src/utils/paper-defaults.ts
var hasFormat = (obj) => !!obj && "format" in obj && typeof obj.format === "string" && Object.keys(PAPER_SIZE).includes(obj.format);
var hasPpi = (obj) => !!obj && "ppi" in obj && typeof obj?.ppi === "number" && !isNaN(obj.ppi) && obj.ppi > 18 && obj.ppi < 42e3;
var hasWidth = (obj) => !!obj && "width" in obj && typeof obj?.width === "number" && !isNaN(obj.width) && obj.width > 1 && obj.width <= 42e4;
var hasHeight = (obj) => !!obj && "height" in obj && typeof obj?.height === "number" && !isNaN(obj.height) && obj.height > 1 && obj.height <= 42e4;
var convertMmToPx = (mm, ppi) => Math.round(mm * (ppi / 25.4));
var DEFAULT_FORMAT = "a4";
var DEFAULT_PPI = 72;
var DEFAULT_WIDTH = 595;
var DEFAULT_HEIGHT = 842;
var DEFAULT_BODY_MARGIN_TOP = 0;
var DEFAULT_BODY_MARGIN_BOTTOM = 0;
var PaperDefaults = class {
constructor(opts) {
this.ppi = hasPpi(opts) ? opts.ppi : DEFAULT_PPI;
if (hasFormat(opts)) {
this.format = opts.format;
this.width = convertMmToPx(PAPER_SIZE[this.format].width, this.ppi);
this.height = convertMmToPx(PAPER_SIZE[this.format].height, this.ppi);
} else if (hasWidth(opts) && hasHeight(opts)) {
this.format = void 0;
this.width = opts.width;
this.height = opts.height;
} else if (hasWidth(opts)) {
this.format = void 0;
this.width = opts.width;
this.height = DEFAULT_HEIGHT;
} else if (hasHeight(opts)) {
this.format = void 0;
this.width = DEFAULT_WIDTH;
this.height = opts.height;
} else {
this.format = DEFAULT_FORMAT;
this.width = DEFAULT_WIDTH;
this.height = DEFAULT_HEIGHT;
}
}
};
// src/utils/normalize-setting.ts
var capNumber = (num, min, max, base) => {
if (typeof num !== "number" || isNaN(num)) return base;
return Math.min(Math.max(num, min), max);
};
function normalizeSetting(setting) {
return {
index: setting.index,
width: capNumber(setting.width, 1, 42e4, DEFAULT_WIDTH),
height: capNumber(setting.height, 1, 42e4, DEFAULT_HEIGHT),
bodyMarginTop: capNumber(setting.bodyMarginTop, 0, 42e4, DEFAULT_BODY_MARGIN_TOP),
bodyMarginBottom: capNumber(setting.bodyMarginBottom, 0, 42e4, DEFAULT_BODY_MARGIN_BOTTOM),
hasSections: setting.hasSections || false
};
}
// src/evaluators/section-settings.ts
function evalSectionSettings(documentPageIndex) {
const variants = ["first", "last", "even", "odd", "default"];
const getElementHeight = (el) => {
return Math.ceil(
Math.max(el.clientHeight ?? 0, el.offsetHeight ?? 0, el.scrollHeight ?? 0, el.getBoundingClientRect().height ?? 0)
);
};
const hasCurrentPageNumber = (el) => {
const currentPageNumber = el.querySelector("current-page-number, span.page-number");
return !!currentPageNumber;
};
const hasTotalPagesNumber = (el) => {
const totalPagesNumber = el.querySelector("total-pages-number, span.total-pages");
return !!totalPagesNumber;
};
const isVariant = (s) => {
return !!s && variants.includes(s);
};
const getSettings = (el, physicalPageIndex) => {
let physicalPageType;
if (physicalPageIndex === void 0) {
physicalPageType = void 0;
} else {
const selectAttr = el.getAttribute("select");
physicalPageType = isVariant(selectAttr) ? selectAttr : "default";
}
return {
height: getElementHeight(el),
physicalPageIndex,
physicalPageType,
hasCurrentPageNumber: hasCurrentPageNumber(el),
hasTotalPagesNumber: hasTotalPagesNumber(el)
};
};
const getSection = (docPageEl2, type) => {
const sectionEl = docPageEl2.querySelector(`page-${type}`);
if (!sectionEl) return [];
const physicalPageEl = Array.from(sectionEl.querySelectorAll("physical-page"));
if (!physicalPageEl.length) {
return [getSettings(sectionEl)];
}
return physicalPageEl.map((el, index) => getSettings(el, index));
};
const docPageEls = document.querySelectorAll("document-page");
const docPageEl = docPageEls[documentPageIndex];
if (!docPageEl) return { headers: [], footers: [], backgrounds: [] };
const filterSections = (s) => s && s.height > 0;
return {
headers: getSection(docPageEl, "header").filter(filterSections),
footers: getSection(docPageEl, "footer").filter(filterSections),
backgrounds: getSection(docPageEl, "background").filter(filterSections)
};
}
// src/evaluators/prepare-section.ts
function evalPrepareSection(opts) {
function hideAllExcept(els, target) {
let shownElement;
Array.from(els).forEach((el, index) => {
if (typeof target === "number" && index === target || el.tagName.toLowerCase() === target) {
el.style.display = "block";
shownElement = el;
} else {
el.style.display = "none";
}
});
return shownElement;
}
function injectNumbers(el) {
if (opts.currentPageNumber) {
Array.from(el.querySelectorAll("current-page-number, span.page-number")).forEach((el2) => {
el2.textContent = String(opts.currentPageNumber);
});
}
if (opts.totalPagesNumber) {
Array.from(el.querySelectorAll("total-pages-number, span.total-pages")).forEach((el2) => {
el2.textContent = String(opts.totalPagesNumber);
});
}
}
const secType = opts.sectionType ? `page-${opts.sectionType}` : "page-body";
const docPage = hideAllExcept(document.querySelectorAll("document-page"), opts.documentPageIndex);
if (!docPage) return false;
const sectionEl = hideAllExcept(
docPage.querySelectorAll("page-background, page-header, page-body, page-footer"),
secType
);
if (!sectionEl) return false;
if (secType === "page-body") {
sectionEl.style.marginTop = "0px";
sectionEl.style.marginBottom = "0px";
}
if (!opts.sectionType || opts.physicalPageIndex === void 0) {
injectNumbers(sectionEl);
return true;
}
const subSecEl = hideAllExcept(sectionEl.querySelectorAll("physical-page"), opts.physicalPageIndex);
if (!subSecEl) return false;
injectNumbers(subSecEl);
return true;
}
// src/evaluators/template-normalize.ts
function evalTemplateNormalize(opts) {
const addPdfClass = opts?.addPdfClass ?? true;
const setBodyMargin = opts?.setBodyMargin ?? true;
const setBodyPadding = opts?.setBodyPadding ?? true;
const setBodyTransparent = opts?.setBodyTransparent ?? true;
const normalizeBody = opts?.normalizeBody ?? true;
const normalizeDocumentPage = opts?.normalizeDocumentPage ?? true;
if (addPdfClass) document.body.classList.add("pdf");
if (setBodyMargin) document.body.style.margin = "0";
if (setBodyPadding) document.body.style.padding = "0";
if (setBodyTransparent) document.body.style.backgroundColor = "transparent";
if (normalizeBody) {
const freeEls = Array.from(document.body.childNodes).filter(
(el) => !["DOCUMENT-PAGE", "SCRIPT", "STYLE"].includes(el.nodeName)
);
const hasDocumentPage = Array.from(document.body.children).some((el) => el.tagName === "DOCUMENT-PAGE");
if (freeEls.length && !hasDocumentPage) {
const docPage = document.createElement("document-page");
docPage.append(...freeEls);
document.body.append(docPage);
} else if (hasDocumentPage) {
freeEls.forEach((el) => el.remove());
}
}
if (normalizeDocumentPage) {
Array.from(document.querySelectorAll("document-page")).forEach((doc) => {
const docFreeEls = Array.from(doc.childNodes).filter(
(el) => !["PAGE-BODY", "PAGE-BACKGROUND", "PAGE-FOOTER", "PAGE-HEADER", "SCRIPT", "STYLE"].includes(el.nodeName)
);
const hasPageBody = Array.from(doc.children).some((el) => el.tagName === "PAGE-BODY");
if (docFreeEls.length && !hasPageBody) {
const pageBody = document.createElement("page-body");
pageBody.append(...docFreeEls);
doc.append(pageBody);
} else if (hasPageBody) {
docFreeEls.forEach((el) => el.remove());
}
Array.from(doc.querySelectorAll("page-body, page-header, page-footer, page-background")).forEach((el) => {
if (!el.childNodes.length) {
el.remove();
}
});
if (!doc.querySelectorAll("page-body, script, style").length) {
doc.remove();
}
});
}
}
// src/evaluators/template-settings.ts
function evalTemplateSettings(opts) {
const isFormat = (format) => typeof format === "string" && Object.keys(opts.size).includes(format);
const convertMmToPx2 = (mm, ppi) => Math.round(mm * (ppi / 25.4));
const getWxH = (str) => {
const guard = (x) => x && !isNaN(x) ? x : void 0;
const [w, h] = str.split("x").map((x) => guard(Number(x)));
return [w, h ?? w];
};
const getPageSettings = (docPageEl, index) => {
const attrFormat = docPageEl.getAttribute("format");
const attrPpi = Number(docPageEl.getAttribute("ppi"));
const attrSize = docPageEl.getAttribute("size");
const [attrWidth, attrHeight] = attrSize ? getWxH(attrSize) : [];
const ppi = attrPpi && attrPpi > 0 ? attrPpi : opts.default.ppi;
const hasSections = !!docPageEl.querySelector("page-header, page-footer, page-background");
let bodyMarginBottom = 0;
let bodyMarginTop = 0;
const pageBodyEl = docPageEl.querySelector("page-body");
if (pageBodyEl) {
const pageBodyStyle = window.getComputedStyle(pageBodyEl);
const marginTop = parseFloat(pageBodyStyle.marginTop);
const marginBottom = parseFloat(pageBodyStyle.marginBottom);
bodyMarginTop = isNaN(marginTop) ? 0 : Math.ceil(marginTop);
bodyMarginBottom = isNaN(marginBottom) ? 0 : Math.ceil(marginBottom);
}
let width, height;
if (isFormat(attrFormat)) {
const size = opts.size[attrFormat];
width = convertMmToPx2(size.width, ppi);
height = convertMmToPx2(size.height, ppi);
} else if (attrWidth && attrHeight) {
width = attrWidth;
height = attrHeight;
} else {
width = opts.default.width;
height = opts.default.height;
}
return {
index,
width,
height,
bodyMarginTop,
bodyMarginBottom,
hasSections
};
};
const docPageEls = Array.from(document.querySelectorAll("document-page"));
return docPageEls.map(getPageSettings);
}
// src/evaluators/reset-visibility.ts
function evalResetVisibility() {
const hideables = [
"document-page",
"page-background",
"page-header",
"page-body",
"page-footer",
"physical-page"
].join(", ");
Array.from(document.querySelectorAll(hideables)).forEach((el) => {
if (el.style.display === "none") {
el.style.display = "block";
}
});
}
// src/utils/adapter-puppeteer.ts
var HTMLAdapter = class {
constructor(browser) {
this._browser = browser;
}
get browser() {
if (!this._browser) throw new Error("Browser not set");
if ("connected" in this._browser) {
if (!this._browser.connected) throw new Error("Browser not connected");
} else {
if (!this._browser?.isConnected?.()) throw new Error("Browser not connected");
}
return this._browser;
}
get page() {
if (!this._page) throw new Error("Page not set");
if (this._page.isClosed()) throw new Error("Page is closed");
return this._page;
}
async newPage() {
if (this._page) throw new Error("Page already set");
this._page = await this.browser.newPage();
}
setPage(page) {
if (this._page) throw new Error("Page already set");
this._page = page;
}
releasePage() {
this._page = void 0;
}
setContent(content) {
return this.page.setContent(content, {
waitUntil: ["load", "networkidle0"]
});
}
setViewport(opts) {
return this.page.setViewport(opts);
}
normalize(opts) {
return this.page.evaluate(evalTemplateNormalize, opts);
}
async getTemplateSettings(opts) {
return this.page.evaluate(evalTemplateSettings, {
default: opts,
size: PAPER_SIZE
});
}
getSectionSettings(opts) {
return this.page.evaluate(evalSectionSettings, opts.index);
}
prepareSection(opts) {
return this.page.evaluate(evalPrepareSection, opts);
}
resetVisibility() {
return this.page.evaluate(evalResetVisibility);
}
/**
* There is some bug in the PDF generation process, where the height and
* the width of the resulting PDF page get smaller by approximate factor
* of 0.75. During this process, some rounding issues occur and sometimes,
* we end up with 2 pages instead of 1. Also, backgrounds sometimes get
* a narrow white line at the bottom.
*
* To mitigate this, we scale up the width and height by 0.75, as well as
* the scale, to keep the same appearance.
*/
pdf(opts) {
return this.page.pdf({
width: opts.width / 0.75,
height: opts.height / 0.75,
scale: 1 / 0.75,
margin: opts.margin,
omitBackground: opts.transparentBg,
printBackground: true
});
}
async close() {
if (this._page && !this._page.isClosed()) {
await this._page?.close();
this._page = void 0;
}
}
};
// src/utils/debug/time-logger.ts
function buildReportLine(obj, lvl, totalMs, totalLen) {
const pctNum = totalMs ? obj.duration / totalMs * 100 : 0;
const pct = pctNum === 100 ? " 100" : pctNum.toFixed(1).padStart(4, " ");
const dur = obj.duration.toString().padStart(totalLen, " ");
return `${dur}ms | ${pct}% | ${"".padStart(lvl * 2, " ")}${obj.name}`;
}
var TimeLogger = class {
constructor() {
this._nodes = /* @__PURE__ */ new Map();
this._report = "";
this.setupNode("session");
this.setupNode("level 1", "session");
this.setupNode("level 2", "level 1");
this.setupNode("level 3", "level 2");
}
setupNode(key, parentKey) {
const parent = parentKey ? this._nodes.get(parentKey) : void 0;
const defaultName = key[0].toUpperCase() + key.slice(1);
const timeNode = {
defaultName,
current: { name: "", start: 0, duration: 0 },
report: [],
parent
};
if (parent) parent.child = timeNode;
this._nodes.set(key, timeNode);
}
handleNode(key) {
const node = this._nodes.get(key);
return {
start: (name) => {
this.endNode(node);
this.startNode(node, name);
},
end: () => this.endNode(node)
};
}
startNode(node, name) {
if (node.parent && !node.parent.current.name) {
this.startNode(node.parent);
}
node.current.name = name || node.defaultName;
node.current.start = Date.now();
}
endNode(node) {
if (!node.current.name) return;
if (node.child) this.endNode(node.child);
node.current.duration = Date.now() - node.current.start;
const reportNode = { ...node.current };
if (node.child) {
this.endNode(node.child);
reportNode.children = [...node.child.report];
node.child.report = [];
}
node.report.push(reportNode);
this.resetTimeObject(node.current);
if (!node.parent) this.generateReport();
}
// Public methods remain similar but use handleNode
session() {
return this.handleNode("session");
}
level1() {
return this.handleNode("level 1");
}
level2() {
return this.handleNode("level 2");
}
level3() {
return this.handleNode("level 3");
}
resetTimeObject(timeObj) {
timeObj.name = "";
timeObj.start = 0;
timeObj.duration = 0;
}
generateReport() {
const session = this._nodes.get("session").report[0];
const totalMs = session.duration;
const totalLen = totalMs.toString().length;
const title = `${totalMs}ms | ${session.name}`;
const report = [
title,
"".padEnd(title.length, "="),
...session.children.flatMap((level1) => [
buildReportLine(level1, 0, totalMs, totalLen),
...level1.children.flatMap((level2) => [
buildReportLine(level2, 1, totalMs, totalLen),
...level2.children.map((level3) => buildReportLine(level3, 2, totalMs, totalLen))
])
])
];
this._report = report.join("\n");
}
get report() {
return this._report;
}
};
// src/utils/layout/build-pages.ts
var import_pdf_lib2 = require("pdf-lib");
// src/consts/physical-page.ts
var PhysicalPageSelector = /* @__PURE__ */ ((PhysicalPageSelector2) => {
PhysicalPageSelector2["FIRST"] = "first";
PhysicalPageSelector2["LAST"] = "last";
PhysicalPageSelector2["EVEN"] = "even";
PhysicalPageSelector2["ODD"] = "odd";
PhysicalPageSelector2["DEFAULT"] = "default";
return PhysicalPageSelector2;
})(PhysicalPageSelector || {});
var physical_page_default = PhysicalPageSelector;
// src/utils/select-section.ts
function containVariants(sectionSettings) {
return sectionSettings.some(
(section) => section.physicalPageType !== void 0 || section.physicalPageIndex !== void 0
);
}
function findVariant(variants, condition) {
return variants.find((variant) => variant.physicalPageType === condition);
}
function selectSection(sectionSettings, pageIndex, offset, count) {
if (!sectionSettings.length) return;
if (!containVariants(sectionSettings)) return sectionSettings[0];
const isFirst = pageIndex === 0;
const isLast = pageIndex === count - 1;
const isOdd = (pageIndex + 1 + offset) % 2 === 1;
return (isLast || isFirst ? findVariant(sectionSettings, isLast ? physical_page_default.LAST : physical_page_default.FIRST) : void 0) || findVariant(sectionSettings, isOdd ? physical_page_default.ODD : physical_page_default.EVEN) || findVariant(sectionSettings, physical_page_default.DEFAULT);
}
// src/utils/layout/build-pages.ts
async function createSectionElement(sectionType, setting, opts) {
const { documentPageIndex, currentPageNumber, totalPagesNumber, html, layout, logger } = opts;
const { physicalPageIndex } = setting;
html.prepareSection({
documentPageIndex,
sectionType,
physicalPageIndex,
currentPageNumber,
totalPagesNumber
});
logger?.level3().start(`[6.1.1] Rendering ${sectionType} section`);
const uint8Array = await html.pdf({
width: layout.width,
height: setting.height,
transparentBg: !!layout?.[sectionType]?.transparentBg
});
logger?.level3().end();
const buffer = Buffer.from(uint8Array);
const pdf = await import_pdf_lib2.PDFDocument.load(buffer);
return new SectionElement({
buffer,
pdf,
setting,
debug: {
type: sectionType,
pageNumber: currentPageNumber
},
layout: {
width: layout.width,
height: layout[sectionType].height,
x: 0,
y: layout[sectionType].y
}
});
}
async function resolveSectionElement(sectionType, opts) {
const setting = selectSection(
opts.layout?.[sectionType]?.settings ?? [],
opts.pageIndex,
opts.pageCountOffset,
opts.layout.pageCount
);
if (!setting) return void 0;
const element = opts.elements.find(
(el) => el.setting === setting && !el.setting.hasCurrentPageNumber && !el.setting.hasTotalPagesNumber
);
if (element) return element;
const newElement = await createSectionElement(sectionType, setting, opts);
opts.elements.push(newElement);
return newElement;
}
async function buildPages(opts) {
const { documentPageIndex, pageCountOffset, totalPagesNumber, layout, body, target, html, logger } = opts;
const { pageCount } = layout;
if (!pageCount) throw new Error("Document page has no pages");
if (!target) throw new Error("No target PDF document provided");
const pages = [];
const elements = [];
if (!layout.hasAnySection) {
logger?.level2().start("[6.3] Copy body pages (no sections)");
const copiedPages = await target.copyPages(body.pdf, body.pdf.getPageIndices());
copiedPages.forEach((page) => target.addPage(page));
logger?.level2().end();
return { pages, elements };
}
const pageIndices = Array.from({ length: pageCount }, (_, i) => i);
for (const pageIndex of pageIndices) {
const currentPageNumber = pageIndex + 1 + pageCountOffset;
const opts2 = {
documentPageIndex,
pageIndex,
pageCountOffset,
currentPageNumber,
totalPagesNumber,
elements,
layout,
html,
logger
};
logger?.level2().start(`[6.1] Resolve page ${pageIndex} section elements`);
const header = await resolveSectionElement("header", opts2);
const footer = await resolveSectionElement("footer", opts2);
const background = await resolveSectionElement("background", opts2);
logger?.level2().end();
logger?.level2().start(`[6.2] Embed and place page ${pageIndex} sections`);
const targetPage = target.addPage([layout.width, layout.height]);
await embedAndPlaceSection(targetPage, background);
await embedAndPlaceSection(targetPage, header);
await embedAndPlaceSection(targetPage, footer);
await embedAndPlaceBody(targetPage, body, pageIndex);
logger?.level2().end();
pages.push({
pageIndex,
currentPageNumber,
header,
footer,
background
});
}
if (opts.attachSegmentsForDebugging) {
for (const element of elements) {
await target.attach(element.buffer, element.name, {
mimeType: "application/pdf",
description: `${element.debug.type} section first created for page ${element.debug.pageNumber}`,
creationDate: /* @__PURE__ */ new Date(),
modificationDate: /* @__PURE__ */ new Date()
});
}
await target.attach(body.buffer, `${pageCountOffset + 1}-body.pdf`, {
mimeType: "application/pdf",
description: `Body element first created for page ${pageCountOffset + 1}`,
creationDate: /* @__PURE__ */ new Date(),
modificationDate: /* @__PURE__ */ new Date()
});
}
return { pages, elements };
}
async function embedAndPlaceSection(page, section) {
if (!section) return;
const embeddedPage = await section.embedPage(page);
page.drawPage(embeddedPage, {
x: section.x,
y: section.y,
width: section.width,
height: section.height
});
}
async function embedAndPlaceBody(page, body, idx) {
const [embeddedPage] = await body.embedPageIdx(page, idx);
page.drawPage(embeddedPage, {
x: body.x,
y: body.y,
width: body.width,
height: body.height
});
}
// src/utils/set-document-metadata.ts
function isValidDate(date) {
return date instanceof Date && !isNaN(date.getTime());
}
function isValidKeyword(keyword) {
return Array.isArray(keyword) && !!keyword.length && keyword.every((k) => typeof k === "string");
}
function typedEntries(obj) {
return Object.entries(obj);
}
var adapters = {
title: (pdf, value) => pdf.setTitle(value),
author: (pdf, value) => pdf.setAuthor(value),
subject: (pdf, value) => pdf.setSubject(value),
keywords: (pdf, value) => isValidKeyword(value) && pdf.setKeywords(value),
producer: (pdf, value) => pdf.setProducer(value),
creator: (pdf, value) => pdf.setCreator(value),
creationDate: (pdf, value) => isValidDate(value) && pdf.setCreationDate(value),
modificationDate: (pdf, value) => isValidDate(value) && pdf.setModificationDate(value)
};
function setDocumentMetadata(pdf, meta) {
if (!meta || Object.keys(meta).length === 0) return;
for (const [key, value] of typedEntries(meta)) {
if (value !== void 0 && value !== null && key in adapters) {
adapters[key](pdf, value);
}
}
}
// package.json
var version = "1.1.1";
// src/index.ts
var DeclarativePDF = class {
/**
*
* @param browser A puupeteer browser instance, prepared for use
* @param opts Various options for the PDF generator
*/
constructor(browser, opts) {
this.documentPages = [];
this.html = new HTMLAdapter(browser);
this.defaults = new PaperDefaults(opts?.defaults);
this.normalize = opts?.normalize;
this.debug = opts?.debug ?? {};
this.documentOptions = opts?.document;
}
get totalPagesNumber() {
return this.documentPages.reduce((acc, doc) => acc + doc.layout.pageCount, 0);
}
/**
* Generates a pdf buffer from string containing html template.
*
* When calling this method, it is expected that:
* - the browser is initialized and ready
* - the template is a valid HTML document -OR- a valid Page instance
*/
async generate(input) {
const isPageHandledInternally = typeof input === "string";
const logger = this.debug.timeLog ? new TimeLogger() : void 0;
logger?.session().start(`DeclarativePDF v${version} rendering ${this.debug.pdfName ?? "PDF"}`);
this.documentPages = [];
try {
if (isPageHandledInternally) {
logger?.level1().start("[1] Opening new tab");
await this.html.newPage();
logger?.level1().start("[2] Setting content and loading html");
await this.html.setContent(input);
} else {
this.html.setPage(input);
}
logger?.level1().start("[3] Normalizing content");
await this.html.normalize(this.normalize);
logger?.level1().start("[4] Getting document page settings from DOM");
await this.getDocumentPageSettings();
if (!this.documentPages.length) throw new Error("No document pages found");
logger?.level1().start("[5] Build page layout and body");
await this.buildLayoutForEachDocumentPage(logger);
logger?.level1().end();
if (this.documentPages.length === 1 && !this.documentPages[0].hasSections) {
const meta = this.documentOptions?.meta;
if (meta) {
const pdf2 = await import_pdf_lib3.PDFDocument.load(this.documentPages[0].body.buffer);
setDocumentMetadata(pdf2, meta);
return Buffer.from(await pdf2.save());
}
return this.documentPages[0].body.buffer;
}
logger?.level1().start("[6] Process sections and build final PDF");
const pdf = await this.buildPDF(logger);
logger?.level1().end();
if (isPageHandledInternally) {
logger?.level1().start("[7] Closing tab");
await this.html.close();
} else {
this.html.releasePage();
}
logger?.session().end();
const report = logger?.report;
if (report) {
console.log(report);
const reportPdf = await import_pdf_lib3.PDFDocument.create();
const font = await reportPdf.embedFont(import_pdf_lib3.StandardFonts.Courier);
const page = reportPdf.addPage(import_pdf_lib3.PageSizes.A4);
page.setFont(font);
report.split("\n").forEach((line, index) => {
let color = (0, import_pdf_lib3.rgb)(0, 0, 0.8);
if (line.includes("| [")) color = (0, import_pdf_lib3.rgb)(0.6, 0.6, 0.6);
else if (line.includes("| [")) color = (0, import_pdf_lib3.rgb)(0.8, 0.8, 0.8);
page.drawText(line, { x: 50, y: 750 - index * 12, size: 10, color });
});
const reportBytes = await reportPdf.save();
pdf.attach(reportBytes, "time-log.pdf", {
mimeType: "application/pdf",
description: "Time log report",
creationDate: /* @__PURE__ */ new Date(),
modificationDate: /* @__PURE__ */ new Date()
});
}
return Buffer.from(await pdf.save());
} catch (error) {
if (isPageHandledInternally) {
logger?.level1().start("[x] Closing tab after error");
await this.html.close();
} else {
this.html.releasePage();
}
logger?.session().end();
const report = logger?.report;
if (report) console.log(report);
throw error;
}
}
/**
* Creates the document page models.
*
* This method will evaluate the template settings and create a new
* document page model for each setting parsed from the HTML template.
*/
async getDocumentPageSettings() {
const documentPageSettings = await this.html.getTemplateSettings({
width: this.defaults.width,
height: this.defaults.height,
ppi: this.defaults.ppi
});
documentPageSettings.forEach((setting) => {
this.documentPages.push(new DocumentPage({ parent: this, ...normalizeSetting(setting) }));
});
}
/**
* Initializes the document page models.
*
* For every created document page model, this method sets desired
* viewport and gets section settings from the DOM to create layout
* (heights and positions). It then convert to pdf the body element
* from which we get the number of pages and finally have all the
* information needed to build the final PDF.
*/
async buildLayoutForEachDocumentPage(logger) {
for (const [index, doc] of this.documentPages.entries()) {
logger?.level2().start("[5.1] Set viewport");
await this.html.setViewport(doc.viewPort);
logger?.level2().end();
let settings;
if (doc.hasSections) {
logger?.level2().start("[5.2] Get section settings");
settings = await this.html.getSectionSettings({ index });
logger?.level2().end();
}
logger?.level2().start("[5.3] Create layout and body");
await doc.createLayoutAndBody(settings, logger);
logger?.level2().end();
}
}
async buildPDF(logger) {
const outputPDF = await import_pdf_lib3.PDFDocument.create();
for (const doc of this.documentPages) {
await buildPages({
documentPageIndex: doc.index,
pageCountOffset: doc.pageCountOffset,
totalPagesNumber: this.totalPagesNumber,
layout: doc.layout,
body: doc.body,
target: outputPDF,
html: this.html,
logger,
attachSegmentsForDebugging: this.debug.attachSegments
});
}
const meta = this.documentOptions?.meta;
if (meta) setDocumentMetadata(outputPDF, meta);
return outputPDF;
}
};