@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.
package/dist/index.d.ts CHANGED
@@ -172,6 +172,78 @@ declare function vnodeToForme(v: VNode, ctx: TranslateContext): FormeNode | null
172
172
 
173
173
  declare function watermarkNode(spec: WatermarkSpec): FormeNode;
174
174
 
175
+ /**
176
+ * The theme's fonts, made into fonts the engine can embed.
177
+ *
178
+ * A host declares a font once, in `theme.fonts`. The browser has always read that
179
+ * declaration — `@font-face` rules on the canvas, the designer's font pickers, the
180
+ * validator, the Chromium backend — and this engine did not: it took a separate
181
+ * `fonts` option, so a font declared only in the theme was drawn on screen and
182
+ * measured and printed in the standard faces. Nothing said so, and the symptom was
183
+ * a canvas whose sections overlapped and a PDF whose lines broke somewhere else.
184
+ *
185
+ * Three things stand between a `@font-face` declaration and the engine.
186
+ *
187
+ * **Format.** The engine parses TrueType and OpenType and nothing else; there is no
188
+ * WOFF decoder in it. A face often lists a WOFF2 for the browser beside a TTF, so
189
+ * the first source the engine can read is the one it gets, and a WOFF is skipped by
190
+ * its name before anything is downloaded. What is downloaded is checked by its
191
+ * first four bytes, not its extension. A face with nothing readable is a warning,
192
+ * because the alternative is exactly the silence described above.
193
+ *
194
+ * **Where it comes from.** A theme is the host's own code in the SDK and a JSON body
195
+ * from a stranger in the render service, so the service narrows `FontSourcePolicy`
196
+ * to its allowed hosts and to no files at all. The SDK default is to trust the
197
+ * theme, as it trusts the template.
198
+ *
199
+ * **Doing it once.** The engine resolves a font by rewriting its entry in place,
200
+ * and re-reads anything that is not a `data:` URI on every render. Every source is
201
+ * therefore resolved here, once per address, to a `data:` URI the engine passes
202
+ * straight through.
203
+ */
204
+
205
+ interface FormeFont {
206
+ family: string;
207
+ /** TTF/OTF bytes, a data: URI, or a filesystem path (Node only). */
208
+ src: string | Uint8Array;
209
+ weight?: number;
210
+ style?: "normal" | "italic";
211
+ }
212
+ /** Where the theme's fonts may be read from. */
213
+ interface FontSourcePolicy {
214
+ /** Hosts an `http(s)` font may be fetched from; `"*"` for any. Unset means any. */
215
+ allowedHosts?: string[];
216
+ /**
217
+ * Whether a source that is neither `data:` nor `http(s)` may be read — a file in
218
+ * Node, an address relative to the page in a browser. Default true.
219
+ */
220
+ local?: boolean;
221
+ }
222
+ /** Reads a source that is neither `data:` nor `http(s)`. Installed by each entry point. */
223
+ type FontFileReader = (src: string) => Promise<Uint8Array>;
224
+ /**
225
+ * Installs the reader for local sources. `src/index.ts` reads files and
226
+ * `src/index.browser.ts` fetches relative to the page, and neither bundle contains
227
+ * the other's — the same arrangement as `setEngineLoader`.
228
+ */
229
+ declare function setFontFileReader(reader: FontFileReader): void;
230
+ interface ResolvedFonts {
231
+ fonts: FormeFont[];
232
+ warnings: string[];
233
+ }
234
+ /**
235
+ * The fonts to hand the engine: the caller's own `fonts` first, exactly as given,
236
+ * then every face in the theme the engine can read.
237
+ *
238
+ * The first entry for a family, weight and style wins, so an explicit font — or a
239
+ * service's own — is never displaced by a theme naming the same face.
240
+ */
241
+ declare function resolveFonts(input: {
242
+ fonts?: FormeFont[];
243
+ theme?: Partial<Theme>;
244
+ sources?: FontSourcePolicy;
245
+ }): Promise<ResolvedFonts>;
246
+
175
247
  /**
176
248
  * Forme as the editor's measurer.
177
249
  *
@@ -189,8 +261,16 @@ declare function watermarkNode(spec: WatermarkSpec): FormeNode;
189
261
  interface FormeEditorMeasurerOptions {
190
262
  /** The engine call, e.g. `renderSerializedDocWithLayout` from `@formepdf/core/browser`. */
191
263
  render: FormeRenderer;
192
- /** Font files the engine should use, matching the theme's families. */
193
- fonts?: unknown[];
264
+ /**
265
+ * Fonts for the engine beyond the theme's own.
266
+ *
267
+ * Usually unnecessary: the faces in `theme.fonts` are read and given to the
268
+ * engine without being named here. May be a promise, because a browser host has
269
+ * to fetch the files and `createMeasurer` has to return synchronously. Every
270
+ * measurement waits for it, so nothing is ever measured — and cached — with the
271
+ * engine's standard faces standing in for a font that was still arriving.
272
+ */
273
+ fonts?: unknown[] | Promise<unknown[]>;
194
274
  /** Called for every distinct translation warning, once. */
195
275
  onWarning?(message: string): void;
196
276
  }
@@ -212,6 +292,7 @@ declare class FormeEditorMeasurer {
212
292
  destroy(): void;
213
293
  private key;
214
294
  measure(sections: ResolvedNode[], width: number): Promise<GeometryMap>;
295
+ private warn;
215
296
  }
216
297
 
217
298
  /**
@@ -228,13 +309,6 @@ declare class FormeEditorMeasurer {
228
309
  * `renderPdf` returns Forme's own page geometry so callers can compare.
229
310
  */
230
311
 
231
- interface FormeFont {
232
- family: string;
233
- /** TTF/OTF bytes, a data: URI, or a filesystem path (Node only). */
234
- src: string | Uint8Array;
235
- weight?: number;
236
- style?: "normal" | "italic";
237
- }
238
312
  interface RenderFormePdfOptions {
239
313
  template: ReportTemplate | AnyVersionTemplate;
240
314
  registry: BlockRegistry;
@@ -252,14 +326,37 @@ interface RenderFormePdfOptions {
252
326
  creator?: string;
253
327
  lang?: string;
254
328
  };
255
- /** Fonts to embed. Without these Forme falls back to its standard faces. */
329
+ /**
330
+ * Fonts handed to the engine as they are, before the theme's.
331
+ *
332
+ * Most hosts need none: every face in `theme.fonts` with a TrueType or OpenType
333
+ * source is embedded without being named twice. This is for a font the theme
334
+ * does not declare — one a service embeds in every render, say.
335
+ */
256
336
  fonts?: FormeFont[];
337
+ /** Where the theme's fonts may be read from. Unset, anywhere: a host's own theme is trusted. */
338
+ fontSources?: FontSourcePolicy;
257
339
  /** Emit a tagged (accessible) PDF. */
258
340
  tagged?: boolean;
259
341
  /** PDF/UA-1 conformance. Requires an embeddable font. */
260
342
  pdfUa?: boolean;
261
343
  /** PDF/A conformance level. */
262
344
  pdfA?: "2b" | "2u" | "2a" | "3b" | "3u" | "3a";
345
+ /**
346
+ * An extra mark to draw behind every page, on top of whatever the licence
347
+ * decides.
348
+ *
349
+ * Additive and never a replacement: the licence mark is decided by
350
+ * `watermarkFor` and this cannot remove it, which is the property that keeps
351
+ * "a caller cannot reach a PDF without passing the licence check" true. It
352
+ * exists for a host whose own commercial model wants a mark — BroadPaper Cloud
353
+ * marks the documents its free plan produces — and for the ordinary "DRAFT"
354
+ * stamp a business document sometimes needs.
355
+ *
356
+ * Remember that an unstyled Forme watermark draws solid black at full opacity;
357
+ * a spec without a colour and an opacity makes the document unreadable.
358
+ */
359
+ watermark?: WatermarkSpec | null;
263
360
  /**
264
361
  * A licence for this render only, overriding whatever `configure` was given.
265
362
  *
@@ -364,6 +461,49 @@ interface RenderPaginatedResult extends RenderFormePdfResult {
364
461
  * engine at the same width, so the second cannot re-break what the first
365
462
  * measured.
366
463
  */
367
- declare function renderPdfPaginated(opts: RenderFormePdfOptions): Promise<RenderPaginatedResult>;
464
+ declare function renderPdfPaginated(opts: LayoutPaginatedOptions): Promise<RenderPaginatedResult>;
465
+ /** A table longer than this many body rows is measured a batch at a time. */
466
+ declare const MEASURE_BATCH_ROWS = 1000;
467
+ interface LayoutPaginatedOptions extends RenderFormePdfOptions {
468
+ /**
469
+ * Body rows per measuring pass for a long table. Only a test should change it:
470
+ * a small number exercises the batching on a table short enough to compare with
471
+ * measuring it whole.
472
+ */
473
+ measureBatchRows?: number;
474
+ }
475
+ /** A document laid out and paginated, and not yet drawn. */
476
+ interface PaginatedLayout {
477
+ /** The pages BroadPaper's own paginator decided. */
478
+ paged: PagedDocument;
479
+ /** Geometry measured by the engine, keyed by section. */
480
+ geometry: GeometryMap;
481
+ /**
482
+ * Draws pages `[from, to)` — the whole document when no range is given — as one
483
+ * PDF. Pages keep their numbers in the whole document, so page 251 of a split
484
+ * document still says "251 of 1,300".
485
+ */
486
+ draw(range?: {
487
+ from: number;
488
+ to: number;
489
+ }): Promise<{
490
+ pdf: Uint8Array;
491
+ pages: number;
492
+ layout: {
493
+ pages: FormePageInfo[];
494
+ } | null;
495
+ warnings: string[];
496
+ }>;
497
+ }
498
+ /**
499
+ * Resolves, measures and paginates without drawing.
500
+ *
501
+ * Separate from drawing for two reasons a long document made plain. The page
502
+ * count is known here, before the more expensive half of the work, so a caller
503
+ * with a ceiling can refuse or split first rather than after; and a document of
504
+ * a thousand pages can be drawn as several files from one layout, every one of
505
+ * them agreeing about where the page breaks fall and what the totals are.
506
+ */
507
+ declare function layoutPaginated(opts: LayoutPaginatedOptions): Promise<PaginatedLayout>;
368
508
 
369
- export { type Annotation, type EngineLoader, FormeEditorMeasurer, type FormeEditorMeasurerOptions, type FormeFont, FormeMeasurer, type FormeMeasurerOptions, type FormeNode, type FormePageInfo, type FormeRenderer, type LayoutElement, type LayoutPage, PX_TO_PT, type RenderFormePdfOptions, type RenderFormePdfResult, type RenderPaginatedResult, type TranslateContext, type Translated, UNBOUNDED_PAGE_HEIGHT, buildFormeDocument, geometryFromLayout, parseDeclarations, pxToPt, renderPdf, renderPdfPaginated, setEngineLoader, translate, translateStyle, vnodeToForme, watermarkNode };
509
+ export { type Annotation, type EngineLoader, type FontFileReader, type FontSourcePolicy, FormeEditorMeasurer, type FormeEditorMeasurerOptions, type FormeFont, FormeMeasurer, type FormeMeasurerOptions, type FormeNode, type FormePageInfo, type FormeRenderer, type LayoutElement, type LayoutPage, type LayoutPaginatedOptions, MEASURE_BATCH_ROWS, PX_TO_PT, type PaginatedLayout, type RenderFormePdfOptions, type RenderFormePdfResult, type RenderPaginatedResult, type ResolvedFonts, type TranslateContext, type Translated, UNBOUNDED_PAGE_HEIGHT, buildFormeDocument, geometryFromLayout, layoutPaginated, parseDeclarations, pxToPt, renderPdf, renderPdfPaginated, resolveFonts, setEngineLoader, setFontFileReader, translate, translateStyle, vnodeToForme, watermarkNode };
package/dist/index.js CHANGED
@@ -2,26 +2,34 @@ import {
2
2
  BroadPaper,
3
3
  FormeEditorMeasurer,
4
4
  FormeMeasurer,
5
+ MEASURE_BATCH_ROWS,
5
6
  PX_TO_PT,
6
7
  UNBOUNDED_PAGE_HEIGHT,
7
8
  buildFormeDocument,
8
9
  configure,
9
10
  geometryFromLayout,
11
+ layoutPaginated,
10
12
  parseDeclarations,
11
13
  parseLicense,
12
14
  pxToPt,
13
15
  renderPdf,
14
16
  renderPdfPaginated,
17
+ resolveFonts,
15
18
  resolveLicenseStatus,
16
19
  setEngineLoader,
20
+ setFontFileReader,
17
21
  translate,
18
22
  translateStyle,
19
23
  verifyLicense,
20
24
  vnodeToForme,
21
25
  watermarkNode
22
- } from "./chunk-2PT5WRKQ.js";
26
+ } from "./chunk-QA4APTWM.js";
23
27
 
24
28
  // src/engine.node.ts
29
+ var readFontFile = async (path) => {
30
+ const { readFile } = await import("fs/promises");
31
+ return new Uint8Array(await readFile(path));
32
+ };
25
33
  var loadEngine = async () => {
26
34
  const mod = await import("@formepdf/core");
27
35
  return (doc) => mod.renderSerializedDocWithLayout(doc);
@@ -29,22 +37,27 @@ var loadEngine = async () => {
29
37
 
30
38
  // src/index.ts
31
39
  setEngineLoader(loadEngine);
40
+ setFontFileReader(readFontFile);
32
41
  export {
33
42
  BroadPaper,
34
43
  FormeEditorMeasurer,
35
44
  FormeMeasurer,
45
+ MEASURE_BATCH_ROWS,
36
46
  PX_TO_PT,
37
47
  UNBOUNDED_PAGE_HEIGHT,
38
48
  buildFormeDocument,
39
49
  configure,
40
50
  geometryFromLayout,
51
+ layoutPaginated,
41
52
  parseDeclarations,
42
53
  parseLicense,
43
54
  pxToPt,
44
55
  renderPdf,
45
56
  renderPdfPaginated,
57
+ resolveFonts,
46
58
  resolveLicenseStatus,
47
59
  setEngineLoader,
60
+ setFontFileReader,
48
61
  translate,
49
62
  translateStyle,
50
63
  verifyLicense,
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/engine.node.ts","../src/index.ts"],"sourcesContent":["/**\n * The engine as Node, Bun, Deno and edge runtimes get it.\n *\n * `@formepdf/core`'s own conditional exports pick the right build for the\n * runtime resolving it, so the bare specifier is the correct one here — this\n * module is only ever in a graph that is not a browser's.\n */\nimport type { EngineLoader } from \"./api.js\";\n\nexport const loadEngine: EngineLoader = async () => {\n const mod = (await import(\"@formepdf/core\")) as unknown as {\n renderSerializedDocWithLayout(d: Record<string, unknown>): Promise<{ pdf: Uint8Array; layout: unknown; warnings: string[] }>;\n };\n return (doc) => mod.renderSerializedDocWithLayout(doc);\n};\n","/**\n * @broadpaper/forme — the browserless PDF backend, for Node and every other\n * non-browser runtime. Browser bundlers resolve `./index.browser.ts` instead,\n * through the `browser` condition in this package's exports.\n */\nimport { setEngineLoader } from \"./api.js\";\nimport { loadEngine } from \"./engine.node.js\";\n\nsetEngineLoader(loadEngine);\n\nexport * from \"./api.js\";\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;AASO,IAAM,aAA2B,YAAY;AAClD,QAAM,MAAO,MAAM,OAAO,gBAAgB;AAG1C,SAAO,CAAC,QAAQ,IAAI,8BAA8B,GAAG;AACvD;;;ACNA,gBAAgB,UAAU;","names":[]}
1
+ {"version":3,"sources":["../src/engine.node.ts","../src/index.ts"],"sourcesContent":["/**\n * The engine as Node, Bun, Deno and edge runtimes get it.\n *\n * `@formepdf/core`'s own conditional exports pick the right build for the\n * runtime resolving it, so the bare specifier is the correct one here — this\n * module is only ever in a graph that is not a browser's.\n */\nimport type { EngineLoader } from \"./api.js\";\nimport type { FontFileReader } from \"./fonts.js\";\n\n/** A theme font that is not a data: URI or a web address is a file, as the engine itself reads one. */\nexport const readFontFile: FontFileReader = async (path) => {\n const { readFile } = await import(\"node:fs/promises\");\n return new Uint8Array(await readFile(path));\n};\n\nexport const loadEngine: EngineLoader = async () => {\n const mod = (await import(\"@formepdf/core\")) as unknown as {\n renderSerializedDocWithLayout(d: Record<string, unknown>): Promise<{ pdf: Uint8Array; layout: unknown; warnings: string[] }>;\n };\n return (doc) => mod.renderSerializedDocWithLayout(doc);\n};\n","/**\n * @broadpaper/forme — the browserless PDF backend, for Node and every other\n * non-browser runtime. Browser bundlers resolve `./index.browser.ts` instead,\n * through the `browser` condition in this package's exports.\n */\nimport { setEngineLoader, setFontFileReader } from \"./api.js\";\nimport { loadEngine, readFontFile } from \"./engine.node.js\";\n\nsetEngineLoader(loadEngine);\nsetFontFileReader(readFontFile);\n\nexport * from \"./api.js\";\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;AAWO,IAAM,eAA+B,OAAO,SAAS;AAC1D,QAAM,EAAE,SAAS,IAAI,MAAM,OAAO,aAAkB;AACpD,SAAO,IAAI,WAAW,MAAM,SAAS,IAAI,CAAC;AAC5C;AAEO,IAAM,aAA2B,YAAY;AAClD,QAAM,MAAO,MAAM,OAAO,gBAAgB;AAG1C,SAAO,CAAC,QAAQ,IAAI,8BAA8B,GAAG;AACvD;;;ACbA,gBAAgB,UAAU;AAC1B,kBAAkB,YAAY;","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@broadpaper/forme",
3
- "version": "0.2.1",
3
+ "version": "0.4.0",
4
4
  "description": "Browserless PDF backend for BroadPaper, built on the Forme (Rust/WASM) engine. Runs in Node, edge runtimes and the browser.",
5
5
  "keywords": [
6
6
  "pdf",
@@ -50,9 +50,9 @@
50
50
  "dependencies": {
51
51
  "@formepdf/core": "^0.20.0",
52
52
  "@formepdf/shared": "^0.20.0",
53
- "@broadpaper/blocks": "0.2.1",
54
- "@broadpaper/core": "0.2.1",
55
- "@broadpaper/license": "0.2.1"
53
+ "@broadpaper/core": "0.4.0",
54
+ "@broadpaper/license": "0.4.0",
55
+ "@broadpaper/blocks": "0.4.0"
56
56
  },
57
57
  "browser": "./dist/index.browser.js",
58
58
  "scripts": {