@openpresentation/opf-render 0.11.8 → 0.12.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/README.md +139 -9
- package/dist/annotations.js +45 -0
- package/dist/chart-options.js +177 -0
- package/dist/charts.js +194 -36
- package/dist/color-fonts.js +32 -0
- package/dist/deck-runtime.js +343 -0
- package/dist/element-define.d.ts +1 -0
- package/dist/element-define.js +5 -0
- package/dist/element.d.ts +86 -0
- package/dist/element.js +521 -0
- package/dist/export-browser.d.ts +26 -0
- package/dist/export-browser.js +228 -0
- package/dist/font-manifest.js +158 -0
- package/dist/font-policy.js +232 -16
- package/dist/fonts.d.ts +30 -0
- package/dist/fonts.js +48 -22
- package/dist/index.d.ts +105 -2
- package/dist/pdf-fonts.js +333 -0
- package/dist/pdf-style.js +429 -0
- package/dist/pdf-subset.js +176 -0
- package/dist/pdf-text.js +338 -0
- package/dist/pdf-vector.js +1249 -0
- package/dist/pdf-writer.js +154 -0
- package/dist/pdf-xml.js +78 -0
- package/dist/player.d.ts +79 -0
- package/dist/player.js +706 -0
- package/dist/preview-fonts-cli.js +15 -0
- package/dist/preview-fonts-node.d.ts +14 -0
- package/dist/preview-fonts-node.js +86 -0
- package/dist/preview-fonts.d.ts +23 -0
- package/dist/preview-fonts.js +78 -0
- package/dist/raster-images.js +19 -6
- package/dist/raster-text.js +468 -0
- package/dist/raster.js +81 -12
- package/dist/script-font-pack.js +19 -1
- package/dist/script-fonts.js +214 -23
- package/dist/sha256.js +47 -0
- package/dist/svg.d.ts +14 -1
- package/dist/svg.js +457 -73
- package/dist/symbol-encodings.js +7 -0
- package/dist/symbol-fonts.js +96 -0
- package/package.json +84 -7
|
@@ -0,0 +1,429 @@
|
|
|
1
|
+
// SVG value parsing for the vector PDF export (RR-12): colors, lengths, transforms, path data and the
|
|
2
|
+
// presentation-attribute cascade. Only what the renderer's SVG (and ordinary hand-written SVG) uses.
|
|
3
|
+
|
|
4
|
+
export const IDENTITY = [1, 0, 0, 1, 0, 0];
|
|
5
|
+
|
|
6
|
+
/** parent ∘ child: the matrix that applies `child` first, then `parent` (SVG convention). */
|
|
7
|
+
export function multiply(parent, child) {
|
|
8
|
+
const [pa, pb, pc, pd, pe, pf] = parent, [ca, cb, cc, cd, ce, cf] = child;
|
|
9
|
+
return [pa * ca + pc * cb, pb * ca + pd * cb, pa * cc + pc * cd, pb * cc + pd * cd, pa * ce + pc * cf + pe, pb * ce + pd * cf + pf];
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function invert([a, b, c, d, e, f]) {
|
|
13
|
+
const det = a * d - b * c;
|
|
14
|
+
if (!det || !Number.isFinite(det)) return null;
|
|
15
|
+
return [d / det, -b / det, -c / det, a / det, (c * f - d * e) / det, (b * e - a * f) / det];
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function applyPoint([a, b, c, d, e, f], x, y) {
|
|
19
|
+
return [a * x + c * y + e, b * x + d * y + f];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function parseTransform(value) {
|
|
23
|
+
let matrix = IDENTITY;
|
|
24
|
+
if (!value) return matrix;
|
|
25
|
+
for (const match of String(value).matchAll(/([a-zA-Z]+)\s*\(([^)]*)\)/g)) {
|
|
26
|
+
const args = match[2].trim().split(/[\s,]+/).filter(Boolean).map(Number);
|
|
27
|
+
if (args.some((item) => !Number.isFinite(item))) continue;
|
|
28
|
+
let next;
|
|
29
|
+
switch (match[1]) {
|
|
30
|
+
case "matrix": if (args.length === 6) next = args; break;
|
|
31
|
+
case "translate": next = [1, 0, 0, 1, args[0] ?? 0, args[1] ?? 0]; break;
|
|
32
|
+
case "scale": next = [args[0] ?? 1, 0, 0, args[1] ?? args[0] ?? 1, 0, 0]; break;
|
|
33
|
+
case "rotate": {
|
|
34
|
+
const angle = (args[0] ?? 0) * Math.PI / 180, cos = Math.cos(angle), sin = Math.sin(angle);
|
|
35
|
+
next = [cos, sin, -sin, cos, 0, 0];
|
|
36
|
+
if (args.length >= 3) next = multiply(multiply([1, 0, 0, 1, args[1], args[2]], next), [1, 0, 0, 1, -args[1], -args[2]]);
|
|
37
|
+
break;
|
|
38
|
+
}
|
|
39
|
+
case "skewX": next = [1, 0, Math.tan((args[0] ?? 0) * Math.PI / 180), 1, 0, 0]; break;
|
|
40
|
+
case "skewY": next = [1, Math.tan((args[0] ?? 0) * Math.PI / 180), 0, 1, 0, 0]; break;
|
|
41
|
+
default: break;
|
|
42
|
+
}
|
|
43
|
+
if (next) matrix = multiply(matrix, next);
|
|
44
|
+
}
|
|
45
|
+
return matrix;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const NAMED = { "aliceblue":"f0f8ff", "antiquewhite":"faebd7", "aqua":"00ffff", "aquamarine":"7fffd4", "azure":"f0ffff", "beige":"f5f5dc", "bisque":"ffe4c4", "black":"000000", "blanchedalmond":"ffebcd", "blue":"0000ff", "blueviolet":"8a2be2", "brown":"a52a2a", "burlywood":"deb887", "cadetblue":"5f9ea0", "chartreuse":"7fff00", "chocolate":"d2691e", "coral":"ff7f50", "cornflowerblue":"6495ed", "cornsilk":"fff8dc", "crimson":"dc143c", "cyan":"00ffff", "darkblue":"00008b", "darkcyan":"008b8b", "darkgoldenrod":"b8860b", "darkgray":"a9a9a9", "darkgreen":"006400", "darkgrey":"a9a9a9", "darkkhaki":"bdb76b", "darkmagenta":"8b008b", "darkolivegreen":"556b2f", "darkorange":"ff8c00", "darkorchid":"9932cc", "darkred":"8b0000", "darksalmon":"e9967a", "darkseagreen":"8fbc8f", "darkslateblue":"483d8b", "darkslategray":"2f4f4f", "darkslategrey":"2f4f4f", "darkturquoise":"00ced1", "darkviolet":"9400d3", "deeppink":"ff1493", "deepskyblue":"00bfff", "dimgray":"696969", "dimgrey":"696969", "dodgerblue":"1e90ff", "firebrick":"b22222", "floralwhite":"fffaf0", "forestgreen":"228b22", "fuchsia":"ff00ff", "gainsboro":"dcdcdc", "ghostwhite":"f8f8ff", "gold":"ffd700", "goldenrod":"daa520", "gray":"808080", "green":"008000", "greenyellow":"adff2f", "grey":"808080", "honeydew":"f0fff0", "hotpink":"ff69b4", "indianred":"cd5c5c", "indigo":"4b0082", "ivory":"fffff0", "khaki":"f0e68c", "lavender":"e6e6fa", "lavenderblush":"fff0f5", "lawngreen":"7cfc00", "lemonchiffon":"fffacd", "lightblue":"add8e6", "lightcoral":"f08080", "lightcyan":"e0ffff", "lightgoldenrodyellow":"fafad2", "lightgray":"d3d3d3", "lightgreen":"90ee90", "lightgrey":"d3d3d3", "lightpink":"ffb6c1", "lightsalmon":"ffa07a", "lightseagreen":"20b2aa", "lightskyblue":"87cefa", "lightslategray":"778899", "lightslategrey":"778899", "lightsteelblue":"b0c4de", "lightyellow":"ffffe0", "lime":"00ff00", "limegreen":"32cd32", "linen":"faf0e6", "magenta":"ff00ff", "maroon":"800000", "mediumaquamarine":"66cdaa", "mediumblue":"0000cd", "mediumorchid":"ba55d3", "mediumpurple":"9370db", "mediumseagreen":"3cb371", "mediumslateblue":"7b68ee", "mediumspringgreen":"00fa9a", "mediumturquoise":"48d1cc", "mediumvioletred":"c71585", "midnightblue":"191970", "mintcream":"f5fffa", "mistyrose":"ffe4e1", "moccasin":"ffe4b5", "navajowhite":"ffdead", "navy":"000080", "oldlace":"fdf5e6", "olive":"808000", "olivedrab":"6b8e23", "orange":"ffa500", "orangered":"ff4500", "orchid":"da70d6", "palegoldenrod":"eee8aa", "palegreen":"98fb98", "paleturquoise":"afeeee", "palevioletred":"db7093", "papayawhip":"ffefd5", "peachpuff":"ffdab9", "peru":"cd853f", "pink":"ffc0cb", "plum":"dda0dd", "powderblue":"b0e0e6", "purple":"800080", "rebeccapurple":"663399", "red":"ff0000", "rosybrown":"bc8f8f", "royalblue":"4169e1", "saddlebrown":"8b4513", "salmon":"fa8072", "sandybrown":"f4a460", "seagreen":"2e8b57", "seashell":"fff5ee", "sienna":"a0522d", "silver":"c0c0c0", "skyblue":"87ceeb", "slateblue":"6a5acd", "slategray":"708090", "slategrey":"708090", "snow":"fffafa", "springgreen":"00ff7f", "steelblue":"4682b4", "tan":"d2b48c", "teal":"008080", "thistle":"d8bfd8", "tomato":"ff6347", "turquoise":"40e0d0", "violet":"ee82ee", "wheat":"f5deb3", "white":"ffffff", "whitesmoke":"f5f5f5", "yellow":"ffff00", "yellowgreen":"9acd32", "transparent":"00000000" };
|
|
49
|
+
|
|
50
|
+
/** A color as {r, g, b, a} in 0..1, `null` for none, or `undefined` when the text is not a color this reader knows. */
|
|
51
|
+
export function parseColor(value, current) {
|
|
52
|
+
if (value === undefined || value === null) return undefined;
|
|
53
|
+
const text = String(value).trim().toLowerCase();
|
|
54
|
+
if (text === "none") return null;
|
|
55
|
+
if (text === "currentcolor") return current;
|
|
56
|
+
let match = /^#([0-9a-f]{3,8})$/.exec(text);
|
|
57
|
+
if (match) {
|
|
58
|
+
let digits = match[1];
|
|
59
|
+
if (digits.length === 3 || digits.length === 4) digits = [...digits].map((digit) => digit + digit).join("");
|
|
60
|
+
if (digits.length !== 6 && digits.length !== 8) return undefined;
|
|
61
|
+
const channel = (index) => parseInt(digits.slice(index * 2, index * 2 + 2), 16) / 255;
|
|
62
|
+
return { r: channel(0), g: channel(1), b: channel(2), a: digits.length === 8 ? channel(3) : 1 };
|
|
63
|
+
}
|
|
64
|
+
match = /^rgba?\(([^)]*)\)$/.exec(text);
|
|
65
|
+
if (match) {
|
|
66
|
+
const parts = match[1].split(/[\s,/]+/).filter(Boolean);
|
|
67
|
+
if (parts.length < 3) return undefined;
|
|
68
|
+
const channel = (part) => Math.min(1, Math.max(0, part.endsWith("%") ? parseFloat(part) / 100 : parseFloat(part) / 255));
|
|
69
|
+
const alpha = parts[3] === undefined ? 1 : Math.min(1, Math.max(0, parts[3].endsWith("%") ? parseFloat(parts[3]) / 100 : parseFloat(parts[3])));
|
|
70
|
+
const result = { r: channel(parts[0]), g: channel(parts[1]), b: channel(parts[2]), a: alpha };
|
|
71
|
+
return Object.values(result).every(Number.isFinite) ? result : undefined;
|
|
72
|
+
}
|
|
73
|
+
match = /^hsla?\(([^)]*)\)$/.exec(text);
|
|
74
|
+
if (match) {
|
|
75
|
+
const parts = match[1].split(/[\s,/]+/).filter(Boolean);
|
|
76
|
+
if (parts.length < 3) return undefined;
|
|
77
|
+
const hue = (((parseFloat(parts[0]) % 360) + 360) % 360) / 360;
|
|
78
|
+
const saturation = Math.min(1, Math.max(0, parseFloat(parts[1]) / 100)), lightness = Math.min(1, Math.max(0, parseFloat(parts[2]) / 100));
|
|
79
|
+
const alpha = parts[3] === undefined ? 1 : Math.min(1, Math.max(0, parts[3].endsWith("%") ? parseFloat(parts[3]) / 100 : parseFloat(parts[3])));
|
|
80
|
+
const q = lightness < 0.5 ? lightness * (1 + saturation) : lightness + saturation - lightness * saturation, p = 2 * lightness - q;
|
|
81
|
+
const channel = (t) => {
|
|
82
|
+
t = (t + 1) % 1;
|
|
83
|
+
return t < 1 / 6 ? p + (q - p) * 6 * t : t < 1 / 2 ? q : t < 2 / 3 ? p + (q - p) * (2 / 3 - t) * 6 : p;
|
|
84
|
+
};
|
|
85
|
+
const result = { r: channel(hue + 1 / 3), g: channel(hue), b: channel(hue - 1 / 3), a: alpha };
|
|
86
|
+
return Object.values(result).every(Number.isFinite) ? result : undefined;
|
|
87
|
+
}
|
|
88
|
+
if (NAMED[text]) return parseColor("#" + NAMED[text]);
|
|
89
|
+
return undefined;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** A number with an optional unit as user units (px). `reference` resolves percentages, `fontSize` em. */
|
|
93
|
+
export function parseLength(value, { reference = 0, fontSize = 16 } = {}) {
|
|
94
|
+
if (value === undefined || value === null) return undefined;
|
|
95
|
+
const match = /^\s*([+-]?(?:\d+\.?\d*|\.\d+)(?:e[+-]?\d+)?)\s*(px|pt|pc|mm|cm|in|em|ex|%)?\s*$/i.exec(String(value));
|
|
96
|
+
if (!match) return undefined;
|
|
97
|
+
const number = parseFloat(match[1]);
|
|
98
|
+
switch ((match[2] ?? "").toLowerCase()) {
|
|
99
|
+
case "pt": return number * 96 / 72;
|
|
100
|
+
case "pc": return number * 16;
|
|
101
|
+
case "mm": return number * 96 / 25.4;
|
|
102
|
+
case "cm": return number * 96 / 2.54;
|
|
103
|
+
case "in": return number * 96;
|
|
104
|
+
case "em": return number * fontSize;
|
|
105
|
+
case "ex": return number * fontSize / 2;
|
|
106
|
+
case "%": return number * reference / 100;
|
|
107
|
+
default: return number;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function parseNumberList(value) {
|
|
112
|
+
if (value === undefined || value === null) return [];
|
|
113
|
+
return String(value).trim().split(/[\s,]+/).filter(Boolean).map(Number).filter(Number.isFinite);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** Inline `style="a:b;c:d"` declarations as an object. */
|
|
117
|
+
export function parseDeclarations(style) {
|
|
118
|
+
const result = {};
|
|
119
|
+
if (!style) return result;
|
|
120
|
+
for (const declaration of String(style).split(";")) {
|
|
121
|
+
const at = declaration.indexOf(":");
|
|
122
|
+
if (at > 0) result[declaration.slice(0, at).trim().toLowerCase()] = declaration.slice(at + 1).trim();
|
|
123
|
+
}
|
|
124
|
+
return result;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** The element's attributes with its style declarations merged over them (declarations win). */
|
|
128
|
+
export function attributesOf(node) {
|
|
129
|
+
if (node.__attributes) return node.__attributes;
|
|
130
|
+
const merged = { ...node.attrs, ...node.__css, ...parseDeclarations(node.attrs.style) };
|
|
131
|
+
Object.defineProperty(node, "__attributes", { value: merged, enumerable: false });
|
|
132
|
+
return merged;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// ---------------------------------------------------------------------------------------------------------------
|
|
136
|
+
// Path data
|
|
137
|
+
|
|
138
|
+
/** Parse SVG path data to absolute commands: ["M",x,y] ["L",x,y] ["C",x1,y1,x2,y2,x,y] ["Z"]. */
|
|
139
|
+
export function parsePath(data) {
|
|
140
|
+
const tokens = String(data ?? "").match(/[a-zA-Z]|[+-]?(?:\d+\.?\d*|\.\d+)(?:e[+-]?\d+)?/gi) ?? [];
|
|
141
|
+
const commands = [];
|
|
142
|
+
let index = 0, command = "", x = 0, y = 0, startX = 0, startY = 0, lastControlX = 0, lastControlY = 0, previous = "";
|
|
143
|
+
const read = () => Number(tokens[index++]);
|
|
144
|
+
const available = () => index < tokens.length && !/^[a-zA-Z]$/.test(tokens[index]);
|
|
145
|
+
const flag = () => { const token = tokens[index]; if (token.length > 1 && /^[01]/.test(token) && !token.includes(".")) { tokens[index] = token.slice(1); return Number(token[0]); } index++; return Number(token); };
|
|
146
|
+
while (index < tokens.length) {
|
|
147
|
+
if (/^[a-zA-Z]$/.test(tokens[index])) command = tokens[index++];
|
|
148
|
+
else if (!command) break;
|
|
149
|
+
const relative = command === command.toLowerCase();
|
|
150
|
+
const type = command.toUpperCase();
|
|
151
|
+
if (type === "Z") {
|
|
152
|
+
commands.push(["Z"]);
|
|
153
|
+
x = startX; y = startY; previous = "Z";
|
|
154
|
+
if (available()) break;
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
if (!available()) break;
|
|
158
|
+
const arity = { M: 2, L: 2, H: 1, V: 1, C: 6, S: 4, Q: 4, T: 2, A: 7 }[type];
|
|
159
|
+
if (!arity) break;
|
|
160
|
+
if (type === "A") {
|
|
161
|
+
const rx = read(), ry = read(), rotation = read(), large = flag(), sweep = flag();
|
|
162
|
+
let ex = read(), ey = read();
|
|
163
|
+
if (relative) { ex += x; ey += y; }
|
|
164
|
+
for (const curve of arcToCurves(x, y, rx, ry, rotation, large, sweep, ex, ey)) commands.push(curve);
|
|
165
|
+
x = ex; y = ey; previous = "A";
|
|
166
|
+
} else {
|
|
167
|
+
const values = [];
|
|
168
|
+
for (let count = 0; count < arity; count++) values.push(read());
|
|
169
|
+
if (values.some((value) => !Number.isFinite(value))) break;
|
|
170
|
+
switch (type) {
|
|
171
|
+
case "M": {
|
|
172
|
+
const px = values[0] + (relative ? x : 0), py = values[1] + (relative ? y : 0);
|
|
173
|
+
commands.push(["M", px, py]);
|
|
174
|
+
x = startX = px; y = startY = py;
|
|
175
|
+
command = relative ? "l" : "L";
|
|
176
|
+
break;
|
|
177
|
+
}
|
|
178
|
+
case "L": { x = values[0] + (relative ? x : 0); y = values[1] + (relative ? y : 0); commands.push(["L", x, y]); break; }
|
|
179
|
+
case "H": { x = values[0] + (relative ? x : 0); commands.push(["L", x, y]); break; }
|
|
180
|
+
case "V": { y = values[0] + (relative ? y : 0); commands.push(["L", x, y]); break; }
|
|
181
|
+
case "C": {
|
|
182
|
+
const c = relative ? [x + values[0], y + values[1], x + values[2], y + values[3], x + values[4], y + values[5]] : values;
|
|
183
|
+
commands.push(["C", ...c]);
|
|
184
|
+
lastControlX = c[2]; lastControlY = c[3]; x = c[4]; y = c[5];
|
|
185
|
+
break;
|
|
186
|
+
}
|
|
187
|
+
case "S": {
|
|
188
|
+
const first = previous === "C" || previous === "S" ? [2 * x - lastControlX, 2 * y - lastControlY] : [x, y];
|
|
189
|
+
const c = relative ? [x + values[0], y + values[1], x + values[2], y + values[3]] : values;
|
|
190
|
+
commands.push(["C", first[0], first[1], c[0], c[1], c[2], c[3]]);
|
|
191
|
+
lastControlX = c[0]; lastControlY = c[1]; x = c[2]; y = c[3];
|
|
192
|
+
break;
|
|
193
|
+
}
|
|
194
|
+
case "Q": {
|
|
195
|
+
const c = relative ? [x + values[0], y + values[1], x + values[2], y + values[3]] : values;
|
|
196
|
+
commands.push(quadratic(x, y, c[0], c[1], c[2], c[3]));
|
|
197
|
+
lastControlX = c[0]; lastControlY = c[1]; x = c[2]; y = c[3];
|
|
198
|
+
break;
|
|
199
|
+
}
|
|
200
|
+
case "T": {
|
|
201
|
+
const control = previous === "Q" || previous === "T" ? [2 * x - lastControlX, 2 * y - lastControlY] : [x, y];
|
|
202
|
+
const end = relative ? [x + values[0], y + values[1]] : values;
|
|
203
|
+
commands.push(quadratic(x, y, control[0], control[1], end[0], end[1]));
|
|
204
|
+
lastControlX = control[0]; lastControlY = control[1]; x = end[0]; y = end[1];
|
|
205
|
+
break;
|
|
206
|
+
}
|
|
207
|
+
default: break;
|
|
208
|
+
}
|
|
209
|
+
previous = type;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
return commands;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function quadratic(x0, y0, cx, cy, x, y) {
|
|
216
|
+
return ["C", x0 + 2 / 3 * (cx - x0), y0 + 2 / 3 * (cy - y0), x + 2 / 3 * (cx - x), y + 2 / 3 * (cy - y), x, y];
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function arcToCurves(x1, y1, rx, ry, rotationDegrees, large, sweep, x2, y2) {
|
|
220
|
+
if ((x1 === x2 && y1 === y2)) return [];
|
|
221
|
+
rx = Math.abs(rx); ry = Math.abs(ry);
|
|
222
|
+
if (!rx || !ry) return [["L", x2, y2]];
|
|
223
|
+
const phi = rotationDegrees * Math.PI / 180, cos = Math.cos(phi), sin = Math.sin(phi);
|
|
224
|
+
const dx = (x1 - x2) / 2, dy = (y1 - y2) / 2;
|
|
225
|
+
const x1p = cos * dx + sin * dy, y1p = -sin * dx + cos * dy;
|
|
226
|
+
const lambda = (x1p * x1p) / (rx * rx) + (y1p * y1p) / (ry * ry);
|
|
227
|
+
if (lambda > 1) { const scale = Math.sqrt(lambda); rx *= scale; ry *= scale; }
|
|
228
|
+
const numerator = rx * rx * ry * ry - rx * rx * y1p * y1p - ry * ry * x1p * x1p;
|
|
229
|
+
const denominator = rx * rx * y1p * y1p + ry * ry * x1p * x1p;
|
|
230
|
+
const factor = (large === sweep ? -1 : 1) * Math.sqrt(Math.max(0, numerator / denominator));
|
|
231
|
+
const cxp = factor * (rx * y1p / ry), cyp = factor * (-ry * x1p / rx);
|
|
232
|
+
const cx = cos * cxp - sin * cyp + (x1 + x2) / 2, cy = sin * cxp + cos * cyp + (y1 + y2) / 2;
|
|
233
|
+
const angle = (ux, uy, vx, vy) => Math.atan2(ux * vy - uy * vx, ux * vx + uy * vy);
|
|
234
|
+
const start = angle(1, 0, (x1p - cxp) / rx, (y1p - cyp) / ry);
|
|
235
|
+
let delta = angle((x1p - cxp) / rx, (y1p - cyp) / ry, (-x1p - cxp) / rx, (-y1p - cyp) / ry);
|
|
236
|
+
if (!sweep && delta > 0) delta -= 2 * Math.PI;
|
|
237
|
+
else if (sweep && delta < 0) delta += 2 * Math.PI;
|
|
238
|
+
const segments = Math.max(1, Math.ceil(Math.abs(delta) / (Math.PI / 2) - 1e-9));
|
|
239
|
+
const step = delta / segments, alpha = 4 / 3 * Math.tan(step / 4);
|
|
240
|
+
const curves = [];
|
|
241
|
+
let theta = start;
|
|
242
|
+
const point = (t) => [cx + rx * Math.cos(t) * cos - ry * Math.sin(t) * sin, cy + rx * Math.cos(t) * sin + ry * Math.sin(t) * cos];
|
|
243
|
+
const derivative = (t) => [-rx * Math.sin(t) * cos - ry * Math.cos(t) * sin, -rx * Math.sin(t) * sin + ry * Math.cos(t) * cos];
|
|
244
|
+
for (let count = 0; count < segments; count++) {
|
|
245
|
+
const next = theta + step;
|
|
246
|
+
const p1 = point(theta), p2 = point(next), d1 = derivative(theta), d2 = derivative(next);
|
|
247
|
+
curves.push(["C", p1[0] + alpha * d1[0], p1[1] + alpha * d1[1], p2[0] - alpha * d2[0], p2[1] - alpha * d2[1], count === segments - 1 ? x2 : p2[0], count === segments - 1 ? y2 : p2[1]]);
|
|
248
|
+
theta = next;
|
|
249
|
+
}
|
|
250
|
+
return curves;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/** Path commands for a basic shape element, or null. */
|
|
254
|
+
/** Percentages in shape attributes are of the viewport: width for x-like keys, height for y-like keys, the normalized diagonal for r. */
|
|
255
|
+
export function shapeToPath(node, attrs, resolve, viewport = { w: 0, h: 0 }) {
|
|
256
|
+
const axis = { x: viewport.w, cx: viewport.w, x1: viewport.w, x2: viewport.w, width: viewport.w, rx: viewport.w, y: viewport.h, cy: viewport.h, y1: viewport.h, y2: viewport.h, height: viewport.h, ry: viewport.h, r: Math.sqrt((viewport.w ** 2 + viewport.h ** 2) / 2) };
|
|
257
|
+
const length = (key) => resolve(attrs[key], axis[key]) ?? 0;
|
|
258
|
+
switch (node.name) {
|
|
259
|
+
case "rect": {
|
|
260
|
+
const x = length("x"), y = length("y"), width = length("width"), height = length("height");
|
|
261
|
+
if (!(width > 0 && height > 0)) return null;
|
|
262
|
+
let rx = attrs.rx === undefined ? undefined : resolve(attrs.rx, axis.rx), ry = attrs.ry === undefined ? undefined : resolve(attrs.ry, axis.ry);
|
|
263
|
+
if (rx === undefined && ry === undefined) { rx = ry = 0; } else { rx ??= ry; ry ??= rx; }
|
|
264
|
+
rx = Math.min(Math.max(rx, 0), width / 2); ry = Math.min(Math.max(ry, 0), height / 2);
|
|
265
|
+
if (!rx || !ry) return [["M", x, y], ["L", x + width, y], ["L", x + width, y + height], ["L", x, y + height], ["Z"]];
|
|
266
|
+
const k = 0.5522847498;
|
|
267
|
+
return [
|
|
268
|
+
["M", x + rx, y], ["L", x + width - rx, y],
|
|
269
|
+
["C", x + width - rx + rx * k, y, x + width, y + ry - ry * k, x + width, y + ry], ["L", x + width, y + height - ry],
|
|
270
|
+
["C", x + width, y + height - ry + ry * k, x + width - rx + rx * k, y + height, x + width - rx, y + height], ["L", x + rx, y + height],
|
|
271
|
+
["C", x + rx - rx * k, y + height, x, y + height - ry + ry * k, x, y + height - ry], ["L", x, y + ry],
|
|
272
|
+
["C", x, y + ry - ry * k, x + rx - rx * k, y, x + rx, y], ["Z"],
|
|
273
|
+
];
|
|
274
|
+
}
|
|
275
|
+
case "circle":
|
|
276
|
+
case "ellipse": {
|
|
277
|
+
const cx = length("cx"), cy = length("cy");
|
|
278
|
+
const rx = node.name === "circle" ? length("r") : length("rx"), ry = node.name === "circle" ? rx : length("ry");
|
|
279
|
+
if (!(rx > 0 && ry > 0)) return null;
|
|
280
|
+
const k = 0.5522847498;
|
|
281
|
+
return [
|
|
282
|
+
["M", cx + rx, cy], ["C", cx + rx, cy + ry * k, cx + rx * k, cy + ry, cx, cy + ry], ["C", cx - rx * k, cy + ry, cx - rx, cy + ry * k, cx - rx, cy],
|
|
283
|
+
["C", cx - rx, cy - ry * k, cx - rx * k, cy - ry, cx, cy - ry], ["C", cx + rx * k, cy - ry, cx + rx, cy - ry * k, cx + rx, cy], ["Z"],
|
|
284
|
+
];
|
|
285
|
+
}
|
|
286
|
+
case "line": return [["M", length("x1"), length("y1")], ["L", length("x2"), length("y2")]];
|
|
287
|
+
case "polyline":
|
|
288
|
+
case "polygon": {
|
|
289
|
+
const points = parseNumberList(attrs.points);
|
|
290
|
+
if (points.length < 4) return null;
|
|
291
|
+
const commands = [];
|
|
292
|
+
for (let index = 0; index + 1 < points.length; index += 2) commands.push([index ? "L" : "M", points[index], points[index + 1]]);
|
|
293
|
+
if (node.name === "polygon") commands.push(["Z"]);
|
|
294
|
+
return commands;
|
|
295
|
+
}
|
|
296
|
+
case "path": {
|
|
297
|
+
const commands = parsePath(attrs.d);
|
|
298
|
+
return commands.length ? commands : null;
|
|
299
|
+
}
|
|
300
|
+
default: return null;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
export function transformPath(commands, matrix) {
|
|
305
|
+
return commands.map(([type, ...values]) => {
|
|
306
|
+
const out = [type];
|
|
307
|
+
for (let index = 0; index < values.length; index += 2) out.push(...applyPoint(matrix, values[index], values[index + 1]));
|
|
308
|
+
return out;
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
export function pathBounds(commands) {
|
|
313
|
+
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
|
|
314
|
+
for (const [, ...values] of commands) {
|
|
315
|
+
for (let index = 0; index + 1 < values.length; index += 2) {
|
|
316
|
+
minX = Math.min(minX, values[index]); maxX = Math.max(maxX, values[index]);
|
|
317
|
+
minY = Math.min(minY, values[index + 1]); maxY = Math.max(maxY, values[index + 1]);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
return Number.isFinite(minX) ? { x: minX, y: minY, width: maxX - minX, height: maxY - minY } : null;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// ---------------------------------------------------------------------------------------------------------------
|
|
324
|
+
// Style cascade
|
|
325
|
+
|
|
326
|
+
const INHERITED = ["fill", "fill-opacity", "fill-rule", "stroke", "stroke-opacity", "stroke-width", "stroke-linecap", "stroke-linejoin",
|
|
327
|
+
"stroke-miterlimit", "stroke-dasharray", "stroke-dashoffset", "font-family", "font-size", "font-style", "font-weight", "text-anchor", "direction",
|
|
328
|
+
"color", "letter-spacing", "word-spacing", "visibility", "white-space", "xml:space", "lang", "xml:lang", "font-variant-ligatures", "font-feature-settings"];
|
|
329
|
+
|
|
330
|
+
export const ROOT_STYLE = Object.freeze({
|
|
331
|
+
fill: "#000000", "fill-opacity": "1", "fill-rule": "nonzero", stroke: "none", "stroke-opacity": "1", "stroke-width": "1",
|
|
332
|
+
"stroke-linecap": "butt", "stroke-linejoin": "miter", "stroke-miterlimit": "4", "font-size": "16", "font-weight": "400", "font-style": "normal",
|
|
333
|
+
"text-anchor": "start", color: "#000000", visibility: "visible", direction: "ltr",
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
/** The inherited properties of `node` over its parent's. */
|
|
337
|
+
export function inheritStyle(parentStyle, attrs) {
|
|
338
|
+
const style = { ...parentStyle };
|
|
339
|
+
for (const key of INHERITED) {
|
|
340
|
+
if (attrs[key] !== undefined && attrs[key] !== "inherit") style[key] = attrs[key];
|
|
341
|
+
}
|
|
342
|
+
if (attrs["font-size"] !== undefined && attrs["font-size"] !== "inherit") {
|
|
343
|
+
const parentSize = parseFloat(parentStyle["font-size"]) || 16;
|
|
344
|
+
const named = { "xx-small": 9, "x-small": 10, small: 13, medium: 16, large: 18, "x-large": 24, "xx-large": 32 }[String(attrs["font-size"]).trim()];
|
|
345
|
+
style["font-size"] = String(named ?? parseLength(attrs["font-size"], { reference: parentSize, fontSize: parentSize }) ?? parentSize);
|
|
346
|
+
}
|
|
347
|
+
if (attrs["font-weight"] !== undefined) {
|
|
348
|
+
const parentWeight = parseFontWeight(parentStyle["font-weight"], 400);
|
|
349
|
+
style["font-weight"] = String(parseFontWeight(attrs["font-weight"], parentWeight));
|
|
350
|
+
}
|
|
351
|
+
return style;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
export function parseFontWeight(value, parent = 400) {
|
|
355
|
+
const text = String(value ?? "").trim().toLowerCase();
|
|
356
|
+
if (text === "normal") return 400;
|
|
357
|
+
if (text === "bold") return 700;
|
|
358
|
+
if (text === "bolder") return parent < 400 ? 400 : parent < 600 ? 700 : 900;
|
|
359
|
+
if (text === "lighter") return parent > 700 ? 700 : parent > 500 ? 400 : 100;
|
|
360
|
+
const number = Number(text);
|
|
361
|
+
return Number.isFinite(number) && number >= 1 && number <= 1000 ? number : parent;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
export function parseFontFamilies(value) {
|
|
365
|
+
if (!value) return [];
|
|
366
|
+
return String(value).split(",").map((item) => item.trim().replace(/^["']|["']$/g, "")).filter(Boolean);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// ---------------------------------------------------------------------------------------------------------------
|
|
370
|
+
// Style sheets: type, class, id and universal selectors, comma lists (what generator output uses). Anything else in a
|
|
371
|
+
// selector (combinators, attribute and pseudo selectors) is reported through `unsupported` and the rule is skipped.
|
|
372
|
+
|
|
373
|
+
/** Apply the <style> rules of an SVG tree: each matching element gets `__css`, merged by `attributesOf` under inline styles. */
|
|
374
|
+
export function applyStyleSheets(root, unsupported) {
|
|
375
|
+
const rules = [];
|
|
376
|
+
const sheets = [];
|
|
377
|
+
const collect = (node) => {
|
|
378
|
+
if (node.name === "style" && (!node.attrs.type || /css/i.test(node.attrs.type))) sheets.push(node.children.map((child) => child.text ?? "").join(""));
|
|
379
|
+
for (const child of node.children ?? []) if (child.name) collect(child);
|
|
380
|
+
};
|
|
381
|
+
collect(root);
|
|
382
|
+
let order = 0;
|
|
383
|
+
for (const sheet of sheets) {
|
|
384
|
+
const css = sheet.replace(/\/\*[\s\S]*?\*\//g, "");
|
|
385
|
+
for (const rule of css.matchAll(/([^{}]+)\{([^{}]*)\}/g)) {
|
|
386
|
+
const selectorText = rule[1].trim();
|
|
387
|
+
if (selectorText.startsWith("@")) continue;
|
|
388
|
+
const declarations = parseDeclarations(rule[2].replace(/!important/g, ""));
|
|
389
|
+
for (const raw of selectorText.split(",")) {
|
|
390
|
+
const selector = raw.trim();
|
|
391
|
+
const match = /^(\*|[A-Za-z][\w-]*)?((?:[.#][\w-]+)*)$/.exec(selector);
|
|
392
|
+
if (!match || !selector) {
|
|
393
|
+
unsupported(`The CSS selector "${selector.slice(0, 60)}" is not supported (only type, class, id and universal selectors); its rule is not applied.`);
|
|
394
|
+
continue;
|
|
395
|
+
}
|
|
396
|
+
const parts = [...match[2].matchAll(/([.#])([\w-]+)/g)].map((part) => ({ kind: part[1], name: part[2] }));
|
|
397
|
+
const specificity = (match[1] && match[1] !== "*" ? 1 : 0) + parts.filter((part) => part.kind === ".").length * 10 + parts.filter((part) => part.kind === "#").length * 100;
|
|
398
|
+
rules.push({ tag: match[1], parts, specificity, order: order++, declarations });
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
if (!rules.length) return;
|
|
403
|
+
rules.sort((a, b) => a.specificity - b.specificity || a.order - b.order);
|
|
404
|
+
const visit = (node) => {
|
|
405
|
+
if (node.name) {
|
|
406
|
+
const classes = (node.attrs.class ?? "").split(/\s+/).filter(Boolean);
|
|
407
|
+
let merged;
|
|
408
|
+
for (const rule of rules) {
|
|
409
|
+
if (rule.tag && rule.tag !== "*" && rule.tag !== node.name) continue;
|
|
410
|
+
if (!rule.parts.every((part) => (part.kind === "." ? classes.includes(part.name) : node.attrs.id === part.name))) continue;
|
|
411
|
+
merged = { ...merged, ...rule.declarations };
|
|
412
|
+
}
|
|
413
|
+
if (merged) Object.defineProperty(node, "__css", { value: merged, enumerable: false });
|
|
414
|
+
}
|
|
415
|
+
for (const child of node.children ?? []) visit(child);
|
|
416
|
+
};
|
|
417
|
+
visit(root);
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
/** The matrix mapping a viewBox [minX, minY, width, height] into the viewport (x, y, width, height) under preserveAspectRatio. */
|
|
421
|
+
export function viewBoxTransform(viewBox, preserveAspectRatio, x, y, width, height) {
|
|
422
|
+
const [minX, minY, boxWidth, boxHeight] = viewBox;
|
|
423
|
+
const align = String(preserveAspectRatio ?? "xMidYMid meet").trim().split(/\s+/);
|
|
424
|
+
if (align[0] === "none") return [width / boxWidth, 0, 0, height / boxHeight, x - minX * width / boxWidth, y - minY * height / boxHeight];
|
|
425
|
+
const slice = align[1] === "slice";
|
|
426
|
+
const scale = slice ? Math.max(width / boxWidth, height / boxHeight) : Math.min(width / boxWidth, height / boxHeight);
|
|
427
|
+
const alignX = /xMin/.test(align[0]) ? 0 : /xMax/.test(align[0]) ? 1 : 0.5, alignY = /YMin/.test(align[0]) ? 0 : /YMax/.test(align[0]) ? 1 : 0.5;
|
|
428
|
+
return [scale, 0, 0, scale, x - minX * scale + (width - boxWidth * scale) * alignX, y - minY * scale + (height - boxHeight * scale) * alignY];
|
|
429
|
+
}
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
// A deterministic TrueType (glyf) subsetter for the vector PDF export (RR-12). It keeps the glyphs a document
|
|
2
|
+
// uses (plus the components of composite glyphs and .notdef), renumbers them in ascending original order, and
|
|
3
|
+
// writes head, hhea, maxp, hmtx, loca, glyf plus the original cvt/fpgm/prep when present, so the glyph
|
|
4
|
+
// programs keep running. Every table is derived from the font bytes and the glyph set only.
|
|
5
|
+
|
|
6
|
+
const TABLE_ORDER_ERROR = "Not a TrueType (glyf) font.";
|
|
7
|
+
|
|
8
|
+
function readTables(data) {
|
|
9
|
+
const view = new DataView(data.buffer, data.byteOffset, data.byteLength);
|
|
10
|
+
const signature = view.getUint32(0);
|
|
11
|
+
if (signature !== 0x00010000 && signature !== 0x74727565) throw new Error(TABLE_ORDER_ERROR);
|
|
12
|
+
const count = view.getUint16(4);
|
|
13
|
+
const tables = new Map();
|
|
14
|
+
for (let index = 0; index < count; index++) {
|
|
15
|
+
const at = 12 + index * 16;
|
|
16
|
+
const tag = String.fromCharCode(data[at], data[at + 1], data[at + 2], data[at + 3]);
|
|
17
|
+
tables.set(tag, { offset: view.getUint32(at + 8), length: view.getUint32(at + 12) });
|
|
18
|
+
}
|
|
19
|
+
for (const required of ["head", "hhea", "maxp", "hmtx", "loca", "glyf"]) if (!tables.has(required)) throw new Error(TABLE_ORDER_ERROR);
|
|
20
|
+
// Every table must lie inside the file and be long enough for what is read from it (corrupt fonts must fail cleanly).
|
|
21
|
+
for (const [tag, { offset, length }] of tables) if (offset > data.length || offset + length > data.length) throw new Error(`Font table ${tag} lies outside the file.`);
|
|
22
|
+
const minimum = { head: 54, hhea: 36, maxp: 6 };
|
|
23
|
+
for (const [tag, size] of Object.entries(minimum)) if (tables.get(tag).length < size) throw new Error(`Font table ${tag} is truncated.`);
|
|
24
|
+
return { view, tables };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Whether `data` is an sfnt with TrueType outlines (the only outline format this subsetter writes). */
|
|
28
|
+
export function isTrueTypeOutlines(data) {
|
|
29
|
+
if (data.length < 12) return false;
|
|
30
|
+
const signature = new DataView(data.buffer, data.byteOffset, data.byteLength).getUint32(0);
|
|
31
|
+
return signature === 0x00010000 || signature === 0x74727565;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function subsetTrueType(data, wantedGlyphs) {
|
|
35
|
+
const { view, tables } = readTables(data);
|
|
36
|
+
const head = tables.get("head"), hhea = tables.get("hhea"), maxp = tables.get("maxp");
|
|
37
|
+
const hmtx = tables.get("hmtx"), loca = tables.get("loca"), glyf = tables.get("glyf");
|
|
38
|
+
const longLoca = view.getInt16(head.offset + 50) !== 0;
|
|
39
|
+
const numGlyphs = view.getUint16(maxp.offset + 4);
|
|
40
|
+
const metricCount = view.getUint16(hhea.offset + 34);
|
|
41
|
+
if (numGlyphs < 1 || metricCount < 1 || metricCount > numGlyphs) throw new Error("Font glyph counts are inconsistent.");
|
|
42
|
+
if (loca.length < (numGlyphs + 1) * (longLoca ? 4 : 2) || hmtx.length < metricCount * 4) throw new Error("Font loca or hmtx table is truncated.");
|
|
43
|
+
const glyphRange = (id) => {
|
|
44
|
+
if (id >= numGlyphs) return [0, 0];
|
|
45
|
+
const at = loca.offset + id * (longLoca ? 4 : 2);
|
|
46
|
+
const range = longLoca
|
|
47
|
+
? [view.getUint32(at), view.getUint32(at + 4)]
|
|
48
|
+
: [view.getUint16(at) * 2, view.getUint16(at + 2) * 2];
|
|
49
|
+
if (range[1] < range[0] || range[1] > glyf.length) throw new Error(`Font glyph ${id} lies outside the glyf table.`);
|
|
50
|
+
return range;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
// Close over composite glyph components.
|
|
54
|
+
const included = new Set();
|
|
55
|
+
const pending = [0, ...wantedGlyphs];
|
|
56
|
+
const components = (id) => {
|
|
57
|
+
const [start, end] = glyphRange(id);
|
|
58
|
+
if (end - start < 10) return [];
|
|
59
|
+
const base = glyf.offset + start;
|
|
60
|
+
if (view.getInt16(base) >= 0) return [];
|
|
61
|
+
const found = [];
|
|
62
|
+
let at = base + 10;
|
|
63
|
+
for (;;) {
|
|
64
|
+
const flags = view.getUint16(at), component = view.getUint16(at + 2);
|
|
65
|
+
found.push({ at: at + 2, id: component });
|
|
66
|
+
at += 4 + (flags & 0x0001 ? 4 : 2);
|
|
67
|
+
if (flags & 0x0008) at += 2; else if (flags & 0x0040) at += 4; else if (flags & 0x0080) at += 8;
|
|
68
|
+
if (!(flags & 0x0020)) break;
|
|
69
|
+
}
|
|
70
|
+
return found;
|
|
71
|
+
};
|
|
72
|
+
while (pending.length) {
|
|
73
|
+
const id = pending.pop();
|
|
74
|
+
if (id >= numGlyphs || included.has(id)) continue;
|
|
75
|
+
included.add(id);
|
|
76
|
+
for (const component of components(id)) pending.push(component.id);
|
|
77
|
+
}
|
|
78
|
+
const order = [...included].sort((a, b) => a - b);
|
|
79
|
+
const mapping = new Map(order.map((id, index) => [id, index]));
|
|
80
|
+
|
|
81
|
+
// glyf and loca (always the long format).
|
|
82
|
+
const glyphChunks = [];
|
|
83
|
+
const newLoca = new Uint8Array((order.length + 1) * 4);
|
|
84
|
+
const locaView = new DataView(newLoca.buffer);
|
|
85
|
+
let glyfLength = 0;
|
|
86
|
+
order.forEach((id, index) => {
|
|
87
|
+
locaView.setUint32(index * 4, glyfLength);
|
|
88
|
+
const [start, end] = glyphRange(id);
|
|
89
|
+
const length = end - start;
|
|
90
|
+
if (length > 0) {
|
|
91
|
+
const bytes = data.slice(glyf.offset + start, glyf.offset + end);
|
|
92
|
+
const glyphView = new DataView(bytes.buffer);
|
|
93
|
+
if (length >= 10 && glyphView.getInt16(0) < 0) {
|
|
94
|
+
for (const component of components(id)) glyphView.setUint16(component.at - (glyf.offset + start), mapping.get(component.id));
|
|
95
|
+
}
|
|
96
|
+
const padded = new Uint8Array((length + 3) & ~3);
|
|
97
|
+
padded.set(bytes);
|
|
98
|
+
glyphChunks.push(padded);
|
|
99
|
+
glyfLength += padded.length;
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
locaView.setUint32(order.length * 4, glyfLength);
|
|
103
|
+
const newGlyf = new Uint8Array(glyfLength);
|
|
104
|
+
let position = 0;
|
|
105
|
+
for (const chunk of glyphChunks) { newGlyf.set(chunk, position); position += chunk.length; }
|
|
106
|
+
|
|
107
|
+
// hmtx: one full (advance, lsb) record per kept glyph.
|
|
108
|
+
const newHmtx = new Uint8Array(order.length * 4);
|
|
109
|
+
const hmtxView = new DataView(newHmtx.buffer);
|
|
110
|
+
order.forEach((id, index) => {
|
|
111
|
+
const advance = hmtx.length >= 4 ? view.getUint16(hmtx.offset + Math.min(id, metricCount - 1) * 4) : 0;
|
|
112
|
+
const side = id < metricCount
|
|
113
|
+
? view.getInt16(hmtx.offset + id * 4 + 2)
|
|
114
|
+
: view.getInt16(hmtx.offset + metricCount * 4 + (id - metricCount) * 2);
|
|
115
|
+
hmtxView.setUint16(index * 4, advance);
|
|
116
|
+
hmtxView.setInt16(index * 4 + 2, side);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
const newHead = data.slice(head.offset, head.offset + head.length);
|
|
120
|
+
const newHeadView = new DataView(newHead.buffer);
|
|
121
|
+
newHeadView.setUint32(8, 0); // checkSumAdjustment, filled in once the file is complete
|
|
122
|
+
newHeadView.setInt16(50, 1);
|
|
123
|
+
const newHhea = data.slice(hhea.offset, hhea.offset + hhea.length);
|
|
124
|
+
new DataView(newHhea.buffer).setUint16(34, order.length);
|
|
125
|
+
const newMaxp = data.slice(maxp.offset, maxp.offset + maxp.length);
|
|
126
|
+
new DataView(newMaxp.buffer).setUint16(4, order.length);
|
|
127
|
+
|
|
128
|
+
const output = [["head", newHead], ["hhea", newHhea], ["maxp", newMaxp], ["hmtx", newHmtx], ["loca", newLoca], ["glyf", newGlyf]];
|
|
129
|
+
for (const optional of ["cvt ", "fpgm", "prep"]) {
|
|
130
|
+
const table = tables.get(optional);
|
|
131
|
+
if (table) output.push([optional, data.slice(table.offset, table.offset + table.length)]);
|
|
132
|
+
}
|
|
133
|
+
output.sort((a, b) => (a[0] < b[0] ? -1 : 1));
|
|
134
|
+
return { data: assemble(output), mapping };
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function assemble(tables) {
|
|
138
|
+
const count = tables.length;
|
|
139
|
+
let searchRange = 1, entrySelector = 0;
|
|
140
|
+
while (searchRange * 2 <= count) { searchRange *= 2; entrySelector++; }
|
|
141
|
+
searchRange *= 16;
|
|
142
|
+
let offset = 12 + count * 16;
|
|
143
|
+
const records = tables.map(([tag, bytes]) => {
|
|
144
|
+
const record = { tag, bytes, offset, checksum: checksum(bytes) };
|
|
145
|
+
offset += (bytes.length + 3) & ~3;
|
|
146
|
+
return record;
|
|
147
|
+
});
|
|
148
|
+
const out = new Uint8Array(offset);
|
|
149
|
+
const view = new DataView(out.buffer);
|
|
150
|
+
view.setUint32(0, 0x00010000);
|
|
151
|
+
view.setUint16(4, count);
|
|
152
|
+
view.setUint16(6, searchRange);
|
|
153
|
+
view.setUint16(8, entrySelector);
|
|
154
|
+
view.setUint16(10, count * 16 - searchRange);
|
|
155
|
+
records.forEach((record, index) => {
|
|
156
|
+
const at = 12 + index * 16;
|
|
157
|
+
for (let i = 0; i < 4; i++) out[at + i] = record.tag.charCodeAt(i);
|
|
158
|
+
view.setUint32(at + 4, record.checksum);
|
|
159
|
+
view.setUint32(at + 8, record.offset);
|
|
160
|
+
view.setUint32(at + 12, record.bytes.length);
|
|
161
|
+
out.set(record.bytes, record.offset);
|
|
162
|
+
});
|
|
163
|
+
const adjustment = (0xb1b0afba - checksum(out)) >>> 0;
|
|
164
|
+
const head = records.find((record) => record.tag === "head");
|
|
165
|
+
view.setUint32(head.offset + 8, adjustment);
|
|
166
|
+
return out;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function checksum(bytes) {
|
|
170
|
+
let sum = 0;
|
|
171
|
+
const length = bytes.length;
|
|
172
|
+
for (let index = 0; index < length; index += 4) {
|
|
173
|
+
sum = (sum + (((bytes[index] ?? 0) << 24) | ((bytes[index + 1] ?? 0) << 16) | ((bytes[index + 2] ?? 0) << 8) | (bytes[index + 3] ?? 0))) >>> 0;
|
|
174
|
+
}
|
|
175
|
+
return sum >>> 0;
|
|
176
|
+
}
|