Generate TTFAutohint control files for better glyph display for variant glyphs (#1963).

This commit is contained in:
be5invis 2023-08-27 21:48:13 -07:00
parent 6fed1572c1
commit 25ee0bcc50
9 changed files with 932 additions and 31 deletions

View file

@ -5,3 +5,4 @@
- COMBINING DOUBLE VERTICAL STROKE OVERLAY (`U+20E6`).
- COMBINING LONG DOUBLE SOLIDUS OVERLAY (`U+20EB`).
* Improve glyphs for COLON SIGN (`U+20A1`), GUARANI SIGN (`U+20B2`), and CEDI SIGN (`U+20B5`).
* Generate TTFAutohint control files for better glyph display for variant glyphs (#1963).

View file

@ -7,6 +7,7 @@ import { finalizeFont } from "./finalize/index.mjs";
import { CreateEmptyFont } from "./meta/empty-font.mjs";
import { assignFontNames } from "./meta/naming.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);
@ -30,6 +31,7 @@ export async function buildFont(argv, para) {
if (cache.isUpdated()) {
await Caching.save(argv.oCache, argv.menu.version, cache, true);
}
const font = convertOtd(baseFont, otl, finalGs);
return { font, glyphStore: finalGs, cacheUpdated: cache.isUpdated() };
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,7 +3,7 @@ import { Ot } from "ot-builder";
import { Point } from "../../support/geometry/point.mjs";
import * as Gr from "../../support/gr.mjs";
import { byCode, bySpacing, byGr, byBuildOrder } from "./glyph-name.mjs";
import { byBuildOrder, byCode, byGr, bySpacing } from "./glyph-name.mjs";
function byRank([gna, a], [gnb, b]) {
return (
@ -28,9 +28,11 @@ class MappedGlyphStore {
this.m_primaryUnicodeMapping.set(u, source);
}
queryBySourceGlyph(source) {
if (!source) return undefined;
return this.m_mapping.get(source);
}
queryByName(name) {
if (!name) return undefined;
return this.m_nameMapping.get(name);
}
decideOrder() {

View file

@ -0,0 +1,56 @@
import ttfaRanges from "../../generated/ttfa-ranges.mjs";
import * as Gr from "../../support/gr.mjs";
import { ArrayUtil } from "../../support/utils.mjs";
export async function generateTtfaControls(gsOrig, gs) {
let ttfaControls = [];
for (const alignment of ttfaRanges) {
ttfaControls.push(generateTTFAAlignments(alignment, gsOrig, gs));
}
return ttfaControls;
}
function generateTTFAAlignments(alignment, gsOrig, gsTtf) {
let collectedGlyphs = new Map();
for (const [lo, hi] of alignment.ranges) {
for (let lch = lo; lch <= hi; lch++) {
const go = gsOrig.queryByUnicode(lch);
if (!go) continue;
const gd = gsTtf.queryBySourceGlyph(go);
if (!gd) continue;
collectedGlyphs.set(go, gd);
}
}
for (;;) {
let sizeBefore = collectedGlyphs.size;
for (const [go, gd] of collectedGlyphs) {
const cvs = Gr.AnyCv.query(go);
for (const gr of cvs) {
const gnLinked = gr.get(go);
if (!gnLinked) continue;
const goLinked = gsOrig.queryByName(gnLinked);
if (!goLinked) continue;
const gdLinked = gsTtf.queryBySourceGlyph(goLinked);
if (!gdLinked) continue;
collectedGlyphs.set(goLinked, gdLinked);
}
}
let sizeAfter = collectedGlyphs.size;
if (sizeAfter <= sizeBefore) break;
}
const gOrd = gsTtf.decideOrder();
const glyphIndices = Array.from(collectedGlyphs.values()).map(gd => gOrd.reverse(gd));
const glyphIndicesRangesStr = ArrayUtil.toRanges(glyphIndices)
.map(([lo, hi]) => (lo === hi ? `${lo}` : `${lo}-${hi}`))
.join(", ");
const styleAdjustLine = `${alignment.scriptTag} ${alignment.featureTag} @ ${glyphIndicesRangesStr}`;
return styleAdjustLine;
}

View file

@ -0,0 +1,514 @@
/* eslint-disable */
// Machine generated. Do not modify。
export default [
{
"scriptTag": "latn",
"featureTag": "dflt",
"ranges": [
[
0,
177
],
[
180,
184
],
[
186,
442
],
[
444,
447
],
[
452,
452
],
[
454,
455
],
[
457,
458
],
[
460,
497
],
[
499,
659
],
[
661,
687
],
[
7424,
7461
],
[
7531,
7543
],
[
7545,
7578
],
[
7680,
7935
],
[
8192,
8303
],
[
8352,
8399
],
[
8450,
8479
],
[
8483,
8506
],
[
8508,
8524
],
[
8526,
8527
],
[
8579,
8580
],
[
11360,
11387
],
[
11390,
11391
],
[
11776,
11903
],
[
42786,
42863
],
[
42865,
42887
],
[
42891,
42894
],
[
42896,
42954
],
[
42960,
42961
],
[
42963,
42963
],
[
42965,
42969
],
[
42997,
42998
],
[
43002,
43002
],
[
43824,
43866
],
[
43872,
43876
],
[
43878,
43880
],
[
64256,
64262
],
[
122624,
122633
],
[
122635,
122654
],
[
122661,
122666
]
]
},
{
"scriptTag": "latb",
"featureTag": "dflt",
"ranges": [
[
178,
179
],
[
185,
185
],
[
7522,
7525
],
[
8304,
8304
],
[
8308,
8318
],
[
8320,
8334
],
[
8336,
8348
],
[
11388,
11388
]
]
},
{
"scriptTag": "latp",
"featureTag": "dflt",
"ranges": [
[
688,
696
],
[
736,
740
],
[
7468,
7516
],
[
7579,
7614
],
[
8305,
8305
],
[
8319,
8319
],
[
11389,
11389
],
[
42864,
42864
],
[
42994,
42996
],
[
43000,
43001
],
[
43868,
43871
],
[
43881,
43881
],
[
67456,
67461
],
[
67463,
67504
],
[
67506,
67514
]
]
},
{
"scriptTag": "grek",
"featureTag": "dflt",
"ranges": [
[
880,
883
],
[
886,
887
],
[
891,
893
],
[
895,
895
],
[
902,
902
],
[
904,
906
],
[
908,
908
],
[
910,
929
],
[
931,
993
],
[
1008,
1013
],
[
1015,
1023
],
[
7462,
7466
],
[
7936,
7957
],
[
7960,
7965
],
[
7968,
8005
],
[
8008,
8013
],
[
8016,
8023
],
[
8025,
8025
],
[
8027,
8027
],
[
8029,
8029
],
[
8031,
8061
],
[
8064,
8071
],
[
8080,
8087
],
[
8096,
8103
],
[
8112,
8116
],
[
8118,
8123
],
[
8126,
8126
],
[
8130,
8132
],
[
8134,
8139
],
[
8144,
8147
],
[
8150,
8155
],
[
8160,
8172
],
[
8178,
8180
],
[
8182,
8187
],
[
43877,
43877
]
]
},
{
"scriptTag": "grek",
"featureTag": "sups",
"ranges": [
[
890,
890
],
[
7517,
7521
],
[
7615,
7615
]
]
},
{
"scriptTag": "cyrl",
"featureTag": "dflt",
"ranges": [
[
1024,
1153
],
[
1162,
1327
],
[
7296,
7304
],
[
7467,
7467
],
[
42560,
42605
],
[
42624,
42651
]
]
},
{
"scriptTag": "grek",
"featureTag": "subs",
"ranges": [
[
7526,
7530
]
]
},
{
"scriptTag": "cyrl",
"featureTag": "sups",
"ranges": [
[
7544,
7544
],
[
42623,
42623
],
[
42652,
42653
],
[
122928,
122960
],
[
122987,
122989
]
]
},
{
"scriptTag": "cyrl",
"featureTag": "subs",
"ranges": [
[
122961,
122986
]
]
}
];

View file

@ -22,10 +22,13 @@ export default main;
async function main(argv) {
const paraT = await getParameters();
const para = paraT(argv);
const { font, glyphStore, cacheUpdated } = await buildFont(argv, para);
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);

View file

@ -114,5 +114,27 @@ export const ArrayUtil = {
},
insertSliceAt(a, i, b) {
a.splice(i, 0, ...b);
},
// Convert character array to array of ranges. Input may be unsorted.
// The output ranges has both ends inclusive.
toRanges(chars) {
chars.sort((a, b) => a - b);
const ranges = [];
let range = null;
for (const ch of chars) {
if (!range) {
range = [ch, ch];
ranges.push(range);
} else if (ch === range[1] + 1) {
range[1] = ch;
} else {
range = [ch, ch];
ranges.push(range);
}
}
return ranges;
}
};

View file

@ -0,0 +1,264 @@
import fs from "fs";
import { ArrayUtil } from "../font-src/support/utils.mjs";
setImmediate(() => {
main().catch(e => {
console.error(e);
process.exit(1);
});
});
///////////////////////////////////////////////////////////////////////////////////////////////////
async function main() {
for (const target of Targets) {
await target.filter.load();
}
const results = new Map();
nextChar: for (let lch = 0; lch < 0x20000; lch++) {
for (const target of Targets) {
if (target.filter.has(lch)) {
const resultKey = `${target.scriptTag}-${target.featureTag}`;
let result = results.get(resultKey);
if (!result) {
result = [];
results.set(resultKey, result);
}
result.push(lch);
continue nextChar;
}
}
}
let out = [];
for (const [key, value] of results) {
out.push({
scriptTag: key.split("-")[0],
featureTag: key.split("-")[1],
ranges: ArrayUtil.toRanges(value)
});
}
await fs.promises.writeFile(
"font-src/generated/ttfa-ranges.mjs",
`/* eslint-disable */\n` +
`// Machine generated. Do not modify。\n` +
`export default ` +
JSON.stringify(out, null, "\t") +
";\n"
);
}
///////////////////////////////////////////////////////////////////////////////////////////////////
class InUnicodeDataSet {
constructor(subpath) {
this.subpath = subpath;
this.dataset = null;
}
async load() {
if (this.dataset) return;
const d = (await import(`@unicode/unicode-15.0.0/${this.subpath}/code-points.js`)).default;
this.dataset = new Set(d);
}
has(lch) {
return this.dataset.has(lch);
}
}
class InScriptDataSet extends InUnicodeDataSet {
constructor(script) {
super(`Script/${script}`);
}
}
class InBlockDataSet extends InUnicodeDataSet {
constructor(block) {
super(`Block/${block}`);
}
}
class InGeneralCategoryDataSet extends InUnicodeDataSet {
constructor(general_category) {
super(`General_Category/${general_category}`);
}
}
class InString {
constructor(s) {
this.s = s;
this.dataset = null;
}
async load() {
if (this.dataset) return;
this.dataset = new Set(this.s);
}
has(lch) {
return this.dataset.has(String.fromCodePoint(lch));
}
}
class Negation {
constructor(operand) {
this.operand = operand;
}
async load() {
await this.operand.load();
}
has(lch) {
return !this.operand.has(lch);
}
}
class Conjunct {
constructor(operands) {
this.operands = operands;
}
async load() {
for (const operand of this.operands) await operand.load();
}
has(lch) {
for (const operand of this.operands) if (!operand.has(lch)) return false;
return true;
}
}
class Disjunct {
constructor(operands) {
this.operands = operands;
}
async load() {
for (const operand of this.operands) await operand.load();
}
has(lch) {
for (const operand of this.operands) if (operand.has(lch)) return true;
return false;
}
}
///////////////////////////////////////////////////////////////////////////////////////////////////
const Script = s => new InScriptDataSet(s);
const Block = b => new InBlockDataSet(b);
const GeneralCategory = gc => new InGeneralCategoryDataSet(gc);
const In = s => new InString(s);
const All = (...operands) => new Conjunct(operands);
const Either = (...operands) => new Disjunct(operands);
const Not = operand => new Negation(operand);
///////////////////////////////////////////////////////////////////////////////////////////////////
const LatinBase = {
scriptTag: "latn",
featureTag: "dflt",
filter: Either(
All(
Script("Latin"),
Either(GeneralCategory("Uppercase_Letter"), GeneralCategory("Lowercase_Letter")),
Not(Block("Halfwidth_And_Fullwidth_Forms"))
),
Block("Currency_Symbols"),
All(Block("Letterlike_Symbols"), Not(In("℀℁⅍℠℡™℻")))
)
};
const CyrillicBase = {
scriptTag: "cyrl",
featureTag: "dflt",
filter: All(
Script("Cyrillic"),
Either(GeneralCategory("Uppercase_Letter"), GeneralCategory("Lowercase_Letter"))
)
};
const GreekBase = {
scriptTag: "grek",
featureTag: "dflt",
filter: All(
Script("Greek"),
Either(GeneralCategory("Uppercase_Letter"), GeneralCategory("Lowercase_Letter"))
)
};
const LatinSubscript = {
scriptTag: "latb",
featureTag: "dflt",
filter: Either(In("ₐₑₔₕᵢⱼₖₗₘₙₒₚᵣₛₜᵤᵥₓ"))
};
const GreekSubscript = {
scriptTag: "grek",
featureTag: "subs",
filter: Either(In("ᵦᵧᵨᵩᵪ"))
};
const CyrillicSubscript = {
scriptTag: "cyrl",
featureTag: "subs",
filter: Either(In("𞁑𞁒𞁓𞁔𞁧𞁕𞁖𞁗𞁘𞁩𞁙𞁨𞁚𞁛𞁜𞁝𞁞𞁟𞁠𞁡𞁢𞁣𞁪𞁤𞁥𞁦"))
};
const LatinSuperscript = {
scriptTag: "latp",
featureTag: "dflt",
filter: Either(All(Script("Latin"), Either(GeneralCategory("Modifier_Letter"))))
};
const GreekSuperscript = {
scriptTag: "grek",
featureTag: "sups",
filter: All(Script("Greek"), Either(GeneralCategory("Modifier_Letter")))
};
const CyrillicSuperscript = {
scriptTag: "cyrl",
featureTag: "sups",
filter: All(Script("Cyrillic"), Either(GeneralCategory("Modifier_Letter")))
};
const DigitBase = {
scriptTag: "latn",
featureTag: "dflt",
filter: Either(In("0123456789"))
};
const DigitSubscript = {
scriptTag: "latb",
featureTag: "dflt",
filter: Either(In("₀₁₂₃₄₅₆₇₈₉₊₋₌₍₎"))
};
const DigitSuperscript = {
scriptTag: "latb",
featureTag: "dflt",
filter: Either(In("⁰¹²³⁴⁵⁶⁷⁸⁹⁺⁻⁼⁽⁾"))
};
const LatinPunctuation = {
scriptTag: "latn",
featureTag: "dflt",
filter: Either(
Block("Basic_Latin"),
Block("Latin_1_Supplement"),
Block("General_Punctuation"),
Block("Supplemental_Punctuation")
)
};
const Targets = [
LatinBase,
LatinSubscript,
LatinSuperscript,
GreekBase,
GreekSubscript,
GreekSuperscript,
CyrillicBase,
CyrillicSubscript,
CyrillicSuperscript,
DigitBase,
DigitSubscript,
DigitSuperscript,
LatinPunctuation
];

View file

@ -379,22 +379,25 @@ const DistUnhintedTTF = file.make(
const charMapDir = `${BUILD}/ttf/${gr}`;
const charMapPath = `${charMapDir}/${fn}.charmap.mpz`;
const noGcTtfPath = `${charMapDir}/${fn}.no-gc.ttf`;
const ttfaControlsPath = `${charMapDir}/${fn}.ttfa.txt`;
if (fi.spacingDerive) {
// The font is a spacing variant, and is derivable form an existing
// normally-spaced variant.
const noGcTtfPath = `${charMapDir}/${fn}.no-gc.unhinted.ttf`;
const spD = fi.spacingDerive;
const [deriveFrom] = await target.need(
DistUnhintedTTF(spD.prefix, spD.fileName),
de(charMapDir)
);
echo.action(echo.hl.command(`Create TTF`), out.full);
echo.action(echo.hl.command(`Hint TTF`), out.full);
await silently.node(`font-src/derive-spacing.mjs`, {
i: deriveFrom.full,
oNoGc: noGcTtfPath,
o: out.full,
oNoGc: noGcTtfPath,
...fi
});
} else {
@ -415,6 +418,7 @@ const DistUnhintedTTF = file.make(
const { cacheUpdated } = await silently.node("font-src/index.mjs", {
o: out.full,
oCharMap: charMapPath,
oTtfaControls: ttfaControlsPath,
cacheFreshAgeKey: ageKey,
iCache: cachePath,
oCache: cacheDiffPath,
@ -437,13 +441,66 @@ const DistUnhintedTTF = file.make(
}
);
const BuildNoGcTtfImpl = file.make(
(gr, f) => `${BUILD}/ttf/${gr}/${f}.no-gc.ttf`,
const BuildCM = file.make(
(gr, f) => `${BUILD}/ttf/${gr}/${f}.charmap.mpz`,
async (target, output, gr, f) => {
await target.need(DistUnhintedTTF(gr, f));
}
);
const BuildTtfaControls = file.make(
(gr, f) => `${BUILD}/ttf/${gr}/${f}.ttfa.txt`,
async (target, output, gr, f) => {
await target.need(DistUnhintedTTF(gr, f));
}
);
const DistHintedTTF = file.make(
(gr, fn) => `${DIST}/${gr}/ttf/${fn}.ttf`,
async (target, out, gr, fn) => {
const [fi, hint] = await target.need(
FontInfoOf(fn),
CheckTtfAutoHintExists,
de`${out.dir}`
);
if (fi.spacingDerive) {
// The font is a spacing variant, and is derivable form an existing
// normally-spaced variant.
const spD = fi.spacingDerive;
const charMapDir = `${BUILD}/ttf/${gr}`;
const noGcTtfPath = `${charMapDir}/${fn}.no-gc.hinted.ttf`;
const [deriveFrom] = await target.need(
DistHintedTTF(spD.prefix, spD.fileName),
de(charMapDir)
);
echo.action(echo.hl.command(`Create TTF`), out.full);
await silently.node(`font-src/derive-spacing.mjs`, {
i: deriveFrom.full,
oNoGc: noGcTtfPath,
o: out.full,
...fi
});
} else {
const [from, ttfaControls] = await target.need(
DistUnhintedTTF(gr, fn),
BuildTtfaControls(gr, fn)
);
echo.action(echo.hl.command(`Hint TTF`), out.full, echo.hl.operator("<-"), from.full);
await silently.run(hint, fi.hintParams, "-m", ttfaControls.full, from.full, out.full);
}
}
);
const BuildNoGcTtfImpl = file.make(
(gr, f) => `${BUILD}/ttf/${gr}/${f}.no-gc.hinted.ttf`,
async (target, output, gr, f) => {
await target.need(DistHintedTTF(gr, f));
}
);
const BuildNoGcTtf = task.make(
(gr, fn) => `BuildNoGcTtf::${gr}/${fn}`,
async (target, gr, fn) => {
@ -452,29 +509,12 @@ const BuildNoGcTtf = task.make(
const [noGc] = await target.need(BuildNoGcTtfImpl(gr, fn));
return noGc;
} else {
const [distUnhinted] = await target.need(DistUnhintedTTF(gr, fn));
const [distUnhinted] = await target.need(DistHintedTTF(gr, fn));
return distUnhinted;
}
}
);
const BuildCM = file.make(
(gr, f) => `${BUILD}/ttf/${gr}/${f}.charmap.mpz`,
async (target, output, gr, f) => {
await target.need(DistUnhintedTTF(gr, f));
}
);
const DistHintedTTF = file.make(
(gr, fn) => `${DIST}/${gr}/ttf/${fn}.ttf`,
async (target, out, gr, fn) => {
const [fi, hint] = await target.need(FontInfoOf(fn), CheckTtfAutoHintExists);
const [from] = await target.need(DistUnhintedTTF(gr, fn), de`${out.dir}`);
echo.action(echo.hl.command(`Hint TTF`), out.full, echo.hl.operator("<-"), from.full);
await silently.run(hint, fi.hintParams, from.full, out.full);
}
);
function formatSuffix(fmt, unhinted) {
return fmt + (unhinted ? "-unhinted" : "");
}
@ -712,12 +752,9 @@ async function buildCompositeTtc(out, inputs) {
async function buildGlyphSharingTtc(target, parts, out) {
await target.need(de`${out.dir}`);
const [ttfInputs] = await target.need(parts.map(part => BuildNoGcTtf(part.dir, part.file)));
const tmpTtc = `${out.dir}/${out.name}.unhinted.ttc`;
const ttfInputPaths = ttfInputs.map(p => p.full);
echo.action(echo.hl.command(`Create TTC`), out.full, echo.hl.operator("<-"), ttfInputPaths);
await silently.run(TTCIZE, "-u", ["-o", tmpTtc], ttfInputPaths);
await silently.run("ttfautohint", tmpTtc, out.full);
await rm(tmpTtc);
await silently.run(TTCIZE, "-u", ["-o", out.full], ttfInputPaths);
}
///////////////////////////////////////////////////////////