@quario/pdf 0.3.0 → 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/lib/fonts.js DELETED
@@ -1,223 +0,0 @@
1
- /**
2
- * The faces a render can draw with.
3
- *
4
- * Every face is embedded on the document before the walk starts, so resolving
5
- * a style to a face during layout is synchronous — pdf-lib's `embedFont` is
6
- * async, and the walk is not. Embedding the twelve base-14 faces up front
7
- * costs under 200 bytes in the output, which is cheaper than threading a
8
- * two-pass measure/draw split through the layout to embed only what is used.
9
- */
10
- import { StandardFonts } from "pdf-lib";
11
-
12
- // The three families the vocabulary names, four variants each, indexed by
13
- // `bold + 2 * italic` — the same ordering the schema's `bold`/`italic` flags
14
- // produce. Unknown family names fall back to sans, as SCHEMA.md specifies.
15
- let BASE = {
16
- sans: [
17
- StandardFonts.Helvetica,
18
- StandardFonts.HelveticaBold,
19
- StandardFonts.HelveticaOblique,
20
- StandardFonts.HelveticaBoldOblique,
21
- ],
22
- serif: [
23
- StandardFonts.TimesRoman,
24
- StandardFonts.TimesRomanBold,
25
- StandardFonts.TimesRomanItalic,
26
- StandardFonts.TimesRomanBoldItalic,
27
- ],
28
- mono: [
29
- StandardFonts.Courier,
30
- StandardFonts.CourierBold,
31
- StandardFonts.CourierOblique,
32
- StandardFonts.CourierBoldOblique,
33
- ],
34
- };
35
-
36
- /** @typedef {{ families: Record<string, any[]> }} Fonts */
37
-
38
- // Host-supplied TrueType families need a font parser, which is an optional
39
- // peer: base-14 reports install nothing extra. Loaded only when `fonts` is
40
- // passed, and a missing package is reported as what it is.
41
- /** @type {(doc: any) => Promise<void>} */
42
- let useFontkit = async (doc) => {
43
- let fontkit;
44
- try {
45
- fontkit = (await import("@pdf-lib/fontkit")).default;
46
- } catch {
47
- throw Error("options.fonts: install @pdf-lib/fontkit to embed TrueType families");
48
- }
49
- doc.registerFontkit(fontkit);
50
- };
51
-
52
- /** @type {(doc: any) => Promise<Record<string, any[]>>} */
53
- let embedBase = async (doc) => {
54
- /** @type {Record<string, any[]>} */
55
- let families = {};
56
- for (let [name, variants] of Object.entries(BASE))
57
- families[name] = await Promise.all(variants.map((v) => doc.embedFont(v)));
58
- return families;
59
- };
60
-
61
- /** @type {(name: string, def: any) => string} */
62
- let asFamily = (name, def) => {
63
- let path = "options.fonts." + name;
64
- if (!def || typeof def !== "object")
65
- throw Error(path + ": expected { regular, bold?, italic?, boldItalic? }");
66
- if (def.regular == null) throw Error(path + ".regular: required");
67
- return path;
68
- };
69
-
70
- /** @type {(doc: any, src: any, path: string, variant: string) => Promise<any>} */
71
- let embedOne = async (doc, src, path, variant) => {
72
- try {
73
- return await doc.embedFont(src[variant], { subset: true });
74
- } catch (e) {
75
- throw Error(path + "." + variant + ": " + /** @type {Error} */ (e).message);
76
- }
77
- };
78
-
79
- // Missing variants fall back to the family's regular, so a face is
80
- // always resolvable once the family is named.
81
- /** @type {(doc: any, src: any, path: string, variant: string, faces: any[]) => Promise<any>} */
82
- let embedVariant = async (doc, src, path, variant, faces) => {
83
- if (src[variant] == null) return faces[0];
84
- return embedOne(doc, src, path, variant);
85
- };
86
-
87
- /** @type {(doc: any, name: string, def: any) => Promise<any[]>} */
88
- let familyOf = async (doc, name, def) => {
89
- let path = asFamily(name, def);
90
- /** @type {any[]} */
91
- let faces = [];
92
- for (let variant of ["regular", "bold", "italic", "boldItalic"])
93
- faces.push(await embedVariant(doc, def, path, variant, faces));
94
- return faces;
95
- };
96
-
97
- /** @type {(doc: any, custom: any, families: Record<string, any[]>) => Promise<void>} */
98
- let embedCustom = async (doc, custom, families) => {
99
- if (!(custom && Object.keys(custom).length)) return;
100
- await useFontkit(doc);
101
- for (let [name, def] of Object.entries(custom))
102
- families[name.toLowerCase()] = await familyOf(doc, name, def);
103
- };
104
-
105
- /**
106
- * Embed every face the render can reach and return the registry `face()` reads.
107
- *
108
- * @param {any} doc The pdf-lib document.
109
- * @param {any} custom `options.fonts`, or null.
110
- * @returns {Promise<Fonts>}
111
- */
112
- export async function embedFonts(doc, custom) {
113
- let families = await embedBase(doc);
114
- await embedCustom(doc, custom, families);
115
- return { families };
116
- }
117
-
118
- // What a declared `family` normalises to before it is looked up: lower case,
119
- // and nothing at all when the value is not a name. Exported because the report
120
- // default is normalised once per render rather than once per cell, and two
121
- // spellings of this would drift.
122
- /** @type {(style: any) => string} */
123
- export let familyName = (style) =>
124
- typeof style.family === "string" ? style.family.toLowerCase() : "";
125
-
126
- // Own-key lookup: the family name is render data, so `constructor` must
127
- // fall back to sans rather than resolve an inherited member.
128
- /** @type {(fonts: Fonts, name: string | null) => any[]} */
129
- let variantsOf = (fonts, name) =>
130
- name && Object.hasOwn(fonts.families, name) ? fonts.families[name] : fonts.families.sans;
131
-
132
- /** @type {(style: any) => number} */
133
- let faceIndex = (style) => (style.bold ? 1 : 0) | (style.italic ? 2 : 0);
134
-
135
- /**
136
- * Resolve a style block to one embedded face.
137
- *
138
- * `fallback` is the report default's family, which the canvas carries for the
139
- * whole render (docs/adr/0033) — so text declaring no family is set in the
140
- * document's.
141
- *
142
- * The fallback turns on whether a family was *declared*, never on whether the
143
- * declared one resolves: a literal `family` is a non-empty string by the time
144
- * the engine passes it, but an `=` expression is resolved at render and
145
- * reaches here unchecked, so an item can declare `""`, a number or `null`.
146
- * Those are the item's own declaration and fall to sans like any other name
147
- * this target cannot resolve (SCHEMA.md) — the document's family is for text
148
- * that declared none. Own-key, because the block is render data.
149
- *
150
- * @type {(fonts: Fonts, style: any, fallback: string | null) => any}
151
- */
152
- export function face(fonts, style, fallback) {
153
- let declared = Object.hasOwn(style, "family");
154
- return variantsOf(fonts, declared ? familyName(style) : fallback)[faceIndex(style)];
155
- }
156
-
157
- // The distance from a line's top to its baseline: pdf-lib's ascender, which
158
- // matches the per-family fractions the hand-rolled writer used.
159
- /** @type {(font: any, size: number) => number} */
160
- export let ascOf = (font, size) => font.heightAtSize(size, { descender: false });
161
-
162
- // Text measurement. pdf-lib encodes on draw, so nothing here needs to know
163
- // about WinAnsi or glyph ids.
164
- /** @type {(font: any, text: string, size: number) => number} */
165
- export let width = (font, text, size) => font.widthOfTextAtSize(text, size);
166
-
167
- // Printable ASCII, which every text face encodes and which almost all report
168
- // text is. Asking the cmap instead costs a set walk per atom.
169
- let ASCII = /^[\x20-\x7E]*$/;
170
-
171
- // pdf-lib's getCharacterSet() is the cmap for both kinds of face: WinAnsi
172
- // code points on the base-14, fontkit's characterSet on an embedded TTF.
173
- // widthOfTextAtSize does not throw on a TrueType .notdef, so the old probe
174
- // let cmap holes through as glyph id 0. A face with no cmap (the layout
175
- // suite's measuring doubles) does not substitute: width is its whole contract.
176
- let CHARSETS = new WeakMap();
177
- let OPEN = { has: () => true };
178
-
179
- /** @typedef {{ has: (code: number) => boolean }} Cmap */
180
-
181
- /** @type {(font: any) => Cmap} */
182
- let charsetOf = (font) => {
183
- let set = CHARSETS.get(font);
184
- if (set) return set;
185
- set = typeof font.getCharacterSet === "function" ? new Set(font.getCharacterSet()) : OPEN;
186
- CHARSETS.set(font, set);
187
- return set;
188
- };
189
-
190
- /**
191
- * Replace characters a face cannot draw with `?`.
192
- *
193
- * Cell text is untrusted data, so one stray character must not fail the
194
- * render: SCHEMA.md promises `?` for any face — WinAnsi on the base-14, the
195
- * cmap on an embedded TrueType. A face that also lacks `?` omits the
196
- * character rather than drawing .notdef.
197
- *
198
- * The ASCII guard carries the common case; only text with a character
199
- * outside it pays for the cmap walk. Printable ASCII is a subset of WinAnsi
200
- * and of every text face this target embeds.
201
- *
202
- * @type {(font: any, text: string) => string}
203
- */
204
- export let printable = (font, text) => {
205
- if (!text || ASCII.test(text) || encodable(font, text)) return text;
206
- return replaceUndrawable(charsetOf(font), text);
207
- };
208
-
209
- /** @type {(set: Cmap, text: string) => string} */
210
- let replaceUndrawable = (set, text) => {
211
- let sub = set.has(0x3f) ? "?" : "";
212
- // Encodedness is a per-code-point question, so that is the unit to ask it
213
- // in — the grapheme clusters the rule protects are undrawable anyway.
214
- // oxlint-disable-next-line typescript/no-misused-spread
215
- return [...text].map((char) => (set.has(char.codePointAt(0) ?? -1) ? char : sub)).join("");
216
- };
217
-
218
- /** @type {(font: any, text: string) => boolean} */
219
- let encodable = (font, text) => {
220
- let set = charsetOf(font);
221
- // oxlint-disable-next-line typescript/no-misused-spread
222
- return [...text].every((char) => set.has(char.codePointAt(0) ?? -1));
223
- };
package/lib/image.js DELETED
@@ -1,58 +0,0 @@
1
- /**
2
- * An image's own size, read from its header. Sizing is a fact about the bytes
3
- * rather than anything a page can answer, so it sits here and not on the
4
- * drawing surface: the band flow asks for it while measuring, where no
5
- * document is in reach at all, and again while drawing, where one is.
6
- *
7
- * Read, never decoded — a PNG's IHDR and a JPEG's frame header carry the two
8
- * numbers this target needs, and nothing else here looks at a pixel.
9
- *
10
- * The XLSX target reads the same two headers for its own placement, in pixels.
11
- * Whether that fact belongs on the engine's event instead — it already reads
12
- * the magic numbers to name the format — is bead `quario-b0h`.
13
- */
14
-
15
- // Points, at the conventional 96 dpi the schema names: 72/96 of a pixel.
16
- let PER_PX = 72 / 96;
17
-
18
- /** @type {(bytes: Uint8Array, at: number) => number} */
19
- let word = (bytes, at) => (bytes[at] << 8) | bytes[at + 1];
20
-
21
- /** @type {(code: number) => boolean} */
22
- let inSof = (code) => code >= 0xc0 && code <= 0xcf;
23
-
24
- /** @type {(code: number) => boolean} */
25
- let notTable = (code) => code !== 0xc4 && code !== 0xc8 && code !== 0xcc;
26
-
27
- // The frame header carries the dimensions, and it is the first SOFn marker:
28
- // every code in C0..CF except the three in that range that are not frames --
29
- // DHT, the JPG extension, and DAC.
30
- /** @type {(code: number) => boolean} */
31
- let isFrame = (code) => inSof(code) && notTable(code);
32
-
33
- /** @type {(bytes: Uint8Array) => { w: number, h: number }} */
34
- let jpegSize = (bytes) => {
35
- for (let at = 2; at + 9 < bytes.length; at += 2 + word(bytes, at + 2)) {
36
- if (bytes[at] !== 0xff) break;
37
- if (isFrame(bytes[at + 1])) return { w: word(bytes, at + 7), h: word(bytes, at + 5) };
38
- }
39
- return { w: 0, h: 0 };
40
- };
41
-
42
- /**
43
- * The image's intrinsic size in points. The format is the engine's sniff,
44
- * riding on the event, so nothing here decides it a second time.
45
- *
46
- * @param {Uint8Array} bytes The image file.
47
- * @param {string} format The engine's sniff: `"png"` or `"jpeg"`.
48
- * @returns {{ w: number, h: number }} The size, in points.
49
- */
50
- export let intrinsic = (bytes, format) => {
51
- // A PNG carries the two numbers in its IHDR at a fixed offset.
52
- let { w, h } = format === "png" ? { w: word(bytes, 18), h: word(bytes, 22) } : jpegSize(bytes);
53
- // The engine vouched for the magic numbers, not for the rest of the file:
54
- // a truncated header reaches here as a zero, and failing loudly beats
55
- // drawing an image with no size (SCHEMA.md, "Image item").
56
- if (!(w > 0 && h > 0)) throw Error("image: could not read the image's size from its bytes");
57
- return { w: w * PER_PX, h: h * PER_PX };
58
- };