diff --git a/font-src/gen/caching/index.mjs b/font-src/gen/caching/index.mjs index a4447211e..7a96a52b3 100644 --- a/font-src/gen/caching/index.mjs +++ b/font-src/gen/caching/index.mjs @@ -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) { diff --git a/font-src/gen/finalize/gc.mjs b/font-src/gen/finalize/gc.mjs index 858c08840..c3098ea97 100644 --- a/font-src/gen/finalize/gc.mjs +++ b/font-src/gen/finalize/gc.mjs @@ -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()); + } +} diff --git a/font-src/gen/finalize/glyphs.mjs b/font-src/gen/finalize/glyphs.mjs index 1b2c82086..495a40b37 100644 --- a/font-src/gen/finalize/glyphs.mjs +++ b/font-src/gen/finalize/glyphs.mjs @@ -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); } diff --git a/font-src/gen/finalize/index.mjs b/font-src/gen/finalize/index.mjs index 79eba3061..d734ea022 100644 --- a/font-src/gen/finalize/index.mjs +++ b/font-src/gen/finalize/index.mjs @@ -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; diff --git a/font-src/gen/otd-conv/layout.mjs b/font-src/gen/otd-conv/layout.mjs index 656383341..fd1f64fc0 100644 --- a/font-src/gen/otd-conv/layout.mjs +++ b/font-src/gen/otd-conv/layout.mjs @@ -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]); } diff --git a/font-src/glyphs/letter/latin/s.ptl b/font-src/glyphs/letter/latin/s.ptl index d9c500a17..a6854d5e5 100644 --- a/font-src/glyphs/letter/latin/s.ptl +++ b/font-src/glyphs/letter/latin/s.ptl @@ -1,7 +1,7 @@ $$include '../../../meta/macros.ptl' import [mix linreg clamp fallback] from"../../../support/utils.mjs" -import [DependentSelector CvDecompose MathSansSerif] from"../../../support/gr.mjs" +import [DependentSelector MathSansSerif] from"../../../support/gr.mjs" glyph-module diff --git a/font-src/support/geometry/index.mjs b/font-src/support/geometry/index.mjs index 9b4d8b20a..9a35bae9f 100644 --- a/font-src/support/geometry/index.mjs +++ b/font-src/support/geometry/index.mjs @@ -319,6 +319,38 @@ export class TransformedGeometry extends GeometryBase { } } +export class RadicalGeometry extends GeometryBase { + constructor(g) { + super(); + this.m_geom = g; + } + asContours() { + return this.m_geom.asContours(); + } + asReferences() { + return null; + } + filterTag(fn) { + const e = this.m_geom.filterTag(fn); + if (!e) return null; + return new RadicalGeometry(e); + } + isEmpty() { + return this.m_geom.isEmpty(); + } + measureComplexity() { + return this.m_geom.measureComplexity(); + } + unlinkReferences() { + return this.m_geom.unlinkReferences(); + } + toShapeStringOrNull() { + const sTarget = this.m_geom.toShapeStringOrNull(); + if (!sTarget) return null; + return Format.struct("RadicalGeometry", sTarget); + } +} + export class CombineGeometry extends GeometryBase { constructor(parts) { super(); diff --git a/font-src/support/glyph/index.mjs b/font-src/support/glyph/index.mjs index 2ee47e0b2..223796f63 100644 --- a/font-src/support/glyph/index.mjs +++ b/font-src/support/glyph/index.mjs @@ -1,3 +1,5 @@ +import * as util from "util"; + import { Anchor } from "../geometry/anchor.mjs"; import * as Geom from "../geometry/index.mjs"; import { Point, Vec2 } from "../geometry/point.mjs"; @@ -23,6 +25,10 @@ export class Glyph { this.ctxTag = null; } + [util.inspect.custom](depth, options) { + return options.stylize(this.toString(), "special"); + } + toString() { return ``; }