@mark1russell7/typeset 0.1.2 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build/chunks/font.d.ts +25 -0
- package/build/chunks/font.d.ts.map +1 -1
- package/build/chunks/font.js +269 -40
- package/build/chunks/font.js.map +1 -1
- package/build/chunks/fonts.d.ts +25 -2
- package/build/chunks/fonts.d.ts.map +1 -1
- package/build/chunks/fonts.js +140 -3
- package/build/chunks/fonts.js.map +1 -1
- package/build/fonts-inspect.js +1 -1
- package/build/fonts.d.ts +2 -2
- package/build/fonts.js +2 -2
- package/package.json +2 -2
- package/src/fonts/bound.ts +167 -0
- package/src/fonts/font.ts +20 -4
- package/src/fonts/index.ts +1 -0
- package/src/fonts/indic.ts +54 -13
- package/src/fonts/kerning.ts +68 -7
- package/src/fonts/substitution.ts +101 -1
- package/src/fonts/tables.ts +22 -0
- package/src/fonts.ts +1 -0
package/build/chunks/fonts.d.ts
CHANGED
|
@@ -1,8 +1,31 @@
|
|
|
1
1
|
import { a as measureWidth, c as Axis, l as Instance, n as MeasureOptions, o as readFont, r as ReadOptions, t as Font } from "./font.js";
|
|
2
2
|
import { i as isEngineName, n as EngineRules, r as engines, t as EngineName } from "./engines.js";
|
|
3
3
|
import { t as Decompress } from "./sfnt.js";
|
|
4
|
+
/** The limit of `widthBound` for the width of a text over a declared set of characters. It needs no measurement. */
|
|
5
|
+
interface WidthBound {
|
|
6
|
+
/** The largest total advance of one character with all its expansions, in font units. */
|
|
7
|
+
readonly advance: number;
|
|
8
|
+
/** The largest positive kerning of one glyph, in font units. */
|
|
9
|
+
readonly kerning: number;
|
|
10
|
+
/** The largest count of glyphs of one character. */
|
|
11
|
+
readonly glyphs: number;
|
|
12
|
+
/**
|
|
13
|
+
* This function gives the limit in px for a text of `length` UTF-16 units at the size `size`. It takes the length of a
|
|
14
|
+
* string, so it does not read the text. The limit is for a text with only the characters of the set.
|
|
15
|
+
*/
|
|
16
|
+
width(length: number, size: number): number;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* This function gives a limit for the width of each text over the characters `characters`, with no measurement. For each such
|
|
20
|
+
* text, `measureWidth(font, text, size, options)` is at most `width(text.length, size)` of the result. Give the same `options`.
|
|
21
|
+
* A text with another character has no limit from this function.
|
|
22
|
+
*
|
|
23
|
+
* The limit is sound, not tight. It follows the widest character of the set. It includes each glyph of the GSUB closure, each
|
|
24
|
+
* dotted circle, each part or composite of the Indic normalization, and the largest positive kerning.
|
|
25
|
+
*/
|
|
26
|
+
declare function widthBound(font: Font, characters: string, options?: MeasureOptions): WidthBound;
|
|
4
27
|
declare namespace fonts_d_exports {
|
|
5
|
-
export { Axis, Decompress, EngineName, EngineRules, Font, Instance, MeasureOptions, ReadOptions, engines, isEngineName, measureWidth, readFont };
|
|
28
|
+
export { Axis, Decompress, EngineName, EngineRules, Font, Instance, MeasureOptions, ReadOptions, WidthBound, engines, isEngineName, measureWidth, readFont, widthBound };
|
|
6
29
|
}
|
|
7
|
-
export { fonts_d_exports as t };
|
|
30
|
+
export { WidthBound as n, widthBound as r, fonts_d_exports as t };
|
|
8
31
|
//# sourceMappingURL=fonts.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"fonts.d.ts","names":[],"sources":["../../src/fonts.ts"],"mappings":""}
|
|
1
|
+
{"version":3,"file":"fonts.d.ts","names":[],"sources":["../../src/fonts/bound.ts","../../src/fonts.ts"],"mappings":";;;;UAMiB;;WAEN;;WAEA;;WAEA;;;;;EAKT,MAAM,gBAAgB;;;;;;;;;;iBA6FR,WAAW,MAAM,MAAM,oBAAoB,UAAS,iBAAsB"}
|
package/build/chunks/fonts.js
CHANGED
|
@@ -1,13 +1,150 @@
|
|
|
1
1
|
import { t as __exportAll } from "./rolldown-runtime.js";
|
|
2
|
-
import { n as measureWidth, r as readFont } from "./font.js";
|
|
2
|
+
import { c as indicFeatures, i as shapingOf, l as indicForms, n as measureWidth, r as readFont, s as joiningStages } from "./font.js";
|
|
3
3
|
import { n as isEngineName, t as engines } from "./engines.js";
|
|
4
|
+
// src/fonts/bound.ts
|
|
5
|
+
/**
|
|
6
|
+
* The largest count of passes over the lookups of a shaper with stages. A pass covers one stage, and no shaper has more stages:
|
|
7
|
+
* the Indic shaper has 13 and the joining has 11. A rule that expands its own glyph has no fixed point, so it stops at this count.
|
|
8
|
+
*/
|
|
9
|
+
const ROUNDS = 16;
|
|
10
|
+
/** The relative error of one rounding to single precision. */
|
|
11
|
+
const FLOAT32 = 2 ** -24;
|
|
12
|
+
/** The relative error of one rounding to double precision. */
|
|
13
|
+
const FLOAT64 = 2 ** -53;
|
|
14
|
+
/** The largest size of a kerning value of a `ValueRecord`, and of its variation delta, in font units: the range of `int16`. */
|
|
15
|
+
const KERNING_RANGE = 2 ** 15;
|
|
16
|
+
function larger(a, b) {
|
|
17
|
+
return {
|
|
18
|
+
advance: Math.max(a.advance, b.advance),
|
|
19
|
+
glyphs: Math.max(a.glyphs, b.glyphs)
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
function sum(a, b) {
|
|
23
|
+
return {
|
|
24
|
+
advance: a.advance + b.advance,
|
|
25
|
+
glyphs: a.glyphs + b.glyphs
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
/** This function tells if a rule can apply to the glyphs `reached`. A ligature needs its first glyph and all its components. */
|
|
29
|
+
function applies(rule, reached) {
|
|
30
|
+
return reached.has(rule.from) && (rule.components ?? []).every((glyph) => reached.has(glyph));
|
|
31
|
+
}
|
|
32
|
+
/** This function adds the glyphs that the rules can give to `reached`. */
|
|
33
|
+
function reach(reached, rules) {
|
|
34
|
+
for (let size = -1; size !== reached.size;) {
|
|
35
|
+
size = reached.size;
|
|
36
|
+
for (const rule of rules) if (applies(rule, reached)) for (const glyph of rule.to) reached.add(glyph);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* This function applies one list of rules to the expansions. Each rule uses the values from before the list. A lookup applies
|
|
41
|
+
* once at a position, so its output does not go back into it. The function tells if a value changed.
|
|
42
|
+
*/
|
|
43
|
+
function relax(rules, expansions) {
|
|
44
|
+
const changes = [];
|
|
45
|
+
for (const { from, to } of rules) {
|
|
46
|
+
const before = expansions.get(from);
|
|
47
|
+
const after = to.reduce((total, glyph) => sum(total, expansions.get(glyph)), {
|
|
48
|
+
advance: 0,
|
|
49
|
+
glyphs: 0
|
|
50
|
+
});
|
|
51
|
+
if (after.advance > before.advance || after.glyphs > before.glyphs) changes.push([from, after]);
|
|
52
|
+
}
|
|
53
|
+
for (const [from, after] of changes) expansions.set(from, larger(expansions.get(from), after));
|
|
54
|
+
return changes.length > 0;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* This function gives the largest expansion of each glyph of `reached`. Each ligature charges its first glyph, so each output
|
|
58
|
+
* glyph comes from one input glyph. Each pass goes backward over the lookups: a glyph then gets the expansions of the lookups
|
|
59
|
+
* after the lookup that makes it. A shaper with stages goes over the list again for each stage, so the passes repeat.
|
|
60
|
+
*
|
|
61
|
+
* One context rule can apply its nested lookups in any order. So the lists of a lookup with nested lookups repeat until no
|
|
62
|
+
* value changes, with the same limit. A value that stops changing holds for each order.
|
|
63
|
+
*/
|
|
64
|
+
function expansionsOf(font, reached, lookups, rounds) {
|
|
65
|
+
const expansions = /* @__PURE__ */ new Map();
|
|
66
|
+
for (const glyph of reached) expansions.set(glyph, {
|
|
67
|
+
advance: Math.max(0, font.advance(glyph)),
|
|
68
|
+
glyphs: 1
|
|
69
|
+
});
|
|
70
|
+
const active = lookups.map((lists) => lists.map((rules) => rules.filter((rule) => applies(rule, reached))));
|
|
71
|
+
for (let round = 0; round < rounds; round++) {
|
|
72
|
+
let changed = false;
|
|
73
|
+
for (let i = active.length - 1; i >= 0; i--) {
|
|
74
|
+
const lists = active[i];
|
|
75
|
+
for (let step = 0; step < (lists.length > 1 ? ROUNDS : 1); step++) {
|
|
76
|
+
let moved = false;
|
|
77
|
+
for (const rules of lists) moved = relax(rules, expansions) || moved;
|
|
78
|
+
if (!moved) break;
|
|
79
|
+
changed = true;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
if (!changed) break;
|
|
83
|
+
}
|
|
84
|
+
return expansions;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* This function gives a limit for the width of each text over the characters `characters`, with no measurement. For each such
|
|
88
|
+
* text, `measureWidth(font, text, size, options)` is at most `width(text.length, size)` of the result. Give the same `options`.
|
|
89
|
+
* A text with another character has no limit from this function.
|
|
90
|
+
*
|
|
91
|
+
* The limit is sound, not tight. It follows the widest character of the set. It includes each glyph of the GSUB closure, each
|
|
92
|
+
* dotted circle, each part or composite of the Indic normalization, and the largest positive kerning.
|
|
93
|
+
*/
|
|
94
|
+
function widthBound(font, characters, options = {}) {
|
|
95
|
+
const { script, features, shaper, kerning: pairLookups } = shapingOf(font, options);
|
|
96
|
+
const set = new Set(Array.from(characters, (character) => character.codePointAt(0)));
|
|
97
|
+
const indic = shaper === "indic" ? indicForms(set, (codePoint) => font.glyph(codePoint) !== 0) : void 0;
|
|
98
|
+
const reached = new Set([...indic?.codePoints ?? set].map((codePoint) => font.glyph(codePoint)));
|
|
99
|
+
const tags = shaper === "indic" ? indicFeatures(features) : shaper === "joining" ? joiningStages(features, true).flatMap((stage) => stage.features) : features;
|
|
100
|
+
const lookups = font.substitutions(script, tags).map((lookup) => lookup.rules());
|
|
101
|
+
reach(reached, lookups.flat(2));
|
|
102
|
+
const expansions = expansionsOf(font, reached, lookups, shaper === "plain" ? 1 : ROUNDS);
|
|
103
|
+
const form = (codePoint) => (indic?.composites.get(codePoint) ?? []).reduce((best, composite) => larger(best, form(composite)), expansions.get(font.glyph(codePoint)));
|
|
104
|
+
const inserted = indic === void 0 ? {
|
|
105
|
+
advance: 0,
|
|
106
|
+
glyphs: 0
|
|
107
|
+
} : expansions.get(font.glyph(9676));
|
|
108
|
+
let largest = {
|
|
109
|
+
advance: 0,
|
|
110
|
+
glyphs: 0
|
|
111
|
+
};
|
|
112
|
+
for (const codePoint of set) {
|
|
113
|
+
const parts = indic?.parts.get(codePoint) ?? [codePoint];
|
|
114
|
+
largest = larger(largest, parts.reduce((total, part) => sum(total, sum(form(part), inserted)), {
|
|
115
|
+
advance: 0,
|
|
116
|
+
glyphs: 0
|
|
117
|
+
}));
|
|
118
|
+
}
|
|
119
|
+
let kerning = 0;
|
|
120
|
+
for (const lookup of pairLookups) {
|
|
121
|
+
const { first, second } = lookup.largest(reached);
|
|
122
|
+
kerning += first + second;
|
|
123
|
+
}
|
|
124
|
+
const { advance, glyphs } = largest;
|
|
125
|
+
const pixel = options.rounding === "pixel";
|
|
126
|
+
const float32 = options.sum === "float32" && !pixel;
|
|
127
|
+
const pixelMargin = pixel ? .5078125 + (kerning > 0 ? .5 : 0) : 0;
|
|
128
|
+
const positions = pairLookups.length * 2;
|
|
129
|
+
const positionMargin = float32 ? positions * (1.5 + 2 * KERNING_RANGE / font.unitsPerEm) / 65536 : 0;
|
|
130
|
+
const kerningMargin = float32 ? positions * 5 * FLOAT32 * KERNING_RANGE : 0;
|
|
131
|
+
return {
|
|
132
|
+
advance,
|
|
133
|
+
kerning,
|
|
134
|
+
glyphs,
|
|
135
|
+
width(length, size) {
|
|
136
|
+
return length * ((advance + glyphs * (kerning + kerningMargin)) * (size / font.unitsPerEm) + glyphs * (pixelMargin + positionMargin)) * (float32 ? (1 + FLOAT32) ** (length + 6) : 1) * (1 + (8192 + length) * FLOAT64);
|
|
137
|
+
}
|
|
138
|
+
};
|
|
139
|
+
}
|
|
4
140
|
// src/fonts.ts
|
|
5
141
|
var fonts_exports = /* @__PURE__ */ __exportAll({
|
|
6
142
|
engines: () => engines,
|
|
7
143
|
isEngineName: () => isEngineName,
|
|
8
144
|
measureWidth: () => measureWidth,
|
|
9
|
-
readFont: () => readFont
|
|
145
|
+
readFont: () => readFont,
|
|
146
|
+
widthBound: () => widthBound
|
|
10
147
|
});
|
|
11
|
-
export { fonts_exports as t };
|
|
148
|
+
export { widthBound as n, fonts_exports as t };
|
|
12
149
|
|
|
13
150
|
//# sourceMappingURL=fonts.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"fonts.js","names":[],"sources":["../../src/fonts.ts"],"sourcesContent":["/**\n * The subpath `./fonts`: the font model of typeset. It reads the tables of a TrueType, OpenType or WOFF2 file. It gives\n * the width of a text as the text engine of a browser gives it. The host gives the bytes of the font files.\n */\nexport { measureWidth, readFont, type Font, type MeasureOptions, type ReadOptions } from \"./fonts/font.ts\";\nexport { engines, isEngineName, type EngineName, type EngineRules } from \"./fonts/engines.ts\";\nexport type { Decompress } from \"./fonts/sfnt.ts\";\nexport type { Axis, Instance } from \"./fonts/variations.ts\";\n"],"mappings":""}
|
|
1
|
+
{"version":3,"file":"fonts.js","names":[],"sources":["../../src/fonts/bound.ts","../../src/fonts.ts"],"sourcesContent":["import { shapingOf, type Font, type MeasureOptions } from \"./font.ts\";\nimport { indicFeatures, indicForms } from \"./indic.ts\";\nimport { joiningStages } from \"./joining.ts\";\nimport type { SubstitutionRule } from \"./substitution.ts\";\n\n/** The limit of `widthBound` for the width of a text over a declared set of characters. It needs no measurement. */\nexport interface WidthBound {\n /** The largest total advance of one character with all its expansions, in font units. */\n readonly advance: number;\n /** The largest positive kerning of one glyph, in font units. */\n readonly kerning: number;\n /** The largest count of glyphs of one character. */\n readonly glyphs: number;\n /**\n * This function gives the limit in px for a text of `length` UTF-16 units at the size `size`. It takes the length of a\n * string, so it does not read the text. The limit is for a text with only the characters of the set.\n */\n width(length: number, size: number): number;\n}\n\n/**\n * The largest count of passes over the lookups of a shaper with stages. A pass covers one stage, and no shaper has more stages:\n * the Indic shaper has 13 and the joining has 11. A rule that expands its own glyph has no fixed point, so it stops at this count.\n */\nconst ROUNDS = 16;\n/** The relative error of one rounding to single precision. */\nconst FLOAT32 = 2 ** -24;\n/** The relative error of one rounding to double precision. */\nconst FLOAT64 = 2 ** -53;\n/** The largest size of a kerning value of a `ValueRecord`, and of its variation delta, in font units: the range of `int16`. */\nconst KERNING_RANGE = 2 ** 15;\n\n/** The largest expansion of a glyph: the sum of its advances, and the count of its glyphs. */\ninterface Expansion {\n readonly advance: number;\n readonly glyphs: number;\n}\n\nfunction larger(a: Expansion, b: Expansion): Expansion {\n return { advance: Math.max(a.advance, b.advance), glyphs: Math.max(a.glyphs, b.glyphs) };\n}\n\nfunction sum(a: Expansion, b: Expansion): Expansion {\n return { advance: a.advance + b.advance, glyphs: a.glyphs + b.glyphs };\n}\n\n/** This function tells if a rule can apply to the glyphs `reached`. A ligature needs its first glyph and all its components. */\nfunction applies(rule: SubstitutionRule, reached: ReadonlySet<number>): boolean {\n return reached.has(rule.from) && (rule.components ?? []).every((glyph) => reached.has(glyph));\n}\n\n/** This function adds the glyphs that the rules can give to `reached`. */\nfunction reach(reached: Set<number>, rules: readonly SubstitutionRule[]): void {\n for (let size = -1; size !== reached.size; ) {\n size = reached.size;\n for (const rule of rules) if (applies(rule, reached)) for (const glyph of rule.to) reached.add(glyph);\n }\n}\n\n/**\n * This function applies one list of rules to the expansions. Each rule uses the values from before the list. A lookup applies\n * once at a position, so its output does not go back into it. The function tells if a value changed.\n */\nfunction relax(rules: readonly SubstitutionRule[], expansions: Map<number, Expansion>): boolean {\n const changes: (readonly [number, Expansion])[] = [];\n for (const { from, to } of rules) {\n const before = expansions.get(from)!;\n const after = to.reduce((total, glyph) => sum(total, expansions.get(glyph)!), { advance: 0, glyphs: 0 });\n if (after.advance > before.advance || after.glyphs > before.glyphs) changes.push([from, after]);\n }\n for (const [from, after] of changes) expansions.set(from, larger(expansions.get(from)!, after));\n return changes.length > 0;\n}\n\n/**\n * This function gives the largest expansion of each glyph of `reached`. Each ligature charges its first glyph, so each output\n * glyph comes from one input glyph. Each pass goes backward over the lookups: a glyph then gets the expansions of the lookups\n * after the lookup that makes it. A shaper with stages goes over the list again for each stage, so the passes repeat.\n *\n * One context rule can apply its nested lookups in any order. So the lists of a lookup with nested lookups repeat until no\n * value changes, with the same limit. A value that stops changing holds for each order.\n */\nfunction expansionsOf(font: Font, reached: ReadonlySet<number>, lookups: readonly (readonly (readonly SubstitutionRule[])[])[], rounds: number): Map<number, Expansion> {\n const expansions = new Map<number, Expansion>();\n for (const glyph of reached) expansions.set(glyph, { advance: Math.max(0, font.advance(glyph)), glyphs: 1 });\n const active = lookups.map((lists) => lists.map((rules) => rules.filter((rule) => applies(rule, reached))));\n for (let round = 0; round < rounds; round++) {\n let changed = false;\n for (let i = active.length - 1; i >= 0; i--) {\n const lists = active[i]!;\n for (let step = 0; step < (lists.length > 1 ? ROUNDS : 1); step++) {\n let moved = false;\n for (const rules of lists) moved = relax(rules, expansions) || moved;\n if (!moved) break;\n changed = true;\n }\n }\n if (!changed) break;\n }\n return expansions;\n}\n\n/**\n * This function gives a limit for the width of each text over the characters `characters`, with no measurement. For each such\n * text, `measureWidth(font, text, size, options)` is at most `width(text.length, size)` of the result. Give the same `options`.\n * A text with another character has no limit from this function.\n *\n * The limit is sound, not tight. It follows the widest character of the set. It includes each glyph of the GSUB closure, each\n * dotted circle, each part or composite of the Indic normalization, and the largest positive kerning.\n */\nexport function widthBound(font: Font, characters: string, options: MeasureOptions = {}): WidthBound {\n const { script, features, shaper, kerning: pairLookups } = shapingOf(font, options);\n const set = new Set(Array.from(characters, (character) => character.codePointAt(0)!));\n const indic = shaper === \"indic\" ? indicForms(set, (codePoint) => font.glyph(codePoint) !== 0) : undefined;\n\n // The roots: the glyph of each code point that the shaper can see. A character with no glyph gives the glyph 0\n const reached = new Set([...(indic?.codePoints ?? set)].map((codePoint) => font.glyph(codePoint)));\n // A union of the features gives the same lookups as the union of the stages. Each stage then counts, in the rounds\n const tags = shaper === \"indic\" ? indicFeatures(features) : shaper === \"joining\" ? joiningStages(features, true).flatMap((stage) => stage.features) : features;\n const lookups = font.substitutions(script, tags).map((lookup) => lookup.rules());\n reach(reached, lookups.flat(2));\n // The plain shaper is one stage: it applies each lookup once, in the order of the list\n const expansions = expansionsOf(font, reached, lookups, shaper === \"plain\" ? 1 : ROUNDS);\n\n const form = (codePoint: number): Expansion => (indic?.composites.get(codePoint) ?? []).reduce((best, composite) => larger(best, form(composite)), expansions.get(font.glyph(codePoint))!);\n // The Indic shaper can insert a dotted circle before each code point. Without a glyph, the code point U+25CC gives the glyph 0.\n // One code point gets at most one: a circle of the vowel constraints takes the next matra, so that syllable is not broken\n const inserted = indic === undefined ? { advance: 0, glyphs: 0 } : expansions.get(font.glyph(0x25cc))!;\n let largest: Expansion = { advance: 0, glyphs: 0 };\n for (const codePoint of set) {\n const parts = indic?.parts.get(codePoint) ?? [codePoint];\n largest = larger(largest, parts.reduce((total, part) => sum(total, sum(form(part), inserted)), { advance: 0, glyphs: 0 }));\n }\n\n // One glyph gets at most one first value and one second value from each lookup, and each subtable of a lookup is an alternative\n let kerning = 0;\n for (const lookup of pairLookups) {\n const { first, second } = lookup.largest(reached);\n kerning += first + second;\n }\n\n const { advance, glyphs } = largest;\n const pixel = options.rounding === \"pixel\";\n const float32 = options.sum === \"float32\" && !pixel;\n // Pixel: an advance gets at most 1/128 px from the grid of 26.6, and then 0.5 px. A kerning sum gets at most 0.5 px\n const pixelMargin = pixel ? 0.5 + 1 / 128 + (kerning > 0 ? 0.5 : 0) : 0;\n // Float32: each lookup gives a glyph two positions, in units of 1/65536 px. A negative value gets no help from the relative\n // factor below, so it has its own margin. The floor of a value and the round of a delta give 1 unit, the floor of the\n // multiplier gives 0.5 units, and the cut of the scale gives KERNING_RANGE / unitsPerEm units to each of the two parts.\n // The single precision gives FLOAT32 to the value and 4 FLOAT32 to the delta\n const positions = pairLookups.length * 2;\n const positionMargin = float32 ? (positions * (1.5 + (2 * KERNING_RANGE) / font.unitsPerEm)) / 65536 : 0;\n const kerningMargin = float32 ? positions * 5 * FLOAT32 * KERNING_RANGE : 0;\n return {\n advance,\n kerning,\n glyphs,\n width(length: number, size: number): number {\n const total = length * ((advance + glyphs * (kerning + kerningMargin)) * (size / font.unitsPerEm) + glyphs * (pixelMargin + positionMargin));\n // Float32: the fround of a glyph or 4 steps of a positive delta, the fround of a segment, and one step of the sum for\n // each segment. A text has at most one segment for each UTF-16 unit\n const single = float32 ? (1 + FLOAT32) ** (length + 6) : 1;\n // The double precision: the scale and a few products, and one rounding for each step of a sum of fractions\n return total * single * (1 + (2 ** 13 + length) * FLOAT64);\n },\n };\n}\n","/**\n * The subpath `./fonts`: the font model of typeset. It reads the tables of a TrueType, OpenType or WOFF2 file. It gives\n * the width of a text as the text engine of a browser gives it. The host gives the bytes of the font files.\n */\nexport { measureWidth, readFont, type Font, type MeasureOptions, type ReadOptions } from \"./fonts/font.ts\";\nexport { widthBound, type WidthBound } from \"./fonts/bound.ts\";\nexport { engines, isEngineName, type EngineName, type EngineRules } from \"./fonts/engines.ts\";\nexport type { Decompress } from \"./fonts/sfnt.ts\";\nexport type { Axis, Instance } from \"./fonts/variations.ts\";\n"],"mappings":";;;;;;;;AAwBA,MAAM,SAAS;;AAEf,MAAM,UAAU,KAAK;;AAErB,MAAM,UAAU,KAAK;;AAErB,MAAM,gBAAgB,KAAK;AAQ3B,SAAS,OAAO,GAAc,GAAyB;CACrD,OAAO;EAAE,SAAS,KAAK,IAAI,EAAE,SAAS,EAAE,OAAO;EAAG,QAAQ,KAAK,IAAI,EAAE,QAAQ,EAAE,MAAM;CAAE;AACzF;AAEA,SAAS,IAAI,GAAc,GAAyB;CAClD,OAAO;EAAE,SAAS,EAAE,UAAU,EAAE;EAAS,QAAQ,EAAE,SAAS,EAAE;CAAO;AACvE;;AAGA,SAAS,QAAQ,MAAwB,SAAuC;CAC9E,OAAO,QAAQ,IAAI,KAAK,IAAI,MAAM,KAAK,cAAc,CAAC,EAAA,CAAG,OAAO,UAAU,QAAQ,IAAI,KAAK,CAAC;AAC9F;;AAGA,SAAS,MAAM,SAAsB,OAA0C;CAC7E,KAAK,IAAI,OAAO,IAAI,SAAS,QAAQ,OAAQ;EAC3C,OAAO,QAAQ;EACf,KAAK,MAAM,QAAQ,OAAO,IAAI,QAAQ,MAAM,OAAO,GAAG,KAAK,MAAM,SAAS,KAAK,IAAI,QAAQ,IAAI,KAAK;CACtG;AACF;;;;;AAMA,SAAS,MAAM,OAAoC,YAA6C;CAC9F,MAAM,UAA4C,CAAC;CACnD,KAAK,MAAM,EAAE,MAAM,QAAQ,OAAO;EAChC,MAAM,SAAS,WAAW,IAAI,IAAI;EAClC,MAAM,QAAQ,GAAG,QAAQ,OAAO,UAAU,IAAI,OAAO,WAAW,IAAI,KAAK,CAAE,GAAG;GAAE,SAAS;GAAG,QAAQ;EAAE,CAAC;EACvG,IAAI,MAAM,UAAU,OAAO,WAAW,MAAM,SAAS,OAAO,QAAQ,QAAQ,KAAK,CAAC,MAAM,KAAK,CAAC;CAChG;CACA,KAAK,MAAM,CAAC,MAAM,UAAU,SAAS,WAAW,IAAI,MAAM,OAAO,WAAW,IAAI,IAAI,GAAI,KAAK,CAAC;CAC9F,OAAO,QAAQ,SAAS;AAC1B;;;;;;;;;AAUA,SAAS,aAAa,MAAY,SAA8B,SAAgE,QAAwC;CACtK,MAAM,6BAAa,IAAI,IAAuB;CAC9C,KAAK,MAAM,SAAS,SAAS,WAAW,IAAI,OAAO;EAAE,SAAS,KAAK,IAAI,GAAG,KAAK,QAAQ,KAAK,CAAC;EAAG,QAAQ;CAAE,CAAC;CAC3G,MAAM,SAAS,QAAQ,KAAK,UAAU,MAAM,KAAK,UAAU,MAAM,QAAQ,SAAS,QAAQ,MAAM,OAAO,CAAC,CAAC,CAAC;CAC1G,KAAK,IAAI,QAAQ,GAAG,QAAQ,QAAQ,SAAS;EAC3C,IAAI,UAAU;EACd,KAAK,IAAI,IAAI,OAAO,SAAS,GAAG,KAAK,GAAG,KAAK;GAC3C,MAAM,QAAQ,OAAO;GACrB,KAAK,IAAI,OAAO,GAAG,QAAQ,MAAM,SAAS,IAAI,SAAS,IAAI,QAAQ;IACjE,IAAI,QAAQ;IACZ,KAAK,MAAM,SAAS,OAAO,QAAQ,MAAM,OAAO,UAAU,KAAK;IAC/D,IAAI,CAAC,OAAO;IACZ,UAAU;GACZ;EACF;EACA,IAAI,CAAC,SAAS;CAChB;CACA,OAAO;AACT;;;;;;;;;AAUA,SAAgB,WAAW,MAAY,YAAoB,UAA0B,CAAC,GAAe;CACnG,MAAM,EAAE,QAAQ,UAAU,QAAQ,SAAS,gBAAgB,UAAU,MAAM,OAAO;CAClF,MAAM,MAAM,IAAI,IAAI,MAAM,KAAK,aAAa,cAAc,UAAU,YAAY,CAAC,CAAE,CAAC;CACpF,MAAM,QAAQ,WAAW,UAAU,WAAW,MAAM,cAAc,KAAK,MAAM,SAAS,MAAM,CAAC,IAAI,KAAA;CAGjG,MAAM,UAAU,IAAI,IAAI,CAAC,GAAI,OAAO,cAAc,GAAI,CAAC,CAAC,KAAK,cAAc,KAAK,MAAM,SAAS,CAAC,CAAC;CAEjG,MAAM,OAAO,WAAW,UAAU,cAAc,QAAQ,IAAI,WAAW,YAAY,cAAc,UAAU,IAAI,CAAC,CAAC,SAAS,UAAU,MAAM,QAAQ,IAAI;CACtJ,MAAM,UAAU,KAAK,cAAc,QAAQ,IAAI,CAAC,CAAC,KAAK,WAAW,OAAO,MAAM,CAAC;CAC/E,MAAM,SAAS,QAAQ,KAAK,CAAC,CAAC;CAE9B,MAAM,aAAa,aAAa,MAAM,SAAS,SAAS,WAAW,UAAU,IAAI,MAAM;CAEvF,MAAM,QAAQ,eAAkC,OAAO,WAAW,IAAI,SAAS,KAAK,CAAC,EAAA,CAAG,QAAQ,MAAM,cAAc,OAAO,MAAM,KAAK,SAAS,CAAC,GAAG,WAAW,IAAI,KAAK,MAAM,SAAS,CAAC,CAAE;CAGzL,MAAM,WAAW,UAAU,KAAA,IAAY;EAAE,SAAS;EAAG,QAAQ;CAAE,IAAI,WAAW,IAAI,KAAK,MAAM,IAAM,CAAC;CACpG,IAAI,UAAqB;EAAE,SAAS;EAAG,QAAQ;CAAE;CACjD,KAAK,MAAM,aAAa,KAAK;EAC3B,MAAM,QAAQ,OAAO,MAAM,IAAI,SAAS,KAAK,CAAC,SAAS;EACvD,UAAU,OAAO,SAAS,MAAM,QAAQ,OAAO,SAAS,IAAI,OAAO,IAAI,KAAK,IAAI,GAAG,QAAQ,CAAC,GAAG;GAAE,SAAS;GAAG,QAAQ;EAAE,CAAC,CAAC;CAC3H;CAGA,IAAI,UAAU;CACd,KAAK,MAAM,UAAU,aAAa;EAChC,MAAM,EAAE,OAAO,WAAW,OAAO,QAAQ,OAAO;EAChD,WAAW,QAAQ;CACrB;CAEA,MAAM,EAAE,SAAS,WAAW;CAC5B,MAAM,QAAQ,QAAQ,aAAa;CACnC,MAAM,UAAU,QAAQ,QAAQ,aAAa,CAAC;CAE9C,MAAM,cAAc,QAAQ,YAAiB,UAAU,IAAI,KAAM,KAAK;CAKtE,MAAM,YAAY,YAAY,SAAS;CACvC,MAAM,iBAAiB,UAAW,aAAa,MAAO,IAAI,gBAAiB,KAAK,cAAe,QAAQ;CACvG,MAAM,gBAAgB,UAAU,YAAY,IAAI,UAAU,gBAAgB;CAC1E,OAAO;EACL;EACA;EACA;EACA,MAAM,QAAgB,MAAsB;GAM1C,OALc,WAAW,UAAU,UAAU,UAAU,mBAAmB,OAAO,KAAK,cAAc,UAAU,cAAc,oBAG7G,WAAW,IAAI,aAAa,SAAS,KAAK,MAEhC,KAAK,OAAU,UAAU;EACpD;CACF;AACF"}
|
package/build/fonts-inspect.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export {
|
|
1
|
+
export { o as canvasWords, u as defaultFeatures, t as glyphAdvances, d as readTables, a as shapingWords } from "./chunks/font.js";
|
package/build/fonts.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { t as Decompress } from "./chunks/sfnt.js";
|
|
2
2
|
import { a as measureWidth, c as Axis, l as Instance, n as MeasureOptions, o as readFont, r as ReadOptions, t as Font } from "./chunks/font.js";
|
|
3
|
+
import { n as WidthBound, r as widthBound } from "./chunks/fonts.js";
|
|
3
4
|
import { i as isEngineName, n as EngineRules, r as engines, t as EngineName } from "./chunks/engines.js";
|
|
4
|
-
|
|
5
|
-
export { type Axis, type Decompress, type EngineName, type EngineRules, type Font, type Instance, type MeasureOptions, type ReadOptions, engines, isEngineName, measureWidth, readFont };
|
|
5
|
+
export { type Axis, type Decompress, type EngineName, type EngineRules, type Font, type Instance, type MeasureOptions, type ReadOptions, type WidthBound, engines, isEngineName, measureWidth, readFont, widthBound };
|
package/build/fonts.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
import { n as measureWidth, r as readFont } from "./chunks/font.js";
|
|
2
|
+
import { n as widthBound } from "./chunks/fonts.js";
|
|
2
3
|
import { n as isEngineName, t as engines } from "./chunks/engines.js";
|
|
3
|
-
|
|
4
|
-
export { engines, isEngineName, measureWidth, readFont };
|
|
4
|
+
export { engines, isEngineName, measureWidth, readFont, widthBound };
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://json.schemastore.org/package",
|
|
3
3
|
"name": "@mark1russell7/typeset",
|
|
4
|
-
"version": "0.1.
|
|
4
|
+
"version": "0.1.3",
|
|
5
5
|
"description": "Text measurement and line breaking. The subpath ./fonts reads font tables and gives widths, ./measurer gives the advance function of a layout, ./breaking has the line breakers, and ./text has a rope.",
|
|
6
6
|
"keywords": [
|
|
7
7
|
"typeset",
|
|
@@ -68,7 +68,7 @@
|
|
|
68
68
|
"@mark1russell7/rigor-generators": "^0.1.0"
|
|
69
69
|
},
|
|
70
70
|
"devDependencies": {
|
|
71
|
-
"@mark1russell7/cue": "^
|
|
71
|
+
"@mark1russell7/cue": "^2.0.0",
|
|
72
72
|
"@mark1russell7/layout-contract": "^0.3.2",
|
|
73
73
|
"@types/node": "^26.6.5",
|
|
74
74
|
"@typeset/corpus": "0.1.0",
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import { shapingOf, type Font, type MeasureOptions } from "./font.ts";
|
|
2
|
+
import { indicFeatures, indicForms } from "./indic.ts";
|
|
3
|
+
import { joiningStages } from "./joining.ts";
|
|
4
|
+
import type { SubstitutionRule } from "./substitution.ts";
|
|
5
|
+
|
|
6
|
+
/** The limit of `widthBound` for the width of a text over a declared set of characters. It needs no measurement. */
|
|
7
|
+
export interface WidthBound {
|
|
8
|
+
/** The largest total advance of one character with all its expansions, in font units. */
|
|
9
|
+
readonly advance: number;
|
|
10
|
+
/** The largest positive kerning of one glyph, in font units. */
|
|
11
|
+
readonly kerning: number;
|
|
12
|
+
/** The largest count of glyphs of one character. */
|
|
13
|
+
readonly glyphs: number;
|
|
14
|
+
/**
|
|
15
|
+
* This function gives the limit in px for a text of `length` UTF-16 units at the size `size`. It takes the length of a
|
|
16
|
+
* string, so it does not read the text. The limit is for a text with only the characters of the set.
|
|
17
|
+
*/
|
|
18
|
+
width(length: number, size: number): number;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* The largest count of passes over the lookups of a shaper with stages. A pass covers one stage, and no shaper has more stages:
|
|
23
|
+
* the Indic shaper has 13 and the joining has 11. A rule that expands its own glyph has no fixed point, so it stops at this count.
|
|
24
|
+
*/
|
|
25
|
+
const ROUNDS = 16;
|
|
26
|
+
/** The relative error of one rounding to single precision. */
|
|
27
|
+
const FLOAT32 = 2 ** -24;
|
|
28
|
+
/** The relative error of one rounding to double precision. */
|
|
29
|
+
const FLOAT64 = 2 ** -53;
|
|
30
|
+
/** The largest size of a kerning value of a `ValueRecord`, and of its variation delta, in font units: the range of `int16`. */
|
|
31
|
+
const KERNING_RANGE = 2 ** 15;
|
|
32
|
+
|
|
33
|
+
/** The largest expansion of a glyph: the sum of its advances, and the count of its glyphs. */
|
|
34
|
+
interface Expansion {
|
|
35
|
+
readonly advance: number;
|
|
36
|
+
readonly glyphs: number;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function larger(a: Expansion, b: Expansion): Expansion {
|
|
40
|
+
return { advance: Math.max(a.advance, b.advance), glyphs: Math.max(a.glyphs, b.glyphs) };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function sum(a: Expansion, b: Expansion): Expansion {
|
|
44
|
+
return { advance: a.advance + b.advance, glyphs: a.glyphs + b.glyphs };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** This function tells if a rule can apply to the glyphs `reached`. A ligature needs its first glyph and all its components. */
|
|
48
|
+
function applies(rule: SubstitutionRule, reached: ReadonlySet<number>): boolean {
|
|
49
|
+
return reached.has(rule.from) && (rule.components ?? []).every((glyph) => reached.has(glyph));
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** This function adds the glyphs that the rules can give to `reached`. */
|
|
53
|
+
function reach(reached: Set<number>, rules: readonly SubstitutionRule[]): void {
|
|
54
|
+
for (let size = -1; size !== reached.size; ) {
|
|
55
|
+
size = reached.size;
|
|
56
|
+
for (const rule of rules) if (applies(rule, reached)) for (const glyph of rule.to) reached.add(glyph);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* This function applies one list of rules to the expansions. Each rule uses the values from before the list. A lookup applies
|
|
62
|
+
* once at a position, so its output does not go back into it. The function tells if a value changed.
|
|
63
|
+
*/
|
|
64
|
+
function relax(rules: readonly SubstitutionRule[], expansions: Map<number, Expansion>): boolean {
|
|
65
|
+
const changes: (readonly [number, Expansion])[] = [];
|
|
66
|
+
for (const { from, to } of rules) {
|
|
67
|
+
const before = expansions.get(from)!;
|
|
68
|
+
const after = to.reduce((total, glyph) => sum(total, expansions.get(glyph)!), { advance: 0, glyphs: 0 });
|
|
69
|
+
if (after.advance > before.advance || after.glyphs > before.glyphs) changes.push([from, after]);
|
|
70
|
+
}
|
|
71
|
+
for (const [from, after] of changes) expansions.set(from, larger(expansions.get(from)!, after));
|
|
72
|
+
return changes.length > 0;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* This function gives the largest expansion of each glyph of `reached`. Each ligature charges its first glyph, so each output
|
|
77
|
+
* glyph comes from one input glyph. Each pass goes backward over the lookups: a glyph then gets the expansions of the lookups
|
|
78
|
+
* after the lookup that makes it. A shaper with stages goes over the list again for each stage, so the passes repeat.
|
|
79
|
+
*
|
|
80
|
+
* One context rule can apply its nested lookups in any order. So the lists of a lookup with nested lookups repeat until no
|
|
81
|
+
* value changes, with the same limit. A value that stops changing holds for each order.
|
|
82
|
+
*/
|
|
83
|
+
function expansionsOf(font: Font, reached: ReadonlySet<number>, lookups: readonly (readonly (readonly SubstitutionRule[])[])[], rounds: number): Map<number, Expansion> {
|
|
84
|
+
const expansions = new Map<number, Expansion>();
|
|
85
|
+
for (const glyph of reached) expansions.set(glyph, { advance: Math.max(0, font.advance(glyph)), glyphs: 1 });
|
|
86
|
+
const active = lookups.map((lists) => lists.map((rules) => rules.filter((rule) => applies(rule, reached))));
|
|
87
|
+
for (let round = 0; round < rounds; round++) {
|
|
88
|
+
let changed = false;
|
|
89
|
+
for (let i = active.length - 1; i >= 0; i--) {
|
|
90
|
+
const lists = active[i]!;
|
|
91
|
+
for (let step = 0; step < (lists.length > 1 ? ROUNDS : 1); step++) {
|
|
92
|
+
let moved = false;
|
|
93
|
+
for (const rules of lists) moved = relax(rules, expansions) || moved;
|
|
94
|
+
if (!moved) break;
|
|
95
|
+
changed = true;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
if (!changed) break;
|
|
99
|
+
}
|
|
100
|
+
return expansions;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* This function gives a limit for the width of each text over the characters `characters`, with no measurement. For each such
|
|
105
|
+
* text, `measureWidth(font, text, size, options)` is at most `width(text.length, size)` of the result. Give the same `options`.
|
|
106
|
+
* A text with another character has no limit from this function.
|
|
107
|
+
*
|
|
108
|
+
* The limit is sound, not tight. It follows the widest character of the set. It includes each glyph of the GSUB closure, each
|
|
109
|
+
* dotted circle, each part or composite of the Indic normalization, and the largest positive kerning.
|
|
110
|
+
*/
|
|
111
|
+
export function widthBound(font: Font, characters: string, options: MeasureOptions = {}): WidthBound {
|
|
112
|
+
const { script, features, shaper, kerning: pairLookups } = shapingOf(font, options);
|
|
113
|
+
const set = new Set(Array.from(characters, (character) => character.codePointAt(0)!));
|
|
114
|
+
const indic = shaper === "indic" ? indicForms(set, (codePoint) => font.glyph(codePoint) !== 0) : undefined;
|
|
115
|
+
|
|
116
|
+
// The roots: the glyph of each code point that the shaper can see. A character with no glyph gives the glyph 0
|
|
117
|
+
const reached = new Set([...(indic?.codePoints ?? set)].map((codePoint) => font.glyph(codePoint)));
|
|
118
|
+
// A union of the features gives the same lookups as the union of the stages. Each stage then counts, in the rounds
|
|
119
|
+
const tags = shaper === "indic" ? indicFeatures(features) : shaper === "joining" ? joiningStages(features, true).flatMap((stage) => stage.features) : features;
|
|
120
|
+
const lookups = font.substitutions(script, tags).map((lookup) => lookup.rules());
|
|
121
|
+
reach(reached, lookups.flat(2));
|
|
122
|
+
// The plain shaper is one stage: it applies each lookup once, in the order of the list
|
|
123
|
+
const expansions = expansionsOf(font, reached, lookups, shaper === "plain" ? 1 : ROUNDS);
|
|
124
|
+
|
|
125
|
+
const form = (codePoint: number): Expansion => (indic?.composites.get(codePoint) ?? []).reduce((best, composite) => larger(best, form(composite)), expansions.get(font.glyph(codePoint))!);
|
|
126
|
+
// The Indic shaper can insert a dotted circle before each code point. Without a glyph, the code point U+25CC gives the glyph 0.
|
|
127
|
+
// One code point gets at most one: a circle of the vowel constraints takes the next matra, so that syllable is not broken
|
|
128
|
+
const inserted = indic === undefined ? { advance: 0, glyphs: 0 } : expansions.get(font.glyph(0x25cc))!;
|
|
129
|
+
let largest: Expansion = { advance: 0, glyphs: 0 };
|
|
130
|
+
for (const codePoint of set) {
|
|
131
|
+
const parts = indic?.parts.get(codePoint) ?? [codePoint];
|
|
132
|
+
largest = larger(largest, parts.reduce((total, part) => sum(total, sum(form(part), inserted)), { advance: 0, glyphs: 0 }));
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// One glyph gets at most one first value and one second value from each lookup, and each subtable of a lookup is an alternative
|
|
136
|
+
let kerning = 0;
|
|
137
|
+
for (const lookup of pairLookups) {
|
|
138
|
+
const { first, second } = lookup.largest(reached);
|
|
139
|
+
kerning += first + second;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const { advance, glyphs } = largest;
|
|
143
|
+
const pixel = options.rounding === "pixel";
|
|
144
|
+
const float32 = options.sum === "float32" && !pixel;
|
|
145
|
+
// Pixel: an advance gets at most 1/128 px from the grid of 26.6, and then 0.5 px. A kerning sum gets at most 0.5 px
|
|
146
|
+
const pixelMargin = pixel ? 0.5 + 1 / 128 + (kerning > 0 ? 0.5 : 0) : 0;
|
|
147
|
+
// Float32: each lookup gives a glyph two positions, in units of 1/65536 px. A negative value gets no help from the relative
|
|
148
|
+
// factor below, so it has its own margin. The floor of a value and the round of a delta give 1 unit, the floor of the
|
|
149
|
+
// multiplier gives 0.5 units, and the cut of the scale gives KERNING_RANGE / unitsPerEm units to each of the two parts.
|
|
150
|
+
// The single precision gives FLOAT32 to the value and 4 FLOAT32 to the delta
|
|
151
|
+
const positions = pairLookups.length * 2;
|
|
152
|
+
const positionMargin = float32 ? (positions * (1.5 + (2 * KERNING_RANGE) / font.unitsPerEm)) / 65536 : 0;
|
|
153
|
+
const kerningMargin = float32 ? positions * 5 * FLOAT32 * KERNING_RANGE : 0;
|
|
154
|
+
return {
|
|
155
|
+
advance,
|
|
156
|
+
kerning,
|
|
157
|
+
glyphs,
|
|
158
|
+
width(length: number, size: number): number {
|
|
159
|
+
const total = length * ((advance + glyphs * (kerning + kerningMargin)) * (size / font.unitsPerEm) + glyphs * (pixelMargin + positionMargin));
|
|
160
|
+
// Float32: the fround of a glyph or 4 steps of a positive delta, the fround of a segment, and one step of the sum for
|
|
161
|
+
// each segment. A text has at most one segment for each UTF-16 unit
|
|
162
|
+
const single = float32 ? (1 + FLOAT32) ** (length + 6) : 1;
|
|
163
|
+
// The double precision: the scale and a few products, and one rounding for each step of a sum of fractions
|
|
164
|
+
return total * single * (1 + (2 ** 13 + length) * FLOAT64);
|
|
165
|
+
},
|
|
166
|
+
};
|
|
167
|
+
}
|
package/src/fonts/font.ts
CHANGED
|
@@ -293,13 +293,29 @@ export function shapingWords(text: string): string[] {
|
|
|
293
293
|
return words;
|
|
294
294
|
}
|
|
295
295
|
|
|
296
|
-
/** The
|
|
297
|
-
|
|
296
|
+
/** The choices that a set of options makes for the GSUB pass and for the kerning. */
|
|
297
|
+
export interface Shaping {
|
|
298
|
+
readonly script: string;
|
|
299
|
+
/** The features of the option `features`, or `defaultFeatures`. */
|
|
300
|
+
readonly features: readonly string[];
|
|
301
|
+
readonly shaper: "indic" | "joining" | "plain";
|
|
302
|
+
readonly kerning: readonly PairLookup[];
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/** This function gives the choices of a set of options: the script, the features, the shaper and the kerning lookups. */
|
|
306
|
+
export function shapingOf(font: Font, options: MeasureOptions): Shaping {
|
|
298
307
|
const script = options.script ?? "DFLT";
|
|
299
308
|
const features = options.features ?? defaultFeatures;
|
|
300
309
|
const kerning = options.kerning === false ? [] : font.kerning(script);
|
|
301
|
-
if (options.indic !== false && indicScripts.includes(script)) return {
|
|
302
|
-
|
|
310
|
+
if (options.indic !== false && indicScripts.includes(script)) return { script, features, shaper: "indic", kerning };
|
|
311
|
+
return { script, features, shaper: options.joining !== false && joiningScripts.includes(script) ? "joining" : "plain", kerning };
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/** The GSUB pass of one set of options: a function that gives the glyphs of one word, and the kerning lookups. */
|
|
315
|
+
function shaperOf(font: Font, options: MeasureOptions): { substitute: (word: string) => number[]; kerning: readonly PairLookup[] } {
|
|
316
|
+
const { script, features, shaper, kerning } = shapingOf(font, options);
|
|
317
|
+
if (shaper === "indic") return { substitute: indicShaper(font, script, features), kerning };
|
|
318
|
+
const joins = shaper === "joining";
|
|
303
319
|
const lookups = joins ? [] : font.substitutions(script, features);
|
|
304
320
|
const stages = joins ? joiningStages(features, features.includes("rclt") && font.substitutions(script, ["rclt"]).length > 0).map((stage) => ({ stage, lookups: font.substitutions(script, stage.features) })) : [];
|
|
305
321
|
const substitute = (word: string): number[] => {
|
package/src/fonts/index.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export { readFont, measureWidth, glyphAdvances, shapingWords, type AdvanceSource, type Font, type ReadOptions, type MeasureOptions } from "./font.ts";
|
|
2
|
+
export { widthBound, type WidthBound } from "./bound.ts";
|
|
2
3
|
export { canvasSegments, canvasWords, isCjkIdeographOrSymbol } from "./words.ts";
|
|
3
4
|
export { bidiLevels, visualOrder } from "./bidi.ts";
|
|
4
5
|
export { indicScripts, indicShaper, indicSyllables, type IndicSyllableType } from "./indic.ts";
|
package/src/fonts/indic.ts
CHANGED
|
@@ -142,17 +142,7 @@ function vowelConstraints(codePoints: readonly number[]): number[] {
|
|
|
142
142
|
*/
|
|
143
143
|
function normalize(codePoints: readonly number[], hasGlyph: (codePoint: number) => boolean): number[] {
|
|
144
144
|
const allSimple = !codePoints.some((codePoint, i) => i > 0 && isMark(codePoint));
|
|
145
|
-
const decomposed
|
|
146
|
-
for (const codePoint of codePoints) {
|
|
147
|
-
const parts = codePoint === 0x931 ? [codePoint] : [...String.fromCodePoint(codePoint).normalize("NFD")].map((part) => part.codePointAt(0)!);
|
|
148
|
-
if (parts.length > 1 || parts[0] !== codePoint) {
|
|
149
|
-
if (parts.every(hasGlyph)) {
|
|
150
|
-
decomposed.push(...parts);
|
|
151
|
-
continue;
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
decomposed.push(codePoint);
|
|
155
|
-
}
|
|
145
|
+
const decomposed = codePoints.flatMap((codePoint) => decomposition(codePoint, hasGlyph));
|
|
156
146
|
if (allSimple) return decomposed;
|
|
157
147
|
// A stable sort of each run of marks with a class that is not 0
|
|
158
148
|
for (let i = 1; i < decomposed.length; i++) {
|
|
@@ -180,6 +170,48 @@ function normalize(codePoints: readonly number[], hasGlyph: (codePoint: number)
|
|
|
180
170
|
return out;
|
|
181
171
|
}
|
|
182
172
|
|
|
173
|
+
/** This function gives the code points of a character after the decomposition of `normalize`. */
|
|
174
|
+
function decomposition(codePoint: number, hasGlyph: (codePoint: number) => boolean): number[] {
|
|
175
|
+
const parts = codePoint === 0x931 ? [codePoint] : [...String.fromCodePoint(codePoint).normalize("NFD")].map((part) => part.codePointAt(0)!);
|
|
176
|
+
return (parts.length > 1 || parts[0] !== codePoint) && parts.every(hasGlyph) ? parts : [codePoint];
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/** The code points that `normalize` and `vowelConstraints` can give for a set of characters. */
|
|
180
|
+
export interface IndicForms {
|
|
181
|
+
/** The parts of each character of the set after the decomposition. A character with no decomposition is its one part. */
|
|
182
|
+
readonly parts: ReadonlyMap<number, readonly number[]>;
|
|
183
|
+
/** The composites of each code point: the code points that it becomes when a mark composes with it. */
|
|
184
|
+
readonly composites: ReadonlyMap<number, readonly number[]>;
|
|
185
|
+
/** All the code points that the shaper can see: the set, its parts, the dotted circle and each composite. */
|
|
186
|
+
readonly codePoints: ReadonlySet<number>;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* This function gives the code points that the normalization can give for the characters `characters`. A composite is
|
|
191
|
+
* not always in the set: a nukta that follows U+0928 gives U+0929. The function repeats until no new composite appears.
|
|
192
|
+
*/
|
|
193
|
+
export function indicForms(characters: ReadonlySet<number>, hasGlyph: (codePoint: number) => boolean): IndicForms {
|
|
194
|
+
const parts = new Map([...characters].map((codePoint) => [codePoint, decomposition(codePoint, hasGlyph)]));
|
|
195
|
+
const codePoints = new Set([...parts.values()].flat());
|
|
196
|
+
for (const codePoint of characters) codePoints.add(codePoint);
|
|
197
|
+
codePoints.add(0x25cc);
|
|
198
|
+
const composites = new Map<number, number[]>();
|
|
199
|
+
for (let size = 0; size !== codePoints.size; ) {
|
|
200
|
+
size = codePoints.size;
|
|
201
|
+
const marks = [...codePoints].filter(isMark);
|
|
202
|
+
for (const starter of codePoints) {
|
|
203
|
+
for (const mark of marks) {
|
|
204
|
+
const composed = compose(starter, mark);
|
|
205
|
+
if (composed === undefined || !hasGlyph(composed)) continue;
|
|
206
|
+
const known = composites.get(starter) ?? [];
|
|
207
|
+
if (!known.includes(composed)) composites.set(starter, [...known, composed]);
|
|
208
|
+
codePoints.add(composed);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
return { parts, composites, codePoints };
|
|
213
|
+
}
|
|
214
|
+
|
|
183
215
|
/** This function gives the canonical composite of two code points, or `undefined`. A mark does not compose with a mark. */
|
|
184
216
|
function compose(first: number, second: number): number | undefined {
|
|
185
217
|
if (isMark(first)) return undefined;
|
|
@@ -288,6 +320,16 @@ const basicFeatures: readonly (readonly [string, boolean])[] = [
|
|
|
288
320
|
];
|
|
289
321
|
/** The features of the last stage, after the final reordering. They match only in a syllable. */
|
|
290
322
|
const finalFeatures: readonly string[] = ["init", "pres", "abvs", "blws", "psts", "haln"];
|
|
323
|
+
/** This function gives the features of the page that the last stage applies. */
|
|
324
|
+
function pageFeatures(features: readonly string[]): string[] {
|
|
325
|
+
return features.filter((tag) => !["ccmp", "locl", "liga"].includes(tag) && !finalFeatures.includes(tag));
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/** This function gives all the features that `indicShaper` applies for the features `features` of the page. */
|
|
329
|
+
export function indicFeatures(features: readonly string[]): string[] {
|
|
330
|
+
return ["locl", "ccmp", ...basicFeatures.map(([tag]) => tag), ...finalFeatures, ...pageFeatures(features)];
|
|
331
|
+
}
|
|
332
|
+
|
|
291
333
|
/** The mask bit of the global features. Each glyph has it. */
|
|
292
334
|
const GLOBAL = 1;
|
|
293
335
|
/** The mask bits of the features that apply only to some glyphs of a syllable. */
|
|
@@ -352,8 +394,7 @@ export function indicShaper(font: Font, script: string, features: readonly strin
|
|
|
352
394
|
};
|
|
353
395
|
const first = lookupsOf(["locl", "ccmp"]);
|
|
354
396
|
const basic = basicFeatures.map(([tag, global]) => ({ lookups: lookupsOf([tag]), mask: global ? GLOBAL : featureBits.get(tag)! }));
|
|
355
|
-
const
|
|
356
|
-
const last = lookupsOf([...finalFeatures, ...extra]).map((lookup) => ({
|
|
397
|
+
const last = lookupsOf([...finalFeatures, ...pageFeatures(features)]).map((lookup) => ({
|
|
357
398
|
lookup,
|
|
358
399
|
mask: lookup.features.reduce((mask, tag) => mask | (tag === "init" ? initMask : GLOBAL), 0),
|
|
359
400
|
perSyllable: lookup.features.some((tag) => finalFeatures.includes(tag)),
|