From 602923f5bc15f5f578dc2c18d45da5abbf9e6a43 Mon Sep 17 00:00:00 2001 From: minjaesong Date: Mon, 2 Mar 2026 06:04:34 +0900 Subject: [PATCH] intonation marks --- CONTRIBUTING.md | 4 +- OTFbuild/calligra_font_tests.odt | Bin 15709 -> 15695 bytes OTFbuild/opentype_features.py | 94 +++++++++++++++++++++++++++++++ 3 files changed, 96 insertions(+), 2 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a7e83a3..efcb89b 100755 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -150,7 +150,7 @@ To implement those, this two extra code points are needed, which are provided in For working examples, take a note at the bengali sprite sheet. -This tag can be used as a general "replace this with these" directive, as long as you're replacing it into two letters. This directive is exploited to construct dutch ligature "IJ" (U+0132 and U+0133), in the sheet LatinExtA. +This tag might be exploited as a general "replace this with these" directive, as long as you're replacing it into two letters. Such construction is FORBIDDEN due to diacritics incompatibility. Use Compiler Directives for such purposes. Also note that the font compiler will not "stack" these diacritics. @@ -170,7 +170,7 @@ Keming Machine Tags define the rough shape of the glyph. Please read `keming_mac ## Technical Limitations - Each spritesheet is 4096x4096 maximum, which is a size of 4K Texture. However it is recommended to be smaller or equal to 1024x1024. -- Glyphs exceeding 15px of width needs to be broken down with 2 or more characters. Wider sheets WILL NOT BE IMPLEMENTED, can't waste much pixels just for few superwide glyphs. +- Glyphs exceeding 15px of width needs to be broken down with 2 or more characters, or use EXTRAWIDE spritesheets. - Due to how the compiler is coded, actual glyph must have alpha value of 255, the tags must have alpha values LESS THAN 255 (and obviously greater than zero). RGB plane of the TGA image doesn't do anything, keep it as #FFFFFF white. ## Implementation of the Korean writing system diff --git a/OTFbuild/calligra_font_tests.odt b/OTFbuild/calligra_font_tests.odt index 2edd0a4881deafe95b42e5c784e648f80e3356e4..55afc04c8f8bce3bc0ebb989afeb962efb1f5b22 100644 GIT binary patch delta 5995 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 delta 6034 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 diff --git a/OTFbuild/opentype_features.py b/OTFbuild/opentype_features.py index d0bdf0a..7ca5876 100644 --- a/OTFbuild/opentype_features.py +++ b/OTFbuild/opentype_features.py @@ -88,6 +88,11 @@ def generate_features(glyphs, kern_pairs, font_glyph_set, if sund_code: parts.append(sund_code) + # IPA tone bar graphs + tone_code = _generate_tone_bars(has) + if tone_code: + parts.append(tone_code) + # mark feature mark_code = _generate_mark(glyphs, has) if mark_code: @@ -1599,3 +1604,92 @@ def _generate_anusvara_gpos(glyphs, has): lines.append("} abvm;") return '\n'.join(lines) + + +def _generate_tone_bars(has): + """ + Generate GSUB lookups for IPA tone bar graphs (U+02E5-U+02E9). + + When two or more modifier letter tone bars appear consecutively, + they are ligated into graph glyphs: + - Each pair produces: hairspace (U+200A) + graph glyph (U+FFE20 + tone1*5 + tone2) + - The final tone bar in a sequence becomes an end cap (U+FFE39) + - Lone tone bars are left unchanged. + """ + tone_bars = [0x02E5 + i for i in range(5)] + hairspace = 0x200A + endcap = 0xFFE39 + + # Check required glyphs exist + if not all(has(tb) for tb in tone_bars): + return "" + if not has(hairspace) or not has(endcap): + return "" + + lines = [] + + # Step 1: Multiple substitution lookups for each (tone1, tone2) pair. + # Each lookup maps one tone bar to hairspace + graph glyph. + # These are standalone lookups, only invoked via chaining context. + valid_pairs = [] + for i in range(5): + for j in range(5): + graph_cp = 0xFFE20 + i * 5 + j + if has(graph_cp): + lookup_name = f"tone_{i}_{j}" + lines.append(f"lookup {lookup_name} {{") + lines.append(f" sub {glyph_name(tone_bars[i])} by" + f" {glyph_name(hairspace)} {glyph_name(graph_cp)};") + lines.append(f"}} {lookup_name};") + lines.append("") + valid_pairs.append((i, j)) + + if not valid_pairs: + return "" + + # Step 2: End cap single substitution lookup. + # Replaces any tone bar with the end cap glyph. + lines.append("lookup tone_endcap {") + for i in range(5): + lines.append(f" sub {glyph_name(tone_bars[i])} by {glyph_name(endcap)};") + lines.append("} tone_endcap;") + lines.append("") + + # Step 3: Chaining contextual substitution for pairs. + # When tone_bar_i is followed by tone_bar_j, apply multisub to replace + # tone_bar_i with hairspace + graph(i,j). The following tone_bar_j is + # lookahead context only, so it remains for the next pair. + lines.append("lookup ToneBarPairs {") + for i, j in valid_pairs: + lines.append(f" sub {glyph_name(tone_bars[i])}'" + f" lookup tone_{i}_{j}" + f" {glyph_name(tone_bars[j])};") + lines.append("} ToneBarPairs;") + lines.append("") + + # Step 4: Chaining contextual substitution for end cap. + # After ToneBarPairs, the last tone bar in a sequence is preceded by + # a graph glyph. Replace it with the end cap. + graph_names = [] + for cp in range(0xFFE20, 0xFFE39): + if has(cp): + graph_names.append(glyph_name(cp)) + + if graph_names: + lines.append(f"@tone_graph = [{' '.join(graph_names)}];") + lines.append("") + lines.append("lookup ToneBarEndCap {") + for i in range(5): + lines.append(f" sub @tone_graph" + f" {glyph_name(tone_bars[i])}' lookup tone_endcap;") + lines.append("} ToneBarEndCap;") + lines.append("") + + # Register lookups in liga feature + lines.append("feature liga {") + lines.append(" lookup ToneBarPairs;") + if graph_names: + lines.append(" lookup ToneBarEndCap;") + lines.append("} liga;") + + return '\n'.join(lines)