import { Ot } from "ot-builder"; import { Vec2 } from "../../support/geometry/point.mjs"; export function convertGsub(table, glyphs) { return ConvertGsubGposImpl(GsubHandlers, Ot.Gsub.Table, table, glyphs); } export function convertGpos(table, glyphs) { return ConvertGsubGposImpl(GposHandlers, Ot.Gpos.Table, table, glyphs); } export function convertGdef(otdGdef, glyphs) { const gdef = new Ot.Gdef.Table(); gdef.glyphClassDef = new Map(); for (const gn in otdGdef.glyphClassDef) { const g = glyphs.queryByName(gn); if (g) gdef.glyphClassDef.set(g, otdGdef.glyphClassDef[gn]); } gdef.markAttachClassDef = new Map(); for (const gn in otdGdef.markAttachClassDef) { const g = glyphs.queryByName(gn); if (g) gdef.markAttachClassDef.set(g, otdGdef.markAttachClassDef[gn]); } gdef.markGlyphSets = []; for (const s of otdGdef.markGlyphSets) { const result = new Set(); for (const gn of s) { const g = glyphs.queryByName(gn); if (g) result.add(g); } if (result.size) gdef.markGlyphSets.push(result); } return gdef; } /////////////////////////////////////////////////////////////////////////////////////////////////// function ConvertGsubGposImpl(handlers, T, table, glyphs) { if (!table) return null; const ls = new LookupStore(handlers, glyphs); if (table.lookups) { if (table.lookupOrder) { for (const l of table.lookupOrder) { if (!table.lookups[l]) throw new Error(`Cannot find lookup '${l}'`); ls.declare(l, table.lookups[l]); } } for (const l in table.lookups) { if (!ls.query(l)) throw new Error("Unreachable: lookupOrder must contain everything"); } for (const l in table.lookups) ls.fill(l, table.lookups[l]); } const fs = new FeatureStore(ls); if (table.features) { for (const f in table.features) fs.fill(f, table.features[f]); } const ss = new ScriptLanguageStore(fs); if (table.languages) { for (const sl in table.languages) ss.fill(sl, table.languages[sl]); } return new T(ss.extract(), fs.extract(), ls.extract()); } class ScriptLanguageStore { constructor(features) { this.featureStore = features; this.m_scriptMapping = new Map(); } extract() { return this.m_scriptMapping; } fill(id, data) { const scriptTag = id.slice(0, 4); const languageTag = id.slice(5, 9).padEnd(4); let sr = this.m_scriptMapping.get(scriptTag); if (!sr) { sr = { defaultLanguage: null, languages: new Map() }; this.m_scriptMapping.set(scriptTag, sr); } const lr = this.createLanguageRecord(data); if (languageTag === "dflt" || languageTag === "DFLT") sr.defaultLanguage = lr; else sr.languages.set(languageTag, lr); } createLanguageRecord(data) { const features = []; for (const fid of data.features) { const feature = this.featureStore.query(fid); if (feature) features.push(feature); } return { requiredFeature: this.featureStore.query(data.requiredFeature) || null, features: features }; } } class FeatureStore { constructor(lookups) { this.lookupStore = lookups; this.m_mapping = new Map(); } extract() { return Array.from(this.m_mapping.values()); } query(id) { return this.m_mapping.get(id); } fill(id, data) { const tag = id.slice(0, 4); const lookups = []; for (const lid of data) { const lookup = this.lookupStore.query(lid); if (lookup) lookups.push(lookup); } this.m_mapping.set(id, { tag, lookups }); } } class LookupStore { constructor(handlers, glyphs) { this.glyphs = glyphs; this.m_handlers = handlers; this.m_mapping = new Map(); } extract() { return Array.from(this.m_mapping.values()); } query(id) { return this.m_mapping.get(id); } declare(id, otdLookup) { if (this.m_mapping.has(id)) return; const handler = this.m_handlers[otdLookup.type]; if (!handler) return; this.m_mapping.set(id, handler.init()); } fill(id, otdLookup) { const dst = this.query(id); const handler = this.m_handlers[otdLookup.type]; if (!dst || !handler) return; if (otdLookup.subtables) throw new Error("Unreachable."); if (otdLookup.ignoreGlyphs) { let s = new Set(); for (const gn of otdLookup.ignoreGlyphs) { const g = this.glyphs.queryByName(gn); if (g) s.add(g); } if (s.size) dst.ignoreGlyphs = s; } handler.fill(dst, otdLookup, this); } } /////////////////////////////////////////////////////////////////////////////////////////////////// const GsubSingleHandler = { init() { return new Ot.Gsub.Single(); }, fill(dst, src, store) { const st = src.substitutions; for (const k in st) { const from = store.glyphs.queryByName(k); const to = store.glyphs.queryByName(st[k]); if (from && to) dst.mapping.set(from, to); } } }; const GsubMultipleHandler = { init() { return new Ot.Gsub.Multiple(); }, fill(dst, src, store) { const st = src.substitutions; for (const k in st) { const from = store.glyphs.queryByName(k); const to = mapGlyphListAll(st[k], store); if (!from || !to) continue; dst.mapping.set(from, to); } } }; const GsubAlternateHandler = { init() { return new Ot.Gsub.Alternate(); }, fill: GsubMultipleHandler.fill }; const GsubLigatureHandler = { init() { return new Ot.Gsub.Ligature(); }, fill(dst, src, store) { const st = src.substitutions; for (const { from: _from, to: _to } of st) { const to = store.glyphs.queryByName(_to); const from = mapGlyphListAll(_from, store); if (!from || !to) continue; dst.mapping.push({ from, to }); } } }; const GsubChainingHandler = { init() { return new Ot.Gsub.Chaining(); }, fill(dst, src, store) { out: for (const st of src.rules) { const match = []; for (const m of st.match) { const m1 = mapGlyphListSome(m, store); if (!m1) continue out; match.push(new Set(m1)); } const inputBegins = st.inputBegins; const inputEnds = st.inputEnds; const applications = []; for (const ap of st.apply) { const lookup = store.query(ap.lookup); if (!lookup) continue out; applications.push({ at: ap.at - inputBegins, apply: lookup }); } dst.rules.push({ match, inputBegins, inputEnds, applications }); } } }; const GsubReverseHandler = { init() { return new Ot.Gsub.ReverseSub(); }, fill(dst, src, store) { out: for (const st of src.rules) { const match = []; const doSubAt = st.inputIndex; const replacement = new Map(); for (let j = 0; j < st.match.length; j++) { { const m1 = new Set(); for (let k = 0; k < st.match[j].length; k++) { const gFrom = store.glyphs.queryByName(st.match[j][k]); if (gFrom) m1.add(gFrom); } if (!m1.size) continue out; match.push(m1); } if (j === doSubAt) { for (let k = 0; k < st.match[j].length; k++) { const gFrom = store.glyphs.queryByName(st.match[j][k]); const gTo = store.glyphs.queryByName(st.to[k]); if (!gFrom) continue; if (gTo) { replacement.set(gFrom, gTo); } else { replacement.set(gFrom, gFrom); } } } } dst.rules.push({ match, doSubAt, replacement }); } } }; function mapGlyphListAll(gl, store) { const out = []; for (const item of gl) { const fg = store.glyphs.queryByName(item); if (!fg) return null; out.push(fg); } return out; } function mapGlyphListSome(gl, store) { const out = []; for (const item of gl) { const fg = store.glyphs.queryByName(item); if (!fg) continue; out.push(fg); } if (!out.length) return null; return out; } const GsubHandlers = { gsub_single: GsubSingleHandler, gsub_multiple: GsubMultipleHandler, gsub_alternate: GsubAlternateHandler, gsub_ligature: GsubLigatureHandler, gsub_chaining: GsubChainingHandler, gsub_reverse: GsubReverseHandler }; /////////////////////////////////////////////////////////////////////////////////////////////////// const GposMarkToBaseHandler = { init() { return new Ot.Gpos.MarkToBase(); }, fill(dst, src, store) { const mm = collectClassMap(src.marks); dst.marks = convertMarkRecords(src.marks, mm, store); dst.bases = convertBaseRecords(src.bases, mm, store); } }; const GposMarkToMarkHandler = { init() { return new Ot.Gpos.MarkToMark(); }, fill(dst, src, store) { const mm = collectClassMap(src.marks); dst.marks = convertMarkRecords(src.marks, mm, store); dst.baseMarks = convertBaseRecords(src.bases, mm, store); } }; function collectClassMap(marks) { let n = 0; const m = new Map(); for (const gn in marks) { const mark = marks[gn]; if (!m.has(mark.class)) { m.set(mark.class, n); n++; } } return m; } function convertMarkRecords(marks, mm, store) { const out = new Map(); for (const gn in marks) { const mark = marks[gn]; const g = store.glyphs.queryByName(gn); if (!g) continue; let markAnchors = []; markAnchors[mm.get(mark.class)] = Vec2.from(mark); out.set(g, { markAnchors: markAnchors }); } return out; } function convertBaseRecords(bases, mm, store) { const out = new Map(); for (const gn in bases) { const baseObj = bases[gn]; const g = store.glyphs.queryByName(gn); if (!g) continue; const baseArray = []; for (const bkStr in baseObj) { baseArray[mm.get(bkStr)] = baseObj[bkStr]; } out.set(g, { baseAnchors: baseArray }); } return out; } const GposHandlers = { gpos_mark_to_base: GposMarkToBaseHandler, gpos_mark_to_mark: GposMarkToMarkHandler };