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 { 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,
|
||||
|
|
|
@ -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 [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]
|
||||
|
|
|
@ -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
6
package-lock.json
generated
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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";
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue