ntk 8.17.0 → 8.17.3
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/lib/fontconfig.js +40 -2
- package/lib/text/font.js +48 -13
- package/lib/text/sfnt.js +388 -0
- package/lib/widgets/svgview.js +133 -13
- package/package.json +1 -1
package/lib/fontconfig.js
CHANGED
|
@@ -226,8 +226,26 @@ export function platformFamilies(family, platform = globalThis.process?.platform
|
|
|
226
226
|
return changed ? out.join(',') : family;
|
|
227
227
|
}
|
|
228
228
|
|
|
229
|
+
/**
|
|
230
|
+
* A family name as fontconfig's pattern syntax reads it (FcNameParse): a
|
|
231
|
+
* `-` starts the point size, a `:` the properties, a `,` the next family,
|
|
232
|
+
* and a `\` escapes each of them. Unescaped, the CSS list
|
|
233
|
+
* `tablet-gothic-condensed, "arial narrow", arial` was the family `tablet`
|
|
234
|
+
* and a size fc-match could not read, the families after it were gone, and
|
|
235
|
+
* the face was fontconfig's default for nothing: Verdana, where a browser
|
|
236
|
+
* sets Arial Narrow.
|
|
237
|
+
*/
|
|
238
|
+
function fcName(name) {
|
|
239
|
+
return name.replace(/[\\\-:,]/g, '\\$&');
|
|
240
|
+
}
|
|
241
|
+
|
|
229
242
|
function patternFor({ family, weight, style }) {
|
|
230
|
-
let fc = platformFamilies(family || 'sans-serif')
|
|
243
|
+
let fc = String(platformFamilies(family || 'sans-serif'))
|
|
244
|
+
.split(',')
|
|
245
|
+
.map((name) => name.trim().replace(/^["']|["']$/g, ''))
|
|
246
|
+
.filter(Boolean)
|
|
247
|
+
.map(fcName)
|
|
248
|
+
.join(',');
|
|
231
249
|
const fcWeight = normalizeWeight(weight);
|
|
232
250
|
if (fcWeight !== undefined) fc += `:weight=${fcWeight}`;
|
|
233
251
|
if (style && style.includes('italic')) fc += ':slant=italic';
|
|
@@ -883,11 +901,31 @@ export function charsetHas(candidate, codepoint) {
|
|
|
883
901
|
return false;
|
|
884
902
|
}
|
|
885
903
|
|
|
904
|
+
/**
|
|
905
|
+
* A CSS font weight on fontconfig's scale, which is not CSS's: regular is 80
|
|
906
|
+
* there and black is 210. The hundreds are the table's; a weight between two
|
|
907
|
+
* of them is as far between their fontconfig weights, so 450 is 90, halfway
|
|
908
|
+
* from regular to medium. CSS takes 1 to 1000, and what is outside the table
|
|
909
|
+
* is its nearest end.
|
|
910
|
+
*
|
|
911
|
+
* Handed over as it was, 450 was a weight past twice black, and fontconfig
|
|
912
|
+
* answered with the heaviest face a family has: Arial Bold, where a browser
|
|
913
|
+
* sets 450 in Arial Regular. At 90 fontconfig picks the face nearest in
|
|
914
|
+
* weight, which is the regular's 80 and not the bold's 200.
|
|
915
|
+
*
|
|
916
|
+
* A whole number, because a pattern is a cache key and an fc-match of its
|
|
917
|
+
* own: the weights an axis is animated through then share the ones they
|
|
918
|
+
* round to.
|
|
919
|
+
*/
|
|
886
920
|
function normalizeWeight(weight) {
|
|
887
921
|
if (weight === undefined) return undefined;
|
|
888
922
|
if (weight === 'normal') return cssToFcWeight[400];
|
|
889
923
|
if (weight === 'bold') return cssToFcWeight[700];
|
|
890
924
|
const n = parseInt(weight, 10);
|
|
891
925
|
if (Number.isNaN(n)) return undefined;
|
|
892
|
-
|
|
926
|
+
const css = Math.min(900, Math.max(100, n));
|
|
927
|
+
const below = Math.floor(css / 100) * 100;
|
|
928
|
+
const lighter = cssToFcWeight[below];
|
|
929
|
+
if (css === below) return lighter;
|
|
930
|
+
return Math.round(lighter + ((cssToFcWeight[below + 100] - lighter) * (css - below)) / 100);
|
|
893
931
|
}
|
package/lib/text/font.js
CHANGED
|
@@ -4,6 +4,7 @@ import { flatten, rasterizePath } from '../rasterize.js';
|
|
|
4
4
|
import { NO_ANCHOR, tolerateAnchors, watchAnchors } from './anchors.js';
|
|
5
5
|
import { marksCover, standInMarks } from './marks.js';
|
|
6
6
|
import { pairGlyphs, pairsCover } from './pairs.js';
|
|
7
|
+
import { sfntOf } from './sfnt.js';
|
|
7
8
|
|
|
8
9
|
// Axis coordinates are rounded to this many decimals before anything is
|
|
9
10
|
// instantiated or cached. Every distinct coordinate is a font in its own
|
|
@@ -145,11 +146,20 @@ function statedOrGlyph(fk, field, codePoint) {
|
|
|
145
146
|
* the space among them, and every layout in it threw, which left a whole
|
|
146
147
|
* document blank. An empty glyph's box is its empty outline's, as the rest
|
|
147
148
|
* of fontkit has it. Installed once, on the class every TrueType glyph is.
|
|
149
|
+
*
|
|
150
|
+
* Not on a WOFF2's glyphs, where its `glyf` table is transformed: those are
|
|
151
|
+
* decoded whole, with no header to read, and the file's `loca` is empty —
|
|
152
|
+
* fontkit reads the table after it in its place, which is wrong about which
|
|
153
|
+
* glyphs are empty (about 24 of Geist's 975). Asked, it sent a glyph to a
|
|
154
|
+
* header that is not there, and in an instance of a variable face, whose
|
|
155
|
+
* box is needed to make the outline, round and round until the stack ran
|
|
156
|
+
* out.
|
|
148
157
|
*/
|
|
149
158
|
const MENDED = new WeakSet();
|
|
150
159
|
|
|
151
160
|
function mendEmptyGlyphBoxes(fk) {
|
|
152
|
-
|
|
161
|
+
const glyf = fk.directory?.tables?.glyf;
|
|
162
|
+
if (!glyf || glyf.transformed) return;
|
|
153
163
|
let glyph;
|
|
154
164
|
try {
|
|
155
165
|
glyph = fk.getGlyph(0);
|
|
@@ -169,6 +179,28 @@ function mendEmptyGlyphBoxes(fk) {
|
|
|
169
179
|
};
|
|
170
180
|
}
|
|
171
181
|
|
|
182
|
+
/**
|
|
183
|
+
* `fk`, or where it is a variable face read out of a WOFF or a WOFF2, the
|
|
184
|
+
* same font read from the sfnt inside the container (./sfnt.js): fontkit
|
|
185
|
+
* cuts an instance out of an uncompressed font only, and a variable web
|
|
186
|
+
* font is nearly always a WOFF2. A static face is left as it came — nothing
|
|
187
|
+
* is asked of it that a container does not answer.
|
|
188
|
+
*
|
|
189
|
+
* A container this cannot take apart is left too, and says so when an
|
|
190
|
+
* instance is asked of it (`_instantiate`).
|
|
191
|
+
*/
|
|
192
|
+
function instantiable(fk) {
|
|
193
|
+
if (fk.type !== 'WOFF' && fk.type !== 'WOFF2') return fk;
|
|
194
|
+
if (!fk.directory?.tables?.fvar) return fk;
|
|
195
|
+
try {
|
|
196
|
+
const bytes = sfntOf(fk);
|
|
197
|
+
if (!bytes) return fk;
|
|
198
|
+
return fontkit.create(Buffer.from(bytes.buffer, bytes.byteOffset, bytes.byteLength));
|
|
199
|
+
} catch {
|
|
200
|
+
return fk;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
172
204
|
export default class Font {
|
|
173
205
|
constructor(fkFont, path, postscriptName) {
|
|
174
206
|
answerWithCharacters(fkFont);
|
|
@@ -205,7 +237,7 @@ export default class Font {
|
|
|
205
237
|
(postscriptName && fk.fonts.find((f) => f.postscriptName === postscriptName)) ||
|
|
206
238
|
fk.fonts[0];
|
|
207
239
|
}
|
|
208
|
-
return new Font(fk, path, postscriptName);
|
|
240
|
+
return new Font(instantiable(fk), path, postscriptName);
|
|
209
241
|
}
|
|
210
242
|
|
|
211
243
|
/**
|
|
@@ -226,7 +258,7 @@ export default class Font {
|
|
|
226
258
|
(postscriptName && fk.fonts.find((f) => f.postscriptName === postscriptName)) ||
|
|
227
259
|
fk.fonts[0];
|
|
228
260
|
}
|
|
229
|
-
const font = new Font(fk, null, postscriptName);
|
|
261
|
+
const font = new Font(instantiable(fk), null, postscriptName);
|
|
230
262
|
font.key = key ?? `mem:#${fk.postscriptName || ''}`;
|
|
231
263
|
return font;
|
|
232
264
|
}
|
|
@@ -307,15 +339,18 @@ export default class Font {
|
|
|
307
339
|
|
|
308
340
|
/**
|
|
309
341
|
* fontkit's instancing, with the one failure it has turned into something
|
|
310
|
-
* that says what
|
|
342
|
+
* that says what happened.
|
|
311
343
|
*
|
|
312
|
-
* `getVariation` rebuilds the face
|
|
313
|
-
*
|
|
314
|
-
* Brotli-compressed
|
|
315
|
-
*
|
|
344
|
+
* `getVariation` rebuilds the face as a plain sfnt from the original
|
|
345
|
+
* stream, which for a WOFF is another directory and for a WOFF2 is still
|
|
346
|
+
* Brotli-compressed: the copy comes back with a directory parsed out of
|
|
347
|
+
* the wrong bytes — no `cmap`, and a `Cannot read properties of undefined
|
|
316
348
|
* (reading 'tables')` from somewhere deep in shaping, at the first
|
|
317
|
-
* character drawn rather than here.
|
|
318
|
-
*
|
|
349
|
+
* character drawn rather than here. A variable face in either container
|
|
350
|
+
* is read from the sfnt inside it for that reason (`instantiable`), so
|
|
351
|
+
* this is a container that could not be taken apart. Checking the
|
|
352
|
+
* instance is well-formed costs one property read and moves the report to
|
|
353
|
+
* the call that caused it.
|
|
319
354
|
*/
|
|
320
355
|
_instantiate(coords) {
|
|
321
356
|
const instance = this.fk.getVariation(coords);
|
|
@@ -323,9 +358,9 @@ export default class Font {
|
|
|
323
358
|
throw new Error(
|
|
324
359
|
`ntk: cannot instantiate a variation of ${this.postscriptName || this.key}` +
|
|
325
360
|
(this.fk.type ? ` (${this.fk.type})` : '') +
|
|
326
|
-
'. fontkit
|
|
327
|
-
'
|
|
328
|
-
'See docs/fonts.md#variable-fonts'
|
|
361
|
+
'. fontkit instantiates axes from an uncompressed font only, and ' +
|
|
362
|
+
'the sfnt inside this file could not be rebuilt — ship the ' +
|
|
363
|
+
'.ttf/.otf of the font. See docs/fonts.md#variable-fonts'
|
|
329
364
|
);
|
|
330
365
|
}
|
|
331
366
|
return instance;
|
package/lib/text/sfnt.js
ADDED
|
@@ -0,0 +1,388 @@
|
|
|
1
|
+
// A WOFF or a WOFF2, as the sfnt it was made from.
|
|
2
|
+
//
|
|
3
|
+
// fontkit reads a font out of either container, a table at a time, and that
|
|
4
|
+
// is all most faces are asked for. A variable face is asked for more: an
|
|
5
|
+
// instance, cut at a point of its design space, and fontkit (2.0.4) cuts
|
|
6
|
+
// one by reading the file again as a plain sfnt — a WOFF's directory is
|
|
7
|
+
// another, and a WOFF2 is Brotli-compressed — so the instance came back
|
|
8
|
+
// with no tables, and the first text set at a weight off the file's default
|
|
9
|
+
// threw. A variable web font is nearly always a WOFF2: a page set in one
|
|
10
|
+
// drew nothing.
|
|
11
|
+
//
|
|
12
|
+
// So a variable face that arrives in a container is handed to fontkit as
|
|
13
|
+
// the sfnt inside it (`Font`, text/font.js), and everything after that is
|
|
14
|
+
// the path an uncompressed font takes. Built here rather than fixed there,
|
|
15
|
+
// because there is nowhere to put a fixed fontkit: upstream releases
|
|
16
|
+
// rarely, and a fork has to be named by URL or by git, neither of which npm
|
|
17
|
+
// 12 installs for a dependency of a dependency (`allow-remote` and
|
|
18
|
+
// `allow-git` are `none` unless an application says otherwise).
|
|
19
|
+
//
|
|
20
|
+
// A WOFF is the sfnt's tables, each deflated on its own, and fontkit
|
|
21
|
+
// inflates them. A WOFF2 is the tables in one Brotli stream, which fontkit
|
|
22
|
+
// decompresses, with `glyf` and `loca` *transformed*: the glyphs' contour
|
|
23
|
+
// counts, point counts, flags, coordinates, composites, boxes and
|
|
24
|
+
// instructions are each a stream of their own, and `loca` is left out. The
|
|
25
|
+
// work here is writing those back as the two tables (WOFF2, 5.1 to 5.3).
|
|
26
|
+
|
|
27
|
+
/** Reads big-endian numbers off a byte array, from a position it keeps. */
|
|
28
|
+
class Reader {
|
|
29
|
+
constructor(bytes, pos = 0, end = bytes.length) {
|
|
30
|
+
this.bytes = bytes;
|
|
31
|
+
this.view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
32
|
+
this.pos = pos;
|
|
33
|
+
this.end = end;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
need(n) {
|
|
37
|
+
if (this.pos + n > this.end) throw new RangeError('a stream of the WOFF2 glyf table ends short');
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
u8() {
|
|
41
|
+
this.need(1);
|
|
42
|
+
return this.bytes[this.pos++];
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
u16() {
|
|
46
|
+
this.need(2);
|
|
47
|
+
const value = this.view.getUint16(this.pos);
|
|
48
|
+
this.pos += 2;
|
|
49
|
+
return value;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
i16() {
|
|
53
|
+
this.need(2);
|
|
54
|
+
const value = this.view.getInt16(this.pos);
|
|
55
|
+
this.pos += 2;
|
|
56
|
+
return value;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
u32() {
|
|
60
|
+
this.need(4);
|
|
61
|
+
const value = this.view.getUint32(this.pos);
|
|
62
|
+
this.pos += 4;
|
|
63
|
+
return value;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
take(n) {
|
|
67
|
+
this.need(n);
|
|
68
|
+
const out = this.bytes.subarray(this.pos, this.pos + n);
|
|
69
|
+
this.pos += n;
|
|
70
|
+
return out;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** WOFF2's 255UInt16: a number up to 65535 in one to three bytes. */
|
|
74
|
+
u255() {
|
|
75
|
+
const code = this.u8();
|
|
76
|
+
if (code === 253) return this.u16();
|
|
77
|
+
if (code === 255) return this.u8() + 253;
|
|
78
|
+
if (code === 254) return this.u8() + 506;
|
|
79
|
+
return code;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const signed = (flag, value) => (flag & 1 ? value : -value);
|
|
84
|
+
|
|
85
|
+
// glyf: a simple glyph's flags, and a composite's
|
|
86
|
+
const ON_CURVE = 0x01;
|
|
87
|
+
const OVERLAP_SIMPLE = 0x40;
|
|
88
|
+
const ARGS_ARE_WORDS = 0x0001;
|
|
89
|
+
const HAS_SCALE = 0x0008;
|
|
90
|
+
const MORE_COMPONENTS = 0x0020;
|
|
91
|
+
const HAS_XY_SCALE = 0x0040;
|
|
92
|
+
const HAS_TWO_BY_TWO = 0x0080;
|
|
93
|
+
const HAS_INSTRUCTIONS = 0x0100;
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* The `glyf` and `loca` tables a WOFF2's transformed `glyf` stands for.
|
|
97
|
+
*
|
|
98
|
+
* A glyph comes out as the points it had, not the bytes: a coordinate is
|
|
99
|
+
* written as a 16-bit step from the one before, where the font it was made
|
|
100
|
+
* from had short forms and repeats. What reads the table reads the same
|
|
101
|
+
* outline, and nothing here is written to a file.
|
|
102
|
+
*
|
|
103
|
+
* @param {Uint8Array} table the transformed table
|
|
104
|
+
* @returns {{ glyf: Uint8Array, loca: Uint8Array }}
|
|
105
|
+
*/
|
|
106
|
+
export function untransformGlyf(table) {
|
|
107
|
+
const head = new Reader(table);
|
|
108
|
+
head.u16(); // reserved
|
|
109
|
+
const options = head.u16();
|
|
110
|
+
const numGlyphs = head.u16();
|
|
111
|
+
head.u16(); // the loca format the font had: a long one is written
|
|
112
|
+
const sizes = [];
|
|
113
|
+
for (let i = 0; i < 7; i += 1) sizes.push(head.u32());
|
|
114
|
+
let at = head.pos;
|
|
115
|
+
const stream = (size) => {
|
|
116
|
+
const reader = new Reader(table, at, at + size);
|
|
117
|
+
at += size;
|
|
118
|
+
if (at > table.length) throw new RangeError('the WOFF2 glyf table is shorter than its streams');
|
|
119
|
+
return reader;
|
|
120
|
+
};
|
|
121
|
+
const contours = stream(sizes[0]);
|
|
122
|
+
const pointCounts = stream(sizes[1]);
|
|
123
|
+
const flags = stream(sizes[2]);
|
|
124
|
+
const glyphs = stream(sizes[3]);
|
|
125
|
+
const composites = stream(sizes[4]);
|
|
126
|
+
const boxes = stream(sizes[5]);
|
|
127
|
+
const instructions = stream(sizes[6]);
|
|
128
|
+
// a bit a glyph, set where its box is stored; the boxes follow the bits
|
|
129
|
+
const stored = boxes.take(((numGlyphs + 31) >> 5) << 2);
|
|
130
|
+
const overlaps = options & 1 ? new Reader(table, at).take((numGlyphs + 7) >> 3) : null;
|
|
131
|
+
const bit = (bits, i) => (bits[i >> 3] & (0x80 >> (i & 7))) !== 0;
|
|
132
|
+
|
|
133
|
+
const records = [];
|
|
134
|
+
let total = 0;
|
|
135
|
+
for (let id = 0; id < numGlyphs; id += 1) {
|
|
136
|
+
const count = contours.i16();
|
|
137
|
+
let record = null;
|
|
138
|
+
if (count > 0) {
|
|
139
|
+
record = simpleGlyph(count, {
|
|
140
|
+
pointCounts,
|
|
141
|
+
flags,
|
|
142
|
+
glyphs,
|
|
143
|
+
instructions,
|
|
144
|
+
box: bit(stored, id) ? boxes : null,
|
|
145
|
+
overlap: overlaps !== null && bit(overlaps, id)
|
|
146
|
+
});
|
|
147
|
+
} else if (count < 0) {
|
|
148
|
+
// a composite's box cannot be had from its points: it has to be stored
|
|
149
|
+
if (!bit(stored, id)) throw new RangeError('a composite glyph of the WOFF2 has no box');
|
|
150
|
+
record = compositeGlyph(count, { composites, glyphs, instructions, box: boxes });
|
|
151
|
+
}
|
|
152
|
+
records.push(record);
|
|
153
|
+
if (record) total += (record.length + 3) & ~3;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const glyf = new Uint8Array(total);
|
|
157
|
+
const loca = new Uint8Array((numGlyphs + 1) * 4);
|
|
158
|
+
const offsets = new DataView(loca.buffer);
|
|
159
|
+
let offset = 0;
|
|
160
|
+
for (let id = 0; id < numGlyphs; id += 1) {
|
|
161
|
+
offsets.setUint32(id * 4, offset);
|
|
162
|
+
const record = records[id];
|
|
163
|
+
if (!record) continue;
|
|
164
|
+
glyf.set(record, offset);
|
|
165
|
+
offset += (record.length + 3) & ~3;
|
|
166
|
+
}
|
|
167
|
+
offsets.setUint32(numGlyphs * 4, offset);
|
|
168
|
+
return { glyf, loca };
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function simpleGlyph(count, { pointCounts, flags, glyphs, instructions, box, overlap }) {
|
|
172
|
+
const ends = [];
|
|
173
|
+
let points = 0;
|
|
174
|
+
for (let i = 0; i < count; i += 1) {
|
|
175
|
+
points += pointCounts.u255();
|
|
176
|
+
ends.push(points - 1);
|
|
177
|
+
}
|
|
178
|
+
const on = new Uint8Array(points);
|
|
179
|
+
const xs = new Int32Array(points);
|
|
180
|
+
const ys = new Int32Array(points);
|
|
181
|
+
let x = 0;
|
|
182
|
+
let y = 0;
|
|
183
|
+
let xMin = 0x7fff;
|
|
184
|
+
let yMin = 0x7fff;
|
|
185
|
+
let xMax = -0x8000;
|
|
186
|
+
let yMax = -0x8000;
|
|
187
|
+
for (let i = 0; i < points; i += 1) {
|
|
188
|
+
// a point is a flag and one to four bytes: which, and how they divide
|
|
189
|
+
// between the two steps, is the flag's (WOFF2, 5.2)
|
|
190
|
+
let flag = flags.u8();
|
|
191
|
+
on[i] = flag >> 7 ? 0 : 1;
|
|
192
|
+
flag &= 0x7f;
|
|
193
|
+
let dx = 0;
|
|
194
|
+
let dy = 0;
|
|
195
|
+
if (flag < 10) {
|
|
196
|
+
dy = signed(flag, ((flag & 14) << 7) + glyphs.u8());
|
|
197
|
+
} else if (flag < 20) {
|
|
198
|
+
dx = signed(flag, (((flag - 10) & 14) << 7) + glyphs.u8());
|
|
199
|
+
} else if (flag < 84) {
|
|
200
|
+
const b0 = flag - 20;
|
|
201
|
+
const b1 = glyphs.u8();
|
|
202
|
+
dx = signed(flag, 1 + (b0 & 0x30) + (b1 >> 4));
|
|
203
|
+
dy = signed(flag >> 1, 1 + ((b0 & 0x0c) << 2) + (b1 & 0x0f));
|
|
204
|
+
} else if (flag < 120) {
|
|
205
|
+
const b0 = flag - 84;
|
|
206
|
+
dx = signed(flag, 1 + (Math.floor(b0 / 12) << 8) + glyphs.u8());
|
|
207
|
+
dy = signed(flag >> 1, 1 + (((b0 % 12) >> 2) << 8) + glyphs.u8());
|
|
208
|
+
} else if (flag < 124) {
|
|
209
|
+
const b1 = glyphs.u8();
|
|
210
|
+
const b2 = glyphs.u8();
|
|
211
|
+
dx = signed(flag, (b1 << 4) + (b2 >> 4));
|
|
212
|
+
dy = signed(flag >> 1, ((b2 & 0x0f) << 8) + glyphs.u8());
|
|
213
|
+
} else {
|
|
214
|
+
dx = signed(flag, glyphs.u16());
|
|
215
|
+
dy = signed(flag >> 1, glyphs.u16());
|
|
216
|
+
}
|
|
217
|
+
xs[i] = dx;
|
|
218
|
+
ys[i] = dy;
|
|
219
|
+
x += dx;
|
|
220
|
+
y += dy;
|
|
221
|
+
if (x < xMin) xMin = x;
|
|
222
|
+
if (x > xMax) xMax = x;
|
|
223
|
+
if (y < yMin) yMin = y;
|
|
224
|
+
if (y > yMax) yMax = y;
|
|
225
|
+
}
|
|
226
|
+
const program = instructions.take(glyphs.u255());
|
|
227
|
+
if (box) {
|
|
228
|
+
xMin = box.i16();
|
|
229
|
+
yMin = box.i16();
|
|
230
|
+
xMax = box.i16();
|
|
231
|
+
yMax = box.i16();
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
const out = new Uint8Array(10 + count * 2 + 2 + program.length + points * 5);
|
|
235
|
+
const view = new DataView(out.buffer);
|
|
236
|
+
view.setInt16(0, count);
|
|
237
|
+
view.setInt16(2, xMin);
|
|
238
|
+
view.setInt16(4, yMin);
|
|
239
|
+
view.setInt16(6, xMax);
|
|
240
|
+
view.setInt16(8, yMax);
|
|
241
|
+
let pos = 10;
|
|
242
|
+
for (const end of ends) {
|
|
243
|
+
view.setUint16(pos, end);
|
|
244
|
+
pos += 2;
|
|
245
|
+
}
|
|
246
|
+
view.setUint16(pos, program.length);
|
|
247
|
+
pos += 2;
|
|
248
|
+
out.set(program, pos);
|
|
249
|
+
pos += program.length;
|
|
250
|
+
for (let i = 0; i < points; i += 1) out[pos + i] = on[i] ? ON_CURVE : 0;
|
|
251
|
+
if (overlap && points) out[pos] |= OVERLAP_SIMPLE;
|
|
252
|
+
pos += points;
|
|
253
|
+
for (let i = 0; i < points; i += 1) view.setInt16(pos + i * 2, xs[i]);
|
|
254
|
+
pos += points * 2;
|
|
255
|
+
for (let i = 0; i < points; i += 1) view.setInt16(pos + i * 2, ys[i]);
|
|
256
|
+
return out;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function compositeGlyph(count, { composites, glyphs, instructions, box }) {
|
|
260
|
+
// the components are in the stream as they are in a glyf table
|
|
261
|
+
const from = composites.pos;
|
|
262
|
+
let flags;
|
|
263
|
+
let programmed = false;
|
|
264
|
+
do {
|
|
265
|
+
flags = composites.u16();
|
|
266
|
+
let size = 2 + (flags & ARGS_ARE_WORDS ? 4 : 2);
|
|
267
|
+
if (flags & HAS_SCALE) size += 2;
|
|
268
|
+
else if (flags & HAS_XY_SCALE) size += 4;
|
|
269
|
+
else if (flags & HAS_TWO_BY_TWO) size += 8;
|
|
270
|
+
composites.take(size);
|
|
271
|
+
if (flags & HAS_INSTRUCTIONS) programmed = true;
|
|
272
|
+
} while (flags & MORE_COMPONENTS);
|
|
273
|
+
const components = composites.bytes.subarray(from, composites.pos);
|
|
274
|
+
const program = programmed ? instructions.take(glyphs.u255()) : null;
|
|
275
|
+
|
|
276
|
+
const out = new Uint8Array(10 + components.length + (program ? 2 + program.length : 0));
|
|
277
|
+
const view = new DataView(out.buffer);
|
|
278
|
+
view.setInt16(0, count);
|
|
279
|
+
for (let i = 0; i < 4; i += 1) view.setInt16(2 + i * 2, box.i16());
|
|
280
|
+
out.set(components, 10);
|
|
281
|
+
if (program) {
|
|
282
|
+
view.setUint16(10 + components.length, program.length);
|
|
283
|
+
out.set(program, 12 + components.length);
|
|
284
|
+
}
|
|
285
|
+
return out;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/** Where a `head` table says which of `loca`'s two formats the font uses. */
|
|
289
|
+
const LOCA_FORMAT = 50;
|
|
290
|
+
|
|
291
|
+
/** A table's checksum: its bytes as 32-bit numbers, the last one padded. */
|
|
292
|
+
function checksum(data) {
|
|
293
|
+
let sum = 0;
|
|
294
|
+
const view = new DataView(data.buffer, data.byteOffset, data.byteLength);
|
|
295
|
+
const whole = data.length & ~3;
|
|
296
|
+
for (let i = 0; i < whole; i += 4) sum = (sum + view.getUint32(i)) >>> 0;
|
|
297
|
+
let last = 0;
|
|
298
|
+
for (let i = whole; i < data.length; i += 1) last |= data[i] << (24 - (i - whole) * 8);
|
|
299
|
+
return (sum + (last >>> 0)) >>> 0;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/** An sfnt of `tables`, each `{ tag, data }`, under a version `flavor`. */
|
|
303
|
+
function assemble(flavor, tables) {
|
|
304
|
+
tables.sort((a, b) => (a.tag < b.tag ? -1 : a.tag > b.tag ? 1 : 0));
|
|
305
|
+
const count = tables.length;
|
|
306
|
+
let size = 12 + count * 16;
|
|
307
|
+
for (const table of tables) size += (table.data.length + 3) & ~3;
|
|
308
|
+
const out = new Uint8Array(size);
|
|
309
|
+
const view = new DataView(out.buffer);
|
|
310
|
+
const power = Math.floor(Math.log2(count));
|
|
311
|
+
view.setUint32(0, flavor);
|
|
312
|
+
view.setUint16(4, count);
|
|
313
|
+
view.setUint16(6, 16 << power);
|
|
314
|
+
view.setUint16(8, power);
|
|
315
|
+
view.setUint16(10, count * 16 - (16 << power));
|
|
316
|
+
let offset = 12 + count * 16;
|
|
317
|
+
tables.forEach(({ tag, data }, i) => {
|
|
318
|
+
const entry = 12 + i * 16;
|
|
319
|
+
for (let c = 0; c < 4; c += 1) out[entry + c] = tag.charCodeAt(c);
|
|
320
|
+
view.setUint32(entry + 4, checksum(data));
|
|
321
|
+
view.setUint32(entry + 8, offset);
|
|
322
|
+
view.setUint32(entry + 12, data.length);
|
|
323
|
+
out.set(data, offset);
|
|
324
|
+
offset += (data.length + 3) & ~3;
|
|
325
|
+
});
|
|
326
|
+
return out;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/** `ttcf`: a collection, which neither container is read as here. */
|
|
330
|
+
const COLLECTION = 0x74746366;
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* The sfnt inside a font fontkit read out of a WOFF or a WOFF2, or null
|
|
334
|
+
* where there is none to be had: a font that is not in a container, a
|
|
335
|
+
* collection, or a WOFF2 whose `hmtx` is transformed, which fontkit does
|
|
336
|
+
* not read either.
|
|
337
|
+
*
|
|
338
|
+
* Reads fontkit's own state — its directory, and the tables as it inflated
|
|
339
|
+
* or decompressed them — so the bytes are decompressed once, by the code
|
|
340
|
+
* that already does it.
|
|
341
|
+
*
|
|
342
|
+
* @param {object} fk a fontkit font
|
|
343
|
+
* @returns {Uint8Array|null}
|
|
344
|
+
*/
|
|
345
|
+
export function sfntOf(fk) {
|
|
346
|
+
const entries = fk?.directory?.tables;
|
|
347
|
+
if (!entries || fk.directory.flavor === COLLECTION) return null;
|
|
348
|
+
const tables = [];
|
|
349
|
+
|
|
350
|
+
if (fk.type === 'WOFF') {
|
|
351
|
+
for (const tag of Object.keys(entries)) {
|
|
352
|
+
const entry = entries[tag];
|
|
353
|
+
const stream = fk._getTableStream(tag);
|
|
354
|
+
// a table deflated in the file is a stream of its own, and one that
|
|
355
|
+
// was stored is the file's, at the table
|
|
356
|
+
const data =
|
|
357
|
+
stream === fk.stream
|
|
358
|
+
? fk.stream.buffer.subarray(entry.offset, entry.offset + entry.length)
|
|
359
|
+
: stream.buffer;
|
|
360
|
+
tables.push({ tag, data });
|
|
361
|
+
}
|
|
362
|
+
return assemble(fk.directory.flavor, tables);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
if (fk.type !== 'WOFF2') return null;
|
|
366
|
+
fk._decompress();
|
|
367
|
+
const bytes = fk.stream.buffer;
|
|
368
|
+
const stored = (entry) =>
|
|
369
|
+
bytes.subarray(entry.offset, entry.offset + (entry.transformLength ?? entry.length));
|
|
370
|
+
for (const tag of Object.keys(entries)) {
|
|
371
|
+
const entry = entries[tag];
|
|
372
|
+
if (tag === 'loca' && entries.glyf?.transformed) continue;
|
|
373
|
+
if (tag === 'glyf' && entry.transformed) {
|
|
374
|
+
const { glyf, loca } = untransformGlyf(stored(entry));
|
|
375
|
+
tables.push({ tag: 'glyf', data: glyf }, { tag: 'loca', data: loca });
|
|
376
|
+
} else if (entry.transformed) {
|
|
377
|
+
return null;
|
|
378
|
+
} else if (tag === 'head' && entries.glyf?.transformed) {
|
|
379
|
+
// `loca` is written long, whatever it was
|
|
380
|
+
const head = stored(entry).slice();
|
|
381
|
+
new DataView(head.buffer).setInt16(LOCA_FORMAT, 1);
|
|
382
|
+
tables.push({ tag, data: head });
|
|
383
|
+
} else {
|
|
384
|
+
tables.push({ tag, data: stored(entry) });
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
return assemble(fk.directory.flavor, tables);
|
|
388
|
+
}
|
package/lib/widgets/svgview.js
CHANGED
|
@@ -76,6 +76,26 @@ function attrNum(node, name, fallback = 0) {
|
|
|
76
76
|
return Number.isFinite(n) ? n : fallback;
|
|
77
77
|
}
|
|
78
78
|
|
|
79
|
+
/**
|
|
80
|
+
* Whether an element is `display: none`, by its attribute or in its
|
|
81
|
+
* `style`, which wins. Such an element is not rendered, and nor is anything
|
|
82
|
+
* in it, whatever that says of itself: `display` is not inherited, and a
|
|
83
|
+
* child's `display="inline"` brings nothing back (SVG 1.1, 11.5). That is
|
|
84
|
+
* how an editor writes a hidden layer — Illustrator's `<g display="none">`
|
|
85
|
+
* around shapes it marks `display="inline"`.
|
|
86
|
+
*/
|
|
87
|
+
function displayNone(node) {
|
|
88
|
+
let v = attr(node, 'display');
|
|
89
|
+
const style = node.attribs?.style;
|
|
90
|
+
if (style && style.includes('display')) {
|
|
91
|
+
for (const decl of style.split(';')) {
|
|
92
|
+
const idx = decl.indexOf(':');
|
|
93
|
+
if (idx > 0 && decl.slice(0, idx).trim() === 'display') v = decl.slice(idx + 1);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return v !== undefined && String(v).trim().toLowerCase() === 'none';
|
|
97
|
+
}
|
|
98
|
+
|
|
79
99
|
const NON_RENDERED = new Set([
|
|
80
100
|
'defs', 'title', 'desc', 'metadata', 'symbol', 'style',
|
|
81
101
|
'lineargradient', 'radialgradient', 'clippath', 'mask', 'filter', 'pattern', 'marker'
|
|
@@ -259,7 +279,7 @@ function scanPaints(root, ids) {
|
|
|
259
279
|
const visit = (node, fill, stroke, depth) => {
|
|
260
280
|
if (multi || depth > 32) return;
|
|
261
281
|
const name = tag(node);
|
|
262
|
-
if (NON_RENDERED.has(name)) return;
|
|
282
|
+
if (NON_RENDERED.has(name) || displayNone(node)) return;
|
|
263
283
|
const f = paintOf(node, 'fill', fill);
|
|
264
284
|
const s = paintOf(node, 'stroke', stroke);
|
|
265
285
|
switch (name) {
|
|
@@ -477,9 +497,38 @@ export default class SvgView {
|
|
|
477
497
|
return this._gradient(ctx, node, path);
|
|
478
498
|
}
|
|
479
499
|
|
|
500
|
+
/**
|
|
501
|
+
* A gradient and the gradients it takes after: the one its `href` names,
|
|
502
|
+
* the one that names, and so on (SVG 1.1, 13.2.2). What a gradient does
|
|
503
|
+
* not set itself it takes from the first of these that sets it, and its
|
|
504
|
+
* stops, where it has none, from the first that has some. An editor
|
|
505
|
+
* writes a drawing's colours once, in a gradient of stops alone, and
|
|
506
|
+
* every gradient that uses them as one line with an `xlink:href` to it:
|
|
507
|
+
* read without it, each had no stops, and what it filled was not drawn.
|
|
508
|
+
*/
|
|
509
|
+
_gradientChain(node) {
|
|
510
|
+
const chain = [node];
|
|
511
|
+
for (let at = node; chain.length < 32;) {
|
|
512
|
+
const href = attr(at, 'href') || attr(at, 'xlink:href') || '';
|
|
513
|
+
const next = href.startsWith('#') ? this._ids.get(href.slice(1)) : null;
|
|
514
|
+
if (!next || chain.includes(next)) break;
|
|
515
|
+
if (tag(next) !== 'lineargradient' && tag(next) !== 'radialgradient') break;
|
|
516
|
+
chain.push(next);
|
|
517
|
+
at = next;
|
|
518
|
+
}
|
|
519
|
+
return chain;
|
|
520
|
+
}
|
|
521
|
+
|
|
480
522
|
_gradient(ctx, node, path) {
|
|
481
|
-
const
|
|
482
|
-
const
|
|
523
|
+
const chain = this._gradientChain(node);
|
|
524
|
+
const own = (name) => {
|
|
525
|
+
for (const from of chain) {
|
|
526
|
+
const value = attr(from, name);
|
|
527
|
+
if (value !== undefined) return value;
|
|
528
|
+
}
|
|
529
|
+
return undefined;
|
|
530
|
+
};
|
|
531
|
+
const units = own('gradientUnits') || 'objectBoundingBox';
|
|
483
532
|
const bbox = units === 'objectBoundingBox' ? pathBBox(path) : null;
|
|
484
533
|
const coord = (raw, fallback, axis) => {
|
|
485
534
|
let v = raw === undefined ? fallback : parseFloat(raw);
|
|
@@ -488,26 +537,45 @@ export default class SvgView {
|
|
|
488
537
|
if (!bbox) return v;
|
|
489
538
|
return axis === 'x' ? bbox.x + v * bbox.w : bbox.y + v * bbox.h;
|
|
490
539
|
};
|
|
540
|
+
// `gradientTransform` is set in the gradient's own space, the bounding
|
|
541
|
+
// box's where that is its units: here, as the matrix it comes to in
|
|
542
|
+
// the path's user space
|
|
543
|
+
const matrix = gradientMatrix(own('gradientTransform'), bbox);
|
|
491
544
|
// gradient coordinates are user space, like the path's own — the
|
|
492
545
|
// context resolves them against the transform in force when it paints,
|
|
493
546
|
// which is this one (issue #271)
|
|
494
547
|
let gradient;
|
|
495
548
|
if (tag(node) === 'lineargradient') {
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
549
|
+
let x1 = coord(own('x1'), 0, 'x');
|
|
550
|
+
let y1 = coord(own('y1'), 0, 'y');
|
|
551
|
+
let x2 = coord(own('x2'), 1, 'x');
|
|
552
|
+
let y2 = coord(own('y2'), 0, 'y');
|
|
553
|
+
if (matrix) [x1, y1, x2, y2] = transformedLine(matrix, x1, y1, x2, y2);
|
|
500
554
|
gradient = ctx.createLinearGradient(x1, y1, x2, y2);
|
|
501
555
|
} else {
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
556
|
+
let cx = coord(own('cx'), 0.5, 'x');
|
|
557
|
+
let cy = coord(own('cy'), 0.5, 'y');
|
|
558
|
+
const raw = own('r');
|
|
559
|
+
let r = raw === undefined ? 0.5 : parseFloat(raw);
|
|
560
|
+
if (String(raw ?? '').endsWith('%')) r /= 100;
|
|
506
561
|
if (bbox) r *= (bbox.w + bbox.h) / 2;
|
|
562
|
+
if (matrix) {
|
|
563
|
+
// a circle's centre goes where the matrix takes it, and its radius
|
|
564
|
+
// grows by what the matrix scales an area by: exact for a matrix
|
|
565
|
+
// that turns, moves and scales evenly, and a circle of the same
|
|
566
|
+
// area where one stretches it into an ellipse, which a context's
|
|
567
|
+
// radial gradient is not
|
|
568
|
+
const [a, b, c, d, e, f] = matrix;
|
|
569
|
+
[cx, cy] = [a * cx + c * cy + e, b * cx + d * cy + f];
|
|
570
|
+
r *= Math.sqrt(Math.abs(a * d - b * c));
|
|
571
|
+
}
|
|
507
572
|
gradient = ctx.createRadialGradient(cx, cy, 0, cx, cy, r);
|
|
508
573
|
}
|
|
509
574
|
|
|
510
|
-
|
|
575
|
+
const stops = chain.find((from) =>
|
|
576
|
+
(from.children || []).some((child) => child.type === 'tag' && tag(child) === 'stop')
|
|
577
|
+
);
|
|
578
|
+
for (const stop of stops?.children || []) {
|
|
511
579
|
if (stop.type !== 'tag' || tag(stop) !== 'stop') continue;
|
|
512
580
|
const sa = stop.attribs || {};
|
|
513
581
|
const decls = {};
|
|
@@ -539,7 +607,7 @@ export default class SvgView {
|
|
|
539
607
|
_renderNode(node, ctx, parentStyle, alpha, depth) {
|
|
540
608
|
if (depth > 32) return;
|
|
541
609
|
const name = tag(node);
|
|
542
|
-
if (NON_RENDERED.has(name)) return;
|
|
610
|
+
if (NON_RENDERED.has(name) || displayNone(node)) return;
|
|
543
611
|
|
|
544
612
|
const style = this._style(node, parentStyle);
|
|
545
613
|
const opacity = parseFloat(node.attribs?.opacity ?? '1');
|
|
@@ -627,6 +695,58 @@ export default class SvgView {
|
|
|
627
695
|
}
|
|
628
696
|
|
|
629
697
|
// fold an opacity into a CSS color by going through rgba()
|
|
698
|
+
/**
|
|
699
|
+
* A `gradientTransform` as one matrix in the path's user space, or null
|
|
700
|
+
* where there is none. With `objectBoundingBox` units the transform is set
|
|
701
|
+
* in the box's own 0-to-1 space, so it is carried out of it and back:
|
|
702
|
+
* `B · M · B⁻¹`, for the matrix `B` that takes that space to the box.
|
|
703
|
+
*/
|
|
704
|
+
function gradientMatrix(value, bbox) {
|
|
705
|
+
const list = parseSvgTransform(value);
|
|
706
|
+
if (!list.length) return null;
|
|
707
|
+
const mul = (m, n) => [
|
|
708
|
+
m[0] * n[0] + m[2] * n[1],
|
|
709
|
+
m[1] * n[0] + m[3] * n[1],
|
|
710
|
+
m[0] * n[2] + m[2] * n[3],
|
|
711
|
+
m[1] * n[2] + m[3] * n[3],
|
|
712
|
+
m[0] * n[4] + m[2] * n[5] + m[4],
|
|
713
|
+
m[1] * n[4] + m[3] * n[5] + m[5]
|
|
714
|
+
];
|
|
715
|
+
let matrix = list.reduce(mul);
|
|
716
|
+
if (bbox) {
|
|
717
|
+
if (!(bbox.w > 0 && bbox.h > 0)) return null;
|
|
718
|
+
const box = [bbox.w, 0, 0, bbox.h, bbox.x, bbox.y];
|
|
719
|
+
const back = [1 / bbox.w, 0, 0, 1 / bbox.h, -bbox.x / bbox.w, -bbox.y / bbox.h];
|
|
720
|
+
matrix = mul(mul(box, matrix), back);
|
|
721
|
+
}
|
|
722
|
+
return matrix;
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
/**
|
|
726
|
+
* A linear gradient's line under a matrix. Its start goes where the matrix
|
|
727
|
+
* takes it. Its end does only where the matrix keeps right angles: the
|
|
728
|
+
* lines of one colour cross the gradient's line squarely, and go where the
|
|
729
|
+
* matrix takes *them*, so the new line is the one square to those, as long
|
|
730
|
+
* as takes it from the start's line of colour to the end's.
|
|
731
|
+
*/
|
|
732
|
+
function transformedLine([a, b, c, d, e, f], x1, y1, x2, y2) {
|
|
733
|
+
const px = a * x1 + c * y1 + e;
|
|
734
|
+
const py = b * x1 + d * y1 + f;
|
|
735
|
+
const qx = a * x2 + c * y2 + e;
|
|
736
|
+
const qy = b * x2 + d * y2 + f;
|
|
737
|
+
// a line of one colour runs square to the gradient's, (-dy, dx)
|
|
738
|
+
const dx = x2 - x1;
|
|
739
|
+
const dy = y2 - y1;
|
|
740
|
+
const lx = a * -dy + c * dx;
|
|
741
|
+
const ly = b * -dy + d * dx;
|
|
742
|
+
// the new line is square to that, (ly, -lx), and as long as (q - p) is
|
|
743
|
+
// along it
|
|
744
|
+
const n2 = lx * lx + ly * ly;
|
|
745
|
+
if (!(n2 > 0)) return [px, py, qx, qy];
|
|
746
|
+
const along = ((qx - px) * ly + (qy - py) * -lx) / n2;
|
|
747
|
+
return [px, py, px + ly * along, py - lx * along];
|
|
748
|
+
}
|
|
749
|
+
|
|
630
750
|
function rgbaWithAlpha(color, alpha) {
|
|
631
751
|
// cheap path for #rrggbb / #rgb; anything else goes through rgba() string
|
|
632
752
|
if (/^#([0-9a-f]{6})$/i.test(color)) {
|