@broadpaper/forme 0.2.1 → 0.4.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.
@@ -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;
@@ -902,6 +902,146 @@ var FormeMeasurer = class {
902
902
  }
903
903
  };
904
904
 
905
+ // src/fonts.ts
906
+ var localReader = null;
907
+ function setFontFileReader(reader) {
908
+ localReader = reader;
909
+ }
910
+ var MAX_FONT_BYTES = 32 * 1024 * 1024;
911
+ var FETCH_TIMEOUT_MS = 15e3;
912
+ var MAX_CACHED = 64;
913
+ var cache = /* @__PURE__ */ new Map();
914
+ async function resolveFonts(input) {
915
+ const fonts = [];
916
+ const warnings = [];
917
+ const seen = /* @__PURE__ */ new Set();
918
+ const add = (font) => {
919
+ const weight = font.weight ?? 400;
920
+ const style = font.style ?? "normal";
921
+ const key = `${font.family.trim().toLowerCase()}|${weight}|${style}`;
922
+ if (seen.has(key)) return;
923
+ seen.add(key);
924
+ fonts.push({ family: font.family, src: font.src, weight, style });
925
+ };
926
+ for (const font of input.fonts ?? []) add(font);
927
+ const faces = await Promise.all((input.theme?.fonts ?? []).map((face) => themeFace(face, input.sources ?? {})));
928
+ for (const face of faces) {
929
+ warnings.push(...face.warnings);
930
+ if (face.font) add(face.font);
931
+ }
932
+ return { fonts, warnings };
933
+ }
934
+ async function themeFace(face, policy) {
935
+ const { weight, range } = weightOf(face.weight);
936
+ const style = face.style === "italic" ? "italic" : "normal";
937
+ const name = `"${face.family}" (${weight}${style === "italic" ? " italic" : ""})`;
938
+ const sources = (Array.isArray(face.src) ? face.src : [face.src]).filter((s) => typeof s === "string" && s.trim() !== "");
939
+ const reasons = [];
940
+ for (const src of sources) {
941
+ const loaded = await load(src.trim(), policy);
942
+ if ("reason" in loaded) {
943
+ reasons.push(loaded.reason);
944
+ continue;
945
+ }
946
+ const warnings = range ? [`The font "${face.family}" declares the weights ${String(face.weight)} from one file; the PDF engine is given it at ${weight} only. Declare a file per weight for the others to be embedded.`] : [];
947
+ return { font: { family: face.family, src: loaded.src, weight, style }, warnings };
948
+ }
949
+ return {
950
+ warnings: [
951
+ `The font ${name} has no TrueType or OpenType source the PDF engine can read, so the PDF draws that text in a standard face and breaks its lines differently from the screen. Add a .ttf or .otf to its src${reasons.length ? ` (${reasons.join("; ")})` : ""}.`
952
+ ]
953
+ };
954
+ }
955
+ function weightOf(value) {
956
+ if (typeof value === "number" && Number.isFinite(value)) return { weight: Math.round(value), range: false };
957
+ const text = String(value ?? "").trim().toLowerCase();
958
+ if (text === "bold") return { weight: 700, range: false };
959
+ const numbers = text.split(/\s+/).map(Number).filter((n) => Number.isFinite(n) && n > 0);
960
+ if (!numbers.length) return { weight: 400, range: false };
961
+ return { weight: Math.round(numbers[0]), range: numbers.length > 1 && numbers[1] !== numbers[0] };
962
+ }
963
+ async function load(src, policy) {
964
+ if (/^data:(font|application)\/(x-)?woff2?[;,]/i.test(src) || /\.woff2?([?#]|$)/i.test(src.startsWith("data:") ? "" : src)) {
965
+ return { reason: `${describe(src)} is WOFF, which only a browser reads` };
966
+ }
967
+ if (src.startsWith("data:")) return fromDataUri(src);
968
+ const remote = /^https?:\/\//i.test(src);
969
+ if (remote) {
970
+ let host;
971
+ try {
972
+ host = new URL(src).hostname;
973
+ } catch {
974
+ return { reason: `${describe(src)} is not a valid address` };
975
+ }
976
+ const allowed = policy.allowedHosts;
977
+ if (allowed && !allowed.includes("*") && !allowed.includes(host)) return { reason: `${host} is not an allowed host` };
978
+ } else if (policy.local === false) {
979
+ return { reason: `${describe(src)} is neither a data: URI nor a web address, and files are not read here` };
980
+ } else if (!localReader) {
981
+ return { src };
982
+ }
983
+ const key = src;
984
+ const hit = cache.get(key);
985
+ if (hit) return hit;
986
+ const pending = (async () => {
987
+ const bytes = remote ? await download(src, Boolean(policy.allowedHosts && !policy.allowedHosts.includes("*"))) : await localReader(src);
988
+ if (bytes.byteLength > MAX_FONT_BYTES) return { reason: `${describe(src)} is larger than ${MAX_FONT_BYTES / 1024 / 1024} MB` };
989
+ const problem = notEmbeddable(bytes, src);
990
+ return problem ? { reason: problem } : { src: `data:font/ttf;base64,${toBase64(bytes)}` };
991
+ })().catch((e) => {
992
+ cache.delete(key);
993
+ return { reason: `${describe(src)} could not be read (${e.message || String(e)})` };
994
+ });
995
+ cache.set(key, pending);
996
+ while (cache.size > MAX_CACHED) cache.delete(cache.keys().next().value);
997
+ return pending;
998
+ }
999
+ async function download(src, restricted) {
1000
+ const signal = typeof AbortSignal !== "undefined" && "timeout" in AbortSignal ? AbortSignal.timeout(FETCH_TIMEOUT_MS) : void 0;
1001
+ const response = await fetch(src, { redirect: restricted ? "error" : "follow", ...signal ? { signal } : {} });
1002
+ if (!response.ok) throw new Error(`HTTP ${response.status}`);
1003
+ const length2 = Number(response.headers.get("content-length"));
1004
+ if (length2 > MAX_FONT_BYTES) throw new Error(`larger than ${MAX_FONT_BYTES / 1024 / 1024} MB`);
1005
+ return new Uint8Array(await response.arrayBuffer());
1006
+ }
1007
+ function fromDataUri(src) {
1008
+ const match = /^data:([^,]*),(.*)$/s.exec(src);
1009
+ if (!match) return { reason: "a data: URI with no data" };
1010
+ const base64 = /;base64$/i.test(match[1]);
1011
+ let head;
1012
+ try {
1013
+ head = base64 ? fromBase64(match[2].slice(0, 8)) : Uint8Array.from(decodeURIComponent(match[2].slice(0, 64)), (c) => c.charCodeAt(0) & 255);
1014
+ } catch {
1015
+ return { reason: "a data: URI that does not decode" };
1016
+ }
1017
+ const problem = notEmbeddable(head, "a data: URI");
1018
+ if (problem) return { reason: problem };
1019
+ if (base64) return { src };
1020
+ const bytes = Uint8Array.from(decodeURIComponent(match[2]), (c) => c.charCodeAt(0) & 255);
1021
+ return { src: `data:font/ttf;base64,${toBase64(bytes)}` };
1022
+ }
1023
+ function notEmbeddable(bytes, src) {
1024
+ if (bytes.length < 4) return `${describe(src)} is not a font file`;
1025
+ const tag = String.fromCharCode(bytes[0], bytes[1], bytes[2], bytes[3]);
1026
+ if (bytes[0] === 0 && bytes[1] === 1 && bytes[2] === 0 && bytes[3] === 0 || tag === "OTTO" || tag === "true") return null;
1027
+ if (tag === "wOFF" || tag === "wOF2") return `${describe(src)} is WOFF, which only a browser reads`;
1028
+ if (tag === "ttcf") return `${describe(src)} is a font collection; give the single face instead`;
1029
+ return `${describe(src)} is not a TrueType or OpenType file`;
1030
+ }
1031
+ function describe(src) {
1032
+ return src.startsWith("data:") ? "a data: URI" : src.length > 80 ? `${src.slice(0, 77)}\u2026` : src;
1033
+ }
1034
+ function toBase64(bytes) {
1035
+ let binary = "";
1036
+ for (let i = 0; i < bytes.length; i += 32768) binary += String.fromCharCode(...bytes.subarray(i, i + 32768));
1037
+ return btoa(binary);
1038
+ }
1039
+ function fromBase64(text) {
1040
+ const clean = text.replace(/[^A-Za-z0-9+/]/g, "");
1041
+ const binary = atob(clean.slice(0, clean.length - clean.length % 4));
1042
+ return Uint8Array.from(binary, (c) => c.charCodeAt(0));
1043
+ }
1044
+
905
1045
  // src/editor.ts
906
1046
  function hash(s) {
907
1047
  let h = 2166136261;
@@ -928,8 +1068,8 @@ var FormeEditorMeasurer = class {
928
1068
  destroy() {
929
1069
  this.cache.clear();
930
1070
  }
931
- key(section, width, mode) {
932
- return hash(JSON.stringify(section)) + ":" + Math.round(width * 100) + ":" + mode;
1071
+ key(section, width, mode, faces) {
1072
+ return hash(JSON.stringify(section)) + ":" + Math.round(width * 100) + ":" + mode + ":" + faces;
933
1073
  }
934
1074
  async measure(sections, width) {
935
1075
  const opts = this.opts;
@@ -937,8 +1077,9 @@ var FormeEditorMeasurer = class {
937
1077
  const out = {};
938
1078
  const pending = [];
939
1079
  const keys = /* @__PURE__ */ new Map();
1080
+ const faces = facesKey(opts.theme.fonts);
940
1081
  for (const section of sections) {
941
- const k = this.key(section, width, opts.mode);
1082
+ const k = this.key(section, width, opts.mode, faces);
942
1083
  const hit = this.cache.get(k);
943
1084
  if (hit) out[section.key] = hit;
944
1085
  else {
@@ -947,6 +1088,9 @@ var FormeEditorMeasurer = class {
947
1088
  }
948
1089
  }
949
1090
  if (!pending.length) return out;
1091
+ const given = await this.config.fonts;
1092
+ const { fonts, warnings } = await resolveFonts({ fonts: given, theme: opts.theme });
1093
+ for (const w of warnings) this.warn(w);
950
1094
  const measurer = new FormeMeasurer({
951
1095
  registry: opts.registry,
952
1096
  theme: opts.theme,
@@ -955,7 +1099,7 @@ var FormeEditorMeasurer = class {
955
1099
  mode: opts.mode,
956
1100
  pageWidth: width,
957
1101
  margins: { top: 0, right: 0, bottom: 0, left: 0 },
958
- fonts: this.config.fonts,
1102
+ fonts,
959
1103
  render: this.config.render
960
1104
  });
961
1105
  const measured = await measurer.measure(pending, width);
@@ -964,14 +1108,21 @@ var FormeEditorMeasurer = class {
964
1108
  const k = keys.get(sectionKey);
965
1109
  if (k) this.cache.set(k, geometry);
966
1110
  }
967
- for (const w of measurer.warnings) {
968
- if (this.seen.has(w)) continue;
969
- this.seen.add(w);
970
- this.config.onWarning?.(w);
971
- }
1111
+ for (const w of measurer.warnings) this.warn(w);
972
1112
  return out;
973
1113
  }
1114
+ warn(message) {
1115
+ if (this.seen.has(message)) return;
1116
+ this.seen.add(message);
1117
+ this.config.onWarning?.(message);
1118
+ }
974
1119
  };
1120
+ function facesKey(faces) {
1121
+ if (!faces?.length) return "";
1122
+ return hash(
1123
+ faces.map((f) => [f.family, f.weight, f.style, ...(Array.isArray(f.src) ? f.src : [f.src]).map((s) => `${s.length}:${s.slice(0, 64)}${s.slice(-64)}`)].join("|")).join(";")
1124
+ );
1125
+ }
975
1126
 
976
1127
  // src/api.ts
977
1128
  import {
@@ -983,47 +1134,261 @@ import {
983
1134
  } from "@broadpaper/core";
984
1135
  import { mapStyle as mapStyle4 } from "@formepdf/shared";
985
1136
  import { producerString, resolveLicenseStatus, watermarkFor } from "@broadpaper/license";
1137
+
1138
+ // src/credit.ts
1139
+ function creditNode(spec, width, ctx) {
1140
+ const size = spec.fontSize;
1141
+ const children = [];
1142
+ if (spec.mark) {
1143
+ children.push({
1144
+ tag: "img",
1145
+ props: { src: spec.mark, alt: "", style: { width: `${size}px`, height: `${size}px`, opacity: String(spec.opacity) } },
1146
+ children: []
1147
+ });
1148
+ }
1149
+ children.push({
1150
+ tag: "span",
1151
+ props: { style: { "font-size": `${size}px`, color: spec.color, opacity: String(spec.opacity), "line-height": "1.2" } },
1152
+ children: [spec.text]
1153
+ });
1154
+ const line = {
1155
+ tag: "div",
1156
+ props: {
1157
+ style: {
1158
+ display: "flex",
1159
+ "align-items": "center",
1160
+ gap: `${Math.max(2, Math.round(size * 0.5))}px`,
1161
+ width: `${width}px`,
1162
+ "justify-content": "center"
1163
+ }
1164
+ },
1165
+ children
1166
+ };
1167
+ return translate(line, ctx)?.node ?? null;
1168
+ }
1169
+
1170
+ // src/api.ts
986
1171
  import { BroadPaper, configure, resolveLicenseStatus as resolveLicenseStatus2, verifyLicense, parseLicense } from "@broadpaper/license";
987
1172
  import { computePageGeometry as pageGeometry, paginate, resolveValue } from "@broadpaper/core";
988
1173
 
1174
+ // src/long-tables.ts
1175
+ var EPS = 0.5;
1176
+ function round3(n) {
1177
+ return Math.round(n * 100) / 100;
1178
+ }
1179
+ function isRow(c) {
1180
+ return typeof c !== "string" && c.tag === "tr";
1181
+ }
1182
+ function bodyRowCount(table) {
1183
+ let n = 0;
1184
+ for (const c of table.children) {
1185
+ if (typeof c === "string") continue;
1186
+ if (c.tag === "tbody") n += c.children.filter(isRow).length;
1187
+ else if (c.tag === "tr") n++;
1188
+ }
1189
+ return n;
1190
+ }
1191
+ function longTables(tree, limit) {
1192
+ const out = [];
1193
+ const visit = (v, key) => {
1194
+ const own = typeof v.props["data-key"] === "string" ? v.props["data-key"] : key;
1195
+ if (v.tag === "table") {
1196
+ const rows = bodyRowCount(v);
1197
+ if (rows > limit && own) out.push({ table: v, key: own, bodyRows: rows });
1198
+ return;
1199
+ }
1200
+ for (const c of v.children) if (typeof c !== "string") visit(c, own);
1201
+ };
1202
+ visit(tree, void 0);
1203
+ return out;
1204
+ }
1205
+ function sliceTables(tree, tables, start, count) {
1206
+ const targets = new Set(tables.map((t) => t.table));
1207
+ const visit = (v) => {
1208
+ if (targets.has(v)) return sliceRows(v, start, count);
1209
+ let changed = false;
1210
+ const children = v.children.map((c) => {
1211
+ if (typeof c === "string") return c;
1212
+ const next = visit(c);
1213
+ if (next !== c) changed = true;
1214
+ return next;
1215
+ });
1216
+ return changed ? { ...v, children } : v;
1217
+ };
1218
+ return visit(tree);
1219
+ }
1220
+ function sliceRows(table, start, count) {
1221
+ let index = 0;
1222
+ const keep = () => {
1223
+ const i = index++;
1224
+ return i >= start && i < start + count;
1225
+ };
1226
+ const children = [];
1227
+ for (const c of table.children) {
1228
+ if (typeof c === "string") children.push(c);
1229
+ else if (c.tag === "tbody") children.push({ ...c, children: c.children.filter((r) => !isRow(r) || keep()) });
1230
+ else if (c.tag === "tr") {
1231
+ if (keep()) children.push(c);
1232
+ } else children.push(c);
1233
+ }
1234
+ return { ...table, children };
1235
+ }
1236
+ function sameRows(a, b) {
1237
+ return !!a && a.length === b.length && a[0]?.top === b[0]?.top && a[a.length - 1]?.top === b[b.length - 1]?.top;
1238
+ }
1239
+ function stitchTables(first, tables, batches) {
1240
+ const g = {
1241
+ ...first,
1242
+ nodes: Object.fromEntries(Object.entries(first.nodes).map(([k, n]) => [k, { ...n }]))
1243
+ };
1244
+ const bottomOf = (rows) => Math.max(...rows.map((r) => r.top + r.height));
1245
+ 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));
1246
+ for (const { t } of order) {
1247
+ const main = g.nodes[t.key].rows;
1248
+ const body = main.filter((r) => r.kind === "body");
1249
+ const last = body[body.length - 1];
1250
+ const bodyEnd = last.top + last.height;
1251
+ const gap = body.length > 1 ? Math.max(0, body[1].top - (body[0].top + body[0].height)) : 0;
1252
+ const extra = [];
1253
+ let cursor = bodyEnd + gap;
1254
+ for (const batch of batches) {
1255
+ const rows = batch.nodes[t.key]?.rows?.filter((r) => r.kind === "body") ?? [];
1256
+ if (!rows.length) continue;
1257
+ const base = rows[0].top;
1258
+ for (const r of rows) extra.push({ top: round3(cursor + r.top - base), height: r.height, kind: "body" });
1259
+ const end = rows[rows.length - 1];
1260
+ cursor += end.top + end.height - base + gap;
1261
+ }
1262
+ if (!extra.length) continue;
1263
+ const tail = extra[extra.length - 1];
1264
+ const delta = round3(tail.top + tail.height - bodyEnd);
1265
+ const merged = [
1266
+ ...main.filter((r) => r.kind === "header"),
1267
+ ...body,
1268
+ ...extra,
1269
+ ...main.filter((r) => r.kind === "footer").map((r) => ({ ...r, top: round3(r.top + delta) }))
1270
+ ];
1271
+ for (const n of Object.values(g.nodes)) {
1272
+ const holdsThisTable = sameRows(n.rows, main);
1273
+ if (n.top >= bodyEnd - EPS) {
1274
+ n.top = round3(n.top + delta);
1275
+ if (n.lines) n.lines = n.lines.map((y) => round3(y + delta));
1276
+ if (n.rows && !holdsThisTable) n.rows = n.rows.map((r) => ({ ...r, top: round3(r.top + delta) }));
1277
+ } else if (n.top + n.height > bodyEnd - EPS) {
1278
+ n.height = round3(n.height + delta);
1279
+ }
1280
+ if (holdsThisTable) n.rows = merged;
1281
+ }
1282
+ g.height = round3(g.height + delta);
1283
+ }
1284
+ return g;
1285
+ }
1286
+
989
1287
  // src/paged.ts
990
1288
  import {
991
- renderNode as renderNode2,
992
- walkVNodes as walkVNodes2
1289
+ renderNode as renderNode2
993
1290
  } from "@broadpaper/core";
994
1291
  import { mapStyle as mapStyle3 } from "@formepdf/shared";
995
1292
 
996
1293
  // src/clip.ts
997
1294
  import { styleString as styleString2 } from "@broadpaper/core";
998
- var EPS = 0.01;
1295
+ var EPS2 = 0.01;
999
1296
  function cropToWindow(root, geom, from, to) {
1000
1297
  const w = { geom, from, to, dropped: 0 };
1001
- const node = crop(root, w, void 0);
1298
+ const node = crop(root, w, void 0, true);
1002
1299
  return { node, offset: w.dropped };
1003
1300
  }
1004
1301
  function outside(w, top, height) {
1005
- return top + height <= w.from + EPS || top >= w.to - EPS;
1302
+ return top + height <= w.from + EPS2 || top >= w.to - EPS2;
1006
1303
  }
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;
1304
+ function crop(v, w, rows, stacked) {
1305
+ const g = geometryOf(v, w.geom);
1010
1306
  if (g) {
1011
1307
  if (outside(w, g.top, g.height)) return blank(v, g.height);
1012
1308
  if (g.rows) rows = g.rows;
1013
1309
  }
1014
1310
  if (v.tag === "table" && rows) return cropTable(v, w, rows);
1015
- let changed = false;
1016
- const children = v.children.map((c) => {
1311
+ const childrenStacked = stacked && !sideBySide(v.children, w.geom);
1312
+ const removed = childrenStacked ? removeAbove(v.children, w) : 0;
1313
+ let changed = removed > 0;
1314
+ const children = v.children.slice(removed).map((c) => {
1017
1315
  if (typeof c === "string") return c;
1018
- const next = crop(c, w, rows);
1316
+ const next = crop(c, w, rows, childrenStacked);
1019
1317
  if (next !== c) changed = true;
1020
1318
  return next;
1021
1319
  });
1022
1320
  return changed ? { ...v, children } : v;
1023
1321
  }
1322
+ function geometryOf(v, geom) {
1323
+ const key = typeof v.props["data-key"] === "string" ? v.props["data-key"] : void 0;
1324
+ return key ? geom.nodes[key] : void 0;
1325
+ }
1326
+ function extent(v, geom) {
1327
+ const g = geometryOf(v, geom);
1328
+ if (g) return g.height > EPS2 ? [g.top, g.top + g.height] : null;
1329
+ let span = null;
1330
+ for (const c of v.children) {
1331
+ if (typeof c === "string") continue;
1332
+ const e = extent(c, geom);
1333
+ if (e) span = span ? [Math.min(span[0], e[0]), Math.max(span[1], e[1])] : e;
1334
+ }
1335
+ return span;
1336
+ }
1337
+ function sideBySide(children, geom) {
1338
+ const spans = children.filter((c) => typeof c !== "string").map((c) => extent(c, geom)).filter((e) => e !== null).sort((a, b) => a[0] - b[0]);
1339
+ let bottom = Number.NEGATIVE_INFINITY;
1340
+ for (const [top, end] of spans) {
1341
+ if (top < bottom - EPS2) return true;
1342
+ bottom = Math.max(bottom, end);
1343
+ }
1344
+ return false;
1345
+ }
1346
+ function removeAbove(children, w) {
1347
+ let start = null;
1348
+ let end = 0;
1349
+ let count = 0;
1350
+ for (let i = 0; i < children.length; i++) {
1351
+ const c = children[i];
1352
+ if (typeof c === "string") {
1353
+ if (c.trim()) return 0;
1354
+ continue;
1355
+ }
1356
+ const g = geometryOf(c, w.geom);
1357
+ if (!g) return 0;
1358
+ if (g.top + g.height > w.from + EPS2) {
1359
+ if (start === null) return 0;
1360
+ w.dropped += g.top - margin(c, "top") - start;
1361
+ return i;
1362
+ }
1363
+ start ??= g.top - margin(c, "top");
1364
+ end = g.top + g.height + margin(c, "bottom");
1365
+ count = i + 1;
1366
+ }
1367
+ if (start === null) return 0;
1368
+ w.dropped += end - start;
1369
+ return count;
1370
+ }
1371
+ function margin(v, side) {
1372
+ let value = 0;
1373
+ for (const declaration of styleString2(v.props.style).split(";")) {
1374
+ const m = /^\s*margin(?:-(top|bottom|left|right))?\s*:\s*(.+?)\s*$/i.exec(declaration);
1375
+ if (!m) continue;
1376
+ if (m[1]) {
1377
+ if (m[1].toLowerCase() === side) value = pixels(m[2]);
1378
+ } else {
1379
+ const parts = m[2].split(/\s+/);
1380
+ value = pixels(side === "top" ? parts[0] : parts.length >= 3 ? parts[2] : parts[0]);
1381
+ }
1382
+ }
1383
+ return value;
1384
+ }
1385
+ function pixels(length2) {
1386
+ const m = /^(-?\d*\.?\d+)(px)?$/i.exec(length2.trim());
1387
+ return m && (m[2] || Number(m[1]) === 0) ? Number(m[1]) : 0;
1388
+ }
1024
1389
  function blank(v, height) {
1025
1390
  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`);
1391
+ kept.push(`height:${round4(height)}px`);
1027
1392
  return { ...v, props: { ...v.props, style: kept.join(";") }, children: [] };
1028
1393
  }
1029
1394
  function cropTable(table, w, rows) {
@@ -1050,7 +1415,7 @@ function cropTable(table, w, rows) {
1050
1415
  };
1051
1416
  return rewrite(table);
1052
1417
  }
1053
- function round3(n) {
1418
+ function round4(n) {
1054
1419
  return Math.round(n * 100) / 100;
1055
1420
  }
1056
1421
 
@@ -1067,15 +1432,17 @@ function box(left, top, width, height, children, clip = true, bleed = 0) {
1067
1432
  return { kind: { type: "View" }, style, children };
1068
1433
  }
1069
1434
  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;
1435
+ if (v.tag === "table") {
1436
+ return { ...v, children: v.children.filter((c) => typeof c !== "string" && (c.tag === "colgroup" || c.tag === "thead")) };
1437
+ }
1438
+ let changed = false;
1439
+ const children = v.children.map((c) => {
1440
+ if (typeof c === "string") return c;
1441
+ const next = headerOnly(c);
1442
+ if (next !== c) changed = true;
1443
+ return next;
1077
1444
  });
1078
- return clone;
1445
+ return changed ? { ...v, children } : v;
1079
1446
  }
1080
1447
  function sectionsByKey(doc) {
1081
1448
  const map = /* @__PURE__ */ new Map();
@@ -1096,20 +1463,36 @@ function emitPages(input) {
1096
1463
  const width = page.contentWidth;
1097
1464
  const bleed = page.margins.right;
1098
1465
  const bodyDependsOnPage = isPageDependent(resolved.body);
1099
- const cache = /* @__PURE__ */ new Map();
1466
+ const cache2 = /* @__PURE__ */ new Map();
1467
+ const rendered = /* @__PURE__ */ new Map();
1468
+ const treeOf = (section, opts) => {
1469
+ if (bodyDependsOnPage) return renderNode2(section, opts, width);
1470
+ if (!rendered.has(section.key)) rendered.set(section.key, renderNode2(section, opts, width));
1471
+ return rendered.get(section.key);
1472
+ };
1100
1473
  const translateSection = (section, opts, from, to) => {
1101
1474
  const cacheKey = `${section.key}|${from}|${to}`;
1102
- if (!bodyDependsOnPage && cache.has(cacheKey)) return cache.get(cacheKey);
1103
- const v = renderNode2(section, opts, width);
1475
+ if (!bodyDependsOnPage && cache2.has(cacheKey)) return cache2.get(cacheKey);
1476
+ const v = treeOf(section, opts);
1104
1477
  if (!v) return null;
1105
1478
  const geom = geometry[section.key];
1106
1479
  const cropped = geom ? cropToWindow(v, geom, from, to) : { node: v, offset: 0 };
1107
1480
  const t = translate(cropped.node, ctx);
1108
1481
  if (!t) return null;
1109
1482
  const out = { node: t.node, offset: cropped.offset };
1110
- if (!bodyDependsOnPage) cache.set(cacheKey, out);
1483
+ if (!bodyDependsOnPage) cache2.set(cacheKey, out);
1111
1484
  return out;
1112
1485
  };
1486
+ const headers = /* @__PURE__ */ new Map();
1487
+ const repeatedHeader = (tableKey, table, opts, headerWidth) => {
1488
+ const key = `${tableKey}|${headerWidth}`;
1489
+ if (!bodyDependsOnPage && headers.has(key)) return headers.get(key);
1490
+ const v = renderNode2(table, opts, headerWidth);
1491
+ const t = v ? translate(headerOnly(v), ctx) : null;
1492
+ const node = t ? t.node : null;
1493
+ if (!bodyDependsOnPage) headers.set(key, node);
1494
+ return node;
1495
+ };
1113
1496
  const region = (sections, opts) => {
1114
1497
  const out = [];
1115
1498
  for (const s of sections) {
@@ -1123,7 +1506,7 @@ function emitPages(input) {
1123
1506
  return paged.pages.map((p) => {
1124
1507
  const opts = { ...renderOpts, page: { number: p.number, total: paged.totalPages } };
1125
1508
  const children = [];
1126
- if (input.watermark) children.push(watermarkNode(input.watermark));
1509
+ for (const mark of input.watermarks ?? []) if (mark && (mark.placement ?? "center") === "center") children.push(watermarkNode(mark));
1127
1510
  const headerSections = p.first && resolved.header.firstPage ? resolved.header.firstPage : resolved.header.default;
1128
1511
  if (headerSections.length && page.headerHeight > 0) {
1129
1512
  children.push(box(page.margins.left, page.headerTop, width, page.headerHeight, region(headerSections, opts)));
@@ -1137,10 +1520,8 @@ function emitPages(input) {
1137
1520
  for (const r of f.repeatHeaders) {
1138
1521
  const table = byKey.get(r.tableKey);
1139
1522
  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]));
1523
+ const header = repeatedHeader(r.tableKey, table, opts, r.width);
1524
+ if (header) inner.push(box(r.left, 0, r.width, r.height, [header]));
1144
1525
  }
1145
1526
  const content = translateSection(section, opts, f.from, f.to);
1146
1527
  if (content) {
@@ -1158,6 +1539,14 @@ function emitPages(input) {
1158
1539
  if (footerSections.length && page.footerHeight > 0) {
1159
1540
  children.push(box(page.margins.left, page.footerTop, width, page.footerHeight, region(footerSections, opts)));
1160
1541
  }
1542
+ for (const mark of input.watermarks ?? []) {
1543
+ if (!mark || (mark.placement ?? "center") !== "bottom") continue;
1544
+ const node = creditNode(mark, width, ctx);
1545
+ if (!node) continue;
1546
+ const h = mark.fontSize * 1.6;
1547
+ const top = Math.max(page.height - h, page.height - page.margins.bottom + Math.max(0, (page.margins.bottom - h) / 2));
1548
+ children.push(box(page.margins.left, top, width, h, [node]));
1549
+ }
1161
1550
  return {
1162
1551
  kind: {
1163
1552
  type: "Page",
@@ -1220,12 +1609,15 @@ async function buildFormeDocument(opts) {
1220
1609
  return out;
1221
1610
  };
1222
1611
  const children = [];
1223
- const mark = watermarkFor(license);
1224
- if (mark) children.push(watermarkNode(mark));
1612
+ const marks = [watermarkFor(license), opts.watermark].filter((m) => Boolean(m));
1613
+ for (const mark of marks) if ((mark.placement ?? "center") === "center") children.push(watermarkNode(mark));
1225
1614
  const header = translate2(resolved.header.default);
1226
1615
  if (header.length) children.push({ kind: { type: "Fixed", position: "Header" }, style: {}, children: header });
1227
1616
  const footer = translate2(resolved.footer.default);
1228
- if (footer.length) children.push({ kind: { type: "Fixed", position: "Footer" }, style: {}, children: footer });
1617
+ const credits = marks.filter((m) => (m.placement ?? "center") === "bottom").map((m) => creditNode(m, geom.contentWidth, ctx)).filter((n) => Boolean(n));
1618
+ if (footer.length || credits.length) {
1619
+ children.push({ kind: { type: "Fixed", position: "Footer" }, style: {}, children: [...footer, ...credits] });
1620
+ }
1229
1621
  children.push(...translate2(resolved.body));
1230
1622
  const page = {
1231
1623
  kind: {
@@ -1245,11 +1637,12 @@ async function buildFormeDocument(opts) {
1245
1637
  defaultPage: { size: "A4", margin: { top: 54, right: 54, bottom: 54, left: 54 }, wrap: true },
1246
1638
  defaultStyle: mapStyle4({ fontSize: pxToPt(theme.typography.baseFontSize ?? 11), fontFamily: theme.typography.bodyFont.split(",")[0].trim().replace(/^['"]|['"]$/g, "") })
1247
1639
  };
1248
- if (opts.fonts?.length) doc.fonts = opts.fonts.map((f) => ({ family: f.family, src: f.src, weight: f.weight ?? 400, style: f.style ?? "normal" }));
1640
+ const fonts = await resolveFonts({ fonts: opts.fonts, theme, sources: opts.fontSources });
1641
+ if (fonts.fonts.length) doc.fonts = fontEntries(fonts.fonts);
1249
1642
  if (opts.tagged) doc.tagged = true;
1250
1643
  if (opts.pdfUa) doc.pdfUa = true;
1251
1644
  if (opts.pdfA) doc.pdfa = opts.pdfA;
1252
- const warnings = [...ctx.warnings, ...resolved.warnings.map((w) => w.message)];
1645
+ const warnings = [...ctx.warnings, ...resolved.warnings.map((w) => w.message), ...fonts.warnings];
1253
1646
  return { doc, warnings, license };
1254
1647
  }
1255
1648
  var engineLoader = null;
@@ -1282,6 +1675,20 @@ async function renderPdf(opts) {
1282
1675
  }
1283
1676
  async function renderPdfPaginated(opts) {
1284
1677
  const started = Date.now();
1678
+ const layout = await layoutPaginated(opts);
1679
+ const drawn = await layout.draw();
1680
+ return {
1681
+ pdf: drawn.pdf,
1682
+ pages: drawn.pages,
1683
+ layout: drawn.layout,
1684
+ paged: layout.paged,
1685
+ geometry: layout.geometry,
1686
+ warnings: drawn.warnings,
1687
+ durationMs: Date.now() - started
1688
+ };
1689
+ }
1690
+ var MEASURE_BATCH_ROWS = 1e3;
1691
+ async function layoutPaginated(opts) {
1285
1692
  const license = await resolveLicenseStatus(opts.license !== void 0 ? { license: opts.license } : {});
1286
1693
  const { template } = migrateTemplate(opts.template);
1287
1694
  const theme = mergeTheme(opts.theme);
@@ -1316,17 +1723,16 @@ async function renderPdfPaginated(opts) {
1316
1723
  };
1317
1724
  const ctx = { warnings: /* @__PURE__ */ new Set(), suppressPageBreaks: true };
1318
1725
  const render = opts.renderer ?? defaultRenderer;
1319
- const measure = async (sections) => {
1320
- if (!sections.length) return {};
1726
+ const fonts = await resolveFonts({ fonts: opts.fonts, theme, sources: opts.fontSources });
1727
+ const layoutSections = async (list) => {
1728
+ if (!list.length) return {};
1321
1729
  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);
1730
+ for (const { section, tree } of list) {
1731
+ const t = translate(tree, ctx);
1326
1732
  if (t) measured.push({ section, node: t.node, ann: t.ann });
1327
1733
  }
1328
1734
  if (!measured.length) return {};
1329
- const doc2 = {
1735
+ const doc = {
1330
1736
  children: [
1331
1737
  {
1332
1738
  kind: {
@@ -1345,53 +1751,86 @@ async function renderPdfPaginated(opts) {
1345
1751
  defaultPage: { size: "A4", margin: { top: 54, right: 54, bottom: 54, left: 54 }, wrap: true },
1346
1752
  defaultStyle: baseStyle(theme)
1347
1753
  };
1348
- if (opts.fonts?.length) doc2.fonts = fontEntries(opts.fonts);
1349
- const result2 = await render(doc2);
1350
- const pages2 = result2.layout?.pages ?? [];
1754
+ if (fonts.fonts.length) doc.fonts = fontEntries(fonts.fonts);
1755
+ const result = await render(doc);
1756
+ const pages = result.layout?.pages ?? [];
1351
1757
  const scale2 = 1 / PX_TO_PT_LOCAL;
1352
- const scaled = pages2[0]?.elements.map((e) => scaleElement(e, scale2)) ?? [];
1758
+ const scaled = pages[0]?.elements.map((e) => scaleElement(e, scale2)) ?? [];
1353
1759
  return geometryFromLayout(measured, scaled, geom.contentWidth);
1354
1760
  };
1761
+ const batchRows = Math.max(1, opts.measureBatchRows ?? MEASURE_BATCH_ROWS);
1762
+ const measure = async (sections) => {
1763
+ const parts = [];
1764
+ for (const section of sections) {
1765
+ const tree = renderNode3(section, renderOpts, geom.contentWidth);
1766
+ if (tree) parts.push({ section, tree, long: longTables(tree, batchRows) });
1767
+ }
1768
+ const out = await layoutSections(parts.map((p) => ({ section: p.section, tree: p.long.length ? sliceTables(p.tree, p.long, 0, batchRows) : p.tree })));
1769
+ for (const p of parts) {
1770
+ const first = out[p.section.key];
1771
+ if (!p.long.length || !first) continue;
1772
+ const most = Math.max(...p.long.map((t) => t.bodyRows));
1773
+ const batches = [];
1774
+ for (let start = batchRows; start < most; start += batchRows) {
1775
+ batches.push({ section: { ...p.section, key: `${p.section.key}#rows-${start}` }, tree: sliceTables(p.tree, p.long, start, batchRows) });
1776
+ }
1777
+ const measured = [];
1778
+ for (let i = 0; i < batches.length; i += BATCHES_PER_PASS) {
1779
+ const chunk = batches.slice(i, i + BATCHES_PER_PASS);
1780
+ const got = await layoutSections(chunk);
1781
+ for (const b of chunk) {
1782
+ const g = got[b.section.key];
1783
+ if (g) measured.push(g);
1784
+ }
1785
+ }
1786
+ out[p.section.key] = stitchTables(first, p.long, measured);
1787
+ }
1788
+ return out;
1789
+ };
1355
1790
  const geometry = await measure(resolved.body);
1356
1791
  const regionGeometry = await measure([...resolved.header.default, ...resolved.header.firstPage ?? [], ...resolved.footer.default, ...resolved.footer.firstPage ?? []]);
1357
1792
  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;
1793
+ const background = template.page.background ? resolveValue(template.page.background, theme, "#ffffff") : void 0;
1380
1794
  return {
1381
- pdf: result.pdf,
1382
- pages: layout?.pages.length ?? paged.totalPages,
1383
- layout,
1384
1795
  paged,
1385
1796
  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
1797
+ async draw(range) {
1798
+ const from = Math.max(0, range?.from ?? 0);
1799
+ const to = Math.min(paged.pages.length, range?.to ?? paged.pages.length);
1800
+ const pages = emitPages({
1801
+ resolved,
1802
+ // `totalPages` stays the whole document's, so a later file still counts
1803
+ // its pages as part of the whole.
1804
+ paged: { ...paged, pages: paged.pages.slice(from, to) },
1805
+ page: geom,
1806
+ renderOpts,
1807
+ ctx,
1808
+ geometry,
1809
+ background,
1810
+ watermarks: [watermarkFor(license), opts.watermark]
1811
+ });
1812
+ const doc = {
1813
+ children: pages,
1814
+ metadata: { title: template.name, creator: producerString(license), ...opts.metadata ?? {} },
1815
+ defaultPage: { size: "A4", margin: { top: 54, right: 54, bottom: 54, left: 54 }, wrap: true },
1816
+ defaultStyle: baseStyle(theme)
1817
+ };
1818
+ if (fonts.fonts.length) doc.fonts = fontEntries(fonts.fonts);
1819
+ if (opts.tagged) doc.tagged = true;
1820
+ if (opts.pdfUa) doc.pdfUa = true;
1821
+ if (opts.pdfA) doc.pdfa = opts.pdfA;
1822
+ const result = await render(doc);
1823
+ const layout = result.layout ?? null;
1824
+ return {
1825
+ pdf: result.pdf,
1826
+ pages: layout?.pages.length ?? to - from,
1827
+ layout,
1828
+ warnings: [...ctx.warnings, ...resolved.warnings.map((w) => w.message), ...paged.warnings.map((w) => w.message), ...fonts.warnings, ...result.warnings ?? []]
1829
+ };
1830
+ }
1393
1831
  };
1394
1832
  }
1833
+ var BATCHES_PER_PASS = 2;
1395
1834
  var PX_TO_PT_LOCAL = 72 / 96;
1396
1835
  function scaleElement(el, scale2) {
1397
1836
  return {
@@ -1424,15 +1863,19 @@ export {
1424
1863
  UNBOUNDED_PAGE_HEIGHT,
1425
1864
  geometryFromLayout,
1426
1865
  FormeMeasurer,
1866
+ setFontFileReader,
1867
+ resolveFonts,
1427
1868
  FormeEditorMeasurer,
1428
1869
  buildFormeDocument,
1429
1870
  setEngineLoader,
1430
1871
  renderPdf,
1431
1872
  renderPdfPaginated,
1873
+ MEASURE_BATCH_ROWS,
1874
+ layoutPaginated,
1432
1875
  BroadPaper,
1433
1876
  configure,
1434
1877
  resolveLicenseStatus2 as resolveLicenseStatus,
1435
1878
  verifyLicense,
1436
1879
  parseLicense
1437
1880
  };
1438
- //# sourceMappingURL=chunk-2PT5WRKQ.js.map
1881
+ //# sourceMappingURL=chunk-QA4APTWM.js.map