Files
Terrarum-sans-bitmap/OTFbuild/keming_machine.py
2026-02-23 18:32:03 +09:00

127 lines
3.8 KiB
Python

"""
Generate kerning pairs from shape rules.
Ported from TerrarumSansBitmap.kt "The Keming Machine" section.
6 base rules + 6 mirrored (auto-generated) = 12 rules total.
Also includes r+dot special pairs.
Output kern values scaled by SCALE (50 units/pixel):
-1px -> -50 units, -2px -> -100 units
"""
from typing import Dict, Tuple
from glyph_parser import ExtractedGlyph
import sheet_config as SC
SCALE = SC.SCALE
class _Ing:
"""Pattern matcher for kerning shape bits."""
def __init__(self, s):
self.s = s
self.care_bits = 0
self.rule_bits = 0
for index, char in enumerate(s):
if char == '@':
self.care_bits |= SC.KEMING_BIT_MASK[index]
self.rule_bits |= SC.KEMING_BIT_MASK[index]
elif char == '`':
self.care_bits |= SC.KEMING_BIT_MASK[index]
def matches(self, shape_bits):
return (shape_bits & self.care_bits) == self.rule_bits
class _Kem:
def __init__(self, first, second, bb=2, yy=1):
self.first = first
self.second = second
self.bb = bb
self.yy = yy
def _build_kerning_rules():
"""Build the 12 kerning rules (6 base + 6 mirrored)."""
base_rules = [
_Kem(_Ing("_`_@___`__"), _Ing("`_`___@___")),
_Kem(_Ing("_@_`___`__"), _Ing("`_________")),
_Kem(_Ing("_@_@___`__"), _Ing("`___@_@___"), 1, 1),
_Kem(_Ing("_@_@_`_`__"), _Ing("`_____@___")),
_Kem(_Ing("___`_`____"), _Ing("`___@_`___")),
_Kem(_Ing("___`_`____"), _Ing("`_@___`___")),
]
mirrored = []
for rule in base_rules:
left = rule.first.s
right = rule.second.s
new_left = []
new_right = []
for c in range(0, len(left), 2):
new_left.append(right[c + 1])
new_left.append(right[c])
new_right.append(left[c + 1])
new_right.append(left[c])
mirrored.append(_Kem(
_Ing(''.join(new_left)),
_Ing(''.join(new_right)),
rule.bb, rule.yy
))
return base_rules + mirrored
_KERNING_RULES = _build_kerning_rules()
def generate_kerning_pairs(glyphs: Dict[int, ExtractedGlyph]) -> Dict[Tuple[int, int], int]:
"""
Generate kerning pairs from all glyphs that have kerning data.
Returns dict of (left_codepoint, right_codepoint) -> kern_offset_in_font_units.
Negative values = tighter spacing.
"""
result = {}
# Collect all codepoints with kerning data
kernable = {cp: g for cp, g in glyphs.items() if g.props.has_kern_data}
if not kernable:
print(" [KemingMachine] No glyphs with kern data found")
return result
print(f" [KemingMachine] {len(kernable)} glyphs with kern data")
# Special rule: lowercase r + dot
r_dot_count = 0
for r in SC.LOWERCASE_RS:
for d in SC.DOTS:
if r in glyphs and d in glyphs:
result[(r, d)] = -1 * SCALE
r_dot_count += 1
# Apply kerning rules to all pairs
kern_codes = list(kernable.keys())
pairs_found = 0
for left_code in kern_codes:
left_props = kernable[left_code].props
mask_l = left_props.kerning_mask
for right_code in kern_codes:
right_props = kernable[right_code].props
mask_r = right_props.kerning_mask
for rule in _KERNING_RULES:
if rule.first.matches(mask_l) and rule.second.matches(mask_r):
contraction = rule.yy if (left_props.is_kern_y_type or right_props.is_kern_y_type) else rule.bb
if contraction > 0:
result[(left_code, right_code)] = -contraction * SCALE
pairs_found += 1
break # first matching rule wins
print(f" [KemingMachine] Generated {pairs_found} kerning pairs (+ {r_dot_count} r-dot pairs)")
return result