From 7c788eb9d89a988b69305e544a325a5bf883a751 Mon Sep 17 00:00:00 2001 From: minjaesong Date: Sat, 28 Feb 2026 05:55:09 +0900 Subject: [PATCH] diacritics pos for glyphs sans explicit anchor tags --- OTFbuild/calligra_font_tests.odt | Bin 15232 -> 15239 bytes OTFbuild/opentype_features.py | 109 ++++++++++++++++++++++--------- 2 files changed, 78 insertions(+), 31 deletions(-) diff --git a/OTFbuild/calligra_font_tests.odt b/OTFbuild/calligra_font_tests.odt index f6cd61ba0750993cc391d8b9d5d4c8f7051d479e..b6ff3c6b23d018d9ecb5a9316cb69006bb857c53 100644 GIT binary patch delta 8083 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 delta 8058 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 diff --git a/OTFbuild/opentype_features.py b/OTFbuild/opentype_features.py index 2f12232..c873d0d 100644 --- a/OTFbuild/opentype_features.py +++ b/OTFbuild/opentype_features.py @@ -1244,8 +1244,22 @@ def _generate_sundanese(glyphs, has): def _generate_mark(glyphs, has): """ Generate GPOS mark-to-base positioning using diacritics anchors from tag column. + + Marks are grouped by (writeOnTop, alignment) into separate mark classes + and lookups. Different alignments need different default base anchor + positions to match the Kotlin engine's fallback behaviour: + - ALIGN_CENTRE: base anchor at width/2 + - ALIGN_RIGHT: base anchor at width + - ALIGN_LEFT/BEFORE: base anchor at 0 (mark sits at base origin) """ - bases_with_anchors = {} + # Collect ALL non-mark glyphs as potential bases (excluding CJK + # ideographs and Braille which are unlikely to receive combining marks + # and would bloat the GPOS table). + _EXCLUDE_RANGES = ( + range(0x3400, 0xA000), # CJK Unified Ideographs (Ext A + main) + range(0x2800, 0x2900), # Braille + ) + all_bases = {} marks = {} for cp, g in glyphs.items(): @@ -1253,33 +1267,37 @@ def _generate_mark(glyphs, has): continue if g.props.write_on_top >= 0: marks[cp] = g - elif any(a.x_used or a.y_used for a in g.props.diacritics_anchors): - bases_with_anchors[cp] = g + elif g.bitmap and g.props.width > 0: + if not any(cp in r for r in _EXCLUDE_RANGES): + all_bases[cp] = g - if not bases_with_anchors or not marks: + if not all_bases or not marks: return "" lines = [] - # Group marks by writeOnTop type - mark_classes = {} - for cp, g in marks.items(): - mark_type = g.props.write_on_top - if mark_type not in mark_classes: - mark_classes[mark_type] = [] - mark_classes[mark_type].append((cp, g)) + _align_suffix = { + SC.ALIGN_LEFT: 'l', + SC.ALIGN_RIGHT: 'r', + SC.ALIGN_CENTRE: 'c', + SC.ALIGN_BEFORE: 'b', + } - for mark_type, mark_list in sorted(mark_classes.items()): - class_name = f"@mark_type{mark_type}" + # Group marks by (writeOnTop, alignment) for separate mark classes. + mark_groups = {} # (mark_type, align) -> [(cp, g), ...] + for cp, g in marks.items(): + key = (g.props.write_on_top, g.props.align_where) + mark_groups.setdefault(key, []).append((cp, g)) + + # Emit markClass definitions + for (mark_type, align), mark_list in sorted(mark_groups.items()): + suffix = _align_suffix.get(align, 'x') + class_name = f"@mark_t{mark_type}_{suffix}" for cp, g in mark_list: - if g.props.align_where == SC.ALIGN_CENTRE: + if align == SC.ALIGN_CENTRE: # Match Kotlin: anchorPoint - HALF_VAR_INIT centres the # cell on the anchor. For U+0900-0902 the Kotlin engine # uses (W_VAR_INIT + 1) / 2 instead (1 px nudge left). - # mark_x must match font_builder's total x_offset - # (alignment + nudge) so column `half` sits on the anchor. - # The Kotlin engine always uses W_VAR_INIT for alignment, - # even for EXTRAWIDE sheets. bm_cols = SC.W_VAR_INIT if 0x0900 <= cp <= 0x0902: half = (SC.W_VAR_INIT + 1) // 2 @@ -1288,28 +1306,57 @@ def _generate_mark(glyphs, has): x_offset = math.ceil((g.props.width - bm_cols) / 2) * SC.SCALE x_offset -= g.props.nudge_x * SC.SCALE mark_x = x_offset + half * SC.SCALE + elif align == SC.ALIGN_RIGHT: + # Match Kotlin: mark at base anchor - W_VAR_INIT. + # The contour x_offset already includes (width - W_VAR_INIT), + # so mark_x just needs the nudge_x component. + mark_x = g.props.nudge_x * SC.SCALE else: - mark_x = ((g.props.width + 1) // 2) * SC.SCALE + # ALIGN_LEFT / ALIGN_BEFORE: mark sits at base origin. + mark_x = 0 mark_y = SC.ASCENT lines.append( f"markClass {glyph_name(cp)} {class_name};" ) - # Define lookups at top level so they can be referenced from - # multiple script registrations in the mark feature. + # Generate one lookup per (mark_type, align) group. lookup_names = [] - for mark_type, mark_list in sorted(mark_classes.items()): - class_name = f"@mark_type{mark_type}" - lookup_name = f"mark_type{mark_type}" + for (mark_type, align), mark_list in sorted(mark_groups.items()): + suffix = _align_suffix.get(align, 'x') + class_name = f"@mark_t{mark_type}_{suffix}" + lookup_name = f"mark_t{mark_type}_{suffix}" lines.append(f"lookup {lookup_name} {{") - for cp, g in sorted(bases_with_anchors.items()): - anchor = g.props.diacritics_anchors[mark_type] if mark_type < 6 else None - if anchor and (anchor.x_used or anchor.y_used): - # Match Kotlin: when x_used is false, default to width / 2 - ax = (anchor.x if anchor.x_used else g.props.width // 2) * SC.SCALE - ay = (SC.ASCENT // SC.SCALE - anchor.y) * SC.SCALE - lines.append(f" pos base {glyph_name(cp)} mark {class_name};") + for cp, g in sorted(all_bases.items()): + anchor = (g.props.diacritics_anchors[mark_type] + if mark_type < len(g.props.diacritics_anchors) + else None) + has_explicit = anchor and (anchor.x_used or anchor.y_used) + + if align in (SC.ALIGN_LEFT, SC.ALIGN_BEFORE): + # Kotlin ignores base anchors for these alignments; + # the mark always sits at posX[base]. + ax = 0 + ay = SC.ASCENT + elif has_explicit: + ax = (anchor.x if anchor.x_used + else g.props.width // 2) * SC.SCALE + ay = ((SC.ASCENT // SC.SCALE - anchor.y) * SC.SCALE + if anchor.y_used else SC.ASCENT) + elif align == SC.ALIGN_CENTRE: + ax = (g.props.width // 2) * SC.SCALE + ay = SC.ASCENT + elif align == SC.ALIGN_RIGHT: + ax = g.props.width * SC.SCALE + ay = SC.ASCENT + else: + ax = 0 + ay = SC.ASCENT + + lines.append( + f" pos base {glyph_name(cp)}" + f" mark {class_name};" + ) lines.append(f"}} {lookup_name};") lines.append("")