diff --git a/packages/font/src/build-font/index.mjs b/packages/font/src/build-font/index.mjs new file mode 100644 index 000000000..a6caf3b7f --- /dev/null +++ b/packages/font/src/build-font/index.mjs @@ -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 }; +} diff --git a/packages/font/src/finalize/gc.mjs b/packages/font/src/cleanup/gc.mjs similarity index 100% rename from packages/font/src/finalize/gc.mjs rename to packages/font/src/cleanup/gc.mjs diff --git a/packages/font/src/cleanup/glyphs.mjs b/packages/font/src/cleanup/glyphs.mjs new file mode 100644 index 000000000..6ba3f4f50 --- /dev/null +++ b/packages/font/src/cleanup/glyphs.mjs @@ -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; + } +} diff --git a/packages/font/src/cleanup/index.mjs b/packages/font/src/cleanup/index.mjs new file mode 100644 index 000000000..08e054d0a --- /dev/null +++ b/packages/font/src/cleanup/index.mjs @@ -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++; +} diff --git a/packages/font/src/derive-spacing.mjs b/packages/font/src/derive-spacing.mjs index 85775d72c..ff8f033a7 100644 --- a/packages/font/src/derive-spacing.mjs +++ b/packages/font/src/derive-spacing.mjs @@ -6,6 +6,7 @@ import { CliProc, Ot } from "ot-builder"; import { readTTF, saveTTF } from "./font-io/index.mjs"; import { assignFontNames, createNamingDictFromArgv } from "./naming/index.mjs"; +import { validateFontConfigMono } from "./validate/metrics.mjs"; export default main; 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"); - } -} diff --git a/packages/font/src/finalize/glyphs.mjs b/packages/font/src/finalize/glyphs.mjs deleted file mode 100644 index 8ffea041c..000000000 --- a/packages/font/src/finalize/glyphs.mjs +++ /dev/null @@ -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; - } - } -} diff --git a/packages/font/src/finalize/index.mjs b/packages/font/src/finalize/index.mjs deleted file mode 100644 index e360bc949..000000000 --- a/packages/font/src/finalize/index.mjs +++ /dev/null @@ -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"); - } -} diff --git a/packages/font/src/font-io/index.mjs b/packages/font/src/font-io/index.mjs index 0e20b4017..a546ed986 100644 --- a/packages/font/src/font-io/index.mjs +++ b/packages/font/src/font-io/index.mjs @@ -2,12 +2,12 @@ import fs from "fs"; import { FontIo, Ot } from "ot-builder"; -export function CreateEmptyFont(argv) { +export function CreateEmptyFont(para) { let font = { head: new Ot.Head.Table(), hhea: new Ot.MetricHead.Hhea(), 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(), name: new Ot.Name.Table(), }; diff --git a/packages/font/src/font.mjs b/packages/font/src/font.mjs deleted file mode 100644 index df6c8b15f..000000000 --- a/packages/font/src/font.mjs +++ /dev/null @@ -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 }; -} diff --git a/packages/font/src/hb-compat-ligature/index.mjs b/packages/font/src/hb-compat-ligature/index.mjs index a341eec8d..9f7bb19f5 100644 --- a/packages/font/src/hb-compat-ligature/index.mjs +++ b/packages/font/src/hb-compat-ligature/index.mjs @@ -3,12 +3,11 @@ import { Ot } from "ot-builder"; import { buildTTF } from "../font-io/index.mjs"; 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 gsFixed = Ot.ListGlyphStoreFactory.createStoreFromList(Array.from(glyphList)); - font.glyphs = gsFixed; const completedCodePoints = new Set(); + const jobs = []; // Build a provisional in-memory TTF for shaping const provisionalTtf = buildTTF(font); @@ -55,12 +54,17 @@ export async function buildCompatLigatures(para, font) { } // 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); + jobs.push({ name: ligature.name, unicode: entry.unicode, glyph: ligature }); 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(); hbFace.destroy(); hbBlob.destroy(); diff --git a/packages/font/src/index.mjs b/packages/font/src/index.mjs index 4af82f141..2bd651381 100644 --- a/packages/font/src/index.mjs +++ b/packages/font/src/index.mjs @@ -3,6 +3,7 @@ import path from "path"; import zlib from "zlib"; import * as Toml from "@iarna/toml"; +import * as Caching from "@iosevka/geometry-cache"; import { createGrDisplaySheet } from "@iosevka/glyph/relation"; import * as Parameters from "@iosevka/param"; 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 { encode } from "@msgpack/msgpack"; +import { buildFont } from "./build-font/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"; export default main; async function main(argv) { - const paraT = await getParameters(argv); + // Set up parameters + const paraT = await getParametersT(argv); const para = paraT(argv); - const { font, glyphStore, cacheUpdated, ttfaControls } = await buildFont(argv, para); - if (argv.oCharMap) { - await saveCharMap(argv, glyphStore); - } - if (argv.oTtfaControls) { - await fs.promises.writeFile(argv.oTtfaControls, ttfaControls.join("\n") + "\n"); - } - if (argv.o) { - if (para.compatibilityLigatures) await buildCompatLigatures(para, font); - await saveTTF(argv.o, font); + + // Set up cache + const cache = argv.cache + ? await Caching.load(argv.cache.input, argv.menu.version, argv.cache.freshAgeKey) + : null; + // Build font + const { font, glyphStore, cacheUpdated, ttfaControls } = await buildFont(para, cache); + + // Save charmap + 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 }; } /////////////////////////////////////////////////////////////////////////////////////////////////// // Parameter preparation -async function getParameters(argv) { +async function getParametersT(argv) { const PARAMETERS_TOML = path.resolve(argv.paramsDir, "./parameters.toml"); const WEIGHTS_TOML = path.resolve(argv.paramsDir, "./shape-weight.toml"); const WIDTHS_TOML = path.resolve(argv.paramsDir, "./shape-width.toml"); diff --git a/packages/font/src/otd-conv/glyphs.mjs b/packages/font/src/otd-conv/glyphs.mjs index 53037c247..ca8ef0acb 100644 --- a/packages/font/src/otd-conv/glyphs.mjs +++ b/packages/font/src/otd-conv/glyphs.mjs @@ -1,5 +1,6 @@ import * as Geom from "@iosevka/geometry"; import { Point } from "@iosevka/geometry/point"; +import { Glyph } from "@iosevka/glyph"; import * as Gr from "@iosevka/glyph/relation"; import { Ot } from "ot-builder"; @@ -57,6 +58,13 @@ class MappedGlyphStore { if (!name) return undefined; 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() { const gs = Ot.ListGlyphStoreFactory.createStoreFromList([...this.m_mapping.values()]); return gs.decideOrder(); diff --git a/packages/font/src/ttfa-controls/index.mjs b/packages/font/src/ttfa-controls/index.mjs index 5c816d33f..b9fb2fee0 100644 --- a/packages/font/src/ttfa-controls/index.mjs +++ b/packages/font/src/ttfa-controls/index.mjs @@ -30,7 +30,7 @@ export async function generateTtfaControls(gsOrig, gsTtf) { alignment.write(ttfaControls, gsTtf); } - return ttfaControls; + return ttfaControls.join("\n") + "\n"; } class Alignment { diff --git a/packages/font/src/validate/metrics.mjs b/packages/font/src/validate/metrics.mjs new file mode 100644 index 000000000..7000ad075 --- /dev/null +++ b/packages/font/src/validate/metrics.mjs @@ -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"); + } +} diff --git a/packages/param/src/index.mjs b/packages/param/src/index.mjs index cf602cf3b..f0642bcab 100644 --- a/packages/param/src/index.mjs +++ b/packages/param/src/index.mjs @@ -12,6 +12,7 @@ export function init(data, argv) { if (argv.featureControl.noCvSs) para.enableCvSs = false; if (argv.featureControl.noLigation) para.enableLigation = false; if (argv.featureControl.buildTextureFeature) para.buildTextureFeature = true; + if (argv.featureControl.exportGlyphNames) para.exportGlyphNames = true; return para; } function applyBlendingParam(argv, para, data, key, keyArgv) { diff --git a/verdafile.mjs b/verdafile.mjs index d6b2f23aa..1095c6c3b 100644 --- a/verdafile.mjs +++ b/verdafile.mjs @@ -254,6 +254,8 @@ const FontInfoOf = computed.group("metadata:font-info-of", async (target, fileNa }; } + const [compositesFromBuildPlan] = await target.need(CompositesFromBuildPlan); + return { name: fileName, 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 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 cacheDiffPath = `${charMapPath.dir}/${fn}.cache.mpz`; - const [comps] = await target.need( - CompositesFromBuildPlan, - de(charMapPath.dir), - de(ttfaControlsPath.dir), - de(SHARED_CACHE), - ); + await target.need(de(charMapPath.dir), de(ttfaControlsPath.dir), de(SHARED_CACHE)); echo.action(echo.hl.command(`Create TTF`), out.full); const { cacheUpdated } = await silently.node("packages/font/src/index.mjs", { - o: out.full, - ...(fi.buildCharMap ? { oCharMap: charMapPath.full } : {}), - paramsDir: Path.resolve("params"), - oTtfaControls: ttfaControlsPath.full, - cacheFreshAgeKey: ageKey, - iCache: cachePath, - oCache: cacheDiffPath, - compositesFromBuildPlan: comps, + // INPUT: font info ...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) {