Optimize the garbage collector to produce smaller files
This commit is contained in:
parent
a3836f8144
commit
80700d9dc8
8 changed files with 322 additions and 70 deletions
|
@ -3,7 +3,7 @@ import zlib from "zlib";
|
|||
|
||||
import { encode, decode } from "@msgpack/msgpack";
|
||||
|
||||
const Edition = 28;
|
||||
const Edition = 29;
|
||||
const MAX_AGE = 16;
|
||||
class GfEntry {
|
||||
constructor(age, value) {
|
||||
|
|
|
@ -1,22 +1,28 @@
|
|||
import * as Geometry from "../../support/geometry/index.mjs";
|
||||
import { Transform } from "../../support/geometry/transform.mjs";
|
||||
import { Radical, VS01 } from "../../support/gr.mjs";
|
||||
|
||||
export function gcFont(glyphStore, excludedChars, otl, cfg) {
|
||||
markSweepOtlLookups(otl.GSUB);
|
||||
export function gcFont(glyphStore, excludedChars, otl) {
|
||||
const daGsub = markSweepOtlLookups(otl.GSUB);
|
||||
markSweepOtlLookups(otl.GPOS);
|
||||
const sink = markGlyphs(glyphStore, excludedChars, otl, cfg);
|
||||
const sink = markGlyphs(glyphStore, excludedChars, otl, daGsub);
|
||||
return sweepGlyphs(glyphStore, sink);
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
function markSweepOtlLookups(table) {
|
||||
if (!table || !table.features || !table.lookups) return;
|
||||
const accessibleLookupsIds = new Set();
|
||||
markLookups(table, accessibleLookupsIds);
|
||||
const directAccessibleLookupsIds = new Set();
|
||||
markLookups(table, accessibleLookupsIds, directAccessibleLookupsIds);
|
||||
sweepLookups(table, accessibleLookupsIds);
|
||||
sweepFeatures(table, accessibleLookupsIds);
|
||||
return directAccessibleLookupsIds;
|
||||
}
|
||||
function markLookups(table, sink) {
|
||||
function markLookups(table, sink, sinkDirect) {
|
||||
if (!table || !table.features) return;
|
||||
markLookupsStart(table, sink);
|
||||
markLookupsStart(table, sink, sinkDirect);
|
||||
let loop = 0,
|
||||
lookupSetChanged = false;
|
||||
do {
|
||||
|
@ -36,11 +42,14 @@ function markLookups(table, sink) {
|
|||
lookupSetChanged = sizeBefore !== sink.size;
|
||||
} while (loop < 0xff && lookupSetChanged);
|
||||
}
|
||||
function markLookupsStart(table, sink) {
|
||||
function markLookupsStart(table, sink, sinkDirect) {
|
||||
for (let f in table.features) {
|
||||
const feature = table.features[f];
|
||||
if (!feature) continue;
|
||||
for (const l of feature) sink.add(l);
|
||||
for (const l of feature) {
|
||||
sink.add(l);
|
||||
sinkDirect.add(l);
|
||||
}
|
||||
}
|
||||
}
|
||||
function sweepLookups(table, accessibleLookupsIds) {
|
||||
|
@ -64,89 +73,128 @@ function sweepFeatures(table, accessibleLookupsIds) {
|
|||
table.features = features1;
|
||||
}
|
||||
|
||||
function markGlyphs(glyphStore, excludedChars, otl, cfg) {
|
||||
const sink = markGlyphsInitial(glyphStore, excludedChars);
|
||||
while (markGlyphsStep(glyphStore, sink, otl, cfg));
|
||||
return sink;
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
function markGlyphs(glyphStore, excludedChars, otl, daGsub) {
|
||||
const markedGlyphs = markGlyphsInitial(glyphStore, excludedChars);
|
||||
while (markGlyphsGr(glyphStore, markedGlyphs, otl));
|
||||
if (otl.GSUB) markGlyphsByGsub(otl.GSUB, markedGlyphs, daGsub);
|
||||
while (markGlyphsGr(glyphStore, markedGlyphs, otl));
|
||||
analyzeReferenceGraph(glyphStore, markedGlyphs);
|
||||
return markedGlyphs;
|
||||
}
|
||||
|
||||
function markSingleGlyph(markedGlyphs, gName, d) {
|
||||
let existing = markedGlyphs.get(gName);
|
||||
if (!existing || d < existing) markedGlyphs.set(gName, d);
|
||||
}
|
||||
|
||||
function markGlyphsInitial(glyphStore, excludedChars) {
|
||||
let sink = new Set();
|
||||
let markedGlyphs = new Map();
|
||||
for (const [gName, g] of glyphStore.namedEntries()) {
|
||||
if (!g) continue;
|
||||
if (g.glyphRank > 0) sink.add(gName);
|
||||
if (Radical.get(g)) sink.add(gName);
|
||||
if (g.glyphRank > 0) markSingleGlyph(markedGlyphs, gName, 1);
|
||||
if (Radical.get(g)) markSingleGlyph(markedGlyphs, gName, 1);
|
||||
const unicodeSet = glyphStore.queryUnicodeOf(g);
|
||||
if (unicodeSet) {
|
||||
for (const u of unicodeSet) {
|
||||
if (!excludedChars.has(u)) sink.add(gName);
|
||||
if (excludedChars.has(u)) continue;
|
||||
let d = Math.max(1, Math.min(u, 0xffff) >> 4);
|
||||
markSingleGlyph(markedGlyphs, gName, d);
|
||||
}
|
||||
}
|
||||
}
|
||||
return sink;
|
||||
|
||||
return markedGlyphs;
|
||||
}
|
||||
function markGlyphsStep(glyphStore, sink, otl, cfg) {
|
||||
const glyphCount = sink.size;
|
||||
|
||||
function markGlyphsGr(glyphStore, markedGlyphs, otl) {
|
||||
const glyphCount = markedGlyphs.size;
|
||||
for (const g of glyphStore.glyphs()) {
|
||||
markLinkedGlyph(sink, g, VS01);
|
||||
markLinkedGlyph(markedGlyphs, g, VS01);
|
||||
}
|
||||
if (otl.GSUB) {
|
||||
for (const l in otl.GSUB.lookups) {
|
||||
const lookup = otl.GSUB.lookups[l];
|
||||
if (!lookup) continue;
|
||||
markGlyphsLookupImpl(sink, lookup, cfg);
|
||||
}
|
||||
}
|
||||
const glyphCount1 = sink.size;
|
||||
const glyphCount1 = markedGlyphs.size;
|
||||
return glyphCount1 > glyphCount;
|
||||
}
|
||||
function markLinkedGlyph(sink, g, gr) {
|
||||
function markLinkedGlyph(markedGlyphs, g, gr) {
|
||||
const linked = gr.get(g);
|
||||
if (linked) sink.add(linked);
|
||||
const d = markedGlyphs.get(g);
|
||||
if (d && linked) markSingleGlyph(markedGlyphs, linked, d + 0x1000);
|
||||
}
|
||||
function markGlyphsLookupImpl(sink, lookup, cfg) {
|
||||
|
||||
function markGlyphsByGsub(gsub, markedGlyphs, daGsub) {
|
||||
for (const lid of gsub.lookupOrder) {
|
||||
if (!daGsub.has(lid)) continue;
|
||||
markGlyphsByLookup(gsub, lid, markedGlyphs);
|
||||
}
|
||||
}
|
||||
|
||||
function markGlyphsByLookup(gsub, lid, markedGlyphs) {
|
||||
const lookup = gsub.lookups[lid];
|
||||
if (!lookup) return;
|
||||
switch (lookup.type) {
|
||||
case "gsub_single":
|
||||
return markGlyphsGsubSingle(sink, lookup, cfg);
|
||||
return markGlyphsGsubSingle(markedGlyphs, lookup);
|
||||
case "gsub_multiple":
|
||||
return markGlyphsGsubMultiple(sink, lookup, cfg);
|
||||
return markGlyphsGsubMultiple(markedGlyphs, lookup);
|
||||
case "gsub_alternate":
|
||||
return markGlyphsGsubAlternate(sink, lookup, cfg);
|
||||
return markGlyphsGsubAlternate(markedGlyphs, lookup);
|
||||
case "gsub_ligature":
|
||||
return markGlyphsGsubLigature(sink, lookup, cfg);
|
||||
case "gsub_chaining":
|
||||
return markGlyphsGsubLigature(markedGlyphs, lookup);
|
||||
case "gsub_chaining": {
|
||||
rules: for (const rule of lookup.rules) {
|
||||
// Check whether all match coverages has at least one glyph in the sink
|
||||
for (const m of rule.match) {
|
||||
let atLeastOneMatch = false;
|
||||
for (const matchGlyph of m)
|
||||
if (markedGlyphs.has(matchGlyph)) atLeastOneMatch = true;
|
||||
if (!atLeastOneMatch) continue rules;
|
||||
}
|
||||
// If so traverse through the lookup applications
|
||||
for (const app of rule.apply) markGlyphsByLookup(gsub, app.lookup, markedGlyphs);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "gsub_reverse":
|
||||
return markGlyphsGsubReverse(sink, lookup, cfg);
|
||||
return markGlyphsGsubReverse(markedGlyphs, lookup);
|
||||
}
|
||||
}
|
||||
function markGlyphsGsubSingle(sink, lookup, cfg) {
|
||||
|
||||
function markGlyphsGsubSingle(markedGlyphs, lookup) {
|
||||
const st = lookup.substitutions;
|
||||
for (const k in st) if (sink.has(k) && st[k]) sink.add(st[k]);
|
||||
}
|
||||
function markGlyphsGsubMultiple(sink, lookup, cfg) {
|
||||
const st = lookup.substitutions;
|
||||
for (const k in st) if (sink.has(k) && st[k]) for (const g of st[k]) sink.add(g);
|
||||
}
|
||||
function markGlyphsGsubAlternate(sink, lookup, cfg) {
|
||||
const st = lookup.substitutions;
|
||||
if (!cfg || !cfg.ignoreAltSub) {
|
||||
for (const k in st) if (sink.has(k) && st[k]) for (const g of st[k]) sink.add(g);
|
||||
for (const k in st) {
|
||||
const d = markedGlyphs.get(k);
|
||||
if (d && st[k]) markSingleGlyph(markedGlyphs, st[k], d + 0x1000);
|
||||
}
|
||||
}
|
||||
function markGlyphsGsubLigature(sink, lookup, cfg) {
|
||||
function markGlyphsGsubMultiple(markedGlyphs, lookup) {
|
||||
const st = lookup.substitutions;
|
||||
for (const k in st) {
|
||||
const d = markedGlyphs.get(k);
|
||||
if (d && st[k]) for (const g of st[k]) markSingleGlyph(markedGlyphs, g, d + 0x1000);
|
||||
}
|
||||
}
|
||||
function markGlyphsGsubAlternate(markedGlyphs, lookup) {
|
||||
markGlyphsGsubMultiple(markedGlyphs, lookup);
|
||||
}
|
||||
function markGlyphsGsubLigature(markedGlyphs, lookup) {
|
||||
const st = lookup.substitutions;
|
||||
for (const sub of st) {
|
||||
let check = true;
|
||||
for (const g of sub.from) if (!sink.has(g)) check = false;
|
||||
if (check && sub.to) sink.add(sub.to);
|
||||
let maxD = 0;
|
||||
for (const g of sub.from) {
|
||||
const d = markedGlyphs.get(g);
|
||||
if (d && d > maxD) maxD = d;
|
||||
}
|
||||
if (maxD && sub.to) markSingleGlyph(markedGlyphs, sub.to, maxD + 0x1000);
|
||||
}
|
||||
}
|
||||
function markGlyphsGsubReverse(sink, lookup, cfg) {
|
||||
function markGlyphsGsubReverse(markedGlyphs, lookup) {
|
||||
for (const rule of lookup.rules) {
|
||||
if (rule.match && rule.to) {
|
||||
const matchCoverage = rule.match[rule.inputIndex];
|
||||
for (let j = 0; j < matchCoverage.length; j++) {
|
||||
if (sink.has(matchCoverage[j]) && rule.to[j]) sink.add(rule.to[j]);
|
||||
const d = markedGlyphs.get(matchCoverage[j]);
|
||||
if (d && rule.to[j]) markSingleGlyph(markedGlyphs, rule.to[j], d + 0x1000);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -155,3 +203,181 @@ function markGlyphsGsubReverse(sink, lookup, cfg) {
|
|||
function sweepGlyphs(glyphStore, gnSet) {
|
||||
return glyphStore.filterByName(gnSet);
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
function analyzeReferenceGraph(glyphStore, markedGlyphs) {
|
||||
let depthMap = new Map();
|
||||
let aliasMap = new Map();
|
||||
|
||||
for (const [gn, g] of glyphStore.namedEntries()) {
|
||||
const d = markedGlyphs.get(gn);
|
||||
if (d) traverseReferenceTree(depthMap, aliasMap, g, d);
|
||||
}
|
||||
aliasMap = optimizeAliasMap(aliasMap, depthMap);
|
||||
|
||||
let memo = new Set();
|
||||
for (const [gn, g] of glyphStore.namedEntries()) {
|
||||
const d = markedGlyphs.get(gn);
|
||||
if (d) rectifyGlyphAndMarkComponents(glyphStore, aliasMap, markedGlyphs, memo, g, d);
|
||||
}
|
||||
}
|
||||
|
||||
// Traverse the glyphs' reference tree and mark the depth of each glyph.
|
||||
// For aliases (a glyphs which only contains a single reference), mark the aliasing relationship.
|
||||
function traverseReferenceTree(depthMap, aliasMap, g, d) {
|
||||
depthMapSet(depthMap, g, d);
|
||||
|
||||
let refs = g.geometry.asReferences();
|
||||
if (!refs) return;
|
||||
|
||||
for (const sr of refs) {
|
||||
traverseReferenceTree(depthMap, aliasMap, sr.glyph, d + 0x10000);
|
||||
}
|
||||
if (refs.length === 1) {
|
||||
const sr = refs[0];
|
||||
aliasMap.set(g, sr);
|
||||
}
|
||||
}
|
||||
|
||||
function depthMapSet(depthMap, g, d) {
|
||||
let existing = depthMap.get(g);
|
||||
if (null == existing || d < existing) {
|
||||
depthMap.set(g, d);
|
||||
return d;
|
||||
} else {
|
||||
return existing;
|
||||
}
|
||||
}
|
||||
|
||||
// Optimize the alias map by altering the geometry of glyphs to reference the "representative glyph",
|
||||
// which is the glyph with the smallest depth in the cluster of glyphs that aliased to each other.
|
||||
function optimizeAliasMap(aliasMap, depthMap) {
|
||||
let collection = collectAliasMap(aliasMap);
|
||||
resolveCollectionRepresentative(collection, depthMap);
|
||||
return alterGeometryAndOptimize(collection);
|
||||
}
|
||||
|
||||
// Collect all glyphs into clusters, grouped by the terminal glyph of alias chains.
|
||||
// Each cluster will contain all the the glyphs that are aliases of the terminal glyph.
|
||||
function collectAliasMap(aliasMap) {
|
||||
let aliasResolution = new Map();
|
||||
for (const g of aliasMap.keys()) {
|
||||
const terminal = getAliasTerminal(aliasMap, g);
|
||||
let m = aliasResolution.get(terminal.glyph);
|
||||
if (!m) {
|
||||
m = {
|
||||
representative: null,
|
||||
aliases: new Map()
|
||||
};
|
||||
aliasResolution.set(terminal.glyph, m);
|
||||
}
|
||||
m.aliases.set(g, { x: terminal.x, y: terminal.y });
|
||||
}
|
||||
|
||||
for (const [gT, cluster] of aliasResolution) cluster.aliases.set(gT, { x: 0, y: 0 });
|
||||
return aliasResolution;
|
||||
}
|
||||
|
||||
// Resolve the representative glyph of each cluster, using the glyph with the smallest depth.
|
||||
function resolveCollectionRepresentative(collection, depthMap) {
|
||||
for (const [gT, cluster] of collection) {
|
||||
let d = null;
|
||||
for (const [g, tf] of cluster.aliases) {
|
||||
const dt = depthMap.get(g);
|
||||
if ((d == null && dt != null) || (d != null && dt != null && dt < d)) {
|
||||
d = dt;
|
||||
cluster.representative = { glyph: g, x: tf.x, y: tf.y };
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Use the collected alias map to alter the geometry of glyphs and produce the optimized alias map.
|
||||
// The geometry of each glyph will be altered to reference the representative glyph of its cluster,
|
||||
// while the representative itself's geometry will be the terminal glyph's geometry with translation.
|
||||
function alterGeometryAndOptimize(collection) {
|
||||
let optimized = new Map();
|
||||
for (const [gT, cluster] of collection) {
|
||||
if (!cluster.representative) {
|
||||
throw new Error("Unreachable: each cluster should have at least one representative");
|
||||
}
|
||||
|
||||
cluster.representative.glyph.geometry = new Geometry.TransformedGeometry(
|
||||
gT.geometry,
|
||||
Transform.Translate(cluster.representative.x, cluster.representative.y)
|
||||
);
|
||||
|
||||
for (const [g, tf] of cluster.aliases) {
|
||||
if (g != cluster.representative.glyph) {
|
||||
g.geometry = new Geometry.ReferenceGeometry(
|
||||
cluster.representative.glyph,
|
||||
tf.x - cluster.representative.x,
|
||||
tf.y - cluster.representative.y
|
||||
);
|
||||
optimized.set(g, {
|
||||
glyph: cluster.representative.glyph,
|
||||
x: tf.x - cluster.representative.x,
|
||||
y: tf.y - cluster.representative.y
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
return optimized;
|
||||
}
|
||||
|
||||
function getAliasTerminal(aliasMap, g) {
|
||||
let x = 0,
|
||||
y = 0;
|
||||
for (;;) {
|
||||
const alias = aliasMap.get(g);
|
||||
if (!alias) {
|
||||
return { glyph: g, x, y };
|
||||
} else {
|
||||
x += alias.x;
|
||||
y += alias.y;
|
||||
g = alias.glyph;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function rectifyGlyphAndMarkComponents(glyphStore, aliasMap, markedGlyphs, memo, g, d) {
|
||||
if (memo.has(g)) return;
|
||||
memo.add(g);
|
||||
|
||||
let refs = g.geometry.asReferences();
|
||||
if (refs) {
|
||||
let parts = [];
|
||||
for (let sr of refs) {
|
||||
// Resolve alias
|
||||
const alias = aliasMap.get(sr.glyph);
|
||||
if (alias) {
|
||||
sr.glyph = alias.glyph;
|
||||
sr.x += alias.x;
|
||||
sr.y += alias.y;
|
||||
}
|
||||
|
||||
const gn = glyphStore.queryNameOf(sr.glyph);
|
||||
if (!gn) {
|
||||
// Reference is invalid. The root glyph will be radicalized.
|
||||
g.geometry = new Geometry.RadicalGeometry(g.geometry.unlinkReferences());
|
||||
return;
|
||||
} else {
|
||||
// Reference is valid. Process the referenced glyph.
|
||||
if (!markedGlyphs.has(gn)) markedGlyphs.set(gn, d + 0x10000);
|
||||
rectifyGlyphAndMarkComponents(
|
||||
glyphStore,
|
||||
aliasMap,
|
||||
markedGlyphs,
|
||||
memo,
|
||||
sr.glyph,
|
||||
d + 0x10000
|
||||
);
|
||||
parts.push(new Geometry.ReferenceGeometry(sr.glyph, sr.x, sr.y));
|
||||
}
|
||||
}
|
||||
g.geometry = new Geometry.CombineGeometry(parts);
|
||||
} else {
|
||||
g.geometry = new Geometry.RadicalGeometry(g.geometry.unlinkReferences());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -41,17 +41,6 @@ function regulateCompositeGlyph(glyphStore, memo, g) {
|
|||
if (!gn) return memoSet(memo, g, false);
|
||||
}
|
||||
|
||||
// De-doppelganger
|
||||
while (refs.length === 1 && regulateCompositeGlyph(glyphStore, memo, refs[0].glyph)) {
|
||||
const sr = refs[0];
|
||||
const targetRefs = sr.glyph.geometry.asReferences();
|
||||
g.clearGeometry();
|
||||
for (const tr of targetRefs) {
|
||||
g.includeGeometry(new Geom.ReferenceGeometry(tr.glyph, tr.x + sr.x, tr.y + sr.y));
|
||||
}
|
||||
refs = g.geometry.asReferences();
|
||||
}
|
||||
|
||||
return memoSet(memo, g, true);
|
||||
}
|
||||
|
||||
|
|
|
@ -35,7 +35,7 @@ function validateMonospace(para, glyphStore) {
|
|||
export function finalizeFont(cache, para, glyphStore, excludedCodePoints, restFont) {
|
||||
assignGrAndCodeRank(glyphStore, Nwid, Wwid);
|
||||
assignSubRank(glyphStore);
|
||||
glyphStore = gcFont(glyphStore, excludedCodePoints, restFont, {});
|
||||
glyphStore = gcFont(glyphStore, excludedCodePoints, restFont);
|
||||
glyphStore = finalizeGlyphs(cache, para, glyphStore);
|
||||
validateMonospace(para, glyphStore);
|
||||
return glyphStore;
|
||||
|
|
|
@ -47,8 +47,7 @@ function ConvertGsubGposImpl(handlers, T, table, glyphs) {
|
|||
}
|
||||
}
|
||||
for (const l in table.lookups) {
|
||||
if (!table.lookups[l]) throw new Error(`Cannot find lookup '${l}'`);
|
||||
ls.declare(l, table.lookups[l]);
|
||||
if (!ls.query(l)) throw new Error("Unreachable: lookupOrder must contain everything");
|
||||
}
|
||||
for (const l in table.lookups) ls.fill(l, table.lookups[l]);
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue