@mark1russell7/typeset 0.1.1 → 0.1.2
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/README.md +21 -1
- package/build/chunks/engines.d.ts +32 -0
- package/build/chunks/engines.d.ts.map +1 -0
- package/build/chunks/engines.js +35 -0
- package/build/chunks/engines.js.map +1 -0
- package/build/chunks/font.d.ts +203 -0
- package/build/chunks/font.d.ts.map +1 -0
- package/build/chunks/font.js +3550 -0
- package/build/chunks/font.js.map +1 -0
- package/build/chunks/fonts.d.ts +8 -0
- package/build/chunks/fonts.d.ts.map +1 -0
- package/build/chunks/fonts.js +13 -0
- package/build/chunks/fonts.js.map +1 -0
- package/build/chunks/measurer.d.ts +67 -0
- package/build/chunks/measurer.d.ts.map +1 -0
- package/build/chunks/measurer.js +277 -0
- package/build/chunks/measurer.js.map +1 -0
- package/build/chunks/sfnt.d.ts +22 -0
- package/build/chunks/sfnt.d.ts.map +1 -0
- package/build/chunks/text.js.map +1 -1
- package/build/fonts-inspect.d.ts +10 -0
- package/build/fonts-inspect.d.ts.map +1 -0
- package/build/fonts-inspect.js +1 -0
- package/build/fonts.d.ts +5 -0
- package/build/fonts.js +4 -0
- package/build/index.d.ts +3 -1
- package/build/index.js +3 -1
- package/build/measurer.d.ts +2 -0
- package/build/measurer.js +1 -0
- package/package.json +17 -3
- package/src/fonts/bidi.ts +125 -0
- package/src/fonts/binary.ts +41 -0
- package/src/fonts/engines.ts +38 -0
- package/src/fonts/font.ts +442 -0
- package/src/fonts/gvar.ts +220 -0
- package/src/fonts/index.ts +10 -0
- package/src/fonts/indic.ts +585 -0
- package/src/fonts/joining.ts +141 -0
- package/src/fonts/kerning.ts +217 -0
- package/src/fonts/sfnt.ts +102 -0
- package/src/fonts/substitution.ts +586 -0
- package/src/fonts/tables.ts +140 -0
- package/src/fonts/variations.ts +175 -0
- package/src/fonts/words.ts +119 -0
- package/src/fonts-inspect.ts +8 -0
- package/src/fonts.ts +8 -0
- package/src/index.ts +2 -0
- package/src/measurer/advance.ts +307 -0
- package/src/measurer.ts +5 -0
- package/src/text/rope.ts +1 -1
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The joining of the Arabic shaper. This module follows `hb-ot-shaper-arabic.cc` of HarfBuzz. It has the joining types of
|
|
3
|
+
* the characters and the state machine that gives the form of each letter. It also has the order of the GSUB features.
|
|
4
|
+
* The table of the joining types comes from `ArabicShaping.txt` of the Unicode Character Database, version 18.0.0.
|
|
5
|
+
* It covers the Arabic, Syriac, NKo and Mandaic blocks, with their supplements and extensions.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/** The joining form of a letter, as the tag of the GSUB feature that makes it. */
|
|
9
|
+
export type JoiningForm = "isol" | "fina" | "fin2" | "fin3" | "medi" | "med2" | "init";
|
|
10
|
+
|
|
11
|
+
/** The forms in the order of the features of the shaper. The index of a form gives its bit in a feature mask. */
|
|
12
|
+
export const joiningFeatures: readonly JoiningForm[] = ["isol", "fina", "fin2", "fin3", "medi", "med2", "init"];
|
|
13
|
+
|
|
14
|
+
/** The OpenType script tags that use the joining of the Arabic shaper in this model. */
|
|
15
|
+
export const joiningScripts: readonly string[] = ["arab", "syrc"];
|
|
16
|
+
|
|
17
|
+
// The columns of the state machine of HarfBuzz: the joining types U, L, R and D (C is D), and the Syriac joining groups.
|
|
18
|
+
// The type L is the column 1. No character of these blocks has the type L.
|
|
19
|
+
const U = 0;
|
|
20
|
+
const R = 2;
|
|
21
|
+
const D = 3;
|
|
22
|
+
const ALAPH = 4;
|
|
23
|
+
const DALATH_RISH = 5;
|
|
24
|
+
/** The joining type T: a transparent character, for example a mark. The state machine does not see it. */
|
|
25
|
+
const T = 6;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* The joining types of `ArabicShaping.txt`. Each triple has the first code point, the last code point and the type.
|
|
29
|
+
* The type `C` (join causing) is `D` here, as in HarfBuzz. A code point that is not in the table has the type `U`.
|
|
30
|
+
* When its general category is `Mn`, `Me` or `Cf`, it has the type `T`.
|
|
31
|
+
*/
|
|
32
|
+
const joiningTable: readonly number[] = [
|
|
33
|
+
0x600, 0x605, U, 0x608, 0x608, U, 0x60b, 0x60b, U, 0x620, 0x620, D, 0x621, 0x621, U, 0x622, 0x625, R, 0x626, 0x626, D,
|
|
34
|
+
0x627, 0x627, R, 0x628, 0x628, D, 0x629, 0x629, R, 0x62a, 0x62e, D, 0x62f, 0x632, R, 0x633, 0x63f, D, 0x640, 0x640, D,
|
|
35
|
+
0x641, 0x647, D, 0x648, 0x648, R, 0x649, 0x64a, D, 0x66e, 0x66f, D, 0x671, 0x673, R, 0x674, 0x674, U, 0x675, 0x677, R,
|
|
36
|
+
0x678, 0x687, D, 0x688, 0x699, R, 0x69a, 0x6bf, D, 0x6c0, 0x6c0, R, 0x6c1, 0x6c2, D, 0x6c3, 0x6cb, R, 0x6cc, 0x6cc, D,
|
|
37
|
+
0x6cd, 0x6cd, R, 0x6ce, 0x6ce, D, 0x6cf, 0x6cf, R, 0x6d0, 0x6d1, D, 0x6d2, 0x6d3, R, 0x6d5, 0x6d5, R, 0x6dd, 0x6dd, U,
|
|
38
|
+
0x6ee, 0x6ef, R, 0x6fa, 0x6fc, D, 0x6ff, 0x6ff, D, 0x70f, 0x70f, T, 0x710, 0x710, ALAPH, 0x712, 0x714, D,
|
|
39
|
+
0x715, 0x716, DALATH_RISH, 0x717, 0x719, R, 0x71a, 0x71d, D, 0x71e, 0x71e, R, 0x71f, 0x727, D, 0x728, 0x728, R,
|
|
40
|
+
0x729, 0x729, D, 0x72a, 0x72a, DALATH_RISH, 0x72b, 0x72b, D, 0x72c, 0x72c, R, 0x72d, 0x72e, D, 0x72f, 0x72f, DALATH_RISH,
|
|
41
|
+
0x74d, 0x74d, R, 0x74e, 0x758, D, 0x759, 0x75b, R, 0x75c, 0x76a, D, 0x76b, 0x76c, R, 0x76d, 0x770, D, 0x771, 0x771, R,
|
|
42
|
+
0x772, 0x772, D, 0x773, 0x774, R, 0x775, 0x777, D, 0x778, 0x779, R, 0x77a, 0x77f, D, 0x7ca, 0x7ea, D, 0x7fa, 0x7fa, D,
|
|
43
|
+
0x840, 0x840, R, 0x841, 0x845, D, 0x846, 0x847, R, 0x848, 0x848, D, 0x849, 0x849, R, 0x84a, 0x853, D, 0x854, 0x854, R,
|
|
44
|
+
0x855, 0x855, D, 0x856, 0x858, R, 0x860, 0x860, D, 0x861, 0x861, U, 0x862, 0x865, D, 0x866, 0x866, U, 0x867, 0x867, R,
|
|
45
|
+
0x868, 0x868, D, 0x869, 0x86a, R, 0x870, 0x882, R, 0x883, 0x885, D, 0x886, 0x886, D, 0x887, 0x888, U, 0x889, 0x88d, D,
|
|
46
|
+
0x88e, 0x88e, R, 0x88f, 0x88f, D, 0x890, 0x891, U, 0x8a0, 0x8a9, D, 0x8aa, 0x8ac, R, 0x8ad, 0x8ad, U, 0x8ae, 0x8ae, R,
|
|
47
|
+
0x8af, 0x8b0, D, 0x8b1, 0x8b2, R, 0x8b3, 0x8b8, D, 0x8b9, 0x8b9, R, 0x8ba, 0x8c8, D, 0x8e2, 0x8e2, U, 0x200c, 0x200c, U,
|
|
48
|
+
0x200d, 0x200d, D,
|
|
49
|
+
];
|
|
50
|
+
|
|
51
|
+
const transparent = /[\p{Mn}\p{Me}\p{Cf}]/u;
|
|
52
|
+
|
|
53
|
+
/** This function gives the joining type of a code point: a column of the state machine, or `T`. */
|
|
54
|
+
function joiningType(codePoint: number): number {
|
|
55
|
+
let low = 0;
|
|
56
|
+
let high = joiningTable.length / 3 - 1;
|
|
57
|
+
while (low <= high) {
|
|
58
|
+
const middle = (low + high) >> 1;
|
|
59
|
+
if (codePoint < joiningTable[middle * 3]!) high = middle - 1;
|
|
60
|
+
else if (codePoint > joiningTable[middle * 3 + 1]!) low = middle + 1;
|
|
61
|
+
else return joiningTable[middle * 3 + 2]!;
|
|
62
|
+
}
|
|
63
|
+
return transparent.test(String.fromCodePoint(codePoint)) ? T : U;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// The actions of the state machine: the index of a form in `joiningFeatures`, or NONE
|
|
67
|
+
const ISOL = 0;
|
|
68
|
+
const FINA = 1;
|
|
69
|
+
const FIN2 = 2;
|
|
70
|
+
const FIN3 = 3;
|
|
71
|
+
const MEDI = 4;
|
|
72
|
+
const MED2 = 5;
|
|
73
|
+
const INIT = 6;
|
|
74
|
+
const NONE = 7;
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* The state machine of `arabic_state_table` of HarfBuzz. Each row is a state, with one entry for each column. An entry
|
|
78
|
+
* has the action for the previous letter, the action for the current letter and the next state.
|
|
79
|
+
*/
|
|
80
|
+
const states: readonly (readonly (readonly [number, number, number])[])[] = [
|
|
81
|
+
// State 0: the previous letter was U, and it does not join
|
|
82
|
+
[[NONE, NONE, 0], [NONE, ISOL, 2], [NONE, ISOL, 1], [NONE, ISOL, 2], [NONE, ISOL, 1], [NONE, ISOL, 6]],
|
|
83
|
+
// State 1: the previous letter was R, or ISOL ALAPH, and it does not join
|
|
84
|
+
[[NONE, NONE, 0], [NONE, ISOL, 2], [NONE, ISOL, 1], [NONE, ISOL, 2], [NONE, FIN2, 5], [NONE, ISOL, 6]],
|
|
85
|
+
// State 2: the previous letter was D or L in the form ISOL, and it can join
|
|
86
|
+
[[NONE, NONE, 0], [NONE, ISOL, 2], [INIT, FINA, 1], [INIT, FINA, 3], [INIT, FINA, 4], [INIT, FINA, 6]],
|
|
87
|
+
// State 3: the previous letter was D in the form FINA, and it can join
|
|
88
|
+
[[NONE, NONE, 0], [NONE, ISOL, 2], [MEDI, FINA, 1], [MEDI, FINA, 3], [MEDI, FINA, 4], [MEDI, FINA, 6]],
|
|
89
|
+
// State 4: the previous letter was FINA ALAPH, and it does not join
|
|
90
|
+
[[NONE, NONE, 0], [NONE, ISOL, 2], [MED2, ISOL, 1], [MED2, ISOL, 2], [MED2, FIN2, 5], [MED2, ISOL, 6]],
|
|
91
|
+
// State 5: the previous letter was FIN2 or FIN3 ALAPH, and it does not join
|
|
92
|
+
[[NONE, NONE, 0], [NONE, ISOL, 2], [ISOL, ISOL, 1], [ISOL, ISOL, 2], [ISOL, FIN2, 5], [ISOL, ISOL, 6]],
|
|
93
|
+
// State 6: the previous letter was DALATH or RISH, and it does not join
|
|
94
|
+
[[NONE, NONE, 0], [NONE, ISOL, 2], [NONE, ISOL, 1], [NONE, ISOL, 2], [NONE, FIN3, 5], [NONE, ISOL, 6]],
|
|
95
|
+
];
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* This function gives the joining form of each code point of a text, in logical order. A transparent character, for
|
|
99
|
+
* example a mark, and a character that does not join get `undefined`. The text before and after the code points is not
|
|
100
|
+
* known, so the function sees it as a character of the type U.
|
|
101
|
+
*/
|
|
102
|
+
export function joiningForms(codePoints: readonly number[]): (JoiningForm | undefined)[] {
|
|
103
|
+
const actions: number[] = codePoints.map(() => NONE);
|
|
104
|
+
let previous = -1;
|
|
105
|
+
let state = 0;
|
|
106
|
+
codePoints.forEach((codePoint, i) => {
|
|
107
|
+
const type = joiningType(codePoint);
|
|
108
|
+
if (type === T) return;
|
|
109
|
+
const [previousAction, currentAction, next] = states[state]![type]!;
|
|
110
|
+
if (previousAction !== NONE && previous >= 0) actions[previous] = previousAction;
|
|
111
|
+
actions[i] = currentAction;
|
|
112
|
+
previous = i;
|
|
113
|
+
state = next;
|
|
114
|
+
});
|
|
115
|
+
return actions.map((action) => joiningFeatures[action]);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* One stage of the GSUB features of the Arabic shaper. The lookups of one stage go in the order of the lookup list.
|
|
120
|
+
* A stage with a `form` applies only to the glyphs of that joining form.
|
|
121
|
+
*/
|
|
122
|
+
export interface JoiningStage {
|
|
123
|
+
readonly features: readonly string[];
|
|
124
|
+
readonly form?: JoiningForm;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* This function gives the stages of the Arabic shaper of HarfBuzz for a list of features. The stages are `ccmp` and
|
|
129
|
+
* `locl`, then each joining form alone, then `rlig`, then `calt`, then the other features. When the font has `rclt`,
|
|
130
|
+
* `calt` and the other features are one stage, as in HarfBuzz. The list `features` gives the features apart from the
|
|
131
|
+
* joining forms, so a feature that is not in the list is not in a stage.
|
|
132
|
+
*/
|
|
133
|
+
export function joiningStages(features: readonly string[], hasRclt: boolean): JoiningStage[] {
|
|
134
|
+
const take = (tags: readonly string[]) => features.filter((tag) => tags.includes(tag));
|
|
135
|
+
const first = ["ccmp", "locl"];
|
|
136
|
+
const rest = features.filter((tag) => !first.includes(tag) && tag !== "rlig" && (hasRclt || tag !== "calt"));
|
|
137
|
+
const stages: JoiningStage[] = [{ features: take(first) }, ...joiningFeatures.map((form) => ({ features: [form], form })), { features: take(["rlig"]) }];
|
|
138
|
+
if (!hasRclt) stages.push({ features: take(["calt"]) });
|
|
139
|
+
stages.push({ features: rest });
|
|
140
|
+
return stages.filter((stage) => stage.features.length > 0);
|
|
141
|
+
}
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
import type { Reader } from "./binary.ts";
|
|
2
|
+
import type { GlyphFilter } from "./substitution.ts";
|
|
3
|
+
import { classOf, coverageIndex } from "./tables.ts";
|
|
4
|
+
import type { VariationStore } from "./variations.ts";
|
|
5
|
+
|
|
6
|
+
/** The change of the advances of two glyphs, in font units. A variable font can give a value that is not a whole number. */
|
|
7
|
+
export interface PairValue {
|
|
8
|
+
readonly first: number;
|
|
9
|
+
readonly second: number;
|
|
10
|
+
/** The part of `first` that comes from a VariationIndex table. HarfBuzz scales this part and the rest in different ways. */
|
|
11
|
+
readonly firstDelta?: number;
|
|
12
|
+
/** The part of `second` that comes from a VariationIndex table. */
|
|
13
|
+
readonly secondDelta?: number;
|
|
14
|
+
/** It is true when the subtable has a value for the second glyph. Then the next pair starts after the second glyph. */
|
|
15
|
+
readonly skipSecond: boolean;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** A lookup of pair adjustments, with the flags that tell which glyphs it skips. */
|
|
19
|
+
export interface PairLookup {
|
|
20
|
+
readonly flags: number;
|
|
21
|
+
/**
|
|
22
|
+
* This function tells if the lookup skips a glyph, when the lookup uses a mark filtering set. Then the lookup skips each
|
|
23
|
+
* mark that is not in the set. Without the function, the flags alone tell which glyphs the lookup skips.
|
|
24
|
+
*/
|
|
25
|
+
readonly skip?: (glyph: number) => boolean;
|
|
26
|
+
/** This function gives the value for a pair, or `undefined` when no subtable applies. */
|
|
27
|
+
pair(first: number, second: number): PairValue | undefined;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** The lookup flags of OpenType. */
|
|
31
|
+
export const IGNORE_BASE = 0x2;
|
|
32
|
+
export const IGNORE_LIGATURES = 0x4;
|
|
33
|
+
export const IGNORE_MARKS = 0x8;
|
|
34
|
+
|
|
35
|
+
/** This function gives the size in bytes of a ValueRecord of the format `format`. */
|
|
36
|
+
function valueSize(format: number): number {
|
|
37
|
+
let bits = format & 0xff;
|
|
38
|
+
let size = 0;
|
|
39
|
+
while (bits !== 0) {
|
|
40
|
+
size += bits & 1;
|
|
41
|
+
bits >>= 1;
|
|
42
|
+
}
|
|
43
|
+
return size * 2;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* This function gives the XAdvance of a ValueRecord at `offset`, or 0 when the format has none. The offset of a device table
|
|
48
|
+
* is relative to `parent`. A VariationIndex table adds its delta from `store`, not rounded, as HarfBuzz adds it. A device
|
|
49
|
+
* table of hinting deltas adds nothing, because the shaper of the browser does not set a ppem.
|
|
50
|
+
*/
|
|
51
|
+
function xAdvance(table: Reader, offset: number, format: number, parent: Reader, store: VariationStore | undefined): { value: number; delta: number } {
|
|
52
|
+
const value = (format & 0x4) === 0 ? 0 : table.i16(offset + valueSize(format & 0x3));
|
|
53
|
+
let delta = 0;
|
|
54
|
+
if ((format & 0x40) !== 0 && store !== undefined) {
|
|
55
|
+
const device = table.u16(offset + valueSize(format & 0x3f));
|
|
56
|
+
if (device !== 0 && parent.u16(device + 4) === 0x8000) delta = store.delta(parent.u16(device), parent.u16(device + 2));
|
|
57
|
+
}
|
|
58
|
+
return { value: value + delta, delta };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** This function makes the value of a pair from the XAdvance of its two glyphs. */
|
|
62
|
+
function pairValue(first: { value: number; delta: number }, second: { value: number; delta: number }, skipSecond: boolean): PairValue {
|
|
63
|
+
return { first: first.value, second: second.value, firstDelta: first.delta, secondDelta: second.delta, skipSecond };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** This function makes the pair function of one PairPos subtable. */
|
|
67
|
+
function pairSubtable(table: Reader, store: VariationStore | undefined): (first: number, second: number) => PairValue | undefined {
|
|
68
|
+
const format = table.u16(0);
|
|
69
|
+
const coverage = table.part(table.u16(2));
|
|
70
|
+
const format1 = table.u16(4);
|
|
71
|
+
const format2 = table.u16(6);
|
|
72
|
+
const size1 = valueSize(format1);
|
|
73
|
+
const size2 = valueSize(format2);
|
|
74
|
+
const skipSecond = format2 !== 0;
|
|
75
|
+
if (format === 1) {
|
|
76
|
+
return (first, second) => {
|
|
77
|
+
const index = coverageIndex(coverage, first);
|
|
78
|
+
if (index < 0) return undefined;
|
|
79
|
+
const set = table.part(table.u16(10 + index * 2));
|
|
80
|
+
const recordSize = 2 + size1 + size2;
|
|
81
|
+
let low = 0;
|
|
82
|
+
let high = set.u16(0) - 1;
|
|
83
|
+
while (low <= high) {
|
|
84
|
+
const middle = (low + high) >> 1;
|
|
85
|
+
const at = 2 + middle * recordSize;
|
|
86
|
+
const glyph = set.u16(at);
|
|
87
|
+
if (second < glyph) high = middle - 1;
|
|
88
|
+
else if (second > glyph) low = middle + 1;
|
|
89
|
+
else return pairValue(xAdvance(set, at + 2, format1, set, store), xAdvance(set, at + 2 + size1, format2, set, store), skipSecond);
|
|
90
|
+
}
|
|
91
|
+
return undefined;
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
if (format === 2) {
|
|
95
|
+
const classes1 = table.part(table.u16(8));
|
|
96
|
+
const classes2 = table.part(table.u16(10));
|
|
97
|
+
const count1 = table.u16(12);
|
|
98
|
+
const count2 = table.u16(14);
|
|
99
|
+
return (first, second) => {
|
|
100
|
+
if (coverageIndex(coverage, first) < 0) return undefined;
|
|
101
|
+
const class1 = classOf(classes1, first);
|
|
102
|
+
const class2 = classOf(classes2, second);
|
|
103
|
+
if (class1 >= count1 || class2 >= count2) return undefined;
|
|
104
|
+
const at = 16 + (class1 * count2 + class2) * (size1 + size2);
|
|
105
|
+
return pairValue(xAdvance(table, at, format1, table, store), xAdvance(table, at + size1, format2, table, store), skipSecond);
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
throw new Error(`the PairPos format ${format} is unknown`);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** This function gives the tags of the scripts to try, in order, for a script tag, as HarfBuzz tries them. */
|
|
112
|
+
export function scriptTags(script: string): string[] {
|
|
113
|
+
const tags = script === "deva" ? ["dev2", "deva"] : [script];
|
|
114
|
+
return [...tags, "DFLT", "dflt", "latn"];
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** The GPOS features with pair lookups that HarfBuzz applies to horizontal text. */
|
|
118
|
+
const pairFeatures = new Set(["kern", "dist"]);
|
|
119
|
+
|
|
120
|
+
/** The lookup flag that selects a mark filtering set of GDEF. */
|
|
121
|
+
const USE_MARK_FILTERING_SET = 0x10;
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* This function gives the pair lookups of the features `kern` and `dist` of GPOS for a script, in the order of the lookup
|
|
125
|
+
* list. The store gives the deltas of a variable font at its instance. The filter gives the mark filtering sets of GDEF.
|
|
126
|
+
*/
|
|
127
|
+
export function readGposKerning(gpos: Reader, script: string, store?: VariationStore, filter?: GlyphFilter): PairLookup[] {
|
|
128
|
+
const scripts = gpos.part(gpos.u16(4));
|
|
129
|
+
const features = gpos.part(gpos.u16(6));
|
|
130
|
+
const lookups = gpos.part(gpos.u16(8));
|
|
131
|
+
const scriptCount = scripts.u16(0);
|
|
132
|
+
let langSys: Reader | undefined;
|
|
133
|
+
for (const wanted of scriptTags(script)) {
|
|
134
|
+
for (let i = 0; i < scriptCount && langSys === undefined; i++) {
|
|
135
|
+
if (scripts.tag(2 + i * 6) !== wanted) continue;
|
|
136
|
+
const table = scripts.part(scripts.u16(2 + i * 6 + 4));
|
|
137
|
+
const fallback = table.u16(0);
|
|
138
|
+
if (fallback !== 0) langSys = table.part(fallback);
|
|
139
|
+
}
|
|
140
|
+
if (langSys !== undefined) break;
|
|
141
|
+
}
|
|
142
|
+
if (langSys === undefined) return [];
|
|
143
|
+
const indices = new Set<number>();
|
|
144
|
+
const featureCount = langSys.u16(4);
|
|
145
|
+
for (let i = 0; i < featureCount; i++) {
|
|
146
|
+
const record = 2 + langSys.u16(6 + i * 2) * 6;
|
|
147
|
+
if (!pairFeatures.has(features.tag(record))) continue;
|
|
148
|
+
const feature = features.part(features.u16(record + 4));
|
|
149
|
+
for (let k = 0; k < feature.u16(2); k++) indices.add(feature.u16(4 + k * 2));
|
|
150
|
+
}
|
|
151
|
+
return [...indices].sort((a, b) => a - b).flatMap((index) => {
|
|
152
|
+
const lookup = lookups.part(lookups.u16(2 + index * 2));
|
|
153
|
+
const subtables: ((first: number, second: number) => PairValue | undefined)[] = [];
|
|
154
|
+
const type = lookup.u16(0);
|
|
155
|
+
for (let s = 0; s < lookup.u16(4); s++) {
|
|
156
|
+
let subtable = lookup.part(lookup.u16(6 + s * 2));
|
|
157
|
+
let subtype = type;
|
|
158
|
+
if (type === 9) {
|
|
159
|
+
subtype = subtable.u16(2);
|
|
160
|
+
subtable = subtable.part(subtable.u32(4));
|
|
161
|
+
}
|
|
162
|
+
if (subtype === 2) subtables.push(pairSubtable(subtable, store));
|
|
163
|
+
}
|
|
164
|
+
if (subtables.length === 0) return [];
|
|
165
|
+
const flags = lookup.u16(2);
|
|
166
|
+
const markSet = (flags & USE_MARK_FILTERING_SET) !== 0 && filter !== undefined ? lookup.u16(6 + lookup.u16(4) * 2) : -1;
|
|
167
|
+
const skip = markSet < 0 ? undefined : (glyph: number): boolean => {
|
|
168
|
+
const kind = filter!.glyphClass(glyph);
|
|
169
|
+
if ((flags & IGNORE_BASE) !== 0 && kind === 1) return true;
|
|
170
|
+
if ((flags & IGNORE_LIGATURES) !== 0 && kind === 2) return true;
|
|
171
|
+
if (kind !== 3) return false;
|
|
172
|
+
return (flags & IGNORE_MARKS) !== 0 || !filter!.inMarkSet(markSet, glyph);
|
|
173
|
+
};
|
|
174
|
+
return [{
|
|
175
|
+
flags,
|
|
176
|
+
...(skip === undefined ? {} : { skip }),
|
|
177
|
+
pair(first: number, second: number): PairValue | undefined {
|
|
178
|
+
for (const subtable of subtables) {
|
|
179
|
+
const value = subtable(first, second);
|
|
180
|
+
if (value !== undefined) return value;
|
|
181
|
+
}
|
|
182
|
+
return undefined;
|
|
183
|
+
},
|
|
184
|
+
}];
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/** This function gives the pairs of format 0 of the `kern` table, as one lookup. */
|
|
189
|
+
export function readKernTable(kern: Reader): PairLookup[] {
|
|
190
|
+
// Only the version 0 of Microsoft. The version 1 of Apple starts with the 32-bit number 0x00010000
|
|
191
|
+
if (kern.u16(0) !== 0) return [];
|
|
192
|
+
const pairs = new Map<number, number>();
|
|
193
|
+
const count = kern.u16(2);
|
|
194
|
+
let offset = 4;
|
|
195
|
+
for (let t = 0; t < count; t++) {
|
|
196
|
+
const length = kern.u16(offset + 2);
|
|
197
|
+
const coverage = kern.u16(offset + 4);
|
|
198
|
+
// Only the horizontal kerning of format 0, with no minimum values and no cross-stream values
|
|
199
|
+
if ((coverage & 0xff07) === 0x0001) {
|
|
200
|
+
const n = kern.u16(offset + 6);
|
|
201
|
+
for (let i = 0; i < n; i++) {
|
|
202
|
+
const at = offset + 14 + i * 6;
|
|
203
|
+
const key = kern.u16(at) * 0x10000 + kern.u16(at + 2);
|
|
204
|
+
pairs.set(key, (pairs.get(key) ?? 0) + kern.i16(at + 4));
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
offset += length;
|
|
208
|
+
}
|
|
209
|
+
if (pairs.size === 0) return [];
|
|
210
|
+
return [{
|
|
211
|
+
flags: 0,
|
|
212
|
+
pair(first: number, second: number): PairValue | undefined {
|
|
213
|
+
const value = pairs.get(first * 0x10000 + second);
|
|
214
|
+
return value === undefined ? undefined : { first: value, second: 0, skipSecond: false };
|
|
215
|
+
},
|
|
216
|
+
}];
|
|
217
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { Reader } from "./binary.ts";
|
|
2
|
+
|
|
3
|
+
/** A function that decompresses Brotli data. Node gives one as `brotliDecompressSync` of `node:zlib`. */
|
|
4
|
+
export type Decompress = (data: Uint8Array) => Uint8Array;
|
|
5
|
+
|
|
6
|
+
/** The tables of a font, by tag. A table that the decoder does not need can be absent. */
|
|
7
|
+
export type TableMap = ReadonlyMap<string, Reader>;
|
|
8
|
+
|
|
9
|
+
/** The tags of the WOFF2 table directory, in the order of the WOFF2 specification. */
|
|
10
|
+
const knownTags = [
|
|
11
|
+
"cmap", "head", "hhea", "hmtx", "maxp", "name", "OS/2", "post", "cvt ", "fpgm", "glyf", "loca", "prep", "CFF ", "VORG", "EBDT",
|
|
12
|
+
"EBLC", "gasp", "hdmx", "kern", "LTSH", "PCLT", "VDMX", "vhea", "vmtx", "BASE", "GDEF", "GPOS", "GSUB", "EBSC", "JSTF", "MATH",
|
|
13
|
+
"CBDT", "CBLC", "COLR", "CPAL", "SVG ", "sbix", "acnt", "avar", "bdat", "bloc", "bsln", "cvar", "fdsc", "feat", "fmtx", "fvar",
|
|
14
|
+
"gvar", "hsty", "just", "lcar", "mort", "morx", "opbd", "prop", "trak", "Zapf", "Silf", "Glat", "Gloc", "Feat", "Sill",
|
|
15
|
+
] as const;
|
|
16
|
+
|
|
17
|
+
/** This function gives the tables of a TrueType, OpenType or WOFF2 file. */
|
|
18
|
+
export function readTables(bytes: Uint8Array, decompress?: Decompress): TableMap {
|
|
19
|
+
const file = new Reader(bytes);
|
|
20
|
+
if (file.length < 12) throw new RangeError("the file is too short for a font");
|
|
21
|
+
const signature = file.tag(0);
|
|
22
|
+
if (signature === "wOF2") {
|
|
23
|
+
if (decompress === undefined) throw new Error("a WOFF2 file needs a Brotli decompress function");
|
|
24
|
+
return readWoff2(file, decompress);
|
|
25
|
+
}
|
|
26
|
+
if (signature === "wOFF") throw new Error("WOFF 1 files are not supported");
|
|
27
|
+
if (signature === "ttcf") throw new Error("font collections are not supported");
|
|
28
|
+
if (file.u32(0) !== 0x00010000 && signature !== "OTTO" && signature !== "true") throw new Error(`the signature ${JSON.stringify(signature)} is not a font`);
|
|
29
|
+
const tables = new Map<string, Reader>();
|
|
30
|
+
const count = file.u16(4);
|
|
31
|
+
for (let i = 0; i < count; i++) {
|
|
32
|
+
const record = 12 + i * 16;
|
|
33
|
+
tables.set(file.tag(record), file.part(file.u32(record + 8), file.u32(record + 12)));
|
|
34
|
+
}
|
|
35
|
+
return tables;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** This function reads a UIntBase128 number of WOFF2 and gives the number and the next offset. */
|
|
39
|
+
function base128(file: Reader, offset: number): [value: number, next: number] {
|
|
40
|
+
let value = 0;
|
|
41
|
+
for (let i = 0; i < 5; i++) {
|
|
42
|
+
const byte = file.u8(offset + i);
|
|
43
|
+
if (i === 0 && byte === 0x80) throw new RangeError("a UIntBase128 has a leading zero");
|
|
44
|
+
value = value * 128 + (byte & 0x7f);
|
|
45
|
+
if ((byte & 0x80) === 0) return [value, offset + i + 1];
|
|
46
|
+
}
|
|
47
|
+
throw new RangeError("a UIntBase128 is longer than 5 bytes");
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* This function gives the tables of a WOFF2 file. The `glyf` and `loca` tables stay in their transformed form,
|
|
52
|
+
* because the width does not use them. A transformed `hmtx` table becomes a plain table again.
|
|
53
|
+
*/
|
|
54
|
+
function readWoff2(file: Reader, decompress: Decompress): TableMap {
|
|
55
|
+
const flavor = file.u32(4);
|
|
56
|
+
if (flavor === 0x74746366) throw new Error("WOFF2 font collections are not supported");
|
|
57
|
+
const count = file.u16(12);
|
|
58
|
+
const compressedLength = file.u32(20);
|
|
59
|
+
let offset = 48;
|
|
60
|
+
const entries: { tag: string; length: number; transformed: boolean }[] = [];
|
|
61
|
+
for (let i = 0; i < count; i++) {
|
|
62
|
+
const flags = file.u8(offset++);
|
|
63
|
+
let tag: string;
|
|
64
|
+
if ((flags & 0x3f) === 0x3f) {
|
|
65
|
+
tag = file.tag(offset);
|
|
66
|
+
offset += 4;
|
|
67
|
+
} else {
|
|
68
|
+
const known = knownTags[flags & 0x3f];
|
|
69
|
+
if (known === undefined) throw new RangeError(`the WOFF2 tag index ${flags & 0x3f} is unknown`);
|
|
70
|
+
tag = known;
|
|
71
|
+
}
|
|
72
|
+
const version = (flags >> 6) & 3;
|
|
73
|
+
let length: number;
|
|
74
|
+
[length, offset] = base128(file, offset);
|
|
75
|
+
// For glyf and loca, the version 0 is the transform. For the other tables, each version other than 0 is a transform
|
|
76
|
+
const transformed = tag === "glyf" || tag === "loca" ? version === 0 : version !== 0;
|
|
77
|
+
if (transformed) [length, offset] = base128(file, offset);
|
|
78
|
+
entries.push({ tag, length, transformed });
|
|
79
|
+
}
|
|
80
|
+
const data = new Reader(decompress(file.bytes.subarray(offset, offset + compressedLength)));
|
|
81
|
+
const tables = new Map<string, Reader>();
|
|
82
|
+
let at = 0;
|
|
83
|
+
for (const entry of entries) {
|
|
84
|
+
tables.set(entry.tag, data.part(at, entry.length));
|
|
85
|
+
at += entry.length;
|
|
86
|
+
}
|
|
87
|
+
const hmtx = entries.find((entry) => entry.tag === "hmtx");
|
|
88
|
+
if (hmtx?.transformed === true) tables.set("hmtx", untransformHmtx(tables));
|
|
89
|
+
return tables;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** This function gives a plain `hmtx` table from the transform of WOFF2. Only the advances matter, so each side bearing is 0. */
|
|
93
|
+
function untransformHmtx(tables: Map<string, Reader>): Reader {
|
|
94
|
+
const source = tables.get("hmtx")!;
|
|
95
|
+
const hhea = tables.get("hhea");
|
|
96
|
+
if (hhea === undefined) throw new Error("a transformed hmtx needs hhea");
|
|
97
|
+
const metrics = hhea.u16(34);
|
|
98
|
+
const plain = new Uint8Array(metrics * 4);
|
|
99
|
+
const view = new DataView(plain.buffer);
|
|
100
|
+
for (let i = 0; i < metrics; i++) view.setUint16(i * 4, source.u16(1 + i * 2));
|
|
101
|
+
return new Reader(plain);
|
|
102
|
+
}
|