imagescript 3.2.0 → 3.3.0

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.
Files changed (59) hide show
  1. package/README.md +8 -3
  2. package/THIRD_PARTY_NOTICES.md +131 -0
  3. package/dist/animation.d.ts +7 -0
  4. package/dist/animation.js +22 -12
  5. package/dist/codecs/crc.js +5 -0
  6. package/dist/codecs/gif.js +30 -52
  7. package/dist/codecs/jpeg.js +9 -27
  8. package/dist/codecs/png.js +22 -10
  9. package/dist/codecs/tga.js +3 -1
  10. package/dist/codecs/tiff.js +7 -2
  11. package/dist/codecs/vp8.js +3 -1
  12. package/dist/codecs/vp8l.js +126 -58
  13. package/dist/codecs/webp.js +11 -9
  14. package/dist/codecs/zlib.js +81 -69
  15. package/dist/frame.d.ts +1 -0
  16. package/dist/frame.js +4 -2
  17. package/dist/gif.d.ts +1 -0
  18. package/dist/gif.js +2 -2
  19. package/dist/image.d.ts +42 -7
  20. package/dist/image.js +224 -113
  21. package/dist/index.js +4 -4
  22. package/dist/render/bidi.js +350 -0
  23. package/dist/render/buffer.js +362 -0
  24. package/dist/render/color.js +164 -0
  25. package/dist/render/colr.js +523 -0
  26. package/dist/render/font.js +173 -241
  27. package/dist/render/hinting.js +12 -4
  28. package/dist/render/layout.js +1553 -0
  29. package/dist/render/linebreak.js +210 -0
  30. package/dist/render/segment.js +106 -0
  31. package/dist/render/shape.js +984 -0
  32. package/dist/render/shapers/arabic.js +284 -0
  33. package/dist/render/shapers/hangul.js +142 -0
  34. package/dist/render/shapers/hebrew.js +49 -0
  35. package/dist/render/shapers/index.js +52 -0
  36. package/dist/render/shapers/indic.js +579 -0
  37. package/dist/render/shapers/khmer.js +104 -0
  38. package/dist/render/shapers/machine.js +129 -0
  39. package/dist/render/shapers/myanmar.js +121 -0
  40. package/dist/render/shapers/syllabic.js +100 -0
  41. package/dist/render/shapers/thai.js +125 -0
  42. package/dist/render/shapers/use.js +235 -0
  43. package/dist/render/svg.js +101 -22
  44. package/dist/render/text.js +373 -103
  45. package/dist/render/unicode-data.js +63 -0
  46. package/dist/render/unicode.js +58 -0
  47. package/dist/text-layout.d.ts +4 -1
  48. package/dist/text-layout.js +2 -2
  49. package/dist/util/alpha.js +43 -0
  50. package/dist/util/blur.js +80 -5
  51. package/dist/util/bytes.js +31 -0
  52. package/dist/util/limits.js +34 -0
  53. package/dist/util/options.js +7 -0
  54. package/dist/util/resample.js +359 -79
  55. package/dist/util/rotate.js +43 -0
  56. package/dist/version.js +1 -1
  57. package/licenses/HarfBuzz.txt +42 -0
  58. package/licenses/Unicode-3.0.txt +39 -0
  59. package/package.json +1 -1
@@ -0,0 +1,579 @@
1
+ /*!
2
+ * The Indic shaper (Bengali, Devanagari, Gujarati, Gurmukhi, Kannada, Malayalam, Oriya, Tamil, Telugu), ported from
3
+ * HarfBuzz (hb-ot-shaper-indic.cc, hb-ot-shaper-indic-machine.rl)
4
+ * Copyright © 2011,2012 Google, Inc., Old MIT license (see THIRD_PARTY_NOTICES.md and licenses/HarfBuzz.txt)
5
+ */
6
+ import { isLigated, isSubstituted, isMultiplied, generalCategory, isMark } from "../buffer.js";
7
+ import { decompose, compose, unicodeCategory } from "../shape.js";
8
+ import { indicCategory } from "../unicode-data.js";
9
+ import { value } from "../unicode.js";
10
+ import { lazyScanner, cat, seq, alt, opt, star, any } from "./machine.js";
11
+ import { forEachSyllable, insertDottedCircles, vowelConstraintsPreprocess } from "./syllabic.js";
12
+ const CATEGORY_NAMES = {
13
+ X: 0, C: 1, V: 2, N: 3, H: 4, ZWNJ: 5, ZWJ: 6, M: 7, SM: 8, A: 9, VD: 9, PLACEHOLDER: 10,
14
+ DOTTEDCIRCLE: 11, RS: 12, MPst: 13, Repha: 14, Ra: 15, CM: 16, Symbol: 17, CS: 18, VAbv: 20, VBlw: 21,
15
+ VPre: 22, VPst: 23, Robatic: 25, Xgroup: 26, Ygroup: 27, IV: 2, DB: 3, GB: 10, As: 32, MH: 35, MR: 36,
16
+ MW: 37, MY: 38, PT: 39, VS: 40, ML: 41, SMPst: 57,
17
+ };
18
+ const POSITION_NAMES = {
19
+ START: 0, RA_TO_BECOME_REPH: 1, PRE_M: 2, PRE_C: 3, BASE_C: 4, AFTER_MAIN: 5,
20
+ ABOVE_C: 6, BEFORE_SUB: 7, BELOW_C: 8, AFTER_SUB: 9, BEFORE_POST: 10, POST_C: 11,
21
+ AFTER_POST: 12, SMVD: 13, END: 14,
22
+ };
23
+ const properties = new Map();
24
+ export function indicProperties(u) {
25
+ const v = value(indicCategory, u);
26
+ let p = properties.get(v);
27
+ if (!p) {
28
+ const [category, position] = v.split('/');
29
+ properties.set(v, p = [CATEGORY_NAMES[category], POSITION_NAMES[position]]);
30
+ }
31
+ return p;
32
+ }
33
+ export function setIndicProperties(info) {
34
+ [info.category, info.position] = indicProperties(info.codepoint);
35
+ }
36
+ export const isOneOf = (info, categories) => !isLigated(info) && categories.includes(info.category);
37
+ const CONSONANTS = [1, 18, 15, 16, 2, 10, 11];
38
+ const JOINERS = [6, 5];
39
+ const isConsonant = (info) => isOneOf(info, CONSONANTS);
40
+ const isJoiner = (info) => isOneOf(info, JOINERS);
41
+ const isHalant = (info) => isOneOf(info, [4]);
42
+ const findSyllables = lazyScanner(() => {
43
+ const c = cat(1, 15);
44
+ const n = seq(opt(opt(cat(5)), cat(12)), opt(cat(3), opt(cat(3))));
45
+ const z = cat(6, 5);
46
+ const reph = alt(seq(cat(15), cat(4)), cat(14));
47
+ const sm = cat(8, 57);
48
+ const cn = seq(c, opt(cat(6)), opt(n));
49
+ const symbol = seq(cat(17), opt(cat(3)));
50
+ const matraGroup = seq(star(z), alt(cat(7), seq(opt(sm), cat(13))), opt(cat(3)), opt(cat(4)));
51
+ const syllableTail = seq(opt(opt(z), sm, opt(sm), opt(cat(5))), star(cat(9, 9)));
52
+ const halantGroup = seq(opt(z), cat(4), opt(cat(6), opt(cat(3))));
53
+ const finalHalantGroup = alt(halantGroup, seq(cat(4), cat(5)));
54
+ const medialGroup = opt(cat(16));
55
+ const halantOrMatraGroup = alt(finalHalantGroup, star(matraGroup));
56
+ const complexSyllableTail = seq(star(halantGroup, cn), medialGroup, halantOrMatraGroup, syllableTail);
57
+ const consonantSyllable = seq(opt(cat(14, 18)), cn, complexSyllableTail);
58
+ const vowelSyllable = seq(opt(reph), cat(2), opt(n), alt(cat(6), complexSyllableTail));
59
+ const standaloneCluster = seq(alt(seq(opt(cat(14, 18)), cat(10)), seq(opt(reph), cat(11))), opt(n), complexSyllableTail);
60
+ const symbolCluster = seq(symbol, syllableTail);
61
+ const brokenCluster = seq(opt(reph), opt(n), complexSyllableTail);
62
+ return [
63
+ [consonantSyllable, 0],
64
+ [vowelSyllable, 1],
65
+ [standaloneCluster, 2],
66
+ [symbolCluster, 3],
67
+ [cat(57), 5],
68
+ [brokenCluster, 4],
69
+ [any, 5],
70
+ ];
71
+ });
72
+ export function setSyllables(buffer, syllables, broken) {
73
+ let serial = 1;
74
+ for (const [start, end, type] of syllables) {
75
+ for (let i = start; i < end; i++)
76
+ buffer.info[i].syllable = serial << 4 | type;
77
+ if (type === broken)
78
+ buffer.hasBrokenSyllable = true;
79
+ if (++serial === 16)
80
+ serial = 1;
81
+ }
82
+ }
83
+ const DEFAULT_CONFIG = { hasOldSpec: false, virama: 0, rephPos: 10, rephMode: 0, blwfMode: 0 };
84
+ const CONFIGS = {
85
+ Deva: { hasOldSpec: true, virama: 0x094d, rephPos: 10, rephMode: 0, blwfMode: 0 },
86
+ Beng: { hasOldSpec: true, virama: 0x09cd, rephPos: 9, rephMode: 0, blwfMode: 0 },
87
+ Guru: { hasOldSpec: true, virama: 0x0a4d, rephPos: 7, rephMode: 0, blwfMode: 0 },
88
+ Gujr: { hasOldSpec: true, virama: 0x0acd, rephPos: 10, rephMode: 0, blwfMode: 0 },
89
+ Orya: { hasOldSpec: true, virama: 0x0b4d, rephPos: 5, rephMode: 0, blwfMode: 0 },
90
+ Taml: { hasOldSpec: true, virama: 0x0bcd, rephPos: 12, rephMode: 0, blwfMode: 0 },
91
+ Telu: { hasOldSpec: true, virama: 0x0c4d, rephPos: 12, rephMode: 1, blwfMode: 1 },
92
+ Knda: { hasOldSpec: true, virama: 0x0ccd, rephPos: 12, rephMode: 0, blwfMode: 1 },
93
+ Mlym: { hasOldSpec: true, virama: 0x0d4d, rephPos: 5, rephMode: 2, blwfMode: 0 },
94
+ };
95
+ const FEATURES = [
96
+ ['nukt', 13 | 64],
97
+ ['akhn', 13 | 64],
98
+ ['rphf', 12 | 64],
99
+ ['rkrf', 13 | 64],
100
+ ['pref', 12 | 64],
101
+ ['blwf', 12 | 64],
102
+ ['abvf', 12 | 64],
103
+ ['half', 12 | 64],
104
+ ['pstf', 12 | 64],
105
+ ['vatu', 13 | 64],
106
+ ['cjct', 13 | 64],
107
+ ['init', 12 | 64],
108
+ ['pres', 13 | 64],
109
+ ['abvs', 13 | 64],
110
+ ['blws', 13 | 64],
111
+ ['psts', 13 | 64],
112
+ ['haln', 13 | 64],
113
+ ];
114
+ export class WouldSubstitute {
115
+ zeroContext;
116
+ lookups;
117
+ constructor(plan, tag, zeroContext) {
118
+ this.zeroContext = zeroContext;
119
+ this.lookups = plan.map.stageLookups(0, tag).map(l => l.index);
120
+ }
121
+ would(font, glyphs) {
122
+ if (!font.gsub)
123
+ return false;
124
+ for (const index of this.lookups) {
125
+ const l = font.gsub.lookup(index);
126
+ if (l && l.would(glyphs, this.zeroContext))
127
+ return true;
128
+ }
129
+ return false;
130
+ }
131
+ }
132
+ function collectFeatures(planner) {
133
+ const map = planner.map;
134
+ map.addGsubPause(setupSyllables);
135
+ map.enableFeature('locl', 64);
136
+ map.enableFeature('ccmp', 64);
137
+ map.addGsubPause(initialReordering);
138
+ let i = 0;
139
+ for (; i < 11; i++) {
140
+ map.addFeature(FEATURES[i][0], FEATURES[i][1]);
141
+ map.addGsubPause(null);
142
+ }
143
+ map.addGsubPause(finalReordering);
144
+ for (; i < 17; i++)
145
+ map.addFeature(FEATURES[i][0], FEATURES[i][1]);
146
+ }
147
+ function overrideFeatures(planner) {
148
+ planner.map.disableFeature('liga');
149
+ planner.map.addGsubPause(null);
150
+ }
151
+ function dataCreate(plan) {
152
+ const config = CONFIGS[plan.script] ?? DEFAULT_CONFIG;
153
+ const isOldSpec = config.hasOldSpec && plan.map.chosenScript[0][3] !== '2';
154
+ const zeroContext = !isOldSpec && plan.script !== 'Mlym';
155
+ return {
156
+ config, isOldSpec, viramaGlyph: -1,
157
+ rphf: new WouldSubstitute(plan, 'rphf', zeroContext),
158
+ pref: new WouldSubstitute(plan, 'pref', zeroContext),
159
+ blwf: new WouldSubstitute(plan, 'blwf', zeroContext),
160
+ pstf: new WouldSubstitute(plan, 'pstf', zeroContext),
161
+ vatu: new WouldSubstitute(plan, 'vatu', zeroContext),
162
+ masks: FEATURES.map(([tag, flags]) => flags & 1 ? 0 : plan.map.get1Mask(tag)),
163
+ };
164
+ }
165
+ function viramaGlyph(indic, font) {
166
+ if (indic.viramaGlyph === -1)
167
+ indic.viramaGlyph = indic.config.virama ? font.glyph(indic.config.virama) : 0;
168
+ return indic.viramaGlyph;
169
+ }
170
+ function consonantPositionFromFace(indic, consonant, virama, font) {
171
+ const a = [virama, consonant], b = [consonant, virama];
172
+ if (indic.blwf.would(font, a) || indic.blwf.would(font, b) || indic.vatu.would(font, a) || indic.vatu.would(font, b))
173
+ return 8;
174
+ if (indic.pstf.would(font, a) || indic.pstf.would(font, b))
175
+ return 11;
176
+ if (indic.pref.would(font, a) || indic.pref.would(font, b))
177
+ return 11;
178
+ return 4;
179
+ }
180
+ function setupSyllables(_plan, _font, buffer) {
181
+ setSyllables(buffer, findSyllables(buffer.info.map(info => info.category)), 4);
182
+ }
183
+ function initialReorderingConsonantSyllable(plan, font, buffer, start, end) {
184
+ const indic = plan.data, info = buffer.info;
185
+ if (buffer.script === 'Knda' && start + 3 <= end && isOneOf(info[start], [15]) && isOneOf(info[start + 1], [4]) && isOneOf(info[start + 2], [6])) {
186
+ buffer.mergeClusters(start + 1, start + 3);
187
+ [info[start + 1], info[start + 2]] = [info[start + 2], info[start + 1]];
188
+ }
189
+ let base = end, hasReph = false;
190
+ {
191
+ let limit = start;
192
+ const config = indic.config;
193
+ if (indic.masks[2] && start + 3 <= end
194
+ && (config.rephMode === 0 && !isJoiner(info[start + 2]) || config.rephMode === 1 && info[start + 2].category === 6)) {
195
+ const glyphs = [info[start].codepoint, info[start + 1].codepoint, config.rephMode === 1 ? info[start + 2].codepoint : 0];
196
+ if (indic.rphf.would(font, glyphs.slice(0, 2)) || config.rephMode === 1 && indic.rphf.would(font, glyphs)) {
197
+ limit += 2;
198
+ while (limit < end && isJoiner(info[limit]))
199
+ limit++;
200
+ base = start;
201
+ hasReph = true;
202
+ }
203
+ }
204
+ else if (config.rephMode === 2 && info[start].category === 14) {
205
+ limit += 1;
206
+ while (limit < end && isJoiner(info[limit]))
207
+ limit++;
208
+ base = start;
209
+ hasReph = true;
210
+ }
211
+ let i = end, seenBelow = false;
212
+ do {
213
+ i--;
214
+ if (isConsonant(info[i])) {
215
+ if (info[i].position !== 8 && (info[i].position !== 11 || seenBelow)) {
216
+ base = i;
217
+ break;
218
+ }
219
+ if (info[i].position === 8)
220
+ seenBelow = true;
221
+ base = i;
222
+ }
223
+ else if (start < i && info[i].category === 6 && info[i - 1].category === 4)
224
+ break;
225
+ } while (i > limit);
226
+ if (hasReph && base === start && limit - base <= 2)
227
+ hasReph = false;
228
+ }
229
+ for (let i = start; i < base; i++)
230
+ info[i].position = Math.min(3, info[i].position);
231
+ if (base < end)
232
+ info[base].position = 4;
233
+ if (hasReph)
234
+ info[start].position = 1;
235
+ if (indic.isOldSpec) {
236
+ const disallowDoubleHalants = buffer.script === 'Knda';
237
+ for (let i = base + 1; i < end; i++)
238
+ if (info[i].category === 4) {
239
+ let j = end - 1;
240
+ for (; j > i; j--)
241
+ if (isConsonant(info[j]) || disallowDoubleHalants && info[j].category === 4)
242
+ break;
243
+ if (info[j].category !== 4 && j > i) {
244
+ const t = info[i];
245
+ for (let k = i; k < j; k++)
246
+ info[k] = info[k + 1];
247
+ info[j] = t;
248
+ }
249
+ break;
250
+ }
251
+ }
252
+ {
253
+ let lastPos = 0;
254
+ for (let i = start; i < end; i++) {
255
+ const category = info[i].category;
256
+ if ([6, 5, 3, 12, 16, 4].includes(category)) {
257
+ info[i].position = lastPos;
258
+ if (category === 4 && info[i].position === 2)
259
+ for (let j = i; j > start; j--)
260
+ if (info[j - 1].position !== 2) {
261
+ info[i].position = info[j - 1].position;
262
+ break;
263
+ }
264
+ }
265
+ else if (info[i].position !== 13) {
266
+ if (category === 13 && i > start && info[i - 1].category === 8)
267
+ info[i - 1].position = info[i].position;
268
+ lastPos = info[i].position;
269
+ }
270
+ }
271
+ }
272
+ {
273
+ let last = base;
274
+ for (let i = base + 1; i < end; i++)
275
+ if (isConsonant(info[i])) {
276
+ for (let j = last + 1; j < i; j++)
277
+ if (info[j].position < 13)
278
+ info[j].position = info[i].position;
279
+ last = i;
280
+ }
281
+ else if (info[i].category === 7 || info[i].category === 13)
282
+ last = i;
283
+ }
284
+ {
285
+ const syllable = info[start].syllable;
286
+ for (let i = start; i < end; i++)
287
+ info[i].syllable = i - start;
288
+ const sorted = info.slice(start, end).sort((a, b) => a.position - b.position || a.syllable - b.syllable);
289
+ for (let i = start; i < end; i++)
290
+ info[i] = sorted[i - start];
291
+ let firstLeftMatra = end, lastLeftMatra = end;
292
+ base = end;
293
+ for (let i = start; i < end; i++) {
294
+ if (info[i].position === 4) {
295
+ base = i;
296
+ break;
297
+ }
298
+ else if (info[i].position === 2) {
299
+ if (firstLeftMatra === end)
300
+ firstLeftMatra = i;
301
+ lastLeftMatra = i;
302
+ }
303
+ }
304
+ if (firstLeftMatra < lastLeftMatra) {
305
+ buffer.reverseRange(firstLeftMatra, lastLeftMatra + 1);
306
+ let i = firstLeftMatra;
307
+ for (let j = i; j <= lastLeftMatra; j++)
308
+ if (info[j].category === 7 || info[j].category === 13) {
309
+ buffer.reverseRange(i, j + 1);
310
+ i = j + 1;
311
+ }
312
+ }
313
+ if (indic.isOldSpec || end - start > 127)
314
+ buffer.mergeClusters(base, end);
315
+ else {
316
+ for (let i = base; i < end; i++)
317
+ if (info[i].syllable !== 255) {
318
+ let min = i, max = i, j = start + info[i].syllable;
319
+ while (j !== i) {
320
+ min = Math.min(min, j);
321
+ max = Math.max(max, j);
322
+ const next = start + info[j].syllable;
323
+ info[j].syllable = 255;
324
+ j = next;
325
+ }
326
+ buffer.mergeClusters(Math.max(base, min), max + 1);
327
+ }
328
+ }
329
+ for (let i = start; i < end; i++)
330
+ info[i].syllable = syllable;
331
+ }
332
+ for (let i = start; i < end && info[i].position === 1; i++)
333
+ info[i].mask |= indic.masks[2];
334
+ let mask = indic.masks[7];
335
+ if (!indic.isOldSpec && indic.config.blwfMode === 0)
336
+ mask |= indic.masks[5];
337
+ for (let i = start; i < base; i++)
338
+ info[i].mask |= mask;
339
+ mask = indic.masks[5] | indic.masks[6] | indic.masks[8];
340
+ for (let i = base + 1; i < end; i++)
341
+ info[i].mask |= mask;
342
+ if (indic.isOldSpec && buffer.script === 'Deva')
343
+ for (let i = start; i + 1 < base; i++)
344
+ if (info[i].category === 15 && info[i + 1].category === 4 && (i + 2 === base || info[i + 2].category !== 6)) {
345
+ info[i].mask |= indic.masks[5];
346
+ info[i + 1].mask |= indic.masks[5];
347
+ }
348
+ const prefLength = 2;
349
+ if (indic.masks[4] && base + prefLength < end)
350
+ for (let i = base + 1; i + prefLength - 1 < end; i++) {
351
+ if (indic.pref.would(font, [info[i].codepoint, info[i + 1].codepoint])) {
352
+ for (let j = 0; j < prefLength; j++)
353
+ info[i++].mask |= indic.masks[4];
354
+ break;
355
+ }
356
+ }
357
+ for (let i = start + 1; i < end; i++)
358
+ if (isJoiner(info[i])) {
359
+ const nonJoiner = info[i].category === 5;
360
+ let j = i;
361
+ do {
362
+ j--;
363
+ if (nonJoiner)
364
+ info[j].mask &= ~indic.masks[7];
365
+ } while (j > start && !isConsonant(info[j]));
366
+ }
367
+ }
368
+ function initialReordering(plan, font, buffer) {
369
+ const indic = plan.data;
370
+ const virama = viramaGlyph(indic, font);
371
+ if (virama)
372
+ for (const info of buffer.info)
373
+ if (info.position === 4)
374
+ info.position = consonantPositionFromFace(indic, info.codepoint, virama, font);
375
+ insertDottedCircles(font, buffer, 4, 11, 14, 14);
376
+ forEachSyllable(buffer, (start, end) => {
377
+ const type = buffer.info[start].syllable & 0x0f;
378
+ if (type === 0 || type === 1 || type === 4 || type === 2)
379
+ initialReorderingConsonantSyllable(plan, font, buffer, start, end);
380
+ });
381
+ }
382
+ function finalReorderingSyllable(plan, buffer, start, end) {
383
+ const indic = plan.data, info = buffer.info;
384
+ const virama = indic.viramaGlyph;
385
+ if (virama > 0)
386
+ for (let i = start; i < end; i++)
387
+ if (info[i].codepoint === virama && isLigated(info[i]) && isMultiplied(info[i])) {
388
+ info[i].category = 4;
389
+ info[i].glyphProps &= ~(32 | 64);
390
+ }
391
+ let tryPref = !!indic.masks[4];
392
+ let base = start;
393
+ for (; base < end; base++)
394
+ if (info[base].position >= 4) {
395
+ if (tryPref && base + 1 < end) {
396
+ for (let i = base + 1; i < end; i++)
397
+ if (info[i].mask & indic.masks[4]) {
398
+ if (!(isSubstituted(info[i]) && isLigated(info[i]) && !isMultiplied(info[i]))) {
399
+ base = i;
400
+ while (base < end && isHalant(info[base]))
401
+ base++;
402
+ if (base < end)
403
+ info[base].position = 4;
404
+ tryPref = false;
405
+ }
406
+ break;
407
+ }
408
+ if (base === end)
409
+ break;
410
+ }
411
+ if (buffer.script === 'Mlym')
412
+ for (let i = base + 1; i < end; i++) {
413
+ while (i < end && isJoiner(info[i]))
414
+ i++;
415
+ if (i === end || !isHalant(info[i]))
416
+ break;
417
+ i++;
418
+ while (i < end && isJoiner(info[i]))
419
+ i++;
420
+ if (i < end && isConsonant(info[i]) && info[i].position === 8) {
421
+ base = i;
422
+ info[base].position = 4;
423
+ }
424
+ }
425
+ if (start < base && info[base].position > 4)
426
+ base--;
427
+ break;
428
+ }
429
+ if (base === end && start < base && isOneOf(info[base - 1], [6]))
430
+ base--;
431
+ if (base < end)
432
+ while (start < base && isOneOf(info[base], [3, 4]))
433
+ base--;
434
+ if (start + 1 < end && start < base) {
435
+ let newPos = base === end ? base - 2 : base - 1;
436
+ if (buffer.script !== 'Mlym' && buffer.script !== 'Taml') {
437
+ for (;;) {
438
+ while (newPos > start && !isOneOf(info[newPos], [7, 13, 4]))
439
+ newPos--;
440
+ if (isHalant(info[newPos]) && info[newPos].position !== 2) {
441
+ if (newPos + 1 < end && info[newPos + 1].category === 6 && newPos > start) {
442
+ newPos--;
443
+ continue;
444
+ }
445
+ }
446
+ else
447
+ newPos = start;
448
+ break;
449
+ }
450
+ }
451
+ if (start < newPos && info[newPos].position !== 2) {
452
+ for (let i = newPos; i > start; i--)
453
+ if (info[i - 1].position === 2) {
454
+ const oldPos = i - 1;
455
+ if (oldPos < base && base <= newPos)
456
+ base--;
457
+ const tmp = info[oldPos];
458
+ for (let k = oldPos; k < newPos; k++)
459
+ info[k] = info[k + 1];
460
+ info[newPos] = tmp;
461
+ buffer.mergeClusters(newPos, Math.min(end, base + 1));
462
+ newPos--;
463
+ }
464
+ }
465
+ else {
466
+ for (let i = start; i < base; i++)
467
+ if (info[i].position === 2) {
468
+ buffer.mergeClusters(i, Math.min(end, base + 1));
469
+ break;
470
+ }
471
+ }
472
+ }
473
+ if (start + 1 < end && info[start].position === 1
474
+ && (info[start].category === 14) !== (isLigated(info[start]) && !isMultiplied(info[start]))) {
475
+ const rephPos = indic.config.rephPos;
476
+ let newRephPos = -1;
477
+ const afterHalant = () => {
478
+ let p = start + 1;
479
+ while (p < base && !isHalant(info[p]))
480
+ p++;
481
+ if (p < base && isHalant(info[p])) {
482
+ if (p + 1 < base && isJoiner(info[p + 1]))
483
+ p++;
484
+ return p;
485
+ }
486
+ return -1;
487
+ };
488
+ if (rephPos !== 12)
489
+ newRephPos = afterHalant();
490
+ if (newRephPos < 0 && rephPos === 5) {
491
+ let p = base;
492
+ while (p + 1 < end && info[p + 1].position <= 5)
493
+ p++;
494
+ if (p < end)
495
+ newRephPos = p;
496
+ }
497
+ if (newRephPos < 0 && rephPos === 9) {
498
+ let p = base;
499
+ while (p + 1 < end && ![11, 12, 13].includes(info[p + 1].position))
500
+ p++;
501
+ if (p < end)
502
+ newRephPos = p;
503
+ }
504
+ if (newRephPos < 0)
505
+ newRephPos = afterHalant();
506
+ if (newRephPos < 0) {
507
+ newRephPos = end - 1;
508
+ while (newRephPos > start && info[newRephPos].position === 13)
509
+ newRephPos--;
510
+ if (isHalant(info[newRephPos]))
511
+ for (let i = base + 1; i < newRephPos; i++)
512
+ if (info[i].category === 7 || info[i].category === 13)
513
+ newRephPos--;
514
+ }
515
+ buffer.mergeClusters(start, newRephPos + 1);
516
+ const reph = info[start];
517
+ for (let k = start; k < newRephPos; k++)
518
+ info[k] = info[k + 1];
519
+ info[newRephPos] = reph;
520
+ if (start < base && base <= newRephPos)
521
+ base--;
522
+ }
523
+ if (tryPref && base + 1 < end)
524
+ for (let i = base + 1; i < end; i++)
525
+ if (info[i].mask & indic.masks[4]) {
526
+ if (isLigated(info[i]) && !isMultiplied(info[i])) {
527
+ let newPos = base;
528
+ if (buffer.script !== 'Mlym' && buffer.script !== 'Taml')
529
+ while (newPos > start && !isOneOf(info[newPos - 1], [7, 13, 4]))
530
+ newPos--;
531
+ if (newPos > start && isHalant(info[newPos - 1]) && newPos < end && isJoiner(info[newPos]))
532
+ newPos++;
533
+ const oldPos = i;
534
+ buffer.mergeClusters(newPos, oldPos + 1);
535
+ const tmp = info[oldPos];
536
+ for (let k = oldPos; k > newPos; k--)
537
+ info[k] = info[k - 1];
538
+ info[newPos] = tmp;
539
+ if (newPos <= base && base < oldPos)
540
+ base++;
541
+ }
542
+ break;
543
+ }
544
+ if (info[start].position === 2) {
545
+ const c = start ? generalCategory(info[start - 1]) : -1;
546
+ if (!start || !(c >= 1 && c <= 12))
547
+ info[start].mask |= indic.masks[11];
548
+ }
549
+ }
550
+ function finalReordering(plan, _font, buffer) {
551
+ if (!buffer.len)
552
+ return;
553
+ forEachSyllable(buffer, (start, end) => finalReorderingSyllable(plan, buffer, start, end));
554
+ }
555
+ export const indicShaper = {
556
+ collectFeatures,
557
+ overrideFeatures,
558
+ dataCreate,
559
+ preprocessText: (_plan, buffer) => vowelConstraintsPreprocess(buffer),
560
+ decompose: (_c, ab) => {
561
+ if (ab === 0x0931 || ab === 0x09dc || ab === 0x09dd || ab === 0x0b94)
562
+ return null;
563
+ return decompose(ab);
564
+ },
565
+ compose: (_c, a, b) => {
566
+ if (isMark(unicodeCategory(a)))
567
+ return 0;
568
+ if (a === 0x09af && b === 0x09bc)
569
+ return 0x09df;
570
+ return compose(a, b);
571
+ },
572
+ setupMasks: (_plan, buffer) => {
573
+ for (const info of buffer.info)
574
+ setIndicProperties(info);
575
+ },
576
+ normalization: 3,
577
+ zeroWidthMarks: 0,
578
+ fallbackPosition: false,
579
+ };
@@ -0,0 +1,104 @@
1
+ /*!
2
+ * The Khmer shaper, ported from HarfBuzz (hb-ot-shaper-khmer.cc, hb-ot-shaper-khmer-machine.rl)
3
+ * Copyright © 2011,2012 Google, Inc., Old MIT license (see THIRD_PARTY_NOTICES.md and licenses/HarfBuzz.txt)
4
+ */
5
+ import { isMark } from "../buffer.js";
6
+ import { decompose, compose, unicodeCategory } from "../shape.js";
7
+ import { lazyScanner, cat, seq, alt, opt, star, any } from "./machine.js";
8
+ import { forEachSyllable, insertDottedCircles } from "./syllabic.js";
9
+ import { indicProperties, setSyllables } from "./indic.js";
10
+ const findSyllables = lazyScanner(() => {
11
+ const c = cat(1, 15, 2);
12
+ const joiner = cat(6, 5);
13
+ const cn = seq(c, opt(opt(joiner), cat(25)));
14
+ const xgroup = star(star(joiner), cat(26));
15
+ const ygroup = star(cat(27));
16
+ const matraGroup = seq(opt(cat(22)), xgroup, opt(cat(21)), xgroup, opt(opt(joiner), cat(20)), xgroup, opt(cat(23)));
17
+ const syllableTail = seq(xgroup, matraGroup, xgroup, opt(cat(4), c), ygroup);
18
+ const brokenCluster = seq(opt(cat(25)), star(cat(4), cn), alt(cat(4), syllableTail));
19
+ const consonantSyllable = seq(alt(cn, cat(10), cat(11)), brokenCluster);
20
+ return [[consonantSyllable, 0], [brokenCluster, 1], [any, 2]];
21
+ });
22
+ const FEATURES = [
23
+ ['pref', 12 | 64],
24
+ ['blwf', 12 | 64],
25
+ ['abvf', 12 | 64],
26
+ ['pstf', 12 | 64],
27
+ ['cfar', 12 | 64],
28
+ ['pres', 13],
29
+ ['abvs', 13],
30
+ ['blws', 13],
31
+ ['psts', 13],
32
+ ];
33
+ function reorderConsonantSyllable(plan, buffer, start, end) {
34
+ const masks = plan.data, info = buffer.info;
35
+ const mask = masks[1] | masks[2] | masks[3];
36
+ for (let i = start + 1; i < end; i++)
37
+ info[i].mask |= mask;
38
+ let coengs = 0;
39
+ for (let i = start + 1; i < end; i++) {
40
+ if (info[i].category === 4 && coengs <= 2 && i + 1 < end) {
41
+ coengs++;
42
+ if (info[i + 1].category === 15) {
43
+ for (let j = 0; j < 2; j++)
44
+ info[i + j].mask |= masks[0];
45
+ buffer.mergeClusters(start, i + 2);
46
+ const t0 = info[i], t1 = info[i + 1];
47
+ for (let k = i - 1; k >= start; k--)
48
+ info[k + 2] = info[k];
49
+ info[start] = t0;
50
+ info[start + 1] = t1;
51
+ if (masks[4])
52
+ for (let j = i + 2; j < end; j++)
53
+ info[j].mask |= masks[4];
54
+ coengs = 2;
55
+ }
56
+ }
57
+ else if (info[i].category === 22) {
58
+ buffer.mergeClusters(start, i + 1);
59
+ const t = info[i];
60
+ for (let k = i - 1; k >= start; k--)
61
+ info[k + 1] = info[k];
62
+ info[start] = t;
63
+ }
64
+ }
65
+ }
66
+ function reorder(plan, font, buffer) {
67
+ insertDottedCircles(font, buffer, 1, 11, -1, -1);
68
+ forEachSyllable(buffer, (start, end) => {
69
+ const type = buffer.info[start].syllable & 0x0f;
70
+ if (type === 0 || type === 1)
71
+ reorderConsonantSyllable(plan, buffer, start, end);
72
+ });
73
+ }
74
+ function collectFeatures(planner) {
75
+ const map = planner.map;
76
+ map.addGsubPause((_plan, _font, buffer) => setSyllables(buffer, findSyllables(buffer.info.map(info => info.category)), 1));
77
+ map.addGsubPause(reorder);
78
+ map.enableFeature('locl', 64);
79
+ map.enableFeature('ccmp', 64);
80
+ let i = 0;
81
+ for (; i < 5; i++)
82
+ map.addFeature(FEATURES[i][0], FEATURES[i][1]);
83
+ map.addGsubPause(null);
84
+ for (; i < FEATURES.length; i++)
85
+ map.addFeature(FEATURES[i][0], FEATURES[i][1]);
86
+ }
87
+ const SPLIT = new Set([0x17be, 0x17bf, 0x17c0, 0x17c4, 0x17c5]);
88
+ export const khmerShaper = {
89
+ collectFeatures,
90
+ overrideFeatures: planner => {
91
+ planner.map.enableFeature('clig');
92
+ planner.map.disableFeature('liga');
93
+ },
94
+ dataCreate: plan => FEATURES.map(([tag, flags]) => flags & 1 ? 0 : plan.map.get1Mask(tag)),
95
+ decompose: (_c, ab) => SPLIT.has(ab) ? [0x17c1, ab] : decompose(ab),
96
+ compose: (_c, a, b) => isMark(unicodeCategory(a)) ? 0 : compose(a, b),
97
+ setupMasks: (_plan, buffer) => {
98
+ for (const info of buffer.info)
99
+ info.category = indicProperties(info.codepoint)[0];
100
+ },
101
+ normalization: 3,
102
+ zeroWidthMarks: 0,
103
+ fallbackPosition: false,
104
+ };