diff --git a/OTFbuild/calligra_font_tests.odt b/OTFbuild/calligra_font_tests.odt index 5d1f130..652cf32 100644 Binary files a/OTFbuild/calligra_font_tests.odt and b/OTFbuild/calligra_font_tests.odt differ diff --git a/OTFbuild/font_builder.py b/OTFbuild/font_builder.py index 6cb4301..327ac43 100644 --- a/OTFbuild/font_builder.py +++ b/OTFbuild/font_builder.py @@ -264,6 +264,12 @@ def build_font(assets_dir, output_path, no_bitmap=False, no_features=False): # ALIGN_CENTRE: offset = ceil((width - W_VAR_INIT) / 2) (negative) # ALIGN_BEFORE: offset = 0 # The bitmap cell width depends on the sheet type. + # nudge_x shifts the glyph left by that many pixels in the + # bitmap engine. For zero-advance glyphs (marks and width-0 + # non-marks like U+0361) this is a pure visual shift that must + # be baked into the contours. For positive-advance glyphs the + # bitmap engine's nudge/extraWidth mechanism already maps to + # the OTF advance, so we must NOT shift contours. import math bm_cols = len(g.bitmap[0]) if g.bitmap and g.bitmap[0] else 0 if g.props.align_where == SC.ALIGN_RIGHT: @@ -272,6 +278,8 @@ def build_font(assets_dir, output_path, no_bitmap=False, no_features=False): x_offset = math.ceil((g.props.width - bm_cols) / 2) * SCALE else: x_offset = 0 + if advance == 0: + x_offset -= g.props.nudge_x * SCALE contours = trace_bitmap(g.bitmap, g.props.width) diff --git a/OTFbuild/opentype_features.py b/OTFbuild/opentype_features.py index 4ca9703..be9c532 100644 --- a/OTFbuild/opentype_features.py +++ b/OTFbuild/opentype_features.py @@ -11,6 +11,7 @@ Features implemented: - mark: GPOS mark-to-base positioning (diacritics anchors) """ +import math from typing import Dict, List, Set, Tuple from glyph_parser import ExtractedGlyph @@ -1097,7 +1098,22 @@ def _generate_mark(glyphs, has): for mark_type, mark_list in sorted(mark_classes.items()): class_name = f"@mark_type{mark_type}" for cp, g in mark_list: - mark_x = ((g.props.width + 1) // 2) * SC.SCALE + if g.props.align_where == 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. + bm_cols = len(g.bitmap[0]) if g.bitmap and g.bitmap[0] else 0 + if 0x0900 <= cp <= 0x0902: + half = (SC.W_VAR_INIT + 1) // 2 + else: + half = (SC.W_VAR_INIT - 1) // 2 + 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 + else: + mark_x = ((g.props.width + 1) // 2) * SC.SCALE mark_y = SC.ASCENT lines.append( f"markClass {glyph_name(cp)} {class_name};" @@ -1114,7 +1130,8 @@ def _generate_mark(glyphs, has): 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): - ax = anchor.x * SC.SCALE + # 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};")