Utilize hb.js for building compatibility ligatures.

This commit is contained in:
be5invis 2023-08-16 21:56:56 -07:00
parent d35b849f68
commit 74846d3113
8 changed files with 99 additions and 101 deletions

View file

@ -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();
}

View file

@ -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,

View file

@ -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

View file

@ -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]

View file

@ -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));
}

6
package-lock.json generated
View file

@ -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",

View file

@ -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",

View file

@ -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";
}