pdfmkr
Version:
Generate PDF documents from JavaScript objects
1,499 lines (1,468 loc) • 88 kB
JavaScript
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