From 74846d311349892b943aa2e0b62e8da3203471a6 Mon Sep 17 00:00:00 2001 From: be5invis Date: Wed, 16 Aug 2023 21:56:56 -0700 Subject: [PATCH] Utilize hb.js for building compatibility ligatures. --- font-src/gen/hb-compat-ligature/index.mjs | 67 +++++++++++++++++ font-src/index.mjs | 15 +++- font-src/otl/compat-ligature.ptl | 90 ----------------------- font-src/otl/index.ptl | 5 -- font-src/support/font-io/font-io.mjs | 12 ++- package-lock.json | 6 ++ package.json | 1 + verdafile.mjs | 4 + 8 files changed, 99 insertions(+), 101 deletions(-) create mode 100644 font-src/gen/hb-compat-ligature/index.mjs delete mode 100644 font-src/otl/compat-ligature.ptl diff --git a/font-src/gen/hb-compat-ligature/index.mjs b/font-src/gen/hb-compat-ligature/index.mjs new file mode 100644 index 000000000..4095b54df --- /dev/null +++ b/font-src/gen/hb-compat-ligature/index.mjs @@ -0,0 +1,67 @@ +import { Ot } from "ot-builder"; + +import { buildTTF } from "../../support/font-io/font-io.mjs"; + +export async function buildCompatLigatures(para, font) { + // We need to fix the glyph order before building the TTF + const glyphList = font.glyphs.decideOrder(); + const gsFixed = Ot.ListGlyphStoreFactory.createStoreFromList(Array.from(glyphList)); + font.glyphs = gsFixed; + + const completedCodePoints = new Set(); + + // Build a provisional in-memory TTF for shaping + const provisionalTtf = buildTTF(font); + const hb = await (await import("harfbuzzjs")).default; + + // Setup HB objects + const hbBlob = hb.createBlob(provisionalTtf); + const hbFace = hb.createFace(hbBlob, 0); + const hbFont = hb.createFont(hbFace); + + for (const entry of para.compatibilityLigatures) { + if (completedCodePoints.has(entry.unicode)) continue; + + // Shape the text, produce the glyph list and their positions + const buffer = hb.createBuffer(); + buffer.addText(entry.sequence); + buffer.guessSegmentProperties(); + hb.shapeWithTrace(hbFont, buffer, entry.featureTag, 0xffff, 0); + const shapingResults = buffer.json(); + buffer.destroy(); + + // Create the ligature glyph + const ligature = new Ot.Glyph(); + ligature.horizontal = { start: 0, end: 0 }; + ligature.geometry = new Ot.Glyph.GeometryList(); + ligature.name = `uni${entry.unicode.toString(16).toUpperCase().padStart(4, "0")}`; + + let xCursor = 0; + let yCursor = 0; + for (const component of shapingResults) { + const x = xCursor + component.dx; + const y = yCursor + component.dy; + + ligature.horizontal.end += component.ax; + ligature.geometry.items.push( + new Ot.Glyph.TtReference( + glyphList.at(component.g), + Ot.Glyph.Transform2X3.Translate(x, y) + ) + ); + + xCursor += component.ax; + yCursor += component.ay; + } + + // Save the ligature glyph + font.glyphs.items.push(ligature); + font.cmap.unicode.set(entry.unicode, ligature); + if (font.gdef) font.gdef.glyphClassDef.set(ligature, Ot.Gdef.GlyphClass.Ligature); + completedCodePoints.add(entry.unicode); + } + + hbFont.destroy(); + hbFace.destroy(); + hbBlob.destroy(); +} diff --git a/font-src/index.mjs b/font-src/index.mjs index 661ad6ec2..111734b72 100644 --- a/font-src/index.mjs +++ b/font-src/index.mjs @@ -7,6 +7,7 @@ import * as Toml from "@iarna/toml"; import { encode } from "@msgpack/msgpack"; import { buildFont } from "./gen/build-font.mjs"; +import { buildCompatLigatures } from "./gen/hb-compat-ligature/index.mjs"; import { createNamingDictFromArgv } from "./gen/meta/naming.mjs"; import { saveTTF } from "./support/font-io/font-io.mjs"; import { createGrDisplaySheet } from "./support/gr.mjs"; @@ -20,9 +21,15 @@ const __dirname = url.fileURLToPath(new URL(".", import.meta.url)); export default main; async function main(argv) { const paraT = await getParameters(); - const { font, glyphStore, cacheUpdated } = await buildFont(argv, paraT(argv)); - if (argv.oCharMap) await saveCharMap(argv, glyphStore); - if (argv.o) await saveTTF(argv.o, font); + const para = paraT(argv); + const { font, glyphStore, cacheUpdated } = await buildFont(argv, para); + if (argv.oCharMap) { + await saveCharMap(argv, glyphStore); + } + if (argv.o) { + if (para.compatibilityLigatures) await buildCompatLigatures(para, font); + await saveTTF(argv.o, font); + } return { cacheUpdated }; } @@ -52,7 +59,7 @@ async function getParameters() { VariantData.apply(deepClone(rawVariantsData), para, argv); applyLigationData(deepClone(rawLigationData), para, argv); if (argv.excludedCharRanges) para.excludedCharRanges = argv.excludedCharRanges; - if (argv.compatibilityLigatures) para.compLig = argv.compatibilityLigatures; + if (argv.compatibilityLigatures) para.compatibilityLigatures = argv.compatibilityLigatures; if (argv.metricOverride) applyMetricOverride(para, argv.metricOverride, argv); para.naming = { miscNames: para.naming, diff --git a/font-src/otl/compat-ligature.ptl b/font-src/otl/compat-ligature.ptl deleted file mode 100644 index 0ee9caed1..000000000 --- a/font-src/otl/compat-ligature.ptl +++ /dev/null @@ -1,90 +0,0 @@ -import [Glyph] from"../support/glyph/index.mjs" -import [Transform] from"../support/geometry/transform.mjs" - -define GDEF_SIMPLE 1 -define GDEF_LIGATURE 2 -define GDEF_MARK 3 - -# Compatibility ligatures -define [interpretLookups gs lutns lookups] : begin - foreach [lutn : items-of lutns] : begin - local lut lookups.(lutn) - if lut : interpretLookup gs lut lookups - -define [interpretLookup gs lut lookups] : match lut.type - "gsub_chaining" : begin - local j 0 - while (j < gs.length) : begin - local incN 1 - do : foreach [rule : items-of lut.rules] : begin - local matchT rule.match - local ib rule.inputBegins - local foundMatch true - for [local k 0] (foundMatch && k < matchT.length) [inc k] : begin - if [not gs.(j + k - ib)] - : then : set foundMatch false - : else : if ([matchT.(k).indexOf gs.(j + k - ib)] < 0) : set foundMatch false - if foundMatch : begin - foreach [app : items-of rule.apply] : do - local aj : j - ib + app.at - local alut lookups.(app.lookup) - interpretLookupAt gs aj alut - set incN : incN + rule.inputEnds - rule.inputBegins - break nothing - set j : j + incN - "gsub_reverse" : begin - local j (gs.length - 1) - while (j >= 0) : begin - do : foreach [rule : items-of lut.rules] : begin - local matchT rule.match - local ib rule.inputIndex - local foundMatch true - for [local k 0] (foundMatch && k < matchT.length) [inc k] : begin - if [not gs.(j + k - ib)] - : then : set foundMatch false - : else : if ([matchT.(k).indexOf gs.(j + k - ib)] < 0) : set foundMatch false - if foundMatch : begin - set gs.(j) : rule.to.[matchT.(ib).indexOf gs.(j)] || gs.(j) - set j : j - 1 - "gsub_single" : begin - for [local j 0] (j < gs.length) [inc j] : begin - interpretLookupAt gs j lut - -define [interpretLookupAt gs j lut] : match lut.type - "gsub_single" : if lut.substitutions.(gs.(j)) : set gs.(j) lut.substitutions.(gs.(j)) - -export : define [BuildCompatLigatures para glyphStore GSUB GDEF config] : begin - foreach [cldef : items-of config] : do - if [not cldef.unicode] : break nothing - if [not cldef.featureTag] : break nothing - if [not cldef.sequence] : break nothing - - local feature null - foreach [fn : items-of GSUB.languages.'DFLT_DFLT'.features] - if (cldef.featureTag === [fn.slice 0 4]) : set feature GSUB.features.(fn) - - if [not feature] : break nothing - - local gnames {} - for [local j 0] [j < cldef.sequence.length] [inc j] : begin - if [not : glyphStore.queryByUnicode : cldef.sequence.charCodeAt j] : break nothing - gnames.push : glyphStore.queryNameOfUnicode : cldef.sequence.charCodeAt j - - interpretLookups gnames feature GSUB.lookups - - define g1Name : '$clig.' + cldef.unicode - local g1 : new Glyph g1Name - set g1.advanceWidth 0 - foreach [gn : items-of gnames] : begin - local g : glyphStore.queryByName gn - g1.applyTransform : new Transform 1 0 0 1 (-g1.advanceWidth) 0 - g1.includeGlyph g - g1.applyTransform : new Transform 1 0 0 1 (g1.advanceWidth) 0 - set g1.advanceWidth : g1.advanceWidth + g.advanceWidth - - if(para.forceMonospace && [Math.round g1.advanceWidth] > [Math.round para.width]) - throw : new Error "Compat ligature wider than one unit, conflicts with fontconfig-mono: \[cldef.unicode.toString 16]" - - glyphStore.addGlyph g1Name g1 - glyphStore.encodeGlyph cldef.unicode g1 - set GDEF.glyphClassDef.(g1Name) GDEF_LIGATURE diff --git a/font-src/otl/index.ptl b/font-src/otl/index.ptl index 307ccce8f..ac00b41b3 100644 --- a/font-src/otl/index.ptl +++ b/font-src/otl/index.ptl @@ -11,7 +11,6 @@ import [buildCVSS] from"./gsub-cv-ss.mjs" import [buildLOCL] from"./gsub-locl.mjs" import [buildGsubThousands] from"./gsub-thousands.mjs" import [buildMarkMkmk] from"./gpos-mark-mkmk.mjs" -import [BuildCompatLigatures] from"./compat-ligature.mjs" define GDEF_SIMPLE 1 define GDEF_LIGATURE 2 @@ -113,8 +112,4 @@ export : define [buildOtl para glyphStore] : begin foreach gnMark markGlyphs.all : begin Gr.Joining.or [glyphStore.queryByName gnMark] Gr.Joining.Classes.Left - # Build compatibility ligatures - if (para.enableLigation && para.compLig) : begin - BuildCompatLigatures para glyphStore GSUB GDEF para.compLig - return [object GSUB GPOS GDEF] diff --git a/font-src/support/font-io/font-io.mjs b/font-src/support/font-io/font-io.mjs index 2d867325b..054b09351 100644 --- a/font-src/support/font-io/font-io.mjs +++ b/font-src/support/font-io/font-io.mjs @@ -4,16 +4,24 @@ import { FontIo, Ot } from "ot-builder"; export async function readTTF(input) { const buf = await fs.promises.readFile(input); + return parseTTF(buf); +} + +export function parseTTF(buf) { const sfnt = FontIo.readSfntOtf(buf); const font = FontIo.readFont(sfnt, Ot.ListGlyphStoreFactory); return font; } -export async function saveTTF(output, font) { +export function buildTTF(font) { const sfnt = FontIo.writeFont(font, { glyphStore: { statOs2XAvgCharWidth: false }, generateDummyDigitalSignature: true }); const buf = FontIo.writeSfntOtf(sfnt); - await fs.promises.writeFile(output, buf); + return buf; +} + +export async function saveTTF(output, font) { + await fs.promises.writeFile(output, buildTTF(font)); } diff --git a/package-lock.json b/package-lock.json index 672098060..af9314670 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,6 +11,7 @@ "@iarna/toml": "^2.2.5", "@msgpack/msgpack": "^2.8.0", "deep-equal": "^2.2.2", + "harfbuzzjs": "^0.3.3", "ot-builder": "^1.6.4", "otb-ttc-bundle": "^1.6.4", "semver": "^7.5.4", @@ -2032,6 +2033,11 @@ "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", "dev": true }, + "node_modules/harfbuzzjs": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/harfbuzzjs/-/harfbuzzjs-0.3.3.tgz", + "integrity": "sha512-48C/LOUweD//LTqaQAS9VMOBNPh7DhyJEmdzh5/1GgjNA8kGZMVZKTzkvarBDtiKKaKG5whx7qXU8OeSNLmWcA==" + }, "node_modules/has": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", diff --git a/package.json b/package.json index 0c7ea7534..16d11d4de 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ "@iarna/toml": "^2.2.5", "@msgpack/msgpack": "^2.8.0", "deep-equal": "^2.2.2", + "harfbuzzjs": "^0.3.3", "ot-builder": "^1.6.4", "otb-ttc-bundle": "^1.6.4", "semver": "^7.5.4", diff --git a/verdafile.mjs b/verdafile.mjs index 93c4413f1..9db857488 100644 --- a/verdafile.mjs +++ b/verdafile.mjs @@ -158,6 +158,7 @@ function linkSpacingDerivableBuildPlans(bps) { for (const pfxTo in bps) { const planTo = bps[pfxTo]; const planToVal = rectifyPlanForSpacingDerivation(planTo); + if (blockSpacingDerivation(planTo)) continue; if (!isLinkDeriveToSpacing(planTo.spacing)) continue; for (const pfxFrom in bps) { const planFrom = bps[pfxFrom]; @@ -169,6 +170,9 @@ function linkSpacingDerivableBuildPlans(bps) { } } +function blockSpacingDerivation(bp) { + return !!bp["compatibility-ligatures"]; +} function isLinkDeriveToSpacing(spacing) { return spacing === "term" || spacing === "fontconfig-mono" || spacing === "fixed"; }