aboutsummaryrefslogtreecommitdiffstats
path: root/text/shape.c
diff options
context:
space:
mode:
authoreunhhu <[email protected]>2026-09-28 14:53:40 +0100
committereunhhu <[email protected]>2026-09-28 14:53:40 +0100
commit9bb85cffb10e0d4bc007406d1487a6dede3db975 (patch)
treeed8058b7ede0702391fafec91e4985339b30e4b4 /text/shape.c
parent60a3c0232bbca8d5d8c1107ab8197d272212ef42 (diff)
downloadvega-9bb85cffb10e0d4bc007406d1487a6dede3db975.tar.gz
vega-9bb85cffb10e0d4bc007406d1487a6dede3db975.zip
Shape Khmer and Myanmar as HarfBuzz does
Two more shapers on the syllabic groundwork, from HarfBuzz 10.2's source. Khmer reorders before any lookup: a coeng and Ro go before the base, masked for pref, what follows them for cfar, a left matra goes first; split matras decompose with U+17C1. Myanmar finds the base after a kinzi, gives each glyph its place around it and sorts by place, and zeroes mark advances before GPOS rather than after. Noto Sans Khmer and Noto Sans Myanmar shape as HarfBuzz does at 20,000 runs a plan, and so do Unifont's Khmer and Myanmar (no GSUB).
Diffstat (limited to 'text/shape.c')
-rw-r--r--text/shape.c194
1 files changed, 175 insertions, 19 deletions
diff --git a/text/shape.c b/text/shape.c
index 3443b0b..214d73b 100644
--- a/text/shape.c
+++ b/text/shape.c
@@ -227,7 +227,7 @@ bool vega_shaping_open(VegaShaping *s, const unsigned char *bytes, size_t size,
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 + 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)
+ if ((p[4] != VEGA_SHAPE_LTR && p[4] != VEGA_SHAPE_RTL) || p[5] > VEGA_SHAPE_RTL || p[6] > VEGA_SHAPER_MYANMAR)
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) {
@@ -823,6 +823,7 @@ static bool compose(const Normalizer *c, uint32_t a, uint32_t b, uint32_t *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);
+ case VEGA_SHAPER_KHMER: return !is_mark_category(vega_category(a)) && compose_unicode(a, b, ab);
default: return compose_unicode(a, b, ab);
}
}
@@ -845,6 +846,13 @@ static void next_char(VegaShaper *b, uint32_t glyph) {
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;
+ /* Khmer's split matras, which Unicode does not decompose */
+ if (c->plan->shaper == VEGA_SHAPER_KHMER &&
+ (ab == 0x17BE || ab == 0x17BF || ab == 0x17C0 || ab == 0x17C4 || ab == 0x17C5)) {
+ *a = 0x17C1;
+ *b = ab;
+ return true;
+ }
return decompose_unicode(ab, a, b);
}
@@ -1026,7 +1034,9 @@ static void normalize(const VegaShaping *s, const Plan *plan, VegaShaper *b) {
* 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 always_short_circuit = none, might_short_circuit = none || plan->shaper != VEGA_SHAPER_INDIC;
+ const bool syllabic = plan->shaper == VEGA_SHAPER_INDIC || plan->shaper == VEGA_SHAPER_KHMER ||
+ plan->shaper == VEGA_SHAPER_MYANMAR;
+ const bool always_short_circuit = none, might_short_circuit = none || !syllabic;
bool all_simple = true;
clear_output(b);
size_t count = b->len;
@@ -2574,11 +2584,12 @@ static size_t next_syllable(const VegaShaper *b, size_t start) {
/* 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) {
+ int 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};
+ Info circle = {.codepoint = glyph, .uprops = VEGA_CC, .position = (uint8_t)(position >= 0 ? position : 0),
+ .category = (uint8_t)category};
clear_output(b);
unsigned last = 0;
while (b->idx < b->len && b->successful) {
@@ -3186,15 +3197,153 @@ static void indic_final_reordering(const Indic *d, VegaShaper *b) {
}
}
+/* ---- Khmer (hb-ot-shaper-khmer.cc): a coeng and Ro moved before the base
+ * and masked for pref, what follows them for cfar; a left matra moved
+ * first; the post-base features on all but the first glyph. */
+
+static void setup_masks_category(VegaShaper *b) {
+ for (size_t i = 0; i < b->len; ++i) b->info[i].category = vega_indic_categories(b->info[i].codepoint) & 0xFF;
+}
+
+static void khmer_reorder_syllable(const Plan *plan, VegaShaper *b, size_t start, size_t end) {
+ Info *info = b->info;
+ const uint32_t *masks = plan->masks;
+ uint32_t post = masks[VEGA_MASK_KHMER_BLWF] | masks[VEGA_MASK_KHMER_ABVF] | masks[VEGA_MASK_KHMER_PSTF];
+ for (size_t i = start + 1; i < end; ++i) info[i].mask |= post;
+ unsigned coengs = 0;
+ for (size_t i = start + 1; i < end; ++i)
+ if (info[i].category == VEGA_OT_H && coengs <= 2 && i + 1 < end) {
+ coengs++;
+ if (info[i + 1].category == VEGA_OT_RA) {
+ info[i].mask |= masks[VEGA_MASK_KHMER_PREF];
+ info[i + 1].mask |= masks[VEGA_MASK_KHMER_PREF];
+ merge_clusters(b, start, i + 2);
+ Info t0 = info[i], t1 = info[i + 1];
+ memmove(&info[start + 2], &info[start], (i - start) * sizeof(Info));
+ info[start] = t0;
+ info[start + 1] = t1;
+ if (masks[VEGA_MASK_KHMER_CFAR])
+ for (size_t j = i + 2; j < end; ++j) info[j].mask |= masks[VEGA_MASK_KHMER_CFAR];
+ coengs = 2;
+ }
+ } else if (info[i].category == VEGA_OT_VPRE) {
+ merge_clusters(b, start, i + 1);
+ Info t = info[i];
+ memmove(&info[start + 1], &info[start], (i - start) * sizeof(Info));
+ info[start] = t;
+ }
+}
+
+static bool khmer_reorder(const VegaShaping *s, const Plan *plan, VegaShaper *b) {
+ bool changed = insert_dotted_circles(s, b, 1, VEGA_OT_DOTTEDCIRCLE, -1, -1);
+ for (size_t start = 0, end; start < b->len; start = end) {
+ end = next_syllable(b, start);
+ if ((b->info[start].syllable & 0x0F) <= 1) khmer_reorder_syllable(plan, b, start, end);
+ }
+ return changed;
+}
+
+/* ---- Myanmar (hb-ot-shaper-myanmar.cc): the base (after a kinzi), each
+ * glyph given its place around it, and the syllable sorted by place. */
+
+#define CONSONANT_FLAGS_MYANMAR \
+ (FLAG(VEGA_OT_C) | FLAG(VEGA_OT_CS) | FLAG(VEGA_OT_RA) | FLAG(VEGA_OT_V) | FLAG(VEGA_OT_PLACEHOLDER) | \
+ FLAG(VEGA_OT_DOTTEDCIRCLE))
+
+/* hb_buffer_t::sort by position: stable; a glyph that moves back merges
+ * the clusters it passes. */
+static void sort_by_position(VegaShaper *b, size_t start, size_t end) {
+ for (size_t i = start + 1; i < end; ++i) {
+ size_t j = i;
+ while (j > start && b->info[j - 1].position > b->info[i].position) j--;
+ if (i == j) continue;
+ merge_clusters(b, j, i + 1);
+ Info t = b->info[i];
+ memmove(&b->info[j + 1], &b->info[j], (i - j) * sizeof(Info));
+ b->info[j] = t;
+ }
+}
+
+static void myanmar_reorder_syllable(VegaShaper *b, size_t start, size_t end) {
+ Info *info = b->info;
+ size_t base = end, limit = start;
+ bool has_reph = false;
+ if (start + 3 <= end && info[start].category == VEGA_OT_RA && info[start + 1].category == VEGA_OT_AS &&
+ info[start + 2].category == VEGA_OT_H) {
+ limit += 3;
+ base = start;
+ has_reph = true;
+ }
+ if (!has_reph) base = limit;
+ for (size_t i = limit; i < end; ++i)
+ if (is_one_of(&info[i], CONSONANT_FLAGS_MYANMAR)) {
+ base = i;
+ break;
+ }
+ size_t i = start;
+ for (; i < start + (has_reph ? 3 : 0); ++i) info[i].position = VEGA_POS_AFTER_MAIN;
+ for (; i < base; ++i) info[i].position = VEGA_POS_PRE_C;
+ if (i < end) info[i++].position = VEGA_POS_BASE_C;
+ uint8_t pos = VEGA_POS_AFTER_MAIN;
+ for (; i < end; ++i) {
+ unsigned c = info[i].category;
+ if (c == VEGA_OT_MR) info[i].position = VEGA_POS_PRE_C; /* pre-base reordering */
+ else if (c == VEGA_OT_VPRE) info[i].position = VEGA_POS_PRE_M; /* left matra */
+ else if (c == VEGA_OT_VS) info[i].position = info[i - 1].position;
+ else if (pos == VEGA_POS_AFTER_MAIN && c == VEGA_OT_VBLW) info[i].position = pos = VEGA_POS_BELOW_C;
+ else if (pos == VEGA_POS_BELOW_C && c == VEGA_OT_A) info[i].position = VEGA_POS_BEFORE_SUB;
+ else if (pos == VEGA_POS_BELOW_C && c == VEGA_OT_VBLW) info[i].position = pos;
+ else if (pos == VEGA_POS_BELOW_C) info[i].position = pos = VEGA_POS_AFTER_SUB;
+ else info[i].position = pos;
+ }
+ sort_by_position(b, start, end);
+ /* A run of left matras back in their order (variation selectors after
+ * theirs). */
+ size_t first_left_matra = end, last_left_matra = end;
+ for (size_t k = start; k < end; ++k)
+ if (info[k].position == VEGA_POS_PRE_M) {
+ if (first_left_matra == end) first_left_matra = k;
+ last_left_matra = k;
+ }
+ if (first_left_matra < last_left_matra) {
+ reverse_range(b, first_left_matra, last_left_matra + 1, false);
+ size_t k = first_left_matra;
+ for (size_t j = k; j <= last_left_matra; ++j)
+ if (info[j].category == VEGA_OT_VPRE) {
+ reverse_range(b, k, j + 1, false);
+ k = j + 1;
+ }
+ }
+}
+
+static bool myanmar_reorder(const VegaShaping *s, VegaShaper *b) {
+ bool changed = insert_dotted_circles(s, b, 1, VEGA_OT_DOTTEDCIRCLE, -1, -1);
+ for (size_t start = 0, end; start < b->len; start = end) {
+ end = next_syllable(b, start);
+ if ((b->info[start].syllable & 0x0F) <= 1) myanmar_reorder_syllable(b, start, end);
+ }
+ return changed;
+}
+
/* 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;
+ switch (plan->shaper) {
+ case VEGA_SHAPER_INDIC: {
+ 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;
+ }
+ }
+ case VEGA_SHAPER_KHMER:
+ if (pause == VEGA_PAUSE_SYLLABLES) find_syllables(b, &vega_khmer_machine);
+ return pause == VEGA_PAUSE_INITIAL_REORDERING && khmer_reorder(s, plan, b);
+ case VEGA_SHAPER_MYANMAR:
+ if (pause == VEGA_PAUSE_SYLLABLES) find_syllables(b, &vega_myanmar_machine);
+ return pause == VEGA_PAUSE_INITIAL_REORDERING && myanmar_reorder(s, b);
default: return false;
}
}
@@ -3256,6 +3405,17 @@ static void propagate_attachment_offsets(Position *pos, size_t len, size_t i, in
}
}
+static void zero_mark_widths(VegaShaper *b, bool adjust) {
+ for (size_t i = 0; i < b->len; ++i)
+ if (b->info[i].gprops & MARK) {
+ if (adjust) {
+ b->pos[i].x_offset -= b->pos[i].x_advance;
+ b->pos[i].y_offset -= b->pos[i].y_advance;
+ }
+ b->pos[i].x_advance = b->pos[i].y_advance = 0;
+ }
+}
+
static void position(const VegaShaping *s, const Plan *plan, VegaShaper *b) {
b->have_output = false;
b->out_len = 0;
@@ -3263,17 +3423,12 @@ 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;
+ /* Marks' advances go by GDEF: before GPOS (Myanmar's shaper) or after. */
+ bool early = plan->shaper == VEGA_SHAPER_MYANMAR;
+ if ((plan->flags & VEGA_PLAN_ZERO_MARKS) && early) zero_mark_widths(b, adjust);
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)
- if (b->info[i].gprops & MARK) {
- if (adjust) {
- b->pos[i].x_offset -= b->pos[i].x_advance;
- b->pos[i].y_offset -= b->pos[i].y_advance;
- }
- b->pos[i].x_advance = b->pos[i].y_advance = 0;
- }
+ if ((plan->flags & VEGA_PLAN_ZERO_MARKS) && !early) zero_mark_widths(b, adjust);
if (b->scratch & HAS_DEFAULT_IGNORABLES)
for (size_t i = 0; i < b->len; ++i)
if (is_default_ignorable(&b->info[i]))
@@ -3318,6 +3473,7 @@ long vega_shape(VegaShaper *b, const VegaShaping *s, const uint32_t *codes, size
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_KHMER || plan.shaper == VEGA_SHAPER_MYANMAR) setup_masks_category(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)