@quario/pdf 0.3.0 → 0.5.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/CHANGELOG.md CHANGED
@@ -7,13 +7,92 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.5.0] - 2026-09-05
11
+
12
+ ### Added
13
+
14
+ - **A spanning cell draws as one box** across the columns it covers, through
15
+ `@quario/layout`. It takes no part in measuring them, and a spanning row that
16
+ slices across pages or page-column strips follows the geometry of the strip
17
+ each slice lands in.
18
+ - **`valign`** on table cells and split slots, through `@quario/layout`: middle
19
+ and bottom place the content in the height its row or split leaves over it.
20
+
21
+ ### Changed
22
+
23
+ - **A row's box is drawn by the row's cells**, through `@quario/layout`. A box
24
+ declared on a table row used to be one rect across the columns; each covered
25
+ cell now draws its own. A row's `borderBottom` still reads as one continuous
26
+ edge, a row's `borderLeft` becomes an edge on every cell rather than one at
27
+ the row's outer left, and a bordered row is taller by its border's width,
28
+ since a border occupies height as a cell's always has.
29
+
30
+ - **Embedding TrueType fonts now needs `fontkit`, not `@pdf-lib/fontkit`.**
31
+ Install `fontkit` instead; nothing else about `options.fonts` changes. The
32
+ old package's bundle crashed with a bare `ReferenceError` on any OpenType
33
+ face needing a shaping state machine — which is every Devanagari, Bengali,
34
+ Tamil, Khmer or Myanmar webfont in practice, and some fifty further scripts
35
+ besides. Those faces now measure, embed and draw. Which scripts a face
36
+ supports remains the font's and the parser's to answer, not this package's.
37
+
38
+ - **`pdf-lib` is now `@cantoo/pdf-lib`.** A maintained fork, and what
39
+ `fontkit`'s subsetting requires. Rendered documents are unchanged in what
40
+ they draw: every drawn string is identical and every filled area lands in
41
+ the same place, but the file's bytes differ, so a host comparing digests
42
+ against stored output will see them move once.
43
+
44
+ ## [0.4.0] - 2026-09-03
45
+
46
+ ### Changed
47
+
48
+ - **A split slot's box now fills the split's height.** A slot's background and
49
+ border stopped at that slot's own content and now take the split's height,
50
+ the way a table cell's take their row's. See the `@quario/layout` changelog
51
+ for the rule and for what it costs a report that relied on the short box.
52
+
53
+ - **A date string under `format: "date"` now presents as a date.** The
54
+ drawn text comes from the engine's `format()` helper, which revives the two
55
+ read forms. See the `quario` changelog for the forms and the timezone rule.
56
+
57
+ ### Changed
58
+
59
+ - **The layout moved to `@quario/layout`; this target paints its list.**
60
+ Measurement, wrapping, pagination, page furniture, the marking's geometry
61
+ and the page-size table now live in the layout package, which this target
62
+ depends on and runs first; pdf-lib writes the resulting display list. **The
63
+ move itself changed no output**, byte for byte, for the same input; the fix
64
+ below is a separate change and does move some. The `page` and `fonts`
65
+ options are the layout's own, so the same object configures `pdf()`, the
66
+ viewer and the editor. The font mapping's shape is now refused at the
67
+ factory call rather than at render.
68
+
69
+ ### Fixed
70
+
71
+ - **A nested group's footer keeps the page columns.** Inside a page-column
72
+ region, the first footer of a group nested in it ended the region: the
73
+ strips collapsed and every band after it was laid out across the page, as
74
+ if `columns` were absent. Only the node that declared `columns` closes its
75
+ own region now — a nested group's header and footer are region content,
76
+ laid out in the strips.
77
+
78
+ - **A padded or bordered item or row that breaks across a page keeps its
79
+ box.** Content too tall for one page is drawn as slices, and a slice now
80
+ carries the box sides the break left it — the top on the first, the bottom
81
+ on the last. Previously an item that overflowed lost its box entirely, so
82
+ the same declaration drew a border on short data and none on long; a
83
+ bordered table cell drew all four sides on every slice, putting an edge
84
+ across the middle of its own text; and a row's own border vanished when
85
+ the row was too tall for a page. A slice's lines are also drawn inside
86
+ the horizontal padding they were wrapped for, rather than flush to the
87
+ column. **This changes output** for reports where such content breaks.
88
+
10
89
  ## [0.3.0] - 2026-09-02
11
90
 
12
91
  ### Added
13
92
 
14
93
  - **`format` stringifies at the edge from the instance locale.** A PDF
15
94
  without a host locale still uses `en-US` / UTC, so the bytes stay
16
- reproducible (`docs/adr/0041`, `docs/adr/0025`).
95
+ reproducible.
17
96
 
18
97
  - **A height-declared report header pins from the page top.** Leftover sits
19
98
  under the items; the next band starts at the pin. The half-line group gap
package/README.md CHANGED
@@ -3,8 +3,9 @@
3
3
  **The PDF render target for [quario](https://www.npmjs.com/package/quario).** Renders a report
4
4
  definition straight to a paginated PDF. No headless browser, no print CSS, deterministic bytes.
5
5
 
6
- quario does the typesetting: pagination, keep-together, tables, fonts, the document outline.
7
- [pdf-lib](https://pdf-lib.js.org) writes the file. You get typesetting plus pdf-lib: no Chromium
6
+ quario does the typesetting pagination, keep-together, tables, fonts, the document outline
7
+ through `@quario/layout`, the same layout the viewer paints on screen, so the preview and the
8
+ document break their pages in the same places. [`@cantoo/pdf-lib`](https://github.com/Cantoo-Scribe/pdf-lib) writes the file. You get typesetting plus pdf-lib: no Chromium
8
9
  in your container, no page-load race, no fonts-not-ready flake.
9
10
 
10
11
  ## Install
@@ -13,13 +14,13 @@ in your container, no page-load race, no fonts-not-ready flake.
13
14
  npm install quario @quario/pdf
14
15
  ```
15
16
 
16
- pdf-lib comes along as a dependency; the engine is a peer, installed beside it. ESM-only, Node 22+, and browser-ready through
17
+ `@cantoo/pdf-lib` comes along as a dependency; the engine is a peer, installed beside it. ESM-only, Node 22+, and browser-ready through
17
18
  any standards-based ESM bundler. The renderer returns bytes, so the host decides where they go.
18
19
 
19
20
  Embedding TrueType fonts also needs the optional peer:
20
21
 
21
22
  ```bash
22
- npm install @pdf-lib/fontkit
23
+ npm install fontkit
23
24
  ```
24
25
 
25
26
  It loads only when you pass `fonts` to the factory. The base-14 families need nothing extra.
package/lib/embed.js ADDED
@@ -0,0 +1,102 @@
1
+ /**
2
+ * The faces a document can draw with, embedded before anything is painted.
3
+ *
4
+ * Every face is embedded on the document before the list is painted, so
5
+ * resolving a list op's face to a pdf-lib font is synchronous — pdf-lib's
6
+ * `embedFont` is async, and the paint is not. Embedding the twelve base-14
7
+ * faces up front costs under 200 bytes in the output, which is cheaper than a
8
+ * two-pass split to embed only what is used.
9
+ *
10
+ * The registry mirrors the layout's: the same family names, the same four
11
+ * variants indexed by `bold + 2 * italic`, a missing variant falling back to
12
+ * the family's regular — so a `FaceRef` on the list names one font here.
13
+ */
14
+ import { StandardFonts } from "@cantoo/pdf-lib";
15
+
16
+ // A family's four names are one stem plus the weight and slant suffixes that
17
+ // family spells, so each family names its two and the four are walked out of
18
+ // them rather than written twelve times over.
19
+ /** @type {(stem: string, slant: string) => string[]} */
20
+ let variants = (stem, slant) =>
21
+ ["", "Bold", slant, "Bold" + slant].map((v) => /** @type {any} */ (StandardFonts)[stem + v]);
22
+ let BASE = {
23
+ sans: variants("Helvetica", "Oblique"),
24
+ serif: variants("TimesRoman", "Italic"),
25
+ mono: variants("Courier", "Oblique"),
26
+ };
27
+
28
+ /** @typedef {{ families: Record<string, any[]> }} Fonts */
29
+
30
+ // Host-supplied TrueType families need a font parser, which is an optional
31
+ // peer: base-14 reports install nothing extra. Loaded only when `fonts` is
32
+ // passed, and a missing package is reported as what it is.
33
+ /** @type {(doc: any) => Promise<void>} */
34
+ let useFontkit = async (doc) => {
35
+ let fontkit;
36
+ try {
37
+ fontkit = await import("fontkit");
38
+ } catch {
39
+ throw Error("options.fonts: install fontkit to embed TrueType families");
40
+ }
41
+ doc.registerFontkit(fontkit);
42
+ };
43
+
44
+ /** @type {(doc: any) => Promise<Record<string, any[]>>} */
45
+ let embedBase = async (doc) => {
46
+ /** @type {Record<string, any[]>} */
47
+ let families = {};
48
+ for (let [name, variants] of Object.entries(BASE))
49
+ families[name] = await Promise.all(variants.map((v) => doc.embedFont(v)));
50
+ return families;
51
+ };
52
+
53
+ /** @type {(doc: any, src: any, path: string, variant: string) => Promise<any>} */
54
+ let embedOne = async (doc, src, path, variant) => {
55
+ try {
56
+ return await doc.embedFont(src[variant], { subset: true });
57
+ } catch (e) {
58
+ throw Error(path + "." + variant + ": " + /** @type {Error} */ (e).message);
59
+ }
60
+ };
61
+
62
+ // Missing variants fall back to the family's regular, so a face is always
63
+ // resolvable once the family is named. The shape was checked at the layout's
64
+ // factory call; this only embeds.
65
+ /** @type {(doc: any, name: string, def: any) => Promise<any[]>} */
66
+ let familyOf = async (doc, name, def) => {
67
+ let path = "options.fonts." + name;
68
+ /** @type {any[]} */
69
+ let faces = [];
70
+ for (let variant of ["regular", "bold", "italic", "boldItalic"])
71
+ faces.push(def[variant] == null ? faces[0] : await embedOne(doc, def, path, variant));
72
+ return faces;
73
+ };
74
+
75
+ /** @type {(doc: any, custom: any, families: Record<string, any[]>) => Promise<void>} */
76
+ let embedCustom = async (doc, custom, families) => {
77
+ if (!(custom && Object.keys(custom).length)) return;
78
+ await useFontkit(doc);
79
+ for (let [name, def] of Object.entries(custom))
80
+ families[name.toLowerCase()] = await familyOf(doc, name, def);
81
+ };
82
+
83
+ /**
84
+ * Embed every face the list can name and return the registry the painter
85
+ * reads.
86
+ *
87
+ * @param {any} doc The pdf-lib document.
88
+ * @param {any} custom `options.fonts`, or null.
89
+ * @returns {Promise<Fonts>}
90
+ */
91
+ export async function embedFonts(doc, custom) {
92
+ let families = await embedBase(doc);
93
+ await embedCustom(doc, custom, families);
94
+ return { families };
95
+ }
96
+
97
+ /**
98
+ * The embedded font one list op names.
99
+ *
100
+ * @type {(fonts: Fonts, ref: { family: string, variant: number }) => any}
101
+ */
102
+ export let fontFor = (fonts, ref) => fonts.families[ref.family][ref.variant];
package/lib/index.d.ts CHANGED
@@ -1,12 +1,8 @@
1
+ import type { LayoutFontFamily, LayoutPage } from "@quario/layout";
1
2
  import type { Target } from "quario";
2
3
 
3
- /** Page geometry, in PostScript points. Target configuration, never schema. */
4
- export interface PdfPage {
5
- /** Named size or `[width, height]` in points. Default `'A4'`. */
6
- size?: "A4" | "letter" | [number, number];
7
- /** Margin on all four sides, in points. Default `54` (0.75 in). */
8
- margin?: number;
9
- }
4
+ /** Page geometry, in PostScript points: the layout's own, never schema. */
5
+ export type PdfPage = LayoutPage;
10
6
 
11
7
  /** Optional document information; never includes dates, so output stays deterministic. */
12
8
  export interface PdfMeta {
@@ -15,13 +11,8 @@ export interface PdfMeta {
15
11
  subject?: string;
16
12
  }
17
13
 
18
- /** One embeddable TrueType family; missing variants fall back to `regular`. */
19
- export interface PdfFontFamily {
20
- regular: Uint8Array | ArrayBuffer;
21
- bold?: Uint8Array | ArrayBuffer;
22
- italic?: Uint8Array | ArrayBuffer;
23
- boldItalic?: Uint8Array | ArrayBuffer;
24
- }
14
+ /** One embeddable TrueType family: the layout's own, measured and embedded from the same bytes. */
15
+ export type PdfFontFamily = LayoutFontFamily;
25
16
 
26
17
  /** Host controls, taken and validated at the factory call. */
27
18
  export interface PdfOptions {
@@ -30,7 +21,7 @@ export interface PdfOptions {
30
21
  /**
31
22
  * TrueType families to embed (subset), selected from styles by
32
23
  * `family: '<name>'`. Fonts are trusted host assets. Requires the optional
33
- * peer `@pdf-lib/fontkit`; the base-14 families need nothing extra.
24
+ * peer `fontkit`; the base-14 families need nothing extra.
34
25
  */
35
26
  fonts?: Record<string, PdfFontFamily>;
36
27
  }
package/lib/index.js CHANGED
@@ -1,102 +1,28 @@
1
1
  /**
2
2
  * @quario/pdf — the PDF render target: a pure consumer of the engine's public
3
- * event stream, passed to a compiled report's `render`, laying the banded walk out on
4
- * pages. Layout is ours wrapped paragraphs, keep-together, repeated table
5
- * headers, page bands, `break: "page"`, `reset: "page"` — while the writing is pdf-lib's, so
6
- * this target carries a typesetter rather than a PDF implementation. Output
7
- * is deterministic: the document carries no dates of its own, so the same
8
- * input renders the same bytes. Page size is target configuration
3
+ * event stream, passed to a compiled report's `render`. The layout is
4
+ * `@quario/layout`'s one paged display list, the same one the viewer paints
5
+ * on screen (docs/adr/0039) and **pdf-lib writes the file**, so this target
6
+ * is a painter of that list rather than a typesetter or a PDF implementation.
7
+ * Output is deterministic: the document carries no dates of its own, so the
8
+ * same input renders the same bytes. Page size is target configuration
9
9
  * (`pdf({ page.size })`); `page.margin` may also be a document field.
10
10
  *
11
- * This file is options, fonts and geometry, the walk itself, and the passes
12
- * that follow it over the finished pages page furniture and the marking,
13
- * neither of which can be drawn until the page count is known. Every handler
14
- * the driver dispatches to is the band flow's, next door in `layout.js`; this
15
- * file adds none. The work lives in the sibling modules, which ship beside it
16
- * — the package publishes `lib/` verbatim.
11
+ * This file is options, the layout pass, and the document: faces embedded,
12
+ * pages painted, the group tree attached as bookmarks. The work lives in the
13
+ * sibling modules, which ship beside it the package publishes `lib/`
14
+ * verbatim.
17
15
  */
18
- import { PDFDocument } from "pdf-lib";
19
- import { breathe, walk } from "quario";
20
- import { drawing, frame, stamp } from "./canvas.js";
21
- import { embedFonts } from "./fonts.js";
22
- import { flow, furniture } from "./layout.js";
16
+ import { PDFDocument } from "@cantoo/pdf-lib";
17
+ import { layout } from "@quario/layout";
18
+ import { embedFonts } from "./embed.js";
23
19
  import { outline } from "./outline.js";
20
+ import { paint } from "./painter.js";
24
21
 
25
22
  // The options are described once, in the hand-written public declarations, and
26
23
  // read back here — a second copy in JSDoc is a copy that drifts.
27
24
  /** @import { PdfOptions } from './index.d.ts' */
28
25
 
29
- let SIZES = /** @type {Record<string, [number, number]>} */ ({
30
- A4: [595.28, 841.89],
31
- letter: [612, 792],
32
- });
33
-
34
- /** @type {(msg: string) => never} */
35
- let err = (msg) => {
36
- throw Error(msg);
37
- };
38
-
39
- /** @type {(value: number) => boolean} */
40
- let positive = (value) => Number.isFinite(value) && value > 0;
41
-
42
- /** @type {(size: any) => any} */
43
- let named = (size = "A4") => {
44
- let dimensions = Array.isArray(size) ? size : SIZES[size];
45
- // oxlint-disable-next-line no-unused-expressions
46
- dimensions || err('options.page.size: unknown page size "' + size + '"');
47
- return dimensions;
48
- };
49
-
50
- /** @type {(dimensions: any) => { width: number, height: number }} */
51
- let pair = (dimensions) => {
52
- let width = +dimensions[0],
53
- height = +dimensions[1];
54
- // oxlint-disable-next-line no-unused-expressions
55
- (positive(width) && positive(height)) ||
56
- err("options.page.size: expected finite positive dimensions");
57
- return { width, height };
58
- };
59
-
60
- /** @type {(margin: any, width: number, height: number, path?: string) => number} */
61
- let marginOf = (margin = 54, width, height, path = "options.page.margin") => {
62
- let fits = Number.isFinite(margin) && margin >= 0 && 2 * margin < Math.min(width, height);
63
- // oxlint-disable-next-line no-unused-expressions
64
- fits || err(path + ": expected a non-negative number smaller than half the page");
65
- return margin;
66
- };
67
-
68
- // This target's baseline type size. Not a host option: a document's type size
69
- // is the document's own, so it is `style.size` on the report and the number
70
- // here is only what text renders at when nothing declares one. The XLSX target
71
- // carries the same 10 for the same reason (docs/adr/0014, docs/adr/0033).
72
- let BASE = 10;
73
-
74
- // The page box and the content box in one. Size is settled at the factory;
75
- // margin may come from the document's `report-start`, so a render peeks that
76
- // event before opening a canvas.
77
- /** @type {(page: any) => any} */
78
- let hostMargin = (page) => (Object.hasOwn(page, "margin") ? page.margin : undefined);
79
- /** @type {(opening: any) => any} */
80
- let docMargin = (opening) => opening?.margin;
81
- /** @type {(doc: any, host: any) => boolean} */
82
- let bothMargins = (doc, host) => doc != null && host !== undefined;
83
- /** @type {(fromDoc: boolean) => string} */
84
- let marginPath = (fromDoc) => (fromDoc ? "page.margin" : "options.page.margin");
85
- /** @type {(opening: any, page: any) => { value: any, path: string }} */
86
- let marginChoice = (opening, page) => {
87
- let host = hostMargin(page);
88
- let doc = docMargin(opening);
89
- if (bothMargins(doc, host)) err("page.margin: document and target both declare a margin");
90
- return { value: doc ?? host, path: marginPath(doc != null) };
91
- };
92
- /** @type {(options: any, opening?: any) => import('./canvas.js').Frame} */
93
- let geometry = (options, opening) => {
94
- let { page = {} } = options ?? {};
95
- let { width, height } = pair(named(page.size));
96
- let chosen = marginChoice(opening, page);
97
- return frame(width, height, marginOf(chosen.value, width, height, chosen.path), BASE);
98
- };
99
-
100
26
  // Optional document information. Never a date: pdf-lib stamps the current time
101
27
  // unless told otherwise, and a timestamp would make the same input render
102
28
  // different bytes on every run. Each key names the pdf-lib setter it feeds,
@@ -115,27 +41,11 @@ let describe = (doc, meta) => {
115
41
  }
116
42
  };
117
43
 
118
- /** @type {(canvas: import('./canvas.js').Drawing, draw: (i: number) => void) => Promise<void>} */
119
- let overPages = async (canvas, draw) => {
120
- for (let i = 0; i < canvas.count; i++) {
121
- canvas.select(i);
122
- draw(i);
123
- if (i % 50 === 49) await breathe();
124
- }
125
- };
126
-
127
- /** @type {(canvas: import('./canvas.js').Drawing, bands: any,
128
- * pages: { number: number, total: number }[]) => Promise<void>} */
129
- let furnish = async (canvas, bands, pages) => {
130
- if (!bands) return;
131
- await overPages(canvas, (i) => furniture(canvas, bands, pages[i]));
132
- };
133
-
134
- /** @type {(canvas: import('./canvas.js').Drawing, marking: any) => Promise<void>} */
135
- let markPages = async (canvas, marking) => {
136
- if (!marking) return;
137
- let mark = stamp(canvas, marking);
138
- await overPages(canvas, () => canvas.watermark(mark));
44
+ /** @type {(meta: any, custom: any) => Promise<{ doc: any, fonts: import('./embed.js').Fonts }>} */
45
+ let open = async (meta, custom) => {
46
+ let doc = await PDFDocument.create();
47
+ describe(doc, meta);
48
+ return { doc, fonts: await embedFonts(doc, custom) };
139
49
  };
140
50
 
141
51
  /**
@@ -143,57 +53,29 @@ let markPages = async (canvas, marking) => {
143
53
  * `quario().report(schema).render(pdf({ page }), data)` resolves the
144
54
  * complete document as a `Uint8Array` (the host writes the file). Rendering
145
55
  * hands the loop back between event and page batches, so large reports stay
146
- * cooperative. Geometry and options are validated here, at the factory call.
56
+ * cooperative. Geometry and the font mapping are validated here, at the
57
+ * factory call — by the layout target, whose options they are.
147
58
  *
148
59
  * @param {PdfOptions} [options] Host controls (see SCHEMA.md, "The PDF target").
149
60
  * @returns {{name: "pdf", compile: (stream: any) => (data?: any) => Promise<Uint8Array>}}
150
61
  * The target (see SCHEMA.md, "Instances and targets").
151
62
  */
152
63
  export function pdf(options) {
153
- geometry(options);
154
- let meta = options?.meta,
155
- custom = options?.fonts;
64
+ let { page, fonts: custom, meta } = options ?? {};
65
+ let paged = layout({ page, fonts: custom });
156
66
  /** @type {(stream: any) => (data?: any) => Promise<Uint8Array>} */
157
- let compile = (stream) => async (data) => {
158
- let doc = await PDFDocument.create();
159
- describe(doc, meta);
160
- // Every face is embedded before the walk, so resolving a style to a
161
- // font during layout stays synchronous.
162
- let fonts = await embedFonts(doc, custom);
163
- let gen = stream(data);
164
- let first = gen.next();
165
- let geo = geometry(options, first.done ? null : first.value);
166
- let canvas = drawing(doc, geo, fonts);
167
- // The band flow owns the placement state and every handler over it, and
168
- // opens the first page as it is built; this file only hands it the stream.
169
- let { handlers, finish } = flow(canvas);
170
- function* events() {
171
- if (!first.done) yield first.value;
172
- yield* gen;
173
- }
174
- await walk(events(), handlers);
175
- // The flow settled the opening event on its way past — it reserves the
176
- // page bands off it — and hands it back with the marks. What is left on it
177
- // is what the passes below want: the band closures to render per page, and
178
- // the marking's wording to stamp.
179
- // Null when nothing settled one, which reads as a document owing neither.
180
- let { marks, opening, pages } = finish();
181
- opening = opening || {};
182
- // The passes below run over the finished pages, not the stream — no walk
183
- // at all — so they open each page themselves and breathe on their own
184
- // rather than through the driver.
185
- await furnish(canvas, opening.page, pages);
186
- // Every image the walk and the furniture pass placed, embedded and drawn
187
- // now: a walk handler cannot await, and the page bands' images are not
188
- // placed until the pass above has run. Before the marking, so an image can
189
- // never cover it.
190
- await canvas.pictures();
191
- // The unlicensed marking goes on last, over content and page furniture
192
- // alike, once per page (LICENSE section 6). Its wording rode in on
193
- // `report-start`; only the placement is this target's.
194
- await markPages(canvas, opening.marking);
195
- outline(doc, marks, canvas.refs);
196
- return doc.save();
67
+ let compile = (stream) => {
68
+ let lay = paged.compile(stream);
69
+ return async (data) => {
70
+ // The list measures against its own faces, so the document is written
71
+ // from the same breaks the preview shows; opening the document and
72
+ // embedding its faces depend on nothing the list decides, so they
73
+ // overlap the layout pass.
74
+ let [list, { doc, fonts }] = await Promise.all([lay(data), open(meta, custom)]);
75
+ let refs = await paint(doc, fonts, list);
76
+ outline(doc, list.marks, refs);
77
+ return doc.save();
78
+ };
197
79
  };
198
80
  return { name: "pdf", compile };
199
81
  }
package/lib/outline.js CHANGED
@@ -7,7 +7,7 @@
7
7
  * own position. pdf-lib has no bookmark API, so the objects are built through
8
8
  * its low-level context — the only place in this target that does.
9
9
  */
10
- import { PDFHexString, PDFName, PDFNumber } from "pdf-lib";
10
+ import { PDFHexString, PDFName, PDFNumber } from "@cantoo/pdf-lib";
11
11
 
12
12
  // A PDF *text string*: UTF-16BE with a BOM, so a title survives whatever the
13
13
  // author wrote. `PDFHexString.fromText` encodes exactly that.