@geastack/core 0.1.39 → 0.1.41
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 +15 -0
- package/events.cpp +2 -0
- package/gea_app_entry.cpp +2 -2
- package/gea_sources.mjs +4 -3
- package/include/audio.h +11 -1
- package/include/audio_oscillator.h +52 -0
- package/include/camera.h +21 -0
- package/include/captive_dns.h +71 -0
- package/include/display_present.h +66 -28
- package/include/events.h +31 -3
- package/include/gea/embedded.h +2 -0
- package/include/graphics/font.h +19 -0
- package/include/image.h +39 -1
- package/include/input.h +12 -1
- package/include/platform/atomic_file.h +72 -0
- package/include/platform/file_cache.h +6 -3
- package/include/power.h +2 -0
- package/include/services/frame_scheduler.h +6 -0
- package/include/wifi.h +60 -45
- package/index.d.ts +229 -45
- package/package.json +3 -3
- package/runtime/audio-worklet.ts +7 -11
- package/runtime/host.ts +132 -77
- package/runtime/images.ts +5 -0
- package/runtime-surface.ts +2 -0
- package/runtime.cpp +23 -5
- package/scripts/bitmap-font-source.mjs +155 -0
- package/scripts/build-gea-vite-geatsc.mjs +97 -6
- package/scripts/gea-embedded-asset-policy.mjs +38 -0
- package/scripts/gea-embedded-compat-transform.mjs +4 -2
- package/scripts/gea-vite-module-graph-plugin.mjs +64 -15
- package/scripts/generate-gea-embedded-assets.mjs +17 -3
- package/scripts/generate-gea-embedded-fonts.mjs +986 -670
|
@@ -1,60 +1,87 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import fs from
|
|
3
|
-
import
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
import
|
|
8
|
-
import
|
|
9
|
-
import
|
|
10
|
-
|
|
11
|
-
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import {
|
|
4
|
+
buildBitmapFont,
|
|
5
|
+
loadBitmapFontSource,
|
|
6
|
+
} from "./bitmap-font-source.mjs";
|
|
7
|
+
import path from "node:path";
|
|
8
|
+
import process from "node:process";
|
|
9
|
+
import zlib from "node:zlib";
|
|
10
|
+
import { execFileSync } from "node:child_process";
|
|
11
|
+
import { createHash } from "node:crypto";
|
|
12
|
+
import { createRequire } from "node:module";
|
|
13
|
+
import { fileURLToPath } from "node:url";
|
|
14
|
+
|
|
15
|
+
const args = process.argv.slice(2);
|
|
12
16
|
|
|
13
17
|
function readOption(name) {
|
|
14
|
-
const index = args.indexOf(name)
|
|
15
|
-
return index >= 0 ? args[index + 1] : undefined
|
|
18
|
+
const index = args.indexOf(name);
|
|
19
|
+
return index >= 0 ? args[index + 1] : undefined;
|
|
16
20
|
}
|
|
17
21
|
|
|
18
22
|
function hasFlag(name) {
|
|
19
|
-
return args.includes(name)
|
|
23
|
+
return args.includes(name);
|
|
20
24
|
}
|
|
21
25
|
|
|
22
26
|
function readAllOptions(name) {
|
|
23
|
-
const values = []
|
|
27
|
+
const values = [];
|
|
24
28
|
for (let i = 0; i < args.length; i++) {
|
|
25
|
-
if (args[i] === name && i + 1 < args.length) values.push(args[i + 1])
|
|
29
|
+
if (args[i] === name && i + 1 < args.length) values.push(args[i + 1]);
|
|
26
30
|
}
|
|
27
|
-
return values
|
|
31
|
+
return values;
|
|
28
32
|
}
|
|
29
33
|
|
|
30
34
|
function fail(message) {
|
|
31
|
-
process.stderr.write(`${message}\n`)
|
|
32
|
-
process.exit(1)
|
|
35
|
+
process.stderr.write(`${message}\n`);
|
|
36
|
+
process.exit(1);
|
|
33
37
|
}
|
|
34
38
|
|
|
35
|
-
const appDir = path.resolve(
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
const
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
const
|
|
42
|
-
|
|
43
|
-
|
|
39
|
+
const appDir = path.resolve(
|
|
40
|
+
readOption("--app-dir") ?? fail("missing --app-dir <dir>"),
|
|
41
|
+
);
|
|
42
|
+
const cssDir = path.resolve(
|
|
43
|
+
readOption("--css-dir") ?? fail("missing --css-dir <dir>"),
|
|
44
|
+
);
|
|
45
|
+
const outCpp = path.resolve(
|
|
46
|
+
readOption("--out-cpp") ?? fail("missing --out-cpp <file>"),
|
|
47
|
+
);
|
|
48
|
+
const outH = path.resolve(
|
|
49
|
+
readOption("--out-h") ?? fail("missing --out-h <file>"),
|
|
50
|
+
);
|
|
51
|
+
const symbolPrefix = readOption("--symbol-prefix");
|
|
52
|
+
const viewportWidths = parseViewportWidths(readAllOptions("--viewport-width"));
|
|
53
|
+
const viewportHeights = parseViewportWidths(
|
|
54
|
+
readAllOptions("--viewport-height"),
|
|
55
|
+
);
|
|
56
|
+
const devicePixelRatios = parseDevicePixelRatios(
|
|
57
|
+
readAllOptions("--device-pixel-ratio"),
|
|
58
|
+
);
|
|
59
|
+
const runtimeTtfFonts = hasFlag("--runtime-ttf-fonts");
|
|
44
60
|
// Glyph rasterization supersampling. 4 (default) = antialiased coverage for
|
|
45
61
|
// grayscale/color panels. 1 = sample at 1:1 = NO antialiasing = crisp binary
|
|
46
62
|
// glyphs, which read better on 1-bit B/W e-paper than thresholded AA (every
|
|
47
63
|
// glyph pixel is fully on/off, so the panel's mono threshold is a no-op).
|
|
48
64
|
// Set per board by build-gea-vite-geatsc.mjs.
|
|
49
|
-
const glyphSupersample = Math.max(
|
|
65
|
+
const glyphSupersample = Math.max(
|
|
66
|
+
1,
|
|
67
|
+
Math.round(Number(readOption("--supersample") ?? "4")) || 4,
|
|
68
|
+
);
|
|
50
69
|
// Atlas pixel depth. 8 (default) = one coverage byte per pixel. 2 = four pixels
|
|
51
70
|
// per byte, quantized offline to the four levels a 4-gray e-paper panel can
|
|
52
71
|
// actually show — the atlas costs a quarter of the flash bandwidth to read for
|
|
53
72
|
// no visible loss on such a panel. Set per board by build-gea-vite-geatsc.mjs.
|
|
54
|
-
const atlasBits = Number(readOption(
|
|
55
|
-
if (atlasBits !== 2 && atlasBits !== 8)
|
|
56
|
-
|
|
57
|
-
|
|
73
|
+
const atlasBits = Number(readOption("--atlas-bits") ?? "8");
|
|
74
|
+
if (atlasBits !== 2 && atlasBits !== 8)
|
|
75
|
+
fail(`unsupported --atlas-bits ${atlasBits} (expected 2 or 8)`);
|
|
76
|
+
|
|
77
|
+
const genericFamilies = new Set([
|
|
78
|
+
"serif",
|
|
79
|
+
"sans-serif",
|
|
80
|
+
"monospace",
|
|
81
|
+
"cursive",
|
|
82
|
+
"fantasy",
|
|
83
|
+
"system-ui",
|
|
84
|
+
]);
|
|
58
85
|
|
|
59
86
|
// Baseline glyph coverage for prose and UI chrome. EPUB text routinely uses
|
|
60
87
|
// typographic quotes, dashes and ellipses even when the language is otherwise
|
|
@@ -93,13 +120,13 @@ const embeddedFontExtraCodepoints = [
|
|
|
93
120
|
0x2192, // right arrow
|
|
94
121
|
0x2193, // down arrow
|
|
95
122
|
0x21bb, // clockwise open circle arrow
|
|
96
|
-
]
|
|
123
|
+
];
|
|
97
124
|
|
|
98
125
|
function baselineCodepoints() {
|
|
99
|
-
const codepoints = []
|
|
100
|
-
for (let cp = 0x20; cp <= 0x7e; cp += 1) codepoints.push(cp)
|
|
101
|
-
codepoints.push(...embeddedFontExtraCodepoints)
|
|
102
|
-
return codepoints
|
|
126
|
+
const codepoints = [];
|
|
127
|
+
for (let cp = 0x20; cp <= 0x7e; cp += 1) codepoints.push(cp);
|
|
128
|
+
codepoints.push(...embeddedFontExtraCodepoints);
|
|
129
|
+
return codepoints;
|
|
103
130
|
}
|
|
104
131
|
|
|
105
132
|
// Every atlas carries that whole baseline, at every size the app asks for. A
|
|
@@ -116,20 +143,20 @@ function baselineCodepoints() {
|
|
|
116
143
|
// (`\2014` for an em dash) are honoured, so a stylesheet never has to carry a
|
|
117
144
|
// literal non-ASCII character to name one.
|
|
118
145
|
function parseFontCharset(value) {
|
|
119
|
-
const text = unquoteCss(value.trim())
|
|
120
|
-
if (text ===
|
|
146
|
+
const text = unquoteCss(value.trim());
|
|
147
|
+
if (text === "") return null;
|
|
121
148
|
// Space is a glyph like any other, and `?` is what the renderer substitutes
|
|
122
149
|
// for one it cannot find, so neither is ever worth leaving out.
|
|
123
|
-
const codepoints = new Set([0x20, 0x3f])
|
|
150
|
+
const codepoints = new Set([0x20, 0x3f]);
|
|
124
151
|
// A CSS escape is a backslash, up to six hex digits, and one optional space
|
|
125
152
|
// that ends the digits rather than being a character in its own right.
|
|
126
|
-
const escapeRe = /\\([0-9a-fA-F]{1,6})[ ]?|\\(.)|([\s\S])/gu
|
|
127
|
-
let escape
|
|
153
|
+
const escapeRe = /\\([0-9a-fA-F]{1,6})[ ]?|\\(.)|([\s\S])/gu;
|
|
154
|
+
let escape;
|
|
128
155
|
while ((escape = escapeRe.exec(text)) !== null) {
|
|
129
|
-
if (escape[1] !== undefined) codepoints.add(Number.parseInt(escape[1], 16))
|
|
130
|
-
else codepoints.add((escape[2] ?? escape[3]).codePointAt(0))
|
|
156
|
+
if (escape[1] !== undefined) codepoints.add(Number.parseInt(escape[1], 16));
|
|
157
|
+
else codepoints.add((escape[2] ?? escape[3]).codePointAt(0));
|
|
131
158
|
}
|
|
132
|
-
return codepoints
|
|
159
|
+
return codepoints;
|
|
133
160
|
}
|
|
134
161
|
|
|
135
162
|
// null is "unrestricted", and it wins: a size is one atlas however many rules
|
|
@@ -137,120 +164,131 @@ function parseFontCharset(value) {
|
|
|
137
164
|
// the atlas back to the baseline. Narrowing on the strength of the rules that
|
|
138
165
|
// opted in would render the other rules' text as `?`.
|
|
139
166
|
function mergeCharsets(a, b) {
|
|
140
|
-
if (a === null || b === null) return null
|
|
141
|
-
return new Set([...a, ...b])
|
|
167
|
+
if (a === null || b === null) return null;
|
|
168
|
+
return new Set([...a, ...b]);
|
|
142
169
|
}
|
|
143
170
|
|
|
144
171
|
function findCssFiles(dir) {
|
|
145
|
-
const files = []
|
|
146
|
-
const stack = [dir]
|
|
172
|
+
const files = [];
|
|
173
|
+
const stack = [dir];
|
|
147
174
|
while (stack.length > 0) {
|
|
148
|
-
const current = stack.pop()
|
|
149
|
-
if (!current || !fs.existsSync(current)) continue
|
|
175
|
+
const current = stack.pop();
|
|
176
|
+
if (!current || !fs.existsSync(current)) continue;
|
|
150
177
|
for (const entry of fs.readdirSync(current, { withFileTypes: true })) {
|
|
151
|
-
const full = path.join(current, entry.name)
|
|
152
|
-
if (entry.isDirectory()) stack.push(full)
|
|
153
|
-
else if (entry.isFile() && entry.name.endsWith(
|
|
178
|
+
const full = path.join(current, entry.name);
|
|
179
|
+
if (entry.isDirectory()) stack.push(full);
|
|
180
|
+
else if (entry.isFile() && entry.name.endsWith(".css")) files.push(full);
|
|
154
181
|
}
|
|
155
182
|
}
|
|
156
|
-
files.sort()
|
|
157
|
-
return files
|
|
183
|
+
files.sort();
|
|
184
|
+
return files;
|
|
158
185
|
}
|
|
159
186
|
|
|
160
187
|
function stripCssComments(css) {
|
|
161
|
-
return css.replace(/\/\*[\s\S]*?\*\//g,
|
|
188
|
+
return css.replace(/\/\*[\s\S]*?\*\//g, "");
|
|
162
189
|
}
|
|
163
190
|
|
|
164
191
|
function unquoteCss(value) {
|
|
165
|
-
const text = value.trim()
|
|
166
|
-
if (
|
|
167
|
-
|
|
192
|
+
const text = value.trim();
|
|
193
|
+
if (
|
|
194
|
+
text.length >= 2 &&
|
|
195
|
+
((text[0] === '"' && text[text.length - 1] === '"') ||
|
|
196
|
+
(text[0] === "'" && text[text.length - 1] === "'"))
|
|
197
|
+
) {
|
|
198
|
+
return text.slice(1, -1);
|
|
168
199
|
}
|
|
169
|
-
return text
|
|
200
|
+
return text;
|
|
170
201
|
}
|
|
171
202
|
|
|
172
203
|
function firstFontFamily(value) {
|
|
173
|
-
let text = value.trim()
|
|
174
|
-
if (!text) return null
|
|
204
|
+
let text = value.trim();
|
|
205
|
+
if (!text) return null;
|
|
175
206
|
if (text[0] === '"' || text[0] === "'") {
|
|
176
|
-
const quote = text[0]
|
|
177
|
-
const end = text.indexOf(quote, 1)
|
|
178
|
-
return end > 0 ? text.slice(1, end) : null
|
|
207
|
+
const quote = text[0];
|
|
208
|
+
const end = text.indexOf(quote, 1);
|
|
209
|
+
return end > 0 ? text.slice(1, end) : null;
|
|
179
210
|
}
|
|
180
|
-
const comma = text.indexOf(
|
|
181
|
-
if (comma >= 0) text = text.slice(0, comma)
|
|
182
|
-
const family = unquoteCss(text.trim())
|
|
183
|
-
return family && !genericFamilies.has(family.toLowerCase()) ? family : null
|
|
211
|
+
const comma = text.indexOf(",");
|
|
212
|
+
if (comma >= 0) text = text.slice(0, comma);
|
|
213
|
+
const family = unquoteCss(text.trim());
|
|
214
|
+
return family && !genericFamilies.has(family.toLowerCase()) ? family : null;
|
|
184
215
|
}
|
|
185
216
|
|
|
186
217
|
function parseViewportWidths(values) {
|
|
187
|
-
const widths = []
|
|
218
|
+
const widths = [];
|
|
188
219
|
for (const value of values) {
|
|
189
|
-
for (const part of String(value).split(
|
|
190
|
-
const width = Math.round(Number(part.trim()))
|
|
191
|
-
if (width > 0 && !widths.includes(width)) widths.push(width)
|
|
220
|
+
for (const part of String(value).split(",")) {
|
|
221
|
+
const width = Math.round(Number(part.trim()));
|
|
222
|
+
if (width > 0 && !widths.includes(width)) widths.push(width);
|
|
192
223
|
}
|
|
193
224
|
}
|
|
194
|
-
return widths
|
|
225
|
+
return widths;
|
|
195
226
|
}
|
|
196
227
|
|
|
197
228
|
function parseDevicePixelRatios(values) {
|
|
198
|
-
const ratios = []
|
|
229
|
+
const ratios = [];
|
|
199
230
|
for (const value of values) {
|
|
200
|
-
for (const part of String(value).split(
|
|
201
|
-
const ratio = Number(part.trim())
|
|
202
|
-
if (ratio > 0 && !ratios.includes(ratio)) ratios.push(ratio)
|
|
231
|
+
for (const part of String(value).split(",")) {
|
|
232
|
+
const ratio = Number(part.trim());
|
|
233
|
+
if (ratio > 0 && !ratios.includes(ratio)) ratios.push(ratio);
|
|
203
234
|
}
|
|
204
235
|
}
|
|
205
|
-
return ratios.length > 0 ? ratios : [1]
|
|
236
|
+
return ratios.length > 0 ? ratios : [1];
|
|
206
237
|
}
|
|
207
238
|
|
|
208
239
|
function splitTopLevel(value, delimiter) {
|
|
209
|
-
const out = []
|
|
210
|
-
let start = 0
|
|
211
|
-
let depth = 0
|
|
240
|
+
const out = [];
|
|
241
|
+
let start = 0;
|
|
242
|
+
let depth = 0;
|
|
212
243
|
for (let i = 0; i < value.length; i++) {
|
|
213
|
-
const char = value[i]
|
|
214
|
-
if (char ===
|
|
215
|
-
else if (char ===
|
|
244
|
+
const char = value[i];
|
|
245
|
+
if (char === "(") depth++;
|
|
246
|
+
else if (char === ")" && depth > 0) depth--;
|
|
216
247
|
else if (char === delimiter && depth === 0) {
|
|
217
|
-
out.push(value.slice(start, i).trim())
|
|
218
|
-
start = i + 1
|
|
248
|
+
out.push(value.slice(start, i).trim());
|
|
249
|
+
start = i + 1;
|
|
219
250
|
}
|
|
220
251
|
}
|
|
221
|
-
out.push(value.slice(start).trim())
|
|
222
|
-
return out.filter(Boolean)
|
|
252
|
+
out.push(value.slice(start).trim());
|
|
253
|
+
return out.filter(Boolean);
|
|
223
254
|
}
|
|
224
255
|
|
|
225
256
|
function functionInner(value, name) {
|
|
226
|
-
const text = value.trim()
|
|
227
|
-
const prefix = `${name}(
|
|
228
|
-
if (!text.toLowerCase().startsWith(prefix) || !text.endsWith(
|
|
229
|
-
|
|
257
|
+
const text = value.trim();
|
|
258
|
+
const prefix = `${name}(`;
|
|
259
|
+
if (!text.toLowerCase().startsWith(prefix) || !text.endsWith(")"))
|
|
260
|
+
return null;
|
|
261
|
+
return text.slice(prefix.length, -1);
|
|
230
262
|
}
|
|
231
263
|
|
|
232
264
|
function uniqueSizes(sizes) {
|
|
233
265
|
return sizes
|
|
234
266
|
.map((size) => Math.round(size))
|
|
235
|
-
.filter((size, index, list) => size > 0 && list.indexOf(size) === index)
|
|
267
|
+
.filter((size, index, list) => size > 0 && list.indexOf(size) === index);
|
|
236
268
|
}
|
|
237
269
|
|
|
238
270
|
function viewportCombos() {
|
|
239
|
-
if (viewportWidths.length === 0 && viewportHeights.length === 0)
|
|
240
|
-
|
|
241
|
-
if (
|
|
271
|
+
if (viewportWidths.length === 0 && viewportHeights.length === 0)
|
|
272
|
+
return [{ width: 0, height: 0 }];
|
|
273
|
+
if (viewportWidths.length === 0)
|
|
274
|
+
return viewportHeights.map((height) => ({ width: 0, height }));
|
|
275
|
+
if (viewportHeights.length === 0)
|
|
276
|
+
return viewportWidths.map((width) => ({ width, height: 0 }));
|
|
242
277
|
if (viewportWidths.length === viewportHeights.length) {
|
|
243
|
-
return viewportWidths.map((width, index) => ({
|
|
278
|
+
return viewportWidths.map((width, index) => ({
|
|
279
|
+
width,
|
|
280
|
+
height: viewportHeights[index],
|
|
281
|
+
}));
|
|
244
282
|
}
|
|
245
|
-
const combos = []
|
|
283
|
+
const combos = [];
|
|
246
284
|
for (const width of viewportWidths) {
|
|
247
|
-
for (const height of viewportHeights) combos.push({ width, height })
|
|
285
|
+
for (const height of viewportHeights) combos.push({ width, height });
|
|
248
286
|
}
|
|
249
|
-
return combos
|
|
287
|
+
return combos;
|
|
250
288
|
}
|
|
251
289
|
|
|
252
290
|
function defaultFontSizes() {
|
|
253
|
-
return uniqueSizes(devicePixelRatios.map((dpr) => 16 * dpr))
|
|
291
|
+
return uniqueSizes(devicePixelRatios.map((dpr) => 16 * dpr));
|
|
254
292
|
}
|
|
255
293
|
|
|
256
294
|
// Resolve a single CSS length token to ONE physical-pixel size for a specific
|
|
@@ -267,42 +305,50 @@ function defaultFontSizes() {
|
|
|
267
305
|
// which is also an exact atlas hit (the board's physical viewport divided by DPR
|
|
268
306
|
// equals its logical viewport, so vh-no-DPR here == logical-vh x DPR at runtime).
|
|
269
307
|
function resolveLengthForCombo(value, width, height, dpr) {
|
|
270
|
-
const text = value.trim()
|
|
271
|
-
const absoluteMatch = text.match(/^(\d+(?:\.\d+)?)(px)?$/)
|
|
308
|
+
const text = value.trim();
|
|
309
|
+
const absoluteMatch = text.match(/^(\d+(?:\.\d+)?)(px)?$/);
|
|
272
310
|
if (absoluteMatch) {
|
|
273
|
-
const cssPx = Number(absoluteMatch[1])
|
|
274
|
-
return absoluteMatch[2] ? cssPx * dpr : cssPx
|
|
311
|
+
const cssPx = Number(absoluteMatch[1]);
|
|
312
|
+
return absoluteMatch[2] ? cssPx * dpr : cssPx;
|
|
275
313
|
}
|
|
276
|
-
const vwMatch = text.match(/^(\d+(?:\.\d+)?)vw$/)
|
|
277
|
-
if (vwMatch) return (Number(vwMatch[1]) * width) / 100
|
|
278
|
-
const vhMatch = text.match(/^(\d+(?:\.\d+)?)vh$/)
|
|
279
|
-
if (vhMatch) return (Number(vhMatch[1]) * height) / 100
|
|
280
|
-
const vminMatch = text.match(/^(\d+(?:\.\d+)?)vmin$/)
|
|
281
|
-
if (vminMatch) return (Number(vminMatch[1]) * Math.min(width, height)) / 100
|
|
282
|
-
const vmaxMatch = text.match(/^(\d+(?:\.\d+)?)vmax$/)
|
|
283
|
-
if (vmaxMatch) return (Number(vmaxMatch[1]) * Math.max(width, height)) / 100
|
|
284
|
-
const clampInner = functionInner(text,
|
|
314
|
+
const vwMatch = text.match(/^(\d+(?:\.\d+)?)vw$/);
|
|
315
|
+
if (vwMatch) return (Number(vwMatch[1]) * width) / 100;
|
|
316
|
+
const vhMatch = text.match(/^(\d+(?:\.\d+)?)vh$/);
|
|
317
|
+
if (vhMatch) return (Number(vhMatch[1]) * height) / 100;
|
|
318
|
+
const vminMatch = text.match(/^(\d+(?:\.\d+)?)vmin$/);
|
|
319
|
+
if (vminMatch) return (Number(vminMatch[1]) * Math.min(width, height)) / 100;
|
|
320
|
+
const vmaxMatch = text.match(/^(\d+(?:\.\d+)?)vmax$/);
|
|
321
|
+
if (vmaxMatch) return (Number(vmaxMatch[1]) * Math.max(width, height)) / 100;
|
|
322
|
+
const clampInner = functionInner(text, "clamp");
|
|
285
323
|
if (clampInner !== null) {
|
|
286
|
-
const parts = splitTopLevel(clampInner,
|
|
287
|
-
if (parts.length !== 3) return null
|
|
288
|
-
const resolved = parts.map((part) =>
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
324
|
+
const parts = splitTopLevel(clampInner, ",");
|
|
325
|
+
if (parts.length !== 3) return null;
|
|
326
|
+
const resolved = parts.map((part) =>
|
|
327
|
+
resolveLengthForCombo(part, width, height, dpr),
|
|
328
|
+
);
|
|
329
|
+
if (resolved.some((size) => size === null)) return null;
|
|
330
|
+
const [lo, preferred, hi] = resolved;
|
|
331
|
+
return Math.max(lo, Math.min(preferred, hi));
|
|
292
332
|
}
|
|
293
|
-
const minInner = functionInner(text,
|
|
333
|
+
const minInner = functionInner(text, "min");
|
|
294
334
|
if (minInner !== null) {
|
|
295
|
-
const resolved = splitTopLevel(minInner,
|
|
296
|
-
|
|
297
|
-
|
|
335
|
+
const resolved = splitTopLevel(minInner, ",").map((part) =>
|
|
336
|
+
resolveLengthForCombo(part, width, height, dpr),
|
|
337
|
+
);
|
|
338
|
+
if (resolved.length === 0 || resolved.some((size) => size === null))
|
|
339
|
+
return null;
|
|
340
|
+
return Math.min(...resolved);
|
|
298
341
|
}
|
|
299
|
-
const maxInner = functionInner(text,
|
|
342
|
+
const maxInner = functionInner(text, "max");
|
|
300
343
|
if (maxInner !== null) {
|
|
301
|
-
const resolved = splitTopLevel(maxInner,
|
|
302
|
-
|
|
303
|
-
|
|
344
|
+
const resolved = splitTopLevel(maxInner, ",").map((part) =>
|
|
345
|
+
resolveLengthForCombo(part, width, height, dpr),
|
|
346
|
+
);
|
|
347
|
+
if (resolved.length === 0 || resolved.some((size) => size === null))
|
|
348
|
+
return null;
|
|
349
|
+
return Math.max(...resolved);
|
|
304
350
|
}
|
|
305
|
-
return null
|
|
351
|
+
return null;
|
|
306
352
|
}
|
|
307
353
|
|
|
308
354
|
// Bake one atlas size per distinct resolved value across every configured
|
|
@@ -311,128 +357,200 @@ function resolveLengthForCombo(value, width, height, dpr) {
|
|
|
311
357
|
// uniqueSizes drops non-positive (viewport-relative tokens with no viewport) and
|
|
312
358
|
// duplicate sizes. This mirrors the runtime so each requested px hits an atlas.
|
|
313
359
|
function parseFontSizes(value) {
|
|
314
|
-
const combos = viewportCombos()
|
|
315
|
-
const sizes = []
|
|
360
|
+
const combos = viewportCombos();
|
|
361
|
+
const sizes = [];
|
|
316
362
|
for (const dpr of devicePixelRatios) {
|
|
317
363
|
for (const { width, height } of combos) {
|
|
318
|
-
const size = resolveLengthForCombo(value, width, height, dpr)
|
|
319
|
-
if (size !== null) sizes.push(size)
|
|
364
|
+
const size = resolveLengthForCombo(value, width, height, dpr);
|
|
365
|
+
if (size !== null) sizes.push(size);
|
|
320
366
|
}
|
|
321
367
|
}
|
|
322
|
-
return uniqueSizes(sizes)
|
|
368
|
+
return uniqueSizes(sizes);
|
|
323
369
|
}
|
|
324
370
|
|
|
325
|
-
function
|
|
371
|
+
function parseFontShorthand(value) {
|
|
372
|
+
// Style/weight precede the required size; optional line-height follows it.
|
|
373
|
+
// Keep the family tail intact so quoted names and fallback lists use the
|
|
374
|
+
// same parser as an explicit font-family declaration.
|
|
375
|
+
const match = value.match(
|
|
376
|
+
/(?:^|\s)([+]?(?:\d*\.)?\d+(?:px|pt|em|rem|vw|vh|vmin|vmax|%))(?:\s*\/\s*[^\s]+)?\s+(.+)$/i,
|
|
377
|
+
);
|
|
378
|
+
if (!match) return null;
|
|
379
|
+
return {
|
|
380
|
+
family: firstFontFamily(match[2]),
|
|
381
|
+
sizePxs: parseFontSizes(match[1]),
|
|
382
|
+
};
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
function fontCssSelectors(selectorText) {
|
|
326
386
|
return selectorText
|
|
327
|
-
.split(
|
|
387
|
+
.split(",")
|
|
328
388
|
.map((selector) => selector.trim())
|
|
329
|
-
.filter((selector) =>
|
|
389
|
+
.filter((selector) => selector.length > 0 && !selector.startsWith("@"));
|
|
330
390
|
}
|
|
331
391
|
|
|
332
392
|
function rememberFontTuple(tuples, fontFaces, family, sizePx, charset) {
|
|
333
|
-
if (!family || !fontFaces.has(family)) return
|
|
334
|
-
const resolvedSize = sizePx ?? defaultFontSizes()[0] ?? 16
|
|
335
|
-
const key = `${family}\0${resolvedSize}
|
|
336
|
-
const existing = tuples.get(key)
|
|
337
|
-
const merged = existing
|
|
338
|
-
|
|
393
|
+
if (!family || !fontFaces.has(family)) return;
|
|
394
|
+
const resolvedSize = sizePx ?? defaultFontSizes()[0] ?? 16;
|
|
395
|
+
const key = `${family}\0${resolvedSize}`;
|
|
396
|
+
const existing = tuples.get(key);
|
|
397
|
+
const merged = existing
|
|
398
|
+
? mergeCharsets(existing.charset, charset ?? null)
|
|
399
|
+
: (charset ?? null);
|
|
400
|
+
tuples.set(key, { family, sizePx: resolvedSize, charset: merged });
|
|
339
401
|
}
|
|
340
402
|
|
|
341
403
|
function rememberFontTuples(tuples, fontFaces, family, sizePxs, charset) {
|
|
342
404
|
if (!sizePxs || sizePxs.length === 0) {
|
|
343
|
-
for (const sizePx of defaultFontSizes())
|
|
344
|
-
|
|
405
|
+
for (const sizePx of defaultFontSizes())
|
|
406
|
+
rememberFontTuple(tuples, fontFaces, family, sizePx, null);
|
|
407
|
+
return;
|
|
345
408
|
}
|
|
346
|
-
for (const sizePx of sizePxs)
|
|
409
|
+
for (const sizePx of sizePxs)
|
|
410
|
+
rememberFontTuple(tuples, fontFaces, family, sizePx, charset);
|
|
347
411
|
}
|
|
348
412
|
|
|
349
413
|
function parseFontFaces(css, cssFile) {
|
|
350
|
-
const faces = []
|
|
351
|
-
const cssDirName = path.dirname(cssFile)
|
|
352
|
-
const re = /@font-face\s*\{([^}]+)\}/gi
|
|
353
|
-
let match
|
|
414
|
+
const faces = [];
|
|
415
|
+
const cssDirName = path.dirname(cssFile);
|
|
416
|
+
const re = /@font-face\s*\{([^}]+)\}/gi;
|
|
417
|
+
let match;
|
|
354
418
|
while ((match = re.exec(css)) !== null) {
|
|
355
|
-
const block = match[1]
|
|
356
|
-
const familyMatch = block.match(
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
const
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
const
|
|
363
|
-
|
|
419
|
+
const block = match[1];
|
|
420
|
+
const familyMatch = block.match(
|
|
421
|
+
/font-family\s*:\s*(?:"([^"]+)"|'([^']+)'|([^;]+))/i,
|
|
422
|
+
);
|
|
423
|
+
const srcMatch = block.match(
|
|
424
|
+
/src\s*:\s*url\(\s*(?:"([^"]+)"|'([^']+)'|([^)"']+))\s*\)/i,
|
|
425
|
+
);
|
|
426
|
+
const family = familyMatch
|
|
427
|
+
? unquoteCss((familyMatch[1] ?? familyMatch[2] ?? familyMatch[3]).trim())
|
|
428
|
+
: null;
|
|
429
|
+
const rawSrc = srcMatch
|
|
430
|
+
? (srcMatch[1] ?? srcMatch[2] ?? srcMatch[3]).trim()
|
|
431
|
+
: null;
|
|
432
|
+
if (
|
|
433
|
+
!family ||
|
|
434
|
+
!rawSrc ||
|
|
435
|
+
/^https?:\/\//i.test(rawSrc) ||
|
|
436
|
+
rawSrc.startsWith("data:")
|
|
437
|
+
)
|
|
438
|
+
continue;
|
|
439
|
+
const cleanSrc = rawSrc.split(/[?#]/, 1)[0];
|
|
440
|
+
const resolved = path.resolve(cssDirName, cleanSrc);
|
|
441
|
+
if (fs.existsSync(resolved)) {
|
|
442
|
+
const embeddedMatch = block.match(
|
|
443
|
+
/--gea-font-source\s*:\s*url\(\s*(?:"([^"]+)"|'([^']+)'|([^)"']+))\s*\)/i,
|
|
444
|
+
);
|
|
445
|
+
const embedded = embeddedMatch
|
|
446
|
+
? path.resolve(
|
|
447
|
+
cssDirName,
|
|
448
|
+
(embeddedMatch[1] ?? embeddedMatch[2] ?? embeddedMatch[3]).trim(),
|
|
449
|
+
)
|
|
450
|
+
: null;
|
|
451
|
+
if (embedded && !fs.existsSync(embedded))
|
|
452
|
+
fail(`missing bitmap font source: ${embedded}`);
|
|
453
|
+
faces.push({ family, src: resolved, embedded });
|
|
454
|
+
}
|
|
364
455
|
}
|
|
365
|
-
return faces
|
|
456
|
+
return faces;
|
|
366
457
|
}
|
|
367
458
|
|
|
368
459
|
function collectSourceFontFaces() {
|
|
369
460
|
// The app, then whatever the project shares between its apps. The project is
|
|
370
461
|
// the directory this ran in.
|
|
371
|
-
const sourceDirs = [appDir, path.join(process.cwd(),
|
|
372
|
-
const fontFaces = new Map()
|
|
462
|
+
const sourceDirs = [appDir, path.join(process.cwd(), "components")];
|
|
463
|
+
const fontFaces = new Map();
|
|
464
|
+
fontFaces.embeddedSources = new Map();
|
|
373
465
|
for (const dir of sourceDirs) {
|
|
374
466
|
for (const file of findCssFiles(dir)) {
|
|
375
|
-
const css = stripCssComments(fs.readFileSync(file,
|
|
467
|
+
const css = stripCssComments(fs.readFileSync(file, "utf8"));
|
|
376
468
|
for (const face of parseFontFaces(css, file)) {
|
|
377
|
-
if (!fontFaces.has(face.family))
|
|
469
|
+
if (!fontFaces.has(face.family)) {
|
|
470
|
+
fontFaces.set(face.family, face.src);
|
|
471
|
+
if (face.embedded)
|
|
472
|
+
fontFaces.embeddedSources.set(face.family, face.embedded);
|
|
473
|
+
}
|
|
378
474
|
}
|
|
379
475
|
}
|
|
380
476
|
}
|
|
381
|
-
return fontFaces
|
|
477
|
+
return fontFaces;
|
|
382
478
|
}
|
|
383
479
|
|
|
384
480
|
function collectUsedFontTuples(fontFaces) {
|
|
385
|
-
const tuples = new Map()
|
|
386
|
-
const usedFamilies = new Set()
|
|
387
|
-
const inheritedSizes = new Map()
|
|
481
|
+
const tuples = new Map();
|
|
482
|
+
const usedFamilies = new Set();
|
|
483
|
+
const inheritedSizes = new Map();
|
|
388
484
|
for (const file of findCssFiles(cssDir)) {
|
|
389
|
-
const css = stripCssComments(fs.readFileSync(file,
|
|
390
|
-
const selectorStates = new Map()
|
|
391
|
-
const ruleRe = /([^{}]+)\{([^{}]*)\}/g
|
|
392
|
-
let match
|
|
485
|
+
const css = stripCssComments(fs.readFileSync(file, "utf8"));
|
|
486
|
+
const selectorStates = new Map();
|
|
487
|
+
const ruleRe = /([^{}]+)\{([^{}]*)\}/g;
|
|
488
|
+
let match;
|
|
393
489
|
while ((match = ruleRe.exec(css)) !== null) {
|
|
394
|
-
const selector = match[1].trim()
|
|
395
|
-
if (selector.startsWith(
|
|
396
|
-
const selectors =
|
|
397
|
-
if (selectors.length === 0) continue
|
|
398
|
-
let family = null
|
|
399
|
-
let sizePxs = null
|
|
400
|
-
let charset = null
|
|
401
|
-
for (const declaration of match[2].split(
|
|
402
|
-
const colon = declaration.indexOf(
|
|
403
|
-
if (colon <= 0) continue
|
|
404
|
-
const name = declaration.slice(0, colon).trim().toLowerCase()
|
|
405
|
-
const value = declaration.slice(colon + 1).trim()
|
|
406
|
-
if (name ===
|
|
407
|
-
else if (name ===
|
|
408
|
-
else if (name ===
|
|
490
|
+
const selector = match[1].trim();
|
|
491
|
+
if (selector.startsWith("@")) continue;
|
|
492
|
+
const selectors = fontCssSelectors(selector);
|
|
493
|
+
if (selectors.length === 0) continue;
|
|
494
|
+
let family = null;
|
|
495
|
+
let sizePxs = null;
|
|
496
|
+
let charset = null;
|
|
497
|
+
for (const declaration of match[2].split(";")) {
|
|
498
|
+
const colon = declaration.indexOf(":");
|
|
499
|
+
if (colon <= 0) continue;
|
|
500
|
+
const name = declaration.slice(0, colon).trim().toLowerCase();
|
|
501
|
+
const value = declaration.slice(colon + 1).trim();
|
|
502
|
+
if (name === "font-family") family = firstFontFamily(value);
|
|
503
|
+
else if (name === "font-size") sizePxs = parseFontSizes(value);
|
|
504
|
+
else if (name === "font") {
|
|
505
|
+
const shorthand = parseFontShorthand(value);
|
|
506
|
+
if (shorthand) {
|
|
507
|
+
family = shorthand.family;
|
|
508
|
+
sizePxs = shorthand.sizePxs;
|
|
509
|
+
}
|
|
510
|
+
} else if (name === "--gea-font-charset")
|
|
511
|
+
charset = parseFontCharset(value);
|
|
409
512
|
}
|
|
410
513
|
// Accumulate every family/size a selector is ever assigned: repeated
|
|
411
514
|
// rules are usually media-query branches (the block regex sees through
|
|
412
515
|
// `@media { ... }` wrappers), and the generator can't evaluate media
|
|
413
516
|
// conditions, so every branch needs an atlas.
|
|
414
517
|
for (const selector of selectors) {
|
|
415
|
-
const state = selectorStates.get(selector) ?? {
|
|
416
|
-
|
|
518
|
+
const state = selectorStates.get(selector) ?? {
|
|
519
|
+
families: new Set(),
|
|
520
|
+
sizePxs: new Set(),
|
|
521
|
+
charset: undefined,
|
|
522
|
+
};
|
|
523
|
+
if (family) state.families.add(family);
|
|
417
524
|
// Only a rule that names a size has an opinion about that size's atlas.
|
|
418
525
|
// A rule on the same selector that just sets a colour must not widen it.
|
|
419
526
|
if (sizePxs) {
|
|
420
|
-
for (const sizePx of sizePxs) state.sizePxs.add(sizePx)
|
|
421
|
-
state.charset =
|
|
527
|
+
for (const sizePx of sizePxs) state.sizePxs.add(sizePx);
|
|
528
|
+
state.charset =
|
|
529
|
+
state.charset === undefined
|
|
530
|
+
? charset
|
|
531
|
+
: mergeCharsets(state.charset, charset);
|
|
422
532
|
}
|
|
423
|
-
selectorStates.set(selector, state)
|
|
533
|
+
selectorStates.set(selector, state);
|
|
424
534
|
}
|
|
425
535
|
}
|
|
426
536
|
for (const state of selectorStates.values()) {
|
|
427
|
-
const charset = state.charset ?? null
|
|
537
|
+
const charset = state.charset ?? null;
|
|
428
538
|
for (const family of state.families) {
|
|
429
|
-
if (fontFaces.has(family)) usedFamilies.add(family)
|
|
430
|
-
rememberFontTuples(
|
|
539
|
+
if (fontFaces.has(family)) usedFamilies.add(family);
|
|
540
|
+
rememberFontTuples(
|
|
541
|
+
tuples,
|
|
542
|
+
fontFaces,
|
|
543
|
+
family,
|
|
544
|
+
[...state.sizePxs],
|
|
545
|
+
charset,
|
|
546
|
+
);
|
|
431
547
|
}
|
|
432
548
|
if (state.families.size === 0) {
|
|
433
549
|
for (const sizePx of state.sizePxs) {
|
|
434
|
-
const merged = inheritedSizes.has(sizePx)
|
|
435
|
-
|
|
550
|
+
const merged = inheritedSizes.has(sizePx)
|
|
551
|
+
? mergeCharsets(inheritedSizes.get(sizePx), charset)
|
|
552
|
+
: charset;
|
|
553
|
+
inheritedSizes.set(sizePx, merged);
|
|
436
554
|
}
|
|
437
555
|
}
|
|
438
556
|
}
|
|
@@ -448,65 +566,100 @@ function collectUsedFontTuples(fontFaces) {
|
|
|
448
566
|
if (inheritedSizes.size > 0) {
|
|
449
567
|
for (const family of usedFamilies) {
|
|
450
568
|
for (const [sizePx, charset] of inheritedSizes) {
|
|
451
|
-
rememberFontTuples(tuples, fontFaces, family, [sizePx], charset)
|
|
569
|
+
rememberFontTuples(tuples, fontFaces, family, [sizePx], charset);
|
|
452
570
|
}
|
|
453
571
|
}
|
|
454
572
|
}
|
|
455
|
-
return [...tuples.values()].sort(
|
|
573
|
+
return [...tuples.values()].sort(
|
|
574
|
+
(a, b) => a.family.localeCompare(b.family) || a.sizePx - b.sizePx,
|
|
575
|
+
);
|
|
456
576
|
}
|
|
457
577
|
|
|
458
578
|
function loadOpentype() {
|
|
459
579
|
const packageJsons = [
|
|
460
|
-
path.join(
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
580
|
+
path.join(
|
|
581
|
+
path.dirname(fileURLToPath(import.meta.url)),
|
|
582
|
+
"..",
|
|
583
|
+
"package.json",
|
|
584
|
+
),
|
|
585
|
+
path.join(appDir, "package.json"),
|
|
586
|
+
path.join(process.cwd(), "package.json"),
|
|
587
|
+
];
|
|
464
588
|
for (const packageJson of packageJsons) {
|
|
465
|
-
if (!fs.existsSync(packageJson)) continue
|
|
589
|
+
if (!fs.existsSync(packageJson)) continue;
|
|
466
590
|
try {
|
|
467
|
-
return createRequire(packageJson)(
|
|
591
|
+
return createRequire(packageJson)("opentype.js");
|
|
468
592
|
} catch {
|
|
469
593
|
// Try the next package root.
|
|
470
594
|
}
|
|
471
595
|
}
|
|
472
|
-
fail(
|
|
596
|
+
fail("missing opentype.js from the @geastack/core installation");
|
|
473
597
|
}
|
|
474
598
|
|
|
475
|
-
function rasterizeFont(
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
const
|
|
485
|
-
const
|
|
486
|
-
|
|
487
|
-
|
|
599
|
+
function rasterizeFont(
|
|
600
|
+
opentype,
|
|
601
|
+
fontPath,
|
|
602
|
+
family,
|
|
603
|
+
familyId,
|
|
604
|
+
sizePx,
|
|
605
|
+
fontId,
|
|
606
|
+
charset,
|
|
607
|
+
) {
|
|
608
|
+
const fontBuffer = fs.readFileSync(fontPath);
|
|
609
|
+
const arrayBuffer = fontBuffer.buffer.slice(
|
|
610
|
+
fontBuffer.byteOffset,
|
|
611
|
+
fontBuffer.byteOffset + fontBuffer.byteLength,
|
|
612
|
+
);
|
|
613
|
+
const font = opentype.parse(arrayBuffer);
|
|
614
|
+
|
|
615
|
+
const scale = sizePx / font.unitsPerEm;
|
|
616
|
+
const ascender = Math.ceil(font.ascender * scale);
|
|
617
|
+
const descender = Math.ceil(Math.abs(font.descender * scale));
|
|
618
|
+
const lineHeight = ascender + descender;
|
|
619
|
+
const glyphs = [];
|
|
620
|
+
const glyphBitmaps = [];
|
|
621
|
+
|
|
622
|
+
const codepoints = charset
|
|
623
|
+
? [...charset].sort((a, b) => a - b)
|
|
624
|
+
: baselineCodepoints();
|
|
488
625
|
|
|
489
626
|
for (const cp of codepoints) {
|
|
490
|
-
const glyph = font.charToGlyph(String.fromCodePoint(cp))
|
|
491
|
-
const advance = Math.round((glyph.advanceWidth ?? 0) * scale)
|
|
492
|
-
const bounds = glyph.getBoundingBox()
|
|
493
|
-
const x0 = Math.floor(bounds.x1 * scale)
|
|
494
|
-
const y0 = Math.floor(-bounds.y2 * scale)
|
|
495
|
-
const x1 = Math.ceil(bounds.x2 * scale)
|
|
496
|
-
const y1 = Math.ceil(-bounds.y1 * scale)
|
|
497
|
-
const gw = Math.max(x1 - x0, 0)
|
|
498
|
-
const gh = Math.max(y1 - y0, 0)
|
|
499
|
-
const bitmap = new Uint8Array(gw * gh)
|
|
627
|
+
const glyph = font.charToGlyph(String.fromCodePoint(cp));
|
|
628
|
+
const advance = Math.round((glyph.advanceWidth ?? 0) * scale);
|
|
629
|
+
const bounds = glyph.getBoundingBox();
|
|
630
|
+
const x0 = Math.floor(bounds.x1 * scale);
|
|
631
|
+
const y0 = Math.floor(-bounds.y2 * scale);
|
|
632
|
+
const x1 = Math.ceil(bounds.x2 * scale);
|
|
633
|
+
const y1 = Math.ceil(-bounds.y1 * scale);
|
|
634
|
+
const gw = Math.max(x1 - x0, 0);
|
|
635
|
+
const gh = Math.max(y1 - y0, 0);
|
|
636
|
+
const bitmap = new Uint8Array(gw * gh);
|
|
500
637
|
|
|
501
638
|
if (gw > 0 && gh > 0) {
|
|
502
|
-
rasterizeGlyph(
|
|
639
|
+
rasterizeGlyph(
|
|
640
|
+
glyph.getPath(0, 0, sizePx).commands,
|
|
641
|
+
bitmap,
|
|
642
|
+
gw,
|
|
643
|
+
gh,
|
|
644
|
+
x0,
|
|
645
|
+
y0,
|
|
646
|
+
);
|
|
503
647
|
}
|
|
504
648
|
|
|
505
|
-
glyphs.push({
|
|
506
|
-
|
|
649
|
+
glyphs.push({
|
|
650
|
+
codepoint: cp,
|
|
651
|
+
sourceX: 0,
|
|
652
|
+
sourceY: 0,
|
|
653
|
+
width: gw,
|
|
654
|
+
height: gh,
|
|
655
|
+
advance,
|
|
656
|
+
bearingX: x0,
|
|
657
|
+
bearingY: -y0,
|
|
658
|
+
});
|
|
659
|
+
glyphBitmaps.push({ data: bitmap, width: gw, height: gh });
|
|
507
660
|
}
|
|
508
661
|
|
|
509
|
-
const { atlasWidth, atlasHeight } = packGlyphAtlas(glyphs)
|
|
662
|
+
const { atlasWidth, atlasHeight } = packGlyphAtlas(glyphs);
|
|
510
663
|
if (atlasWidth === 0 || atlasHeight === 0) {
|
|
511
664
|
return {
|
|
512
665
|
id: fontId,
|
|
@@ -521,19 +674,23 @@ function rasterizeFont(opentype, fontPath, family, familyId, sizePx, fontId, cha
|
|
|
521
674
|
atlasHeight: 1,
|
|
522
675
|
atlasBits,
|
|
523
676
|
atlasData: new Uint8Array(1),
|
|
524
|
-
}
|
|
677
|
+
};
|
|
525
678
|
}
|
|
526
679
|
|
|
527
|
-
const atlasData = new Uint8Array(atlasWidth * atlasHeight)
|
|
680
|
+
const atlasData = new Uint8Array(atlasWidth * atlasHeight);
|
|
528
681
|
for (let i = 0; i < glyphs.length; i += 1) {
|
|
529
|
-
const glyph = glyphs[i]
|
|
530
|
-
const bitmap = glyphBitmaps[i]
|
|
682
|
+
const glyph = glyphs[i];
|
|
683
|
+
const bitmap = glyphBitmaps[i];
|
|
531
684
|
// Quantize per glyph, not across the packed atlas: the neighbourhood rules
|
|
532
685
|
// below must not see a neighbouring glyph's ink through the 1px padding.
|
|
533
|
-
const source =
|
|
686
|
+
const source =
|
|
687
|
+
atlasBits === 2
|
|
688
|
+
? quantizeCoverageToLevels(bitmap.data, bitmap.width, bitmap.height)
|
|
689
|
+
: bitmap.data;
|
|
534
690
|
for (let row = 0; row < bitmap.height; row += 1) {
|
|
535
691
|
for (let col = 0; col < bitmap.width; col += 1) {
|
|
536
|
-
atlasData[(glyph.sourceY + row) * atlasWidth + glyph.sourceX + col] =
|
|
692
|
+
atlasData[(glyph.sourceY + row) * atlasWidth + glyph.sourceX + col] =
|
|
693
|
+
source[row * bitmap.width + col];
|
|
537
694
|
}
|
|
538
695
|
}
|
|
539
696
|
}
|
|
@@ -550,8 +707,11 @@ function rasterizeFont(opentype, fontPath, family, familyId, sizePx, fontId, cha
|
|
|
550
707
|
atlasWidth,
|
|
551
708
|
atlasHeight,
|
|
552
709
|
atlasBits,
|
|
553
|
-
atlasData:
|
|
554
|
-
|
|
710
|
+
atlasData:
|
|
711
|
+
atlasBits === 2
|
|
712
|
+
? packLevelsTo2Bit(atlasData, atlasWidth, atlasHeight)
|
|
713
|
+
: atlasData,
|
|
714
|
+
};
|
|
555
715
|
}
|
|
556
716
|
|
|
557
717
|
// Offline quantization of 8-bit supersampled coverage to the four levels of a
|
|
@@ -580,20 +740,22 @@ function rasterizeFont(opentype, fontPath, family, familyId, sizePx, fontId, cha
|
|
|
580
740
|
// - The 48 floor drops coverage below ~19% instead of promoting it to level 1
|
|
581
741
|
// (85/255 = 33%), so faint spill outside a stroke does not fatten it.
|
|
582
742
|
function quantizeCoverageToLevels(coverage, width, height) {
|
|
583
|
-
const levels = new Uint8Array(width * height)
|
|
743
|
+
const levels = new Uint8Array(width * height);
|
|
584
744
|
for (let i = 0; i < coverage.length; i += 1) {
|
|
585
|
-
const value = coverage[i]
|
|
586
|
-
levels[i] = value >= 208 ? 3 : value >= 128 ? 2 : value >= 48 ? 1 : 0
|
|
745
|
+
const value = coverage[i];
|
|
746
|
+
levels[i] = value >= 208 ? 3 : value >= 128 ? 2 : value >= 48 ? 1 : 0;
|
|
587
747
|
}
|
|
588
748
|
|
|
589
749
|
// Neighbourhood fixups, read from the pass above and written to a copy so the
|
|
590
750
|
// result does not depend on scan order.
|
|
591
|
-
const result = Uint8Array.from(levels)
|
|
592
|
-
const levelAt = (x, y) =>
|
|
593
|
-
|
|
751
|
+
const result = Uint8Array.from(levels);
|
|
752
|
+
const levelAt = (x, y) =>
|
|
753
|
+
x < 0 || y < 0 || x >= width || y >= height ? 0 : levels[y * width + x];
|
|
754
|
+
const coverageAt = (x, y) =>
|
|
755
|
+
x < 0 || y < 0 || x >= width || y >= height ? 0 : coverage[y * width + x];
|
|
594
756
|
for (let y = 0; y < height; y += 1) {
|
|
595
757
|
for (let x = 0; x < width; x += 1) {
|
|
596
|
-
const index = y * width + x
|
|
758
|
+
const index = y * width + x;
|
|
597
759
|
// Stem solidity: a pixel enclosed by fully-inked neighbours is interior,
|
|
598
760
|
// whatever rounding did to its own coverage. Forcing it to level 3 stops
|
|
599
761
|
// rasterizer noise from punching grey pinholes down the middle of a stem.
|
|
@@ -604,22 +766,22 @@ function quantizeCoverageToLevels(coverage, width, height) {
|
|
|
604
766
|
coverageAt(x, y - 1) >= 208 &&
|
|
605
767
|
coverageAt(x, y + 1) >= 208
|
|
606
768
|
) {
|
|
607
|
-
result[index] = 3
|
|
608
|
-
continue
|
|
769
|
+
result[index] = 3;
|
|
770
|
+
continue;
|
|
609
771
|
}
|
|
610
772
|
// Speckle removal: a faint pixel with no ink at all around it is a stray
|
|
611
773
|
// dot off a curve tip, not a stroke. On four levels it reads as dirt.
|
|
612
|
-
if (levels[index] !== 1) continue
|
|
613
|
-
let neighbours = 0
|
|
774
|
+
if (levels[index] !== 1) continue;
|
|
775
|
+
let neighbours = 0;
|
|
614
776
|
for (let dy = -1; dy <= 1; dy += 1) {
|
|
615
777
|
for (let dx = -1; dx <= 1; dx += 1) {
|
|
616
|
-
if (dx !== 0 || dy !== 0) neighbours += levelAt(x + dx, y + dy)
|
|
778
|
+
if (dx !== 0 || dy !== 0) neighbours += levelAt(x + dx, y + dy);
|
|
617
779
|
}
|
|
618
780
|
}
|
|
619
|
-
if (neighbours === 0) result[index] = 0
|
|
781
|
+
if (neighbours === 0) result[index] = 0;
|
|
620
782
|
}
|
|
621
783
|
}
|
|
622
|
-
return result
|
|
784
|
+
return result;
|
|
623
785
|
}
|
|
624
786
|
|
|
625
787
|
// Pack 0..3 levels four to a byte, rows byte-aligned (stride = ceil(width / 4)).
|
|
@@ -627,48 +789,51 @@ function quantizeCoverageToLevels(coverage, width, height) {
|
|
|
627
789
|
// dump of a row reads left to right like the glyph does. Kept in sync with
|
|
628
790
|
// `RasterizedFont::coverage` in packages/engine/rasterized_font.cpp.
|
|
629
791
|
function packLevelsTo2Bit(levels, width, height) {
|
|
630
|
-
const stride = (width + 3) >> 2
|
|
631
|
-
const packed = new Uint8Array(stride * height)
|
|
792
|
+
const stride = (width + 3) >> 2;
|
|
793
|
+
const packed = new Uint8Array(stride * height);
|
|
632
794
|
for (let y = 0; y < height; y += 1) {
|
|
633
795
|
for (let x = 0; x < width; x += 1) {
|
|
634
|
-
const level = levels[y * width + x] & 0x3
|
|
635
|
-
packed[y * stride + (x >> 2)] |= level << ((3 - (x & 3)) * 2)
|
|
796
|
+
const level = levels[y * width + x] & 0x3;
|
|
797
|
+
packed[y * stride + (x >> 2)] |= level << ((3 - (x & 3)) * 2);
|
|
636
798
|
}
|
|
637
799
|
}
|
|
638
|
-
return packed
|
|
800
|
+
return packed;
|
|
639
801
|
}
|
|
640
802
|
|
|
641
803
|
function rasterizeGlyph(cmds, bitmap, gw, gh, x0, y0) {
|
|
642
|
-
const ss = glyphSupersample
|
|
643
|
-
const ssWidth = gw * ss
|
|
644
|
-
const ssHeight = gh * ss
|
|
645
|
-
const hires = new Uint8Array(ssWidth * ssHeight)
|
|
646
|
-
const segments = pathCommandsToSegments(cmds, x0, y0)
|
|
647
|
-
const maxEdgesPerRow = 128
|
|
648
|
-
const scanlines = new Float32Array(ssHeight * maxEdgesPerRow)
|
|
649
|
-
const scanCounts = new Int32Array(ssHeight)
|
|
804
|
+
const ss = glyphSupersample;
|
|
805
|
+
const ssWidth = gw * ss;
|
|
806
|
+
const ssHeight = gh * ss;
|
|
807
|
+
const hires = new Uint8Array(ssWidth * ssHeight);
|
|
808
|
+
const segments = pathCommandsToSegments(cmds, x0, y0);
|
|
809
|
+
const maxEdgesPerRow = 128;
|
|
810
|
+
const scanlines = new Float32Array(ssHeight * maxEdgesPerRow);
|
|
811
|
+
const scanCounts = new Int32Array(ssHeight);
|
|
650
812
|
|
|
651
813
|
for (const [sx0, sy0, sx1, sy1] of segments) {
|
|
652
|
-
const hsy0 = sy0 * ss
|
|
653
|
-
const hsy1 = sy1 * ss
|
|
654
|
-
const hsx0 = sx0 * ss
|
|
655
|
-
const hsx1 = sx1 * ss
|
|
656
|
-
const minRow = Math.max(0, Math.floor(Math.min(hsy0, hsy1)))
|
|
657
|
-
const maxRow = Math.min(ssHeight - 1, Math.ceil(Math.max(hsy0, hsy1)))
|
|
814
|
+
const hsy0 = sy0 * ss;
|
|
815
|
+
const hsy1 = sy1 * ss;
|
|
816
|
+
const hsx0 = sx0 * ss;
|
|
817
|
+
const hsx1 = sx1 * ss;
|
|
818
|
+
const minRow = Math.max(0, Math.floor(Math.min(hsy0, hsy1)));
|
|
819
|
+
const maxRow = Math.min(ssHeight - 1, Math.ceil(Math.max(hsy0, hsy1)));
|
|
658
820
|
for (let row = minRow; row <= maxRow; row += 1) {
|
|
659
|
-
const y = row + 0.5
|
|
821
|
+
const y = row + 0.5;
|
|
660
822
|
if ((hsy0 <= y && hsy1 > y) || (hsy1 <= y && hsy0 > y)) {
|
|
661
|
-
const pos = hsx0 + ((y - hsy0) / (hsy1 - hsy0)) * (hsx1 - hsx0)
|
|
662
|
-
const count = scanCounts[row]
|
|
663
|
-
if (count < maxEdgesPerRow)
|
|
823
|
+
const pos = hsx0 + ((y - hsy0) / (hsy1 - hsy0)) * (hsx1 - hsx0);
|
|
824
|
+
const count = scanCounts[row];
|
|
825
|
+
if (count < maxEdgesPerRow)
|
|
826
|
+
scanlines[row * maxEdgesPerRow + scanCounts[row]++] = pos;
|
|
664
827
|
}
|
|
665
828
|
}
|
|
666
829
|
}
|
|
667
830
|
|
|
668
831
|
for (let row = 0; row < ssHeight; row += 1) {
|
|
669
|
-
const count = scanCounts[row]
|
|
670
|
-
if (count < 2) continue
|
|
671
|
-
const edges = Array.from(
|
|
832
|
+
const count = scanCounts[row];
|
|
833
|
+
if (count < 2) continue;
|
|
834
|
+
const edges = Array.from(
|
|
835
|
+
scanlines.slice(row * maxEdgesPerRow, row * maxEdgesPerRow + count),
|
|
836
|
+
).sort((a, b) => a - b);
|
|
672
837
|
for (let i = 0; i < edges.length - 1; i += 2) {
|
|
673
838
|
// At 1:1 (no supersampling) ink a pixel only when its CENTER falls inside
|
|
674
839
|
// the span — the standard 50% coverage rule — so glyphs keep their true
|
|
@@ -676,195 +841,215 @@ function rasterizeGlyph(cmds, bitmap, gw, gh, x0, y0) {
|
|
|
676
841
|
// up to a pixel per side and reads as bold. Supersampled paths keep the
|
|
677
842
|
// outward rounding: it's averaged away on downsample and avoids dropping
|
|
678
843
|
// thin sub-pixel coverage.
|
|
679
|
-
const left =
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
844
|
+
const left =
|
|
845
|
+
ss === 1
|
|
846
|
+
? Math.max(0, Math.ceil(edges[i] - 0.5))
|
|
847
|
+
: Math.max(0, Math.floor(edges[i]));
|
|
848
|
+
const right =
|
|
849
|
+
ss === 1
|
|
850
|
+
? Math.min(ssWidth - 1, Math.floor(edges[i + 1] - 0.5))
|
|
851
|
+
: Math.min(ssWidth - 1, Math.ceil(edges[i + 1]));
|
|
852
|
+
for (let col = left; col <= right; col += 1)
|
|
853
|
+
hires[row * ssWidth + col] = 1;
|
|
686
854
|
}
|
|
687
855
|
}
|
|
688
856
|
|
|
689
857
|
for (let py = 0; py < gh; py += 1) {
|
|
690
858
|
for (let px = 0; px < gw; px += 1) {
|
|
691
|
-
let sum = 0
|
|
859
|
+
let sum = 0;
|
|
692
860
|
for (let sy = 0; sy < ss; sy += 1) {
|
|
693
|
-
for (let sx = 0; sx < ss; sx += 1)
|
|
861
|
+
for (let sx = 0; sx < ss; sx += 1)
|
|
862
|
+
sum += hires[(py * ss + sy) * ssWidth + (px * ss + sx)];
|
|
694
863
|
}
|
|
695
|
-
bitmap[py * gw + px] = Math.round((sum / (ss * ss)) * 255)
|
|
864
|
+
bitmap[py * gw + px] = Math.round((sum / (ss * ss)) * 255);
|
|
696
865
|
}
|
|
697
866
|
}
|
|
698
867
|
}
|
|
699
868
|
|
|
700
869
|
function pathCommandsToSegments(cmds, x0, y0) {
|
|
701
|
-
const segments = []
|
|
702
|
-
let curX = 0
|
|
703
|
-
let curY = 0
|
|
704
|
-
let startX = 0
|
|
705
|
-
let startY = 0
|
|
870
|
+
const segments = [];
|
|
871
|
+
let curX = 0;
|
|
872
|
+
let curY = 0;
|
|
873
|
+
let startX = 0;
|
|
874
|
+
let startY = 0;
|
|
706
875
|
|
|
707
876
|
for (const cmd of cmds) {
|
|
708
|
-
if (cmd.type ===
|
|
709
|
-
curX = cmd.x - x0
|
|
710
|
-
curY = cmd.y - y0
|
|
711
|
-
startX = curX
|
|
712
|
-
startY = curY
|
|
713
|
-
} else if (cmd.type ===
|
|
714
|
-
const endX = cmd.x - x0
|
|
715
|
-
const endY = cmd.y - y0
|
|
716
|
-
segments.push([curX, curY, endX, endY])
|
|
717
|
-
curX = endX
|
|
718
|
-
curY = endY
|
|
719
|
-
} else if (cmd.type ===
|
|
877
|
+
if (cmd.type === "M") {
|
|
878
|
+
curX = cmd.x - x0;
|
|
879
|
+
curY = cmd.y - y0;
|
|
880
|
+
startX = curX;
|
|
881
|
+
startY = curY;
|
|
882
|
+
} else if (cmd.type === "L") {
|
|
883
|
+
const endX = cmd.x - x0;
|
|
884
|
+
const endY = cmd.y - y0;
|
|
885
|
+
segments.push([curX, curY, endX, endY]);
|
|
886
|
+
curX = endX;
|
|
887
|
+
curY = endY;
|
|
888
|
+
} else if (cmd.type === "C") {
|
|
720
889
|
for (let s = 1; s <= 12; s += 1) {
|
|
721
|
-
const t = s / 12
|
|
722
|
-
const mt = 1 - t
|
|
723
|
-
const endX =
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
890
|
+
const t = s / 12;
|
|
891
|
+
const mt = 1 - t;
|
|
892
|
+
const endX =
|
|
893
|
+
mt ** 3 * curX +
|
|
894
|
+
3 * mt * mt * t * (cmd.x1 - x0) +
|
|
895
|
+
3 * mt * t * t * (cmd.x2 - x0) +
|
|
896
|
+
t ** 3 * (cmd.x - x0);
|
|
897
|
+
const endY =
|
|
898
|
+
mt ** 3 * curY +
|
|
899
|
+
3 * mt * mt * t * (cmd.y1 - y0) +
|
|
900
|
+
3 * mt * t * t * (cmd.y2 - y0) +
|
|
901
|
+
t ** 3 * (cmd.y - y0);
|
|
902
|
+
segments.push([curX, curY, endX, endY]);
|
|
903
|
+
curX = endX;
|
|
904
|
+
curY = endY;
|
|
728
905
|
}
|
|
729
|
-
} else if (cmd.type ===
|
|
906
|
+
} else if (cmd.type === "Q") {
|
|
730
907
|
for (let s = 1; s <= 8; s += 1) {
|
|
731
|
-
const t = s / 8
|
|
732
|
-
const mt = 1 - t
|
|
733
|
-
const endX =
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
curY
|
|
908
|
+
const t = s / 8;
|
|
909
|
+
const mt = 1 - t;
|
|
910
|
+
const endX =
|
|
911
|
+
mt * mt * curX + 2 * mt * t * (cmd.x1 - x0) + t * t * (cmd.x - x0);
|
|
912
|
+
const endY =
|
|
913
|
+
mt * mt * curY + 2 * mt * t * (cmd.y1 - y0) + t * t * (cmd.y - y0);
|
|
914
|
+
segments.push([curX, curY, endX, endY]);
|
|
915
|
+
curX = endX;
|
|
916
|
+
curY = endY;
|
|
738
917
|
}
|
|
739
|
-
} else if (cmd.type ===
|
|
740
|
-
if (curX !== startX || curY !== startY)
|
|
741
|
-
|
|
742
|
-
|
|
918
|
+
} else if (cmd.type === "Z") {
|
|
919
|
+
if (curX !== startX || curY !== startY)
|
|
920
|
+
segments.push([curX, curY, startX, startY]);
|
|
921
|
+
curX = startX;
|
|
922
|
+
curY = startY;
|
|
743
923
|
}
|
|
744
924
|
}
|
|
745
925
|
|
|
746
|
-
return segments
|
|
926
|
+
return segments;
|
|
747
927
|
}
|
|
748
928
|
|
|
749
929
|
function packGlyphAtlas(glyphs) {
|
|
750
|
-
const padding = 1
|
|
751
|
-
const maxAtlasWidth = 512
|
|
752
|
-
let atlasWidth = 0
|
|
753
|
-
let curX = 0
|
|
754
|
-
let curY = 0
|
|
755
|
-
let maxRowHeight = 0
|
|
930
|
+
const padding = 1;
|
|
931
|
+
const maxAtlasWidth = 512;
|
|
932
|
+
let atlasWidth = 0;
|
|
933
|
+
let curX = 0;
|
|
934
|
+
let curY = 0;
|
|
935
|
+
let maxRowHeight = 0;
|
|
756
936
|
|
|
757
937
|
for (const glyph of glyphs) {
|
|
758
938
|
if (curX + glyph.width + padding > maxAtlasWidth) {
|
|
759
|
-
curX = 0
|
|
760
|
-
curY += maxRowHeight + padding
|
|
761
|
-
maxRowHeight = 0
|
|
939
|
+
curX = 0;
|
|
940
|
+
curY += maxRowHeight + padding;
|
|
941
|
+
maxRowHeight = 0;
|
|
762
942
|
}
|
|
763
|
-
glyph.sourceX = curX
|
|
764
|
-
glyph.sourceY = curY
|
|
765
|
-
if (curX + glyph.width > atlasWidth) atlasWidth = curX + glyph.width
|
|
766
|
-
if (glyph.height > maxRowHeight) maxRowHeight = glyph.height
|
|
767
|
-
curX += glyph.width + padding
|
|
943
|
+
glyph.sourceX = curX;
|
|
944
|
+
glyph.sourceY = curY;
|
|
945
|
+
if (curX + glyph.width > atlasWidth) atlasWidth = curX + glyph.width;
|
|
946
|
+
if (glyph.height > maxRowHeight) maxRowHeight = glyph.height;
|
|
947
|
+
curX += glyph.width + padding;
|
|
768
948
|
}
|
|
769
949
|
|
|
770
|
-
return { atlasWidth, atlasHeight: curY + maxRowHeight }
|
|
950
|
+
return { atlasWidth, atlasHeight: curY + maxRowHeight };
|
|
771
951
|
}
|
|
772
952
|
|
|
773
953
|
function cppString(value) {
|
|
774
|
-
return JSON.stringify(value)
|
|
954
|
+
return JSON.stringify(value);
|
|
775
955
|
}
|
|
776
956
|
|
|
777
957
|
function generatedHeader(fontCount, familyCount, options = {}) {
|
|
778
958
|
const lines = [
|
|
779
|
-
|
|
780
|
-
|
|
959
|
+
"#pragma once",
|
|
960
|
+
"#define GEA_EMBEDDED_HAS_GENERATED_FONTS 1",
|
|
781
961
|
'#include "graphics/font.h"',
|
|
782
962
|
`#define GEA_EMBEDDED_FONT_COUNT ${fontCount}`,
|
|
783
963
|
`#define GEA_EMBEDDED_FONT_FAMILY_COUNT ${familyCount}`,
|
|
784
|
-
]
|
|
785
|
-
if (options.runtimeTtfFonts)
|
|
786
|
-
|
|
787
|
-
|
|
964
|
+
];
|
|
965
|
+
if (options.runtimeTtfFonts)
|
|
966
|
+
lines.push("#define GEA_EMBEDDED_HAS_RUNTIME_TTF_FONTS 1");
|
|
967
|
+
lines.push("");
|
|
968
|
+
return lines.join("\n");
|
|
788
969
|
}
|
|
789
970
|
|
|
790
971
|
function fontFunctionNames() {
|
|
791
972
|
if (!symbolPrefix) {
|
|
792
973
|
return {
|
|
793
|
-
namespaceOpen:
|
|
794
|
-
namespaceClose:
|
|
795
|
-
ensureLinked:
|
|
796
|
-
lookupFont:
|
|
797
|
-
lookupFontForFamily:
|
|
798
|
-
lookupFontFamily:
|
|
799
|
-
lookupFontFamilyName:
|
|
800
|
-
lookupRuntimeTtfFontForFamily:
|
|
801
|
-
}
|
|
974
|
+
namespaceOpen: "namespace gea::framework::graphics::generated {",
|
|
975
|
+
namespaceClose: "} // namespace gea::framework::graphics::generated",
|
|
976
|
+
ensureLinked: "ensureLinked",
|
|
977
|
+
lookupFont: "lookupFont",
|
|
978
|
+
lookupFontForFamily: "lookupFontForFamily",
|
|
979
|
+
lookupFontFamily: "lookupFontFamily",
|
|
980
|
+
lookupFontFamilyName: "lookupFontFamilyName",
|
|
981
|
+
lookupRuntimeTtfFontForFamily: "lookupRuntimeTtfFontForFamily",
|
|
982
|
+
};
|
|
802
983
|
}
|
|
803
984
|
return {
|
|
804
|
-
namespaceOpen:
|
|
805
|
-
namespaceClose:
|
|
985
|
+
namespaceOpen: "",
|
|
986
|
+
namespaceClose: "",
|
|
806
987
|
ensureLinked: `${symbolPrefix}_ensure_linked_fonts`,
|
|
807
988
|
lookupFont: `${symbolPrefix}_lookup_font`,
|
|
808
989
|
lookupFontForFamily: `${symbolPrefix}_lookup_font_for_family`,
|
|
809
990
|
lookupFontFamily: `${symbolPrefix}_lookup_font_family`,
|
|
810
991
|
lookupFontFamilyName: `${symbolPrefix}_lookup_font_family_name`,
|
|
811
992
|
lookupRuntimeTtfFontForFamily: `${symbolPrefix}_lookup_runtime_ttf_font_for_family`,
|
|
812
|
-
}
|
|
993
|
+
};
|
|
813
994
|
}
|
|
814
995
|
|
|
815
996
|
function emitEmptyFontLookup(lines, names) {
|
|
816
|
-
lines.push(`void ${names.ensureLinked}()`)
|
|
817
|
-
lines.push(
|
|
818
|
-
lines.push(
|
|
819
|
-
lines.push(
|
|
820
|
-
lines.push(
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
lines.push(
|
|
824
|
-
lines.push(
|
|
825
|
-
lines.push(
|
|
826
|
-
lines.push(
|
|
827
|
-
lines.push(
|
|
828
|
-
lines.push(
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
lines.push(
|
|
832
|
-
lines.push(
|
|
833
|
-
lines.push(
|
|
834
|
-
lines.push(
|
|
835
|
-
lines.push(
|
|
836
|
-
lines.push(
|
|
837
|
-
lines.push(
|
|
838
|
-
lines.push(
|
|
839
|
-
lines.push(
|
|
840
|
-
lines.push(
|
|
841
|
-
lines.push(
|
|
842
|
-
lines.push(
|
|
843
|
-
lines.push(
|
|
844
|
-
lines.push(
|
|
997
|
+
lines.push(`void ${names.ensureLinked}()`);
|
|
998
|
+
lines.push("{");
|
|
999
|
+
lines.push("}");
|
|
1000
|
+
lines.push("");
|
|
1001
|
+
lines.push(
|
|
1002
|
+
`const gea::framework::graphics::RasterizedFontData *${names.lookupFont}(int fontId)`,
|
|
1003
|
+
);
|
|
1004
|
+
lines.push("{");
|
|
1005
|
+
lines.push(" (void)fontId;");
|
|
1006
|
+
lines.push(" return nullptr;");
|
|
1007
|
+
lines.push("}");
|
|
1008
|
+
lines.push("");
|
|
1009
|
+
lines.push(
|
|
1010
|
+
`const gea::framework::graphics::RasterizedFontData *${names.lookupFontForFamily}(int familyId, int sizePx)`,
|
|
1011
|
+
);
|
|
1012
|
+
lines.push("{");
|
|
1013
|
+
lines.push(" (void)familyId;");
|
|
1014
|
+
lines.push(" (void)sizePx;");
|
|
1015
|
+
lines.push(" return nullptr;");
|
|
1016
|
+
lines.push("}");
|
|
1017
|
+
lines.push("");
|
|
1018
|
+
lines.push(`int ${names.lookupFontFamily}(const char *family)`);
|
|
1019
|
+
lines.push("{");
|
|
1020
|
+
lines.push(" (void)family;");
|
|
1021
|
+
lines.push(" return -1;");
|
|
1022
|
+
lines.push("}");
|
|
1023
|
+
lines.push("");
|
|
1024
|
+
lines.push(`const char *${names.lookupFontFamilyName}(int familyId)`);
|
|
1025
|
+
lines.push("{");
|
|
1026
|
+
lines.push(" (void)familyId;");
|
|
1027
|
+
lines.push(" return nullptr;");
|
|
1028
|
+
lines.push("}");
|
|
1029
|
+
lines.push("");
|
|
845
1030
|
}
|
|
846
1031
|
|
|
847
1032
|
function generatedCpp(fonts, families) {
|
|
848
|
-
const names = fontFunctionNames()
|
|
1033
|
+
const names = fontFunctionNames();
|
|
849
1034
|
const lines = [
|
|
850
1035
|
'#include "gea_embedded_font_generated.h"',
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
]
|
|
1036
|
+
"#include <cstring>",
|
|
1037
|
+
"",
|
|
1038
|
+
];
|
|
854
1039
|
if (names.namespaceOpen) {
|
|
855
|
-
lines.push(names.namespaceOpen)
|
|
856
|
-
lines.push(
|
|
1040
|
+
lines.push(names.namespaceOpen);
|
|
1041
|
+
lines.push("");
|
|
857
1042
|
} else {
|
|
858
|
-
lines.push(
|
|
859
|
-
lines.push(
|
|
860
|
-
lines.push(
|
|
1043
|
+
lines.push("using gea::framework::graphics::Glyph;");
|
|
1044
|
+
lines.push("using gea::framework::graphics::RasterizedFontData;");
|
|
1045
|
+
lines.push("");
|
|
861
1046
|
}
|
|
862
1047
|
|
|
863
1048
|
if (fonts.length === 0) {
|
|
864
|
-
emitEmptyFontLookup(lines, names)
|
|
865
|
-
if (names.namespaceClose) lines.push(names.namespaceClose)
|
|
866
|
-
lines.push(
|
|
867
|
-
return lines.join(
|
|
1049
|
+
emitEmptyFontLookup(lines, names);
|
|
1050
|
+
if (names.namespaceClose) lines.push(names.namespaceClose);
|
|
1051
|
+
lines.push("");
|
|
1052
|
+
return lines.join("\n");
|
|
868
1053
|
}
|
|
869
1054
|
|
|
870
1055
|
// Atlas bitmaps are the bulk of the font footprint and are frequently
|
|
@@ -874,237 +1059,305 @@ function generatedCpp(fonts, families) {
|
|
|
874
1059
|
// copies into one, deduping the bundle with no runtime change. We dedup within
|
|
875
1060
|
// this TU too (a weak symbol can't be defined twice in one TU); `font_data`
|
|
876
1061
|
// points at the shared symbol either way.
|
|
877
|
-
const atlasSymbols = new Map()
|
|
1062
|
+
const atlasSymbols = new Map();
|
|
878
1063
|
for (const font of fonts) {
|
|
879
|
-
const hash = createHash(
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
1064
|
+
const hash = createHash("sha1")
|
|
1065
|
+
.update(Buffer.from(font.atlasData))
|
|
1066
|
+
.digest("hex")
|
|
1067
|
+
.slice(0, 16);
|
|
1068
|
+
const symbol = `gea_font_atlas_${hash}`;
|
|
1069
|
+
font.atlasSymbol = symbol;
|
|
1070
|
+
if (atlasSymbols.has(hash)) continue;
|
|
1071
|
+
atlasSymbols.set(hash, symbol);
|
|
884
1072
|
// `extern` + initializer: a namespace-scope `const` is internal by default,
|
|
885
1073
|
// and `weak` requires external linkage ("weak declaration must be public").
|
|
886
|
-
lines.push(
|
|
1074
|
+
lines.push(
|
|
1075
|
+
`extern __attribute__((weak)) const std::uint8_t ${symbol}[${font.atlasData.length}] = {`,
|
|
1076
|
+
);
|
|
887
1077
|
for (let i = 0; i < font.atlasData.length; i += 32) {
|
|
888
|
-
const chunk = Array.from(
|
|
889
|
-
|
|
1078
|
+
const chunk = Array.from(
|
|
1079
|
+
font.atlasData.slice(i, Math.min(i + 32, font.atlasData.length)),
|
|
1080
|
+
);
|
|
1081
|
+
lines.push(
|
|
1082
|
+
` ${chunk.map((byte) => `0x${byte.toString(16).padStart(2, "0")}`).join(", ")},`,
|
|
1083
|
+
);
|
|
890
1084
|
}
|
|
891
|
-
lines.push(
|
|
892
|
-
lines.push(
|
|
1085
|
+
lines.push("};");
|
|
1086
|
+
lines.push("");
|
|
893
1087
|
}
|
|
894
1088
|
|
|
895
1089
|
for (const font of fonts) {
|
|
896
1090
|
// `atlasBits` defaults to 8 in RasterizedFontData, so an 8-bit atlas leaves
|
|
897
1091
|
// the trailing initializer off entirely and keeps this TU byte-identical to
|
|
898
1092
|
// what every board generated before 2-bit atlases existed.
|
|
899
|
-
const atlasBitsInit = font.atlasBits === 8 ?
|
|
900
|
-
lines.push(
|
|
1093
|
+
const atlasBitsInit = font.atlasBits === 8 ? "" : `, ${font.atlasBits}`;
|
|
1094
|
+
lines.push(
|
|
1095
|
+
`static const Glyph font_glyphs_${font.id}[${font.glyphs.length}] = {`,
|
|
1096
|
+
);
|
|
901
1097
|
for (const glyph of font.glyphs) {
|
|
902
1098
|
lines.push(
|
|
903
|
-
` { ${glyph.codepoint}, ${glyph.sourceX}, ${glyph.sourceY}, ${glyph.width}, ${glyph.height}, ${glyph.advance}, ${glyph.bearingX}, ${glyph.bearingY} },`,
|
|
904
|
-
)
|
|
1099
|
+
` { ${glyph.codepoint}, ${glyph.sourceX}, ${glyph.sourceY}, ${glyph.width}, ${glyph.height}, ${glyph.advance}, ${glyph.bearingX}, ${glyph.bearingY}${glyph.advance16 === undefined ? "" : `, ${glyph.advance16}`} },`,
|
|
1100
|
+
);
|
|
1101
|
+
}
|
|
1102
|
+
lines.push("};");
|
|
1103
|
+
lines.push("");
|
|
1104
|
+
|
|
1105
|
+
let importedFields = "";
|
|
1106
|
+
if (font.importedBitmap) {
|
|
1107
|
+
if (font.kerning.length > 0) {
|
|
1108
|
+
lines.push(
|
|
1109
|
+
`static const gea::framework::graphics::FontKerningPair font_kerning_${font.id}[${font.kerning.length}] = {`,
|
|
1110
|
+
);
|
|
1111
|
+
for (const pair of font.kerning)
|
|
1112
|
+
lines.push(
|
|
1113
|
+
` { ${pair.left}, ${pair.right}, ${pair.adjustment16} },`,
|
|
1114
|
+
);
|
|
1115
|
+
lines.push("};");
|
|
1116
|
+
}
|
|
1117
|
+
importedFields = `, 8, ${font.kerning.length}, ${font.kerning.length ? `font_kerning_${font.id}` : "nullptr"}, true, ${font.fallbackCodepoint}`;
|
|
905
1118
|
}
|
|
906
|
-
lines.push('};')
|
|
907
|
-
lines.push('')
|
|
908
|
-
|
|
909
1119
|
lines.push(
|
|
910
|
-
`static const RasterizedFontData font_data_${font.id} = { ${font.id}, ${font.sizePx}, ${font.lineHeight}, ${font.ascender}, ${font.descender}, ${font.glyphs.length}, font_glyphs_${font.id}, ${font.atlasWidth}, ${font.atlasHeight}, ${font.atlasSymbol}${atlasBitsInit} };`,
|
|
911
|
-
)
|
|
912
|
-
lines.push(
|
|
1120
|
+
`static const RasterizedFontData font_data_${font.id} = { ${font.id}, ${font.sizePx}, ${font.lineHeight}, ${font.ascender}, ${font.descender}, ${font.glyphs.length}, font_glyphs_${font.id}, ${font.atlasWidth}, ${font.atlasHeight}, ${font.atlasSymbol}${font.importedBitmap ? importedFields : atlasBitsInit} };`,
|
|
1121
|
+
);
|
|
1122
|
+
lines.push("");
|
|
913
1123
|
}
|
|
914
1124
|
|
|
915
|
-
lines.push(
|
|
916
|
-
lines.push(
|
|
917
|
-
lines.push(
|
|
918
|
-
lines.push(
|
|
919
|
-
lines.push(
|
|
920
|
-
lines.push(
|
|
921
|
-
for (const font of fonts)
|
|
922
|
-
|
|
923
|
-
lines.push(
|
|
924
|
-
lines.push(
|
|
925
|
-
lines.push(
|
|
926
|
-
lines.push(
|
|
927
|
-
lines.push(
|
|
928
|
-
lines.push(
|
|
929
|
-
lines.push(
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
lines.push(
|
|
934
|
-
lines.push(
|
|
935
|
-
lines.push(
|
|
936
|
-
lines.push(
|
|
937
|
-
lines.push(
|
|
938
|
-
lines.push(
|
|
939
|
-
lines.push(
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
lines.push(
|
|
943
|
-
lines.push(
|
|
944
|
-
lines.push(
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
lines.push(
|
|
948
|
-
lines.push(
|
|
949
|
-
lines.push(
|
|
950
|
-
lines.push(
|
|
951
|
-
lines.push(
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
lines.push(
|
|
955
|
-
lines.push(
|
|
956
|
-
lines.push(
|
|
957
|
-
lines.push(
|
|
958
|
-
lines.push(
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
lines.push(
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
lines.push(
|
|
965
|
-
lines.push(
|
|
966
|
-
lines.push(
|
|
967
|
-
lines.push(
|
|
968
|
-
lines.push(
|
|
969
|
-
lines.push(
|
|
970
|
-
lines.push(
|
|
971
|
-
lines.push(
|
|
972
|
-
lines.push(
|
|
973
|
-
lines.push(
|
|
974
|
-
lines.push(
|
|
975
|
-
lines.push(
|
|
976
|
-
lines.push(
|
|
977
|
-
lines.push(
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
1125
|
+
lines.push("struct GeneratedFontEntry {");
|
|
1126
|
+
lines.push(" int familyId;");
|
|
1127
|
+
lines.push(" const RasterizedFontData *font;");
|
|
1128
|
+
lines.push("};");
|
|
1129
|
+
lines.push("");
|
|
1130
|
+
lines.push("static const GeneratedFontEntry generated_fonts[] = {");
|
|
1131
|
+
for (const font of fonts)
|
|
1132
|
+
lines.push(` { ${font.familyId}, &font_data_${font.id} },`);
|
|
1133
|
+
lines.push("};");
|
|
1134
|
+
lines.push("");
|
|
1135
|
+
lines.push("struct GeneratedFontFamily {");
|
|
1136
|
+
lines.push(" int id;");
|
|
1137
|
+
lines.push(" const char *name;");
|
|
1138
|
+
lines.push("};");
|
|
1139
|
+
lines.push("");
|
|
1140
|
+
lines.push("static const GeneratedFontFamily generated_families[] = {");
|
|
1141
|
+
for (const family of families)
|
|
1142
|
+
lines.push(` { ${family.id}, ${cppString(family.name)} },`);
|
|
1143
|
+
lines.push("};");
|
|
1144
|
+
lines.push("");
|
|
1145
|
+
lines.push(`void ${names.ensureLinked}()`);
|
|
1146
|
+
lines.push("{");
|
|
1147
|
+
lines.push("}");
|
|
1148
|
+
lines.push("");
|
|
1149
|
+
lines.push(
|
|
1150
|
+
`const gea::framework::graphics::RasterizedFontData *${names.lookupFont}(int fontId)`,
|
|
1151
|
+
);
|
|
1152
|
+
lines.push("{");
|
|
1153
|
+
lines.push(" for (const auto &entry : generated_fonts) {");
|
|
1154
|
+
lines.push(
|
|
1155
|
+
" if (entry.font && entry.font->id == fontId) return entry.font;",
|
|
1156
|
+
);
|
|
1157
|
+
lines.push(" }");
|
|
1158
|
+
lines.push(" return nullptr;");
|
|
1159
|
+
lines.push("}");
|
|
1160
|
+
lines.push("");
|
|
1161
|
+
lines.push(
|
|
1162
|
+
`const gea::framework::graphics::RasterizedFontData *${names.lookupFontForFamily}(int familyId, int sizePx)`,
|
|
1163
|
+
);
|
|
1164
|
+
lines.push("{");
|
|
1165
|
+
lines.push(" const RasterizedFontData *best = nullptr;");
|
|
1166
|
+
lines.push(" int bestDiff = 2147483647;");
|
|
1167
|
+
lines.push(" for (const auto &entry : generated_fonts) {");
|
|
1168
|
+
lines.push(
|
|
1169
|
+
" if (entry.familyId != familyId || !entry.font) continue;",
|
|
1170
|
+
);
|
|
1171
|
+
lines.push(
|
|
1172
|
+
" int diff = sizePx > 0 ? entry.font->sizePx - sizePx : 0;",
|
|
1173
|
+
);
|
|
1174
|
+
lines.push(" if (diff < 0) diff = -diff;");
|
|
1175
|
+
lines.push(" if (diff < bestDiff) {");
|
|
1176
|
+
lines.push(" best = entry.font;");
|
|
1177
|
+
lines.push(" bestDiff = diff;");
|
|
1178
|
+
lines.push(" }");
|
|
1179
|
+
lines.push(" }");
|
|
1180
|
+
lines.push(" return best;");
|
|
1181
|
+
lines.push("}");
|
|
1182
|
+
lines.push("");
|
|
1183
|
+
lines.push(`int ${names.lookupFontFamily}(const char *family)`);
|
|
1184
|
+
lines.push("{");
|
|
1185
|
+
lines.push(" if (!family) return -1;");
|
|
1186
|
+
lines.push(" for (const auto &entry : generated_families) {");
|
|
1187
|
+
lines.push(
|
|
1188
|
+
" if (std::strcmp(entry.name, family) == 0) return entry.id;",
|
|
1189
|
+
);
|
|
1190
|
+
lines.push(" }");
|
|
1191
|
+
lines.push(" return -1;");
|
|
1192
|
+
lines.push("}");
|
|
1193
|
+
lines.push("");
|
|
1194
|
+
lines.push(`const char *${names.lookupFontFamilyName}(int familyId)`);
|
|
1195
|
+
lines.push("{");
|
|
1196
|
+
lines.push(" for (const auto &entry : generated_families) {");
|
|
1197
|
+
lines.push(" if (entry.id == familyId) return entry.name;");
|
|
1198
|
+
lines.push(" }");
|
|
1199
|
+
lines.push(" return nullptr;");
|
|
1200
|
+
lines.push("}");
|
|
1201
|
+
lines.push("");
|
|
1202
|
+
if (names.namespaceClose) lines.push(names.namespaceClose);
|
|
1203
|
+
lines.push("");
|
|
1204
|
+
return lines.join("\n");
|
|
981
1205
|
}
|
|
982
1206
|
|
|
983
1207
|
function generatedRuntimeTtfCpp(runtimeFonts, families) {
|
|
984
|
-
const names = fontFunctionNames()
|
|
1208
|
+
const names = fontFunctionNames();
|
|
985
1209
|
const lines = [
|
|
986
1210
|
'#include "gea_embedded_font_generated.h"',
|
|
987
1211
|
'#include "memory.h"',
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
1212
|
+
"#include <cstddef>",
|
|
1213
|
+
"#include <cstring>",
|
|
1214
|
+
"",
|
|
991
1215
|
'extern "C" int stbi_zlib_decode_buffer(char *obuffer, int olen, const char *ibuffer, int ilen);',
|
|
992
|
-
|
|
993
|
-
]
|
|
1216
|
+
"",
|
|
1217
|
+
];
|
|
994
1218
|
if (names.namespaceOpen) {
|
|
995
|
-
lines.push(names.namespaceOpen)
|
|
996
|
-
lines.push(
|
|
1219
|
+
lines.push(names.namespaceOpen);
|
|
1220
|
+
lines.push("");
|
|
997
1221
|
} else {
|
|
998
|
-
lines.push(
|
|
999
|
-
lines.push(
|
|
1222
|
+
lines.push("using gea::framework::graphics::RasterizedFontData;");
|
|
1223
|
+
lines.push("");
|
|
1000
1224
|
}
|
|
1001
1225
|
|
|
1002
1226
|
for (const font of runtimeFonts) {
|
|
1003
|
-
const symbol = `gea_runtime_ttf_${font.id}
|
|
1004
|
-
const compressed = zlib.deflateSync(font.data, { level: 9 })
|
|
1005
|
-
font.byteSymbol = `${symbol}_deflated
|
|
1006
|
-
font.byteLengthSymbol = `${symbol}_deflated_len
|
|
1007
|
-
font.uncompressedLength = font.data.length
|
|
1008
|
-
lines.push(
|
|
1227
|
+
const symbol = `gea_runtime_ttf_${font.id}`;
|
|
1228
|
+
const compressed = zlib.deflateSync(font.data, { level: 9 });
|
|
1229
|
+
font.byteSymbol = `${symbol}_deflated`;
|
|
1230
|
+
font.byteLengthSymbol = `${symbol}_deflated_len`;
|
|
1231
|
+
font.uncompressedLength = font.data.length;
|
|
1232
|
+
lines.push(
|
|
1233
|
+
`extern __attribute__((weak)) const std::uint8_t ${font.byteSymbol}[${compressed.length}] = {`,
|
|
1234
|
+
);
|
|
1009
1235
|
for (let i = 0; i < compressed.length; i += 32) {
|
|
1010
|
-
const chunk = Array.from(
|
|
1011
|
-
|
|
1236
|
+
const chunk = Array.from(
|
|
1237
|
+
compressed.slice(i, Math.min(i + 32, compressed.length)),
|
|
1238
|
+
);
|
|
1239
|
+
lines.push(
|
|
1240
|
+
` ${chunk.map((byte) => `0x${byte.toString(16).padStart(2, "0")}`).join(", ")},`,
|
|
1241
|
+
);
|
|
1012
1242
|
}
|
|
1013
|
-
lines.push(
|
|
1014
|
-
lines.push(
|
|
1015
|
-
|
|
1016
|
-
|
|
1243
|
+
lines.push("};");
|
|
1244
|
+
lines.push(
|
|
1245
|
+
`extern __attribute__((weak)) const unsigned long ${font.byteLengthSymbol} = ${compressed.length}UL;`,
|
|
1246
|
+
);
|
|
1247
|
+
lines.push(
|
|
1248
|
+
`extern __attribute__((weak)) const unsigned long ${symbol}_len = ${font.data.length}UL;`,
|
|
1249
|
+
);
|
|
1250
|
+
lines.push("");
|
|
1017
1251
|
}
|
|
1018
1252
|
|
|
1019
|
-
lines.push(
|
|
1020
|
-
lines.push(
|
|
1021
|
-
lines.push(
|
|
1022
|
-
lines.push(
|
|
1023
|
-
lines.push(
|
|
1024
|
-
lines.push(
|
|
1025
|
-
for (const family of families)
|
|
1026
|
-
|
|
1027
|
-
lines.push(
|
|
1028
|
-
lines.push(
|
|
1029
|
-
lines.push(
|
|
1030
|
-
lines.push(
|
|
1031
|
-
lines.push(
|
|
1032
|
-
lines.push(
|
|
1033
|
-
lines.push(
|
|
1034
|
-
lines.push(
|
|
1035
|
-
lines.push(
|
|
1036
|
-
lines.push(
|
|
1253
|
+
lines.push("struct GeneratedFontFamily {");
|
|
1254
|
+
lines.push(" int id;");
|
|
1255
|
+
lines.push(" const char *name;");
|
|
1256
|
+
lines.push("};");
|
|
1257
|
+
lines.push("");
|
|
1258
|
+
lines.push("static const GeneratedFontFamily generated_families[] = {");
|
|
1259
|
+
for (const family of families)
|
|
1260
|
+
lines.push(` { ${family.id}, ${cppString(family.name)} },`);
|
|
1261
|
+
lines.push("};");
|
|
1262
|
+
lines.push("");
|
|
1263
|
+
lines.push("struct RuntimeTtfFontEntry {");
|
|
1264
|
+
lines.push(" int familyId;");
|
|
1265
|
+
lines.push(" const std::uint8_t *compressedBytes;");
|
|
1266
|
+
lines.push(" const unsigned long *compressedLength;");
|
|
1267
|
+
lines.push(" unsigned long uncompressedLength;");
|
|
1268
|
+
lines.push(" std::uint8_t *decodedBytes;");
|
|
1269
|
+
lines.push("};");
|
|
1270
|
+
lines.push("");
|
|
1271
|
+
lines.push("static RuntimeTtfFontEntry runtime_ttf_fonts[] = {");
|
|
1037
1272
|
for (const font of runtimeFonts) {
|
|
1038
|
-
lines.push(
|
|
1273
|
+
lines.push(
|
|
1274
|
+
` { ${font.familyId}, ${font.byteSymbol}, &${font.byteLengthSymbol}, ${font.uncompressedLength}UL, nullptr },`,
|
|
1275
|
+
);
|
|
1039
1276
|
}
|
|
1040
|
-
lines.push(
|
|
1041
|
-
lines.push(
|
|
1042
|
-
lines.push(
|
|
1043
|
-
lines.push(
|
|
1044
|
-
lines.push(
|
|
1045
|
-
lines.push(
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
lines.push(
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
lines.push(
|
|
1052
|
-
lines.push(
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
lines.push(
|
|
1056
|
-
lines.push(
|
|
1057
|
-
lines.push(
|
|
1058
|
-
lines.push(
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
lines.push(
|
|
1062
|
-
lines.push(
|
|
1063
|
-
lines.push(
|
|
1064
|
-
lines.push(
|
|
1065
|
-
lines.push(
|
|
1066
|
-
lines.push(
|
|
1067
|
-
lines.push(
|
|
1068
|
-
lines.push(
|
|
1069
|
-
lines.push(
|
|
1070
|
-
lines.push(
|
|
1071
|
-
lines.push(
|
|
1072
|
-
lines.push(
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
lines.push(
|
|
1076
|
-
lines.push(
|
|
1077
|
-
lines.push(
|
|
1078
|
-
lines.push(
|
|
1079
|
-
lines.push(
|
|
1080
|
-
lines.push(
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
lines.push(
|
|
1084
|
-
lines.push(
|
|
1085
|
-
lines.push(
|
|
1086
|
-
lines.push(
|
|
1087
|
-
lines.push(
|
|
1088
|
-
lines.push(
|
|
1089
|
-
lines.push(
|
|
1090
|
-
lines.push(
|
|
1091
|
-
lines.push(
|
|
1092
|
-
lines.push(
|
|
1093
|
-
lines.push(
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
lines.push(
|
|
1097
|
-
lines.push(
|
|
1098
|
-
lines.push(
|
|
1099
|
-
lines.push(
|
|
1100
|
-
lines.push(
|
|
1101
|
-
lines.push(
|
|
1102
|
-
lines.push(
|
|
1103
|
-
lines.push(
|
|
1104
|
-
lines.push(
|
|
1105
|
-
|
|
1106
|
-
lines.push(
|
|
1107
|
-
|
|
1277
|
+
lines.push("};");
|
|
1278
|
+
lines.push("");
|
|
1279
|
+
lines.push("std::uint8_t *decodeRuntimeTtfFont(RuntimeTtfFontEntry &entry)");
|
|
1280
|
+
lines.push("{");
|
|
1281
|
+
lines.push(" if (entry.decodedBytes) return entry.decodedBytes;");
|
|
1282
|
+
lines.push(
|
|
1283
|
+
" if (!entry.compressedBytes || !entry.compressedLength || *entry.compressedLength == 0UL || entry.uncompressedLength == 0UL) return nullptr;",
|
|
1284
|
+
);
|
|
1285
|
+
lines.push(
|
|
1286
|
+
" auto *decoded = static_cast<std::uint8_t *>(gea::framework::memory::Allocator::allocatePreferSpiram(entry.uncompressedLength, alignof(std::max_align_t)));",
|
|
1287
|
+
);
|
|
1288
|
+
lines.push(" if (!decoded) return nullptr;");
|
|
1289
|
+
lines.push(
|
|
1290
|
+
" const int written = stbi_zlib_decode_buffer(reinterpret_cast<char *>(decoded),",
|
|
1291
|
+
);
|
|
1292
|
+
lines.push(" static_cast<int>(entry.uncompressedLength),");
|
|
1293
|
+
lines.push(" reinterpret_cast<const char *>(entry.compressedBytes),");
|
|
1294
|
+
lines.push(" static_cast<int>(*entry.compressedLength));");
|
|
1295
|
+
lines.push(
|
|
1296
|
+
" if (written != static_cast<int>(entry.uncompressedLength)) {",
|
|
1297
|
+
);
|
|
1298
|
+
lines.push(" gea::framework::memory::Allocator::free(decoded);");
|
|
1299
|
+
lines.push(" return nullptr;");
|
|
1300
|
+
lines.push(" }");
|
|
1301
|
+
lines.push(" entry.decodedBytes = decoded;");
|
|
1302
|
+
lines.push(" return entry.decodedBytes;");
|
|
1303
|
+
lines.push("}");
|
|
1304
|
+
lines.push("");
|
|
1305
|
+
lines.push(`void ${names.ensureLinked}()`);
|
|
1306
|
+
lines.push("{");
|
|
1307
|
+
lines.push("}");
|
|
1308
|
+
lines.push("");
|
|
1309
|
+
lines.push(
|
|
1310
|
+
`const gea::framework::graphics::RasterizedFontData *${names.lookupFont}(int fontId)`,
|
|
1311
|
+
);
|
|
1312
|
+
lines.push("{");
|
|
1313
|
+
lines.push(" (void)fontId;");
|
|
1314
|
+
lines.push(" return nullptr;");
|
|
1315
|
+
lines.push("}");
|
|
1316
|
+
lines.push("");
|
|
1317
|
+
lines.push(
|
|
1318
|
+
`const gea::framework::graphics::RasterizedFontData *${names.lookupFontForFamily}(int familyId, int sizePx)`,
|
|
1319
|
+
);
|
|
1320
|
+
lines.push("{");
|
|
1321
|
+
lines.push(" (void)familyId;");
|
|
1322
|
+
lines.push(" (void)sizePx;");
|
|
1323
|
+
lines.push(" return nullptr;");
|
|
1324
|
+
lines.push("}");
|
|
1325
|
+
lines.push("");
|
|
1326
|
+
lines.push(`int ${names.lookupFontFamily}(const char *family)`);
|
|
1327
|
+
lines.push("{");
|
|
1328
|
+
lines.push(" if (!family) return -1;");
|
|
1329
|
+
lines.push(" for (const auto &entry : generated_families) {");
|
|
1330
|
+
lines.push(
|
|
1331
|
+
" if (std::strcmp(entry.name, family) == 0) return entry.id;",
|
|
1332
|
+
);
|
|
1333
|
+
lines.push(" }");
|
|
1334
|
+
lines.push(" return -1;");
|
|
1335
|
+
lines.push("}");
|
|
1336
|
+
lines.push("");
|
|
1337
|
+
lines.push(`const char *${names.lookupFontFamilyName}(int familyId)`);
|
|
1338
|
+
lines.push("{");
|
|
1339
|
+
lines.push(" for (const auto &entry : generated_families) {");
|
|
1340
|
+
lines.push(" if (entry.id == familyId) return entry.name;");
|
|
1341
|
+
lines.push(" }");
|
|
1342
|
+
lines.push(" return nullptr;");
|
|
1343
|
+
lines.push("}");
|
|
1344
|
+
lines.push("");
|
|
1345
|
+
lines.push(
|
|
1346
|
+
`const std::uint8_t *${names.lookupRuntimeTtfFontForFamily}(int familyId, unsigned long *length)`,
|
|
1347
|
+
);
|
|
1348
|
+
lines.push("{");
|
|
1349
|
+
lines.push(" for (auto &entry : runtime_ttf_fonts) {");
|
|
1350
|
+
lines.push(" if (entry.familyId != familyId) continue;");
|
|
1351
|
+
lines.push(" if (length) *length = entry.uncompressedLength;");
|
|
1352
|
+
lines.push(" return decodeRuntimeTtfFont(entry);");
|
|
1353
|
+
lines.push(" }");
|
|
1354
|
+
lines.push(" if (length) *length = 0UL;");
|
|
1355
|
+
lines.push(" return nullptr;");
|
|
1356
|
+
lines.push("}");
|
|
1357
|
+
lines.push("");
|
|
1358
|
+
if (names.namespaceClose) lines.push(names.namespaceClose);
|
|
1359
|
+
lines.push("");
|
|
1360
|
+
return lines.join("\n");
|
|
1108
1361
|
}
|
|
1109
1362
|
|
|
1110
1363
|
// These TUs are megabytes of generated byte arrays that no human reads, and
|
|
@@ -1113,28 +1366,35 @@ function generatedRuntimeTtfCpp(runtimeFonts, families) {
|
|
|
1113
1366
|
// CI runner, because the stamp below only helps a machine that already paid
|
|
1114
1367
|
// it once). Formatting is therefore opt-in: set GEATSC_FORMAT_CPP=1 when the
|
|
1115
1368
|
// generated output is being read by a person.
|
|
1116
|
-
const CPP_FORMAT_EXTENSIONS = /\.(?:c|cc|cpp|cxx|m|mm|h|hh|hpp|hxx)$/i
|
|
1117
|
-
const CPP_FORMAT_STYLE =
|
|
1118
|
-
|
|
1369
|
+
const CPP_FORMAT_EXTENSIONS = /\.(?:c|cc|cpp|cxx|m|mm|h|hh|hpp|hxx)$/i;
|
|
1370
|
+
const CPP_FORMAT_STYLE =
|
|
1371
|
+
"{BasedOnStyle: LLVM, SortIncludes: false, ColumnLimit: 160}";
|
|
1372
|
+
let clangFormatUnavailable = false;
|
|
1119
1373
|
|
|
1120
1374
|
function formatCppSourceIfNeeded(filePath, contents) {
|
|
1121
|
-
if (!CPP_FORMAT_EXTENSIONS.test(filePath)) return contents
|
|
1122
|
-
if (process.env.GEATSC_FORMAT_CPP !==
|
|
1123
|
-
if (clangFormatUnavailable) return contents
|
|
1124
|
-
const clangFormat = process.env.CLANG_FORMAT ||
|
|
1375
|
+
if (!CPP_FORMAT_EXTENSIONS.test(filePath)) return contents;
|
|
1376
|
+
if (process.env.GEATSC_FORMAT_CPP !== "1") return contents;
|
|
1377
|
+
if (clangFormatUnavailable) return contents;
|
|
1378
|
+
const clangFormat = process.env.CLANG_FORMAT || "clang-format";
|
|
1125
1379
|
try {
|
|
1126
|
-
return execFileSync(
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1380
|
+
return execFileSync(
|
|
1381
|
+
clangFormat,
|
|
1382
|
+
[`--style=${CPP_FORMAT_STYLE}`, `--assume-filename=${filePath}`],
|
|
1383
|
+
{
|
|
1384
|
+
input: contents,
|
|
1385
|
+
encoding: "utf8",
|
|
1386
|
+
maxBuffer: 512 * 1024 * 1024,
|
|
1387
|
+
},
|
|
1388
|
+
);
|
|
1131
1389
|
} catch (error) {
|
|
1132
|
-
if (error?.code ===
|
|
1133
|
-
clangFormatUnavailable = true
|
|
1134
|
-
process.stderr.write(
|
|
1135
|
-
|
|
1390
|
+
if (error?.code === "ENOENT") {
|
|
1391
|
+
clangFormatUnavailable = true;
|
|
1392
|
+
process.stderr.write(
|
|
1393
|
+
"generate-gea-embedded-fonts: clang-format not found; generated C++ will be written unformatted\n",
|
|
1394
|
+
);
|
|
1395
|
+
return contents;
|
|
1136
1396
|
}
|
|
1137
|
-
throw error
|
|
1397
|
+
throw error;
|
|
1138
1398
|
}
|
|
1139
1399
|
}
|
|
1140
1400
|
|
|
@@ -1152,103 +1412,159 @@ function formatCppSourceIfNeeded(filePath, contents) {
|
|
|
1152
1412
|
// re-running clang-format would reproduce exactly the bytes already there.
|
|
1153
1413
|
// Anything else (missing or stale stamp, edited file, different clang-format)
|
|
1154
1414
|
// falls back to the original format-then-compare path.
|
|
1155
|
-
const FORMAT_STAMP_VERSION = 1
|
|
1156
|
-
let clangFormatIdentity
|
|
1415
|
+
const FORMAT_STAMP_VERSION = 1;
|
|
1416
|
+
let clangFormatIdentity;
|
|
1157
1417
|
|
|
1158
1418
|
function sha256(value) {
|
|
1159
|
-
return createHash(
|
|
1419
|
+
return createHash("sha256")
|
|
1420
|
+
.update(value, typeof value === "string" ? "utf8" : undefined)
|
|
1421
|
+
.digest("hex");
|
|
1160
1422
|
}
|
|
1161
1423
|
|
|
1162
1424
|
function formatterIdentity() {
|
|
1163
|
-
if (clangFormatIdentity !== undefined) return clangFormatIdentity
|
|
1164
|
-
const clangFormat = process.env.CLANG_FORMAT ||
|
|
1425
|
+
if (clangFormatIdentity !== undefined) return clangFormatIdentity;
|
|
1426
|
+
const clangFormat = process.env.CLANG_FORMAT || "clang-format";
|
|
1165
1427
|
try {
|
|
1166
|
-
const version = execFileSync(clangFormat, [
|
|
1167
|
-
|
|
1428
|
+
const version = execFileSync(clangFormat, ["--version"], {
|
|
1429
|
+
encoding: "utf8",
|
|
1430
|
+
}).trim();
|
|
1431
|
+
clangFormatIdentity = `${clangFormat} ${version} ${CPP_FORMAT_STYLE}`;
|
|
1168
1432
|
} catch {
|
|
1169
|
-
clangFormatIdentity =
|
|
1433
|
+
clangFormatIdentity = "";
|
|
1170
1434
|
}
|
|
1171
|
-
return clangFormatIdentity
|
|
1435
|
+
return clangFormatIdentity;
|
|
1172
1436
|
}
|
|
1173
1437
|
|
|
1174
1438
|
function formatStampPath(filePath) {
|
|
1175
|
-
return path.join(
|
|
1439
|
+
return path.join(
|
|
1440
|
+
path.dirname(filePath),
|
|
1441
|
+
`.${path.basename(filePath)}.fmtstamp`,
|
|
1442
|
+
);
|
|
1176
1443
|
}
|
|
1177
1444
|
|
|
1178
1445
|
function writeFileIfChanged(filePath, contents) {
|
|
1179
|
-
const formats =
|
|
1446
|
+
const formats =
|
|
1447
|
+
CPP_FORMAT_EXTENSIONS.test(filePath) &&
|
|
1448
|
+
process.env.GEATSC_FORMAT_CPP === "1" &&
|
|
1449
|
+
!clangFormatUnavailable;
|
|
1180
1450
|
if (!formats) {
|
|
1181
|
-
if (
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1451
|
+
if (
|
|
1452
|
+
fs.existsSync(filePath) &&
|
|
1453
|
+
fs.readFileSync(filePath, "utf8") === contents
|
|
1454
|
+
)
|
|
1455
|
+
return false;
|
|
1456
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
1457
|
+
fs.writeFileSync(filePath, contents);
|
|
1458
|
+
return true;
|
|
1185
1459
|
}
|
|
1186
1460
|
|
|
1187
|
-
const stampPath = formatStampPath(filePath)
|
|
1188
|
-
const rawHash = sha256(contents)
|
|
1189
|
-
const identity = formatterIdentity()
|
|
1461
|
+
const stampPath = formatStampPath(filePath);
|
|
1462
|
+
const rawHash = sha256(contents);
|
|
1463
|
+
const identity = formatterIdentity();
|
|
1190
1464
|
if (identity) {
|
|
1191
|
-
let stamp
|
|
1465
|
+
let stamp;
|
|
1192
1466
|
try {
|
|
1193
|
-
stamp = JSON.parse(fs.readFileSync(stampPath,
|
|
1467
|
+
stamp = JSON.parse(fs.readFileSync(stampPath, "utf8"));
|
|
1194
1468
|
} catch {
|
|
1195
|
-
stamp = undefined
|
|
1469
|
+
stamp = undefined;
|
|
1196
1470
|
}
|
|
1197
|
-
if (
|
|
1471
|
+
if (
|
|
1472
|
+
stamp?.version === FORMAT_STAMP_VERSION &&
|
|
1473
|
+
stamp.raw === rawHash &&
|
|
1474
|
+
stamp.identity === identity
|
|
1475
|
+
) {
|
|
1198
1476
|
try {
|
|
1199
|
-
if (sha256(fs.readFileSync(filePath)) === stamp.formatted) return false
|
|
1477
|
+
if (sha256(fs.readFileSync(filePath)) === stamp.formatted) return false;
|
|
1200
1478
|
} catch {
|
|
1201
1479
|
// fall through and format
|
|
1202
1480
|
}
|
|
1203
1481
|
}
|
|
1204
1482
|
}
|
|
1205
1483
|
|
|
1206
|
-
const formattedContents = formatCppSourceIfNeeded(filePath, contents)
|
|
1207
|
-
const changed =
|
|
1484
|
+
const formattedContents = formatCppSourceIfNeeded(filePath, contents);
|
|
1485
|
+
const changed =
|
|
1486
|
+
!fs.existsSync(filePath) ||
|
|
1487
|
+
fs.readFileSync(filePath, "utf8") !== formattedContents;
|
|
1208
1488
|
if (changed) {
|
|
1209
|
-
fs.mkdirSync(path.dirname(filePath), { recursive: true })
|
|
1210
|
-
fs.writeFileSync(filePath, formattedContents)
|
|
1489
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
1490
|
+
fs.writeFileSync(filePath, formattedContents);
|
|
1211
1491
|
}
|
|
1212
1492
|
if (identity) {
|
|
1213
1493
|
try {
|
|
1214
1494
|
fs.writeFileSync(
|
|
1215
1495
|
stampPath,
|
|
1216
|
-
JSON.stringify({
|
|
1217
|
-
|
|
1496
|
+
JSON.stringify({
|
|
1497
|
+
version: FORMAT_STAMP_VERSION,
|
|
1498
|
+
raw: rawHash,
|
|
1499
|
+
formatted: sha256(formattedContents),
|
|
1500
|
+
identity,
|
|
1501
|
+
}),
|
|
1502
|
+
);
|
|
1218
1503
|
} catch {
|
|
1219
1504
|
// a stamp we cannot write only costs the next build the formatting pass
|
|
1220
1505
|
}
|
|
1221
1506
|
}
|
|
1222
|
-
return changed
|
|
1507
|
+
return changed;
|
|
1223
1508
|
}
|
|
1224
1509
|
|
|
1225
1510
|
function writeGeneratedFonts() {
|
|
1226
|
-
const fontFaces = collectSourceFontFaces()
|
|
1227
|
-
const tuples = collectUsedFontTuples(fontFaces)
|
|
1228
|
-
const familyNames = [...new Set(tuples.map((tuple) => tuple.family))].sort()
|
|
1229
|
-
const families = familyNames.map((name, id) => ({ id, name }))
|
|
1230
|
-
const familyIds = new Map(families.map((family) => [family.name, family.id]))
|
|
1231
|
-
|
|
1232
|
-
let fonts = []
|
|
1233
|
-
let runtimeFonts = []
|
|
1511
|
+
const fontFaces = collectSourceFontFaces();
|
|
1512
|
+
const tuples = collectUsedFontTuples(fontFaces);
|
|
1513
|
+
const familyNames = [...new Set(tuples.map((tuple) => tuple.family))].sort();
|
|
1514
|
+
const families = familyNames.map((name, id) => ({ id, name }));
|
|
1515
|
+
const familyIds = new Map(families.map((family) => [family.name, family.id]));
|
|
1516
|
+
|
|
1517
|
+
let fonts = [];
|
|
1518
|
+
let runtimeFonts = [];
|
|
1234
1519
|
if (runtimeTtfFonts) {
|
|
1235
1520
|
runtimeFonts = families.map((family) => {
|
|
1236
|
-
const fontPath = fontFaces.get(family.name)
|
|
1237
|
-
if (!fontPath)
|
|
1238
|
-
|
|
1239
|
-
|
|
1521
|
+
const fontPath = fontFaces.get(family.name);
|
|
1522
|
+
if (!fontPath)
|
|
1523
|
+
fail(`missing @font-face src for font family ${family.name}`);
|
|
1524
|
+
return {
|
|
1525
|
+
id: family.id,
|
|
1526
|
+
familyId: family.id,
|
|
1527
|
+
family: family.name,
|
|
1528
|
+
data: fs.readFileSync(fontPath),
|
|
1529
|
+
};
|
|
1530
|
+
});
|
|
1240
1531
|
} else if (tuples.length > 0) {
|
|
1241
|
-
const opentype = loadOpentype()
|
|
1242
|
-
let fontId = 0
|
|
1532
|
+
const opentype = loadOpentype();
|
|
1533
|
+
let fontId = 0;
|
|
1243
1534
|
fonts = tuples.map((tuple) => {
|
|
1244
|
-
const fontPath = fontFaces.get(tuple.family)
|
|
1245
|
-
if (!fontPath)
|
|
1246
|
-
|
|
1247
|
-
|
|
1535
|
+
const fontPath = fontFaces.get(tuple.family);
|
|
1536
|
+
if (!fontPath)
|
|
1537
|
+
fail(`missing @font-face src for font family ${tuple.family}`);
|
|
1538
|
+
const embedded = fontFaces.embeddedSources.get(tuple.family);
|
|
1539
|
+
if (embedded) {
|
|
1540
|
+
return buildBitmapFont(
|
|
1541
|
+
loadBitmapFontSource(embedded),
|
|
1542
|
+
{ ...tuple, id: fontId++, familyId: familyIds.get(tuple.family) },
|
|
1543
|
+
packGlyphAtlas,
|
|
1544
|
+
);
|
|
1545
|
+
}
|
|
1546
|
+
return rasterizeFont(
|
|
1547
|
+
opentype,
|
|
1548
|
+
fontPath,
|
|
1549
|
+
tuple.family,
|
|
1550
|
+
familyIds.get(tuple.family),
|
|
1551
|
+
tuple.sizePx,
|
|
1552
|
+
fontId++,
|
|
1553
|
+
tuple.charset,
|
|
1554
|
+
);
|
|
1555
|
+
});
|
|
1248
1556
|
}
|
|
1249
1557
|
|
|
1250
|
-
writeFileIfChanged(
|
|
1251
|
-
|
|
1558
|
+
writeFileIfChanged(
|
|
1559
|
+
outH,
|
|
1560
|
+
generatedHeader(fonts.length, families.length, { runtimeTtfFonts }),
|
|
1561
|
+
);
|
|
1562
|
+
writeFileIfChanged(
|
|
1563
|
+
outCpp,
|
|
1564
|
+
runtimeTtfFonts
|
|
1565
|
+
? generatedRuntimeTtfCpp(runtimeFonts, families)
|
|
1566
|
+
: generatedCpp(fonts, families),
|
|
1567
|
+
);
|
|
1252
1568
|
}
|
|
1253
1569
|
|
|
1254
|
-
writeGeneratedFonts()
|
|
1570
|
+
writeGeneratedFonts();
|