Iosevka/font-src/gen/finalize/gc.mjs

397 lines
12 KiB
JavaScript

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) {
const daGsub = markSweepOtlLookups(otl.GSUB);
markSweepOtlLookups(otl.GPOS);
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();
const directAccessibleLookupsIds = new Set();
markLookups(table, accessibleLookupsIds, directAccessibleLookupsIds);
sweepLookups(table, accessibleLookupsIds);
sweepFeatures(table, accessibleLookupsIds);
return directAccessibleLookupsIds;
}
function markLookups(table, sink, sinkDirect) {
if (!table || !table.features) return;
markLookupsStart(table, sink, sinkDirect);
let loop = 0,
lookupSetChanged = false;
do {
lookupSetChanged = false;
let sizeBefore = sink.size;
for (const l of Array.from(sink)) {
const lookup = table.lookups[l];
if (!lookup) continue;
if (lookup.type === "gsub_chaining" || lookup.type === "gpos_chaining") {
for (let st of lookup.rules) {
if (!st || !st.apply) continue;
for (const app of st.apply) {
if (!app.lookup.name)
throw new Error("Unreachable: lookup name should be present");
sink.add(app.lookup.name);
}
}
}
}
loop++;
lookupSetChanged = sizeBefore !== sink.size;
} while (loop < 0xff && lookupSetChanged);
}
function markLookupsStart(table, sink, sinkDirect) {
for (let f in table.features) {
const feature = table.features[f];
if (!feature) continue;
for (const l of feature.lookups) {
sink.add(l);
sinkDirect.add(l);
}
}
}
function sweepLookups(table, accessibleLookupsIds) {
let lookups1 = {};
for (const l in table.lookups) {
if (accessibleLookupsIds.has(l)) lookups1[l] = table.lookups[l];
}
table.lookups = lookups1;
return accessibleLookupsIds;
}
function sweepFeatures(table, accessibleLookupsIds) {
let features1 = {};
for (let f in table.features) {
const feature = table.features[f];
if (!feature) continue;
const featureFiltered = {
name: feature.name,
tag: feature.tag,
lookups: []
};
for (const l of feature.lookups) {
if (accessibleLookupsIds.has(l)) featureFiltered.lookups.push(l);
}
if (!featureFiltered.lookups.length) continue;
features1[f] = featureFiltered;
}
table.features = features1;
}
///////////////////////////////////////////////////////////////////////////////////////////////////
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 markedGlyphs = new Map();
for (const [gName, g] of glyphStore.namedEntries()) {
if (!g) continue;
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)) continue;
let d = Math.max(1, Math.min(u, 0xffff) >> 4);
markSingleGlyph(markedGlyphs, gName, d);
}
}
}
return markedGlyphs;
}
function markGlyphsGr(glyphStore, markedGlyphs, otl) {
const glyphCount = markedGlyphs.size;
for (const g of glyphStore.glyphs()) {
markLinkedGlyph(markedGlyphs, g, VS01);
}
const glyphCount1 = markedGlyphs.size;
return glyphCount1 > glyphCount;
}
function markLinkedGlyph(markedGlyphs, g, gr) {
const linked = gr.get(g);
const d = markedGlyphs.get(g);
if (d && linked) markSingleGlyph(markedGlyphs, linked, d + 0x1000);
}
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(markedGlyphs, lookup);
case "gsub_multiple":
return markGlyphsGsubMultiple(markedGlyphs, lookup);
case "gsub_alternate":
return markGlyphsGsubAlternate(markedGlyphs, lookup);
case "gsub_ligature":
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) {
if (!app.lookup.name)
throw new Error("Unreachable: lookup name should be present");
markGlyphsByLookup(gsub, app.lookup.name, markedGlyphs);
}
}
break;
}
case "gsub_reverse":
return markGlyphsGsubReverse(markedGlyphs, lookup);
}
}
function markGlyphsGsubSingle(markedGlyphs, lookup) {
const st = lookup.substitutions;
for (const k in st) {
const d = markedGlyphs.get(k);
if (d && st[k]) markSingleGlyph(markedGlyphs, st[k], d + 0x1000);
}
}
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 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(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++) {
const d = markedGlyphs.get(matchCoverage[j]);
if (d && rule.to[j]) markSingleGlyph(markedGlyphs, rule.to[j], d + 0x1000);
}
}
}
}
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());
}
}