@quario/pdf 0.1.0 → 0.3.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 +93 -0
- package/README.md +6 -5
- package/lib/balance.js +1 -1
- package/lib/box.js +111 -0
- package/lib/canvas.js +38 -24
- package/lib/fonts.js +71 -29
- package/lib/image.js +2 -7
- package/lib/index.d.ts +0 -2
- package/lib/index.js +60 -62
- package/lib/layout.js +445 -140
- package/lib/outline.js +18 -22
- package/lib/style.js +25 -12
- package/lib/text.js +39 -22
- package/package.json +4 -4
package/lib/outline.js
CHANGED
|
@@ -49,26 +49,6 @@ let linkKids = (entry, refs, kids, n) => {
|
|
|
49
49
|
entry.Count = n;
|
|
50
50
|
};
|
|
51
51
|
|
|
52
|
-
/** @type {(ctx: any, refs: any[], root: any, parent: number[], marks: any[], pageRefs: any[], i: number, mark: any) => void} */
|
|
53
|
-
let writeEntry = (ctx, refs, root, parent, marks, pageRefs, i, mark) => {
|
|
54
|
-
let siblings = kidsOf(parent, parent[i]);
|
|
55
|
-
let index = siblings.indexOf(i);
|
|
56
|
-
let kids = kidsOf(parent, i);
|
|
57
|
-
let target = pageRefs[Math.min(mark.page, pageRefs.length - 1)];
|
|
58
|
-
/** @type {Record<string, any>} */
|
|
59
|
-
let entry = {
|
|
60
|
-
Title: PDFHexString.fromText(mark.title),
|
|
61
|
-
Parent: parentRef(parent, i, root, refs),
|
|
62
|
-
// `/XYZ <x> <y> null`: scroll to the mark's line, keep the zoom. The
|
|
63
|
-
// left edge is the mark's own column, so two instances sharing a y in
|
|
64
|
-
// two page columns are two distinct destinations.
|
|
65
|
-
Dest: [target, PDFName.of("XYZ"), PDFNumber.of(mark.x), PDFNumber.of(mark.y), null],
|
|
66
|
-
};
|
|
67
|
-
linkSiblings(entry, refs, siblings, index);
|
|
68
|
-
linkKids(entry, refs, kids, descendants(marks, i));
|
|
69
|
-
ctx.assign(refs[i], ctx.obj(entry));
|
|
70
|
-
};
|
|
71
|
-
|
|
72
52
|
/**
|
|
73
53
|
* Attach an outline built from `marks` to the document.
|
|
74
54
|
*
|
|
@@ -82,8 +62,24 @@ export function outline(doc, marks, pageRefs) {
|
|
|
82
62
|
let parent = parents(marks);
|
|
83
63
|
let refs = marks.map(() => ctx.nextRef());
|
|
84
64
|
let root = ctx.nextRef();
|
|
85
|
-
for (let [i, mark] of marks.entries())
|
|
86
|
-
|
|
65
|
+
for (let [i, mark] of marks.entries()) {
|
|
66
|
+
let siblings = kidsOf(parent, parent[i]);
|
|
67
|
+
let index = siblings.indexOf(i);
|
|
68
|
+
let kids = kidsOf(parent, i);
|
|
69
|
+
let target = pageRefs[Math.min(mark.page, pageRefs.length - 1)];
|
|
70
|
+
/** @type {Record<string, any>} */
|
|
71
|
+
let entry = {
|
|
72
|
+
Title: PDFHexString.fromText(mark.title),
|
|
73
|
+
Parent: parentRef(parent, i, root, refs),
|
|
74
|
+
// `/XYZ <x> <y> null`: scroll to the mark's line, keep the zoom. The
|
|
75
|
+
// left edge is the mark's own column, so two instances sharing a y in
|
|
76
|
+
// two page columns are two distinct destinations.
|
|
77
|
+
Dest: [target, PDFName.of("XYZ"), PDFNumber.of(mark.x), PDFNumber.of(mark.y), null],
|
|
78
|
+
};
|
|
79
|
+
linkSiblings(entry, refs, siblings, index);
|
|
80
|
+
linkKids(entry, refs, kids, descendants(marks, i));
|
|
81
|
+
ctx.assign(refs[i], ctx.obj(entry));
|
|
82
|
+
}
|
|
87
83
|
|
|
88
84
|
let top = kidsOf(parent, -1);
|
|
89
85
|
ctx.assign(
|
package/lib/style.js
CHANGED
|
@@ -4,13 +4,12 @@
|
|
|
4
4
|
* render: a value of the wrong shape contributes nothing rather than reaching
|
|
5
5
|
* the page.
|
|
6
6
|
*
|
|
7
|
-
* `
|
|
8
|
-
*
|
|
7
|
+
* `HEX` is restated per target on purpose: the coercion is each target's own
|
|
8
|
+
* edge, never shared engine code. Finiteness is not one of those coercions —
|
|
9
|
+
* `Number.isFinite` never coerces, so this target asks it directly.
|
|
9
10
|
*/
|
|
10
11
|
import { rgb } from "pdf-lib";
|
|
11
12
|
|
|
12
|
-
/** @type {(value: any) => boolean} */
|
|
13
|
-
let finite = (value) => typeof value === "number" && Number.isFinite(value);
|
|
14
13
|
let HEX = /^#([0-9a-f]{3}|[0-9a-f]{6})$/i;
|
|
15
14
|
|
|
16
15
|
// Leading as a fraction of the font size, cell padding, the gap between a
|
|
@@ -51,17 +50,20 @@ let col = (value) => {
|
|
|
51
50
|
};
|
|
52
51
|
|
|
53
52
|
/** @type {(style: any, base: number) => number} */
|
|
54
|
-
let sizeOf = (style, base) => (
|
|
53
|
+
let sizeOf = (style, base) => (Number.isFinite(style.size) && style.size > 0 ? style.size : base);
|
|
55
54
|
|
|
56
|
-
// Style blocks layer outward-in: row under cell.
|
|
55
|
+
// Style blocks layer outward-in: row under cell, split under slot. Those are
|
|
56
|
+
// the two innermost of the four layers; the outer two reach a node without
|
|
57
|
+
// being merged into it -- the band-role default through `roled` below, and the
|
|
58
|
+
// report default through the canvas, as `layout.js`'s `adoptDefault` explains.
|
|
57
59
|
/** @type {(under: any, over: any) => any} */
|
|
58
60
|
let merge = (under, over) => (under ? (over ? { ...under, ...over } : under) : over || {});
|
|
59
61
|
|
|
60
|
-
// Band-role omakase defaults — the
|
|
61
|
-
// the author's own
|
|
62
|
-
// roles carry one; the XLSX target carries the same two,
|
|
63
|
-
// `packages/xlsx/lib/index.js`, and they move together
|
|
64
|
-
// pair for both). A target can only import public engine helpers, so there is
|
|
62
|
+
// Band-role omakase defaults — the third of those four layers, over the report
|
|
63
|
+
// default and under the author's own, which therefore always wins. Only the
|
|
64
|
+
// headline roles carry one; the XLSX target carries the same two,
|
|
65
|
+
// byte-identical, in `packages/xlsx/lib/index.js`, and they move together
|
|
66
|
+
// (SCHEMA.md states the pair for both). A target can only import public engine helpers, so there is
|
|
65
67
|
// nowhere to share this from and the two copies are synced by hand.
|
|
66
68
|
//
|
|
67
69
|
// `@quario/html` deliberately carries none of this, nor the leading, padding
|
|
@@ -81,4 +83,15 @@ let roled = (event) =>
|
|
|
81
83
|
? { ...event, style: merge(ROLES[event.role], event.style) }
|
|
82
84
|
: event;
|
|
83
85
|
|
|
84
|
-
|
|
86
|
+
// Whether a resolved style asks for either text decoration -- one reading for
|
|
87
|
+
// the wrapper that stamps it on a line and the canvas that draws it.
|
|
88
|
+
/** @type {(style: any) => boolean} */
|
|
89
|
+
let dressed = (style) => !!(style && (style.underline || style.strikethrough));
|
|
90
|
+
|
|
91
|
+
// Whether a resolved style asks for capitals. This target has no
|
|
92
|
+
// text-transform to defer to, so the reading is here beside the rest of the
|
|
93
|
+
// vocabulary and `text.js` applies it before measuring.
|
|
94
|
+
/** @type {(style: any) => boolean} */
|
|
95
|
+
let upper = (style) => !!(style && style.uppercase);
|
|
96
|
+
|
|
97
|
+
export { BAND, BLACK, GUTTER, LEAD, PADX, PADY, col, dressed, merge, roled, shift, sizeOf, upper };
|
package/lib/text.js
CHANGED
|
@@ -3,13 +3,19 @@
|
|
|
3
3
|
* their resolved typography, and a greedy breaker turns those into lines. Pure
|
|
4
4
|
* measurement against the font registry — nothing here touches the page.
|
|
5
5
|
*/
|
|
6
|
+
import { display, format } from "quario";
|
|
6
7
|
import { ascOf, face, printable, width } from "./fonts.js";
|
|
7
|
-
import { LEAD, col, sizeOf } from "./style.js";
|
|
8
|
+
import { LEAD, col, dressed, sizeOf, upper } from "./style.js";
|
|
8
9
|
|
|
9
|
-
// What measuring needs and no more: the embedded faces to measure against
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
|
|
10
|
+
// What measuring needs and no more: the embedded faces to measure against, and
|
|
11
|
+
// the base size and family a style falls back to — the report default's two
|
|
12
|
+
// declarations, which the canvas carries for the whole render. A `Canvas`
|
|
13
|
+
// satisfies it, so callers pass theirs straight in — but nothing here can
|
|
14
|
+
// touch a page.
|
|
15
|
+
/**
|
|
16
|
+
* @typedef {{ fonts: import('./fonts.js').Fonts, base: number,
|
|
17
|
+
* family: string | null, locale?: string, currency?: string, timeZone?: string }} Metrics
|
|
18
|
+
*/
|
|
13
19
|
|
|
14
20
|
/**
|
|
15
21
|
* @typedef {{ text: string, font: any, size: number, color: any,
|
|
@@ -21,13 +27,21 @@ import { LEAD, col, sizeOf } from "./style.js";
|
|
|
21
27
|
* underline?: boolean, strikethrough?: boolean }} Line
|
|
22
28
|
*/
|
|
23
29
|
|
|
24
|
-
/**
|
|
25
|
-
* @typedef {{ cur: Atom[], w: number, lines: Line[], base: number,
|
|
26
|
-
* fonts: import('./fonts.js').Fonts }} Wrap
|
|
27
|
-
*/
|
|
30
|
+
/** @typedef {{ cur: Atom[], w: number, lines: Line[], base: number }} Wrap */
|
|
28
31
|
|
|
29
|
-
/** @type {(token: any) => string} */
|
|
30
|
-
let rawOf = (token) =>
|
|
32
|
+
/** @type {(token: any, style: any, metrics: Metrics) => string} */
|
|
33
|
+
let rawOf = (token, style, metrics) => {
|
|
34
|
+
if ("literal" in token) return token.literal;
|
|
35
|
+
return format(token.value, style?.format, metrics) ?? display(token.value);
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
// This target has no text-transform to defer to, so `uppercase` is applied to
|
|
39
|
+
// the string before it is measured -- the widths have to be the widths of what
|
|
40
|
+
// is actually drawn. `toUpperCase` rather than `toLocaleUpperCase`: this
|
|
41
|
+
// target's output is byte-reproducible, so the host's locale must not reach
|
|
42
|
+
// the glyphs.
|
|
43
|
+
/** @type {(text: string, style: any) => string} */
|
|
44
|
+
let cased = (text, style) => (upper(style) ? text.toUpperCase() : text);
|
|
31
45
|
|
|
32
46
|
/** @type {(font: any, line: string) => string[]} */
|
|
33
47
|
let partsOf = (font, line) => printable(font, line).split(/( +)/).filter(Boolean);
|
|
@@ -48,20 +62,23 @@ let pushLine = (out, font, size, color, i, line) => {
|
|
|
48
62
|
let look = (metrics, style) => {
|
|
49
63
|
let resolved = style || {};
|
|
50
64
|
return {
|
|
51
|
-
font: face(metrics.fonts, resolved),
|
|
65
|
+
font: face(metrics.fonts, resolved, metrics.family),
|
|
52
66
|
size: sizeOf(resolved, metrics.base),
|
|
53
67
|
color: col(resolved.color),
|
|
54
68
|
};
|
|
55
69
|
};
|
|
56
70
|
|
|
57
71
|
// Flatten a cell's tokens to word/space atoms carrying the cell's resolved
|
|
58
|
-
// typography — one face, size and colour for the whole cell.
|
|
72
|
+
// typography — one face, size and colour for the whole cell. CR, LF, and
|
|
73
|
+
// CRLF are one hard break each (SCHEMA.md, Cell values).
|
|
59
74
|
/** @type {(metrics: Metrics, tokens: any[], style: any) => Atom[]} */
|
|
60
75
|
let atoms = (metrics, tokens, style) => {
|
|
61
76
|
let out = /** @type {Atom[]} */ ([]);
|
|
62
77
|
let { font, size, color } = look(metrics, style);
|
|
63
78
|
for (let token of tokens)
|
|
64
|
-
for (let [i, line] of rawOf(token)
|
|
79
|
+
for (let [i, line] of cased(rawOf(token, style, metrics), style)
|
|
80
|
+
.split(/\r\n|\r|\n/)
|
|
81
|
+
.entries())
|
|
65
82
|
pushLine(out, font, size, color, i, line);
|
|
66
83
|
return out;
|
|
67
84
|
};
|
|
@@ -103,7 +120,9 @@ let growPiece = (run, atom) => {
|
|
|
103
120
|
/** @type {(state: Wrap, run: Run) => void} */
|
|
104
121
|
let finish = (state, run) => {
|
|
105
122
|
if (!run.size) run.size = state.base;
|
|
106
|
-
|
|
123
|
+
// No `asc` fallback: a run with no atoms is a blank line, and an ascender
|
|
124
|
+
// only places pieces. `drawLine` loops over none and `decorateLine` skips a
|
|
125
|
+
// line with no width, so a measured one would be a face lookup nothing reads.
|
|
107
126
|
state.lines.push({
|
|
108
127
|
pieces: run.pieces,
|
|
109
128
|
w: run.w,
|
|
@@ -207,11 +226,12 @@ let placeAtom = (state, atom, avail) => {
|
|
|
207
226
|
|
|
208
227
|
// Greedy wrap against `avail`: spaces never start a line, an over-wide word
|
|
209
228
|
// breaks by character, hard breaks always break. A cell's atoms share one
|
|
210
|
-
// typography, so a line's atoms merge into a single draw piece.
|
|
211
|
-
|
|
212
|
-
|
|
229
|
+
// typography, so a line's atoms merge into a single draw piece. `size` is the
|
|
230
|
+
// empty-run height — a blank item, a hard-break hole.
|
|
231
|
+
/** @type {(metrics: Metrics, list: Atom[], avail: number, size: number) => Line[]} */
|
|
232
|
+
let wrap = (metrics, list, avail, size) => {
|
|
213
233
|
/** @type {Wrap} */
|
|
214
|
-
let state = { cur: [], w: 0, lines: [], base:
|
|
234
|
+
let state = { cur: [], w: 0, lines: [], base: size };
|
|
215
235
|
for (let atom of list) placeAtom(state, atom, avail);
|
|
216
236
|
emit(state);
|
|
217
237
|
return state.lines;
|
|
@@ -225,9 +245,6 @@ let stamp = (line, underline, strikethrough) => {
|
|
|
225
245
|
if (strikethrough) line.strikethrough = true;
|
|
226
246
|
};
|
|
227
247
|
|
|
228
|
-
/** @type {(style: any) => boolean} */
|
|
229
|
-
let dressed = (style) => !!(style && (style.underline || style.strikethrough));
|
|
230
|
-
|
|
231
248
|
/** @type {(lines: Line[], style: any) => Line[]} */
|
|
232
249
|
let dress = (lines, style) => {
|
|
233
250
|
if (!dressed(style)) return lines;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@quario/pdf",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "The browserless, paginated PDF render target for quario — in the makings, not yet released",
|
|
5
5
|
"homepage": "https://getquario.com",
|
|
6
6
|
"license": "SEE LICENSE IN LICENSE",
|
|
@@ -42,13 +42,13 @@
|
|
|
42
42
|
"@arethetypeswrong/cli": "^0.18.3",
|
|
43
43
|
"@pdf-lib/fontkit": "^1.1.1",
|
|
44
44
|
"@size-limit/preset-small-lib": "^13.0.3",
|
|
45
|
-
"quario": "^0.
|
|
45
|
+
"quario": "^0.3.0",
|
|
46
46
|
"size-limit": "^13.0.3",
|
|
47
47
|
"typescript": "^7.0.2"
|
|
48
48
|
},
|
|
49
49
|
"peerDependencies": {
|
|
50
50
|
"@pdf-lib/fontkit": "^1.1.1",
|
|
51
|
-
"quario": "^0.
|
|
51
|
+
"quario": "^0.3.0"
|
|
52
52
|
},
|
|
53
53
|
"peerDependenciesMeta": {
|
|
54
54
|
"@pdf-lib/fontkit": {
|
|
@@ -62,7 +62,7 @@
|
|
|
62
62
|
"quario",
|
|
63
63
|
"pdf-lib"
|
|
64
64
|
],
|
|
65
|
-
"limit": "
|
|
65
|
+
"limit": "10.5 kB"
|
|
66
66
|
}
|
|
67
67
|
],
|
|
68
68
|
"engines": {
|