Utilize hb.js for building compatibility ligatures.
This commit is contained in:
parent
d35b849f68
commit
74846d3113
8 changed files with 99 additions and 101 deletions
67
font-src/gen/hb-compat-ligature/index.mjs
Normal file
67
font-src/gen/hb-compat-ligature/index.mjs
Normal 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();
|
||||||
|
}
|
|
@ -7,6 +7,7 @@ import * as Toml from "@iarna/toml";
|
||||||
import { encode } from "@msgpack/msgpack";
|
import { encode } from "@msgpack/msgpack";
|
||||||
|
|
||||||
import { buildFont } from "./gen/build-font.mjs";
|
import { buildFont } from "./gen/build-font.mjs";
|
||||||
|
import { buildCompatLigatures } from "./gen/hb-compat-ligature/index.mjs";
|
||||||
import { createNamingDictFromArgv } from "./gen/meta/naming.mjs";
|
import { createNamingDictFromArgv } from "./gen/meta/naming.mjs";
|
||||||
import { saveTTF } from "./support/font-io/font-io.mjs";
|
import { saveTTF } from "./support/font-io/font-io.mjs";
|
||||||
import { createGrDisplaySheet } from "./support/gr.mjs";
|
import { createGrDisplaySheet } from "./support/gr.mjs";
|
||||||
|
@ -20,9 +21,15 @@ const __dirname = url.fileURLToPath(new URL(".", import.meta.url));
|
||||||
export default main;
|
export default main;
|
||||||
async function main(argv) {
|
async function main(argv) {
|
||||||
const paraT = await getParameters();
|
const paraT = await getParameters();
|
||||||
const { font, glyphStore, cacheUpdated } = await buildFont(argv, paraT(argv));
|
const para = paraT(argv);
|
||||||
if (argv.oCharMap) await saveCharMap(argv, glyphStore);
|
const { font, glyphStore, cacheUpdated } = await buildFont(argv, para);
|
||||||
if (argv.o) await saveTTF(argv.o, font);
|
if (argv.oCharMap) {
|
||||||
|
await saveCharMap(argv, glyphStore);
|
||||||
|
}
|
||||||
|
if (argv.o) {
|
||||||
|
if (para.compatibilityLigatures) await buildCompatLigatures(para, font);
|
||||||
|
await saveTTF(argv.o, font);
|
||||||
|
}
|
||||||
return { cacheUpdated };
|
return { cacheUpdated };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -52,7 +59,7 @@ async function getParameters() {
|
||||||
VariantData.apply(deepClone(rawVariantsData), para, argv);
|
VariantData.apply(deepClone(rawVariantsData), para, argv);
|
||||||
applyLigationData(deepClone(rawLigationData), para, argv);
|
applyLigationData(deepClone(rawLigationData), para, argv);
|
||||||
if (argv.excludedCharRanges) para.excludedCharRanges = argv.excludedCharRanges;
|
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);
|
if (argv.metricOverride) applyMetricOverride(para, argv.metricOverride, argv);
|
||||||
para.naming = {
|
para.naming = {
|
||||||
miscNames: para.naming,
|
miscNames: para.naming,
|
||||||
|
|
|
@ -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
|
|
|
@ -11,7 +11,6 @@ import [buildCVSS] from"./gsub-cv-ss.mjs"
|
||||||
import [buildLOCL] from"./gsub-locl.mjs"
|
import [buildLOCL] from"./gsub-locl.mjs"
|
||||||
import [buildGsubThousands] from"./gsub-thousands.mjs"
|
import [buildGsubThousands] from"./gsub-thousands.mjs"
|
||||||
import [buildMarkMkmk] from"./gpos-mark-mkmk.mjs"
|
import [buildMarkMkmk] from"./gpos-mark-mkmk.mjs"
|
||||||
import [BuildCompatLigatures] from"./compat-ligature.mjs"
|
|
||||||
|
|
||||||
define GDEF_SIMPLE 1
|
define GDEF_SIMPLE 1
|
||||||
define GDEF_LIGATURE 2
|
define GDEF_LIGATURE 2
|
||||||
|
@ -113,8 +112,4 @@ export : define [buildOtl para glyphStore] : begin
|
||||||
foreach gnMark markGlyphs.all : begin
|
foreach gnMark markGlyphs.all : begin
|
||||||
Gr.Joining.or [glyphStore.queryByName gnMark] Gr.Joining.Classes.Left
|
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]
|
return [object GSUB GPOS GDEF]
|
||||||
|
|
|
@ -4,16 +4,24 @@ import { FontIo, Ot } from "ot-builder";
|
||||||
|
|
||||||
export async function readTTF(input) {
|
export async function readTTF(input) {
|
||||||
const buf = await fs.promises.readFile(input);
|
const buf = await fs.promises.readFile(input);
|
||||||
|
return parseTTF(buf);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parseTTF(buf) {
|
||||||
const sfnt = FontIo.readSfntOtf(buf);
|
const sfnt = FontIo.readSfntOtf(buf);
|
||||||
const font = FontIo.readFont(sfnt, Ot.ListGlyphStoreFactory);
|
const font = FontIo.readFont(sfnt, Ot.ListGlyphStoreFactory);
|
||||||
return font;
|
return font;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function saveTTF(output, font) {
|
export function buildTTF(font) {
|
||||||
const sfnt = FontIo.writeFont(font, {
|
const sfnt = FontIo.writeFont(font, {
|
||||||
glyphStore: { statOs2XAvgCharWidth: false },
|
glyphStore: { statOs2XAvgCharWidth: false },
|
||||||
generateDummyDigitalSignature: true
|
generateDummyDigitalSignature: true
|
||||||
});
|
});
|
||||||
const buf = FontIo.writeSfntOtf(sfnt);
|
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
6
package-lock.json
generated
|
@ -11,6 +11,7 @@
|
||||||
"@iarna/toml": "^2.2.5",
|
"@iarna/toml": "^2.2.5",
|
||||||
"@msgpack/msgpack": "^2.8.0",
|
"@msgpack/msgpack": "^2.8.0",
|
||||||
"deep-equal": "^2.2.2",
|
"deep-equal": "^2.2.2",
|
||||||
|
"harfbuzzjs": "^0.3.3",
|
||||||
"ot-builder": "^1.6.4",
|
"ot-builder": "^1.6.4",
|
||||||
"otb-ttc-bundle": "^1.6.4",
|
"otb-ttc-bundle": "^1.6.4",
|
||||||
"semver": "^7.5.4",
|
"semver": "^7.5.4",
|
||||||
|
@ -2032,6 +2033,11 @@
|
||||||
"integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==",
|
"integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==",
|
||||||
"dev": true
|
"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": {
|
"node_modules/has": {
|
||||||
"version": "1.0.3",
|
"version": "1.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz",
|
||||||
|
|
|
@ -11,6 +11,7 @@
|
||||||
"@iarna/toml": "^2.2.5",
|
"@iarna/toml": "^2.2.5",
|
||||||
"@msgpack/msgpack": "^2.8.0",
|
"@msgpack/msgpack": "^2.8.0",
|
||||||
"deep-equal": "^2.2.2",
|
"deep-equal": "^2.2.2",
|
||||||
|
"harfbuzzjs": "^0.3.3",
|
||||||
"ot-builder": "^1.6.4",
|
"ot-builder": "^1.6.4",
|
||||||
"otb-ttc-bundle": "^1.6.4",
|
"otb-ttc-bundle": "^1.6.4",
|
||||||
"semver": "^7.5.4",
|
"semver": "^7.5.4",
|
||||||
|
|
|
@ -158,6 +158,7 @@ function linkSpacingDerivableBuildPlans(bps) {
|
||||||
for (const pfxTo in bps) {
|
for (const pfxTo in bps) {
|
||||||
const planTo = bps[pfxTo];
|
const planTo = bps[pfxTo];
|
||||||
const planToVal = rectifyPlanForSpacingDerivation(planTo);
|
const planToVal = rectifyPlanForSpacingDerivation(planTo);
|
||||||
|
if (blockSpacingDerivation(planTo)) continue;
|
||||||
if (!isLinkDeriveToSpacing(planTo.spacing)) continue;
|
if (!isLinkDeriveToSpacing(planTo.spacing)) continue;
|
||||||
for (const pfxFrom in bps) {
|
for (const pfxFrom in bps) {
|
||||||
const planFrom = bps[pfxFrom];
|
const planFrom = bps[pfxFrom];
|
||||||
|
@ -169,6 +170,9 @@ function linkSpacingDerivableBuildPlans(bps) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function blockSpacingDerivation(bp) {
|
||||||
|
return !!bp["compatibility-ligatures"];
|
||||||
|
}
|
||||||
function isLinkDeriveToSpacing(spacing) {
|
function isLinkDeriveToSpacing(spacing) {
|
||||||
return spacing === "term" || spacing === "fontconfig-mono" || spacing === "fixed";
|
return spacing === "term" || spacing === "fontconfig-mono" || spacing === "fixed";
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue