rapier-math 1.0.1 → 1.0.2
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 +4 -4
- package/index.mjs +164 -95
- package/math-components.json +6 -6
- package/package.json +3 -3
- package/rapier-math-1.0.2.js +2952 -0
- package/src/math/FONT-SOURCE.md +32 -6
- package/src/math/api.js +4 -3
- package/src/math/browser.js +60 -29
- package/src/math/build-font.py +237 -95
- package/src/math/fallbacks.js +5 -5
- package/src/math/font.js +59 -26
- package/src/math/layout.js +36 -32
- package/src/package.json +1 -1
- package/src/shell/math-resources.json +22 -22
- package/src/tools/build-math.mjs +5 -2
- package/rapier-math-1.0.1.js +0 -2883
package/src/math/FONT-SOURCE.md
CHANGED
|
@@ -82,7 +82,7 @@ vertical axis. Measurements are in design units unless specified otherwise.
|
|
|
82
82
|
| `units`, `xHeight` | Math units per em and nominal x-height. |
|
|
83
83
|
| `textUnits`, `textXHeight` | Geist units per em and nominal x-height. |
|
|
84
84
|
| `chars` | Decimal Unicode code point to glyph index. |
|
|
85
|
-
| `
|
|
85
|
+
| `glyph(id)` | Demand-decoded `[advance, xMin, yMin, xMax, yMax, italicCorrection, topAccent, path]`. Missing IDs return `undefined`. |
|
|
86
86
|
| `constants` | All 56 original OpenType `MathConstants`; percentage constants remain percentages. |
|
|
87
87
|
| `vertical`, `horizontal` | Glyph index to `{variants, assembly?}`. |
|
|
88
88
|
| `variants` | `[glyphIndex, advanceMeasurement]` pairs in font order. |
|
|
@@ -94,7 +94,7 @@ vertical axis. Measurements are in design units unless specified otherwise.
|
|
|
94
94
|
| `flatAccents`, `dotless` | Alternate glyph indices for `flac` and `dtls`. |
|
|
95
95
|
| `extendedShapes` | Glyphs marked by `MathGlyphInfo.ExtendedShapeCoverage`. |
|
|
96
96
|
| `edgeStep` | Height of each contour strip: 32 design units. |
|
|
97
|
-
| `
|
|
97
|
+
| `profile(id)` | Demand-decoded `[y0, [xMin, xMax], null, ...]` for a glyph, or `undefined` when absent; each following entry describes one strip. |
|
|
98
98
|
|
|
99
99
|
Outline bounds round outward to whole units. The outlines themselves are unchanged, including exact fractional
|
|
100
100
|
coordinates if present. Missing italic corrections are zero; missing accent attachments use half the advance.
|
|
@@ -107,10 +107,36 @@ dotless forms. Entry `i + 1` bounds the original contour within `[y0 + i * edgeS
|
|
|
107
107
|
with a small numerical guard. Profiles are derived spacing data; they do not replace outlines or pretend to be
|
|
108
108
|
an original MATH kern table. Layout compares intersecting strips from both glyphs when placing scripts.
|
|
109
109
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
110
|
+
Character mappings, optical substitutions and record offsets occupy a compact index decoded at module load.
|
|
111
|
+
Glyph metrics, outlines and contour profiles stay in independent raw DEFLATE pages until requested. Every page
|
|
112
|
+
ends at a record boundary and contains at most 16,384 decoded bytes. The complete set of pages travels in the
|
|
113
|
+
same asset; rare characters, stretch parts and optical forms require no additional download.
|
|
114
|
+
|
|
115
|
+
Outlines share exactly matching contours after translation. The 8,115 ordered contour references use 4,970
|
|
116
|
+
unique contour records. A record either stores its original command structure and relative coordinates, or
|
|
117
|
+
exact differences from an earlier contour with the same structure. Parent chains have at most four links.
|
|
118
|
+
The generator prefers small coordinate differences as well as short records, because the smallest initial
|
|
119
|
+
record does not necessarily produce the smallest compressed page. Glyph assembly restores the original
|
|
120
|
+
absolute `M/L/H/V/C/Z` paths, including contour order; existing outline-part consumers use those commands.
|
|
121
|
+
|
|
122
|
+
Glyph metrics use reversible bearing and half-advance predictors with a median origin per metric field.
|
|
123
|
+
They retain the complete original bounds, advance, italic and accent measurements. Signed record integers
|
|
124
|
+
use zigzag values: values below 240 take one byte, the next 3,840 values take two bytes, and larger values use
|
|
125
|
+
an escape followed by unsigned LEB128. The coordinate unit is exact; this encoding performs no quantization.
|
|
126
|
+
|
|
127
|
+
Profiles retain the original left/right strip coordinates and exact differences from matching strip structures.
|
|
128
|
+
Identical arrays share storage: all 1,187 glyph-to-profile mappings are represented by 1,080 unique records.
|
|
129
|
+
The renderer only asks for profiles when side-script placement can use them.
|
|
130
|
+
|
|
131
|
+
The font owns three bounded least-recently-used caches: eight inflated pages, 256 completed glyph records and
|
|
132
|
+
128 contour profiles. Temporary parent coordinates and page views are discarded after a glyph is assembled;
|
|
133
|
+
cached glyphs contain the completed path and metrics. Reusing an evicted record reconstructs the same data.
|
|
134
|
+
These caches contain font data only. They retain no formula source, parse tree, layout result, SVG element or
|
|
135
|
+
document. Returned internal records are read-only to consumers.
|
|
136
|
+
|
|
137
|
+
The synchronous decoder checks compressed input, output size, record boundaries and parent depth. It requires
|
|
138
|
+
no network, font installation, font parser or platform font shaping at runtime. `glyph` and `profile` are
|
|
139
|
+
internal font accessors; the public parser and rendering API remain independent of this storage format.
|
|
114
140
|
|
|
115
141
|
The format follows the [OpenType MATH specification](https://learn.microsoft.com/en-us/typography/opentype/spec/math)
|
|
116
142
|
and [RFC 1951](https://www.rfc-editor.org/rfc/rfc1951). Three retained constants have no active consumer:
|
package/src/math/api.js
CHANGED
|
@@ -6,7 +6,8 @@ function mathSvg(box,source,error,utf16,options={}) {
|
|
|
6
6
|
const viewBox=[left*1000,-height*1000,width*1000,(height+depth)*1000];
|
|
7
7
|
const opacity=options.screen?.dark ? .94 : 1;
|
|
8
8
|
if (![width,height,depth,left,...viewBox].every(Number.isFinite)) throw new Error('Mathematical dimensions are not finite');
|
|
9
|
-
const
|
|
9
|
+
const escapedSource=mathEscape(source);
|
|
10
|
+
const svg='<svg xmlns="http://www.w3.org/2000/svg" role="img" aria-label="'+escapedSource+'"'+
|
|
10
11
|
' data-rapier-math="'+(options.displayMode?'display':'inline')+'" data-math-lines="'+(box.lines || 1)+'"'+
|
|
11
12
|
' data-math-natural-width="'+mathNumber(options.naturalWidth ?? width)+'"'+
|
|
12
13
|
(options.screen?' data-math-screen="'+mathNumber(options.screen.fontSize)+'|'+mathNumber(options.screen.pixelRatio)+'|'+options.screen.dark+'"':'')+
|
|
@@ -16,7 +17,7 @@ function mathSvg(box,source,error,utf16,options={}) {
|
|
|
16
17
|
' viewBox="'+viewBox.map(mathNumber).join(' ')+'"'+
|
|
17
18
|
' style="font-size:'+mathNumber(mathFontRatio)+'em;max-width:100%;height:auto;vertical-align:-'+mathNumber(depth)+'em;'+
|
|
18
19
|
'fill-opacity:'+opacity+';stroke-opacity:'+opacity+'" fill="currentColor">'+
|
|
19
|
-
'<title>'+
|
|
20
|
+
'<title>'+escapedSource+'</title>'+(error?'<desc>'+mathEscape(error.message)+'</desc>':'')+
|
|
20
21
|
'<g transform="scale(1000)">'+box.svg+'</g></svg>';
|
|
21
22
|
return {svg,width,height,depth,lines:box.lines || 1,fitted:!!box.fitted,errors:error?[error]:[]};
|
|
22
23
|
}
|
|
@@ -77,7 +78,7 @@ function renderMath(source,options={}) {
|
|
|
77
78
|
}
|
|
78
79
|
|
|
79
80
|
globalThis.RapierMath=Object.freeze({
|
|
80
|
-
version:'1.0.
|
|
81
|
+
version:'1.0.2',
|
|
81
82
|
parse:parseMathTex,
|
|
82
83
|
render:renderMath,
|
|
83
84
|
fit:mathFit,
|
package/src/math/browser.js
CHANGED
|
@@ -116,35 +116,66 @@ function mathFit(root) {
|
|
|
116
116
|
return nodes.length;
|
|
117
117
|
}
|
|
118
118
|
|
|
119
|
+
function mathObserveDocument(doc) {
|
|
120
|
+
const view=doc.defaultView,nodes=new Map(),columns=new Set();
|
|
121
|
+
let frame=null,active=true,printing=view.matchMedia('print').matches;
|
|
122
|
+
const refresh=()=> {
|
|
123
|
+
if(!active)return;
|
|
124
|
+
if(frame!==null)view.cancelAnimationFrame(frame);
|
|
125
|
+
frame=null;
|
|
126
|
+
const wanted=new Set();
|
|
127
|
+
for(const svg of nodes.keys()) {
|
|
128
|
+
if(!svg.isConnected || svg.ownerDocument!==doc || !svg.matches(mathSvgSelector))continue;
|
|
129
|
+
const column=mathFitElement(svg,printing);if(column)wanted.add(column);
|
|
130
|
+
}
|
|
131
|
+
for(const column of columns)if(!wanted.has(column)) {resize.unobserve(column);columns.delete(column);}
|
|
132
|
+
for(const column of wanted)if(!columns.has(column)) {resize.observe(column);columns.add(column);}
|
|
133
|
+
};
|
|
134
|
+
const schedule=()=>{if(active && frame===null)frame=view.requestAnimationFrame(refresh);};
|
|
135
|
+
const resize=new view.ResizeObserver(schedule),theme=new view.MutationObserver(records=>{
|
|
136
|
+
for(const {target} of records)for(const svg of nodes.keys())
|
|
137
|
+
if(target!==svg && target.contains(svg)) {schedule();return;}
|
|
138
|
+
});
|
|
139
|
+
const beforePrint=()=>{printing=true;refresh();},afterPrint=()=>{printing=false;schedule();};
|
|
140
|
+
const media=['(forced-colors: active)','(prefers-contrast: more)','(prefers-color-scheme: dark)'].map(query=>view.matchMedia(query));
|
|
141
|
+
const fonts=doc.fonts;
|
|
142
|
+
theme.observe(doc.documentElement,{subtree:true,attributes:true,attributeFilter:['class','style','data-rapier-theme','data-md-theme']});
|
|
143
|
+
view.addEventListener('resize',schedule,{passive:true});
|
|
144
|
+
view.addEventListener('beforeprint',beforePrint);view.addEventListener('afterprint',afterPrint);
|
|
145
|
+
for(const query of media)query.addEventListener('change',schedule);
|
|
146
|
+
fonts?.addEventListener('loadingdone',schedule);
|
|
147
|
+
const state={nodes,schedule,dispose:()=>{
|
|
148
|
+
active=false;
|
|
149
|
+
if(frame!==null)view.cancelAnimationFrame(frame);
|
|
150
|
+
frame=null;
|
|
151
|
+
resize.disconnect();theme.disconnect();columns.clear();nodes.clear();
|
|
152
|
+
view.removeEventListener('resize',schedule);
|
|
153
|
+
view.removeEventListener('beforeprint',beforePrint);view.removeEventListener('afterprint',afterPrint);
|
|
154
|
+
for(const query of media)query.removeEventListener('change',schedule);
|
|
155
|
+
fonts?.removeEventListener('loadingdone',schedule);
|
|
156
|
+
mathBrowserDocuments.delete(doc);
|
|
157
|
+
}};
|
|
158
|
+
mathBrowserDocuments.set(doc,state);
|
|
159
|
+
return state;
|
|
160
|
+
}
|
|
161
|
+
|
|
119
162
|
function mathObserve(root) {
|
|
120
|
-
const doc=root?.ownerDocument || root
|
|
121
|
-
if(!
|
|
122
|
-
let
|
|
123
|
-
if(!
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
for(const [svg,column] of nodes) {
|
|
128
|
-
if(!svg.isConnected) {nodes.delete(svg);continue;}
|
|
129
|
-
const next=mathFitElement(svg,printing);if(next)nodes.set(svg,next);
|
|
130
|
-
}
|
|
131
|
-
const wanted=new Set(nodes.values());
|
|
132
|
-
for(const column of columns)if(!wanted.has(column)) {resize.unobserve(column);columns.delete(column);}
|
|
133
|
-
for(const column of wanted)if(!columns.has(column)) {resize.observe(column);columns.add(column);}
|
|
134
|
-
};
|
|
135
|
-
const schedule=()=>{if(!frame)frame=view.requestAnimationFrame(refresh);};
|
|
136
|
-
const resize=new view.ResizeObserver(schedule),theme=new view.MutationObserver(records=>{
|
|
137
|
-
if(records.some(({target})=>[...nodes.keys()].some(svg=>target!==svg && target.contains(svg))))schedule();
|
|
138
|
-
});
|
|
139
|
-
theme.observe(doc.documentElement,{subtree:true,attributes:true,attributeFilter:['class','style','data-rapier-theme','data-md-theme']});
|
|
140
|
-
view.addEventListener('resize',schedule,{passive:true});
|
|
141
|
-
view.addEventListener('beforeprint',()=>{printing=true;refresh();});view.addEventListener('afterprint',()=>{printing=false;schedule();});
|
|
142
|
-
for(const query of ['(forced-colors: active)','(prefers-contrast: more)','(prefers-color-scheme: dark)'])view.matchMedia(query).addEventListener('change',schedule);
|
|
143
|
-
doc.fonts?.addEventListener('loadingdone',schedule);
|
|
144
|
-
state={nodes,schedule};mathBrowserDocuments.set(doc,state);
|
|
145
|
-
}
|
|
146
|
-
const nodes=mathElements(root);
|
|
147
|
-
for(const svg of nodes)state.nodes.set(svg,mathColumn(svg));
|
|
163
|
+
const doc=root?.ownerDocument || root;
|
|
164
|
+
if(!doc?.defaultView)return ()=>{};
|
|
165
|
+
let owned=mathElements(root);
|
|
166
|
+
if(!owned.length)return ()=>{};
|
|
167
|
+
let state=mathBrowserDocuments.get(doc) || mathObserveDocument(doc);
|
|
168
|
+
// Each returned cleanup owns its subscription, including overlap and detached nodes.
|
|
169
|
+
for(const svg of owned)state.nodes.set(svg,(state.nodes.get(svg) || 0)+1);
|
|
148
170
|
state.schedule();
|
|
149
|
-
return ()=>{
|
|
171
|
+
return ()=>{
|
|
172
|
+
if(!owned)return;
|
|
173
|
+
const current=state;state=null;
|
|
174
|
+
for(const svg of owned) {
|
|
175
|
+
const remaining=current.nodes.get(svg)-1;
|
|
176
|
+
if(remaining)current.nodes.set(svg,remaining);else current.nodes.delete(svg);
|
|
177
|
+
}
|
|
178
|
+
owned=null;
|
|
179
|
+
if(current.nodes.size)current.schedule();else current.dispose();
|
|
180
|
+
};
|
|
150
181
|
}
|
package/src/math/build-font.py
CHANGED
|
@@ -257,21 +257,39 @@ def edge_profile(glyph_set, name):
|
|
|
257
257
|
return [first, *([math.floor(min(strip) - 1e-7), math.ceil(max(strip) + 1e-7)] if strip else None for strip in strips)]
|
|
258
258
|
|
|
259
259
|
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
260
|
+
PAGE_BYTES = 16384
|
|
261
|
+
PAGE_STRIDE = 32768
|
|
262
|
+
PARENT_DEPTH = 4
|
|
263
|
+
SCALAR_MAPS = ("chars", "dotless", "flatAccents", "extendedShapes")
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
def pack_font(data):
|
|
267
|
+
"""Share translated contours and retain independently inflatable record pages."""
|
|
268
|
+
glyphs, profiles = data["glyphs"], data["edgeProfiles"]
|
|
269
|
+
commands = "MLHVCZ"
|
|
270
|
+
lengths = (2, 2, 1, 1, 6, 0)
|
|
263
271
|
|
|
264
272
|
def unsigned(value, target):
|
|
273
|
+
if value < 0 or value > 2**53 - 1:
|
|
274
|
+
raise ValueError("Font integer exceeds exact JavaScript arithmetic")
|
|
265
275
|
while value >= 128:
|
|
266
276
|
target.append((value & 127) | 128)
|
|
267
277
|
value >>= 7
|
|
268
278
|
target.append(value)
|
|
269
279
|
|
|
270
280
|
def signed(value, target):
|
|
271
|
-
|
|
281
|
+
value = value * 2 if value >= 0 else -value * 2 - 1
|
|
282
|
+
if value > 2**53 - 1:
|
|
283
|
+
raise ValueError("Font integer exceeds exact JavaScript arithmetic")
|
|
284
|
+
if value < 240:
|
|
285
|
+
target.append(value)
|
|
286
|
+
elif value < 4080:
|
|
287
|
+
value -= 240
|
|
288
|
+
target.extend((240 + (value >> 8), value & 255))
|
|
289
|
+
else:
|
|
290
|
+
target.append(255)
|
|
291
|
+
unsigned(value - 4080, target)
|
|
272
292
|
|
|
273
|
-
commands = "MLHVCZ"
|
|
274
|
-
lengths = (2, 2, 1, 1, 6, 0)
|
|
275
293
|
paths = [re.findall(r"[MLHVCZ]|-?\d+(?:\.\d+)?", glyph[7]) for glyph in glyphs]
|
|
276
294
|
unit = 1
|
|
277
295
|
for glyph, tokens in zip(glyphs, paths):
|
|
@@ -280,21 +298,27 @@ def pack_paths(glyphs, profiles):
|
|
|
280
298
|
for token in tokens:
|
|
281
299
|
if "." in token:
|
|
282
300
|
unit = math.lcm(unit, Fraction(token).denominator)
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
for
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
301
|
+
metric_unit = math.lcm(2, *(Fraction(value).denominator for glyph in glyphs for value in glyph[:7]))
|
|
302
|
+
metrics = [[int(value * metric_unit) for value in glyph[:7]] for glyph in glyphs]
|
|
303
|
+
for values in metrics:
|
|
304
|
+
values[3] -= values[0] - values[1]
|
|
305
|
+
values[6] -= values[0] // 2
|
|
306
|
+
metric_bases = [sorted(values[column] for values in metrics)[len(metrics) // 2] for column in range(7)]
|
|
307
|
+
contours, contour_ids, references = [], {}, []
|
|
308
|
+
for tokens in paths:
|
|
309
|
+
cursor, previous = 0, None
|
|
310
|
+
structure, coordinates, refs = [], [], []
|
|
311
|
+
first_x = first_y = x = y = 0
|
|
312
|
+
|
|
313
|
+
def keep_contour():
|
|
314
|
+
if not structure:
|
|
315
|
+
return
|
|
316
|
+
key = bytes(structure), tuple(coordinates)
|
|
317
|
+
if key not in contour_ids:
|
|
318
|
+
contour_ids[key] = len(contours)
|
|
319
|
+
contours.append(key)
|
|
320
|
+
refs.append((contour_ids[key], first_x, first_y))
|
|
321
|
+
|
|
298
322
|
while cursor < len(tokens):
|
|
299
323
|
if tokens[cursor] in commands:
|
|
300
324
|
command = commands.index(tokens[cursor])
|
|
@@ -303,52 +327,70 @@ def pack_paths(glyphs, profiles):
|
|
|
303
327
|
command = 1 if previous == 0 else previous
|
|
304
328
|
else:
|
|
305
329
|
raise ValueError("Font path has an invalid repeated command")
|
|
306
|
-
|
|
307
|
-
values = [int(Fraction(value) * unit)
|
|
308
|
-
if len(values) !=
|
|
330
|
+
count = lengths[command]
|
|
331
|
+
values = [int(Fraction(value) * unit) for value in tokens[cursor:cursor + count]]
|
|
332
|
+
if len(values) != count:
|
|
309
333
|
raise ValueError("Font path has an incomplete command")
|
|
310
334
|
if any(abs(value) * 4 > 2**53 - 1 for value in values):
|
|
311
|
-
raise ValueError("Font coordinates exceed exact JavaScript
|
|
312
|
-
cursor +=
|
|
313
|
-
|
|
335
|
+
raise ValueError("Font coordinates exceed exact JavaScript arithmetic")
|
|
336
|
+
cursor += count
|
|
337
|
+
if command == 0:
|
|
338
|
+
keep_contour()
|
|
339
|
+
structure, coordinates = [], []
|
|
340
|
+
first_x, first_y = x, y = values
|
|
341
|
+
else:
|
|
342
|
+
if previous is None:
|
|
343
|
+
raise ValueError("Font contour does not begin with a move")
|
|
344
|
+
structure.append(command)
|
|
345
|
+
for index, value in enumerate(values):
|
|
346
|
+
if command == 3 or (command not in (2, 3) and index % 2):
|
|
347
|
+
coordinates.append(value - y)
|
|
348
|
+
y = value
|
|
349
|
+
else:
|
|
350
|
+
coordinates.append(value - x)
|
|
351
|
+
x = value
|
|
352
|
+
if command == 5:
|
|
353
|
+
x, y = first_x, first_y
|
|
314
354
|
previous = command
|
|
355
|
+
keep_contour()
|
|
356
|
+
references.append(refs)
|
|
357
|
+
|
|
358
|
+
contour_records, shapes, depths = [], {}, []
|
|
359
|
+
for contour_id, (structure, coordinates) in enumerate(contours):
|
|
315
360
|
record = bytearray()
|
|
316
|
-
unsigned(len(
|
|
317
|
-
|
|
318
|
-
for
|
|
319
|
-
record
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
x = value
|
|
327
|
-
if command == 0:
|
|
328
|
-
first_x, first_y = x, y
|
|
329
|
-
elif command == 5:
|
|
330
|
-
x, y = first_x, first_y
|
|
331
|
-
structure = bytes(command for command, _ in segments)
|
|
332
|
-
coordinates = tuple(value for _, values in segments for value in values)
|
|
333
|
-
candidates = structures.setdefault(structure, [])
|
|
334
|
-
for parent, previous_coordinates in candidates:
|
|
361
|
+
unsigned(len(structure) * 2, record)
|
|
362
|
+
record.extend(structure)
|
|
363
|
+
for value in coordinates:
|
|
364
|
+
signed(value, record)
|
|
365
|
+
# Small residuals compress better even when their first encoding costs another byte.
|
|
366
|
+
best = (len(record) + sum(abs(value).bit_length() for value in coordinates), len(record))
|
|
367
|
+
parent_depth = -1
|
|
368
|
+
for parent, previous_coordinates in shapes.setdefault(structure, []):
|
|
369
|
+
if depths[parent] >= PARENT_DEPTH:
|
|
370
|
+
continue
|
|
335
371
|
shared = bytearray()
|
|
336
372
|
unsigned(parent * 2 + 1, shared)
|
|
337
|
-
for value,
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
if
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
profile_shapes = {}
|
|
373
|
+
residuals = [value - previous_value for value, previous_value in zip(coordinates, previous_coordinates)]
|
|
374
|
+
for value in residuals:
|
|
375
|
+
signed(value, shared)
|
|
376
|
+
score = (len(shared) + sum(abs(value).bit_length() for value in residuals), len(shared))
|
|
377
|
+
if score < best:
|
|
378
|
+
best = score
|
|
379
|
+
record, parent_depth = shared, depths[parent]
|
|
380
|
+
depths.append(parent_depth + 1)
|
|
381
|
+
shapes[structure].append((contour_id, coordinates))
|
|
382
|
+
contour_records.append(record)
|
|
383
|
+
|
|
384
|
+
profile_records, profile_ids, profile_shapes, profile_depths = [], {}, {}, []
|
|
385
|
+
profile_values = {}
|
|
349
386
|
for glyph_id, profile in sorted(profiles.items()):
|
|
350
|
-
|
|
351
|
-
|
|
387
|
+
key = profile[0], tuple(None if strip is None else tuple(strip) for strip in profile[1:])
|
|
388
|
+
if key in profile_values:
|
|
389
|
+
profile_ids[glyph_id] = profile_values[key]
|
|
390
|
+
continue
|
|
391
|
+
profile_id = len(profile_records)
|
|
392
|
+
profile_ids[glyph_id] = profile_id
|
|
393
|
+
profile_values[key] = profile_id
|
|
352
394
|
record = bytearray()
|
|
353
395
|
unsigned((len(profile) - 1) * 2, record)
|
|
354
396
|
signed(profile[0], record)
|
|
@@ -362,7 +404,10 @@ def pack_paths(glyphs, profiles):
|
|
|
362
404
|
signed(strip[1] - right, record)
|
|
363
405
|
left, right = strip
|
|
364
406
|
shape = profile[0], tuple(strip is not None for strip in profile[1:])
|
|
407
|
+
parent_depth = -1
|
|
365
408
|
for parent_id, parent in profile_shapes.setdefault(shape, []):
|
|
409
|
+
if profile_depths[parent_id] >= PARENT_DEPTH:
|
|
410
|
+
continue
|
|
366
411
|
shared = bytearray()
|
|
367
412
|
unsigned(parent_id * 2 + 1, shared)
|
|
368
413
|
left = right = 0
|
|
@@ -376,18 +421,80 @@ def pack_paths(glyphs, profiles):
|
|
|
376
421
|
if len(shared) >= len(record):
|
|
377
422
|
break
|
|
378
423
|
if len(shared) < len(record):
|
|
379
|
-
record = shared
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
424
|
+
record, parent_depth = shared, profile_depths[parent_id]
|
|
425
|
+
profile_depths.append(parent_depth + 1)
|
|
426
|
+
profile_shapes[shape].append((profile_id, profile))
|
|
427
|
+
profile_records.append(record)
|
|
428
|
+
|
|
429
|
+
glyph_records = []
|
|
430
|
+
for glyph_id, (values, refs) in enumerate(zip(metrics, references)):
|
|
431
|
+
record = bytearray()
|
|
432
|
+
for value, base in zip(values, metric_bases):
|
|
433
|
+
signed(value - base, record)
|
|
434
|
+
unsigned(profile_ids.get(glyph_id, -1) + 1, record)
|
|
435
|
+
unsigned(len(refs), record)
|
|
436
|
+
for contour_id, x, y in refs:
|
|
437
|
+
unsigned(contour_id, record)
|
|
438
|
+
signed(x, record)
|
|
439
|
+
signed(y, record)
|
|
440
|
+
glyph_records.append(record)
|
|
441
|
+
|
|
442
|
+
pages, indices = [], bytearray()
|
|
443
|
+
unsigned(unit, indices)
|
|
444
|
+
unsigned(metric_unit, indices)
|
|
445
|
+
for value in metric_bases:
|
|
446
|
+
signed(value, indices)
|
|
447
|
+
for name in SCALAR_MAPS:
|
|
448
|
+
unsigned(len(data[name]), indices)
|
|
449
|
+
previous_key = previous_value = 0
|
|
450
|
+
for key, value in sorted(data[name].items()):
|
|
451
|
+
unsigned(key - previous_key, indices)
|
|
452
|
+
signed(value - previous_value, indices)
|
|
453
|
+
previous_key, previous_value = key, value
|
|
454
|
+
unsigned(len(data["scriptAlternates"]), indices)
|
|
455
|
+
previous_key, previous_values = 0, [0, 0]
|
|
456
|
+
for key, values in sorted(data["scriptAlternates"].items()):
|
|
457
|
+
unsigned(key - previous_key, indices)
|
|
458
|
+
previous_key = key
|
|
459
|
+
unsigned(len(values), indices)
|
|
460
|
+
for index, value in enumerate(values):
|
|
461
|
+
signed(value - previous_values[index], indices)
|
|
462
|
+
previous_values[index] = value
|
|
463
|
+
for records in (glyph_records, contour_records, profile_records):
|
|
464
|
+
page, offsets = bytearray(), []
|
|
465
|
+
for record in records:
|
|
466
|
+
if len(record) > PAGE_BYTES:
|
|
467
|
+
raise ValueError("Font record exceeds its independent page bound")
|
|
468
|
+
if len(page) + len(record) > PAGE_BYTES:
|
|
469
|
+
pages.append(bytes(page))
|
|
470
|
+
page = bytearray()
|
|
471
|
+
offsets.append(len(pages) * PAGE_STRIDE + len(page))
|
|
472
|
+
page.extend(record)
|
|
473
|
+
offsets.append(len(pages) * PAGE_STRIDE + len(page))
|
|
474
|
+
pages.append(bytes(page))
|
|
475
|
+
unsigned(len(offsets), indices)
|
|
476
|
+
previous = 0
|
|
477
|
+
for offset in offsets:
|
|
478
|
+
unsigned(offset - previous, indices)
|
|
479
|
+
previous = offset
|
|
480
|
+
|
|
481
|
+
def compressed(data):
|
|
482
|
+
return base64.b64encode(zlib.compress(data, level=9, wbits=-15)).decode("ascii")
|
|
483
|
+
|
|
484
|
+
packed = [(len(page), compressed(page)) for page in pages]
|
|
485
|
+
return packed, compressed(indices), len(indices), {
|
|
486
|
+
"contours": len(contours), "contourReferences": sum(map(len, references)), "profileRecords": len(profile_records),
|
|
487
|
+
"pages": len(pages), "pageBytes": PAGE_BYTES, "indexBytes": len(indices),
|
|
488
|
+
"recordBytes": sum(map(len, pages)), "maxParentDepth": PARENT_DEPTH,
|
|
489
|
+
}
|
|
384
490
|
|
|
385
491
|
|
|
386
492
|
PATH_DECODER = """{
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
const
|
|
390
|
-
const
|
|
493
|
+
// The font owns immutable pages and bounded hot records. No document data enters these caches.
|
|
494
|
+
const pages=__PAGES__,invalid=()=>{throw new Error('Invalid math font data');};
|
|
495
|
+
const inflate=(encoded,size)=>{
|
|
496
|
+
const source=atob(encoded),bytes=new Uint8Array(size);let position=0,held=0,available=0,written=0;
|
|
497
|
+
const fill=count=>{while(available<count&&position<source.length){held|=source.charCodeAt(position++)<<available;available+=8;}};
|
|
391
498
|
const bits=count=>{fill(count);if(available<count)invalid();const value=held&((1<<count)-1);held>>>=count;available-=count;return value;};
|
|
392
499
|
const tree=lengths=>{const width=Math.max(...lengths),counts=new Uint16Array(width+1),next=new Uint16Array(width+1),table=new Uint16Array(1<<width);
|
|
393
500
|
for(const length of lengths)if(length)counts[length]++;let code=0;
|
|
@@ -414,31 +521,64 @@ const code=value-257,extra=code<8?0:(code>>>2)-1,length=code===28?258:code<8?cod
|
|
|
414
521
|
const offset=symbol(distance);if(offset>29)invalid();const shift=offset<4?0:(offset>>>1)-1,back=offset<4?offset+1:((offset&1)+2)*2**shift+1+bits(shift);
|
|
415
522
|
if(back>written||written+length>bytes.length)invalid();for(let i=0;i<length;i++){bytes[written]=bytes[written-back];written++;}}
|
|
416
523
|
}
|
|
417
|
-
if(written!==bytes.length)invalid();
|
|
418
|
-
|
|
419
|
-
const
|
|
420
|
-
const signed=()
|
|
421
|
-
const
|
|
422
|
-
|
|
423
|
-
const
|
|
424
|
-
for(const
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
for(let
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
const
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
if(
|
|
524
|
+
if(written!==bytes.length||position-Math.floor(available/8)!==source.length)invalid();return bytes;
|
|
525
|
+
};
|
|
526
|
+
const unsigned=r=>{let value=0,shift=0,byte;do{if(r[1]>=r[2]||shift>49)invalid();byte=r[0][r[1]++];value+=(byte&127)*2**shift;shift+=7;}while(byte&128);if(!Number.isSafeInteger(value))invalid();return value;};
|
|
527
|
+
const signed=r=>{if(r[1]>=r[2])invalid();let value=r[0][r[1]++];if(value>=240){if(value===255){value=4080+unsigned(r);if(!Number.isSafeInteger(value))invalid();}else{if(r[1]>=r[2])invalid();value=240+(value-240)*256+r[0][r[1]++];}}return value%2?-(value+1)/2:value/2;};
|
|
528
|
+
const complete=r=>{if(r[1]!==r[2])invalid();};
|
|
529
|
+
const [unit,metricUnit,metricBases,glyphs,contours,profiles]=(()=>{
|
|
530
|
+
const bytes=inflate(__INDEX__,__SIZE__),r=[bytes,0,bytes.length],unit=unsigned(r),metricUnit=unsigned(r);if(!unit||!metricUnit)invalid();const metricBases=Array.from({length:7},()=>signed(r));
|
|
531
|
+
for(const name of __MAP_FIELDS__){const count=unsigned(r),map=MATH_FONT[name]={};if(count>bytes.length)invalid();let key=0,value=0;
|
|
532
|
+
for(let i=0;i<count;i++){key+=unsigned(r);value+=signed(r);map[key]=value;}}
|
|
533
|
+
const scripts=MATH_FONT.scriptAlternates={},scriptCount=unsigned(r),previous=[0,0];let key=0;if(scriptCount>bytes.length)invalid();
|
|
534
|
+
for(let i=0;i<scriptCount;i++){key+=unsigned(r);const count=unsigned(r);if(count<1||count>2)invalid();const values=[];for(let j=0;j<count;j++){previous[j]+=signed(r);values.push(previous[j]);}scripts[key]=values;}
|
|
535
|
+
const tables=Array.from({length:3},()=>{const count=unsigned(r);if(count<1||count>bytes.length)invalid();const offsets=new Uint32Array(count);let previous=0;
|
|
536
|
+
for(let i=0;i<count;i++){previous+=unsigned(r);if(previous>=pages.length*32768)invalid();offsets[i]=previous;}return offsets;});complete(r);return [unit,metricUnit,metricBases,...tables];
|
|
537
|
+
})();
|
|
538
|
+
const pageCache=new Map(),glyphCache=new Map(),profileCache=new Map();
|
|
539
|
+
const remember=(cache,id,value,limit)=>{if(cache.size>=limit)cache.delete(cache.keys().next().value);cache.set(id,value);return value;};
|
|
540
|
+
const touch=(cache,id)=>{const value=cache.get(id);if(value!==undefined){cache.delete(id);cache.set(id,value);}return value;};
|
|
541
|
+
const record=(offsets,id)=>{
|
|
542
|
+
if(!Number.isInteger(id)||id<0||id+1>=offsets.length)return;
|
|
543
|
+
const offset=offsets[id],page=offset>>>15,next=offsets[id+1];let bytes=touch(pageCache,page);
|
|
544
|
+
if(!bytes){const source=pages[page];if(!source||source[0]>16384)invalid();bytes=remember(pageCache,page,inflate(source[1],source[0]),8);}
|
|
545
|
+
const begin=offset&32767,end=next>>>15===page?next&32767:bytes.length;if(begin>=end||end>bytes.length)invalid();return [bytes,begin,end];
|
|
546
|
+
};
|
|
547
|
+
const commandLengths=[2,2,1,1,6,0],commandNames='MLHVCZ';
|
|
548
|
+
const contour=(id,local,depth=0)=>{
|
|
549
|
+
if(local.has(id))return local.get(id);if(depth>4)invalid();const r=record(contours,id);if(!r)invalid();const header=unsigned(r);let commands,coordinates;
|
|
550
|
+
if(header%2){const parentId=Math.floor(header/2);if(parentId>=id)invalid();const parent=contour(parentId,local,depth+1);commands=parent[0];coordinates=new Float64Array(parent[1].length);
|
|
551
|
+
for(let i=0;i<coordinates.length;i++)coordinates[i]=parent[1][i]+signed(r);}
|
|
552
|
+
else{const count=header/2;if(r[1]+count>r[2])invalid();commands=r[0].subarray(r[1],r[1]+count);r[1]+=count;let values=0;
|
|
553
|
+
for(const command of commands){if(command<1||command>5)invalid();values+=commandLengths[command];}if(values>r[2]-r[1])invalid();coordinates=new Float64Array(values);
|
|
554
|
+
for(let i=0;i<values;i++)coordinates[i]=signed(r);}
|
|
555
|
+
complete(r);const value=[commands,coordinates];local.set(id,value);return value;
|
|
556
|
+
};
|
|
557
|
+
const glyphRecord=id=>{
|
|
558
|
+
let cached=touch(glyphCache,id);if(cached)return cached;const r=record(glyphs,id);if(!r)return;
|
|
559
|
+
const glyph=[];for(let i=0;i<7;i++)glyph.push(signed(r)+metricBases[i]);glyph[3]+=glyph[0]-glyph[1];glyph[6]+=glyph[0]/2;for(let i=0;i<7;i++)glyph[i]/=metricUnit;const profile=unsigned(r)-1,count=unsigned(r),local=new Map();let path='';
|
|
560
|
+
if(profile>=profiles.length-1||count>r[2]-r[1])invalid();
|
|
561
|
+
for(let part=0;part<count;part++){const id=unsigned(r),firstX=signed(r),firstY=signed(r),[commands,coordinates]=contour(id,local);let x=firstX,y=firstY,cursor=0;
|
|
562
|
+
path+='M'+x/unit+' '+y/unit;
|
|
563
|
+
for(const command of commands){path+=commandNames[command];for(let index=0;index<commandLengths[command];index++){if(index)path+=' ';
|
|
564
|
+
if(command===3||(command!==2&&command!==3&&index%2)){y+=coordinates[cursor++];path+=y/unit;}else{x+=coordinates[cursor++];path+=x/unit;}}
|
|
565
|
+
if(command===5){x=firstX;y=firstY;}}
|
|
566
|
+
}complete(r);glyph.push(path);cached={glyph,profile};return remember(glyphCache,id,cached,256);
|
|
567
|
+
};
|
|
568
|
+
const profileRecord=(id,depth=0)=>{
|
|
569
|
+
const cached=touch(profileCache,id);if(cached)return cached;if(depth>4)invalid();const r=record(profiles,id);if(!r)invalid();const header=unsigned(r);let profile,left=0,right=0;
|
|
570
|
+
if(header%2){const parentId=Math.floor(header/2);if(parentId>=id)invalid();const parent=profileRecord(parentId,depth+1);profile=[parent[0]];
|
|
571
|
+
for(let i=1;i<parent.length;i++){const strip=parent[i];if(!strip){profile.push(null);continue;}left+=signed(r);right+=signed(r);profile.push([strip[0]+left,strip[1]+right]);}}
|
|
572
|
+
else{profile=[signed(r)];const count=header/2;if(count>r[2]-r[1])invalid();for(let i=0;i<count;i++){const token=unsigned(r);if(!token){profile.push(null);continue;}const value=token-1;left+=value%2?-(value+1)/2:value/2;right+=signed(r);profile.push([left,right]);}}
|
|
573
|
+
complete(r);return remember(profileCache,id,profile,128);
|
|
574
|
+
};
|
|
575
|
+
MATH_FONT.glyph=id=>glyphRecord(id)?.glyph;
|
|
576
|
+
MATH_FONT.profile=id=>{const glyph=glyphRecord(id);return glyph&&glyph.profile>=0?profileRecord(glyph.profile):undefined;};
|
|
438
577
|
}
|
|
439
578
|
"""
|
|
440
579
|
|
|
441
580
|
|
|
581
|
+
|
|
442
582
|
def main():
|
|
443
583
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
444
584
|
parser.add_argument("--font", type=Path, help="Optional pinned source font; otherwise fetch it")
|
|
@@ -464,16 +604,18 @@ def main():
|
|
|
464
604
|
"The font authors do not maintain this conversion.\n"
|
|
465
605
|
"Complete original work: https://mirrors.ctan.org/fonts/lm-math.zip\n"
|
|
466
606
|
"Conversion details: FONT-SOURCE.md.\n\n").encode("utf-8") + gust + b"\n" + lppl
|
|
467
|
-
|
|
607
|
+
pages, index, size, packing = pack_font(data)
|
|
468
608
|
glyph_count = len(data["glyphs"])
|
|
469
609
|
profile_count = len(data["edgeProfiles"])
|
|
470
|
-
data["
|
|
471
|
-
data["edgeProfiles"]
|
|
472
|
-
|
|
610
|
+
character_count = len(data["chars"])
|
|
611
|
+
del data["glyphs"], data["edgeProfiles"]
|
|
612
|
+
for name in (*SCALAR_MAPS, "scriptAlternates"):
|
|
613
|
+
del data[name]
|
|
614
|
+
output = (b"/*!\n" + notice + b"\n*/\nconst MATH_FONT=" + json.dumps(data, ensure_ascii=True, separators=(",", ":")).encode("utf-8") + b";\n" + PATH_DECODER.replace("__PAGES__", json.dumps(pages, separators=(",", ":"))).replace("__INDEX__", json.dumps(index)).replace("__SIZE__", str(size)).replace("__MAP_FIELDS__", json.dumps(SCALAR_MAPS, separators=(",", ":"))).encode("utf-8"))
|
|
473
615
|
args.out.parent.mkdir(parents=True, exist_ok=True)
|
|
474
616
|
args.out.write_bytes(output)
|
|
475
617
|
args.out.with_name("FONT-LICENSE.txt").write_bytes(notice)
|
|
476
|
-
print(json.dumps({"sourceSha256": hashlib.sha256(font_bytes).hexdigest(), "licenseSha256": hashlib.sha256(notice).hexdigest(), "glyphs": glyph_count, "characters":
|
|
618
|
+
print(json.dumps({"sourceSha256": hashlib.sha256(font_bytes).hexdigest(), "licenseSha256": hashlib.sha256(notice).hexdigest(), "glyphs": glyph_count, "characters": character_count, "edgeProfiles": profile_count, "packing": packing, "bytes": len(output), "gzipBytes": len(gzip.compress(output, compresslevel=9, mtime=0))}, indent=2))
|
|
477
619
|
|
|
478
620
|
|
|
479
621
|
if __name__ == "__main__":
|