UNPKG

pdfmkr

Version:

Generate PDF documents from JavaScript objects

1,499 lines (1,468 loc) 88 kB
var __defProp = Object.defineProperty; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; // src/api/colors.ts var colors_exports = {}; __export(colors_exports, { namedColors: () => namedColors }); var namedColors = { black: rgb(0, 0, 0), gray: rgb(0.5, 0.5, 0.5), white: rgb(1, 1, 1), red: rgb(1, 0, 0), blue: rgb(0, 0, 1), green: rgb(0, 0.5, 0), cyan: rgb(0, 1, 1), magenta: rgb(1, 0, 1), yellow: rgb(1, 1, 0), lightgray: rgb(0.83, 0.83, 0.83), darkgray: rgb(0.66, 0.66, 0.66) }; function rgb(red, green, blue) { return [red, green, blue]; } // src/api/document.ts var document_exports = {}; // src/api/graphics.ts var graphics_exports = {}; __export(graphics_exports, { circle: () => circle, line: () => line, path: () => path, rect: () => rect }); function line(x1, y1, x2, y2, props) { return { ...props, type: "line", x1, y1, x2, y2 }; } function rect(x, y, width, height, props) { return { ...props, type: "rect", x, y, width, height }; } function circle(cx, cy, r, props) { return { ...props, type: "circle", cx, cy, r }; } function path(d, props) { return { ...props, type: "path", d }; } // src/api/layout.ts var layout_exports = {}; __export(layout_exports, { columns: () => columns, image: () => image, rows: () => rows, text: () => text }); function text(text2, props) { return { ...props, text: text2 }; } function image(image2, props) { return { ...props, image: image2 }; } function columns(columns2, props) { return { ...props, columns: columns2 }; } function rows(rows2, props) { return { ...props, rows: rows2 }; } // src/api/PdfMaker.ts var PdfMaker_exports = {}; __export(PdfMaker_exports, { PdfMaker: () => PdfMaker, makePdf: () => makePdf }); // src/font-store.ts import fontkit from "@pdf-lib/fontkit"; // src/binary-data.ts import { decodeFromBase64DataUri } from "pdf-lib"; // src/print-value.ts function printValue(value, refs) { if (typeof value === "string") return `'${value}'`; if (Array.isArray(value)) return printArray(value, refs); if (value instanceof Date) return `Date ${value.toISOString()}`; if (value instanceof Function) { return value.name ? `function ${value.name}` : "anonymous function"; } if (value instanceof ArrayBuffer) return `ArrayBuffer ${printArray([...new Uint8Array(value)])}`; if (ArrayBuffer.isView(value)) { return `${value.constructor.name} ${printArray([...new Uint8Array(value.buffer)])}`; } const str = `${value}`; if (str === "[object Object]") return printObject(value, refs); return str; } function printArray(array, refs) { if (refs?.includes(array)) return "recursive ref"; const maxElements = 8; const content = array.slice(0, maxElements).map((v) => printValue(v, [...refs ?? [], array])).join(", "); const tail = array.length > maxElements ? ", \u2026" : ""; return `[${content}${tail}]`; } function printObject(object, refs) { if (refs?.includes(object)) return "recursive ref"; const maxEntries = 8; const entries = Object.entries(object); const tail = entries.length > maxEntries ? ", \u2026" : ""; const main = entries.slice(0, maxEntries).map(([key, value]) => `${key}: ${printValue(value, [...refs ?? [], object])}`).join(", "); return `{${main}${tail}}`; } // src/types.ts function pickDefined(obj) { const result = {}; for (const key in obj) { if (typeof obj[key] !== "undefined") { result[key] = obj[key]; } } return result; } function readFrom(object, name, type) { return readAs(object[name], name, type); } function readAs(value, name, type) { try { return asType(value, type); } catch (error) { if (error instanceof Error && error.message === "Missing value") { throw new TypeError(`Missing value for "${name}"`); } if (error instanceof Error && error.message?.startsWith('Invalid value for "')) { const tail = error.message.replace(/^Invalid value for "/, ""); throw new TypeError(`Invalid value for "${name}/${tail}`); } const errorStr = error instanceof Error ? error.message : String(error); throw new TypeError(`Invalid value for "${name}": ${errorStr}`); } } var types = { boolean: () => readBoolean, date: () => readDate, string: (options) => (value) => readString(value, options), number: (options) => (value) => readNumber(value, options), array: (items, options) => (value) => readArray(value, items, options), object: (properties, options) => (value) => readObject(value, properties, options) }; function optional(type) { return (value) => { if (value === void 0) return void 0; return type ? asType(value, type) : value; }; } function required(type) { return (value) => { if (value === void 0) throw new TypeError(`Missing value`); return type ? asType(value, type) : value; }; } function dynamic(type, name) { return (value) => { if (typeof value !== "function") { const result = asType(value, type); return () => result; } const subject = name ? `Supplied function for "${name}"` : "Supplied function"; return (...args) => { const result = safeCall(value, args, subject); try { return asType(result, type); } catch (error) { throw new Error(`${subject} returned invalid value`, { cause: error }); } }; }; } function safeCall(fn, args, subject) { try { return fn(...args); } catch (error) { throw new Error(`${subject} threw error`, { cause: error }); } } function asType(value, type) { if (type == null) return value; if (typeof type === "function") return type(value); throw new Error(`Invalid type definition: ${printValue(type)}`); } function readBoolean(value) { if (typeof value === "boolean") return value; throw typeError("boolean", value); } function readString(value, options) { if (typeof value !== "string") throw typeError("string", value); if (options?.enum && !options.enum.includes(value)) { throw typeError(`one of (${options.enum.map((s) => `'${s}'`).join(", ")})`, value); } if (options?.pattern && !options.pattern.test(value)) { throw typeError(`string matching pattern ${options.pattern}`, value); } return value; } function readNumber(input, options) { if (!Number.isFinite(input)) throw typeError("number", input); const num = input; if (options?.minimum != null && !(num >= options.minimum)) { throw typeError(`number >= ${options.minimum}`, input); } if (options?.maximum != null && !(num <= options.maximum)) { throw typeError(`number <= ${options.maximum}`, input); } return num; } function readDate(value) { if (value instanceof Date) return value; throw typeError("Date", value); } function readArray(input, items, options) { if (!Array.isArray(input)) throw typeError("array", input); if (options?.minItems != null && input.length < options.minItems) { throw typeError(`array with minimum length ${options.minItems}`, input); } if (options?.maxItems != null && input.length > options.maxItems) { throw typeError(`array with maximum length ${options.maxItems}`, input); } if (options?.uniqueItems === true && !isUniq(input)) { throw typeError(`array with unique items`, input); } return items ? mapItems(input, items) : input; } function isUniq(array) { return array.every((v, i, a) => a.indexOf(v) === i); } function mapItems(array, type) { return array.map((item, idx) => readAs(item, idx.toString(), type)); } function readObject(input, properties, options) { if (!isObject(input)) throw typeError("object", input); if (options?.minProperties != null && Object.keys(input).length < options.minProperties) { throw typeError(`object with min. ${options.minProperties} properties`, input); } if (options?.maxProperties != null && Object.keys(input).length > options.maxProperties) { throw typeError(`object with max. ${options.maxProperties} properties`, input); } return properties ? mapObject(input, properties) : input; } function mapObject(obj, properties) { return pickDefined( Object.fromEntries( Object.entries(properties).map(([key, type]) => { return [key, readFrom(obj, key, type)]; }) ) ); } function isObject(value) { return value != null && typeof value === "object" && !Array.isArray(value) && value.toString() === "[object Object]"; } function typeError(expected, value) { return new TypeError(`Expected ${expected}, got: ${printValue(value)}`); } // src/binary-data.ts function parseBinaryData(input) { if (input instanceof Uint8Array) return input; if (input instanceof ArrayBuffer) return new Uint8Array(input); if (typeof input === "string") return decodeFromBase64DataUri(input); throw typeError("Uint8Array, ArrayBuffer, or base64-encoded string", input); } // src/fonts.ts import { CustomFontSubsetEmbedder, PDFFont } from "pdf-lib"; function readFonts(input) { return Object.entries(readObject(input)).flatMap(([name, fontDef]) => { return readAs(fontDef, name, required(types.array(readFont))).map( (font) => ({ family: name, ...font }) ); }); } function readFont(input) { const obj = readObject(input, { italic: optional((value) => readBoolean(value) || void 0), bold: optional((value) => readBoolean(value) || void 0), data: required(parseBinaryData) }); return { style: obj.italic ? "italic" : "normal", weight: obj.bold ? 700 : 400, data: obj.data }; } function registerFont(font, pdfDoc) { const registeredFonts = pdfDoc._pdfmkr_registeredFonts ??= {}; if (font.key in registeredFonts) return registeredFonts[font.key]; const ref = pdfDoc.context.nextRef(); const embedder = new CustomFontSubsetEmbedder(font.fkFont, font.data); const pdfFont = PDFFont.of(ref, pdfDoc, embedder); pdfDoc.fonts.push(pdfFont); registeredFonts[font.key] = ref; return ref; } function findRegisteredFont(font, pdfDoc) { const registeredFonts = pdfDoc._pdfmkr_registeredFonts ??= {}; const ref = registeredFonts[font.key]; if (ref) { return pdfDoc.fonts?.find((font2) => font2.ref === ref); } } function weightToNumber(weight) { if (weight === "normal") { return 400; } if (weight === "bold") { return 700; } if (typeof weight !== "number" || !isFinite(weight) || weight < 1 || weight > 1e3) { throw new Error(`Invalid font weight: ${printValue(weight)}`); } return weight; } // src/font-store.ts var FontStore = class { #fontDefs; #fontCache = {}; constructor(fontDefs) { this.#fontDefs = fontDefs ?? []; } registerFont(data, config) { const fkFont = fontkit.create(data); const family = config?.family ?? fkFont.familyName ?? "Unknown"; const style = config?.style ?? extractStyle(fkFont); const weight = weightToNumber(config?.weight ?? extractWeight(fkFont)); this.#fontDefs.push({ family, style, weight, data, fkFont }); this.#fontCache = {}; } async selectFont(selector) { const cacheKey = [ selector.fontFamily ?? "any", selector.fontStyle ?? "normal", selector.fontWeight ?? "normal" ].join(":"); try { return await (this.#fontCache[cacheKey] ??= this._loadFont(selector, cacheKey)); } catch (error) { const { fontFamily: family, fontStyle: style, fontWeight: weight } = selector; const selectorStr = `'${family}', style=${style ?? "normal"}, weight=${weight ?? "normal"}`; throw new Error(`Could not load font for ${selectorStr}`, { cause: error }); } } _loadFont(selector, key) { const selectedFont = selectFont(this.#fontDefs, selector); const data = parseBinaryData(selectedFont.data); const fkFont = selectedFont.fkFont ?? fontkit.create(data); return Promise.resolve( pickDefined({ key, name: fkFont.fullName ?? fkFont.postscriptName ?? selectedFont.family, data, style: selector.fontStyle ?? "normal", weight: weightToNumber(selector.fontWeight ?? 400), fkFont }) ); } }; function selectFont(fontDefs, selector) { if (!fontDefs.length) { throw new Error("No fonts defined"); } const fontsWithMatchingFamily = selector.fontFamily ? fontDefs.filter((def) => def.family === selector.fontFamily) : fontDefs; if (!fontsWithMatchingFamily.length) { const uniqueFamilies = [...new Set(fontDefs.map((f) => f.family))]; throw new Error( `No matching font found for family '${selector.fontFamily}'. Registered families are: '${uniqueFamilies.join("', '")}'.` ); } let fontsWithMatchingStyle = fontsWithMatchingFamily.filter( (def) => def.style === (selector.fontStyle ?? "normal") ); if (!fontsWithMatchingStyle.length) { fontsWithMatchingStyle = fontsWithMatchingFamily.filter( (def) => def.style === "italic" && selector.fontStyle === "oblique" || def.style === "oblique" && selector.fontStyle === "italic" ); } if (!fontsWithMatchingStyle.length) { const { fontFamily: family, fontStyle: style } = selector; const selectorStr = `'${family}', style=${style ?? "normal"}`; throw new Error(`No matching font found for ${selectorStr}`); } const selected = selectFontForWeight(fontsWithMatchingStyle, selector.fontWeight ?? "normal"); if (!selected) { const { fontFamily: family, fontStyle: style, fontWeight: weight } = selector; const selectorStr = `'${family}', style=${style ?? "normal"}, weight=${weight ?? "normal"}`; throw new Error(`No matching font found for ${selectorStr}`); } return selected; } function selectFontForWeight(fonts, weight) { const weightNum = weightToNumber(weight); const font = fonts.find((font2) => font2.weight === weightNum); if (font) return font; const ascending = fonts.slice().sort((a, b) => a.weight - b.weight); const descending = ascending.slice().reverse(); if (weightNum >= 400 && weightNum <= 500) { const font2 = ascending.find((font3) => font3.weight > weightNum && font3.weight <= 500) ?? descending.find((font3) => font3.weight < weightNum) ?? ascending.find((font3) => font3.weight > 500); if (font2) return font2; } if (weightNum < 400) { const font2 = descending.find((font3) => font3.weight < weightNum) ?? ascending.find((font3) => font3.weight > weightNum); if (font2) return font2; } if (weightNum > 500) { const font2 = ascending.find((font3) => font3.weight > weightNum) ?? descending.find((font3) => font3.weight < weightNum); if (font2) return font2; } throw new Error(`Could not find font for weight ${weight}`); } function extractStyle(font) { if (font.italicAngle === 0) return "normal"; if ((font.fullName ?? font.postscriptName)?.toLowerCase().includes("oblique")) return "oblique"; return "italic"; } function extractWeight(font) { return font["OS/2"]?.usWeightClass ?? 400; } // src/base64.ts var base64Lookup = createBase64LookupTable(); function decodeBase64(base64) { if (base64.length % 4 !== 0) { throw new Error("Invalid base64 string: length must be a multiple of 4"); } const len = base64.length; const padding = base64[len - 1] === "=" ? base64[len - 2] === "=" ? 2 : 1 : 0; const bufferLength = len * 3 / 4 - padding; const bytes = new Uint8Array(bufferLength); let byteIndex = 0; for (let i = 0; i < len; i += 4) { const encoded1 = lookup(base64, i); const encoded2 = lookup(base64, i + 1); const encoded3 = lookup(base64, i + 2); const encoded4 = lookup(base64, i + 3); bytes[byteIndex++] = encoded1 << 2 | encoded2 >> 4; if (base64[i + 2] !== "=") bytes[byteIndex++] = (encoded2 & 15) << 4 | encoded3 >> 2; if (base64[i + 3] !== "=") bytes[byteIndex++] = (encoded3 & 3) << 6 | encoded4; } return bytes; } function lookup(string, pos) { const code = string.charCodeAt(pos); if (code === 61) return 0; if (code < base64Lookup.length) { const value = base64Lookup[code]; if (value !== 255) { return value; } } throw new Error(`Invalid Base64 character '${string[pos]}' at position ${pos}`); } function createBase64LookupTable() { const base64Chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; const table = new Uint8Array(256).fill(255); for (let i = 0; i < base64Chars.length; i++) { table[base64Chars.charCodeAt(i)] = i; } return table; } // src/fs.ts var readRelativeFile = async (rootDir, relPath) => { let fs; let path2; try { fs = await import("node:fs/promises"); path2 = await import("node:path"); } catch { throw new Error("File system is not available in this environment"); } if (path2.isAbsolute(relPath)) { throw new Error(`Path is not relative: '${relPath}'`); } const resolvedPath = path2.resolve(rootDir, relPath); const realPath = await fs.realpath(resolvedPath); try { return await fs.readFile(realPath); } catch (error) { throw new Error(`Failed to load file '${realPath}'`, { cause: error }); } }; // src/data-loader.ts function createDataLoader(config) { const loaders = { http: loadHttp, https: loadHttp, data: loadData, file: loadFile }; return async function(url) { const schema = getUrlSchema(url).slice(0, -1); const loader = loaders[schema]; if (!loader) { throw new Error(`URL not supported: '${url}'`); } return await loader(url, config); }; } function getUrlSchema(url) { try { return new URL(url).protocol; } catch { throw new Error(`Invalid URL: '${url}'`); } } function loadData(url) { if (!url.startsWith("data:")) { throw new Error(`Not a data URL: '${url}'`); } const endOfHeader = url.indexOf(","); if (endOfHeader === -1) { throw new Error(`Invalid data URL: '${url}'`); } const header = url.slice(5, endOfHeader); if (!header.endsWith(";base64")) { throw new Error(`Unsupported encoding in data URL: '${url}'`); } const dataPart = url.slice(endOfHeader + 1); const data = new Uint8Array(decodeBase64(dataPart)); return { data }; } async function loadHttp(url) { if (!url.startsWith("http:") && !url.startsWith("https:")) { throw new Error(`Not a http(s) URL: '${url}'`); } const response = await fetch(url); if (!response.ok) { throw new Error(`Received ${response.status} ${response.statusText}`); } const data = new Uint8Array(await response.arrayBuffer()); return { data }; } async function loadFile(url, config) { if (!url.startsWith("file:")) { throw new Error(`Not a file URL: '${url}'`); } if (!config?.resourceRoot) { throw new Error("No resource root defined"); } const urlPath = decodeURIComponent(new URL(url).pathname); const relPath = urlPath.replace(/^\//g, ""); const data = new Uint8Array(await readRelativeFile(config.resourceRoot, relPath)); return { data }; } // src/images/jpeg.ts function isJpeg(data) { return data[0] === 255 && data[1] === 216 && data[2] === 255; } function readJpegInfo(data) { if (!isJpeg(data)) { throw new Error("Invalid JPEG data"); } let pos = 0; const len = data.length; let info; while (pos < len - 1) { if (data[pos++] !== 255) { continue; } const type = data[pos++]; if (type === 0) { continue; } if (type >= 208 && type <= 217) { continue; } const length = readUint16BE(data, pos); pos += 2; if (type >= 192 && type <= 207 && type !== 196 && type !== 200 && type !== 204) { const bitDepth = data[pos]; const height = readUint16BE(data, pos + 1); const width = readUint16BE(data, pos + 3); const colorSpace = getColorSpace(data[pos + 5]); info = { width, height, bitDepth, colorSpace }; } pos += length - 2; } if (!info) { throw new Error("Invalid JPEG data"); } return info; } function getColorSpace(colorSpace) { if (colorSpace === 1) return "grayscale"; if (colorSpace === 3) return "rgb"; if (colorSpace === 4) return "cmyk"; throw new Error("Invalid color space"); } function readUint16BE(buffer, offset) { return buffer[offset] << 8 | buffer[offset + 1]; } // src/images/png.ts function isPng(data) { return hasBytes(data, 0, [137, 80, 78, 71, 13, 10, 26, 10]); } function readPngInfo(data) { if (!isPng(data)) { throw new Error("Invalid PNG data"); } if (data[12] !== 73 || data[13] !== 72 || data[14] !== 68 || data[15] !== 82) { throw new Error("Invalid PNG data"); } if (data.length < 33) { throw new Error("Invalid PNG data"); } const width = readUint32BE(data, 16); const height = readUint32BE(data, 20); const bitDepth = data[24]; const colorType = data[25]; const interlacing = data[28]; return { width, height, bitDepth, colorSpace: getColorSpace2(colorType), hasAlpha: colorType === 4 || colorType === 6, isIndexed: colorType === 3, isInterlaced: interlacing === 1 }; } function getColorSpace2(value) { if (value === 0 || value === 4) return "grayscale"; if (value === 2 || value === 3 || value === 6) return "rgb"; throw new Error("Invalid color space"); } function readUint32BE(data, offset) { return data[offset] << 24 | data[offset + 1] << 16 | data[offset + 2] << 8 | data[offset + 3]; } function hasBytes(data, offset, bytes) { for (let i = 0; i < bytes.length; i++) { if (data[offset + i] !== bytes[i]) return false; } return true; } // src/image-store.ts var ImageStore = class { #images; #imageCache = {}; #dataLoader; constructor(images) { this.#images = images ?? []; this.#dataLoader = createDataLoader(); } setResourceRoot(root) { this.#dataLoader = createDataLoader({ resourceRoot: root }); } selectImage(url) { return this.#imageCache[url] ??= this.loadImage(url); } async loadImage(url) { const data = await this.loadImageData(url); const format = determineImageFormat(data); const { width, height } = format === "png" ? readPngInfo(data) : readJpegInfo(data); return { url, format, data, width, height }; } async loadImageData(url) { const imageDef = this.#images.find((image2) => image2.name === url); if (imageDef) { return parseBinaryData(imageDef.data); } const urlSchema = /^(\w+):/.exec(url)?.[1]; try { if (urlSchema) { const { data: data2 } = await this.#dataLoader(url); return data2; } console.warn( `Loading images from file names is deprecated ('${url}'). Use file:/ URLs instead.` ); const data = await readRelativeFile("/", url.replace(/^\/+/, "")); return new Uint8Array(data); } catch (error) { throw new Error(`Could not load image '${url}'`, { cause: error }); } } }; function determineImageFormat(data) { if (isPng(data)) return "png"; if (isJpeg(data)) return "jpeg"; throw new Error("Unknown image format"); } // src/api/sizes.ts var sizes_exports = {}; __export(sizes_exports, { paperSizes: () => paperSizes }); var paperSizes = { "4A0": { width: 4767.87, height: 6740.79 }, "2A0": { width: 3370.39, height: 4767.87 }, A0: { width: 2383.94, height: 3370.39 }, A1: { width: 1683.78, height: 2383.94 }, A2: { width: 1190.55, height: 1683.78 }, A3: { width: 841.89, height: 1190.55 }, A4: { width: 595.28, height: 841.89 }, A5: { width: 419.53, height: 595.28 }, A6: { width: 297.64, height: 419.53 }, A7: { width: 209.76, height: 297.64 }, A8: { width: 147.4, height: 209.76 }, A9: { width: 104.88, height: 147.4 }, A10: { width: 73.7, height: 104.88 }, B0: { width: 2834.65, height: 4008.19 }, B1: { width: 2004.09, height: 2834.65 }, B2: { width: 1417.32, height: 2004.09 }, B3: { width: 1000.63, height: 1417.32 }, B4: { width: 708.66, height: 1000.63 }, B5: { width: 498.9, height: 708.66 }, B6: { width: 354.33, height: 498.9 }, B7: { width: 249.45, height: 354.33 }, B8: { width: 175.75, height: 249.45 }, B9: { width: 124.72, height: 175.75 }, B10: { width: 87.87, height: 124.72 }, C0: { width: 2599.37, height: 3676.54 }, C1: { width: 1836.85, height: 2599.37 }, C2: { width: 1298.27, height: 1836.85 }, C3: { width: 918.43, height: 1298.27 }, C4: { width: 649.13, height: 918.43 }, C5: { width: 459.21, height: 649.13 }, C6: { width: 323.15, height: 459.21 }, C7: { width: 229.61, height: 323.15 }, C8: { width: 161.57, height: 229.61 }, C9: { width: 113.39, height: 161.57 }, C10: { width: 79.37, height: 113.39 }, RA0: { width: 2437.8, height: 3458.27 }, RA1: { width: 1729.13, height: 2437.8 }, RA2: { width: 1218.9, height: 1729.13 }, RA3: { width: 864.57, height: 1218.9 }, RA4: { width: 609.45, height: 864.57 }, SRA0: { width: 2551.18, height: 3628.35 }, SRA1: { width: 1814.17, height: 2551.18 }, SRA2: { width: 1275.59, height: 1814.17 }, SRA3: { width: 907.09, height: 1275.59 }, SRA4: { width: 637.8, height: 907.09 }, Executive: { width: 521.86, height: 756 }, Folio: { width: 612, height: 936 }, Legal: { width: 612, height: 1008 }, Letter: { width: 612, height: 792 }, Tabloid: { width: 792, height: 1224 } }; // src/box.ts var ZERO_EDGES = Object.freeze(parseEdges(0)); function subtractEdges(box, edges) { return { x: box.x + (edges?.left ?? 0), y: box.y + (edges?.top ?? 0), width: Math.max(0, box.width - (edges?.left ?? 0) - (edges?.right ?? 0)), height: Math.max(0, box.height - (edges?.top ?? 0) - (edges?.bottom ?? 0)) }; } function parseEdges(input) { if (typeof input === "number" || typeof input === "string") { const value = parseLength(input); return { right: value, left: value, top: value, bottom: value }; } if (isObject(input)) { const obj = input; return { right: parseLength(obj.right ?? obj.x ?? 0), left: parseLength(obj.left ?? obj.x ?? 0), top: parseLength(obj.top ?? obj.y ?? 0), bottom: parseLength(obj.bottom ?? obj.y ?? 0) }; } throw typeError("number, length string, or object", input); } function parseLength(input) { if (typeof input === "number" && Number.isFinite(input)) { return input; } if (typeof input === "string") { const unit = input.slice(-2); if (unit === "pt" || unit === "in" || unit === "mm" || unit === "cm") { const value = parseFloat(input.slice(0, -2)); if (Number.isFinite(value)) { return convertToPt(value, unit); } } } throw typeError("number or length string", input); } function convertToPt(value, fromUnit) { switch (fromUnit) { case "pt": return value * 1; case "in": return value * 72; case "mm": return value * 72 / 25.4; case "cm": return value * 72 / 2.54; default: throw new TypeError(`Invalid unit: '${fromUnit}'`); } } // src/guides.ts import { rgb as rgb2 } from "pdf-lib"; // src/utils.ts function compact(array) { return array.filter(Boolean); } function omit(obj, ...keys) { const result = { ...obj }; for (const key of keys) { delete result[key]; } return result; } function multiplyMatrices(matrix1, matrix2) { const result = []; const [a, b, c, d, e, f] = matrix1; const [g, h, i, j, k, l] = matrix2; result[0] = a * g + c * h; result[1] = b * g + d * h; result[2] = a * i + c * j; result[3] = b * i + d * j; result[4] = a * k + c * l + e; result[5] = b * k + d * l + f; return result; } function round(n, precision = 6) { const factor = Math.pow(10, precision); return Math.round(n * factor) / factor; } // src/guides.ts function createFrameGuides(frame, block) { const { width: w, height: h } = frame; const { left: ml, right: mr, top: mt, bottom: mb } = block.margin ?? ZERO_EDGES; const { left: pl, right: pr, top: pt, bottom: pb } = block.padding ?? ZERO_EDGES; const { breakBefore: bb, breakAfter: ba, isPage: page } = block; const stroke2 = { lineColor: rgb2(0, 0, 0), lineWidth: 0.5, lineOpacity: 0.25 }; const fill2 = { fillColor: rgb2(0, 0, 0), fillOpacity: 0.25 }; const mFill = { fillColor: rgb2(0.9, 0.8, 0.3), fillOpacity: 0.15 }; const pFill = { fillColor: rgb2(0.2, 0.2, 0.7), fillOpacity: 0.15 }; const shapes = compact([ rect2({ x: 0, y: 0, width: w, height: h, ...stroke2 }), // margin !!mt && rect2({ x: -ml, y: -mt, width: w + ml + mr, height: mt, ...mFill }), !!ml && rect2({ x: -ml, y: 0, width: ml, height: h, ...mFill }), !!mr && rect2({ x: w, y: 0, width: mr, height: h, ...mFill }), !!mb && rect2({ x: -ml, y: h, width: w + ml + mr, height: mb, ...mFill }), // padding !!pt && rect2({ x: 0, y: 0, width: w, height: pt, ...pFill }), !!pl && rect2({ x: 0, y: pt, width: pl, height: h - pt - pb, ...pFill }), !!pr && rect2({ x: w - pr, y: pt, width: pr, height: h - pt - pb, ...pFill }), !!pb && rect2({ x: 0, y: h - pb, width: w, height: pb, ...pFill }), // indicators for breakBefore and breakAfter bb === "avoid" && circle2({ cx: 5, cy: 0, r: 3, ...fill2 }), bb === "always" && rect2({ x: 0, y: -3, width: 30, height: 3, ...fill2 }), ba === "avoid" && circle2({ cx: w - 5, cy: h, r: 3, ...fill2 }), ba === "always" && rect2({ x: w - 30, y: h, width: 30, height: 3, ...fill2 }), // for pages, separator lines for header and footer page && line2({ x1: -ml, y1: -mt, x2: w + mr + ml, y2: -mt, ...stroke2 }), page && line2({ x1: -ml, y1: h + mb, x2: w + mr + ml, y2: h + mb, ...stroke2 }) ]); return { type: "graphics", shapes }; } function createRowGuides({ x, y, width, height, baseline }) { const stroke2 = { lineColor: rgb2(0, 0.5, 0), lineWidth: 0.5, lineOpacity: 0.25 }; const fill2 = { fillColor: rgb2(0, 0.5, 0), fillOpacity: 0.25 }; const by = y + baseline; const shapes = [ rect2({ x, y, width: 3, height: 3, ...fill2 }), rect2({ x, y, width, height, ...stroke2 }), line2({ x1: x, y1: by, x2: x + width, y2: by, ...stroke2 }) ]; return { type: "graphics", shapes }; } function rect2(args) { return { type: "rect", ...args }; } function circle2(args) { return { type: "circle", ...args }; } function line2(args) { return { type: "line", ...args }; } // src/read-page-size.ts function readPageSize(def) { if (typeof def === "string") { const size = paperSizes[def]; if (!size) throw typeError("valid paper size", def); const { width, height } = size; return { width, height }; } if (isObject(def)) { const width = readFrom(def, "width", required(parseLength)); const height = readFrom(def, "height", required(parseLength)); if (width <= 0) throw typeError("positive width", width); if (height <= 0) throw typeError("positive height", height); return { width, height }; } throw typeError("valid page size", def); } function parseOrientation(def) { if (def === "portrait" || def === "landscape") return def; throw typeError("'portrait' or 'landscape'", def); } function applyOrientation(size, orientation) { const { width, height } = size; if (orientation === "portrait") { return { width: Math.min(width, height), height: Math.max(width, height) }; } if (orientation === "landscape") { return { width: Math.max(width, height), height: Math.min(width, height) }; } return size; } // src/layout/layout-columns.ts async function layoutColumnsContent(block, box, ctx) { const children = []; let remainingWidth = box.width; let maxColHeight = 0; const layoutColumn = async (column, idx) => { const marginX = column.margin ? column.margin.left + column.margin.right : 0; const marginY = column.margin ? column.margin.top + column.margin.bottom : 0; const width = column.width ?? (column.autoWidth ? remainingWidth : remainingColWidth) - marginX; const height = column.height ?? box.height; const colBox = { x: 0, y: 0, width, height }; const autoWidth = column.width == null && (column.autoWidth || block.autoWidth); const { frame } = await layoutBlock( { ...column, autoWidth, breakInside: "avoid" }, colBox, ctx ); children[idx] = frame; remainingWidth -= frame.width + marginX; maxColHeight = Math.max(maxColHeight, frame.height + marginY); }; for (const [idx, column] of block.columns.entries()) { if (column.width != null) { await layoutColumn(column, idx); } } for (const [idx, column] of block.columns.entries()) { if (column.autoWidth) { await layoutColumn(column, idx); } } const remainingColCount = block.columns.filter((_, idx) => !children[idx]).length; const remainingColWidth = remainingColCount ? Math.max(0, remainingWidth) / remainingColCount : 0; for (const [idx, column] of block.columns.entries()) { if (!children[idx]) { await layoutColumn(column, idx); } } let colX = box.x; block.columns.forEach((column, idx) => { const child = children[idx]; colX += column.margin?.left ?? 0; child.x = colX; colX += child.width + (column.margin?.right ?? 0); }); const intrinsicWidth = colX - box.x; block.columns.forEach((column, idx) => { const child = children[idx]; const marginY = column.margin ? column.margin.top + column.margin.bottom : 0; child.y = box.y + (column.margin?.top ?? 0); if (column.verticalAlign === "middle") { child.y += (maxColHeight - child.height - marginY) / 2; } else if (column.verticalAlign === "bottom") { child.y += maxColHeight - child.height - marginY; } }); return { frame: { children, width: block.autoWidth ? intrinsicWidth : box.width, height: maxColHeight } }; } // src/layout/layout-image.ts async function layoutImageContent(block, box, ctx) { const image2 = await ctx.imageStore.selectImage(block.image); const hasFixedWidth = block.width != null; const hasFixedHeight = block.height != null; const scale2 = getScale(image2, box, hasFixedWidth, hasFixedHeight); const imageSize = { width: image2.width * scale2, height: image2.height * scale2 }; const width = block.autoWidth ? imageSize.width : box.width; const imageBox = { x: box.x, y: box.y, width, height: imageSize.height }; const imagePos = align(imageBox, imageSize, block.imageAlign); const imageObj = createImageObject(image2, imagePos, imageSize); const objects = [imageObj]; return { frame: { objects, width, height: imageSize.height } }; } function getScale(image2, box, fixedWidth, fixedHeight) { const xScale = box.width / image2.width; const yScale = box.height / image2.height; if (fixedWidth && fixedHeight) return Math.min(xScale, yScale); if (fixedWidth) return xScale; if (fixedHeight) return yScale; return Math.min(xScale, 1); } function align(box, size, alignment) { const space = { width: box.width - size.width, height: box.height - size.height }; const is = (a) => alignment === a; const xShift = is("left") ? 0 : is("right") ? space.width : space.width / 2; const yShift = is("top") ? 0 : is("bottom") ? space.height : space.height / 2; return { x: box.x + xShift, y: box.y + yShift }; } function createImageObject(image2, pos, size) { return { type: "image", image: image2, x: pos.x, y: pos.y, width: size.width, height: size.height }; } // src/layout/layout-rows.ts async function layoutRowsContent(block, box, ctx) { let rowY = box.y; let lastMargin = 0; let remainingHeight = box.height; let aggregatedHeight = 0; const aggregatedHeights = []; let lastBreakOpportunity = -1; const frames = []; let remainingRows = []; for (const [rowIdx, row] of block.rows.entries()) { const margin = row.margin ?? ZERO_EDGES; const topMargin = Math.max(lastMargin, margin.top); const nextPos = { x: box.x + margin.left, y: rowY + topMargin }; const maxSize = { width: box.width - margin.left - margin.right, height: remainingHeight - topMargin - margin.bottom }; const autoWidth = row.width == null && (row.autoWidth || block.autoWidth); const { frame, remainder: remainder2 } = await layoutBlock( { ...row, autoWidth }, { ...nextPos, ...maxSize }, ctx ); const performBreakAt = (breakIdx, remainder3) => { frames.splice(breakIdx); const insertedBlock = block.insertAfterBreak?.(); remainingRows = compact([insertedBlock, remainder3, ...block.rows.slice(breakIdx)]); }; if (frame.height + topMargin + margin.bottom > remainingHeight) { if (lastBreakOpportunity >= 0) { performBreakAt(lastBreakOpportunity + 1); break; } else if (block.breakInside === "enforce-auto") { if (rowIdx > 0) { performBreakAt(rowIdx); break; } } } frames.push(frame); lastMargin = margin.bottom; aggregatedHeight += topMargin + frame.height; aggregatedHeights.push(aggregatedHeight + lastMargin); if (remainder2) { performBreakAt(rowIdx + 1, remainder2); break; } if (row.breakAfter === "always" || block.rows[rowIdx + 1]?.breakBefore === "always") { performBreakAt(rowIdx + 1); break; } rowY += topMargin + frame.height; remainingHeight -= topMargin + frame.height; if (block.breakInside !== "avoid" && isBreakPossible(block.rows, rowIdx)) { lastBreakOpportunity = rowIdx; } } const remainder = remainingRows.length ? ( // do not include the id in the remainder to avoid duplicate anchors { ...omit(block, "id", "breakInside"), rows: remainingRows } ) : void 0; const width = block.autoWidth ? Math.max( ...frames.map((frame, idx) => { const row = block.rows[idx]; const marginX = row.margin ? row.margin.left + row.margin.right : 0; return frame.width + marginX; }) ) : box.width; return { frame: { width, height: aggregatedHeights[frames.length - 1] ?? 0, children: frames }, remainder }; } // src/font-metrics.ts function getTextWidth(text2, font, fontSize) { const { glyphs } = font.layout(text2); const scale2 = 1e3 / font.unitsPerEm; let totalWidth = 0; for (let idx = 0, len = glyphs.length; idx < len; idx++) { totalWidth += glyphs[idx].advanceWidth * scale2; } return totalWidth * fontSize / 1e3; } function getTextHeight(font, fontSize) { const { ascent, descent, bbox } = font; const scale2 = 1e3 / font.unitsPerEm; const yTop = (ascent || bbox.maxY) * scale2; const yBottom = (descent || bbox.minY) * scale2; const height = yTop - yBottom; return height / 1e3 * fontSize; } // src/text.ts var defaultFontSize = 18; var defaultLineHeight = 1.2; async function extractTextSegments(textSpans, fontStore) { const segments = await Promise.all( textSpans.map(async (span2) => { const { text: text2, attrs } = span2; const { fontSize = defaultFontSize, fontFamily, fontStyle, fontWeight, lineHeight = defaultLineHeight, color, link, rise, letterSpacing } = attrs; const font = await fontStore.selectFont({ fontFamily, fontStyle, fontWeight }); const height = getTextHeight(font.fkFont, fontSize); return splitChunks(text2).map( (text3) => ({ text: text3, width: getTextWidth(text3, font.fkFont, fontSize) + text3.length * (letterSpacing ?? 0), height, lineHeight, font, fontFamily, fontStyle, fontWeight, fontSize, color, link, rise, letterSpacing }) ); }) ); return segments.flat(); } function convertToTextSpan(segment) { const { text: text2, fontSize, fontFamily, fontStyle, fontWeight, lineHeight, color, link, rise, letterSpacing } = segment; return { text: text2, attrs: { fontSize, fontFamily, fontStyle, fontWeight, lineHeight, color, link, rise, letterSpacing } }; } function splitChunks(text2) { const segments = []; let tail = text2; let match = /\s+/.exec(tail); while (match) { if (match.index) { segments.push(tail.slice(0, match.index)); } const wsSegment = tail.slice(match.index, match.index + match[0].length); const newlineCount = wsSegment.match(/\n/g)?.length; if (newlineCount) { segments.push(..."\n".repeat(newlineCount).split("")); } else { segments.push(wsSegment); } tail = tail.slice(match.index + match[0].length); match = /\s+/.exec(tail); } if (tail) { segments.push(tail); } return segments; } function breakLine(segments, maxWidth) { const breakIdx = findLinebreak(segments, maxWidth); if (breakIdx === 0) { const head = [{ ...segments[0], text: "", width: 0 }]; const tail = segments.slice(breakIdx + 1); return tail.length ? [head, tail] : [head]; } if (breakIdx !== void 0) { const head = segments.slice(0, breakIdx); const tail = segments.slice(breakIdx + 1); return tail.length ? [head, tail] : [head]; } return [segments]; } function findLinebreak(segments, maxWidth) { let x = 0; for (const [idx, segment] of segments.entries()) { const { text: text2, width } = segment; if (text2 === "\n") { return idx; } x += width; if (x > maxWidth) { return findLinebreakOpportunity(segments, idx); } } } function findLinebreakOpportunity(segments, index) { for (let i = index; i >= 0; i--) { if (isLineBreakOpportunity(segments[i])) { return i; } } for (let i = index + 1; i < segments.length; i++) { if (isLineBreakOpportunity(segments[i])) { return i; } } } function isLineBreakOpportunity(segment) { return segment && /^\s+$/.test(segment.text); } function flattenTextSegments(segments) { const result = []; let prev; segments.forEach((segment) => { if (segment.font === prev?.font && segment.fontSize === prev?.fontSize && segment.lineHeight === prev?.lineHeight && segment.color === prev?.color && segment.link === prev?.link && segment.rise === prev?.rise && segment.letterSpacing === prev?.letterSpacing) { prev.text += segment.text; prev.width += segment.width; } else { prev = { ...segment }; result.push(prev); } }); return result; } // src/layout/layout-text.ts async function layoutTextContent(block, box, ctx) { const text2 = await layoutText(block, box, ctx); const objects = []; if (text2.rows.length) objects.push({ type: "text", rows: text2.rows }); if (text2.objects?.length) objects.push(...text2.objects); if (ctx.guides) objects.push(...text2.rows.map((row) => createRowGuides(row))); const remainder = text2.remainder ? { ...omit(block, "id"), text: text2.remainder } : void 0; return { frame: { ...objects?.length ? { objects } : void 0, width: block.autoWidth ? text2.size.width : box.width, height: text2.size.height }, remainder }; } async function layoutText(block, box, ctx) { const { text: text2 } = block; const segments = await extractTextSegments(text2, ctx.fontStore); const rows2 = []; const objects = []; let remainingSegments = segments; const remainingSpace = { ...box }; const size = { width: 0, height: 0 }; while (remainingSegments?.length) { const layoutResult = layoutTextRow(remainingSegments, remainingSpace); const { row, objects: rowObjects, remainder: remainder2 } = layoutResult; if (row.height > remainingSpace.height) { if (block.breakInside !== "avoid" && rows2.length) { break; } } if (rowObjects.length) objects.push(...rowObjects); rows2.push(row); remainingSegments = remainder2; remainingSpace.height -= row.height; remainingSpace.y += row.height; size.width = Math.max(size.width, row.width); size.height += row.height; } const width = block.autoWidth ? size.width : box.width; if (block.textAlign === "right") { rows2.forEach((row) => { row.x += width - row.width; }); objects.forEach((obj) => { obj.x += width - obj.width; }); } else if (block.textAlign === "center") { rows2.forEach((row) => { row.x += (width - row.width) / 2; }); objects.forEach((obj) => { obj.x += (width - obj.width) / 2; }); } const remainder = remainingSegments?.length ? flattenTextSegments(remainingSegments).map((s) => convertToTextSpan(s)) : void 0; return { rows: rows2, objects, size, remainder }; } function layoutTextRow(segments, box) { const [lineSegments, remainder] = breakLine(segments, box.width); const pos = { x: 0, y: 0 }; const size = { width: 0, height: 0 }; let baseline = 0; let rowHeight = 0; const links = []; const segmentObjects = []; flattenTextSegments(lineSegments).forEach((seg) => { const { text: text2, width, height, lineHeight, font, fontSize, link, color, rise, letterSpacing } = seg; segmentObjects.push({ text: text2, font, fontSize, color, rise, letterSpacing }); const offset = (height * lineHeight - height) / 2; if (link) { const linkPos = { x: box.x + pos.x, y: box.y - pos.y + offset }; links.push({ type: "link", ...linkPos, width, height, url: link }); } pos.x += width; size.width += width; size.height = Math.max(size.height, height); baseline = Math.max(baseline, getDescent(font, fontSize) + offset); rowHeight = Math.max(rowHeight, height * lineHeight); }); const objects = flattenLinks(links); const row = { ...box, width: size.width, height: rowHeight, baseline: rowHeight - baseline, segments: segmentObjects }; return { row, objects, remainder }; } function getDescent(font, fontSize) { return Math.abs((font.fkFont.descent ?? 0) * fontSize / font.fkFont.unitsPerEm); } function flattenLinks(links) { const result = []; let prev; links.forEach((link) => { if (prev?.url === link.url && prev?.x + prev?.width === link.x && prev?.y === link.y) { prev.width += link.width; prev.height = Math.max(prev.height, link.height); } else { prev = link; result.push(prev); } }); return result; } // src/layout/layout.ts var defaultPageMargin = parseEdges("2cm"); async function layoutPages(def, ctx) { const pages = []; let remainingBlocks = def.content; let pageNumber = 1; const pageSize = applyOrientation(def.pageSize ?? paperSizes.A4, def.pageOrientation); const makePage = async () => { const pageInfo = { pageNumber: pageNumber++, pageSize }; const pageMargin = def.margin?.(pageInfo) ?? defaultPageMargin; const header = def.header && await layoutHeader(def.header(pageInfo), pageSize, ctx); const footer = def.footer && await layoutFooter(def.footer(pageInfo), pageSize, ctx); const x = 0; const y = (header?.y ?? 0) + (header?.height ?? 0); const width = pageSize.width; const height = (footer?.y ?? pageSize.height) - y; const contentBox = subtractEdges({ x, y, width, height }, pageMargin); const { frame, remainder } = await layoutPageContent(remainingBlocks, contentBox, ctx); if (ctx.guides) { frame.objects = [createFrameGuides(frame, { margin: pageMargin, isPage: true })]; } remainingBlocks = remainder; pages.push({ size: pageSize, content: frame, header, footer }); }; while (remainingBlocks?.length) { await makePage(); } if (pages.length === 0) { await makePage(); } for (const [idx, page] of pages.entries()) { const pageInfo = { pageCount: pages.length, pageNumber: idx + 1, pageSize }; if (typeof def.header === "function") { page.header = await layoutHeader(def.header(pageInfo), pageSize, ctx); } if (typeof def.footer === "function") { page.footer = await layoutFooter(def.footer(pageInfo), pageSize, ctx); } } return pages.map(pickDefined); } async function layoutHeader(header, pageSize, ctx) { const box = subtractEdges({ x: 0, y: 0, ...pageSize }, header.margin); const { frame } = await layoutBlock({ ...header, breakInside: "avoid" }, box, ctx); return frame; } async function layoutFooter(footer, pageSize, ctx) { const box = subtractEdges({ x: 0, y: 0, ...pageSize }, footer.margin); const { frame } = await layoutBlock({ ...footer, breakInside: "avoid" }, box, ctx); frame.y = pageSize.height - frame.height - (footer.margin?.bottom ?? 0); return frame; } async function layoutPageContent(blocks, box, ctx) { const block = { rows: blocks, breakInside: "enforce-auto" }; const { frame, remainder } = await layoutBlock(block, box, ctx); return { frame: { ...frame, ...box }, remainder: remainder?.rows ?? [] }; } async function layoutBlock(block, box, ctx) { const padding = block.padding ?? ZERO_EDGES; const contentBox = subtractEdges( { x: 0, y: 0, width: block.width ?? box.width, height: block.height ?? box.height }, padding ); const result = await layoutBlockContent(block, contentBox, ctx); const frame = { ...result.frame, x: box.x, y: box.y, width: block.width ?? result.frame.width + padding.left + padding.right, height: block.height ?? result.frame.height + padding.top + padding.bottom }; addAnchor(frame, block); addGraphics(frame, block); if (ctx.guides) addGuides(frame, block); return { frame, remainder: result.remainder }; } async function layoutBlockContent(block, box, ctx) { if ("text" in block) { return await layoutTextContent(block, box, ctx); } if ("image" in block) { return await layoutImageContent(block, box, ctx); } if ("columns" in block) { return await layoutColumnsContent(block, box, ctx); } if ("rows" in block) { return await layoutRowsContent(block, box, ctx); } return { frame: { height: block.height ?? 0, width: block.width ?? (block.autoWidth ? 0 : box.width) } }; } function addAnchor(frame, block) { if (block.id) { (frame.objects ??= []).push(createAnchorObject(block.id)); } } function createAnchorObject