@cantoo/fontkit 2.0.4
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/LICENSE +21 -0
- package/README.md +274 -0
- package/dist/browser-module.mjs +13660 -0
- package/dist/browser-module.mjs.map +1 -0
- package/dist/browser.cjs +13689 -0
- package/dist/browser.cjs.map +1 -0
- package/dist/main.cjs +13712 -0
- package/dist/main.cjs.map +1 -0
- package/dist/module.mjs +13673 -0
- package/dist/module.mjs.map +1 -0
- package/package.json +99 -0
- package/src/CmapProcessor.js +285 -0
- package/src/DFont.js +113 -0
- package/src/TTFFont.js +621 -0
- package/src/TrueTypeCollection.js +65 -0
- package/src/WOFF2Font.js +222 -0
- package/src/WOFFFont.js +56 -0
- package/src/aat/AATFeatureMap.js +545 -0
- package/src/aat/AATLayoutEngine.js +48 -0
- package/src/aat/AATLookupTable.js +125 -0
- package/src/aat/AATMorxProcessor.js +429 -0
- package/src/aat/AATStateMachine.js +96 -0
- package/src/base.js +29 -0
- package/src/cff/CFFCharsets.js +99 -0
- package/src/cff/CFFDict.js +166 -0
- package/src/cff/CFFEncodings.js +48 -0
- package/src/cff/CFFFont.js +158 -0
- package/src/cff/CFFIndex.js +150 -0
- package/src/cff/CFFOperand.js +135 -0
- package/src/cff/CFFPointer.js +50 -0
- package/src/cff/CFFPrivateDict.js +39 -0
- package/src/cff/CFFStandardStrings.js +71 -0
- package/src/cff/CFFTop.js +236 -0
- package/src/decorators.js +36 -0
- package/src/encodings.js +219 -0
- package/src/fs.js +12 -0
- package/src/glyph/BBox.js +72 -0
- package/src/glyph/CBDTGlyph.js +9 -0
- package/src/glyph/CFFGlyph.js +635 -0
- package/src/glyph/COLRGlyph.js +108 -0
- package/src/glyph/Glyph.js +270 -0
- package/src/glyph/GlyphVariationProcessor.js +487 -0
- package/src/glyph/Path.js +257 -0
- package/src/glyph/SBIXGlyph.js +54 -0
- package/src/glyph/StandardNames.js +27 -0
- package/src/glyph/TTFGlyph.js +408 -0
- package/src/glyph/TTFGlyphEncoder.js +158 -0
- package/src/glyph/WOFF2Glyph.js +70 -0
- package/src/index.js +15 -0
- package/src/layout/GlyphPosition.js +30 -0
- package/src/layout/GlyphRun.js +108 -0
- package/src/layout/KernProcessor.js +97 -0
- package/src/layout/LayoutEngine.js +190 -0
- package/src/layout/Script.js +231 -0
- package/src/layout/UnicodeLayoutEngine.js +250 -0
- package/src/node.js +16 -0
- package/src/opentype/GPOSProcessor.js +311 -0
- package/src/opentype/GSUBProcessor.js +208 -0
- package/src/opentype/GlyphInfo.js +57 -0
- package/src/opentype/GlyphIterator.js +82 -0
- package/src/opentype/OTLayoutEngine.js +117 -0
- package/src/opentype/OTProcessor.js +472 -0
- package/src/opentype/ShapingPlan.js +121 -0
- package/src/opentype/shapers/ArabicShaper.js +124 -0
- package/src/opentype/shapers/DefaultShaper.js +116 -0
- package/src/opentype/shapers/HangulShaper.js +285 -0
- package/src/opentype/shapers/HebrewShaper.js +105 -0
- package/src/opentype/shapers/IndicShaper.js +914 -0
- package/src/opentype/shapers/ThaiShaper.js +238 -0
- package/src/opentype/shapers/UniversalShaper.js +186 -0
- package/src/opentype/shapers/data.trie +0 -0
- package/src/opentype/shapers/gen-indic.js +208 -0
- package/src/opentype/shapers/gen-use.js +287 -0
- package/src/opentype/shapers/generate-data.js +33 -0
- package/src/opentype/shapers/index.js +109 -0
- package/src/opentype/shapers/indic-data.js +175 -0
- package/src/opentype/shapers/indic.json +1 -0
- package/src/opentype/shapers/indic.machine +31 -0
- package/src/opentype/shapers/indic.trie +0 -0
- package/src/opentype/shapers/use.json +1 -0
- package/src/opentype/shapers/use.machine +44 -0
- package/src/opentype/shapers/use.trie +0 -0
- package/src/packages/unicode-properties/LICENSE +8 -0
- package/src/packages/unicode-properties/README.md +12 -0
- package/src/packages/unicode-properties/data-trie.js +1 -0
- package/src/packages/unicode-properties/data.js +1 -0
- package/src/packages/unicode-properties/data.json +1 -0
- package/src/packages/unicode-properties/data.trie +0 -0
- package/src/packages/unicode-properties/dist/index.cjs +257 -0
- package/src/packages/unicode-properties/dist/index.cjs.map +7 -0
- package/src/packages/unicode-properties/dist/index.mjs +234 -0
- package/src/packages/unicode-properties/dist/index.mjs.map +7 -0
- package/src/packages/unicode-properties/generate.js +107 -0
- package/src/packages/unicode-properties/index.js +184 -0
- package/src/packages/unicode-properties/package.json +50 -0
- package/src/packages/unicode-trie/LICENSE +8 -0
- package/src/packages/unicode-trie/README.md +13 -0
- package/src/packages/unicode-trie/builder.js +965 -0
- package/src/packages/unicode-trie/dist/builder.cjs +711 -0
- package/src/packages/unicode-trie/dist/builder.cjs.map +7 -0
- package/src/packages/unicode-trie/dist/builder.mjs +690 -0
- package/src/packages/unicode-trie/dist/builder.mjs.map +7 -0
- package/src/packages/unicode-trie/dist/index.cjs +112 -0
- package/src/packages/unicode-trie/dist/index.cjs.map +7 -0
- package/src/packages/unicode-trie/dist/index.mjs +91 -0
- package/src/packages/unicode-trie/dist/index.mjs.map +7 -0
- package/src/packages/unicode-trie/index.js +135 -0
- package/src/packages/unicode-trie/package.json +49 -0
- package/src/packages/unicode-trie/swap.js +21 -0
- package/src/subset/CFFSubset.js +176 -0
- package/src/subset/Subset.js +63 -0
- package/src/subset/TTFSubset.js +130 -0
- package/src/tables/BASE.js +78 -0
- package/src/tables/COLR.js +25 -0
- package/src/tables/CPAL.js +24 -0
- package/src/tables/DSIG.js +21 -0
- package/src/tables/EBDT.js +91 -0
- package/src/tables/EBLC.js +80 -0
- package/src/tables/GDEF.js +57 -0
- package/src/tables/GPOS.js +209 -0
- package/src/tables/GSUB.js +85 -0
- package/src/tables/HVAR.js +44 -0
- package/src/tables/JSTF.js +43 -0
- package/src/tables/LTSH.js +10 -0
- package/src/tables/OS2.js +84 -0
- package/src/tables/PCLT.js +21 -0
- package/src/tables/STAT.js +99 -0
- package/src/tables/VDMX.js +33 -0
- package/src/tables/VORG.js +14 -0
- package/src/tables/WOFF2Directory.js +73 -0
- package/src/tables/WOFFDirectory.js +37 -0
- package/src/tables/aat.js +157 -0
- package/src/tables/avar.js +19 -0
- package/src/tables/bsln.js +31 -0
- package/src/tables/cmap.js +127 -0
- package/src/tables/cvt.js +6 -0
- package/src/tables/directory.js +58 -0
- package/src/tables/feat.js +28 -0
- package/src/tables/fpgm.js +8 -0
- package/src/tables/fvar.js +31 -0
- package/src/tables/gasp.js +15 -0
- package/src/tables/glyf.js +4 -0
- package/src/tables/gvar.js +27 -0
- package/src/tables/hdmx.js +15 -0
- package/src/tables/head.js +25 -0
- package/src/tables/hhea.js +19 -0
- package/src/tables/hmtx.js +11 -0
- package/src/tables/index.js +119 -0
- package/src/tables/just.js +81 -0
- package/src/tables/kern.js +91 -0
- package/src/tables/loca.js +30 -0
- package/src/tables/maxp.js +20 -0
- package/src/tables/morx.js +79 -0
- package/src/tables/name.js +157 -0
- package/src/tables/opbd.js +15 -0
- package/src/tables/opentype.js +217 -0
- package/src/tables/post.js +34 -0
- package/src/tables/prep.js +6 -0
- package/src/tables/sbix.js +17 -0
- package/src/tables/variations.js +80 -0
- package/src/tables/vhea.js +19 -0
- package/src/tables/vmtx.js +12 -0
- package/src/utils.js +62 -0
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import {isDigit, isMark} from '../../packages/unicode-properties/index.js';
|
|
2
|
+
import GlyphInfo from '../GlyphInfo';
|
|
3
|
+
|
|
4
|
+
const VARIATION_FEATURES = ['rvrn'];
|
|
5
|
+
const COMMON_FEATURES = ['ccmp', 'locl', 'rlig', 'mark', 'mkmk'];
|
|
6
|
+
const FRACTIONAL_FEATURES = ['frac', 'numr', 'dnom'];
|
|
7
|
+
const HORIZONTAL_FEATURES = ['calt', 'clig', 'liga', 'rclt', 'curs', 'kern'];
|
|
8
|
+
const _VERTICAL_FEATURES = ['vert'];
|
|
9
|
+
const DIRECTIONAL_FEATURES = {
|
|
10
|
+
ltr: ['ltra', 'ltrm'],
|
|
11
|
+
rtl: ['rtla', 'rtlm']
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export default class DefaultShaper {
|
|
15
|
+
static zeroMarkWidths = 'AFTER_GPOS';
|
|
16
|
+
static plan(plan, glyphs, features) {
|
|
17
|
+
// Plan the features we want to apply
|
|
18
|
+
this.planPreprocessing(plan);
|
|
19
|
+
this.planFeatures(plan);
|
|
20
|
+
this.planPostprocessing(plan, features);
|
|
21
|
+
|
|
22
|
+
// Assign the global features to all the glyphs
|
|
23
|
+
plan.assignGlobalFeatures(glyphs);
|
|
24
|
+
|
|
25
|
+
// Assign local features to glyphs
|
|
26
|
+
this.assignFeatures(plan, glyphs);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
static planPreprocessing(plan) {
|
|
30
|
+
plan.add({
|
|
31
|
+
global: [...VARIATION_FEATURES, ...DIRECTIONAL_FEATURES[plan.direction]],
|
|
32
|
+
local: FRACTIONAL_FEATURES
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
static planFeatures(_plan) {
|
|
37
|
+
// Do nothing by default. Let subclasses override this.
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
static planPostprocessing(plan, userFeatures) {
|
|
41
|
+
plan.add([...COMMON_FEATURES, ...HORIZONTAL_FEATURES]);
|
|
42
|
+
plan.setFeatureOverrides(userFeatures);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
static assignFeatures(plan, glyphs) {
|
|
46
|
+
// Font-aware NFC before GSUB (HarfBuzz default-shaper behavior).
|
|
47
|
+
composeGlyphs(plan.font, glyphs);
|
|
48
|
+
|
|
49
|
+
// Enable contextual fractions
|
|
50
|
+
for (let i = 0; i < glyphs.length; i++) {
|
|
51
|
+
let glyph = glyphs[i];
|
|
52
|
+
if (glyph.codePoints[0] === 0x2044) { // fraction slash
|
|
53
|
+
let start = i;
|
|
54
|
+
let end = i + 1;
|
|
55
|
+
|
|
56
|
+
// Apply numerator
|
|
57
|
+
while (start > 0 && isDigit(glyphs[start - 1].codePoints[0])) {
|
|
58
|
+
glyphs[start - 1].features.numr = true;
|
|
59
|
+
glyphs[start - 1].features.frac = true;
|
|
60
|
+
start--;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Apply denominator
|
|
64
|
+
while (end < glyphs.length && isDigit(glyphs[end].codePoints[0])) {
|
|
65
|
+
glyphs[end].features.dnom = true;
|
|
66
|
+
glyphs[end].features.frac = true;
|
|
67
|
+
end++;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Apply fraction slash
|
|
71
|
+
glyph.features.frac = true;
|
|
72
|
+
i = end - 1;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Compose base+mark clusters when the font has the precomposed glyph.
|
|
79
|
+
// Skip pure mark reorders (same length) so Arabic calt etc. keep expected order.
|
|
80
|
+
function composeGlyphs(font, glyphs) {
|
|
81
|
+
let singleMark = g => g.codePoints.length === 1 && isMark(g.codePoints[0]);
|
|
82
|
+
|
|
83
|
+
for (let i = 0; i < glyphs.length; ) {
|
|
84
|
+
let base = glyphs[i];
|
|
85
|
+
if (base.codePoints.length !== 1 || isMark(base.codePoints[0])) {
|
|
86
|
+
i++;
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
let end = i + 1;
|
|
91
|
+
while (end < glyphs.length && singleMark(glyphs[end])) end++;
|
|
92
|
+
if (end === i + 1) {
|
|
93
|
+
i++;
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
let input = glyphs.slice(i, end).map(g => g.codePoints[0]);
|
|
98
|
+
let composed = Array.from(String.fromCodePoint(...input).normalize('NFC')).flatMap(char => {
|
|
99
|
+
let cp = char.codePointAt(0);
|
|
100
|
+
return font.hasGlyphForCodePoint(cp)
|
|
101
|
+
? [cp]
|
|
102
|
+
: Array.from(char.normalize('NFD'), c => c.codePointAt(0));
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
if (composed.length === input.length) {
|
|
106
|
+
i = end;
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
let replacement = composed.map(
|
|
111
|
+
cp => new GlyphInfo(font, font.glyphForCodePoint(cp).id, [cp], base.features)
|
|
112
|
+
);
|
|
113
|
+
glyphs.splice(i, end - i, ...replacement);
|
|
114
|
+
i += replacement.length;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
import DefaultShaper from './DefaultShaper';
|
|
2
|
+
import GlyphInfo from '../GlyphInfo';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* This is a shaper for the Hangul script, used by the Korean language.
|
|
6
|
+
* It does the following:
|
|
7
|
+
* - decompose if unsupported by the font:
|
|
8
|
+
* <LV> -> <L,V>
|
|
9
|
+
* <LVT> -> <L,V,T>
|
|
10
|
+
* <LV,T> -> <L,V,T>
|
|
11
|
+
*
|
|
12
|
+
* - compose if supported by the font:
|
|
13
|
+
* <L,V> -> <LV>
|
|
14
|
+
* <L,V,T> -> <LVT>
|
|
15
|
+
* <LV,T> -> <LVT>
|
|
16
|
+
*
|
|
17
|
+
* - reorder tone marks (S is any valid syllable):
|
|
18
|
+
* <S, M> -> <M, S>
|
|
19
|
+
*
|
|
20
|
+
* - apply ljmo, vjmo, and tjmo OpenType features to decomposed Jamo sequences.
|
|
21
|
+
*
|
|
22
|
+
* This logic is based on the following documents:
|
|
23
|
+
* - http://www.microsoft.com/typography/OpenTypeDev/hangul/intro.htm
|
|
24
|
+
* - http://ktug.org/~nomos/harfbuzz-hangul/hangulshaper.pdf
|
|
25
|
+
*/
|
|
26
|
+
export default class HangulShaper extends DefaultShaper {
|
|
27
|
+
static zeroMarkWidths = 'NONE';
|
|
28
|
+
static planFeatures(plan) {
|
|
29
|
+
plan.add(['ljmo', 'vjmo', 'tjmo'], false);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
static assignFeatures(plan, glyphs) {
|
|
33
|
+
let state = 0;
|
|
34
|
+
let i = 0;
|
|
35
|
+
while (i < glyphs.length) {
|
|
36
|
+
let action;
|
|
37
|
+
let glyph = glyphs[i];
|
|
38
|
+
let code = glyph.codePoints[0];
|
|
39
|
+
let type = getType(code);
|
|
40
|
+
|
|
41
|
+
[ action, state ] = STATE_TABLE[state][type];
|
|
42
|
+
|
|
43
|
+
switch (action) {
|
|
44
|
+
case DECOMPOSE:
|
|
45
|
+
// Decompose the composed syllable if it is not supported by the font.
|
|
46
|
+
if (!plan.font.hasGlyphForCodePoint(code)) {
|
|
47
|
+
i = decompose(glyphs, i, plan.font);
|
|
48
|
+
}
|
|
49
|
+
break;
|
|
50
|
+
|
|
51
|
+
case COMPOSE:
|
|
52
|
+
// Found a decomposed syllable. Try to compose if supported by the font.
|
|
53
|
+
i = compose(glyphs, i, plan.font);
|
|
54
|
+
break;
|
|
55
|
+
|
|
56
|
+
case TONE_MARK:
|
|
57
|
+
// Got a valid syllable, followed by a tone mark. Move the tone mark to the beginning of the syllable.
|
|
58
|
+
reorderToneMark(glyphs, i, plan.font);
|
|
59
|
+
break;
|
|
60
|
+
|
|
61
|
+
case INVALID:
|
|
62
|
+
// Tone mark has no valid syllable to attach to, so insert a dotted circle
|
|
63
|
+
i = insertDottedCircle(glyphs, i, plan.font);
|
|
64
|
+
break;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
i++;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const HANGUL_BASE = 0xac00;
|
|
73
|
+
const HANGUL_END = 0xd7a4;
|
|
74
|
+
const HANGUL_COUNT = HANGUL_END - HANGUL_BASE + 1;
|
|
75
|
+
const L_BASE = 0x1100; // lead
|
|
76
|
+
const V_BASE = 0x1161; // vowel
|
|
77
|
+
const T_BASE = 0x11a7; // trail
|
|
78
|
+
const L_COUNT = 19;
|
|
79
|
+
const V_COUNT = 21;
|
|
80
|
+
const T_COUNT = 28;
|
|
81
|
+
const L_END = L_BASE + L_COUNT - 1;
|
|
82
|
+
const V_END = V_BASE + V_COUNT - 1;
|
|
83
|
+
const T_END = T_BASE + T_COUNT - 1;
|
|
84
|
+
const DOTTED_CIRCLE = 0x25cc;
|
|
85
|
+
|
|
86
|
+
const isL = code => 0x1100 <= code && code <= 0x115f || 0xa960 <= code && code <= 0xa97c;
|
|
87
|
+
const isV = code => 0x1160 <= code && code <= 0x11a7 || 0xd7b0 <= code && code <= 0xd7c6;
|
|
88
|
+
const isT = code => 0x11a8 <= code && code <= 0x11ff || 0xd7cb <= code && code <= 0xd7fb;
|
|
89
|
+
const isTone = code => 0x302e <= code && code <= 0x302f;
|
|
90
|
+
const isLVT = code => HANGUL_BASE <= code && code <= HANGUL_END;
|
|
91
|
+
const isLV = code => (code - HANGUL_BASE) < HANGUL_COUNT && (code - HANGUL_BASE) % T_COUNT === 0;
|
|
92
|
+
const isCombiningL = code => L_BASE <= code && code <= L_END;
|
|
93
|
+
const isCombiningV = code => V_BASE <= code && code <= V_END;
|
|
94
|
+
const isCombiningT = code => T_BASE + 1 && 1 <= code && code <= T_END;
|
|
95
|
+
|
|
96
|
+
// Character categories
|
|
97
|
+
const X = 0; // Other character
|
|
98
|
+
const L = 1; // Leading consonant
|
|
99
|
+
const V = 2; // Medial vowel
|
|
100
|
+
const T = 3; // Trailing consonant
|
|
101
|
+
const LV = 4; // Composed <LV> syllable
|
|
102
|
+
const LVT = 5; // Composed <LVT> syllable
|
|
103
|
+
const M = 6; // Tone mark
|
|
104
|
+
|
|
105
|
+
// This function classifies a character using the above categories.
|
|
106
|
+
function getType(code) {
|
|
107
|
+
if (isL(code)) { return L; }
|
|
108
|
+
if (isV(code)) { return V; }
|
|
109
|
+
if (isT(code)) { return T; }
|
|
110
|
+
if (isLV(code)) { return LV; }
|
|
111
|
+
if (isLVT(code)) { return LVT; }
|
|
112
|
+
if (isTone(code)) { return M; }
|
|
113
|
+
return X;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// State machine actions
|
|
117
|
+
const NO_ACTION = 0;
|
|
118
|
+
const DECOMPOSE = 1;
|
|
119
|
+
const COMPOSE = 2;
|
|
120
|
+
const TONE_MARK = 4;
|
|
121
|
+
const INVALID = 5;
|
|
122
|
+
|
|
123
|
+
// Build a state machine that accepts valid syllables, and applies actions along the way.
|
|
124
|
+
// The logic this is implementing is documented at the top of the file.
|
|
125
|
+
const STATE_TABLE = [
|
|
126
|
+
// X L V T LV LVT M
|
|
127
|
+
// State 0: start state
|
|
128
|
+
[ [ NO_ACTION, 0 ], [ NO_ACTION, 1 ], [ NO_ACTION, 0 ], [ NO_ACTION, 0 ], [ DECOMPOSE, 2 ], [ DECOMPOSE, 3 ], [ INVALID, 0 ] ],
|
|
129
|
+
|
|
130
|
+
// State 1: <L>
|
|
131
|
+
[ [ NO_ACTION, 0 ], [ NO_ACTION, 1 ], [ COMPOSE, 2 ], [ NO_ACTION, 0 ], [ DECOMPOSE, 2 ], [ DECOMPOSE, 3 ], [ INVALID, 0 ] ],
|
|
132
|
+
|
|
133
|
+
// State 2: <L,V> or <LV>
|
|
134
|
+
[ [ NO_ACTION, 0 ], [ NO_ACTION, 1 ], [ NO_ACTION, 0 ], [ COMPOSE, 3 ], [ DECOMPOSE, 2 ], [ DECOMPOSE, 3 ], [ TONE_MARK, 0 ] ],
|
|
135
|
+
|
|
136
|
+
// State 3: <L,V,T> or <LVT>
|
|
137
|
+
[ [ NO_ACTION, 0 ], [ NO_ACTION, 1 ], [ NO_ACTION, 0 ], [ NO_ACTION, 0 ], [ DECOMPOSE, 2 ], [ DECOMPOSE, 3 ], [ TONE_MARK, 0 ] ]
|
|
138
|
+
];
|
|
139
|
+
|
|
140
|
+
function getGlyph(font, code, features) {
|
|
141
|
+
return new GlyphInfo(font, font.glyphForCodePoint(code).id, [code], features);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function decompose(glyphs, i, font) {
|
|
145
|
+
let glyph = glyphs[i];
|
|
146
|
+
let code = glyph.codePoints[0];
|
|
147
|
+
|
|
148
|
+
let s = code - HANGUL_BASE;
|
|
149
|
+
let t = T_BASE + s % T_COUNT;
|
|
150
|
+
s = s / T_COUNT | 0;
|
|
151
|
+
let l = L_BASE + s / V_COUNT | 0;
|
|
152
|
+
let v = V_BASE + s % V_COUNT;
|
|
153
|
+
|
|
154
|
+
// Don't decompose if all of the components are not available
|
|
155
|
+
if (!font.hasGlyphForCodePoint(l) ||
|
|
156
|
+
!font.hasGlyphForCodePoint(v) ||
|
|
157
|
+
(t !== T_BASE && !font.hasGlyphForCodePoint(t))) {
|
|
158
|
+
return i;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Replace the current glyph with decomposed L, V, and T glyphs,
|
|
162
|
+
// and apply the proper OpenType features to each component.
|
|
163
|
+
let ljmo = getGlyph(font, l, glyph.features);
|
|
164
|
+
ljmo.features.ljmo = true;
|
|
165
|
+
|
|
166
|
+
let vjmo = getGlyph(font, v, glyph.features);
|
|
167
|
+
vjmo.features.vjmo = true;
|
|
168
|
+
|
|
169
|
+
let insert = [ ljmo, vjmo ];
|
|
170
|
+
|
|
171
|
+
if (t > T_BASE) {
|
|
172
|
+
let tjmo = getGlyph(font, t, glyph.features);
|
|
173
|
+
tjmo.features.tjmo = true;
|
|
174
|
+
insert.push(tjmo);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
glyphs.splice(i, 1, ...insert);
|
|
178
|
+
return i + insert.length - 1;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function compose(glyphs, i, font) {
|
|
182
|
+
let glyph = glyphs[i];
|
|
183
|
+
let code = glyphs[i].codePoints[0];
|
|
184
|
+
let type = getType(code);
|
|
185
|
+
|
|
186
|
+
let prev = glyphs[i - 1].codePoints[0];
|
|
187
|
+
let prevType = getType(prev);
|
|
188
|
+
|
|
189
|
+
// Figure out what type of syllable we're dealing with
|
|
190
|
+
let lv, ljmo, vjmo, tjmo;
|
|
191
|
+
if (prevType === LV && type === T) {
|
|
192
|
+
// <LV,T>
|
|
193
|
+
lv = prev;
|
|
194
|
+
tjmo = glyph;
|
|
195
|
+
} else {
|
|
196
|
+
if (type === V) {
|
|
197
|
+
// <L,V>
|
|
198
|
+
ljmo = glyphs[i - 1];
|
|
199
|
+
vjmo = glyph;
|
|
200
|
+
} else {
|
|
201
|
+
// <L,V,T>
|
|
202
|
+
ljmo = glyphs[i - 2];
|
|
203
|
+
vjmo = glyphs[i - 1];
|
|
204
|
+
tjmo = glyph;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
let l = ljmo.codePoints[0];
|
|
208
|
+
let v = vjmo.codePoints[0];
|
|
209
|
+
|
|
210
|
+
// Make sure L and V are combining characters
|
|
211
|
+
if (isCombiningL(l) && isCombiningV(v)) {
|
|
212
|
+
lv = HANGUL_BASE + ((l - L_BASE) * V_COUNT + (v - V_BASE)) * T_COUNT;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
let t = (tjmo && tjmo.codePoints[0]) || T_BASE;
|
|
217
|
+
if ((lv != null) && (t === T_BASE || isCombiningT(t))) {
|
|
218
|
+
let s = lv + (t - T_BASE);
|
|
219
|
+
|
|
220
|
+
// Replace with a composed glyph if supported by the font,
|
|
221
|
+
// otherwise apply the proper OpenType features to each component.
|
|
222
|
+
if (font.hasGlyphForCodePoint(s)) {
|
|
223
|
+
let del = prevType === V ? 3 : 2;
|
|
224
|
+
glyphs.splice(i - del + 1, del, getGlyph(font, s, glyph.features));
|
|
225
|
+
return i - del + 1;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// Didn't compose (either a non-combining component or unsupported by font).
|
|
230
|
+
if (ljmo) { ljmo.features.ljmo = true; }
|
|
231
|
+
if (vjmo) { vjmo.features.vjmo = true; }
|
|
232
|
+
if (tjmo) { tjmo.features.tjmo = true; }
|
|
233
|
+
|
|
234
|
+
if (prevType === LV) {
|
|
235
|
+
// Sequence was originally <L,V>, which got combined earlier.
|
|
236
|
+
// Either the T was non-combining, or the LVT glyph wasn't supported.
|
|
237
|
+
// Decompose the glyph again and apply OT features.
|
|
238
|
+
decompose(glyphs, i - 1, font);
|
|
239
|
+
return i + 1;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
return i;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function getLength(code) {
|
|
246
|
+
switch (getType(code)) {
|
|
247
|
+
case LV:
|
|
248
|
+
case LVT:
|
|
249
|
+
return 1;
|
|
250
|
+
case V:
|
|
251
|
+
return 2;
|
|
252
|
+
case T:
|
|
253
|
+
return 3;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function reorderToneMark(glyphs, i, font) {
|
|
258
|
+
let glyph = glyphs[i];
|
|
259
|
+
let code = glyphs[i].codePoints[0];
|
|
260
|
+
|
|
261
|
+
// Move tone mark to the beginning of the previous syllable, unless it is zero width
|
|
262
|
+
if (font.glyphForCodePoint(code).advanceWidth === 0) { return; }
|
|
263
|
+
|
|
264
|
+
let prev = glyphs[i - 1].codePoints[0];
|
|
265
|
+
let len = getLength(prev);
|
|
266
|
+
|
|
267
|
+
glyphs.splice(i, 1);
|
|
268
|
+
return glyphs.splice(i - len, 0, glyph);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function insertDottedCircle(glyphs, i, font) {
|
|
272
|
+
let glyph = glyphs[i];
|
|
273
|
+
let code = glyphs[i].codePoints[0];
|
|
274
|
+
|
|
275
|
+
if (font.hasGlyphForCodePoint(DOTTED_CIRCLE)) {
|
|
276
|
+
let dottedCircle = getGlyph(font, DOTTED_CIRCLE, glyph.features);
|
|
277
|
+
|
|
278
|
+
// If the tone mark is zero width, insert the dotted circle before, otherwise after
|
|
279
|
+
let idx = font.glyphForCodePoint(code).advanceWidth === 0 ? i : i + 1;
|
|
280
|
+
glyphs.splice(idx, 0, dottedCircle);
|
|
281
|
+
i++;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
return i;
|
|
285
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import {getCombiningClass} from '../../packages/unicode-properties/index.js';
|
|
2
|
+
import DefaultShaper from './DefaultShaper';
|
|
3
|
+
import GlyphInfo from '../GlyphInfo';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Hebrew shaper (HarfBuzz hb-ot-shaper-hebrew.cc):
|
|
7
|
+
* - Compose presentation forms (FBxx) when the font lacks GPOS mark for `hebr`
|
|
8
|
+
* - Reorder patah/qamats + sheva/hiriq + meteg/below so meteg sits next to the base
|
|
9
|
+
*/
|
|
10
|
+
export default class HebrewShaper extends DefaultShaper {
|
|
11
|
+
static assignFeatures(plan, glyphs) {
|
|
12
|
+
super.assignFeatures(plan, glyphs);
|
|
13
|
+
|
|
14
|
+
if (!hasGposMark(plan.font)) {
|
|
15
|
+
composeHebrew(glyphs, plan.font);
|
|
16
|
+
}
|
|
17
|
+
reorderMarksHebrew(glyphs);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Dagesh forms for U+05D0..U+05EA (0 = no precomposed form).
|
|
22
|
+
const DAGESH_FORMS = [
|
|
23
|
+
0xFB30, 0xFB31, 0xFB32, 0xFB33, 0xFB34, 0xFB35, 0xFB36, 0,
|
|
24
|
+
0xFB38, 0xFB39, 0xFB3A, 0xFB3B, 0xFB3C, 0, 0xFB3E, 0,
|
|
25
|
+
0xFB40, 0xFB41, 0, 0xFB43, 0xFB44, 0, 0xFB46, 0xFB47,
|
|
26
|
+
0xFB48, 0xFB49, 0xFB4A
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
// Precomposed Hebrew presentation form for `a + b`, or null (HB compose_hebrew).
|
|
30
|
+
function composeHebrewPair(a, b) {
|
|
31
|
+
switch (b) {
|
|
32
|
+
case 0x05B4: // HIRIQ
|
|
33
|
+
return a === 0x05D9 ? 0xFB1D : null;
|
|
34
|
+
case 0x05B7: // PATAH
|
|
35
|
+
return a === 0x05F2 ? 0xFB1F : a === 0x05D0 ? 0xFB2E : null;
|
|
36
|
+
case 0x05B8: // QAMATS
|
|
37
|
+
return a === 0x05D0 ? 0xFB2F : null;
|
|
38
|
+
case 0x05B9: // HOLAM
|
|
39
|
+
return a === 0x05D5 ? 0xFB4B : null;
|
|
40
|
+
case 0x05BC: // DAGESH
|
|
41
|
+
if (a >= 0x05D0 && a <= 0x05EA) return DAGESH_FORMS[a - 0x05D0] || null;
|
|
42
|
+
if (a === 0xFB2A) return 0xFB2C;
|
|
43
|
+
if (a === 0xFB2B) return 0xFB2D;
|
|
44
|
+
return null;
|
|
45
|
+
case 0x05BF: // RAFE
|
|
46
|
+
return a === 0x05D1 ? 0xFB4C : a === 0x05DB ? 0xFB4D : a === 0x05E4 ? 0xFB4E : null;
|
|
47
|
+
case 0x05C1: // SHIN DOT
|
|
48
|
+
return a === 0x05E9 ? 0xFB2A : a === 0xFB49 ? 0xFB2C : null;
|
|
49
|
+
case 0x05C2: // SIN DOT
|
|
50
|
+
return a === 0x05E9 ? 0xFB2B : a === 0xFB49 ? 0xFB2D : null;
|
|
51
|
+
default:
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Greedily compose adjacent pairs when the font has the glyph
|
|
57
|
+
// (e.g. SHIN + SHIN_DOT → FB2A, then + DAGESH → FB2C).
|
|
58
|
+
function composeHebrew(glyphs, font) {
|
|
59
|
+
for (let i = 0; i + 1 < glyphs.length; ) {
|
|
60
|
+
let composed = glyphs[i].codePoints[0];
|
|
61
|
+
let consumed = 1;
|
|
62
|
+
while (i + consumed < glyphs.length) {
|
|
63
|
+
let ab = composeHebrewPair(composed, glyphs[i + consumed].codePoints[0]);
|
|
64
|
+
if (ab == null || !font.hasGlyphForCodePoint(ab)) break;
|
|
65
|
+
composed = ab;
|
|
66
|
+
consumed++;
|
|
67
|
+
}
|
|
68
|
+
if (consumed > 1) {
|
|
69
|
+
let cps = [];
|
|
70
|
+
for (let j = 0; j < consumed; j++) cps.push(...glyphs[i + j].codePoints);
|
|
71
|
+
glyphs[i] = new GlyphInfo(font, font.glyphForCodePoint(composed).id, cps, glyphs[i].features);
|
|
72
|
+
glyphs.splice(i + 1, consumed - 1);
|
|
73
|
+
}
|
|
74
|
+
i++;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// HB reorder_marks_hebrew: [patah/qamats, sheva/hiriq, meteg/below] → swap last two.
|
|
79
|
+
// unicode-properties already exposes Hebrew modified CCCs (CCC10/14/17/18/22).
|
|
80
|
+
function reorderMarksHebrew(glyphs) {
|
|
81
|
+
for (let i = 2; i < glyphs.length; i++) {
|
|
82
|
+
let c0 = getCombiningClass(glyphs[i - 2].codePoints[0]);
|
|
83
|
+
let c1 = getCombiningClass(glyphs[i - 1].codePoints[0]);
|
|
84
|
+
let c2 = getCombiningClass(glyphs[i].codePoints[0]);
|
|
85
|
+
if (
|
|
86
|
+
(c0 === 'CCC17' || c0 === 'CCC18') &&
|
|
87
|
+
(c1 === 'CCC10' || c1 === 'CCC14') &&
|
|
88
|
+
(c2 === 'CCC22' || c2 === 'Below')
|
|
89
|
+
) {
|
|
90
|
+
[glyphs[i - 1], glyphs[i]] = [glyphs[i], glyphs[i - 1]];
|
|
91
|
+
break;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Gate fallback composition on Hebrew-script GPOS `mark` (not the global featureList).
|
|
97
|
+
function hasGposMark(font) {
|
|
98
|
+
let hebr = font.GPOS?.scriptList?.find(e => e.tag === 'hebr')?.script;
|
|
99
|
+
if (!hebr) return false;
|
|
100
|
+
|
|
101
|
+
let langs = [hebr.defaultLangSys, ...(hebr.langSysRecords || []).map(l => l.langSys)].filter(Boolean);
|
|
102
|
+
return langs.some(ls =>
|
|
103
|
+
(ls.featureIndexes || []).some(i => font.GPOS.featureList[i]?.tag === 'mark')
|
|
104
|
+
);
|
|
105
|
+
}
|