diff --git a/OTFbuild/calligra_font_tests.odt b/OTFbuild/calligra_font_tests.odt index f4ccd72..498a840 100644 Binary files a/OTFbuild/calligra_font_tests.odt and b/OTFbuild/calligra_font_tests.odt differ diff --git a/OTFbuild/glyph_parser.py b/OTFbuild/glyph_parser.py index f9cb99f..0537e83 100644 --- a/OTFbuild/glyph_parser.py +++ b/OTFbuild/glyph_parser.py @@ -38,6 +38,7 @@ class GlyphProps: has_kern_data: bool = False is_kern_y_type: bool = False kerning_mask: int = 255 + dot_removal: Optional[int] = None # codepoint to replace with when followed by a STACK_UP mark directive_opcode: int = 0 directive_arg1: int = 0 directive_arg2: int = 0 @@ -131,7 +132,8 @@ def parse_variable_sheet(image, sheet_index, cell_w, cell_h, cols, is_xy_swapped # Kerning data kerning_bit1 = _tagify(image.get_pixel(code_start_x, code_start_y + 6)) - # kerning_bit2 and kerning_bit3 are reserved + kerning_bit2 = _tagify(image.get_pixel(code_start_x, code_start_y + 7)) + dot_removal = None if kerning_bit2 == 0 else (kerning_bit2 >> 8) is_kern_y_type = (kerning_bit1 & 0x80000000) != 0 kerning_mask = (kerning_bit1 >> 8) & 0xFFFFFF has_kern_data = (kerning_bit1 & 0xFF) != 0 @@ -188,7 +190,7 @@ def parse_variable_sheet(image, sheet_index, cell_w, cell_h, cols, is_xy_swapped align_where=align_where, write_on_top=write_on_top, stack_where=stack_where, ext_info=ext_info, has_kern_data=has_kern_data, is_kern_y_type=is_kern_y_type, - kerning_mask=kerning_mask, + kerning_mask=kerning_mask, dot_removal=dot_removal, directive_opcode=directive_opcode, directive_arg1=directive_arg1, directive_arg2=directive_arg2, ) diff --git a/OTFbuild/opentype_features.py b/OTFbuild/opentype_features.py index 6677e65..1b27797 100644 --- a/OTFbuild/opentype_features.py +++ b/OTFbuild/opentype_features.py @@ -70,6 +70,11 @@ languagesystem sund dflt; if ccmp_code: parts.append(ccmp_code) + # ccmp: dot removal (e.g. i→ı, j→ȷ when followed by STACK_UP marks) + dot_removal_code = _generate_dot_removal(glyphs, has) + if dot_removal_code: + parts.append(dot_removal_code) + # Hangul jamo GSUB assembly hangul_code = _generate_hangul_gsub(glyphs, has, jamo_data) if hangul_code: @@ -162,6 +167,54 @@ def _generate_ccmp(replacewith_subs, has): return '\n'.join(lines) +def _generate_dot_removal(glyphs, has): + """Generate ccmp contextual substitution for dot removal. + + When a base glyph tagged with dot_removal (kerning bit 2, pixel Y+7) is + followed by a STACK_UP mark, substitute the base with its dotless form. + Matches the Kotlin engine's dotRemoval logic. + """ + # Collect all STACK_UP marks + stack_up_marks = [] + for cp, g in glyphs.items(): + if g.props.write_on_top >= 0 and g.props.stack_where == SC.STACK_UP and has(cp): + stack_up_marks.append(cp) + + if not stack_up_marks: + return "" + + # Collect all base glyphs with dot_removal + dot_removal_subs = [] + for cp, g in glyphs.items(): + if g.props.dot_removal is not None and has(cp) and has(g.props.dot_removal): + dot_removal_subs.append((cp, g.props.dot_removal)) + + if not dot_removal_subs: + return "" + + lines = [] + + # Define the STACK_UP marks class + mark_names = ' '.join(glyph_name(cp) for cp in sorted(stack_up_marks)) + lines.append(f"@stackUpMarks = [{mark_names}];") + lines.append("") + + # Single substitution lookup for the replacements + lines.append("lookup DotRemoval {") + for src_cp, dst_cp in sorted(dot_removal_subs): + lines.append(f" sub {glyph_name(src_cp)} by {glyph_name(dst_cp)};") + lines.append("} DotRemoval;") + lines.append("") + + # Contextual rules in ccmp + lines.append("feature ccmp {") + for src_cp, _ in sorted(dot_removal_subs): + lines.append(f" sub {glyph_name(src_cp)}' lookup DotRemoval @stackUpMarks;") + lines.append("} ccmp;") + + return '\n'.join(lines) + + def _generate_hangul_gsub(glyphs, has, jamo_data): """ Generate Hangul jamo GSUB lookups for syllable assembly. diff --git a/keming_machine.txt b/keming_machine.txt index d404073..6a2c00e 100644 --- a/keming_machine.txt +++ b/keming_machine.txt @@ -1,7 +1,7 @@ --- Pixel 0 - Lowheight bit - encoding: has pixel - it's low height -- used by the diacritics system to quickly look up if the character is low height without parsing the Pixel 1 +- bit must be set if above-diacritics should be lowered (e.g. lowercase b, which has 'A' shape bit but considered lowheight) ### Legends #