Code reorg for building

This commit is contained in:
be5invis 2024-03-26 19:06:17 -07:00
parent d32599e0e2
commit d461934be1
16 changed files with 210 additions and 187 deletions

View file

@ -0,0 +1,44 @@
import { buildGlyphs } from "@iosevka/font-glyphs";
import { copyFontMetrics } from "@iosevka/font-glyphs/aesthetics";
import { buildOtl } from "@iosevka/font-otl";
import { cleanupGlyphStore } from "../cleanup/index.mjs";
import { CreateEmptyFont } from "../font-io/index.mjs";
import { buildCompatLigatures } from "../hb-compat-ligature/index.mjs";
import { assignFontNames } from "../naming/index.mjs";
import { convertOtd } from "../otd-conv/index.mjs";
import { generateTtfaControls } from "../ttfa-controls/index.mjs";
import { validateFontConfigMono } from "../validate/metrics.mjs";
export async function buildFont(para, cache) {
const baseFont = CreateEmptyFont(para);
assignFontNames(baseFont, para.naming, para.isQuasiProportional);
// Build glyphs
const gs = buildGlyphs(para);
copyFontMetrics(gs.fontMetrics, baseFont);
// Build OTL
const otl = buildOtl(para, gs.glyphStore);
// Regulate (like geometry conversion)
const excludeChars = new Set();
if (para.excludedCharRanges) {
for (const [start, end] of para.excludedCharRanges) {
for (let p = start; p <= end; p++) excludeChars.add(p);
}
}
const cleanGs = cleanupGlyphStore(cache, para, gs.glyphStore, excludeChars, otl);
// Convert to TTF
const font = await convertOtd(baseFont, otl, cleanGs);
// Build compatibility ligatures
if (para.compatibilityLigatures) await buildCompatLigatures(para, font);
// Generate ttfaControls
const ttfaControls = await generateTtfaControls(cleanGs, font.glyphs);
// Validation : Metrics
if (para.forceMonospace) validateFontConfigMono(font);
return { font, glyphStore: cleanGs, cacheUpdated: cache && cache.isUpdated(), ttfaControls };
}

View file

@ -0,0 +1,56 @@
import * as Geom from "@iosevka/geometry";
import { Transform } from "@iosevka/geometry/transform";
///////////////////////////////////////////////////////////////////////////////////////////////////
export function finalizeGlyphs(cache, para, glyphStore) {
const skew = Math.tan(((para.slopeAngle || 0) / 180) * Math.PI);
regulateGlyphStore(cache, skew, glyphStore);
return glyphStore;
}
///////////////////////////////////////////////////////////////////////////////////////////////////
function regulateGlyphStore(cache, skew, glyphStore) {
for (const g of glyphStore.glyphs()) {
if (!(g.geometry.measureComplexity() & Geom.CPLX_NON_EMPTY)) continue;
if (!g.geometry.toReferences()) flattenSimpleGlyph(cache, skew, g);
}
}
function flattenSimpleGlyph(cache, skew, g) {
// Check if the geometry is already in the cache. If so, use the cached geometry.
const ck = Geom.hashGeometry(g.geometry);
if (ck && cache) {
const cachedGeometry = cache && cache.getGF(ck);
if (cachedGeometry) {
g.clearGeometry();
g.includeContours(cachedGeometry);
cache.refreshGF(ck);
return;
}
}
// Perform the actual simplification
try {
let gSimplified;
if (skew) {
const tfBack = g.gizmo ? g.gizmo.inverse() : new Transform(1, -skew, 0, 1, 0, 0);
const tfForward = g.gizmo ? g.gizmo : new Transform(1, +skew, 0, 1, 0, 0);
gSimplified = new Geom.TransformedGeometry(
tfForward,
new Geom.SimplifyGeometry(new Geom.TransformedGeometry(tfBack, g.geometry)),
);
} else {
gSimplified = new Geom.SimplifyGeometry(g.geometry);
}
const cs = gSimplified.toContours();
g.clearGeometry();
g.includeContours(cs);
if (ck && cache) cache.saveGF(ck, cs);
} catch (e) {
console.error("Detected broken geometry when processing", g._m_identifier);
throw e;
}
}

View file

@ -0,0 +1,25 @@
import { Nwid, Wwid } from "@iosevka/glyph/relation";
import { gcFont } from "./gc.mjs";
import { finalizeGlyphs } from "./glyphs.mjs";
export function cleanupGlyphStore(cache, para, glyphStore, excludedCodePoints, restFont) {
assignGrAndCodeRank(glyphStore, Wwid, Nwid);
assignSubRank(glyphStore);
glyphStore = gcFont(glyphStore, excludedCodePoints, restFont);
glyphStore = finalizeGlyphs(cache, para, glyphStore);
return glyphStore;
}
function assignGrAndCodeRank(glyphStore, ...flatteners) {
for (const g of glyphStore.glyphs()) {
g.codeRank = 0xffffffff;
for (const c of glyphStore.flattenCodes(g, flatteners)) if (c < g.codeRank) g.codeRank = c;
g.grRank = 0;
for (let i = 0; i < flatteners.length; i++) if (flatteners[i].get(g)) g.grRank |= 1 << i;
}
}
function assignSubRank(glyphStore) {
let sr = 0;
for (const g of glyphStore.glyphs()) g.subRank = sr++;
}

View file

@ -6,6 +6,7 @@ import { CliProc, Ot } from "ot-builder";
import { readTTF, saveTTF } from "./font-io/index.mjs"; import { readTTF, saveTTF } from "./font-io/index.mjs";
import { assignFontNames, createNamingDictFromArgv } from "./naming/index.mjs"; import { assignFontNames, createNamingDictFromArgv } from "./naming/index.mjs";
import { validateFontConfigMono } from "./validate/metrics.mjs";
export default main; export default main;
async function main(argv) { async function main(argv) {
@ -112,21 +113,3 @@ async function deriveFixed_DropFeatures(font, argv, fFixed) {
} }
} }
} }
// In FontConfig, a font is considered "monospace" if and only if all encoded non-combining
// characters (AW > 0) have the same width. We use this method to validate whether our
// "Fixed" subfamilies are properly built.
function validateFontConfigMono(font) {
let awSet = new Set();
for (const [ch, g] of [...font.cmap.unicode.entries()]) {
const aw = g.horizontal.end - g.horizontal.start;
if (aw > 0) awSet.add(aw);
}
for (const [ch, vs, g] of [...font.cmap.vs.entries()]) {
const aw = g.horizontal.end - g.horizontal.start;
if (aw > 0) awSet.add(aw);
}
if (awSet.size > 1) {
console.error("Fixed variant has wide characters");
}
}

View file

@ -1,51 +0,0 @@
import * as Geom from "@iosevka/geometry";
import { Transform } from "@iosevka/geometry/transform";
///////////////////////////////////////////////////////////////////////////////////////////////////
export function finalizeGlyphs(cache, para, glyphStore) {
const skew = Math.tan(((para.slopeAngle || 0) / 180) * Math.PI);
regulateGlyphStore(cache, skew, glyphStore);
return glyphStore;
}
///////////////////////////////////////////////////////////////////////////////////////////////////
function regulateGlyphStore(cache, skew, glyphStore) {
for (const g of glyphStore.glyphs()) {
if (!(g.geometry.measureComplexity() & Geom.CPLX_NON_EMPTY)) continue;
if (!g.geometry.toReferences()) flattenSimpleGlyph(cache, skew, g);
}
}
function flattenSimpleGlyph(cache, skew, g) {
const ck = Geom.hashGeometry(g.geometry);
const cached = cache.getGF(ck);
if (ck && cached) {
g.clearGeometry();
g.includeContours(cached);
cache.refreshGF(ck);
} else {
try {
let gSimplified;
if (skew) {
const tfBack = g.gizmo ? g.gizmo.inverse() : new Transform(1, -skew, 0, 1, 0, 0);
const tfForward = g.gizmo ? g.gizmo : new Transform(1, +skew, 0, 1, 0, 0);
gSimplified = new Geom.TransformedGeometry(
tfForward,
new Geom.SimplifyGeometry(new Geom.TransformedGeometry(tfBack, g.geometry)),
);
} else {
gSimplified = new Geom.SimplifyGeometry(g.geometry);
}
const cs = gSimplified.toContours();
g.clearGeometry();
g.includeContours(cs);
if (ck) cache.saveGF(ck, cs);
} catch (e) {
console.error("Detected broken geometry when processing", g._m_identifier);
throw e;
}
}
}

View file

@ -1,43 +0,0 @@
import { Nwid, Wwid } from "@iosevka/glyph/relation";
import { gcFont } from "./gc.mjs";
import { finalizeGlyphs } from "./glyphs.mjs";
export function finalizeFont(cache, para, glyphStore, excludedCodePoints, restFont) {
assignGrAndCodeRank(glyphStore, Wwid, Nwid);
assignSubRank(glyphStore);
glyphStore = gcFont(glyphStore, excludedCodePoints, restFont);
glyphStore = finalizeGlyphs(cache, para, glyphStore);
validateMonospace(para, glyphStore);
return glyphStore;
}
function assignGrAndCodeRank(glyphStore, ...flatteners) {
for (const g of glyphStore.glyphs()) {
g.codeRank = 0xffffffff;
for (const c of glyphStore.flattenCodes(g, flatteners)) if (c < g.codeRank) g.codeRank = c;
g.grRank = 0;
for (let i = 0; i < flatteners.length; i++) if (flatteners[i].get(g)) g.grRank |= 1 << i;
}
}
function assignSubRank(glyphStore) {
let sr = 0;
for (const g of glyphStore.glyphs()) g.subRank = sr++;
}
// In FontConfig, a font is considered "monospace" if and only if all encoded non-combining
// characters (AW > 0) have the same width. We use this method to validate whether our
// "Fixed" subfamilies are properly built.
function validateMonospace(para, glyphStore) {
let awSet = new Set();
for (const [u, n, g] of glyphStore.encodedEntries()) {
const aw = Math.round(g.advanceWidth || 0);
if (aw > 0) awSet.add(aw);
}
if (para.forceMonospace && awSet.size > 1) {
throw new Error("Unreachable! Fixed variant has wide characters");
}
if (!para.isQuasiProportional && !para.compLig && awSet.size > 2) {
throw new Error("Unreachable! Building monospace with more than 2 character widths");
}
}

View file

@ -2,12 +2,12 @@ import fs from "fs";
import { FontIo, Ot } from "ot-builder"; import { FontIo, Ot } from "ot-builder";
export function CreateEmptyFont(argv) { export function CreateEmptyFont(para) {
let font = { let font = {
head: new Ot.Head.Table(), head: new Ot.Head.Table(),
hhea: new Ot.MetricHead.Hhea(), hhea: new Ot.MetricHead.Hhea(),
os2: new Ot.Os2.Table(4), os2: new Ot.Os2.Table(4),
post: new Ot.Post.Table(argv.featureControl.exportGlyphNames ? 2 : 3, 0), post: new Ot.Post.Table(para.exportGlyphNames ? 2 : 3, 0),
maxp: Ot.Maxp.Table.TrueType(), maxp: Ot.Maxp.Table.TrueType(),
name: new Ot.Name.Table(), name: new Ot.Name.Table(),
}; };

View file

@ -1,38 +0,0 @@
import { buildGlyphs } from "@iosevka/font-glyphs";
import { copyFontMetrics } from "@iosevka/font-glyphs/aesthetics";
import { buildOtl } from "@iosevka/font-otl";
import * as Caching from "@iosevka/geometry-cache";
import { finalizeFont } from "./finalize/index.mjs";
import { CreateEmptyFont } from "./font-io/index.mjs";
import { assignFontNames } from "./naming/index.mjs";
import { convertOtd } from "./otd-conv/index.mjs";
import { generateTtfaControls } from "./ttfa-controls/index.mjs";
export async function buildFont(argv, para) {
const baseFont = CreateEmptyFont(argv);
assignFontNames(baseFont, para.naming, para.isQuasiProportional);
// Build glyphs
const gs = buildGlyphs(para);
copyFontMetrics(gs.fontMetrics, baseFont);
// Build OTL
const otl = buildOtl(para, gs.glyphStore);
// Regulate (like geometry conversion)
const excludeChars = new Set();
if (para.excludedCharRanges) {
for (const [start, end] of para.excludedCharRanges) {
for (let p = start; p <= end; p++) excludeChars.add(p);
}
}
const cache = await Caching.load(argv.iCache, argv.menu.version, argv.cacheFreshAgeKey);
const finalGs = finalizeFont(cache, para, gs.glyphStore, excludeChars, otl);
if (cache.isUpdated()) await Caching.save(argv.oCache, argv.menu.version, cache, true);
// Convert to TTF
const font = await convertOtd(baseFont, otl, finalGs);
const ttfaControls = await generateTtfaControls(finalGs, font.glyphs);
return { font, glyphStore: finalGs, cacheUpdated: cache.isUpdated(), ttfaControls };
}

View file

@ -3,12 +3,11 @@ import { Ot } from "ot-builder";
import { buildTTF } from "../font-io/index.mjs"; import { buildTTF } from "../font-io/index.mjs";
export async function buildCompatLigatures(para, font) { export async function buildCompatLigatures(para, font) {
// We need to fix the glyph order before building the TTF // MappedGlyphStore is append-only, so we do not need to worry about the order of glyphs
const glyphList = font.glyphs.decideOrder(); const glyphList = font.glyphs.decideOrder();
const gsFixed = Ot.ListGlyphStoreFactory.createStoreFromList(Array.from(glyphList));
font.glyphs = gsFixed;
const completedCodePoints = new Set(); const completedCodePoints = new Set();
const jobs = [];
// Build a provisional in-memory TTF for shaping // Build a provisional in-memory TTF for shaping
const provisionalTtf = buildTTF(font); const provisionalTtf = buildTTF(font);
@ -55,12 +54,17 @@ export async function buildCompatLigatures(para, font) {
} }
// Save the ligature glyph // Save the ligature glyph
font.glyphs.items.push(ligature); jobs.push({ name: ligature.name, unicode: entry.unicode, glyph: ligature });
font.cmap.unicode.set(entry.unicode, ligature);
if (font.gdef) font.gdef.glyphClassDef.set(ligature, Ot.Gdef.GlyphClass.Ligature);
completedCodePoints.add(entry.unicode); completedCodePoints.add(entry.unicode);
} }
// Commit jobs
for (const job of jobs) {
font.glyphs.addOtGlyph(job.name, job.glyph);
font.cmap.unicode.set(job.unicode, job.glyph);
if (font.gdef) font.gdef.glyphClassDef.set(job.glyph, Ot.Gdef.GlyphClass.Ligature);
}
hbFont.destroy(); hbFont.destroy();
hbFace.destroy(); hbFace.destroy();
hbBlob.destroy(); hbBlob.destroy();

View file

@ -3,6 +3,7 @@ import path from "path";
import zlib from "zlib"; import zlib from "zlib";
import * as Toml from "@iarna/toml"; import * as Toml from "@iarna/toml";
import * as Caching from "@iosevka/geometry-cache";
import { createGrDisplaySheet } from "@iosevka/glyph/relation"; import { createGrDisplaySheet } from "@iosevka/glyph/relation";
import * as Parameters from "@iosevka/param"; import * as Parameters from "@iosevka/param";
import { applyLigationData } from "@iosevka/param/ligation"; import { applyLigationData } from "@iosevka/param/ligation";
@ -10,33 +11,41 @@ import { applyMetricOverride } from "@iosevka/param/metric-override";
import * as VariantData from "@iosevka/param/variant"; import * as VariantData from "@iosevka/param/variant";
import { encode } from "@msgpack/msgpack"; import { encode } from "@msgpack/msgpack";
import { buildFont } from "./build-font/index.mjs";
import { saveTTF } from "./font-io/index.mjs"; import { saveTTF } from "./font-io/index.mjs";
import { buildFont } from "./font.mjs";
import { buildCompatLigatures } from "./hb-compat-ligature/index.mjs";
import { createNamingDictFromArgv } from "./naming/index.mjs"; import { createNamingDictFromArgv } from "./naming/index.mjs";
export default main; export default main;
async function main(argv) { async function main(argv) {
const paraT = await getParameters(argv); // Set up parameters
const paraT = await getParametersT(argv);
const para = paraT(argv); const para = paraT(argv);
const { font, glyphStore, cacheUpdated, ttfaControls } = await buildFont(argv, para);
if (argv.oCharMap) { // Set up cache
await saveCharMap(argv, glyphStore); const cache = argv.cache
} ? await Caching.load(argv.cache.input, argv.menu.version, argv.cache.freshAgeKey)
if (argv.oTtfaControls) { : null;
await fs.promises.writeFile(argv.oTtfaControls, ttfaControls.join("\n") + "\n"); // Build font
} const { font, glyphStore, cacheUpdated, ttfaControls } = await buildFont(para, cache);
if (argv.o) {
if (para.compatibilityLigatures) await buildCompatLigatures(para, font); // Save charmap
await saveTTF(argv.o, font); if (argv.oCharMap) await saveCharMap(argv, glyphStore);
// Save ttfaControls
if (argv.oTtfaControls) await fs.promises.writeFile(argv.oTtfaControls, ttfaControls);
// Save TTF
if (argv.o) await saveTTF(argv.o, font);
// Save cache
if (argv.cache && cache && cache.isUpdated()) {
await Caching.save(argv.cache.output, argv.menu.version, cache, true);
} }
return { cacheUpdated }; return { cacheUpdated };
} }
/////////////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////////////
// Parameter preparation // Parameter preparation
async function getParameters(argv) { async function getParametersT(argv) {
const PARAMETERS_TOML = path.resolve(argv.paramsDir, "./parameters.toml"); const PARAMETERS_TOML = path.resolve(argv.paramsDir, "./parameters.toml");
const WEIGHTS_TOML = path.resolve(argv.paramsDir, "./shape-weight.toml"); const WEIGHTS_TOML = path.resolve(argv.paramsDir, "./shape-weight.toml");
const WIDTHS_TOML = path.resolve(argv.paramsDir, "./shape-width.toml"); const WIDTHS_TOML = path.resolve(argv.paramsDir, "./shape-width.toml");

View file

@ -1,5 +1,6 @@
import * as Geom from "@iosevka/geometry"; import * as Geom from "@iosevka/geometry";
import { Point } from "@iosevka/geometry/point"; import { Point } from "@iosevka/geometry/point";
import { Glyph } from "@iosevka/glyph";
import * as Gr from "@iosevka/glyph/relation"; import * as Gr from "@iosevka/glyph/relation";
import { Ot } from "ot-builder"; import { Ot } from "ot-builder";
@ -57,6 +58,13 @@ class MappedGlyphStore {
if (!name) return undefined; if (!name) return undefined;
return this.m_nameMapping.get(name); return this.m_nameMapping.get(name);
} }
// Add directly from Ot.Glyphs
addOtGlyph(name, g) {
this.m_nameMapping.set(name, g);
this.m_mapping.set(new Glyph(name), g);
}
decideOrder() { decideOrder() {
const gs = Ot.ListGlyphStoreFactory.createStoreFromList([...this.m_mapping.values()]); const gs = Ot.ListGlyphStoreFactory.createStoreFromList([...this.m_mapping.values()]);
return gs.decideOrder(); return gs.decideOrder();

View file

@ -30,7 +30,7 @@ export async function generateTtfaControls(gsOrig, gsTtf) {
alignment.write(ttfaControls, gsTtf); alignment.write(ttfaControls, gsTtf);
} }
return ttfaControls; return ttfaControls.join("\n") + "\n";
} }
class Alignment { class Alignment {

View file

@ -0,0 +1,17 @@
// In FontConfig, a font is considered "monospace" if and only if all encoded non-combining
// characters (AW > 0) have the same width. We use this method to validate whether our
// "Fixed" subfamilies are properly built.
export function validateFontConfigMono(font) {
let awSet = new Set();
for (const [ch, g] of [...font.cmap.unicode.entries()]) {
const aw = g.horizontal.end - g.horizontal.start;
if (aw > 0) awSet.add(aw);
}
for (const [ch, vs, g] of [...font.cmap.vs.entries()]) {
const aw = g.horizontal.end - g.horizontal.start;
if (aw > 0) awSet.add(aw);
}
if (awSet.size > 1) {
console.error("Fixed variant has wide characters");
}
}

View file

@ -12,6 +12,7 @@ export function init(data, argv) {
if (argv.featureControl.noCvSs) para.enableCvSs = false; if (argv.featureControl.noCvSs) para.enableCvSs = false;
if (argv.featureControl.noLigation) para.enableLigation = false; if (argv.featureControl.noLigation) para.enableLigation = false;
if (argv.featureControl.buildTextureFeature) para.buildTextureFeature = true; if (argv.featureControl.buildTextureFeature) para.buildTextureFeature = true;
if (argv.featureControl.exportGlyphNames) para.exportGlyphNames = true;
return para; return para;
} }
function applyBlendingParam(argv, para, data, key, keyArgv) { function applyBlendingParam(argv, para, data, key, keyArgv) {

View file

@ -254,6 +254,8 @@ const FontInfoOf = computed.group("metadata:font-info-of", async (target, fileNa
}; };
} }
const [compositesFromBuildPlan] = await target.need(CompositesFromBuildPlan);
return { return {
name: fileName, name: fileName,
variants: bp.variants || null, variants: bp.variants || null,
@ -304,6 +306,9 @@ const FontInfoOf = computed.group("metadata:font-info-of", async (target, fileNa
// Spacing derivation -- creating faster build for spacing variants // Spacing derivation -- creating faster build for spacing variants
spacingDerive, spacingDerive,
// Composite variants from build plan -- used for variant resolution when building fonts
compositesFromBuildPlan,
}; };
}); });
@ -428,24 +433,27 @@ const DistUnhintedTTF = file.make(
const cachePath = `${SHARED_CACHE}/${cacheFileName}.mpz`; const cachePath = `${SHARED_CACHE}/${cacheFileName}.mpz`;
const cacheDiffPath = `${charMapPath.dir}/${fn}.cache.mpz`; const cacheDiffPath = `${charMapPath.dir}/${fn}.cache.mpz`;
const [comps] = await target.need( await target.need(de(charMapPath.dir), de(ttfaControlsPath.dir), de(SHARED_CACHE));
CompositesFromBuildPlan,
de(charMapPath.dir),
de(ttfaControlsPath.dir),
de(SHARED_CACHE),
);
echo.action(echo.hl.command(`Create TTF`), out.full); echo.action(echo.hl.command(`Create TTF`), out.full);
const { cacheUpdated } = await silently.node("packages/font/src/index.mjs", { const { cacheUpdated } = await silently.node("packages/font/src/index.mjs", {
o: out.full, // INPUT: font info
...(fi.buildCharMap ? { oCharMap: charMapPath.full } : {}),
paramsDir: Path.resolve("params"),
oTtfaControls: ttfaControlsPath.full,
cacheFreshAgeKey: ageKey,
iCache: cachePath,
oCache: cacheDiffPath,
compositesFromBuildPlan: comps,
...fi, ...fi,
// INPUT: path to parameters
paramsDir: Path.resolve("params"),
// TTF output. Optional.
o: out.full,
// Charmap output. Optional.
...(fi.buildCharMap ? { oCharMap: charMapPath.full } : {}),
// TTFAutohint controls output. Optional.
oTtfaControls: ttfaControlsPath.full,
// Geometry cache parameters. Optional.
cache: {
input: cachePath,
output: cacheDiffPath,
freshAgeKey: ageKey,
},
}); });
if (cacheUpdated) { if (cacheUpdated) {