@broadpaper/forme 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,62 @@
1
+ # @broadpaper/forme
2
+
3
+ [![npm](https://img.shields.io/npm/v/@broadpaper/forme?color=171511&label=npm)](https://www.npmjs.com/package/@broadpaper/forme)
4
+ [![Licence: commercial](https://img.shields.io/badge/licence-commercial-171511)](https://www.broadpaper.com/legal/terms/)
5
+ [![Free to evaluate](https://img.shields.io/badge/evaluate-free%2C%20no%20trial%20clock-4a453c)](https://www.broadpaper.com/docs/guide/licensing/)
6
+
7
+ Browserless PDF generation for BroadPaper, via the Forme WebAssembly engine. The recommended backend.
8
+
9
+ Part of [**BroadPaper**](https://www.broadpaper.com/developers/) — an embeddable visual report designer and PDF renderer.
10
+ Your users design the report in your application; BroadPaper owns the layout, the branding,
11
+ the pagination and the output.
12
+
13
+ ## Install
14
+
15
+ ```bash
16
+ npm install @broadpaper/forme
17
+ ```
18
+
19
+ Produces PDFs with no Chromium, no Playwright and no native PDF library — a WebAssembly
20
+ engine and nothing else to install. That is what makes BroadPaper deployable in a container
21
+ that has no browser in it, which is most of the containers a report renderer belongs in.
22
+
23
+ ```ts
24
+ import { createRegistry } from "@broadpaper/blocks";
25
+ import { renderPdfPaginated } from "@broadpaper/forme";
26
+
27
+ const { pdf, pages } = await renderPdfPaginated({ template, data, registry: createRegistry() });
28
+ ```
29
+
30
+ Text is selectable, the fonts you supply are embedded, and output is byte-for-byte deterministic for
31
+ identical input — so a document regenerated a year later is the same file, and a diff of two
32
+ renders shows what actually changed.
33
+
34
+ ## Licensing
35
+
36
+ BroadPaper is **free to evaluate, with no trial clock and nothing behind a feature gate**.
37
+ Install it, use every feature and evaluate it against your real data for as long as you like.
38
+ Production use needs a licence.
39
+
40
+ In the software, the only difference without a licence is that generated PDFs carry an
41
+ evaluation mark, and a licence removes it. Contractually, the licence is also what permits
42
+ production use. No seats, no activation, no machine binding, no telemetry, and no call home at
43
+ any point.
44
+
45
+ - [How licensing works](https://www.broadpaper.com/docs/guide/licensing/)
46
+ - [Pricing](https://www.broadpaper.com/developers/pricing/)
47
+ - [Licence agreement](https://www.broadpaper.com/legal/terms/)
48
+
49
+ ## Documentation
50
+
51
+ - [Browserless PDF](https://www.broadpaper.com/docs/guide/browserless-pdf)
52
+ - [Themes and branding](https://www.broadpaper.com/docs/guide/themes)
53
+ - [Full documentation](https://www.broadpaper.com/docs/)
54
+
55
+ ## Support
56
+
57
+ Paid licences include email support at [support@broadpaper.com](mailto:support@broadpaper.com).
58
+
59
+ ---
60
+
61
+ Copyright © 2026 Codingbadger Creative Limited. See [LICENSE](./LICENSE) in this package, and
62
+ [THIRD-PARTY-NOTICES.md](./THIRD-PARTY-NOTICES.md) for the components it ships with.
@@ -892,7 +892,7 @@ var FormeMeasurer = class {
892
892
  defaultPage: { size: "A4", margin: { top: 54, right: 54, bottom: 54, left: 54 }, wrap: true },
893
893
  defaultStyle: {}
894
894
  };
895
- if (this.opts.fonts?.length) doc.fonts = this.opts.fonts;
895
+ if (this.opts.fonts?.length) doc.fonts = this.opts.fonts.map((f) => ({ ...f }));
896
896
  const result = await this.opts.render(doc);
897
897
  const pages = result.layout?.pages ?? [];
898
898
  const toPx = 96 / 72;
@@ -947,6 +947,7 @@ var FormeEditorMeasurer = class {
947
947
  }
948
948
  }
949
949
  if (!pending.length) return out;
950
+ const fonts = await this.config.fonts;
950
951
  const measurer = new FormeMeasurer({
951
952
  registry: opts.registry,
952
953
  theme: opts.theme,
@@ -955,7 +956,7 @@ var FormeEditorMeasurer = class {
955
956
  mode: opts.mode,
956
957
  pageWidth: width,
957
958
  margins: { top: 0, right: 0, bottom: 0, left: 0 },
958
- fonts: this.config.fonts,
959
+ fonts,
959
960
  render: this.config.render
960
961
  });
961
962
  const measured = await measurer.measure(pending, width);
@@ -983,47 +984,261 @@ import {
983
984
  } from "@broadpaper/core";
984
985
  import { mapStyle as mapStyle4 } from "@formepdf/shared";
985
986
  import { producerString, resolveLicenseStatus, watermarkFor } from "@broadpaper/license";
987
+
988
+ // src/credit.ts
989
+ function creditNode(spec, width, ctx) {
990
+ const size = spec.fontSize;
991
+ const children = [];
992
+ if (spec.mark) {
993
+ children.push({
994
+ tag: "img",
995
+ props: { src: spec.mark, alt: "", style: { width: `${size}px`, height: `${size}px`, opacity: String(spec.opacity) } },
996
+ children: []
997
+ });
998
+ }
999
+ children.push({
1000
+ tag: "span",
1001
+ props: { style: { "font-size": `${size}px`, color: spec.color, opacity: String(spec.opacity), "line-height": "1.2" } },
1002
+ children: [spec.text]
1003
+ });
1004
+ const line = {
1005
+ tag: "div",
1006
+ props: {
1007
+ style: {
1008
+ display: "flex",
1009
+ "align-items": "center",
1010
+ gap: `${Math.max(2, Math.round(size * 0.5))}px`,
1011
+ width: `${width}px`,
1012
+ "justify-content": "center"
1013
+ }
1014
+ },
1015
+ children
1016
+ };
1017
+ return translate(line, ctx)?.node ?? null;
1018
+ }
1019
+
1020
+ // src/api.ts
986
1021
  import { BroadPaper, configure, resolveLicenseStatus as resolveLicenseStatus2, verifyLicense, parseLicense } from "@broadpaper/license";
987
1022
  import { computePageGeometry as pageGeometry, paginate, resolveValue } from "@broadpaper/core";
988
1023
 
1024
+ // src/long-tables.ts
1025
+ var EPS = 0.5;
1026
+ function round3(n) {
1027
+ return Math.round(n * 100) / 100;
1028
+ }
1029
+ function isRow(c) {
1030
+ return typeof c !== "string" && c.tag === "tr";
1031
+ }
1032
+ function bodyRowCount(table) {
1033
+ let n = 0;
1034
+ for (const c of table.children) {
1035
+ if (typeof c === "string") continue;
1036
+ if (c.tag === "tbody") n += c.children.filter(isRow).length;
1037
+ else if (c.tag === "tr") n++;
1038
+ }
1039
+ return n;
1040
+ }
1041
+ function longTables(tree, limit) {
1042
+ const out = [];
1043
+ const visit = (v, key) => {
1044
+ const own = typeof v.props["data-key"] === "string" ? v.props["data-key"] : key;
1045
+ if (v.tag === "table") {
1046
+ const rows = bodyRowCount(v);
1047
+ if (rows > limit && own) out.push({ table: v, key: own, bodyRows: rows });
1048
+ return;
1049
+ }
1050
+ for (const c of v.children) if (typeof c !== "string") visit(c, own);
1051
+ };
1052
+ visit(tree, void 0);
1053
+ return out;
1054
+ }
1055
+ function sliceTables(tree, tables, start, count) {
1056
+ const targets = new Set(tables.map((t) => t.table));
1057
+ const visit = (v) => {
1058
+ if (targets.has(v)) return sliceRows(v, start, count);
1059
+ let changed = false;
1060
+ const children = v.children.map((c) => {
1061
+ if (typeof c === "string") return c;
1062
+ const next = visit(c);
1063
+ if (next !== c) changed = true;
1064
+ return next;
1065
+ });
1066
+ return changed ? { ...v, children } : v;
1067
+ };
1068
+ return visit(tree);
1069
+ }
1070
+ function sliceRows(table, start, count) {
1071
+ let index = 0;
1072
+ const keep = () => {
1073
+ const i = index++;
1074
+ return i >= start && i < start + count;
1075
+ };
1076
+ const children = [];
1077
+ for (const c of table.children) {
1078
+ if (typeof c === "string") children.push(c);
1079
+ else if (c.tag === "tbody") children.push({ ...c, children: c.children.filter((r) => !isRow(r) || keep()) });
1080
+ else if (c.tag === "tr") {
1081
+ if (keep()) children.push(c);
1082
+ } else children.push(c);
1083
+ }
1084
+ return { ...table, children };
1085
+ }
1086
+ function sameRows(a, b) {
1087
+ return !!a && a.length === b.length && a[0]?.top === b[0]?.top && a[a.length - 1]?.top === b[b.length - 1]?.top;
1088
+ }
1089
+ function stitchTables(first, tables, batches) {
1090
+ const g = {
1091
+ ...first,
1092
+ nodes: Object.fromEntries(Object.entries(first.nodes).map(([k, n]) => [k, { ...n }]))
1093
+ };
1094
+ const bottomOf = (rows) => Math.max(...rows.map((r) => r.top + r.height));
1095
+ const order = tables.map((t) => ({ t, rows: first.nodes[t.key]?.rows })).filter((x) => !!x.rows && x.rows.some((r) => r.kind === "body")).sort((a, b) => bottomOf(b.rows) - bottomOf(a.rows));
1096
+ for (const { t } of order) {
1097
+ const main = g.nodes[t.key].rows;
1098
+ const body = main.filter((r) => r.kind === "body");
1099
+ const last = body[body.length - 1];
1100
+ const bodyEnd = last.top + last.height;
1101
+ const gap = body.length > 1 ? Math.max(0, body[1].top - (body[0].top + body[0].height)) : 0;
1102
+ const extra = [];
1103
+ let cursor = bodyEnd + gap;
1104
+ for (const batch of batches) {
1105
+ const rows = batch.nodes[t.key]?.rows?.filter((r) => r.kind === "body") ?? [];
1106
+ if (!rows.length) continue;
1107
+ const base = rows[0].top;
1108
+ for (const r of rows) extra.push({ top: round3(cursor + r.top - base), height: r.height, kind: "body" });
1109
+ const end = rows[rows.length - 1];
1110
+ cursor += end.top + end.height - base + gap;
1111
+ }
1112
+ if (!extra.length) continue;
1113
+ const tail = extra[extra.length - 1];
1114
+ const delta = round3(tail.top + tail.height - bodyEnd);
1115
+ const merged = [
1116
+ ...main.filter((r) => r.kind === "header"),
1117
+ ...body,
1118
+ ...extra,
1119
+ ...main.filter((r) => r.kind === "footer").map((r) => ({ ...r, top: round3(r.top + delta) }))
1120
+ ];
1121
+ for (const n of Object.values(g.nodes)) {
1122
+ const holdsThisTable = sameRows(n.rows, main);
1123
+ if (n.top >= bodyEnd - EPS) {
1124
+ n.top = round3(n.top + delta);
1125
+ if (n.lines) n.lines = n.lines.map((y) => round3(y + delta));
1126
+ if (n.rows && !holdsThisTable) n.rows = n.rows.map((r) => ({ ...r, top: round3(r.top + delta) }));
1127
+ } else if (n.top + n.height > bodyEnd - EPS) {
1128
+ n.height = round3(n.height + delta);
1129
+ }
1130
+ if (holdsThisTable) n.rows = merged;
1131
+ }
1132
+ g.height = round3(g.height + delta);
1133
+ }
1134
+ return g;
1135
+ }
1136
+
989
1137
  // src/paged.ts
990
1138
  import {
991
- renderNode as renderNode2,
992
- walkVNodes as walkVNodes2
1139
+ renderNode as renderNode2
993
1140
  } from "@broadpaper/core";
994
1141
  import { mapStyle as mapStyle3 } from "@formepdf/shared";
995
1142
 
996
1143
  // src/clip.ts
997
1144
  import { styleString as styleString2 } from "@broadpaper/core";
998
- var EPS = 0.01;
1145
+ var EPS2 = 0.01;
999
1146
  function cropToWindow(root, geom, from, to) {
1000
1147
  const w = { geom, from, to, dropped: 0 };
1001
- const node = crop(root, w, void 0);
1148
+ const node = crop(root, w, void 0, true);
1002
1149
  return { node, offset: w.dropped };
1003
1150
  }
1004
1151
  function outside(w, top, height) {
1005
- return top + height <= w.from + EPS || top >= w.to - EPS;
1152
+ return top + height <= w.from + EPS2 || top >= w.to - EPS2;
1006
1153
  }
1007
- function crop(v, w, rows) {
1008
- const key = typeof v.props["data-key"] === "string" ? v.props["data-key"] : void 0;
1009
- const g = key ? w.geom.nodes[key] : void 0;
1154
+ function crop(v, w, rows, stacked) {
1155
+ const g = geometryOf(v, w.geom);
1010
1156
  if (g) {
1011
1157
  if (outside(w, g.top, g.height)) return blank(v, g.height);
1012
1158
  if (g.rows) rows = g.rows;
1013
1159
  }
1014
1160
  if (v.tag === "table" && rows) return cropTable(v, w, rows);
1015
- let changed = false;
1016
- const children = v.children.map((c) => {
1161
+ const childrenStacked = stacked && !sideBySide(v.children, w.geom);
1162
+ const removed = childrenStacked ? removeAbove(v.children, w) : 0;
1163
+ let changed = removed > 0;
1164
+ const children = v.children.slice(removed).map((c) => {
1017
1165
  if (typeof c === "string") return c;
1018
- const next = crop(c, w, rows);
1166
+ const next = crop(c, w, rows, childrenStacked);
1019
1167
  if (next !== c) changed = true;
1020
1168
  return next;
1021
1169
  });
1022
1170
  return changed ? { ...v, children } : v;
1023
1171
  }
1172
+ function geometryOf(v, geom) {
1173
+ const key = typeof v.props["data-key"] === "string" ? v.props["data-key"] : void 0;
1174
+ return key ? geom.nodes[key] : void 0;
1175
+ }
1176
+ function extent(v, geom) {
1177
+ const g = geometryOf(v, geom);
1178
+ if (g) return g.height > EPS2 ? [g.top, g.top + g.height] : null;
1179
+ let span = null;
1180
+ for (const c of v.children) {
1181
+ if (typeof c === "string") continue;
1182
+ const e = extent(c, geom);
1183
+ if (e) span = span ? [Math.min(span[0], e[0]), Math.max(span[1], e[1])] : e;
1184
+ }
1185
+ return span;
1186
+ }
1187
+ function sideBySide(children, geom) {
1188
+ const spans = children.filter((c) => typeof c !== "string").map((c) => extent(c, geom)).filter((e) => e !== null).sort((a, b) => a[0] - b[0]);
1189
+ let bottom = Number.NEGATIVE_INFINITY;
1190
+ for (const [top, end] of spans) {
1191
+ if (top < bottom - EPS2) return true;
1192
+ bottom = Math.max(bottom, end);
1193
+ }
1194
+ return false;
1195
+ }
1196
+ function removeAbove(children, w) {
1197
+ let start = null;
1198
+ let end = 0;
1199
+ let count = 0;
1200
+ for (let i = 0; i < children.length; i++) {
1201
+ const c = children[i];
1202
+ if (typeof c === "string") {
1203
+ if (c.trim()) return 0;
1204
+ continue;
1205
+ }
1206
+ const g = geometryOf(c, w.geom);
1207
+ if (!g) return 0;
1208
+ if (g.top + g.height > w.from + EPS2) {
1209
+ if (start === null) return 0;
1210
+ w.dropped += g.top - margin(c, "top") - start;
1211
+ return i;
1212
+ }
1213
+ start ??= g.top - margin(c, "top");
1214
+ end = g.top + g.height + margin(c, "bottom");
1215
+ count = i + 1;
1216
+ }
1217
+ if (start === null) return 0;
1218
+ w.dropped += end - start;
1219
+ return count;
1220
+ }
1221
+ function margin(v, side) {
1222
+ let value = 0;
1223
+ for (const declaration of styleString2(v.props.style).split(";")) {
1224
+ const m = /^\s*margin(?:-(top|bottom|left|right))?\s*:\s*(.+?)\s*$/i.exec(declaration);
1225
+ if (!m) continue;
1226
+ if (m[1]) {
1227
+ if (m[1].toLowerCase() === side) value = pixels(m[2]);
1228
+ } else {
1229
+ const parts = m[2].split(/\s+/);
1230
+ value = pixels(side === "top" ? parts[0] : parts.length >= 3 ? parts[2] : parts[0]);
1231
+ }
1232
+ }
1233
+ return value;
1234
+ }
1235
+ function pixels(length2) {
1236
+ const m = /^(-?\d*\.?\d+)(px)?$/i.exec(length2.trim());
1237
+ return m && (m[2] || Number(m[1]) === 0) ? Number(m[1]) : 0;
1238
+ }
1024
1239
  function blank(v, height) {
1025
1240
  const kept = styleString2(v.props.style).split(";").map((d) => d.trim()).filter((d) => d && !/^height\s*:/i.test(d));
1026
- kept.push(`height:${round3(height)}px`);
1241
+ kept.push(`height:${round4(height)}px`);
1027
1242
  return { ...v, props: { ...v.props, style: kept.join(";") }, children: [] };
1028
1243
  }
1029
1244
  function cropTable(table, w, rows) {
@@ -1050,7 +1265,7 @@ function cropTable(table, w, rows) {
1050
1265
  };
1051
1266
  return rewrite(table);
1052
1267
  }
1053
- function round3(n) {
1268
+ function round4(n) {
1054
1269
  return Math.round(n * 100) / 100;
1055
1270
  }
1056
1271
 
@@ -1067,15 +1282,17 @@ function box(left, top, width, height, children, clip = true, bleed = 0) {
1067
1282
  return { kind: { type: "View" }, style, children };
1068
1283
  }
1069
1284
  function headerOnly(v) {
1070
- const clone = structuredClone(v);
1071
- walkVNodes2(clone, (n) => {
1072
- if (n.tag === "table") {
1073
- n.children = n.children.filter((c) => typeof c !== "string" && (c.tag === "colgroup" || c.tag === "thead"));
1074
- return false;
1075
- }
1076
- return true;
1285
+ if (v.tag === "table") {
1286
+ return { ...v, children: v.children.filter((c) => typeof c !== "string" && (c.tag === "colgroup" || c.tag === "thead")) };
1287
+ }
1288
+ let changed = false;
1289
+ const children = v.children.map((c) => {
1290
+ if (typeof c === "string") return c;
1291
+ const next = headerOnly(c);
1292
+ if (next !== c) changed = true;
1293
+ return next;
1077
1294
  });
1078
- return clone;
1295
+ return changed ? { ...v, children } : v;
1079
1296
  }
1080
1297
  function sectionsByKey(doc) {
1081
1298
  const map = /* @__PURE__ */ new Map();
@@ -1097,10 +1314,16 @@ function emitPages(input) {
1097
1314
  const bleed = page.margins.right;
1098
1315
  const bodyDependsOnPage = isPageDependent(resolved.body);
1099
1316
  const cache = /* @__PURE__ */ new Map();
1317
+ const rendered = /* @__PURE__ */ new Map();
1318
+ const treeOf = (section, opts) => {
1319
+ if (bodyDependsOnPage) return renderNode2(section, opts, width);
1320
+ if (!rendered.has(section.key)) rendered.set(section.key, renderNode2(section, opts, width));
1321
+ return rendered.get(section.key);
1322
+ };
1100
1323
  const translateSection = (section, opts, from, to) => {
1101
1324
  const cacheKey = `${section.key}|${from}|${to}`;
1102
1325
  if (!bodyDependsOnPage && cache.has(cacheKey)) return cache.get(cacheKey);
1103
- const v = renderNode2(section, opts, width);
1326
+ const v = treeOf(section, opts);
1104
1327
  if (!v) return null;
1105
1328
  const geom = geometry[section.key];
1106
1329
  const cropped = geom ? cropToWindow(v, geom, from, to) : { node: v, offset: 0 };
@@ -1110,6 +1333,16 @@ function emitPages(input) {
1110
1333
  if (!bodyDependsOnPage) cache.set(cacheKey, out);
1111
1334
  return out;
1112
1335
  };
1336
+ const headers = /* @__PURE__ */ new Map();
1337
+ const repeatedHeader = (tableKey, table, opts, headerWidth) => {
1338
+ const key = `${tableKey}|${headerWidth}`;
1339
+ if (!bodyDependsOnPage && headers.has(key)) return headers.get(key);
1340
+ const v = renderNode2(table, opts, headerWidth);
1341
+ const t = v ? translate(headerOnly(v), ctx) : null;
1342
+ const node = t ? t.node : null;
1343
+ if (!bodyDependsOnPage) headers.set(key, node);
1344
+ return node;
1345
+ };
1113
1346
  const region = (sections, opts) => {
1114
1347
  const out = [];
1115
1348
  for (const s of sections) {
@@ -1123,7 +1356,7 @@ function emitPages(input) {
1123
1356
  return paged.pages.map((p) => {
1124
1357
  const opts = { ...renderOpts, page: { number: p.number, total: paged.totalPages } };
1125
1358
  const children = [];
1126
- if (input.watermark) children.push(watermarkNode(input.watermark));
1359
+ for (const mark of input.watermarks ?? []) if (mark && (mark.placement ?? "center") === "center") children.push(watermarkNode(mark));
1127
1360
  const headerSections = p.first && resolved.header.firstPage ? resolved.header.firstPage : resolved.header.default;
1128
1361
  if (headerSections.length && page.headerHeight > 0) {
1129
1362
  children.push(box(page.margins.left, page.headerTop, width, page.headerHeight, region(headerSections, opts)));
@@ -1137,10 +1370,8 @@ function emitPages(input) {
1137
1370
  for (const r of f.repeatHeaders) {
1138
1371
  const table = byKey.get(r.tableKey);
1139
1372
  if (!table) continue;
1140
- const v = renderNode2(table, opts, r.width);
1141
- if (!v) continue;
1142
- const t = translate(headerOnly(v), ctx);
1143
- if (t) inner.push(box(r.left, 0, r.width, r.height, [t.node]));
1373
+ const header = repeatedHeader(r.tableKey, table, opts, r.width);
1374
+ if (header) inner.push(box(r.left, 0, r.width, r.height, [header]));
1144
1375
  }
1145
1376
  const content = translateSection(section, opts, f.from, f.to);
1146
1377
  if (content) {
@@ -1158,6 +1389,14 @@ function emitPages(input) {
1158
1389
  if (footerSections.length && page.footerHeight > 0) {
1159
1390
  children.push(box(page.margins.left, page.footerTop, width, page.footerHeight, region(footerSections, opts)));
1160
1391
  }
1392
+ for (const mark of input.watermarks ?? []) {
1393
+ if (!mark || (mark.placement ?? "center") !== "bottom") continue;
1394
+ const node = creditNode(mark, width, ctx);
1395
+ if (!node) continue;
1396
+ const h = mark.fontSize * 1.6;
1397
+ const top = Math.max(page.height - h, page.height - page.margins.bottom + Math.max(0, (page.margins.bottom - h) / 2));
1398
+ children.push(box(page.margins.left, top, width, h, [node]));
1399
+ }
1161
1400
  return {
1162
1401
  kind: {
1163
1402
  type: "Page",
@@ -1220,12 +1459,15 @@ async function buildFormeDocument(opts) {
1220
1459
  return out;
1221
1460
  };
1222
1461
  const children = [];
1223
- const mark = watermarkFor(license);
1224
- if (mark) children.push(watermarkNode(mark));
1462
+ const marks = [watermarkFor(license), opts.watermark].filter((m) => Boolean(m));
1463
+ for (const mark of marks) if ((mark.placement ?? "center") === "center") children.push(watermarkNode(mark));
1225
1464
  const header = translate2(resolved.header.default);
1226
1465
  if (header.length) children.push({ kind: { type: "Fixed", position: "Header" }, style: {}, children: header });
1227
1466
  const footer = translate2(resolved.footer.default);
1228
- if (footer.length) children.push({ kind: { type: "Fixed", position: "Footer" }, style: {}, children: footer });
1467
+ const credits = marks.filter((m) => (m.placement ?? "center") === "bottom").map((m) => creditNode(m, geom.contentWidth, ctx)).filter((n) => Boolean(n));
1468
+ if (footer.length || credits.length) {
1469
+ children.push({ kind: { type: "Fixed", position: "Footer" }, style: {}, children: [...footer, ...credits] });
1470
+ }
1229
1471
  children.push(...translate2(resolved.body));
1230
1472
  const page = {
1231
1473
  kind: {
@@ -1282,6 +1524,20 @@ async function renderPdf(opts) {
1282
1524
  }
1283
1525
  async function renderPdfPaginated(opts) {
1284
1526
  const started = Date.now();
1527
+ const layout = await layoutPaginated(opts);
1528
+ const drawn = await layout.draw();
1529
+ return {
1530
+ pdf: drawn.pdf,
1531
+ pages: drawn.pages,
1532
+ layout: drawn.layout,
1533
+ paged: layout.paged,
1534
+ geometry: layout.geometry,
1535
+ warnings: drawn.warnings,
1536
+ durationMs: Date.now() - started
1537
+ };
1538
+ }
1539
+ var MEASURE_BATCH_ROWS = 1e3;
1540
+ async function layoutPaginated(opts) {
1285
1541
  const license = await resolveLicenseStatus(opts.license !== void 0 ? { license: opts.license } : {});
1286
1542
  const { template } = migrateTemplate(opts.template);
1287
1543
  const theme = mergeTheme(opts.theme);
@@ -1316,17 +1572,15 @@ async function renderPdfPaginated(opts) {
1316
1572
  };
1317
1573
  const ctx = { warnings: /* @__PURE__ */ new Set(), suppressPageBreaks: true };
1318
1574
  const render = opts.renderer ?? defaultRenderer;
1319
- const measure = async (sections) => {
1320
- if (!sections.length) return {};
1575
+ const layoutSections = async (list) => {
1576
+ if (!list.length) return {};
1321
1577
  const measured = [];
1322
- for (const section of sections) {
1323
- const v = renderNode3(section, renderOpts, geom.contentWidth);
1324
- if (!v) continue;
1325
- const t = translate(v, ctx);
1578
+ for (const { section, tree } of list) {
1579
+ const t = translate(tree, ctx);
1326
1580
  if (t) measured.push({ section, node: t.node, ann: t.ann });
1327
1581
  }
1328
1582
  if (!measured.length) return {};
1329
- const doc2 = {
1583
+ const doc = {
1330
1584
  children: [
1331
1585
  {
1332
1586
  kind: {
@@ -1345,53 +1599,86 @@ async function renderPdfPaginated(opts) {
1345
1599
  defaultPage: { size: "A4", margin: { top: 54, right: 54, bottom: 54, left: 54 }, wrap: true },
1346
1600
  defaultStyle: baseStyle(theme)
1347
1601
  };
1348
- if (opts.fonts?.length) doc2.fonts = fontEntries(opts.fonts);
1349
- const result2 = await render(doc2);
1350
- const pages2 = result2.layout?.pages ?? [];
1602
+ if (opts.fonts?.length) doc.fonts = fontEntries(opts.fonts);
1603
+ const result = await render(doc);
1604
+ const pages = result.layout?.pages ?? [];
1351
1605
  const scale2 = 1 / PX_TO_PT_LOCAL;
1352
- const scaled = pages2[0]?.elements.map((e) => scaleElement(e, scale2)) ?? [];
1606
+ const scaled = pages[0]?.elements.map((e) => scaleElement(e, scale2)) ?? [];
1353
1607
  return geometryFromLayout(measured, scaled, geom.contentWidth);
1354
1608
  };
1609
+ const batchRows = Math.max(1, opts.measureBatchRows ?? MEASURE_BATCH_ROWS);
1610
+ const measure = async (sections) => {
1611
+ const parts = [];
1612
+ for (const section of sections) {
1613
+ const tree = renderNode3(section, renderOpts, geom.contentWidth);
1614
+ if (tree) parts.push({ section, tree, long: longTables(tree, batchRows) });
1615
+ }
1616
+ const out = await layoutSections(parts.map((p) => ({ section: p.section, tree: p.long.length ? sliceTables(p.tree, p.long, 0, batchRows) : p.tree })));
1617
+ for (const p of parts) {
1618
+ const first = out[p.section.key];
1619
+ if (!p.long.length || !first) continue;
1620
+ const most = Math.max(...p.long.map((t) => t.bodyRows));
1621
+ const batches = [];
1622
+ for (let start = batchRows; start < most; start += batchRows) {
1623
+ batches.push({ section: { ...p.section, key: `${p.section.key}#rows-${start}` }, tree: sliceTables(p.tree, p.long, start, batchRows) });
1624
+ }
1625
+ const measured = [];
1626
+ for (let i = 0; i < batches.length; i += BATCHES_PER_PASS) {
1627
+ const chunk = batches.slice(i, i + BATCHES_PER_PASS);
1628
+ const got = await layoutSections(chunk);
1629
+ for (const b of chunk) {
1630
+ const g = got[b.section.key];
1631
+ if (g) measured.push(g);
1632
+ }
1633
+ }
1634
+ out[p.section.key] = stitchTables(first, p.long, measured);
1635
+ }
1636
+ return out;
1637
+ };
1355
1638
  const geometry = await measure(resolved.body);
1356
1639
  const regionGeometry = await measure([...resolved.header.default, ...resolved.header.firstPage ?? [], ...resolved.footer.default, ...resolved.footer.firstPage ?? []]);
1357
1640
  const paged = paginate({ document: resolved, geometry, page: geom, minPages: 1, regionGeometry });
1358
- const pages = emitPages({
1359
- resolved,
1360
- paged,
1361
- page: geom,
1362
- renderOpts,
1363
- ctx,
1364
- geometry,
1365
- background: template.page.background ? resolveValue(template.page.background, theme, "#ffffff") : void 0,
1366
- watermark: watermarkFor(license)
1367
- });
1368
- const doc = {
1369
- children: pages,
1370
- metadata: { title: template.name, creator: producerString(license), ...opts.metadata ?? {} },
1371
- defaultPage: { size: "A4", margin: { top: 54, right: 54, bottom: 54, left: 54 }, wrap: true },
1372
- defaultStyle: baseStyle(theme)
1373
- };
1374
- if (opts.fonts?.length) doc.fonts = fontEntries(opts.fonts);
1375
- if (opts.tagged) doc.tagged = true;
1376
- if (opts.pdfUa) doc.pdfUa = true;
1377
- if (opts.pdfA) doc.pdfa = opts.pdfA;
1378
- const result = await render(doc);
1379
- const layout = result.layout ?? null;
1641
+ const background = template.page.background ? resolveValue(template.page.background, theme, "#ffffff") : void 0;
1380
1642
  return {
1381
- pdf: result.pdf,
1382
- pages: layout?.pages.length ?? paged.totalPages,
1383
- layout,
1384
1643
  paged,
1385
1644
  geometry,
1386
- warnings: [
1387
- ...ctx.warnings,
1388
- ...resolved.warnings.map((w) => w.message),
1389
- ...paged.warnings.map((w) => w.message),
1390
- ...result.warnings ?? []
1391
- ],
1392
- durationMs: Date.now() - started
1645
+ async draw(range) {
1646
+ const from = Math.max(0, range?.from ?? 0);
1647
+ const to = Math.min(paged.pages.length, range?.to ?? paged.pages.length);
1648
+ const pages = emitPages({
1649
+ resolved,
1650
+ // `totalPages` stays the whole document's, so a later file still counts
1651
+ // its pages as part of the whole.
1652
+ paged: { ...paged, pages: paged.pages.slice(from, to) },
1653
+ page: geom,
1654
+ renderOpts,
1655
+ ctx,
1656
+ geometry,
1657
+ background,
1658
+ watermarks: [watermarkFor(license), opts.watermark]
1659
+ });
1660
+ const doc = {
1661
+ children: pages,
1662
+ metadata: { title: template.name, creator: producerString(license), ...opts.metadata ?? {} },
1663
+ defaultPage: { size: "A4", margin: { top: 54, right: 54, bottom: 54, left: 54 }, wrap: true },
1664
+ defaultStyle: baseStyle(theme)
1665
+ };
1666
+ if (opts.fonts?.length) doc.fonts = fontEntries(opts.fonts);
1667
+ if (opts.tagged) doc.tagged = true;
1668
+ if (opts.pdfUa) doc.pdfUa = true;
1669
+ if (opts.pdfA) doc.pdfa = opts.pdfA;
1670
+ const result = await render(doc);
1671
+ const layout = result.layout ?? null;
1672
+ return {
1673
+ pdf: result.pdf,
1674
+ pages: layout?.pages.length ?? to - from,
1675
+ layout,
1676
+ warnings: [...ctx.warnings, ...resolved.warnings.map((w) => w.message), ...paged.warnings.map((w) => w.message), ...result.warnings ?? []]
1677
+ };
1678
+ }
1393
1679
  };
1394
1680
  }
1681
+ var BATCHES_PER_PASS = 2;
1395
1682
  var PX_TO_PT_LOCAL = 72 / 96;
1396
1683
  function scaleElement(el, scale2) {
1397
1684
  return {
@@ -1429,10 +1716,12 @@ export {
1429
1716
  setEngineLoader,
1430
1717
  renderPdf,
1431
1718
  renderPdfPaginated,
1719
+ MEASURE_BATCH_ROWS,
1720
+ layoutPaginated,
1432
1721
  BroadPaper,
1433
1722
  configure,
1434
1723
  resolveLicenseStatus2 as resolveLicenseStatus,
1435
1724
  verifyLicense,
1436
1725
  parseLicense
1437
1726
  };
1438
- //# sourceMappingURL=chunk-2PT5WRKQ.js.map
1727
+ //# sourceMappingURL=chunk-2C3MAW2U.js.map