@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/CHANGELOG.md +46 -1
- package/README.md +3 -2
- package/lib/embed.js +111 -0
- package/lib/index.d.ts +5 -14
- package/lib/index.js +34 -152
- package/lib/painter.js +205 -0
- package/package.json +6 -4
- package/lib/balance.js +0 -90
- package/lib/box.js +0 -111
- package/lib/canvas.js +0 -389
- package/lib/fonts.js +0 -223
- package/lib/image.js +0 -58
- package/lib/layout.js +0 -1804
- package/lib/style.js +0 -97
- package/lib/text.js +0 -258
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
|
-
};
|