@quario/layout 0.1.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 +84 -0
- package/LICENSE +219 -0
- package/README.md +94 -0
- package/lib/balance.js +90 -0
- package/lib/box.js +143 -0
- package/lib/canvas.js +335 -0
- package/lib/fonts.js +409 -0
- package/lib/image.js +58 -0
- package/lib/index.d.ts +220 -0
- package/lib/index.js +114 -0
- package/lib/layout.js +1913 -0
- package/lib/page.js +99 -0
- package/lib/paint.js +266 -0
- package/lib/style.js +104 -0
- package/lib/text.js +258 -0
- package/package.json +71 -0
package/lib/fonts.js
ADDED
|
@@ -0,0 +1,409 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The faces a layout can measure with — the one measurer (docs/adr/0039).
|
|
3
|
+
*
|
|
4
|
+
* Nothing here can write a document. The base-14 families measure against the
|
|
5
|
+
* AFM metrics `@pdf-lib/standard-fonts` carries, over WinAnsi, which is what
|
|
6
|
+
* pdf-lib measures against when it writes the same face — so a line breaks
|
|
7
|
+
* where the PDF breaks it. A host-supplied TrueType family measures against
|
|
8
|
+
* fontkit's shaping of the same bytes the PDF target embeds. Every face is
|
|
9
|
+
* loaded before the walk starts, so resolving a style to one during layout
|
|
10
|
+
* stays synchronous.
|
|
11
|
+
*
|
|
12
|
+
* A face answers three questions: how wide a string is at a size, how far a
|
|
13
|
+
* line's top sits above its baseline, and which code points it can draw. A
|
|
14
|
+
* base-14 face answers a fourth — what each code point's advance is — which
|
|
15
|
+
* the list carries so a painter can correct for the screen face standing in
|
|
16
|
+
* for it. A host's own face is nobody's stand-in and answers only the three.
|
|
17
|
+
*/
|
|
18
|
+
import { Encodings, Font, FontNames } from "@pdf-lib/standard-fonts";
|
|
19
|
+
|
|
20
|
+
// The three families the vocabulary names, four variants each, indexed by
|
|
21
|
+
// `bold + 2 * italic` — the same ordering the schema's `bold`/`italic` flags
|
|
22
|
+
// produce. Unknown family names fall back to sans, as SCHEMA.md specifies.
|
|
23
|
+
let BASE = {
|
|
24
|
+
sans: [
|
|
25
|
+
FontNames.Helvetica,
|
|
26
|
+
FontNames.HelveticaBold,
|
|
27
|
+
FontNames.HelveticaOblique,
|
|
28
|
+
FontNames.HelveticaBoldOblique,
|
|
29
|
+
],
|
|
30
|
+
serif: [
|
|
31
|
+
FontNames.TimesRoman,
|
|
32
|
+
FontNames.TimesRomanBold,
|
|
33
|
+
FontNames.TimesRomanItalic,
|
|
34
|
+
FontNames.TimesRomanBoldItalic,
|
|
35
|
+
],
|
|
36
|
+
mono: [
|
|
37
|
+
FontNames.Courier,
|
|
38
|
+
FontNames.CourierBold,
|
|
39
|
+
FontNames.CourierOblique,
|
|
40
|
+
FontNames.CourierBoldOblique,
|
|
41
|
+
],
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Which face a piece of the list is set in: the family it resolved to, the
|
|
46
|
+
* variant index, and whether the bytes behind it came from the host. A painter
|
|
47
|
+
* maps the three to its own face — pdf-lib's embedded font, a CSS font string.
|
|
48
|
+
*
|
|
49
|
+
* `embedded` is the fact a painter cannot work out for itself. A host may map
|
|
50
|
+
* its own family onto `sans`, `serif` or `mono` (SCHEMA.md), so the family name
|
|
51
|
+
* does not say which kind of face this is, and the two kinds are drawn
|
|
52
|
+
* differently: a stand-in has to be corrected glyph by glyph, a host's own face
|
|
53
|
+
* must not be. See `paint.js`.
|
|
54
|
+
* @typedef {{ family: string, variant: number, embedded: boolean }} FaceRef
|
|
55
|
+
*/
|
|
56
|
+
/**
|
|
57
|
+
* `shape` belongs to the base-14 faces alone: it exists so a painter can
|
|
58
|
+
* correct a [stand-in face](../../../CONTEXT.md#stand-in-face), and nothing
|
|
59
|
+
* stands in for bytes the host supplied. So **a face has `shape` exactly when
|
|
60
|
+
* its ref says `embedded: false`** — one fact, and the two constructors below
|
|
61
|
+
* each settle both halves of it, because a face whose ref disagreed with its
|
|
62
|
+
* own `shape` would have the list promise a painter advances it never carries
|
|
63
|
+
* (or carry advances no painter will place).
|
|
64
|
+
* @typedef {{ ref: FaceRef,
|
|
65
|
+
* widthOfTextAtSize: (text: string, size: number) => number,
|
|
66
|
+
* heightAtSize: (size: number) => number,
|
|
67
|
+
* getCharacterSet?: () => number[],
|
|
68
|
+
* shape?: (text: string, size: number) => { advances: number[] } }} Face
|
|
69
|
+
*/
|
|
70
|
+
/** @typedef {{ families: Record<string, Face[]> }} Fonts */
|
|
71
|
+
|
|
72
|
+
/** @type {(text: string) => string[]} */
|
|
73
|
+
let codePoints = (text) => Array.from(text);
|
|
74
|
+
|
|
75
|
+
// A base-14 face: AFM widths and kerning over WinAnsi. The width of a string
|
|
76
|
+
// is the sum of its glyph widths plus the kerning between neighbours — the
|
|
77
|
+
// same arithmetic pdf-lib's standard-font embedder does, kept in step so the
|
|
78
|
+
// two measure alike. A glyph without a width is 250, as it is there.
|
|
79
|
+
//
|
|
80
|
+
// One glyph per code point, always: the text arrives WinAnsi-encodable because
|
|
81
|
+
// `printable` substituted everything else, the encoding is a one-to-one table,
|
|
82
|
+
// and there is no shaping to fold a pair into a ligature. That is what lets a
|
|
83
|
+
// painter place these advances against the characters of the source string.
|
|
84
|
+
/** @type {(family: string, variant: number, name: string) => Face} */
|
|
85
|
+
let standard = (family, variant, name) => {
|
|
86
|
+
let ref = { family, variant, embedded: false };
|
|
87
|
+
let font = Font.load(/** @type {any} */ (name));
|
|
88
|
+
let encoding = Encodings.WinAnsi;
|
|
89
|
+
/** @type {(text: string) => { code: number, name: string }[]} */
|
|
90
|
+
let glyphs = (text) =>
|
|
91
|
+
codePoints(text).map((char) => encoding.encodeUnicodeCodePoint(char.codePointAt(0) ?? 0));
|
|
92
|
+
/** @type {(glyph: { name: string }, next?: { name: string }) => number} */
|
|
93
|
+
let advance = (glyph, next) =>
|
|
94
|
+
(font.getWidthOfGlyph(glyph.name) || 250) +
|
|
95
|
+
(next ? font.getXAxisKerningForPair(glyph.name, next.name) || 0 : 0);
|
|
96
|
+
let ascender = font.Ascender || font.FontBBox[3];
|
|
97
|
+
return {
|
|
98
|
+
ref,
|
|
99
|
+
widthOfTextAtSize: (text, size) => {
|
|
100
|
+
let list = glyphs(text);
|
|
101
|
+
let total = 0;
|
|
102
|
+
for (let [i, glyph] of list.entries()) total += advance(glyph, list[i + 1]);
|
|
103
|
+
return (total * size) / 1000;
|
|
104
|
+
},
|
|
105
|
+
heightAtSize: (size) => (ascender * size) / 1000,
|
|
106
|
+
getCharacterSet: () => encoding.supportedCodePoints,
|
|
107
|
+
shape: (text, size) => {
|
|
108
|
+
let list = glyphs(text);
|
|
109
|
+
return { advances: list.map((glyph, i) => (advance(glyph, list[i + 1]) * size) / 1000) };
|
|
110
|
+
},
|
|
111
|
+
};
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
// A TrueType face: fontkit's shaping of the bytes, measured in the font's own
|
|
115
|
+
// units. pdf-lib shapes the same bytes with the same fontkit when it embeds
|
|
116
|
+
// them and advances by the same widths, so the two agree.
|
|
117
|
+
//
|
|
118
|
+
// No `shape`. Shaping is not one glyph per code point here — a ligature folds
|
|
119
|
+
// two characters into one glyph, an RTL run comes back in visual order, a
|
|
120
|
+
// combining mark advances by nothing — so an advance array has no character to
|
|
121
|
+
// belong to. A painter with these bytes draws the run whole and lets its own
|
|
122
|
+
// shaper place the glyphs, which is what pdf-lib does too.
|
|
123
|
+
/** @type {(family: string, variant: number, font: any) => Face} */
|
|
124
|
+
let embedded = (family, variant, font) => {
|
|
125
|
+
let ref = { family, variant, embedded: true };
|
|
126
|
+
let scale = 1000 / font.unitsPerEm;
|
|
127
|
+
/** @type {(text: string) => any[]} */
|
|
128
|
+
let glyphs = (text) => font.layout(text).glyphs;
|
|
129
|
+
let ascender = (font.ascent || font.bbox.maxY) * scale;
|
|
130
|
+
return {
|
|
131
|
+
ref,
|
|
132
|
+
widthOfTextAtSize: (text, size) => {
|
|
133
|
+
let total = 0;
|
|
134
|
+
for (let glyph of glyphs(text)) total += glyph.advanceWidth * scale;
|
|
135
|
+
return (total * size) / 1000;
|
|
136
|
+
},
|
|
137
|
+
heightAtSize: (size) => (ascender * size) / 1000,
|
|
138
|
+
getCharacterSet: () => font.characterSet,
|
|
139
|
+
};
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
// The base-14 metrics are immutable, so the twelve AFM parses happen once per
|
|
143
|
+
// process rather than once per render; each render still gets its own record,
|
|
144
|
+
// since a host's families are added to it.
|
|
145
|
+
/** @type {Record<string, Face[]> | null} */
|
|
146
|
+
let base = null;
|
|
147
|
+
/** @type {() => Record<string, Face[]>} */
|
|
148
|
+
let baseFamilies = () =>
|
|
149
|
+
(base ??= Object.fromEntries(
|
|
150
|
+
Object.entries(BASE).map(([family, names]) => [
|
|
151
|
+
family,
|
|
152
|
+
names.map((name, variant) => standard(family, variant, name)),
|
|
153
|
+
]),
|
|
154
|
+
));
|
|
155
|
+
|
|
156
|
+
// A render's own copy, so adding a host's families cannot reach the memo.
|
|
157
|
+
/** @type {() => Record<string, Face[]>} */
|
|
158
|
+
let loadBase = () => ({ ...baseFamilies() });
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* The base-14 sans regular, whatever a host mapped `sans` to. Reached only by
|
|
162
|
+
* the marking, which is the one thing on a page a host's font mapping must not
|
|
163
|
+
* reach — see `stamp` in canvas.js.
|
|
164
|
+
*/
|
|
165
|
+
/** @type {() => Face} */
|
|
166
|
+
export let baseSans = () => baseFamilies().sans[0];
|
|
167
|
+
|
|
168
|
+
// Host-supplied TrueType families need a font parser, which is an optional
|
|
169
|
+
// peer: base-14 reports install nothing extra. Loaded only when `fonts` is
|
|
170
|
+
// passed, and a missing package is reported as what it is.
|
|
171
|
+
/** @type {() => Promise<any>} */
|
|
172
|
+
let useFontkit = async () => {
|
|
173
|
+
try {
|
|
174
|
+
return (await import("@pdf-lib/fontkit")).default;
|
|
175
|
+
} catch {
|
|
176
|
+
throw Error("options.fonts: install @pdf-lib/fontkit to measure TrueType families");
|
|
177
|
+
}
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
/** @type {(name: string, def: any) => string} */
|
|
181
|
+
let asFamily = (name, def) => {
|
|
182
|
+
let path = "options.fonts." + name;
|
|
183
|
+
if (!def || typeof def !== "object")
|
|
184
|
+
throw Error(path + ": expected { regular, bold?, italic?, boldItalic? }");
|
|
185
|
+
if (def.regular == null) throw Error(path + ".regular: required");
|
|
186
|
+
return path;
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
/** The bytes a host handed over, whichever of the two shapes it used. */
|
|
190
|
+
/** @type {(source: any) => Uint8Array} */
|
|
191
|
+
export let bytesOf = (source) => (source instanceof Uint8Array ? source : new Uint8Array(source));
|
|
192
|
+
|
|
193
|
+
/** @type {(fontkit: any, src: any, path: string, variant: string) => any} */
|
|
194
|
+
let parseOne = (fontkit, src, path, variant) => {
|
|
195
|
+
try {
|
|
196
|
+
return fontkit.create(bytesOf(src[variant]));
|
|
197
|
+
} catch (e) {
|
|
198
|
+
throw Error(path + "." + variant + ": " + /** @type {Error} */ (e).message);
|
|
199
|
+
}
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* The four variants of a family in the order `FaceRef.variant` indexes them —
|
|
204
|
+
* `bold + 2 * italic` — which is the one fact every painter's registry has to
|
|
205
|
+
* share with the measurer's.
|
|
206
|
+
*/
|
|
207
|
+
export let VARIANTS = /** @type {const} */ (["regular", "bold", "italic", "boldItalic"]);
|
|
208
|
+
|
|
209
|
+
// Every face built from a host's bytes so far, so a family passed on every
|
|
210
|
+
// render is parsed once. The identity rule is `paint.js`'s `FACES`, to the
|
|
211
|
+
// letter — change one and change the other, or the measurer and the painter
|
|
212
|
+
// disagree about what a face is:
|
|
213
|
+
//
|
|
214
|
+
// • keyed on the host's own source object, never on `bytesOf(source)`, since
|
|
215
|
+
// a host may hand over an `ArrayBuffer` and `bytesOf` allocates a fresh
|
|
216
|
+
// view every call, so a bytes-keyed entry would never be found twice;
|
|
217
|
+
// • the inner key carries family and variant, because a `Face` closes over
|
|
218
|
+
// its `ref`: one file aliased across two families, or given as both a
|
|
219
|
+
// family's regular and its bold, is two faces. A variant falling back to
|
|
220
|
+
// the family's regular is keyed here too, under the regular's source — so
|
|
221
|
+
// all four faces of a `regular`-only family are stable across renders, and
|
|
222
|
+
// `CHARSETS` below keeps that font's cmap rather than rebuilding the set
|
|
223
|
+
// every render.
|
|
224
|
+
//
|
|
225
|
+
// The cache is only as good as the host's object stability, which is what
|
|
226
|
+
// `LayoutOptions.fonts` asks for. No promise caching, unlike `paint.js`:
|
|
227
|
+
// parsing is synchronous, so the first caller past the `useFontkit` await
|
|
228
|
+
// runs a whole family to completion before another resumes.
|
|
229
|
+
/** @type {WeakMap<object, Map<string, Face>>} */
|
|
230
|
+
let FACES = new WeakMap();
|
|
231
|
+
|
|
232
|
+
/** @type {(source: object) => Map<string, Face>} */
|
|
233
|
+
let registryOf = (source) => {
|
|
234
|
+
let registry = FACES.get(source);
|
|
235
|
+
if (!registry) FACES.set(source, (registry = new Map()));
|
|
236
|
+
return registry;
|
|
237
|
+
};
|
|
238
|
+
|
|
239
|
+
/** @type {(source: any, key: string, make: () => Face) => Face} */
|
|
240
|
+
let remembered = (source, key, make) => {
|
|
241
|
+
// A source that is not an object cannot be a `WeakMap` key. `asFamily` only
|
|
242
|
+
// proves `regular` is present, so a host may still hand over a path string
|
|
243
|
+
// or a number; those go straight to `make`, which reports them as the
|
|
244
|
+
// located `options.fonts.<name>.<variant>` mistake they are rather than as
|
|
245
|
+
// a raw "invalid value used as weak map key".
|
|
246
|
+
if (!source || typeof source !== "object") return make();
|
|
247
|
+
let registry = registryOf(source);
|
|
248
|
+
let face = registry.get(key);
|
|
249
|
+
// The entry is written after `make`, never before: a parse that throws is a
|
|
250
|
+
// host mistake that fails the render, so there is no hot path to protect and
|
|
251
|
+
// no rejection to reason about on a later one.
|
|
252
|
+
if (!face) registry.set(key, (face = make()));
|
|
253
|
+
return face;
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
// Missing variants fall back to the family's regular, so a face is always
|
|
257
|
+
// resolvable once the family is named.
|
|
258
|
+
/** @type {(fontkit: any, name: string, def: any) => Face[]} */
|
|
259
|
+
let familyOf = (fontkit, name, def) => {
|
|
260
|
+
let path = asFamily(name, def);
|
|
261
|
+
let family = name.toLowerCase();
|
|
262
|
+
/** @type {Face[]} */
|
|
263
|
+
let faces = [];
|
|
264
|
+
for (let [variant, key] of VARIANTS.entries())
|
|
265
|
+
faces.push(
|
|
266
|
+
remembered(def[key] ?? def.regular, family + "/" + variant, () =>
|
|
267
|
+
def[key] == null
|
|
268
|
+
? { ...faces[0], ref: { ...faces[0].ref, variant } }
|
|
269
|
+
: embedded(family, variant, parseOne(fontkit, def, path, key)),
|
|
270
|
+
),
|
|
271
|
+
);
|
|
272
|
+
return faces;
|
|
273
|
+
};
|
|
274
|
+
|
|
275
|
+
/** @type {(custom: any, families: Record<string, Face[]>) => Promise<void>} */
|
|
276
|
+
let loadCustom = async (custom, families) => {
|
|
277
|
+
if (!(custom && Object.keys(custom).length)) return;
|
|
278
|
+
let fontkit = await useFontkit();
|
|
279
|
+
for (let [name, def] of Object.entries(custom))
|
|
280
|
+
families[name.toLowerCase()] = familyOf(fontkit, name, def);
|
|
281
|
+
};
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Load every face a layout can reach and return the registry `face()` reads.
|
|
285
|
+
*
|
|
286
|
+
* @param {any} custom `options.fonts`, or null.
|
|
287
|
+
* @returns {Promise<Fonts>}
|
|
288
|
+
*/
|
|
289
|
+
export async function loadFonts(custom) {
|
|
290
|
+
let families = loadBase();
|
|
291
|
+
await loadCustom(custom, families);
|
|
292
|
+
return { families };
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Check the shape of a host's font mapping without loading a parser: the
|
|
297
|
+
* factory call is where a malformed option is reported.
|
|
298
|
+
*
|
|
299
|
+
* @param {any} custom `options.fonts`, or null.
|
|
300
|
+
*/
|
|
301
|
+
export let checkFonts = (custom) => {
|
|
302
|
+
if (custom == null) return;
|
|
303
|
+
if (typeof custom !== "object") throw Error("options.fonts: expected a record of families");
|
|
304
|
+
for (let [name, def] of Object.entries(custom)) asFamily(name, def);
|
|
305
|
+
};
|
|
306
|
+
|
|
307
|
+
// What a declared `family` normalises to before it is looked up: lower case,
|
|
308
|
+
// and nothing at all when the value is not a name. Exported because the report
|
|
309
|
+
// default is normalised once per render rather than once per cell, and two
|
|
310
|
+
// spellings of this would drift.
|
|
311
|
+
/** @type {(style: any) => string} */
|
|
312
|
+
export let familyName = (style) =>
|
|
313
|
+
typeof style.family === "string" ? style.family.toLowerCase() : "";
|
|
314
|
+
|
|
315
|
+
// Own-key lookup: the family name is render data, so `constructor` must
|
|
316
|
+
// fall back to sans rather than resolve an inherited member.
|
|
317
|
+
/** @type {(fonts: Fonts, name: string | null) => Face[]} */
|
|
318
|
+
let variantsOf = (fonts, name) =>
|
|
319
|
+
name && Object.hasOwn(fonts.families, name) ? fonts.families[name] : fonts.families.sans;
|
|
320
|
+
|
|
321
|
+
/** @type {(style: any) => number} */
|
|
322
|
+
let faceIndex = (style) => (style.bold ? 1 : 0) | (style.italic ? 2 : 0);
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* Resolve a style block to one face.
|
|
326
|
+
*
|
|
327
|
+
* `fallback` is the report default's family, which the canvas carries for the
|
|
328
|
+
* whole render (docs/adr/0033) — so text declaring no family is set in the
|
|
329
|
+
* document's.
|
|
330
|
+
*
|
|
331
|
+
* The fallback turns on whether a family was *declared*, never on whether the
|
|
332
|
+
* declared one resolves: a literal `family` is a non-empty string by the time
|
|
333
|
+
* the engine passes it, but an `=` expression is resolved at render and
|
|
334
|
+
* reaches here unchecked, so an item can declare `""`, a number or `null`.
|
|
335
|
+
* Those are the item's own declaration and fall to sans like any other name
|
|
336
|
+
* this layout cannot resolve (SCHEMA.md) — the document's family is for text
|
|
337
|
+
* that declared none. Own-key, because the block is render data.
|
|
338
|
+
*
|
|
339
|
+
* @type {(fonts: Fonts, style: any, fallback: string | null) => Face}
|
|
340
|
+
*/
|
|
341
|
+
export function face(fonts, style, fallback) {
|
|
342
|
+
let declared = Object.hasOwn(style, "family");
|
|
343
|
+
return variantsOf(fonts, declared ? familyName(style) : fallback)[faceIndex(style)];
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// The distance from a line's top to its baseline: the face's ascender.
|
|
347
|
+
/** @type {(font: Face, size: number) => number} */
|
|
348
|
+
export let ascOf = (font, size) => font.heightAtSize(size);
|
|
349
|
+
|
|
350
|
+
// Text measurement, against the face's own metrics.
|
|
351
|
+
/** @type {(font: Face, text: string, size: number) => number} */
|
|
352
|
+
export let width = (font, text, size) => font.widthOfTextAtSize(text, size);
|
|
353
|
+
|
|
354
|
+
// Printable ASCII, which every text face encodes and which almost all report
|
|
355
|
+
// text is. Asking the cmap instead costs a set walk per atom.
|
|
356
|
+
let ASCII = /^[\x20-\x7E]*$/;
|
|
357
|
+
|
|
358
|
+
// A face's character set is its cmap: WinAnsi code points on the base-14,
|
|
359
|
+
// fontkit's characterSet on an embedded TrueType. A face with no cmap (the
|
|
360
|
+
// layout suite's measuring doubles) does not substitute: width is its whole
|
|
361
|
+
// contract.
|
|
362
|
+
let CHARSETS = new WeakMap();
|
|
363
|
+
let OPEN = { has: () => true };
|
|
364
|
+
|
|
365
|
+
/** @typedef {{ has: (code: number) => boolean }} Cmap */
|
|
366
|
+
|
|
367
|
+
/** @type {(font: any) => Cmap} */
|
|
368
|
+
let charsetOf = (font) => {
|
|
369
|
+
let set = CHARSETS.get(font);
|
|
370
|
+
if (set) return set;
|
|
371
|
+
set = typeof font.getCharacterSet === "function" ? new Set(font.getCharacterSet()) : OPEN;
|
|
372
|
+
CHARSETS.set(font, set);
|
|
373
|
+
return set;
|
|
374
|
+
};
|
|
375
|
+
|
|
376
|
+
/**
|
|
377
|
+
* Replace characters a face cannot draw with `?`.
|
|
378
|
+
*
|
|
379
|
+
* Cell text is untrusted data, so one stray character must not fail the
|
|
380
|
+
* render: SCHEMA.md promises `?` for any face — WinAnsi on the base-14, the
|
|
381
|
+
* cmap on an embedded TrueType. A face that also lacks `?` omits the
|
|
382
|
+
* character rather than drawing .notdef.
|
|
383
|
+
*
|
|
384
|
+
* The ASCII guard carries the common case; only text with a character
|
|
385
|
+
* outside it pays for the cmap walk. Printable ASCII is a subset of WinAnsi
|
|
386
|
+
* and of every text face this layout measures.
|
|
387
|
+
*
|
|
388
|
+
* @type {(font: any, text: string) => string}
|
|
389
|
+
*/
|
|
390
|
+
export let printable = (font, text) => {
|
|
391
|
+
if (!text || ASCII.test(text) || encodable(font, text)) return text;
|
|
392
|
+
return replaceUndrawable(charsetOf(font), text);
|
|
393
|
+
};
|
|
394
|
+
|
|
395
|
+
/** @type {(set: Cmap, text: string) => string} */
|
|
396
|
+
let replaceUndrawable = (set, text) => {
|
|
397
|
+
let sub = set.has(0x3f) ? "?" : "";
|
|
398
|
+
// Encodedness is a per-code-point question, so that is the unit to ask it
|
|
399
|
+
// in — the grapheme clusters the rule protects are undrawable anyway.
|
|
400
|
+
return codePoints(text)
|
|
401
|
+
.map((char) => (set.has(char.codePointAt(0) ?? -1) ? char : sub))
|
|
402
|
+
.join("");
|
|
403
|
+
};
|
|
404
|
+
|
|
405
|
+
/** @type {(font: any, text: string) => boolean} */
|
|
406
|
+
let encodable = (font, text) => {
|
|
407
|
+
let set = charsetOf(font);
|
|
408
|
+
return codePoints(text).every((char) => set.has(char.codePointAt(0) ?? -1));
|
|
409
|
+
};
|
package/lib/image.js
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
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 the layout 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
|
+
};
|
package/lib/index.d.ts
ADDED
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
import type { Target } from "quario";
|
|
2
|
+
|
|
3
|
+
/** Page geometry, in PostScript points. Host configuration, never schema. */
|
|
4
|
+
export interface LayoutPage {
|
|
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
|
+
}
|
|
10
|
+
|
|
11
|
+
/** One TrueType family; missing variants fall back to `regular`. */
|
|
12
|
+
export interface LayoutFontFamily {
|
|
13
|
+
regular: Uint8Array | ArrayBuffer;
|
|
14
|
+
bold?: Uint8Array | ArrayBuffer;
|
|
15
|
+
italic?: Uint8Array | ArrayBuffer;
|
|
16
|
+
boldItalic?: Uint8Array | ArrayBuffer;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** The font mapping: a declared `family` name to the bytes it measures against. */
|
|
20
|
+
export type LayoutFonts = Record<string, LayoutFontFamily>;
|
|
21
|
+
|
|
22
|
+
/** Host controls, taken and validated at the factory call. */
|
|
23
|
+
export interface LayoutOptions {
|
|
24
|
+
page?: LayoutPage;
|
|
25
|
+
/**
|
|
26
|
+
* TrueType families to measure against, selected from styles by
|
|
27
|
+
* `family: '<name>'`. Pass the same record to `pdf({ fonts })` and to
|
|
28
|
+
* `paint()`, so preview and document break their lines in the same places
|
|
29
|
+
* and draw in the same faces. Requires the optional peer `@pdf-lib/fontkit`;
|
|
30
|
+
* the base-14 families need nothing extra.
|
|
31
|
+
*
|
|
32
|
+
* Hold the record and the buffers in it across renders rather than building
|
|
33
|
+
* them anew each time: they are the identity a face is remembered by, so a
|
|
34
|
+
* host that hands over a fresh buffer parses the file again to measure
|
|
35
|
+
* against it, and registers a fresh face to draw it with.
|
|
36
|
+
*/
|
|
37
|
+
fonts?: LayoutFonts;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** A colour on the list: three channels in `0..1`. */
|
|
41
|
+
export interface Color {
|
|
42
|
+
r: number;
|
|
43
|
+
g: number;
|
|
44
|
+
b: number;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Which face a run is set in: the family it resolved to (`sans`, `serif`,
|
|
49
|
+
* `mono`, or a lower-cased name from the font mapping), the variant index
|
|
50
|
+
* `bold + 2 * italic`, and whether the face is bytes the host supplied.
|
|
51
|
+
*
|
|
52
|
+
* `embedded` is not derivable from the family name — a host may map its own
|
|
53
|
+
* family onto `sans`, `serif` or `mono` — and it decides how a painter draws
|
|
54
|
+
* the run. See `TextOp.advances`.
|
|
55
|
+
*/
|
|
56
|
+
export interface FaceRef {
|
|
57
|
+
family: string;
|
|
58
|
+
variant: number;
|
|
59
|
+
embedded: boolean;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** A filled rectangle. */
|
|
63
|
+
export interface RectOp {
|
|
64
|
+
kind: "rect";
|
|
65
|
+
x: number;
|
|
66
|
+
y: number;
|
|
67
|
+
w: number;
|
|
68
|
+
h: number;
|
|
69
|
+
color: Color;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** A stroked segment: a border side, a rule, a text decoration. */
|
|
73
|
+
export interface LineOp {
|
|
74
|
+
kind: "line";
|
|
75
|
+
x1: number;
|
|
76
|
+
y1: number;
|
|
77
|
+
x2: number;
|
|
78
|
+
y2: number;
|
|
79
|
+
width: number;
|
|
80
|
+
color: Color;
|
|
81
|
+
dash: number[] | null;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** An image, placed. `bytes` is the array the source expression yielded. */
|
|
85
|
+
export interface ImageOp {
|
|
86
|
+
kind: "image";
|
|
87
|
+
bytes: Uint8Array;
|
|
88
|
+
format: "png" | "jpeg";
|
|
89
|
+
x: number;
|
|
90
|
+
y: number;
|
|
91
|
+
w: number;
|
|
92
|
+
h: number;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* One run of text: `y` is the line's top and `asc` how far below it the
|
|
97
|
+
* baseline sits, and `text` is the string to draw.
|
|
98
|
+
*
|
|
99
|
+
* `advances` rides only where a painter can use it — one advance in points per
|
|
100
|
+
* code point of `text`, present exactly when `font.embedded` is false. A
|
|
101
|
+
* base-14 family is drawn with them, character by character, so the face
|
|
102
|
+
* standing in for it on screen cannot drift across the run. A host's own
|
|
103
|
+
* family carries none: the painter has the same bytes the layout measured, so
|
|
104
|
+
* it draws the run whole and its own shaping is the document's.
|
|
105
|
+
*/
|
|
106
|
+
export interface TextOp {
|
|
107
|
+
kind: "text";
|
|
108
|
+
x: number;
|
|
109
|
+
y: number;
|
|
110
|
+
w: number;
|
|
111
|
+
h: number;
|
|
112
|
+
asc: number;
|
|
113
|
+
size: number;
|
|
114
|
+
font: FaceRef;
|
|
115
|
+
color: Color;
|
|
116
|
+
text: string;
|
|
117
|
+
advances?: number[];
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* The unlicensed marking (LICENSE section 6), one per page, drawn last:
|
|
122
|
+
* `x`/`y` start its baseline and `angle` turns it counter-clockwise, in
|
|
123
|
+
* degrees.
|
|
124
|
+
*/
|
|
125
|
+
export interface MarkOp {
|
|
126
|
+
kind: "mark";
|
|
127
|
+
x: number;
|
|
128
|
+
y: number;
|
|
129
|
+
angle: number;
|
|
130
|
+
size: number;
|
|
131
|
+
font: FaceRef;
|
|
132
|
+
text: string;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export type Op = RectOp | LineOp | ImageOp | TextOp | MarkOp;
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* The rectangle one schema node was drawn in, named by its path. One node
|
|
139
|
+
* draws many boxes — a column's cell on every row, a header on every page it
|
|
140
|
+
* repeats on — and every one of them means that node.
|
|
141
|
+
*/
|
|
142
|
+
export interface Box {
|
|
143
|
+
path: string;
|
|
144
|
+
x: number;
|
|
145
|
+
y: number;
|
|
146
|
+
w: number;
|
|
147
|
+
h: number;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* One page of the list. Coordinates are points from the page's top-left
|
|
152
|
+
* corner, `y` descending; `number` and `total` are what `page.number` and
|
|
153
|
+
* `page.total` read on it.
|
|
154
|
+
*/
|
|
155
|
+
export interface Page {
|
|
156
|
+
number: number;
|
|
157
|
+
total: number;
|
|
158
|
+
width: number;
|
|
159
|
+
height: number;
|
|
160
|
+
ops: Op[];
|
|
161
|
+
boxes: Box[];
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** An outline entry: a group instance, and where its content begins. */
|
|
165
|
+
export interface Mark {
|
|
166
|
+
title: string;
|
|
167
|
+
depth: number;
|
|
168
|
+
page: number;
|
|
169
|
+
x: number;
|
|
170
|
+
y: number;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/** The display list: every page of the document, laid out. */
|
|
174
|
+
export interface Layout {
|
|
175
|
+
width: number;
|
|
176
|
+
height: number;
|
|
177
|
+
pages: Page[];
|
|
178
|
+
/** The group tree, in document order — the PDF target's bookmarks. */
|
|
179
|
+
marks: Mark[];
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* The layout target:
|
|
184
|
+
* `quario().report(schema).render(layout({ page }), data)` resolves the
|
|
185
|
+
* display list the PDF target writes and the viewer paints
|
|
186
|
+
* (docs/adr/0039).
|
|
187
|
+
*/
|
|
188
|
+
export function layout(options?: LayoutOptions): Target<"layout", Promise<Layout>>;
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* CSS pixels per point at 100%: 96 dpi over PostScript's 72. Every surface
|
|
192
|
+
* that shows a page shows it at this size; a zoom is a factor on top of it.
|
|
193
|
+
*/
|
|
194
|
+
export const PX_PER_POINT: number;
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Paint one page onto a Canvas 2D context: a white page, then every op in
|
|
198
|
+
* order. `scale` is device pixels per point. `fonts` is the same record
|
|
199
|
+
* given to `layout()`, so a TrueType family draws in its own face.
|
|
200
|
+
*/
|
|
201
|
+
export function paint(
|
|
202
|
+
ctx: CanvasRenderingContext2D,
|
|
203
|
+
page: Page,
|
|
204
|
+
options?: { scale?: number; fonts?: LayoutFonts },
|
|
205
|
+
): Promise<void>;
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* The schema node drawn at a point on a page — the smallest box containing
|
|
209
|
+
* it — or `null` where nothing was drawn. Points from the top-left corner.
|
|
210
|
+
*/
|
|
211
|
+
export function hit(page: Page, x: number, y: number): Box | null;
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* The page box a `page` option describes, validated: width, height and
|
|
215
|
+
* margin in points. `at` prefixes a failure with the option's name.
|
|
216
|
+
*/
|
|
217
|
+
export function pageBox(
|
|
218
|
+
page?: LayoutPage,
|
|
219
|
+
at?: string,
|
|
220
|
+
): { width: number; height: number; margin: number };
|