diff options
| author | eunhhu <[email protected]> | 2026-09-28 14:49:05 +0100 |
|---|---|---|
| committer | eunhhu <[email protected]> | 2026-09-28 14:49:05 +0100 |
| commit | 60a3c0232bbca8d5d8c1107ab8197d272212ef42 (patch) | |
| tree | ba7b4bad98b79ad1abd40cdb038c493d50df0c19 /text/shape.c | |
| parent | c23b79f1cb599ee7766002ad23756643e72113e5 (diff) | |
| download | vega-60a3c0232bbca8d5d8c1107ab8197d272212ef42.tar.gz vega-60a3c0232bbca8d5d8c1107ab8197d272212ef42.zip | |
Shape the nine Indic scripts as HarfBuzz does
The Indic shaper, from HarfBuzz 10.2's source: syllables found by the
Indic machine; each syllable's base consonant, its pre-base matras and
reph moved around it and masked for the basic features, applied one at
a time within the syllable; then matras, reph and pre-base-reordering
consonants placed again by what the features made. Vowel constraints,
Indic decompositions and compositions, dotted circles for broken
syllables, and per-syllable matching come with it.
Plans carry GSUB pauses (steps where the shaper does its part) and the
stages of the features the shaper asks whether they would substitute;
the runtime answers with each subtable's would_apply, as HarfBuzz does.
Masks are now the common ones and each shaper's own.
text/syllabic.py reads HarfBuzz's Ragel grammars (Indic, Khmer,
Myanmar) and compiles each to a minimal DFA with Ragel's scanner
semantics; beside the machines Ragel generated from the same grammars,
100,000 random runs each find no syllable that differs. It also reads
HarfBuzz's Indic table (Unicode 16) cell for cell.
Every installed font shapes as HarfBuzz does (213 fonts, 1,688 plans);
the Indic ones at 20,000 runs a plan (FreeSerif's eight scripts,
FreeSans, Unifont's nine without GSUB), and nine new-spec Noto Sans
fonts at 5,000.
Diffstat (limited to 'text/shape.c')
| -rw-r--r-- | text/shape.c | 872 |
1 files changed, 844 insertions, 28 deletions
diff --git a/text/shape.c b/text/shape.c index 0813bae..3443b0b 100644 --- a/text/shape.c +++ b/text/shape.c @@ -1,15 +1,18 @@ -/* Shaping as HarfBuzz 10 does it (text/shape.h), with its default, Arabic - * and Hebrew shapers. The code follows HarfBuzz's own step for step - * (hb-ot-shape.cc, hb-ot-shape-normalize.cc, hb-buffer.cc, hb-ot-layout- - * gsubgpos.hh, the GSUB and GPOS lookups, hb-ot-shaper-arabic.cc and - * -hebrew.cc), under HarfBuzz's names, so that the two read side by side; - * the build tool compares the results with HarfBuzz's (symbols-tool.c). */ +/* Shaping as HarfBuzz 10 does it (text/shape.h), with its default, Arabic, + * Hebrew, Hangul and Indic shapers. The code follows HarfBuzz's own step + * for step (hb-ot-shape.cc, hb-ot-shape-normalize.cc, hb-buffer.cc, + * hb-ot-layout-gsubgpos.hh, the GSUB and GPOS lookups, hb-ot-shaper-*.cc), + * under HarfBuzz's names, so that the two read side by side; the build + * tool compares the results with HarfBuzz's (symbols-tool.c). */ #include "shape.h" +#include "syllabic.h" #include "text.h" #include <stdio.h> #include <stdlib.h> #include <string.h> +#define TAG4(a, b, c, d) ((uint32_t)(a) << 24 | (uint32_t)(b) << 16 | (uint32_t)(c) << 8 | (uint32_t)(d)) + /* ---- OpenType tables, read in place: a read past a table's end gives 0 */ static uint32_t get16(VegaTable t, uint32_t at) { @@ -223,17 +226,22 @@ bool vega_shaping_open(VegaShaping *s, const unsigned char *bytes, size_t size, REFUSE("out of memory"); for (uint32_t i = 0; i < s->plan_count; ++i) { const unsigned char *p = s->plans + (size_t)i * VEGA_PLAN_BYTES; - uint32_t ranges[2][2] = {{le32(p + 72), le32(p + 76)}, {le32(p + 80), le32(p + 84)}}; - if ((p[4] != VEGA_SHAPE_LTR && p[4] != VEGA_SHAPE_RTL) || p[5] > VEGA_SHAPE_RTL || p[6] > VEGA_SHAPER_HANGUL) + uint32_t ranges[2][2] = {{le32(p + 80), le32(p + 84)}, {le32(p + 88), le32(p + 92)}}; + if ((p[4] != VEGA_SHAPE_LTR && p[4] != VEGA_SHAPE_RTL) || p[5] > VEGA_SHAPE_RTL || p[6] > VEGA_SHAPER_INDIC) REFUSE("plan %u", i); if ((p[7] & VEGA_PLAN_FALLBACK_MARKS) && !s->extents) REFUSE("plan %u: marks placed without extents", i); for (int table = 0; table < 2; ++table) { if (ranges[table][0] > step_count || ranges[table][1] > step_count - ranges[table][0]) REFUSE("plan %u: steps", i); - for (uint32_t k = 0; k < ranges[table][1]; ++k) - if (le16(s->steps + (size_t)(ranges[table][0] + k) * VEGA_STEP_BYTES) >= s->lookup_count[table]) + for (uint32_t k = 0; k < ranges[table][1]; ++k) { + const unsigned char *step = s->steps + (size_t)(ranges[table][0] + k) * VEGA_STEP_BYTES; + bool pause = le16(step) == VEGA_STEP_PAUSE; + if (pause ? table || !step[2] || step[2] > VEGA_PAUSE_CLEAR_SYLLABLES : le16(step) >= s->lookup_count[table]) REFUSE("plan %u: a lookup the font lacks", i); + } } + for (int k = 0; k < VEGA_PLAN_STAGES; ++k) + if ((uint32_t)le16(p + 100 + 8 * k) + le16(p + 102 + 8 * k) > ranges[0][1]) REFUSE("plan %u: stages", i); } return true; } @@ -290,7 +298,13 @@ typedef struct { uint32_t codepoint; /* a character, then (map_glyphs) a glyph */ uint32_t mask, cluster, glyph_index; uint16_t uprops, gprops; - uint8_t lig_props, action; + uint8_t lig_props; + union { + uint8_t action; /* Arabic's joining action, Hangul's jamo feature */ + uint8_t position; /* Indic's */ + }; + uint8_t category; /* Indic's */ + uint8_t syllable; /* serial << 4 | type, once a shaper finds syllables */ } Info; typedef struct { int32_t x_advance, y_advance, x_offset, y_offset; @@ -311,7 +325,7 @@ struct VegaShaper { }; enum { HAS_NON_ASCII = 1, HAS_DEFAULT_IGNORABLES = 2, HAS_SPACE_FALLBACK = 4, HAS_GPOS_ATTACHMENT = 8, HAS_CGJ = 16, - HAS_VARIATION_SELECTOR_FALLBACK = 32 }; + HAS_VARIATION_SELECTOR_FALLBACK = 32, HAS_BROKEN_SYLLABLE = 64 }; VegaShaper *vega_shaper_new(void) { return calloc(1, sizeof(VegaShaper)); } @@ -390,6 +404,11 @@ static void output_glyph(VegaShaper *b, uint32_t glyph) { b->out[b->out_len++].codepoint = glyph; } +static void output_info(VegaShaper *b, Info info) { + if (!ensure(b, b->out_len + 1)) return; + b->out[b->out_len++] = info; +} + /* num_in glyphs become num_out, each a copy of the first with its own * codepoint; their clusters merge. */ static void merge_clusters(VegaShaper *b, size_t start, size_t end); @@ -674,12 +693,15 @@ static void form_clusters(VegaShaper *b) { /* ---- The plan */ +typedef struct { uint32_t first, count; } Stage; typedef struct { uint32_t script; int direction, native, shaper, flags; uint32_t global_mask, kern_mask, masks[VEGA_PLAN_MASKS]; const unsigned char *gsub, *gpos; uint32_t gsub_count, gpos_count; + unsigned shaper_flags; + Stage stages[VEGA_PLAN_STAGES]; } Plan; static bool find_plan(const VegaShaping *s, uint32_t script, int direction, Plan *plan) { @@ -691,9 +713,12 @@ static bool find_plan(const VegaShaping *s, uint32_t script, int direction, Plan } if (!found) return false; *plan = (Plan){le32(found), found[4], found[5], found[6], found[7], le32(found + 8), le32(found + 12), {0}, - s->steps + (size_t)le32(found + 72) * VEGA_STEP_BYTES, - s->steps + (size_t)le32(found + 80) * VEGA_STEP_BYTES, le32(found + 76), le32(found + 84)}; + s->steps + (size_t)le32(found + 80) * VEGA_STEP_BYTES, + s->steps + (size_t)le32(found + 88) * VEGA_STEP_BYTES, le32(found + 84), le32(found + 92), found[96], + {{0, 0}}}; for (int k = 0; k < VEGA_PLAN_MASKS; ++k) plan->masks[k] = le32(found + 16 + 4 * k); + for (int k = 0; k < VEGA_PLAN_STAGES; ++k) + plan->stages[k] = (Stage){le16(found + 100 + 8 * k), le16(found + 102 + 8 * k)}; return true; } @@ -783,8 +808,23 @@ static bool compose_hebrew(const Normalizer *c, uint32_t a, uint32_t b, uint32_t return *ab != 0; } +/* Indic's: no split matra composes again, and Bengali YA with nukta does, + * though Unicode excludes it. */ +static bool compose_indic(uint32_t a, uint32_t b, uint32_t *ab) { + if (is_mark_category(vega_category(a))) return false; + if (a == 0x09AF && b == 0x09BC) { + *ab = 0x09DF; + return true; + } + return compose_unicode(a, b, ab); +} + static bool compose(const Normalizer *c, uint32_t a, uint32_t b, uint32_t *ab) { - return c->plan->shaper == VEGA_SHAPER_HEBREW ? compose_hebrew(c, a, b, ab) : compose_unicode(a, b, ab); + switch (c->plan->shaper) { + case VEGA_SHAPER_HEBREW: return compose_hebrew(c, a, b, ab); + case VEGA_SHAPER_INDIC: return compose_indic(a, b, ab); + default: return compose_unicode(a, b, ab); + } } static bool nominal(const VegaShaping *s, uint32_t u, uint32_t *glyph) { return (*glyph = vega_shaping_glyph(s, u)) != 0; } @@ -801,10 +841,17 @@ static void next_char(VegaShaper *b, uint32_t glyph) { next_glyph(b); } +/* The shaper's decomposition: Indic keeps a few letters whole. */ +static bool decompose_char(const Normalizer *c, uint32_t ab, uint32_t *a, uint32_t *b) { + if (c->plan->shaper == VEGA_SHAPER_INDIC && (ab == 0x0931 || ab == 0x09DC || ab == 0x09DD || ab == 0x0B94)) + return false; + return decompose_unicode(ab, a, b); +} + /* Returns how many characters ab became, 0 if it did not decompose. */ static unsigned decompose(const Normalizer *c, bool shortest, uint32_t ab) { uint32_t a, bb, a_glyph = 0, b_glyph = 0; - if (!decompose_unicode(ab, &a, &bb) || (bb && !nominal(c->s, bb, &b_glyph))) return 0; + if (!decompose_char(c, ab, &a, &bb) || (bb && !nominal(c->s, bb, &b_glyph))) return 0; bool has_a = nominal(c->s, a, &a_glyph); if (shortest && has_a) { output_char(c->b, a, a_glyph); @@ -976,9 +1023,10 @@ static void normalize(const VegaShaping *s, const Plan *plan, VegaShaper *b) { if (!b->len) return; Normalizer c = {s, plan, b}; /* The shapers' preference is "auto" (composed diacritics); Hangul's is - * none: what the font has stays, and nothing is composed again. */ + * none: what the font has stays, and nothing is composed again; + * Indic's decomposes what decomposes even when the font has it whole. */ const bool none = plan->shaper == VEGA_SHAPER_HANGUL; - const bool might_short_circuit = true, always_short_circuit = none; + const bool always_short_circuit = none, might_short_circuit = none || plan->shaper != VEGA_SHAPER_INDIC; bool all_simple = true; clear_output(b); size_t count = b->len; @@ -1237,6 +1285,7 @@ typedef struct { uint32_t lookup_mask, lookup_props; bool auto_zwnj, auto_zwj, random, has_glyph_classes; int nesting_left; + bool per_syllable; /* GSUB matches within the syllable it starts in */ uint32_t random_state; long last_base; size_t last_base_until; @@ -1271,6 +1320,7 @@ typedef struct { Matcher matcher; uint32_t glyph_data; size_t idx, end; + uint8_t syllable; /* per syllable, from where the lookup is: only its glyphs match */ } Skippy; enum { SKIP_NO, SKIP_YES, SKIP_MAYBE }; @@ -1278,9 +1328,11 @@ enum { MATCH_NO, MATCH_YES, MATCH_MAYBE }; enum { MATCH, NOT_MATCH, SKIP }; static void skippy_init(Skippy *it, Apply *c, bool context_match, size_t start) { + VegaShaper *b = c->b; *it = (Skippy){c, c->lookup_props, context_match ? 0xFFFFFFFFu : c->lookup_mask, c->table == 1 || (context_match && c->auto_zwnj), context_match || c->auto_zwj, c->table == 1, - {MATCH_ANY, 0, 0}, 0, start, c->b->len}; + {MATCH_ANY, 0, 0}, 0, start, b->len, + !c->table && c->per_syllable && start == b->idx && b->idx < b->len ? b->info[b->idx].syllable : 0}; } static int may_skip(const Skippy *it, const Info *info) { @@ -1302,7 +1354,7 @@ static bool matches(const Apply *c, const Matcher *m, uint32_t index, uint32_t g } static int may_match(const Skippy *it, const Info *info) { - if (!(info->mask & it->mask)) return MATCH_NO; + if (!(info->mask & it->mask) || (it->syllable && it->syllable != info->syllable)) return MATCH_NO; if (it->matcher.kind != MATCH_ANY) return matches(it->c, &it->matcher, it->glyph_data, info->codepoint) ? MATCH_YES : MATCH_NO; return MATCH_MAYBE; } @@ -2105,13 +2157,23 @@ static VegaDigest buffer_digest(const VegaShaper *b) { return d; } -static void apply_table(const VegaShaping *s, VegaShaper *b, int table, const unsigned char *steps, uint32_t count) { +static bool shaper_pause(const VegaShaping *s, const Plan *plan, VegaShaper *b, unsigned pause); + +/* hb_ot_map_t::apply: the plan's lookups for a table in order; at a pause, + * the shaper's part (when it changed the buffer, the digest starts over). */ +static void apply_table(const VegaShaping *s, const Plan *plan, VegaShaper *b, int table) { + const unsigned char *steps = table ? plan->gpos : plan->gsub; + uint32_t count = table ? plan->gpos_count : plan->gsub_count; /* A lookup can apply only where the buffer has a glyph it starts from. */ - Apply c = {s, b, table, table ? s->gpos : s->gsub, 0, 0, true, true, false, s->glyph_classes != 0, - MAX_NESTING_LEVEL, 1, -1, 0, buffer_digest(b)}; + Apply c = {.s = s, .b = b, .table = table, .t = table ? s->gpos : s->gsub, .has_glyph_classes = s->glyph_classes != 0, + .nesting_left = MAX_NESTING_LEVEL, .random_state = 1, .last_base = -1, .digest = buffer_digest(b)}; for (uint32_t i = 0; i < count; ++i) { const unsigned char *step = steps + (size_t)i * VEGA_STEP_BYTES; uint32_t index = le16(step); + if (index == VEGA_STEP_PAUSE) { + if (shaper_pause(s, plan, b, step[2])) c.digest = buffer_digest(b); + continue; + } if (!digest_may_intersect(&s->lookups[table][index].digest, &c.digest)) continue; c.lookup_mask = le32(step + 4); c.last_base = -1; @@ -2119,6 +2181,7 @@ static void apply_table(const VegaShaping *s, VegaShaper *b, int table, const un c.auto_zwnj = step[2] & VEGA_STEP_AUTO_ZWNJ; c.auto_zwj = step[2] & VEGA_STEP_AUTO_ZWJ; c.random = step[2] & VEGA_STEP_RANDOM; + c.per_syllable = step[2] & VEGA_STEP_PER_SYLLABLE; apply_string(&c, &s->lookups[table][index]); } } @@ -2160,8 +2223,9 @@ static void kern_table(const VegaShaping *s, VegaShaper *b, const Plan *plan) { if ((apple && (coverage & 0x20)) || !horizontal || format != 0) continue; bool reverse = b->direction == VEGA_SHAPE_RTL; if (reverse) reverse_range(b, 0, b->len, true); - Apply c = {s, b, 1, s->gpos, plan->kern_mask, 0x08, true, true, false, s->glyph_classes != 0, MAX_NESTING_LEVEL, 1, - -1, 0, {{0, 0, 0}}}; + Apply c = {.s = s, .b = b, .table = 1, .t = s->gpos, .lookup_mask = plan->kern_mask, .lookup_props = 0x08, + .auto_zwnj = true, .auto_zwj = true, .has_glyph_classes = s->glyph_classes != 0, + .nesting_left = MAX_NESTING_LEVEL, .random_state = 1, .last_base = -1}; for (size_t idx = 0; idx < b->len;) { if (!(b->info[idx].mask & plan->kern_mask)) { idx++; @@ -2385,6 +2449,756 @@ static void fallback_marks(const VegaShaping *s, const Plan *plan, VegaShaper *b position_cluster(s, plan, b, start, b->len, adjust); } +/* ---- Would a lookup substitute glyphs as they are, alone + * (hb_ot_layout_lookup_would_substitute): the Indic shaper's question of + * its basic features. */ + +static bool would_match_input(const Apply *c, Matcher m, const uint32_t *glyphs, unsigned len, unsigned count) { + if (count != len) return false; + for (unsigned i = 1; i < count; ++i) + if (!matches(c, &m, i - 1, glyphs[i])) return false; + return true; +} + +static bool would_apply_rules(const Apply *c, int type, uint32_t set, Matcher input, const uint32_t *glyphs, + unsigned len, bool zero_context) { + VegaTable t = c->t; + for (uint32_t r = 0, n = set ? get16(t, set) : 0; r < n; ++r) { + uint32_t rule = offset16(t, set, set + 2 + 2 * r); + if (type == 5) { + input.values = rule + 4; + if (would_match_input(c, input, glyphs, len, get16(t, rule))) return true; + continue; + } + unsigned back_count = get16(t, rule); + uint32_t input_at = rule + 2 + 2 * back_count; + unsigned input_count = get16(t, input_at); + unsigned ahead_count = input_count ? get16(t, input_at + 2 + 2 * (input_count - 1)) : 0; + if (zero_context && (back_count || ahead_count)) continue; + input.values = input_at + 2; + if (would_match_input(c, input, glyphs, len, input_count)) return true; + } + return false; +} + +static bool would_apply_subtable(const Apply *c, const VegaSubtable *st, const uint32_t *glyphs, unsigned len, + bool zero_context) { + VegaTable t = c->t; + uint32_t at = st->at, format = get16(t, at); + switch (st->type) { + case 1: case 2: case 3: case 8: + if (format != 1 && !(st->type == 1 && format == 2)) return false; + return len == 1 && coverage(t, offset16(t, at, at + 2), glyphs[0]) != NOT_COVERED; + case 4: { + uint32_t index = coverage(t, offset16(t, at, at + 2), glyphs[0]); + if (format != 1 || index == NOT_COVERED || index >= get16(t, at + 4)) return false; + uint32_t set = offset16(t, at, at + 6 + 2 * index); + for (uint32_t k = 0, n = set ? get16(t, set) : 0; k < n; ++k) { + uint32_t lig = offset16(t, set, set + 2 + 2 * k); + if (would_match_input(c, (Matcher){MATCH_GLYPH, 0, lig + 4}, glyphs, len, get16(t, lig + 2))) return true; + } + return false; + } + case 5: case 6: { + bool chain = st->type == 6; + if (format == 1 || format == 2) { + /* format 2 takes the first glyph's class whatever the coverage */ + uint32_t classes = format == 2 ? offset16(t, at, at + (chain ? 6 : 4)) : 0; + uint32_t index = format == 1 ? coverage(t, offset16(t, at, at + 2), glyphs[0]) : class_of(t, classes, glyphs[0]); + uint32_t sets_at = format == 1 ? at + 4 : at + (chain ? 10 : 6); + if (index == NOT_COVERED || index >= get16(t, sets_at)) return false; + Matcher input = {format == 2 ? MATCH_CLASS : MATCH_GLYPH, classes, 0}; + return would_apply_rules(c, st->type, offset16(t, at, sets_at + 2 + 2 * index), input, glyphs, len, + zero_context); + } + if (format != 3) return false; + if (!chain) return would_match_input(c, (Matcher){MATCH_COVERAGE, at, at + 8}, glyphs, len, get16(t, at + 2)); + unsigned back_count = get16(t, at + 2); + uint32_t input_at = at + 4 + 2 * back_count; + unsigned input_count = get16(t, input_at); + unsigned ahead_count = get16(t, input_at + 2 + 2 * input_count); + if (zero_context && (back_count || ahead_count)) return false; + return would_match_input(c, (Matcher){MATCH_COVERAGE, at, input_at + 4}, glyphs, len, input_count); + } + default: return false; + } +} + +/* Whether the lookups of a feature's stage (all of them, whatever their + * masks) would substitute glyphs, len of them. */ +static bool would_substitute(const VegaShaping *s, const Plan *plan, int stage, const uint32_t *glyphs, unsigned len, + bool zero_context) { + const Stage *st = &plan->stages[stage]; + Apply c = {.s = s, .t = s->gsub}; + for (uint32_t k = 0; k < st->count; ++k) { + const unsigned char *step = plan->gsub + (size_t)(st->first + k) * VEGA_STEP_BYTES; + uint32_t index = le16(step); + if (index == VEGA_STEP_PAUSE) continue; + const VegaLookup *l = &s->lookups[0][index]; + if (!len || !digest_may_have(&l->digest, glyphs[0])) continue; + for (uint32_t i = 0; i < l->count; ++i) + if (would_apply_subtable(&c, &s->subtables[l->first + i], glyphs, len, zero_context)) return true; + } + return false; +} + +/* ---- Syllables (hb-ot-shaper-*-machine.rl, hb-ot-shaper-syllabic.cc) */ + +/* A shaper's machine over the buffer's categories: from each position the + * longest syllable it accepts; each numbered, 1 to 15 over again, beside + * its type. */ +static void find_syllables(VegaShaper *b, const VegaMachine *m) { + unsigned serial = 1; + for (size_t p = 0; p < b->len;) { + unsigned state = 0, type = 0; + size_t end = p + 1; + for (size_t q = p; q < b->len; ++q) { + size_t cell = (size_t)state * m->classes + m->class_of[b->info[q].category < 63 ? b->info[q].category : 63]; + state = m->width == 1 ? ((const uint8_t *)m->next)[cell] : ((const uint16_t *)m->next)[cell]; + if (state >= m->states) break; + if (m->accept[state]) type = m->accept[state], end = q + 1; + } + if (type == m->broken) b->scratch |= HAS_BROKEN_SYLLABLE; + for (size_t i = p; i < end; ++i) b->info[i].syllable = (uint8_t)(serial << 4 | (type ? type - 1 : 0)); + if (++serial == 16) serial = 1; + p = end; + } +} + +static size_t next_syllable(const VegaShaper *b, size_t start) { + size_t end = start + 1; + while (end < b->len && b->info[end].syllable == b->info[start].syllable) end++; + return end; +} + +/* A dotted circle at the start of each broken syllable (after a repha that + * starts it), for its marks to sit on; false when none went in. */ +static bool insert_dotted_circles(const VegaShaping *s, VegaShaper *b, unsigned broken, unsigned category, int repha, + unsigned position) { + uint32_t glyph; + if (!(b->scratch & HAS_BROKEN_SYLLABLE) || !nominal(s, 0x25CC, &glyph)) return false; + /* All of it zero but these: HarfBuzz's is a fresh glyph_info. */ + Info circle = {.codepoint = glyph, .uprops = VEGA_CC, .position = (uint8_t)position, .category = (uint8_t)category}; + clear_output(b); + unsigned last = 0; + while (b->idx < b->len && b->successful) { + unsigned syllable = b->info[b->idx].syllable; + if (last != syllable && (syllable & 0x0F) == broken) { + last = syllable; + Info c = circle; + c.cluster = b->info[b->idx].cluster; + c.mask = b->info[b->idx].mask; + c.syllable = (uint8_t)syllable; + if (repha >= 0) + while (b->idx < b->len && b->successful && b->info[b->idx].syllable == last && + b->info[b->idx].category == (unsigned)repha) + next_glyph(b); + output_info(b, c); + } else + next_glyph(b); + } + sync(b); + return true; +} + +/* Vowel sequences that read as another vowel get a dotted circle between + * (hb-ot-shaper-vowel-constraints.cc, from the Universal Shaping Engine's + * IndicShapingInvalidCluster.txt): each first letter with what may not + * follow it. */ +static const struct { uint32_t script, first, second[12]; } vowel_constraints[] = { + {TAG4('D', 'e', 'v', 'a'), 0x0905, {0x093A, 0x093B, 0x093E, 0x0945, 0x0946, 0x0949, 0x094A, 0x094B, 0x094C, 0x094F, 0x0956, 0x0957}}, + {TAG4('D', 'e', 'v', 'a'), 0x0906, {0x093A, 0x0945, 0x0946, 0x0947, 0x0948}}, + {TAG4('D', 'e', 'v', 'a'), 0x0909, {0x0941}}, + {TAG4('D', 'e', 'v', 'a'), 0x090F, {0x0945, 0x0946, 0x0947}}, + {TAG4('B', 'e', 'n', 'g'), 0x0985, {0x09BE}}, + {TAG4('B', 'e', 'n', 'g'), 0x098B, {0x09C3}}, + {TAG4('B', 'e', 'n', 'g'), 0x098C, {0x09E2}}, + {TAG4('G', 'u', 'r', 'u'), 0x0A05, {0x0A3E, 0x0A48, 0x0A4C}}, + {TAG4('G', 'u', 'r', 'u'), 0x0A72, {0x0A3F, 0x0A40, 0x0A47}}, + {TAG4('G', 'u', 'r', 'u'), 0x0A73, {0x0A41, 0x0A42, 0x0A4B}}, + {TAG4('G', 'u', 'j', 'r'), 0x0A85, {0x0ABE, 0x0AC5, 0x0AC7, 0x0AC8, 0x0AC9, 0x0ACB, 0x0ACC}}, + {TAG4('G', 'u', 'j', 'r'), 0x0AC5, {0x0ABE}}, + {TAG4('O', 'r', 'y', 'a'), 0x0B05, {0x0B3E}}, + {TAG4('O', 'r', 'y', 'a'), 0x0B0F, {0x0B57}}, + {TAG4('O', 'r', 'y', 'a'), 0x0B13, {0x0B57}}, + {TAG4('T', 'a', 'm', 'l'), 0x0B85, {0x0BC2}}, + {TAG4('T', 'e', 'l', 'u'), 0x0C12, {0x0C4C, 0x0C55}}, + {TAG4('T', 'e', 'l', 'u'), 0x0C3F, {0x0C55}}, + {TAG4('T', 'e', 'l', 'u'), 0x0C46, {0x0C55}}, + {TAG4('T', 'e', 'l', 'u'), 0x0C4A, {0x0C55}}, + {TAG4('K', 'n', 'd', 'a'), 0x0C89, {0x0CBE}}, + {TAG4('K', 'n', 'd', 'a'), 0x0C8B, {0x0CBE}}, + {TAG4('K', 'n', 'd', 'a'), 0x0C92, {0x0CCC}}, + {TAG4('M', 'l', 'y', 'm'), 0x0D07, {0x0D57}}, + {TAG4('M', 'l', 'y', 'm'), 0x0D09, {0x0D57}}, + {TAG4('M', 'l', 'y', 'm'), 0x0D0E, {0x0D46}}, + {TAG4('M', 'l', 'y', 'm'), 0x0D12, {0x0D3E, 0x0D57}}, + {TAG4('S', 'i', 'n', 'h'), 0x0D85, {0x0DCF, 0x0DD0, 0x0DD1}}, + {TAG4('S', 'i', 'n', 'h'), 0x0D8B, {0x0DDF}}, + {TAG4('S', 'i', 'n', 'h'), 0x0D8D, {0x0DD8}}, + {TAG4('S', 'i', 'n', 'h'), 0x0D8F, {0x0DDF}}, + {TAG4('S', 'i', 'n', 'h'), 0x0D91, {0x0DCA, 0x0DD9, 0x0DDA, 0x0DDC, 0x0DDD, 0x0DDE}}, + {TAG4('S', 'i', 'n', 'h'), 0x0D94, {0x0DDF}}, + {TAG4('B', 'r', 'a', 'h'), 0x11005, {0x11038}}, + {TAG4('B', 'r', 'a', 'h'), 0x1100B, {0x1103E}}, + {TAG4('B', 'r', 'a', 'h'), 0x1100F, {0x11042}}, + {TAG4('K', 'h', 'o', 'j'), 0x11200, {0x1122C, 0x11231, 0x11233}}, + {TAG4('K', 'h', 'o', 'j'), 0x11206, {0x1122C}}, + {TAG4('K', 'h', 'o', 'j'), 0x1122C, {0x11230, 0x11231}}, + {TAG4('K', 'h', 'o', 'j'), 0x11240, {0x1122E}}, + {TAG4('S', 'i', 'n', 'd'), 0x112B0, {0x112E0, 0x112E5, 0x112E6, 0x112E7, 0x112E8}}, + {TAG4('T', 'i', 'r', 'h'), 0x11481, {0x114B0}}, + {TAG4('T', 'i', 'r', 'h'), 0x1148B, {0x114BA}}, + {TAG4('T', 'i', 'r', 'h'), 0x1148D, {0x114BA}}, + {TAG4('T', 'i', 'r', 'h'), 0x114AA, {0x114B5, 0x114B6}}, + {TAG4('M', 'o', 'd', 'i'), 0x11600, {0x11639, 0x1163A}}, + {TAG4('M', 'o', 'd', 'i'), 0x11601, {0x11639, 0x1163A}}, + {TAG4('T', 'a', 'k', 'r'), 0x11680, {0x116AD, 0x116B4, 0x116B5}}, + {TAG4('T', 'a', 'k', 'r'), 0x11686, {0x116B2}}, +}; + +/* A dotted circle out before the current character, as a copy of it (its + * properties, less continuing the character before). */ +static void output_dotted_circle(VegaShaper *b) { + output_glyph(b, 0x25CC); + if (b->out_len) b->out[b->out_len - 1].uprops &= (uint16_t)~UPROPS_CONTINUATION; +} + +static void preprocess_vowel_constraints(const Plan *plan, VegaShaper *b) { + bool any = false; + for (size_t k = 0; k < sizeof(vowel_constraints) / sizeof(*vowel_constraints) && !any; ++k) + any = vowel_constraints[k].script == plan->script; + if (!any && plan->script != TAG4('D', 'e', 'v', 'a')) return; + clear_output(b); + while (b->idx + 1 < b->len && b->successful) { + uint32_t u = b->info[b->idx].codepoint, next = b->info[b->idx + 1].codepoint; + bool matched = false; + for (size_t k = 0; k < sizeof(vowel_constraints) / sizeof(*vowel_constraints) && !matched; ++k) + if (vowel_constraints[k].script == plan->script && vowel_constraints[k].first == u) + for (int j = 0; j < 12 && vowel_constraints[k].second[j] && !matched; ++j) + matched = vowel_constraints[k].second[j] == next; + /* Devanagari's RA, virama, I: the circle goes before the I */ + if (plan->script == TAG4('D', 'e', 'v', 'a') && u == 0x0930 && next == 0x094D && b->idx + 2 < b->len && + b->info[b->idx + 2].codepoint == 0x0907) { + next_glyph(b); + next_glyph(b); + output_dotted_circle(b); + } + next_glyph(b); + if (matched) { + output_dotted_circle(b); + next_glyph(b); + } + } + sync(b); +} + +/* ---- Indic (hb-ot-shaper-indic.cc): each consonant syllable's base found, + * its pre-base matras, reph and the like moved around the base and masked + * for the basic features; after those, the matras, reph and + * pre-base-reordering consonants placed again by what the features made. */ + +enum { REPH_IMPLICIT, REPH_EXPLICIT, REPH_LOG_REPHA }; +typedef struct { uint32_t script, virama; uint8_t reph_pos, reph_mode; bool blwf_post_only; } IndicConfig; +static const IndicConfig indic_configs[] = { + {0, 0, VEGA_POS_BEFORE_POST, REPH_IMPLICIT, false}, + {TAG4('D', 'e', 'v', 'a'), 0x094D, VEGA_POS_BEFORE_POST, REPH_IMPLICIT, false}, + {TAG4('B', 'e', 'n', 'g'), 0x09CD, VEGA_POS_AFTER_SUB, REPH_IMPLICIT, false}, + {TAG4('G', 'u', 'r', 'u'), 0x0A4D, VEGA_POS_BEFORE_SUB, REPH_IMPLICIT, false}, + {TAG4('G', 'u', 'j', 'r'), 0x0ACD, VEGA_POS_BEFORE_POST, REPH_IMPLICIT, false}, + {TAG4('O', 'r', 'y', 'a'), 0x0B4D, VEGA_POS_AFTER_MAIN, REPH_IMPLICIT, false}, + {TAG4('T', 'a', 'm', 'l'), 0x0BCD, VEGA_POS_AFTER_POST, REPH_IMPLICIT, false}, + {TAG4('T', 'e', 'l', 'u'), 0x0C4D, VEGA_POS_AFTER_POST, REPH_EXPLICIT, true}, + {TAG4('K', 'n', 'd', 'a'), 0x0CCD, VEGA_POS_AFTER_POST, REPH_IMPLICIT, true}, + {TAG4('M', 'l', 'y', 'm'), 0x0D4D, VEGA_POS_AFTER_MAIN, REPH_LOG_REPHA, false}, +}; + +typedef struct { + const VegaShaping *s; + const Plan *plan; + const IndicConfig *config; + bool old_spec, zero_context; + uint32_t virama; /* its glyph; 0 when the font has none */ +} Indic; + +static Indic indic_of(const VegaShaping *s, const Plan *plan) { + Indic d = {s, plan, &indic_configs[0], plan->shaper_flags & VEGA_INDIC_OLD_SPEC, false, 0}; + for (size_t i = 1; i < sizeof(indic_configs) / sizeof(*indic_configs); ++i) + if (indic_configs[i].script == plan->script) d.config = &indic_configs[i]; + /* New specifications match the basic features without context; so do + * scripts with one, but Malayalam's both take context. */ + d.zero_context = !d.old_spec && plan->script != TAG4('M', 'l', 'y', 'm'); + if (d.config->virama) nominal(s, d.config->virama, &d.virama); + return d; +} + +#define FLAG(x) (1u << (x)) +#define FLAG_UNSAFE(x) ((x) < 32 ? 1u << (x) : 0u) +static bool is_one_of(const Info *i, uint32_t flags) { + return !(i->gprops & LIGATED) && (FLAG_UNSAFE(i->category) & flags); /* a ligature is none of them */ +} +static bool is_consonant(const Info *i) { + return is_one_of(i, FLAG(VEGA_OT_C) | FLAG(VEGA_OT_CS) | FLAG(VEGA_OT_RA) | FLAG(VEGA_OT_CM) | FLAG(VEGA_OT_V) | + FLAG(VEGA_OT_PLACEHOLDER) | FLAG(VEGA_OT_DOTTEDCIRCLE)); +} +static bool is_joiner(const Info *i) { return is_one_of(i, FLAG(VEGA_OT_ZWJ) | FLAG(VEGA_OT_ZWNJ)); } +static bool is_halant(const Info *i) { return is_one_of(i, FLAG(VEGA_OT_H)); } +static bool ligated_and_didnt_multiply(const Info *i) { return (i->gprops & LIGATED) && !(i->gprops & MULTIPLIED); } + +static bool indic_would(const Indic *d, int stage, const uint32_t *glyphs, unsigned len) { + return would_substitute(d->s, d->plan, stage, glyphs, len, d->zero_context); +} + +/* Below-base, post-base or base: what the font's features would make of + * the consonant with a virama, either way round (fonts copied old lookups + * into new scripts). */ +static uint8_t consonant_position(const Indic *d, uint32_t consonant) { + uint32_t glyphs[3] = {d->virama, consonant, d->virama}; + if (indic_would(d, VEGA_STAGE_BLWF, glyphs, 2) || indic_would(d, VEGA_STAGE_BLWF, glyphs + 1, 2) || + indic_would(d, VEGA_STAGE_VATU, glyphs, 2) || indic_would(d, VEGA_STAGE_VATU, glyphs + 1, 2)) + return VEGA_POS_BELOW_C; + if (indic_would(d, VEGA_STAGE_PSTF, glyphs, 2) || indic_would(d, VEGA_STAGE_PSTF, glyphs + 1, 2) || + indic_would(d, VEGA_STAGE_PREF, glyphs, 2) || indic_would(d, VEGA_STAGE_PREF, glyphs + 1, 2)) + return VEGA_POS_POST_C; + return VEGA_POS_BASE_C; +} + +static void setup_masks_indic(VegaShaper *b) { + for (size_t i = 0; i < b->len; ++i) { + uint16_t categories = vega_indic_categories(b->info[i].codepoint); + b->info[i].category = categories & 0xFF; + b->info[i].position = categories >> 8; + } +} + +static void indic_reorder_consonant_syllable(const Indic *d, VegaShaper *b, size_t start, size_t end) { + Info *info = b->info; + const uint32_t *masks = d->plan->masks; + uint32_t script = d->plan->script; + /* Kannada's Ra, halant, ZWJ is Ra, ZWJ, halant. */ + if (script == TAG4('K', 'n', 'd', 'a') && start + 3 <= end && is_one_of(&info[start], FLAG(VEGA_OT_RA)) && + is_one_of(&info[start + 1], FLAG(VEGA_OT_H)) && is_one_of(&info[start + 2], FLAG(VEGA_OT_ZWJ))) { + merge_clusters(b, start + 1, start + 3); + Info t = info[start + 1]; + info[start + 1] = info[start + 2]; + info[start + 2] = t; + } + + /* 1. The base: from the end back, the first consonant without a + * below-base or post-base form (those follow below-base ones), or the + * first consonant. An initial Ra, halant that the font makes a reph + * is no candidate. */ + size_t base = end, limit = start; + bool has_reph = false; + if (masks[VEGA_MASK_RPHF] && start + 3 <= end && + ((d->config->reph_mode == REPH_IMPLICIT && !is_joiner(&info[start + 2])) || + (d->config->reph_mode == REPH_EXPLICIT && info[start + 2].category == VEGA_OT_ZWJ))) { + uint32_t glyphs[3] = {info[start].codepoint, info[start + 1].codepoint, + d->config->reph_mode == REPH_EXPLICIT ? info[start + 2].codepoint : 0}; + if (indic_would(d, VEGA_STAGE_RPHF, glyphs, 2) || + (d->config->reph_mode == REPH_EXPLICIT && indic_would(d, VEGA_STAGE_RPHF, glyphs, 3))) { + limit += 2; + while (limit < end && is_joiner(&info[limit])) limit++; + base = start; + has_reph = true; + } + } else if (d->config->reph_mode == REPH_LOG_REPHA && info[start].category == VEGA_OT_REPHA) { + limit += 1; + while (limit < end && is_joiner(&info[limit])) limit++; + base = start; + has_reph = true; + } + { + size_t i = end; + bool seen_below = false; + do { + i--; + if (is_consonant(&info[i])) { + if (info[i].position != VEGA_POS_BELOW_C && (info[i].position != VEGA_POS_POST_C || seen_below)) { + base = i; + break; + } + if (info[i].position == VEGA_POS_BELOW_C) seen_below = true; + base = i; + } else if (start < i && info[i].category == VEGA_OT_ZWJ && info[i - 1].category == VEGA_OT_H) + break; /* a ZWJ after a halant asks for a half form: the search stops */ + } while (i > limit); + } + if (has_reph && base == start && limit - base <= 2) has_reph = false; /* Ra alone is the base */ + + /* 2 and 3 (matras decomposed, marks in canonical order) normalization + * did. Reorder: */ + for (size_t i = start; i < base; ++i) + if (info[i].position > VEGA_POS_PRE_C) info[i].position = VEGA_POS_PRE_C; + if (base < end) info[base].position = VEGA_POS_BASE_C; + if (has_reph) info[start].position = VEGA_POS_RA_TO_BECOME_REPH; + + /* Old specifications: the first post-base halant after the last + * consonant (Kannada not past a halant already there). */ + if (d->old_spec) { + bool disallow_double_halants = script == TAG4('K', 'n', 'd', 'a'); + for (size_t i = base + 1; i < end; ++i) + if (info[i].category == VEGA_OT_H) { + size_t j; + for (j = end - 1; j > i; j--) + if (is_consonant(&info[j]) || (disallow_double_halants && info[j].category == VEGA_OT_H)) break; + if (info[j].category != VEGA_OT_H && j > i) { + Info t = info[i]; + memmove(&info[i], &info[i + 1], (j - i) * sizeof(Info)); + info[j] = t; + } + break; + } + } + + /* Joiners, nuktas, register shifters, medials and halants go with what + * is before them (a halant not with a left matra). */ + { + uint8_t last_pos = VEGA_POS_START; + for (size_t i = start; i < end; ++i) { + if (FLAG_UNSAFE(info[i].category) & (FLAG(VEGA_OT_ZWJ) | FLAG(VEGA_OT_ZWNJ) | FLAG(VEGA_OT_N) | + FLAG(VEGA_OT_RS) | FLAG(VEGA_OT_CM) | FLAG(VEGA_OT_H))) { + info[i].position = last_pos; + if (info[i].category == VEGA_OT_H && info[i].position == VEGA_POS_PRE_M) + for (size_t j = i; j > start; j--) + if (info[j - 1].position != VEGA_POS_PRE_M) { + info[i].position = info[j - 1].position; + break; + } + } else if (info[i].position != VEGA_POS_SMVD) { + if (info[i].category == VEGA_OT_MPST && i > start && info[i - 1].category == VEGA_OT_SM) + info[i - 1].position = info[i].position; + last_pos = info[i].position; + } + } + } + /* A post-base consonant owns what is before it since the last + * consonant or matra. */ + { + size_t last = base; + for (size_t i = base + 1; i < end; ++i) + if (is_consonant(&info[i])) { + for (size_t j = last + 1; j < i; ++j) + if (info[j].position < VEGA_POS_SMVD) info[j].position = info[i].position; + last = i; + } else if (FLAG_UNSAFE(info[i].category) & (FLAG(VEGA_OT_M) | FLAG(VEGA_OT_MPST))) + last = i; + } + + { + /* Sorted by position, stably; syllable holds each glyph's place + * before, to merge the clusters of what moved past base. */ + uint8_t syllable = info[start].syllable; + for (size_t i = start; i < end; ++i) info[i].syllable = (uint8_t)(i - start); + for (size_t i = start + 1; i < end; ++i) { + size_t j = i; + while (j > start && info[j - 1].position > info[i].position) j--; + if (j == i) continue; + Info t = info[i]; + memmove(&info[j + 1], &info[j], (i - j) * sizeof(Info)); + info[j] = t; + } + /* The base again; a run of left matras back in their order. */ + size_t first_left_matra = end, last_left_matra = end; + base = end; + for (size_t i = start; i < end; ++i) { + if (info[i].position == VEGA_POS_BASE_C) { + base = i; + break; + } + if (info[i].position == VEGA_POS_PRE_M) { + if (first_left_matra == end) first_left_matra = i; + last_left_matra = i; + } + } + if (first_left_matra < last_left_matra) { + reverse_range(b, first_left_matra, last_left_matra + 1, false); + size_t i = first_left_matra; + for (size_t j = i; j <= last_left_matra; ++j) + if (FLAG_UNSAFE(info[j].category) & (FLAG(VEGA_OT_M) | FLAG(VEGA_OT_MPST))) { + reverse_range(b, i, j + 1, false); + i = j + 1; + } + } + if (d->old_spec || end - start > 127) + merge_clusters(b, base, end); + else + for (size_t i = base; i < end; ++i) + if (info[i].syllable != 255) { + size_t min = i, max = i, j = start + info[i].syllable; + while (j != i) { + if (j < min) min = j; + if (j > max) max = j; + size_t next = start + info[j].syllable; + info[j].syllable = 255; + j = next; + } + merge_clusters(b, base > min ? base : min, max + 1); + } + for (size_t i = start; i < end; ++i) info[i].syllable = syllable; + } + + /* Masks: reph; pre-base (half, and blwf in new specifications that + * have it there); post-base. */ + for (size_t i = start; i < end && info[i].position == VEGA_POS_RA_TO_BECOME_REPH; ++i) + info[i].mask |= masks[VEGA_MASK_RPHF]; + uint32_t mask = masks[VEGA_MASK_HALF]; + if (!d->old_spec && !d->config->blwf_post_only) mask |= masks[VEGA_MASK_BLWF]; + for (size_t i = start; i < base; ++i) info[i].mask |= mask; + mask = masks[VEGA_MASK_BLWF] | masks[VEGA_MASK_ABVF] | masks[VEGA_MASK_PSTF]; + for (size_t i = base + 1; i < end; ++i) info[i].mask |= mask; + + /* Old-spec Devanagari: an eyelash Ra below a half form too, but not + * with a ZWJ. */ + if (d->old_spec && script == TAG4('D', 'e', 'v', 'a')) + for (size_t i = start; i + 1 < base; ++i) + if (info[i].category == VEGA_OT_RA && info[i + 1].category == VEGA_OT_H && + (i + 2 == base || info[i + 2].category != VEGA_OT_ZWJ)) { + info[i].mask |= masks[VEGA_MASK_BLWF]; + info[i + 1].mask |= masks[VEGA_MASK_BLWF]; + } + + /* A halant, Ra the font reorders before the base: marked for pref. */ + if (masks[VEGA_MASK_PREF] && base + 2 < end) + for (size_t i = base + 1; i + 1 < end; ++i) { + uint32_t glyphs[2] = {info[i].codepoint, info[i + 1].codepoint}; + if (indic_would(d, VEGA_STAGE_PREF, glyphs, 2)) { + info[i].mask |= masks[VEGA_MASK_PREF]; + info[i + 1].mask |= masks[VEGA_MASK_PREF]; + break; + } + } + + /* A ZWNJ keeps what is before it back to a consonant from half forms. */ + for (size_t i = start + 1; i < end; ++i) + if (is_joiner(&info[i])) { + bool non_joiner = info[i].category == VEGA_OT_ZWNJ; + size_t j = i; + do { + j--; + if (non_joiner) info[j].mask &= ~masks[VEGA_MASK_HALF]; + } while (j > start && !is_consonant(&info[j])); + } +} + +static bool indic_initial_reordering(const Indic *d, VegaShaper *b) { + if (d->virama) + for (size_t i = 0; i < b->len; ++i) + if (b->info[i].position == VEGA_POS_BASE_C) b->info[i].position = consonant_position(d, b->info[i].codepoint); + bool changed = insert_dotted_circles(d->s, b, 4, VEGA_OT_DOTTEDCIRCLE, VEGA_OT_REPHA, VEGA_POS_END); + for (size_t start = 0, end; start < b->len; start = end) { + end = next_syllable(b, start); + /* Vowel syllables look like consonant ones, and broken ones have + * their dotted circle, like a standalone cluster's. */ + unsigned type = b->info[start].syllable & 0x0F; + if (type <= 2 || type == 4) indic_reorder_consonant_syllable(d, b, start, end); + } + return changed; +} + +/* Categories of the preceding character that keep a left matra from + * 'init': HarfBuzz's Cf to Mn. */ +static bool word_character(unsigned c) { + switch (c) { + case VEGA_CF: case VEGA_CN: case VEGA_CO: case VEGA_CS: case VEGA_LL: case VEGA_LM: case VEGA_LO: case VEGA_LT: + case VEGA_LU: case VEGA_MC: case VEGA_ME: case VEGA_MN: + return true; + default: + return false; + } +} + +static void indic_final_syllable(const Indic *d, VegaShaper *b, size_t start, size_t end) { + Info *info = b->info; + const uint32_t *masks = d->plan->masks; + uint32_t script = d->plan->script; + bool malayalam_or_tamil = script == TAG4('M', 'l', 'y', 'm') || script == TAG4('T', 'a', 'm', 'l'); + /* A virama that ligated and multiplied back out is a halant again. */ + if (d->virama) + for (size_t i = start; i < end; ++i) + if (info[i].codepoint == d->virama && (info[i].gprops & LIGATED) && (info[i].gprops & MULTIPLIED)) { + info[i].category = VEGA_OT_H; + info[i].gprops &= (uint16_t)~(LIGATED | MULTIPLIED); + } + + /* 4. The base again: where a pref candidate formed nothing, or (in + * Malayalam) past unformed below-base forms. */ + bool try_pref = masks[VEGA_MASK_PREF] != 0; + size_t base; + for (base = start; base < end; base++) + if (info[base].position >= VEGA_POS_BASE_C) { + if (try_pref && base + 1 < end) + for (size_t i = base + 1; i < end; ++i) + if (info[i].mask & masks[VEGA_MASK_PREF]) { + if (!((info[i].gprops & SUBSTITUTED) && ligated_and_didnt_multiply(&info[i]))) { + base = i; + while (base < end && is_halant(&info[base])) base++; + if (base < end) info[base].position = VEGA_POS_BASE_C; + try_pref = false; + } + break; + } + if (script == TAG4('M', 'l', 'y', 'm')) + for (size_t i = base + 1; i < end; ++i) { + while (i < end && is_joiner(&info[i])) i++; + if (i == end || !is_halant(&info[i])) break; + i++; + while (i < end && is_joiner(&info[i])) i++; + if (i < end && is_consonant(&info[i]) && info[i].position == VEGA_POS_BELOW_C) { + base = i; + info[base].position = VEGA_POS_BASE_C; + } + } + if (start < base && info[base].position > VEGA_POS_BASE_C) base--; + break; + } + if (base == end && start < base && is_one_of(&info[base - 1], FLAG(VEGA_OT_ZWJ))) base--; + if (base < end) + while (start < base && is_one_of(&info[base], FLAG(VEGA_OT_N) | FLAG(VEGA_OT_H))) base--; + + /* Pre-base matras: after the last halant before the base (not one a + * ZWJ follows, nor the matra's own), else where they are. */ + if (start + 1 < end && start < base) { + size_t new_pos = base == end ? base - 2 : base - 1; + if (!malayalam_or_tamil) { + for (;;) { + while (new_pos > start && + !is_one_of(&info[new_pos], FLAG(VEGA_OT_M) | FLAG(VEGA_OT_MPST) | FLAG(VEGA_OT_H))) + new_pos--; + if (is_halant(&info[new_pos]) && info[new_pos].position != VEGA_POS_PRE_M) { + if (new_pos + 1 < end && info[new_pos + 1].category == VEGA_OT_ZWJ && new_pos > start) { + new_pos--; + continue; /* keep searching */ + } + } else + new_pos = start; + break; + } + } + if (start < new_pos && info[new_pos].position != VEGA_POS_PRE_M) { + for (size_t i = new_pos; i > start; i--) + if (info[i - 1].position == VEGA_POS_PRE_M) { + size_t old_pos = i - 1; + if (old_pos < base && base <= new_pos) base--; + Info t = info[old_pos]; + memmove(&info[old_pos], &info[old_pos + 1], (new_pos - old_pos) * sizeof(Info)); + info[new_pos] = t; + merge_clusters(b, new_pos, end < base + 1 ? end : base + 1); + new_pos--; + } + } else + for (size_t i = start; i < base; ++i) + if (info[i].position == VEGA_POS_PRE_M) { + merge_clusters(b, i, end < base + 1 ? end : base + 1); + break; + } + } + + /* Reph: moved only if it formed (or, encoded as a repha, if it did + * not ligate), to where the script puts it. */ + if (start + 1 < end && info[start].position == VEGA_POS_RA_TO_BECOME_REPH && + ((info[start].category == VEGA_OT_REPHA) ^ ligated_and_didnt_multiply(&info[start]))) { + size_t new_reph_pos = start; + unsigned reph_pos = d->config->reph_pos; + bool found = false; + if (reph_pos != VEGA_POS_AFTER_POST) { + /* 2. after the first explicit halant before the base (and a + * joiner after it) */ + new_reph_pos = start + 1; + while (new_reph_pos < base && !is_halant(&info[new_reph_pos])) new_reph_pos++; + if (new_reph_pos < base && is_halant(&info[new_reph_pos])) { + if (new_reph_pos + 1 < base && is_joiner(&info[new_reph_pos + 1])) new_reph_pos++; + found = true; + } + /* 3. after the main consonant */ + if (!found && reph_pos == VEGA_POS_AFTER_MAIN) { + new_reph_pos = base; + while (new_reph_pos + 1 < end && info[new_reph_pos + 1].position <= VEGA_POS_AFTER_MAIN) new_reph_pos++; + found = new_reph_pos < end; + } + /* 4. before the first post-base consonant or matra */ + if (!found && reph_pos == VEGA_POS_AFTER_SUB) { + new_reph_pos = base; + while (new_reph_pos + 1 < end && + !(FLAG_UNSAFE(info[new_reph_pos + 1].position) & + (FLAG(VEGA_POS_POST_C) | FLAG(VEGA_POS_AFTER_POST) | FLAG(VEGA_POS_SMVD)))) + new_reph_pos++; + found = new_reph_pos < end; + } + } + if (!found) { + /* 5. as step 2 */ + new_reph_pos = start + 1; + while (new_reph_pos < base && !is_halant(&info[new_reph_pos])) new_reph_pos++; + if (new_reph_pos < base && is_halant(&info[new_reph_pos])) { + if (new_reph_pos + 1 < base && is_joiner(&info[new_reph_pos + 1])) new_reph_pos++; + found = true; + } + } + if (!found) { + /* 6. the end, before any syllable modifiers; before a halant + * that follows a matra */ + new_reph_pos = end - 1; + while (new_reph_pos > start && info[new_reph_pos].position == VEGA_POS_SMVD) new_reph_pos--; + if (is_halant(&info[new_reph_pos])) + for (size_t i = base + 1; i < new_reph_pos; ++i) + if (FLAG_UNSAFE(info[i].category) & (FLAG(VEGA_OT_M) | FLAG(VEGA_OT_MPST))) new_reph_pos--; + } + merge_clusters(b, start, new_reph_pos + 1); + Info reph = info[start]; + memmove(&info[start], &info[start + 1], (new_reph_pos - start) * sizeof(Info)); + info[new_reph_pos] = reph; + if (start < base && base <= new_reph_pos) base--; + } + + /* A pre-base-reordering consonant that pref formed: before the base, + * where a pre-base matra would go. */ + if (try_pref && base + 1 < end) + for (size_t i = base + 1; i < end; ++i) + if (info[i].mask & masks[VEGA_MASK_PREF]) { + if (ligated_and_didnt_multiply(&info[i])) { + size_t new_pos = base; + if (!malayalam_or_tamil) + while (new_pos > start && + !is_one_of(&info[new_pos - 1], FLAG(VEGA_OT_M) | FLAG(VEGA_OT_MPST) | FLAG(VEGA_OT_H))) + new_pos--; + if (new_pos > start && is_halant(&info[new_pos - 1]) && new_pos < end && is_joiner(&info[new_pos])) + new_pos++; + size_t old_pos = i; + merge_clusters(b, new_pos, old_pos + 1); + Info t = info[old_pos]; + memmove(&info[new_pos + 1], &info[new_pos], (old_pos - new_pos) * sizeof(Info)); + info[new_pos] = t; + if (new_pos <= base && base < old_pos) base++; + } + break; + } + + /* 'init' on a left matra that starts a word. */ + if (info[start].position == VEGA_POS_PRE_M && (!start || !word_character(gen_cat(&info[start - 1])))) + info[start].mask |= masks[VEGA_MASK_INDIC_INIT]; +} + +static void indic_final_reordering(const Indic *d, VegaShaper *b) { + for (size_t start = 0, end; start < b->len; start = end) { + end = next_syllable(b, start); + indic_final_syllable(d, b, start, end); + } +} + +/* The shaper's part of the work at a pause in GSUB; whether it changed the + * buffer. */ +static bool shaper_pause(const VegaShaping *s, const Plan *plan, VegaShaper *b, unsigned pause) { + if (plan->shaper != VEGA_SHAPER_INDIC) return false; + Indic d = indic_of(s, plan); + switch (pause) { + case VEGA_PAUSE_SYLLABLES: find_syllables(b, &vega_indic_machine); return false; + case VEGA_PAUSE_INITIAL_REORDERING: return indic_initial_reordering(&d, b); + case VEGA_PAUSE_FINAL_REORDERING: indic_final_reordering(&d, b); return false; + default: return false; + } +} + /* ---- Positions */ static void fallback_spaces(const VegaShaping *s, VegaShaper *b) { @@ -2449,7 +3263,7 @@ static void position(const VegaShaping *s, const Plan *plan, VegaShaper *b) { for (size_t i = 0; i < b->len; ++i) b->pos[i].x_advance = advance_of(s, b->info[i].codepoint); if (b->scratch & HAS_SPACE_FALLBACK) fallback_spaces(s, b); bool adjust = (plan->flags & VEGA_PLAN_ADJUST_ZEROING) && b->direction == VEGA_SHAPE_LTR; - if (plan->flags & VEGA_PLAN_GPOS) apply_table(s, b, 1, plan->gpos, plan->gpos_count); + if (plan->flags & VEGA_PLAN_GPOS) apply_table(s, plan, b, 1); if (plan->flags & VEGA_PLAN_KERN) kern_table(s, b, plan); if (plan->flags & VEGA_PLAN_ZERO_MARKS) for (size_t i = 0; i < b->len; ++i) @@ -2484,13 +3298,14 @@ long vega_shape(VegaShaper *b, const VegaShaping *s, const uint32_t *codes, size b->max_len = count * 64 > 16384 ? count * 64 : 16384; b->max_ops = (long)(count * 1024 > 16384 ? count * 1024 : 16384); if (!ensure(b, count + 1)) return -1; - for (size_t i = 0; i < count; ++i) b->info[i] = (Info){codes[i], plan.global_mask, (uint32_t)i, 0, 0, 0, 0, 0}; + for (size_t i = 0; i < count; ++i) b->info[i] = (Info){.codepoint = codes[i], .mask = plan.global_mask, .cluster = (uint32_t)i}; b->len = count; set_unicode_props(b); form_clusters(b); ensure_native_direction(b, &plan); if (plan.shaper == VEGA_SHAPER_HANGUL) preprocess_hangul(s, b); + if (plan.shaper == VEGA_SHAPER_INDIC) preprocess_vowel_constraints(&plan, b); /* Mirror what the font mirrors; mark the rest for rtlm. */ if (direction == VEGA_SHAPE_RTL) @@ -2502,6 +3317,7 @@ long vega_shape(VegaShaper *b, const VegaShaping *s, const uint32_t *codes, size normalize(s, &plan, b); setup_masks_fraction(b, &plan); if (plan.shaper == VEGA_SHAPER_ARABIC) setup_masks_arabic(b, &plan); + if (plan.shaper == VEGA_SHAPER_INDIC) setup_masks_indic(b); if (plan.shaper == VEGA_SHAPER_HANGUL) for (size_t i = 0; i < b->len; ++i) if (b->info[i].action >= LJMO && b->info[i].action <= TJMO) @@ -2517,7 +3333,7 @@ long vega_shape(VegaShaper *b, const VegaShaping *s, const uint32_t *codes, size if (plan.flags & VEGA_PLAN_GUESS_CLASSES) info->gprops = gen_cat(info) != VEGA_MN || is_default_ignorable(info) ? BASE_GLYPH : MARK; } - apply_table(s, b, 0, plan.gsub, plan.gsub_count); + apply_table(s, &plan, b, 0); if (!b->successful || !ensure(b, b->len)) return -1; position(s, &plan, b); |
