@quario/pdf 0.2.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 +96 -0
- package/README.md +6 -4
- package/lib/embed.js +111 -0
- package/lib/index.d.ts +5 -14
- package/lib/index.js +35 -132
- package/lib/painter.js +205 -0
- package/package.json +6 -4
- package/lib/balance.js +0 -90
- package/lib/canvas.js +0 -376
- package/lib/fonts.js +0 -180
- package/lib/image.js +0 -58
- package/lib/layout.js +0 -1663
- package/lib/style.js +0 -106
- package/lib/text.js +0 -247
package/lib/canvas.js
DELETED
|
@@ -1,376 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The drawing surface. Every mark on a page goes through here, so nothing above
|
|
3
|
-
* this module talks to pdf-lib page primitives.
|
|
4
|
-
*
|
|
5
|
-
* Two adapters satisfy one interface. `drawing` puts marks on a document;
|
|
6
|
-
* `measuring` moves the cursor and marks nothing, which is how `probe` reserves
|
|
7
|
-
* a band's height without emitting it. Measuring is therefore a choice of
|
|
8
|
-
* adapter rather than a mode every primitive has to remember to check — and
|
|
9
|
-
* since a measuring canvas is built from seven numbers and a font registry,
|
|
10
|
-
* there is no parameter through which a document could reach it. Pagination is
|
|
11
|
-
* the one thing measuring must never do: a page turn would reset the cursor
|
|
12
|
-
* mid-measure and return a silently wrong height, so `newPage` throws there.
|
|
13
|
-
*/
|
|
14
|
-
import { degrees, drawImage, drawText, rgb } from "pdf-lib";
|
|
15
|
-
import { BLACK, dressed, shift } from "./style.js";
|
|
16
|
-
|
|
17
|
-
// The operator builder wants explicit rotation and skew; report text has none.
|
|
18
|
-
let NO_TURN = degrees(0);
|
|
19
|
-
|
|
20
|
-
/** @typedef {import('./text.js').Line} Line */
|
|
21
|
-
/** @typedef {import('./text.js').Metrics} Metrics */
|
|
22
|
-
/** @typedef {import('pdf-lib').PDFPage} PDFPage */
|
|
23
|
-
/** @typedef {import('pdf-lib').PDFFont} PDFFont */
|
|
24
|
-
/** @typedef {import('pdf-lib').PDFName} PDFName */
|
|
25
|
-
|
|
26
|
-
// The page box and the content box. Settled at `report-start` and fixed
|
|
27
|
-
// thereafter: the geometry is the factory's and never moves, while `base` and
|
|
28
|
-
// `style` take the report default one event into the render, before anything is
|
|
29
|
-
// measured. The page bands are measured against the page box and take their
|
|
30
|
-
// height off `top`/`bottom` through `adopt` below, while the first page is
|
|
31
|
-
// still untouched.
|
|
32
|
-
/**
|
|
33
|
-
* `style` is the report default — the outermost style layer, resolved once per
|
|
34
|
-
* render from `report-start` and carried here so a measuring canvas built off
|
|
35
|
-
* this frame reads the same one the drawing canvas does. Null when the report
|
|
36
|
-
* declares none.
|
|
37
|
-
*
|
|
38
|
-
* @typedef {{ width: number, height: number, margin: number, base: number,
|
|
39
|
-
* content: number, top: number, bottom: number, style: any }} Frame
|
|
40
|
-
*/
|
|
41
|
-
|
|
42
|
-
// A frame from the page box and the base size: how `content`/`top`/`bottom`
|
|
43
|
-
// fall out of a page and a margin is derived here, once, so no caller and no
|
|
44
|
-
// suite has to restate it and drift from what a real render uses.
|
|
45
|
-
/** @type {(width: number, height: number, margin: number, base: number, style?: any) => Frame} */
|
|
46
|
-
let frame = (width, height, margin, base, style = null) => ({
|
|
47
|
-
width,
|
|
48
|
-
height,
|
|
49
|
-
margin,
|
|
50
|
-
base,
|
|
51
|
-
style,
|
|
52
|
-
content: width - 2 * margin,
|
|
53
|
-
top: height - margin,
|
|
54
|
-
bottom: margin,
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
// `y` is the cursor on the open page and `fresh` says nothing has been drawn on
|
|
58
|
-
// it yet, which is what makes a break legal. `count` is how many pages exist.
|
|
59
|
-
/**
|
|
60
|
-
* @typedef {Frame & Metrics & { y: number, fresh: boolean, count: number,
|
|
61
|
-
* newPage: () => void,
|
|
62
|
-
* rect: (color: any, x: number, y: number, w: number, h: number) => void,
|
|
63
|
-
* rule: (x1: number, x2: number, y: number) => void,
|
|
64
|
-
* picture: (bytes: Uint8Array, format: string, x: number, y: number,
|
|
65
|
-
* w: number, h: number) => void,
|
|
66
|
-
* drawLine: (line: Line, x: number, yTop: number, avail: number,
|
|
67
|
-
* align: any) => void }} Canvas
|
|
68
|
-
*/
|
|
69
|
-
|
|
70
|
-
// The page passes on top of the interface: re-visiting finished pages, handing
|
|
71
|
-
// out their refs for the outline, and stamping them. Deliberately not on
|
|
72
|
-
// `Canvas` — the layout may not re-target a page mid-walk, and a measurement
|
|
73
|
-
// has no business doing any of it.
|
|
74
|
-
/**
|
|
75
|
-
* @typedef {Canvas & { select: (i: number) => void, refs: any[],
|
|
76
|
-
* pictures: () => Promise<void>, watermark: (mark: any) => void }} Drawing
|
|
77
|
-
*/
|
|
78
|
-
|
|
79
|
-
// What every canvas starts as, whichever adapter it is: its frame and faces,
|
|
80
|
-
// and a cursor that has drawn nothing yet. Shared because the parity test
|
|
81
|
-
// compares member names, not their values — two hand-written copies of this
|
|
82
|
-
// could drift in what they start from and nothing would notice.
|
|
83
|
-
/**
|
|
84
|
-
* @type {(box: Frame, fonts: import('./fonts.js').Fonts) =>
|
|
85
|
-
* Frame & Metrics & { y: number, fresh: boolean }}
|
|
86
|
-
*/
|
|
87
|
-
let blank = (box, fonts) => ({ ...box, fonts, y: 0, fresh: true });
|
|
88
|
-
|
|
89
|
-
let GREY = rgb(0.5, 0.5, 0.5);
|
|
90
|
-
|
|
91
|
-
// Text decoration in the text colour. Thickness and offset come from the
|
|
92
|
-
// line's ascender (the face metric already measured for baseline placement).
|
|
93
|
-
// Empty lines (no width) draw nothing. Shared by the drawing adapter and the
|
|
94
|
-
// layout recorder so both exercise the same path.
|
|
95
|
-
/** @type {(line: Line) => any} */
|
|
96
|
-
let ink = (line) => (line.pieces[0] && line.pieces[0].color) || BLACK;
|
|
97
|
-
|
|
98
|
-
/** @type {(line: Line) => boolean} */
|
|
99
|
-
let wantsDeco = (line) => !!line.w && dressed(line);
|
|
100
|
-
|
|
101
|
-
/**
|
|
102
|
-
* @type {(stroke: (x1: number, x2: number, y: number, thickness: number, color: any) => void,
|
|
103
|
-
* line: Line, left: number, baseline: number) => void}
|
|
104
|
-
*/
|
|
105
|
-
let decorateLine = (stroke, line, left, baseline) => {
|
|
106
|
-
if (!wantsDeco(line)) return;
|
|
107
|
-
let thickness = Math.max(line.asc / 12, 0.5);
|
|
108
|
-
let right = left + line.w;
|
|
109
|
-
let color = ink(line);
|
|
110
|
-
if (line.underline) stroke(left, right, baseline - line.asc * 0.12, thickness, color);
|
|
111
|
-
if (line.strikethrough) stroke(left, right, baseline + line.asc * 0.35, thickness, color);
|
|
112
|
-
};
|
|
113
|
-
|
|
114
|
-
/**
|
|
115
|
-
* The adapter that draws: a canvas over a real document.
|
|
116
|
-
*
|
|
117
|
-
* @type {(doc: import('pdf-lib').PDFDocument, box: Frame,
|
|
118
|
-
* fonts: import('./fonts.js').Fonts) => Drawing}
|
|
119
|
-
*/
|
|
120
|
-
let drawing = (doc, box, fonts) => {
|
|
121
|
-
/** @type {PDFPage} */
|
|
122
|
-
let page;
|
|
123
|
-
/** @type {PDFPage[]} */
|
|
124
|
-
let pages = [];
|
|
125
|
-
/**
|
|
126
|
-
* The resource name a page refers to a face or an image by, minted once per
|
|
127
|
-
* resource per page.
|
|
128
|
-
*
|
|
129
|
-
* pdf-lib's own page-level state cannot do this: `setFont` mints a fresh
|
|
130
|
-
* random-suffixed name on every call, and `drawText({ font })` calls it twice
|
|
131
|
-
* — once to select the face and once to restore the previous one. A report
|
|
132
|
-
* that alternates bold and regular therefore grows its font dictionary with
|
|
133
|
-
* its text rather than with its faces, and measured 4.8x larger for the same
|
|
134
|
-
* content. `drawImage` on the page has the same shape, so one logo down a
|
|
135
|
-
* thousand rows would put a thousand entries in one XObject dictionary,
|
|
136
|
-
* every one of them pointing at the same stream.
|
|
137
|
-
*
|
|
138
|
-
* Keyed by page and kept for the canvas's whole life, never reset per page,
|
|
139
|
-
* because the page-band pass returns to pages the body already drew on and
|
|
140
|
-
* must reuse what they registered.
|
|
141
|
-
*
|
|
142
|
-
* @type {(mint: (on: PDFPage, resource: any) => PDFName) =>
|
|
143
|
-
* (on: PDFPage, resource: any) => PDFName}
|
|
144
|
-
*/
|
|
145
|
-
let perPage = (mint) => {
|
|
146
|
-
/** @type {WeakMap<PDFPage, Map<any, PDFName>>} */
|
|
147
|
-
let keys = new WeakMap();
|
|
148
|
-
return (on, resource) => {
|
|
149
|
-
let minted = keys.get(on);
|
|
150
|
-
if (!minted) keys.set(on, (minted = new Map()));
|
|
151
|
-
let key = minted.get(resource);
|
|
152
|
-
if (!key) minted.set(resource, (key = mint(on, resource)));
|
|
153
|
-
return key;
|
|
154
|
-
};
|
|
155
|
-
};
|
|
156
|
-
let faceKey = perPage((on, font) => on.node.newFontDictionary(font.name, font.ref));
|
|
157
|
-
let imageKey = perPage((on, image) => on.node.newXObject("Image", image.ref));
|
|
158
|
-
/** @type {(font: PDFFont) => PDFName} */
|
|
159
|
-
let fontKey = (font) => faceKey(page, font);
|
|
160
|
-
|
|
161
|
-
// Open a fresh page and put the cursor at its top.
|
|
162
|
-
let newPage = () => {
|
|
163
|
-
page = doc.addPage([canvas.width, canvas.height]);
|
|
164
|
-
pages.push(page);
|
|
165
|
-
canvas.y = canvas.top;
|
|
166
|
-
canvas.fresh = true;
|
|
167
|
-
};
|
|
168
|
-
|
|
169
|
-
/** @type {Canvas['rect']} */
|
|
170
|
-
let rect = (color, x, y, w, h) => page.drawRectangle({ x, y, width: w, height: h, color });
|
|
171
|
-
|
|
172
|
-
// Where every image goes, recorded as the walk passes it and drawn once the
|
|
173
|
-
// walk is over. Embedding is asynchronous and a walk handler is not, so the
|
|
174
|
-
// placement is what the flow makes and the bytes are turned into a document
|
|
175
|
-
// resource afterwards -- which is also what lets one logo repeated on every
|
|
176
|
-
// page be embedded once and referenced, keyed by the array the source
|
|
177
|
-
// expression yielded.
|
|
178
|
-
/** @type {{ page: PDFPage, bytes: Uint8Array, format: string, x: number,
|
|
179
|
-
* y: number, w: number, h: number }[]} */
|
|
180
|
-
let placed = [];
|
|
181
|
-
// Drawn once, at the end of the render, so nothing clears this: the canvas
|
|
182
|
-
// and the document it marks end together.
|
|
183
|
-
/** @type {Canvas['picture']} */
|
|
184
|
-
let picture = (bytes, format, x, y, w, h) => {
|
|
185
|
-
placed.push({ page, bytes, format, x, y, w, h });
|
|
186
|
-
};
|
|
187
|
-
/** @type {Drawing['pictures']} */
|
|
188
|
-
let pictures = async () => {
|
|
189
|
-
// Keyed by the array the source expression yielded, so one logo reused
|
|
190
|
-
// across pages is embedded once and every placement references it.
|
|
191
|
-
/** @type {Map<Uint8Array, any>} */
|
|
192
|
-
let embedded = new Map();
|
|
193
|
-
for (let spot of placed) {
|
|
194
|
-
let image = embedded.get(spot.bytes);
|
|
195
|
-
if (!image) {
|
|
196
|
-
image = await (spot.format === "png" ? doc.embedPng(spot.bytes) : doc.embedJpg(spot.bytes));
|
|
197
|
-
embedded.set(spot.bytes, image);
|
|
198
|
-
}
|
|
199
|
-
spot.page.pushOperators(
|
|
200
|
-
...drawImage(imageKey(spot.page, image), {
|
|
201
|
-
x: spot.x,
|
|
202
|
-
y: spot.y,
|
|
203
|
-
width: spot.w,
|
|
204
|
-
height: spot.h,
|
|
205
|
-
rotate: NO_TURN,
|
|
206
|
-
xSkew: NO_TURN,
|
|
207
|
-
ySkew: NO_TURN,
|
|
208
|
-
}),
|
|
209
|
-
);
|
|
210
|
-
}
|
|
211
|
-
};
|
|
212
|
-
|
|
213
|
-
/** @type {Canvas['rule']} */
|
|
214
|
-
let rule = (x1, x2, y) =>
|
|
215
|
-
page.drawLine({ start: { x: x1, y }, end: { x: x2, y }, thickness: 0.5, color: BLACK });
|
|
216
|
-
|
|
217
|
-
/** @type {(piece: Line['pieces'][number], x: number, y: number) => void} */
|
|
218
|
-
let writePiece = (piece, x, y) => {
|
|
219
|
-
if (!piece.text) return;
|
|
220
|
-
page.pushOperators(
|
|
221
|
-
...drawText(piece.font.encodeText(piece.text), {
|
|
222
|
-
font: fontKey(piece.font),
|
|
223
|
-
size: piece.size,
|
|
224
|
-
color: piece.color || BLACK,
|
|
225
|
-
x,
|
|
226
|
-
y,
|
|
227
|
-
rotate: NO_TURN,
|
|
228
|
-
xSkew: NO_TURN,
|
|
229
|
-
ySkew: NO_TURN,
|
|
230
|
-
}),
|
|
231
|
-
);
|
|
232
|
-
};
|
|
233
|
-
|
|
234
|
-
/** @type {(left: number, right: number, y: number, thickness: number, color: any) => void} */
|
|
235
|
-
let strokeAt = (left, right, y, thickness, color) =>
|
|
236
|
-
page.drawLine({
|
|
237
|
-
start: { x: left, y },
|
|
238
|
-
end: { x: right, y },
|
|
239
|
-
thickness,
|
|
240
|
-
color,
|
|
241
|
-
});
|
|
242
|
-
|
|
243
|
-
// Draw one wrapped line with its baseline the line's ascender under `yTop`,
|
|
244
|
-
// horizontally placed by `align` within `avail` starting at `x`.
|
|
245
|
-
/** @type {Canvas['drawLine']} */
|
|
246
|
-
let drawLine = (line, x, yTop, avail, align) => {
|
|
247
|
-
let left = x + shift(align, avail - line.w);
|
|
248
|
-
let baseline = yTop - line.asc;
|
|
249
|
-
let cursor = left;
|
|
250
|
-
for (let piece of line.pieces) {
|
|
251
|
-
writePiece(piece, cursor, baseline);
|
|
252
|
-
cursor += piece.w;
|
|
253
|
-
}
|
|
254
|
-
decorateLine(strokeAt, line, left, baseline);
|
|
255
|
-
};
|
|
256
|
-
|
|
257
|
-
// Draw one page's marking through the same operator path as body text, so the
|
|
258
|
-
// face reuses the page's `fontKey` resource instead of growing the dictionary
|
|
259
|
-
// per draw.
|
|
260
|
-
/** @type {Drawing['watermark']} */
|
|
261
|
-
let watermark = (mark) =>
|
|
262
|
-
page.pushOperators(
|
|
263
|
-
...drawText(mark.line, {
|
|
264
|
-
font: fontKey(mark.font),
|
|
265
|
-
size: mark.size,
|
|
266
|
-
color: GREY,
|
|
267
|
-
x: mark.x,
|
|
268
|
-
y: mark.y,
|
|
269
|
-
rotate: mark.rotate,
|
|
270
|
-
xSkew: NO_TURN,
|
|
271
|
-
ySkew: NO_TURN,
|
|
272
|
-
// Opacity needs a named ExtGState resource on the page, and pdf-lib
|
|
273
|
-
// declares the only method that mints one private, with no public
|
|
274
|
-
// equivalent. The cast is deliberate and stays as narrow as the call.
|
|
275
|
-
graphicsState: /** @type {any} */ (page).maybeEmbedGraphicsState({ opacity: 0.15 }),
|
|
276
|
-
}),
|
|
277
|
-
);
|
|
278
|
-
|
|
279
|
-
/** @type {Drawing} */
|
|
280
|
-
let canvas = {
|
|
281
|
-
...blank(box, fonts),
|
|
282
|
-
get count() {
|
|
283
|
-
return pages.length;
|
|
284
|
-
},
|
|
285
|
-
newPage,
|
|
286
|
-
rect,
|
|
287
|
-
rule,
|
|
288
|
-
picture,
|
|
289
|
-
pictures,
|
|
290
|
-
drawLine,
|
|
291
|
-
// Re-open a finished page, for the passes that run over them all.
|
|
292
|
-
select: (i) => {
|
|
293
|
-
page = pages[i];
|
|
294
|
-
},
|
|
295
|
-
get refs() {
|
|
296
|
-
return pages.map((each) => each.ref);
|
|
297
|
-
},
|
|
298
|
-
watermark,
|
|
299
|
-
};
|
|
300
|
-
return canvas;
|
|
301
|
-
};
|
|
302
|
-
|
|
303
|
-
// Nothing at all — what every primitive does when the canvas only measures.
|
|
304
|
-
let MARKS_NOTHING = () => {};
|
|
305
|
-
|
|
306
|
-
/**
|
|
307
|
-
* The adapter that only measures: the same cursor arithmetic with every mark
|
|
308
|
-
* discarded. It holds no document and no page, so a measurement cannot write.
|
|
309
|
-
*
|
|
310
|
-
* @type {(box: Frame, fonts: import('./fonts.js').Fonts) => Canvas}
|
|
311
|
-
*/
|
|
312
|
-
let measuring = (box, fonts) => ({
|
|
313
|
-
...blank(box, fonts),
|
|
314
|
-
count: 0,
|
|
315
|
-
newPage: () => {
|
|
316
|
-
throw new Error("probe reached newPage: the measuring path must not paginate");
|
|
317
|
-
},
|
|
318
|
-
rect: MARKS_NOTHING,
|
|
319
|
-
rule: MARKS_NOTHING,
|
|
320
|
-
picture: MARKS_NOTHING,
|
|
321
|
-
drawLine: MARKS_NOTHING,
|
|
322
|
-
});
|
|
323
|
-
|
|
324
|
-
/**
|
|
325
|
-
* Take on a content box someone else worked out — how much page furniture
|
|
326
|
-
* needs is layout's policy (`reserve` there), while which of a frame's members
|
|
327
|
-
* may move at all is this module's invariant. `top` and `bottom` are the two,
|
|
328
|
-
* and the only two read here: the page box and the base size are fixed for the
|
|
329
|
-
* whole document, so the rest of the frame that arrives is this canvas's own
|
|
330
|
-
* and is left alone. Layout reads the bounds live, so every page the body then
|
|
331
|
-
* flows through sees the narrowed box.
|
|
332
|
-
*
|
|
333
|
-
* Legal only while the render has not committed to a page — the band flow
|
|
334
|
-
* adopts from its `report-start` handler, with its own first page open but
|
|
335
|
-
* empty. `fresh` alone would not say that: a page turn makes it true again on
|
|
336
|
-
* page five, where narrowing would silently mix two geometries in one document.
|
|
337
|
-
*
|
|
338
|
-
* @param {Canvas} canvas The canvas to narrow.
|
|
339
|
-
* @param {Frame} box The frame it takes its content box from.
|
|
340
|
-
*/
|
|
341
|
-
let adopt = (canvas, box) => {
|
|
342
|
-
if (canvas.count > 1 || !canvas.fresh) throw Error("adopt: the content box is fixed");
|
|
343
|
-
canvas.top = box.top;
|
|
344
|
-
canvas.bottom = box.bottom;
|
|
345
|
-
// The open page has drawn nothing, so its cursor moves with the box — the
|
|
346
|
-
// same statement `newPage` makes, and nothing on the page can be lost by it.
|
|
347
|
-
canvas.y = canvas.top;
|
|
348
|
-
};
|
|
349
|
-
|
|
350
|
-
// The unlicensed-output marking (LICENSE section 6): one translucent line
|
|
351
|
-
// drawn corner-to-corner across the finished page — over the content, not
|
|
352
|
-
// under it, so no filled table header or background rectangle can cover it.
|
|
353
|
-
// The wording comes from the engine, on `report-start`; this target owns the
|
|
354
|
-
// geometry, which depends only on the page size and never on key contents,
|
|
355
|
-
// so document bytes stay deterministic in both licensed states.
|
|
356
|
-
|
|
357
|
-
// The per-render half of the marking, computed once: page geometry, the face
|
|
358
|
-
// and the wording are all fixed for a whole render, so the trig and the
|
|
359
|
-
// glyph-width walk never repeat per page.
|
|
360
|
-
/** @type {(canvas: Canvas, text: string) => any} */
|
|
361
|
-
let stamp = (canvas, text) => {
|
|
362
|
-
let font = canvas.fonts.families.sans[0];
|
|
363
|
-
let angle = Math.atan2(canvas.height, canvas.width);
|
|
364
|
-
// Scale the line to three quarters of the page diagonal, whatever the size.
|
|
365
|
-
let span = Math.hypot(canvas.width, canvas.height) * 0.75;
|
|
366
|
-
return {
|
|
367
|
-
font,
|
|
368
|
-
line: font.encodeText(text),
|
|
369
|
-
size: (54 * span) / font.widthOfTextAtSize(text, 54),
|
|
370
|
-
x: canvas.width / 2 - (span / 2) * Math.cos(angle),
|
|
371
|
-
y: canvas.height / 2 - (span / 2) * Math.sin(angle),
|
|
372
|
-
rotate: degrees((angle * 180) / Math.PI),
|
|
373
|
-
};
|
|
374
|
-
};
|
|
375
|
-
|
|
376
|
-
export { adopt, decorateLine, drawing, frame, measuring, stamp };
|
package/lib/fonts.js
DELETED
|
@@ -1,180 +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
|
-
/** @type {(style: any) => string} */
|
|
119
|
-
let familyName = (style) => (typeof style.family === "string" ? style.family.toLowerCase() : "");
|
|
120
|
-
|
|
121
|
-
// Own-key lookup: the family name is render data, so `constructor` must
|
|
122
|
-
// fall back to sans rather than resolve an inherited member.
|
|
123
|
-
/** @type {(fonts: Fonts, name: string) => any[]} */
|
|
124
|
-
let variantsOf = (fonts, name) =>
|
|
125
|
-
Object.hasOwn(fonts.families, name) ? fonts.families[name] : fonts.families.sans;
|
|
126
|
-
|
|
127
|
-
/** @type {(style: any) => number} */
|
|
128
|
-
let faceIndex = (style) => (style.bold ? 1 : 0) | (style.italic ? 2 : 0);
|
|
129
|
-
|
|
130
|
-
// Resolve a style block to one embedded face.
|
|
131
|
-
/** @type {(fonts: Fonts, style: any) => any} */
|
|
132
|
-
export function face(fonts, style) {
|
|
133
|
-
return variantsOf(fonts, familyName(style))[faceIndex(style)];
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
// The distance from a line's top to its baseline: pdf-lib's ascender, which
|
|
137
|
-
// matches the per-family fractions the hand-rolled writer used.
|
|
138
|
-
/** @type {(font: any, size: number) => number} */
|
|
139
|
-
export let ascOf = (font, size) => font.heightAtSize(size, { descender: false });
|
|
140
|
-
|
|
141
|
-
// Text measurement. pdf-lib encodes on draw, so nothing here needs to know
|
|
142
|
-
// about WinAnsi or glyph ids.
|
|
143
|
-
/** @type {(font: any, text: string, size: number) => number} */
|
|
144
|
-
export let width = (font, text, size) => font.widthOfTextAtSize(text, size);
|
|
145
|
-
|
|
146
|
-
// Printable ASCII, which every face encodes and which almost all report text
|
|
147
|
-
// is. Asking the face instead costs a full measurement per atom.
|
|
148
|
-
let ASCII = /^[\x20-\x7E]*$/;
|
|
149
|
-
|
|
150
|
-
/**
|
|
151
|
-
* Replace characters a face cannot encode with `?`.
|
|
152
|
-
*
|
|
153
|
-
* The base-14 faces are WinAnsi and pdf-lib refuses anything outside it. Cell
|
|
154
|
-
* text is untrusted data, so one stray character must not fail the render:
|
|
155
|
-
* SCHEMA.md promises `?`, and full Unicode through `options.fonts`.
|
|
156
|
-
*
|
|
157
|
-
* There is no way to ask a pdf-lib face what it can encode without measuring
|
|
158
|
-
* against it, so the ASCII guard carries the common case and only text with a
|
|
159
|
-
* character outside it pays for the answer. WinAnsi is a superset of printable
|
|
160
|
-
* ASCII, so the guard never lets an unencodable character through.
|
|
161
|
-
*
|
|
162
|
-
* @type {(font: any, text: string) => string}
|
|
163
|
-
*/
|
|
164
|
-
export let printable = (font, text) => {
|
|
165
|
-
if (!text || ASCII.test(text) || encodable(font, text)) return text;
|
|
166
|
-
// WinAnsi encodability is a per-code-point question, so that is the unit to
|
|
167
|
-
// ask it in — the grapheme clusters the rule protects are unencodable anyway.
|
|
168
|
-
// oxlint-disable-next-line typescript/no-misused-spread
|
|
169
|
-
return [...text].map((char) => (encodable(font, char) ? char : "?")).join("");
|
|
170
|
-
};
|
|
171
|
-
|
|
172
|
-
/** @type {(font: any, text: string) => boolean} */
|
|
173
|
-
let encodable = (font, text) => {
|
|
174
|
-
try {
|
|
175
|
-
font.widthOfTextAtSize(text, 1);
|
|
176
|
-
return true;
|
|
177
|
-
} catch {
|
|
178
|
-
return false;
|
|
179
|
-
}
|
|
180
|
-
};
|
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
|
-
};
|