@jasy/pdf 1.0.0-alpha.9 → 1.0.0-beta.1
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/README.md +27 -9
- package/dist/api/color.js +69 -0
- package/dist/api/content.d.ts +40 -1
- package/dist/api/content.js +83 -0
- package/dist/api/descriptor.js +6 -1
- package/dist/api/dimension.d.ts +3 -2
- package/dist/api/font-url.js +3 -2
- package/dist/api/structure.d.ts +7 -0
- package/dist/api/structure.js +38 -3
- package/dist/api/text.d.ts +23 -2
- package/dist/api/text.js +9 -0
- package/dist/canvas/painter.d.ts +102 -0
- package/dist/canvas/painter.js +183 -0
- package/dist/elements/canvas-element.d.ts +38 -0
- package/dist/elements/canvas-element.js +47 -0
- package/dist/elements/container-element.d.ts +2 -0
- package/dist/elements/container-element.js +6 -0
- package/dist/elements/index.d.ts +2 -0
- package/dist/elements/index.js +2 -0
- package/dist/elements/layout/expanded-element.d.ts +2 -0
- package/dist/elements/layout/expanded-element.js +4 -0
- package/dist/elements/layout/keep-together-element.d.ts +2 -0
- package/dist/elements/layout/keep-together-element.js +4 -0
- package/dist/elements/layout/padding-element.d.ts +5 -0
- package/dist/elements/layout/padding-element.js +8 -0
- package/dist/elements/layout/struct-group.d.ts +2 -0
- package/dist/elements/layout/struct-group.js +4 -0
- package/dist/elements/pdf-element.d.ts +19 -3
- package/dist/elements/pdf-element.js +22 -4
- package/dist/elements/rectangle-element.d.ts +2 -0
- package/dist/elements/rectangle-element.js +6 -0
- package/dist/elements/row-element.d.ts +2 -0
- package/dist/elements/row-element.js +6 -0
- package/dist/elements/svg-element.d.ts +41 -0
- package/dist/elements/svg-element.js +93 -0
- package/dist/elements/text-element.d.ts +33 -1
- package/dist/elements/text-element.js +80 -4
- package/dist/ir/display-list.d.ts +44 -5
- package/dist/layout/min-intrinsic.d.ts +29 -0
- package/dist/layout/min-intrinsic.js +37 -0
- package/dist/renderer/anchor-renderer.js +1 -1
- package/dist/renderer/bookmark-renderer.js +1 -1
- package/dist/renderer/canvas-renderer.d.ts +14 -0
- package/dist/renderer/canvas-renderer.js +36 -0
- package/dist/renderer/container-renderer.js +1 -3
- package/dist/renderer/default-text-style-renderer.js +1 -1
- package/dist/renderer/deferred-renderer.js +1 -1
- package/dist/renderer/expanded-renderer.js +1 -1
- package/dist/renderer/index.d.ts +2 -0
- package/dist/renderer/index.js +2 -0
- package/dist/renderer/keep-together-renderer.js +1 -1
- package/dist/renderer/link-renderer.js +1 -1
- package/dist/renderer/padding-renderer.js +1 -1
- package/dist/renderer/page-builder-renderer.js +1 -1
- package/dist/renderer/page-renderer.js +1 -3
- package/dist/renderer/pdf-backend.d.ts +15 -4
- package/dist/renderer/pdf-backend.js +125 -35
- package/dist/renderer/pdf-renderer.js +5 -1
- package/dist/renderer/positioned-renderer.js +1 -1
- package/dist/renderer/rectangle-renderer.js +1 -3
- package/dist/renderer/repeating-header-renderer.js +1 -2
- package/dist/renderer/rotated-renderer.js +1 -1
- package/dist/renderer/row-renderer.js +1 -3
- package/dist/renderer/struct-group-renderer.js +1 -1
- package/dist/renderer/svg-renderer.d.ts +11 -0
- package/dist/renderer/svg-renderer.js +27 -0
- package/dist/renderer/text-renderer.js +30 -19
- package/dist/svg/css.d.ts +38 -0
- package/dist/svg/css.js +115 -0
- package/dist/svg/errors.d.ts +24 -0
- package/dist/svg/errors.js +30 -0
- package/dist/svg/gradients.d.ts +34 -0
- package/dist/svg/gradients.js +156 -0
- package/dist/svg/index.d.ts +22 -0
- package/dist/svg/index.js +331 -0
- package/dist/svg/shapes.d.ts +6 -0
- package/dist/svg/shapes.js +177 -0
- package/dist/svg/style.d.ts +61 -0
- package/dist/svg/style.js +144 -0
- package/dist/svg/transform.d.ts +13 -0
- package/dist/svg/transform.js +68 -0
- package/dist/svg/units.d.ts +11 -0
- package/dist/svg/units.js +73 -0
- package/dist/svg/xml.d.ts +24 -0
- package/dist/svg/xml.js +158 -0
- package/dist/text/advance.d.ts +3 -0
- package/dist/text/advance.js +4 -3
- package/dist/text/glyph-coverage.d.ts +15 -0
- package/dist/text/glyph-coverage.js +77 -0
- package/dist/text/line-breaker.d.ts +8 -0
- package/dist/text/line-breaker.js +146 -70
- package/dist/text/shape.d.ts +3 -3
- package/dist/text/shape.js +34 -10
- package/dist/text/text-style.d.ts +13 -0
- package/dist/text/text-style.js +6 -0
- package/dist/text/whitespace.d.ts +4 -0
- package/dist/text/whitespace.js +43 -0
- package/dist/text/word-splitting.d.ts +42 -0
- package/dist/text/word-splitting.js +72 -0
- package/dist/utils/flex-layout.js +67 -18
- package/dist/utils/font-metrics.d.ts +9 -3
- package/dist/utils/pdf-object-manager.d.ts +25 -5
- package/dist/utils/pdf-object-manager.js +82 -18
- package/dist/utils/renderer-registry.d.ts +13 -1
- package/dist/utils/renderer-registry.js +22 -1
- package/dist/utils/woff2.d.ts +6 -0
- package/dist/utils/woff2.js +651 -0
- package/dist/vector/path.d.ts +33 -0
- package/dist/vector/path.js +81 -0
- package/package.json +5 -2
|
@@ -0,0 +1,651 @@
|
|
|
1
|
+
import { WoffError } from "./woff.js";
|
|
2
|
+
/**
|
|
3
|
+
* WOFF2 -> plain sfnt bytes.
|
|
4
|
+
*
|
|
5
|
+
* WOFF2 is not "WOFF1 with a better compressor". Two things make it a different piece of work:
|
|
6
|
+
*
|
|
7
|
+
* 1. **Brotli**, which `fflate` does not do and which is not a thing to hand-roll - the decoder needs
|
|
8
|
+
* Brotli's built-in 120 KB static dictionary. So it is bought, like `fflate` for deflate, and
|
|
9
|
+
* LAZILY imported so a document without a WOFF2 never loads it (the bargain `jimp` already makes).
|
|
10
|
+
* fontkit - react-pdf's font engine - depends on the same package.
|
|
11
|
+
* 2. **The `glyf` table is REWRITTEN**, not merely compressed: points are triplet-encoded across five
|
|
12
|
+
* separate streams, composites and instructions in two more. Rebuilding `glyf` + `loca` from that
|
|
13
|
+
* is most of this file, and it is what nothing downstream should ever have to know about.
|
|
14
|
+
*
|
|
15
|
+
* Everything here funnels into `woff2ToSfnt`, which `TTFParser` calls at the one point every font
|
|
16
|
+
* path meets - so a file, raw bytes, a URL and a browser upload all get it for free.
|
|
17
|
+
*/
|
|
18
|
+
const SIGNATURE = 0x774f4632; // 'wOF2'
|
|
19
|
+
const HEADER = 48;
|
|
20
|
+
/** The 63 tags WOFF2 can name by index instead of spelling out. Order is normative. */
|
|
21
|
+
const KNOWN_TAGS = [
|
|
22
|
+
"cmap",
|
|
23
|
+
"head",
|
|
24
|
+
"hhea",
|
|
25
|
+
"hmtx",
|
|
26
|
+
"maxp",
|
|
27
|
+
"name",
|
|
28
|
+
"OS/2",
|
|
29
|
+
"post",
|
|
30
|
+
"cvt ",
|
|
31
|
+
"fpgm",
|
|
32
|
+
"glyf",
|
|
33
|
+
"loca",
|
|
34
|
+
"prep",
|
|
35
|
+
"CFF ",
|
|
36
|
+
"VORG",
|
|
37
|
+
"EBDT",
|
|
38
|
+
"EBLC",
|
|
39
|
+
"gasp",
|
|
40
|
+
"hdmx",
|
|
41
|
+
"kern",
|
|
42
|
+
"LTSH",
|
|
43
|
+
"PCLT",
|
|
44
|
+
"VDMX",
|
|
45
|
+
"vhea",
|
|
46
|
+
"vmtx",
|
|
47
|
+
"BASE",
|
|
48
|
+
"GDEF",
|
|
49
|
+
"GPOS",
|
|
50
|
+
"GSUB",
|
|
51
|
+
"EBSC",
|
|
52
|
+
"JSTF",
|
|
53
|
+
"MATH",
|
|
54
|
+
"CBDT",
|
|
55
|
+
"CBLC",
|
|
56
|
+
"COLR",
|
|
57
|
+
"CPAL",
|
|
58
|
+
"SVG ",
|
|
59
|
+
"sbix",
|
|
60
|
+
"acnt",
|
|
61
|
+
"avar",
|
|
62
|
+
"bdat",
|
|
63
|
+
"bloc",
|
|
64
|
+
"bsln",
|
|
65
|
+
"cvar",
|
|
66
|
+
"fdsc",
|
|
67
|
+
"feat",
|
|
68
|
+
"fmtx",
|
|
69
|
+
"fvar",
|
|
70
|
+
"gvar",
|
|
71
|
+
"hsty",
|
|
72
|
+
"just",
|
|
73
|
+
"lcar",
|
|
74
|
+
"mort",
|
|
75
|
+
"morx",
|
|
76
|
+
"opbd",
|
|
77
|
+
"prop",
|
|
78
|
+
"trak",
|
|
79
|
+
"Zapf",
|
|
80
|
+
"Silf",
|
|
81
|
+
"Glat",
|
|
82
|
+
"Gloc",
|
|
83
|
+
"Feat",
|
|
84
|
+
"Sill",
|
|
85
|
+
];
|
|
86
|
+
const be16 = (b, at) => (b[at] << 8) | b[at + 1];
|
|
87
|
+
const be32 = (b, at) => ((b[at] << 24) | (b[at + 1] << 16) | (b[at + 2] << 8) | b[at + 3]) >>> 0;
|
|
88
|
+
export const isWoff2 = (bytes) => bytes.length >= HEADER && be32(bytes, 0) === SIGNATURE;
|
|
89
|
+
/** A cursor over one of the sub-streams; every read moves it on. */
|
|
90
|
+
class Reader {
|
|
91
|
+
constructor(bytes) {
|
|
92
|
+
this.bytes = bytes;
|
|
93
|
+
this.offset = 0;
|
|
94
|
+
}
|
|
95
|
+
u8() {
|
|
96
|
+
if (this.offset >= this.bytes.length)
|
|
97
|
+
throw new WoffError("the WOFF2 data ends mid-value");
|
|
98
|
+
return this.bytes[this.offset++];
|
|
99
|
+
}
|
|
100
|
+
u16() {
|
|
101
|
+
return (this.u8() << 8) | this.u8();
|
|
102
|
+
}
|
|
103
|
+
i16() {
|
|
104
|
+
const n = this.u16();
|
|
105
|
+
return n & 0x8000 ? n - 0x10000 : n;
|
|
106
|
+
}
|
|
107
|
+
u32() {
|
|
108
|
+
return ((this.u16() << 16) | this.u16()) >>> 0;
|
|
109
|
+
}
|
|
110
|
+
take(n) {
|
|
111
|
+
if (this.offset + n > this.bytes.length)
|
|
112
|
+
throw new WoffError("the WOFF2 data ends mid-table");
|
|
113
|
+
const out = this.bytes.subarray(this.offset, this.offset + n);
|
|
114
|
+
this.offset += n;
|
|
115
|
+
return out;
|
|
116
|
+
}
|
|
117
|
+
/** Up to five bytes, seven bits each, high bit continues. Used for every length in the directory. */
|
|
118
|
+
base128() {
|
|
119
|
+
let value = 0;
|
|
120
|
+
for (let i = 0; i < 5; i++) {
|
|
121
|
+
const byte = this.u8();
|
|
122
|
+
// A leading zero byte, or a value past 2^32, is malformed rather than merely unusual.
|
|
123
|
+
if (i === 0 && byte === 0x80)
|
|
124
|
+
throw new WoffError("a WOFF2 length has a leading zero");
|
|
125
|
+
if (value > 0x01ffffff)
|
|
126
|
+
throw new WoffError("a WOFF2 length overflows 32 bits");
|
|
127
|
+
value = value * 128 + (byte & 0x7f);
|
|
128
|
+
if ((byte & 0x80) === 0)
|
|
129
|
+
return value;
|
|
130
|
+
}
|
|
131
|
+
throw new WoffError("a WOFF2 length runs past five bytes");
|
|
132
|
+
}
|
|
133
|
+
/** WOFF2's short-count encoding: one byte for 0..252, otherwise a two- or three-byte form. */
|
|
134
|
+
count255() {
|
|
135
|
+
const code = this.u8();
|
|
136
|
+
if (code === 253)
|
|
137
|
+
return this.u16();
|
|
138
|
+
if (code === 254)
|
|
139
|
+
return this.u8() + 253 * 2;
|
|
140
|
+
if (code === 255)
|
|
141
|
+
return this.u8() + 253;
|
|
142
|
+
return code;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
function readDirectory(reader, numTables) {
|
|
146
|
+
const entries = [];
|
|
147
|
+
for (let i = 0; i < numTables; i++) {
|
|
148
|
+
const flags = reader.u8();
|
|
149
|
+
const index = flags & 0x3f;
|
|
150
|
+
const version = (flags >> 6) & 0x03;
|
|
151
|
+
const tag = index === 0x3f
|
|
152
|
+
? String.fromCharCode(reader.u8(), reader.u8(), reader.u8(), reader.u8())
|
|
153
|
+
: (KNOWN_TAGS[index] ?? "");
|
|
154
|
+
if (!tag)
|
|
155
|
+
throw new WoffError(`the WOFF2 table directory names an unknown table (index ${index})`);
|
|
156
|
+
const origLength = reader.base128();
|
|
157
|
+
// The inversion is real and easy to get backwards: for `glyf` and `loca` the NULL transform is
|
|
158
|
+
// version 3, for every other table it is version 0.
|
|
159
|
+
const nullTransform = tag === "glyf" || tag === "loca" ? version === 3 : version === 0;
|
|
160
|
+
const transformed = !nullTransform;
|
|
161
|
+
const transformLength = transformed ? reader.base128() : origLength;
|
|
162
|
+
entries.push({ tag, transformed, origLength, transformLength });
|
|
163
|
+
}
|
|
164
|
+
return entries;
|
|
165
|
+
}
|
|
166
|
+
const TRIPLETS = (() => {
|
|
167
|
+
const out = [];
|
|
168
|
+
const sign = (bit) => (bit ? 1 : -1);
|
|
169
|
+
// 0..9: y only, 8 bits, dy stepping by 256 every second entry, sign alternating.
|
|
170
|
+
for (let i = 0; i < 10; i++) {
|
|
171
|
+
out.push({
|
|
172
|
+
bytes: 1,
|
|
173
|
+
xBits: 0,
|
|
174
|
+
yBits: 8,
|
|
175
|
+
dx: 0,
|
|
176
|
+
dy: (i >> 1) * 256,
|
|
177
|
+
xSign: 0,
|
|
178
|
+
ySign: sign(i & 1),
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
// 10..19: the same, x only.
|
|
182
|
+
for (let i = 0; i < 10; i++) {
|
|
183
|
+
out.push({
|
|
184
|
+
bytes: 1,
|
|
185
|
+
xBits: 8,
|
|
186
|
+
yBits: 0,
|
|
187
|
+
dx: (i >> 1) * 256,
|
|
188
|
+
dy: 0,
|
|
189
|
+
xSign: sign(i & 1),
|
|
190
|
+
ySign: 0,
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
// 20..83: both axes, 4 bits each in one byte - 4 x-bases x 4 y-bases x 4 sign pairs.
|
|
194
|
+
for (let a = 0; a < 4; a++) {
|
|
195
|
+
for (let b = 0; b < 4; b++) {
|
|
196
|
+
for (let c = 0; c < 4; c++) {
|
|
197
|
+
out.push({
|
|
198
|
+
bytes: 1,
|
|
199
|
+
xBits: 4,
|
|
200
|
+
yBits: 4,
|
|
201
|
+
dx: 1 + a * 16,
|
|
202
|
+
dy: 1 + b * 16,
|
|
203
|
+
xSign: sign(c & 1),
|
|
204
|
+
ySign: sign(c & 2),
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
// 84..119: both axes, 8 bits each - 3 x-bases x 3 y-bases x 4 sign pairs.
|
|
210
|
+
for (let a = 0; a < 3; a++) {
|
|
211
|
+
for (let b = 0; b < 3; b++) {
|
|
212
|
+
for (let c = 0; c < 4; c++) {
|
|
213
|
+
out.push({
|
|
214
|
+
bytes: 2,
|
|
215
|
+
xBits: 8,
|
|
216
|
+
yBits: 8,
|
|
217
|
+
dx: 1 + a * 256,
|
|
218
|
+
dy: 1 + b * 256,
|
|
219
|
+
xSign: sign(c & 1),
|
|
220
|
+
ySign: sign(c & 2),
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
// 120..123: 12 bits each, 124..127: 16 bits each. No base, sign pairs only.
|
|
226
|
+
for (const [bits, bytes] of [
|
|
227
|
+
[12, 3],
|
|
228
|
+
[16, 4],
|
|
229
|
+
]) {
|
|
230
|
+
for (let c = 0; c < 4; c++) {
|
|
231
|
+
out.push({
|
|
232
|
+
bytes,
|
|
233
|
+
xBits: bits,
|
|
234
|
+
yBits: bits,
|
|
235
|
+
dx: 0,
|
|
236
|
+
dy: 0,
|
|
237
|
+
xSign: sign(c & 1),
|
|
238
|
+
ySign: sign(c & 2),
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
return out;
|
|
243
|
+
})();
|
|
244
|
+
/** A growable big-endian writer - the reconstructed `glyf` is built into one. */
|
|
245
|
+
class Writer {
|
|
246
|
+
constructor() {
|
|
247
|
+
this.buffer = new Uint8Array(1024);
|
|
248
|
+
this.length = 0;
|
|
249
|
+
}
|
|
250
|
+
room(n) {
|
|
251
|
+
if (this.length + n <= this.buffer.length)
|
|
252
|
+
return;
|
|
253
|
+
let size = this.buffer.length * 2;
|
|
254
|
+
while (size < this.length + n)
|
|
255
|
+
size *= 2;
|
|
256
|
+
const bigger = new Uint8Array(size);
|
|
257
|
+
bigger.set(this.buffer.subarray(0, this.length));
|
|
258
|
+
this.buffer = bigger;
|
|
259
|
+
}
|
|
260
|
+
u8(v) {
|
|
261
|
+
this.room(1);
|
|
262
|
+
this.buffer[this.length++] = v & 0xff;
|
|
263
|
+
}
|
|
264
|
+
u16(v) {
|
|
265
|
+
this.u8(v >> 8);
|
|
266
|
+
this.u8(v);
|
|
267
|
+
}
|
|
268
|
+
i16(v) {
|
|
269
|
+
this.u16(v < 0 ? v + 0x10000 : v);
|
|
270
|
+
}
|
|
271
|
+
u32(v) {
|
|
272
|
+
this.u16(Math.floor(v / 0x10000));
|
|
273
|
+
this.u16(v & 0xffff);
|
|
274
|
+
}
|
|
275
|
+
bytes(b) {
|
|
276
|
+
this.room(b.length);
|
|
277
|
+
this.buffer.set(b, this.length);
|
|
278
|
+
this.length += b.length;
|
|
279
|
+
}
|
|
280
|
+
/** Pads to a four-byte boundary, which every sfnt table offset must sit on. */
|
|
281
|
+
align() {
|
|
282
|
+
while (this.length % 4 !== 0)
|
|
283
|
+
this.u8(0);
|
|
284
|
+
}
|
|
285
|
+
done() {
|
|
286
|
+
return this.buffer.subarray(0, this.length);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
/**
|
|
290
|
+
* Rebuilds `glyf` and `loca` from the transformed table.
|
|
291
|
+
*
|
|
292
|
+
* The transformed form splits one glyph across seven streams: how many contours, how many points per
|
|
293
|
+
* contour, one flag byte per point, the coordinates themselves, composite descriptions, bounding
|
|
294
|
+
* boxes and hinting instructions. Reading it means walking all seven in lockstep.
|
|
295
|
+
*/
|
|
296
|
+
function rebuildOutlines(data) {
|
|
297
|
+
const head = new Reader(data);
|
|
298
|
+
head.u16(); // reserved
|
|
299
|
+
// Bit 0 says a bitmap follows the seven streams, marking the simple glyphs whose contours OVERLAP.
|
|
300
|
+
// It is a rasterizer hint, not geometry - but dropping it would quietly change what the file says.
|
|
301
|
+
const optionFlags = head.u16();
|
|
302
|
+
const numGlyphs = head.u16();
|
|
303
|
+
const indexFormat = head.u16();
|
|
304
|
+
const sizes = [0, 0, 0, 0, 0, 0, 0].map(() => head.u32());
|
|
305
|
+
const [nContourSize, nPointsSize, flagSize, glyphSize, compositeSize, bboxSize, instructionSize] = sizes;
|
|
306
|
+
let at = head.offset;
|
|
307
|
+
const slice = (n) => {
|
|
308
|
+
const r = new Reader(data.subarray(at, at + n));
|
|
309
|
+
at += n;
|
|
310
|
+
return r;
|
|
311
|
+
};
|
|
312
|
+
const nContours = slice(nContourSize);
|
|
313
|
+
const nPoints = slice(nPointsSize);
|
|
314
|
+
const flags = slice(flagSize);
|
|
315
|
+
const glyphs = slice(glyphSize);
|
|
316
|
+
const composites = slice(compositeSize);
|
|
317
|
+
const bboxes = slice(bboxSize);
|
|
318
|
+
const instructions = slice(instructionSize);
|
|
319
|
+
// Whatever is left is the overlap bitmap, so its padding rule does not have to be guessed at.
|
|
320
|
+
const overlapBitmap = optionFlags & 1 ? data.subarray(at) : undefined;
|
|
321
|
+
const overlaps = (i) => overlapBitmap !== undefined && ((overlapBitmap[i >> 3] ?? 0) >> (7 - (i & 7))) % 2 === 1;
|
|
322
|
+
// The bbox stream opens with one bit per glyph: is its bounding box stored, or to be computed? The
|
|
323
|
+
// bitmap is PADDED to a four-byte boundary - reading only ceil(numGlyphs/8) leaves up to three
|
|
324
|
+
// bytes behind and every following bbox is read shifted, which no outline test would ever notice.
|
|
325
|
+
const bitmapBytes = 4 * Math.ceil(numGlyphs / 32);
|
|
326
|
+
const bitmap = bboxes.take(bitmapBytes);
|
|
327
|
+
const hasBbox = (i) => ((bitmap[i >> 3] >> (7 - (i & 7))) & 1) === 1;
|
|
328
|
+
const glyf = new Writer();
|
|
329
|
+
const offsets = [0];
|
|
330
|
+
for (let g = 0; g < numGlyphs; g++) {
|
|
331
|
+
const contours = nContours.i16();
|
|
332
|
+
if (contours === 0) {
|
|
333
|
+
// An empty glyph occupies no bytes at all; `loca` says so by repeating the offset.
|
|
334
|
+
offsets.push(glyf.length);
|
|
335
|
+
continue;
|
|
336
|
+
}
|
|
337
|
+
if (contours < 0) {
|
|
338
|
+
// Composite: copied through verbatim, but its length is only known by walking the flags.
|
|
339
|
+
const start = composites.offset;
|
|
340
|
+
let more = true;
|
|
341
|
+
let haveInstructions = false;
|
|
342
|
+
while (more) {
|
|
343
|
+
const flag = composites.u16();
|
|
344
|
+
composites.u16(); // glyph index
|
|
345
|
+
more = (flag & 0x0020) !== 0;
|
|
346
|
+
haveInstructions || (haveInstructions = (flag & 0x0100) !== 0);
|
|
347
|
+
composites.take(flag & 0x0001 ? 4 : 2); // arguments
|
|
348
|
+
if (flag & 0x0008)
|
|
349
|
+
composites.take(2);
|
|
350
|
+
else if (flag & 0x0040)
|
|
351
|
+
composites.take(4);
|
|
352
|
+
else if (flag & 0x0080)
|
|
353
|
+
composites.take(8);
|
|
354
|
+
}
|
|
355
|
+
const body = composites.bytes.subarray(start, composites.offset);
|
|
356
|
+
const instructionLength = haveInstructions ? glyphs.count255() : 0;
|
|
357
|
+
glyf.i16(-1);
|
|
358
|
+
if (!hasBbox(g)) {
|
|
359
|
+
throw new WoffError(`composite glyph ${g} has no bounding box, which WOFF2 requires`);
|
|
360
|
+
}
|
|
361
|
+
writeBbox(glyf, bboxes, true, null);
|
|
362
|
+
glyf.bytes(body);
|
|
363
|
+
if (haveInstructions) {
|
|
364
|
+
glyf.u16(instructionLength);
|
|
365
|
+
glyf.bytes(instructions.take(instructionLength));
|
|
366
|
+
}
|
|
367
|
+
glyf.align();
|
|
368
|
+
offsets.push(glyf.length);
|
|
369
|
+
continue;
|
|
370
|
+
}
|
|
371
|
+
// Simple glyph: point counts per contour, then one flag and one coordinate pair per point.
|
|
372
|
+
const endPoints = [];
|
|
373
|
+
let total = 0;
|
|
374
|
+
for (let c = 0; c < contours; c++) {
|
|
375
|
+
total += nPoints.count255();
|
|
376
|
+
endPoints.push(total - 1);
|
|
377
|
+
}
|
|
378
|
+
const xs = [];
|
|
379
|
+
const ys = [];
|
|
380
|
+
const onCurve = [];
|
|
381
|
+
let x = 0;
|
|
382
|
+
let y = 0;
|
|
383
|
+
for (let p = 0; p < total; p++) {
|
|
384
|
+
const flag = flags.u8();
|
|
385
|
+
// The high bit is the ON-curve marker; the low seven index the triplet table.
|
|
386
|
+
onCurve.push((flag & 0x80) === 0);
|
|
387
|
+
const t = TRIPLETS[flag & 0x7f];
|
|
388
|
+
const raw = glyphs.take(t.bytes);
|
|
389
|
+
// The bytes are one big-endian number: the x value in the HIGH `xBits`, y in the low `yBits`.
|
|
390
|
+
// Plain arithmetic rather than shifts, because 16+16 bits overflows a signed 32-bit shift.
|
|
391
|
+
let bits = 0;
|
|
392
|
+
for (const byte of raw)
|
|
393
|
+
bits = bits * 256 + byte;
|
|
394
|
+
const yScale = 2 ** t.yBits;
|
|
395
|
+
x += t.xSign * (t.dx + (t.xBits === 0 ? 0 : Math.floor(bits / yScale)));
|
|
396
|
+
y += t.ySign * (t.dy + (t.yBits === 0 ? 0 : bits % yScale));
|
|
397
|
+
xs.push(x);
|
|
398
|
+
ys.push(y);
|
|
399
|
+
}
|
|
400
|
+
const instructionLength = glyphs.count255();
|
|
401
|
+
glyf.i16(contours);
|
|
402
|
+
writeBbox(glyf, bboxes, hasBbox(g), { xs, ys });
|
|
403
|
+
for (const end of endPoints)
|
|
404
|
+
glyf.u16(end);
|
|
405
|
+
glyf.u16(instructionLength);
|
|
406
|
+
glyf.bytes(instructions.take(instructionLength));
|
|
407
|
+
writePoints(glyf, xs, ys, onCurve, overlaps(g));
|
|
408
|
+
glyf.align();
|
|
409
|
+
offsets.push(glyf.length);
|
|
410
|
+
}
|
|
411
|
+
// The format is decided by what we actually produced, not by what the source used. Our glyf can be
|
|
412
|
+
// LARGER than the original - we do not emit the REPEAT flag - so a font that fitted the short form
|
|
413
|
+
// (offsets/2, up to 131070 bytes) may no longer, and writing it short would truncate in silence.
|
|
414
|
+
// `align()` pads to four bytes, so every offset is even and the short form stays legal below that.
|
|
415
|
+
const last = offsets[offsets.length - 1];
|
|
416
|
+
const format = indexFormat === 0 && last <= 0x1fffe ? 0 : 1;
|
|
417
|
+
const table = glyf.done();
|
|
418
|
+
return { glyf: table, loca: writeLoca(offsets, format), indexToLocFormat: format };
|
|
419
|
+
}
|
|
420
|
+
/** The stored bounding box, or the one computed from the points - a simple glyph may omit it. */
|
|
421
|
+
function writeBbox(out, bboxes, stored, points) {
|
|
422
|
+
if (stored) {
|
|
423
|
+
out.bytes(bboxes.take(8));
|
|
424
|
+
return;
|
|
425
|
+
}
|
|
426
|
+
if (!points || points.xs.length === 0) {
|
|
427
|
+
out.i16(0);
|
|
428
|
+
out.i16(0);
|
|
429
|
+
out.i16(0);
|
|
430
|
+
out.i16(0);
|
|
431
|
+
return;
|
|
432
|
+
}
|
|
433
|
+
// A loop, not `Math.min(...xs)`: spreading tens of thousands of arguments blows the call stack,
|
|
434
|
+
// and a glyph may legitimately carry up to 32,767 points.
|
|
435
|
+
let [xMin, yMin, xMax, yMax] = [Infinity, Infinity, -Infinity, -Infinity];
|
|
436
|
+
for (let i = 0; i < points.xs.length; i++) {
|
|
437
|
+
xMin = Math.min(xMin, points.xs[i]);
|
|
438
|
+
xMax = Math.max(xMax, points.xs[i]);
|
|
439
|
+
yMin = Math.min(yMin, points.ys[i]);
|
|
440
|
+
yMax = Math.max(yMax, points.ys[i]);
|
|
441
|
+
}
|
|
442
|
+
out.i16(xMin);
|
|
443
|
+
out.i16(yMin);
|
|
444
|
+
out.i16(xMax);
|
|
445
|
+
out.i16(yMax);
|
|
446
|
+
}
|
|
447
|
+
/** The sfnt point encoding: run-length flags, then x deltas, then y deltas. */
|
|
448
|
+
function writePoints(out, xs, ys, onCurve, overlap = false) {
|
|
449
|
+
const flags = [];
|
|
450
|
+
const dxs = [];
|
|
451
|
+
const dys = [];
|
|
452
|
+
let px = 0;
|
|
453
|
+
let py = 0;
|
|
454
|
+
for (let i = 0; i < xs.length; i++) {
|
|
455
|
+
const dx = xs[i] - px;
|
|
456
|
+
const dy = ys[i] - py;
|
|
457
|
+
px = xs[i];
|
|
458
|
+
py = ys[i];
|
|
459
|
+
let flag = onCurve[i] ? 0x01 : 0x00;
|
|
460
|
+
if (dx === 0)
|
|
461
|
+
flag |= 0x10;
|
|
462
|
+
else if (dx >= -255 && dx <= 255)
|
|
463
|
+
flag |= 0x02 | (dx > 0 ? 0x10 : 0);
|
|
464
|
+
if (dy === 0)
|
|
465
|
+
flag |= 0x20;
|
|
466
|
+
else if (dy >= -255 && dy <= 255)
|
|
467
|
+
flag |= 0x04 | (dy > 0 ? 0x20 : 0);
|
|
468
|
+
flags.push(flag);
|
|
469
|
+
dxs.push(dx);
|
|
470
|
+
dys.push(dy);
|
|
471
|
+
}
|
|
472
|
+
// OVERLAP_SIMPLE rides on the FIRST point of the glyph and nowhere else.
|
|
473
|
+
if (overlap && flags.length > 0)
|
|
474
|
+
flags[0] |= 0x40;
|
|
475
|
+
for (const f of flags)
|
|
476
|
+
out.u8(f);
|
|
477
|
+
for (let i = 0; i < dxs.length; i++) {
|
|
478
|
+
const f = flags[i];
|
|
479
|
+
if (f & 0x02)
|
|
480
|
+
out.u8(Math.abs(dxs[i]));
|
|
481
|
+
else if (!(f & 0x10))
|
|
482
|
+
out.i16(dxs[i]);
|
|
483
|
+
}
|
|
484
|
+
for (let i = 0; i < dys.length; i++) {
|
|
485
|
+
const f = flags[i];
|
|
486
|
+
if (f & 0x04)
|
|
487
|
+
out.u8(Math.abs(dys[i]));
|
|
488
|
+
else if (!(f & 0x20))
|
|
489
|
+
out.i16(dys[i]);
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
function writeLoca(offsets, indexFormat) {
|
|
493
|
+
const out = new Writer();
|
|
494
|
+
for (const offset of offsets) {
|
|
495
|
+
if (indexFormat === 0)
|
|
496
|
+
out.u16(offset / 2);
|
|
497
|
+
else
|
|
498
|
+
out.u32(offset);
|
|
499
|
+
}
|
|
500
|
+
return out.done();
|
|
501
|
+
}
|
|
502
|
+
/** Brotli, bought and loaded only when a WOFF2 actually turns up. */
|
|
503
|
+
async function inflateBrotli(data) {
|
|
504
|
+
const module = (await import("brotli/decompress.js"));
|
|
505
|
+
const decompress = module.default;
|
|
506
|
+
const out = decompress(data);
|
|
507
|
+
if (!out)
|
|
508
|
+
throw new WoffError("the WOFF2 body could not be decompressed");
|
|
509
|
+
return out instanceof Uint8Array ? out : new Uint8Array(out);
|
|
510
|
+
}
|
|
511
|
+
/**
|
|
512
|
+
* The whole container: header, directory, one Brotli stream holding every table back to back, then
|
|
513
|
+
* `glyf`/`loca` put back together and the sfnt reassembled around them.
|
|
514
|
+
*/
|
|
515
|
+
export async function woff2ToSfnt(woff2) {
|
|
516
|
+
if (!isWoff2(woff2))
|
|
517
|
+
throw new WoffError("these bytes are not a WOFF2 container");
|
|
518
|
+
const flavor = be32(woff2, 4);
|
|
519
|
+
const numTables = be16(woff2, 12);
|
|
520
|
+
if (numTables === 0)
|
|
521
|
+
throw new WoffError("the WOFF2 declares no tables");
|
|
522
|
+
const directory = new Reader(woff2.subarray(HEADER));
|
|
523
|
+
const entries = readDirectory(directory, numTables);
|
|
524
|
+
// Its own length, not "everything left": optional metadata and private blocks follow it.
|
|
525
|
+
const totalCompressed = be32(woff2, 20);
|
|
526
|
+
const dataStart = HEADER + directory.offset;
|
|
527
|
+
const compressed = woff2.subarray(dataStart, dataStart + totalCompressed);
|
|
528
|
+
const body = await inflateBrotli(compressed);
|
|
529
|
+
// The tables lie back to back in directory order, each at its TRANSFORMED length.
|
|
530
|
+
const raw = new Map();
|
|
531
|
+
let at = 0;
|
|
532
|
+
for (const entry of entries) {
|
|
533
|
+
if (at + entry.transformLength > body.length) {
|
|
534
|
+
throw new WoffError(`the WOFF2 body is shorter than its table directory claims ("${entry.tag}")`);
|
|
535
|
+
}
|
|
536
|
+
raw.set(entry.tag, body.subarray(at, at + entry.transformLength));
|
|
537
|
+
at += entry.transformLength;
|
|
538
|
+
}
|
|
539
|
+
const tables = new Map();
|
|
540
|
+
let locaFormat;
|
|
541
|
+
for (const entry of entries) {
|
|
542
|
+
const data = raw.get(entry.tag);
|
|
543
|
+
if (entry.tag === "loca")
|
|
544
|
+
continue; // rebuilt with glyf
|
|
545
|
+
if (entry.tag === "glyf" && entry.transformed) {
|
|
546
|
+
const rebuilt = rebuildOutlines(data);
|
|
547
|
+
tables.set("glyf", rebuilt.glyf);
|
|
548
|
+
tables.set("loca", rebuilt.loca);
|
|
549
|
+
locaFormat = rebuilt.indexToLocFormat;
|
|
550
|
+
continue;
|
|
551
|
+
}
|
|
552
|
+
if (entry.transformed) {
|
|
553
|
+
// The only other transform the format defines is the optional one on `hmtx`, which drops the
|
|
554
|
+
// left side bearings. Neither fontTools nor Google Fonts emits it, so it is named rather than
|
|
555
|
+
// built - and anything else here is a file we do not understand at all.
|
|
556
|
+
throw new WoffError(`this WOFF2 transforms "${entry.tag}", which is not supported - re-export without it`);
|
|
557
|
+
}
|
|
558
|
+
tables.set(entry.tag, data);
|
|
559
|
+
}
|
|
560
|
+
if (!tables.has("loca") && entries.some((e) => e.tag === "loca")) {
|
|
561
|
+
tables.set("loca", raw.get("loca"));
|
|
562
|
+
}
|
|
563
|
+
// `head` tells a reader how to read `loca`, so the two must agree - and only we know which form we
|
|
564
|
+
// just wrote. The table is copied first: it is a view into the decompressed buffer, not ours.
|
|
565
|
+
const head = tables.get("head");
|
|
566
|
+
if (locaFormat !== undefined && head) {
|
|
567
|
+
if (head.length < 52)
|
|
568
|
+
throw new WoffError("the WOFF2 head table is too short to be one");
|
|
569
|
+
const patched = head.slice();
|
|
570
|
+
patched[50] = 0;
|
|
571
|
+
patched[51] = locaFormat;
|
|
572
|
+
tables.set("head", patched);
|
|
573
|
+
}
|
|
574
|
+
return assemble(flavor, tables);
|
|
575
|
+
}
|
|
576
|
+
/** Writes a plain sfnt: header, table directory sorted by tag, then the padded table data. */
|
|
577
|
+
function assemble(flavor, tables) {
|
|
578
|
+
const tags = [...tables.keys()].sort();
|
|
579
|
+
const count = tags.length;
|
|
580
|
+
if (count === 0)
|
|
581
|
+
throw new WoffError("the WOFF2 produced no tables at all");
|
|
582
|
+
// `head` is checksummed with its `checkSumAdjustment` field ZEROED - that field is written last and
|
|
583
|
+
// covers the whole file, so it cannot be part of its own table's sum. Zero it before anything is
|
|
584
|
+
// computed, on a copy: the table is a view into the decompressed buffer, not ours to write to.
|
|
585
|
+
const head = tables.get("head");
|
|
586
|
+
if (head && head.length >= 12) {
|
|
587
|
+
const copy = head.slice();
|
|
588
|
+
copy.set([0, 0, 0, 0], 8);
|
|
589
|
+
tables.set("head", copy);
|
|
590
|
+
}
|
|
591
|
+
const entrySelector = Math.floor(Math.log2(count));
|
|
592
|
+
const searchRange = 2 ** entrySelector * 16;
|
|
593
|
+
const out = new Writer();
|
|
594
|
+
out.u16(flavor >>> 16);
|
|
595
|
+
out.u16(flavor & 0xffff);
|
|
596
|
+
out.u16(count);
|
|
597
|
+
out.u16(searchRange);
|
|
598
|
+
out.u16(entrySelector);
|
|
599
|
+
out.u16(count * 16 - searchRange);
|
|
600
|
+
let offset = 12 + count * 16;
|
|
601
|
+
const placed = [];
|
|
602
|
+
for (const tag of tags) {
|
|
603
|
+
const data = tables.get(tag);
|
|
604
|
+
placed.push({ tag, offset, length: data.length });
|
|
605
|
+
offset += Math.ceil(data.length / 4) * 4;
|
|
606
|
+
}
|
|
607
|
+
for (const { tag, offset: at, length } of placed) {
|
|
608
|
+
for (let i = 0; i < 4; i++)
|
|
609
|
+
out.u8(tag.charCodeAt(i));
|
|
610
|
+
// WOFF2 does not carry checksums, so they are recomputed - an sfnt with zeros there is not one.
|
|
611
|
+
const sum = checksum(tables.get(tag));
|
|
612
|
+
out.u16(Math.floor(sum / 0x10000));
|
|
613
|
+
out.u16(sum & 0xffff);
|
|
614
|
+
out.u16(Math.floor(at / 0x10000));
|
|
615
|
+
out.u16(at & 0xffff);
|
|
616
|
+
out.u16(Math.floor(length / 0x10000));
|
|
617
|
+
out.u16(length & 0xffff);
|
|
618
|
+
}
|
|
619
|
+
for (const tag of tags) {
|
|
620
|
+
out.bytes(tables.get(tag));
|
|
621
|
+
out.align();
|
|
622
|
+
}
|
|
623
|
+
const sfnt = out.done();
|
|
624
|
+
// The field depends on the WHOLE file, so it is written last - the magic constant minus the sum of
|
|
625
|
+
// everything, with the field itself already zero.
|
|
626
|
+
const headEntry = placed.find((p) => p.tag === "head");
|
|
627
|
+
if (headEntry && headEntry.length >= 12) {
|
|
628
|
+
const at = headEntry.offset + 8;
|
|
629
|
+
const total = checksum(sfnt);
|
|
630
|
+
const adjustment = (0xb1b0afba - total) >>> 0;
|
|
631
|
+
sfnt.set([
|
|
632
|
+
(adjustment >>> 24) & 0xff,
|
|
633
|
+
(adjustment >>> 16) & 0xff,
|
|
634
|
+
(adjustment >>> 8) & 0xff,
|
|
635
|
+
adjustment & 0xff,
|
|
636
|
+
], at);
|
|
637
|
+
}
|
|
638
|
+
return sfnt;
|
|
639
|
+
}
|
|
640
|
+
/** The sfnt checksum: the table read as big-endian u32s, zero-padded, summed modulo 2^32. */
|
|
641
|
+
function checksum(data) {
|
|
642
|
+
let sum = 0;
|
|
643
|
+
for (let i = 0; i < data.length; i += 4) {
|
|
644
|
+
const word = (data[i] ?? 0) * 0x1000000 +
|
|
645
|
+
(data[i + 1] ?? 0) * 0x10000 +
|
|
646
|
+
(data[i + 2] ?? 0) * 0x100 +
|
|
647
|
+
(data[i + 3] ?? 0);
|
|
648
|
+
sum = (sum + word) % 0x100000000;
|
|
649
|
+
}
|
|
650
|
+
return sum;
|
|
651
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { PathCommand } from "../ir/display-list.js";
|
|
2
|
+
/**
|
|
3
|
+
* Geometry helpers shared by everything that produces a `Path` IR node - today a colour glyph's
|
|
4
|
+
* outline, next an SVG shape.
|
|
5
|
+
*
|
|
6
|
+
* PDF has no quadratic curve operator: `c` (cubic), `v` and `y` are all there is. TrueType outlines
|
|
7
|
+
* are quadratic and so is SVG's `Q`/`T`, so both have to raise the degree, and there is exactly one
|
|
8
|
+
* correct way to do it - hence one function rather than two copies that drift.
|
|
9
|
+
*/
|
|
10
|
+
/**
|
|
11
|
+
* Raises a quadratic Bézier to the cubic PDF wants. A quadratic with control point `C` between `P0`
|
|
12
|
+
* and `P1` is the cubic with controls `P0 + 2/3(C - P0)` and `P1 + 2/3(C - P1)` - an exact identity,
|
|
13
|
+
* not an approximation, so nothing is lost.
|
|
14
|
+
*/
|
|
15
|
+
export declare function quadToCubic(x0: number, y0: number, cx: number, cy: number, x: number, y: number): PathCommand;
|
|
16
|
+
/**
|
|
17
|
+
* Applies an affine to every point of a command list, BAKING the transform into the coordinates.
|
|
18
|
+
*
|
|
19
|
+
* Normally a transform belongs in the graphics state, so a stroke scales with the drawing. A clip has
|
|
20
|
+
* no stroke and is set from a path in the CURRENT space, so a `<clipPath>` whose children carry their
|
|
21
|
+
* own `transform` has to have it folded in here instead.
|
|
22
|
+
*/
|
|
23
|
+
export declare function transformCommands(commands: readonly PathCommand[], [a, b, c, d, e, f]: readonly [number, number, number, number, number, number]): PathCommand[];
|
|
24
|
+
/**
|
|
25
|
+
* The bounding box of a command list. Control points are included, which overstates a curve slightly -
|
|
26
|
+
* the approximation every renderer makes for `objectBoundingBox` units and for a canvas gradient.
|
|
27
|
+
*/
|
|
28
|
+
export declare function boundsOf(commands: readonly PathCommand[]): {
|
|
29
|
+
x: number;
|
|
30
|
+
y: number;
|
|
31
|
+
width: number;
|
|
32
|
+
height: number;
|
|
33
|
+
};
|