mirror of
https://github.com/curioustorvald/Terrarum-sans-bitmap.git
synced 2026-03-07 11:51:50 +09:00
otf wip3
This commit is contained in:
@@ -6,7 +6,7 @@ Builds a TTF font with both vector-traced outlines (TrueType glyf)
|
||||
and embedded bitmap strike (EBDT/EBLC) from TGA sprite sheets.
|
||||
|
||||
Usage:
|
||||
python3 OTFbuild/build_font.py src/assets -o OTFbuild/TerrarumSansBitmap.ttf
|
||||
python3 OTFbuild/build_font.py src/assets -o OTFbuild/TerrarumSansBitmap.otf
|
||||
|
||||
Options:
|
||||
--no-bitmap Skip EBDT/EBLC bitmap strike
|
||||
|
||||
@@ -102,11 +102,86 @@ def build_font(assets_dir, output_path, no_bitmap=False, no_features=False):
|
||||
glyphs.update(hangul_glyphs)
|
||||
print(f" Total glyphs after Hangul: {len(glyphs)}")
|
||||
|
||||
# Step 2b: Copy PUA consonant glyphs to Unicode positions
|
||||
# In the bitmap font, consonants U+0915-0939 have width=0 and empty bitmaps
|
||||
# because the engine normalises them to PUA forms (0xF0140+) before rendering.
|
||||
# For OTF, we need the Unicode positions to have actual outlines so that
|
||||
# consonants render even without GSUB shaping.
|
||||
print("Step 2b: Populating Devanagari consonant glyphs from PUA forms...")
|
||||
deva_copied = 0
|
||||
for uni_cp in range(0x0915, 0x093A):
|
||||
try:
|
||||
pua_cp = SC.to_deva_internal(uni_cp)
|
||||
except ValueError:
|
||||
continue
|
||||
if pua_cp in glyphs and uni_cp in glyphs:
|
||||
pua_g = glyphs[pua_cp]
|
||||
uni_g = glyphs[uni_cp]
|
||||
if uni_g.props.width == 0 and pua_g.props.width > 0:
|
||||
uni_g.props.width = pua_g.props.width
|
||||
uni_g.bitmap = pua_g.bitmap
|
||||
deva_copied += 1
|
||||
# Also copy nukta consonant forms U+0958-095F
|
||||
for uni_cp in range(0x0958, 0x0960):
|
||||
try:
|
||||
pua_cp = SC.to_deva_internal(uni_cp)
|
||||
except ValueError:
|
||||
continue
|
||||
if pua_cp in glyphs and uni_cp in glyphs:
|
||||
pua_g = glyphs[pua_cp]
|
||||
uni_g = glyphs[uni_cp]
|
||||
if uni_g.props.width == 0 and pua_g.props.width > 0:
|
||||
uni_g.props.width = pua_g.props.width
|
||||
uni_g.bitmap = pua_g.bitmap
|
||||
deva_copied += 1
|
||||
print(f" Copied {deva_copied} consonant glyphs from PUA forms")
|
||||
|
||||
# Step 3: Expand replacewith directives
|
||||
print("Step 3: Processing replacewith directives...")
|
||||
replacewith_subs = _expand_replacewith(glyphs)
|
||||
print(f" Found {len(replacewith_subs)} replacewith substitutions")
|
||||
|
||||
# Step 3b: Compose fallback bitmaps for replacewith glyphs
|
||||
# Glyphs with replacewith directives have width=0 and no bitmap; they
|
||||
# rely on GSUB ccmp to expand into their target sequence. Renderers
|
||||
# without GSUB support would show whitespace. Build a composite
|
||||
# bitmap by concatenating the target glyphs' bitmaps side by side.
|
||||
print("Step 3b: Composing fallback bitmaps for replacewith glyphs...")
|
||||
composed = 0
|
||||
for src_cp, target_cps in replacewith_subs:
|
||||
src_g = glyphs.get(src_cp)
|
||||
if src_g is None or src_g.props.width > 0:
|
||||
continue # already has content (e.g. Deva consonants fixed above)
|
||||
# Resolve target glyphs
|
||||
target_gs = [glyphs.get(t) for t in target_cps]
|
||||
if not all(target_gs):
|
||||
continue
|
||||
# Compute total advance and composite height
|
||||
total_width = sum(g.props.width for g in target_gs)
|
||||
if total_width == 0:
|
||||
continue
|
||||
bm_height = max((len(g.bitmap) for g in target_gs if g.bitmap), default=SC.H)
|
||||
# Build composite bitmap
|
||||
composite = [[0] * total_width for _ in range(bm_height)]
|
||||
x = 0
|
||||
for tg in target_gs:
|
||||
if not tg.bitmap:
|
||||
x += tg.props.width
|
||||
continue
|
||||
cols = min(tg.props.width, len(tg.bitmap[0])) if tg.props.width > 0 else len(tg.bitmap[0])
|
||||
for row in range(min(len(tg.bitmap), bm_height)):
|
||||
for col in range(cols):
|
||||
dst_col = x + col
|
||||
if dst_col < total_width and tg.bitmap[row][col]:
|
||||
composite[row][dst_col] = 1
|
||||
if tg.props.width > 0:
|
||||
x += tg.props.width
|
||||
# Zero-width targets (combining marks) overlay at current position
|
||||
src_g.props.width = total_width
|
||||
src_g.bitmap = composite
|
||||
composed += 1
|
||||
print(f" Composed {composed} fallback bitmaps")
|
||||
|
||||
# Step 4: Create glyph order and cmap
|
||||
print("Step 4: Building glyph order and cmap...")
|
||||
glyph_order = [".notdef"]
|
||||
|
||||
@@ -73,7 +73,7 @@ def generate_features(glyphs, kern_pairs, font_glyph_set,
|
||||
parts.append(locl_code)
|
||||
|
||||
# Devanagari features
|
||||
deva_code = _generate_devanagari(glyphs, has)
|
||||
deva_code = _generate_devanagari(glyphs, has, replacewith_subs or [])
|
||||
if deva_code:
|
||||
parts.append(deva_code)
|
||||
|
||||
@@ -397,13 +397,11 @@ def _generate_locl(glyphs, has):
|
||||
return '\n'.join(lines)
|
||||
|
||||
|
||||
def _generate_devanagari(glyphs, has):
|
||||
"""Generate Devanagari GSUB features: ccmp (consonant mapping), nukt, akhn, half, vatu, pres, blws, rphf."""
|
||||
def _generate_devanagari(glyphs, has, replacewith_subs=None):
|
||||
"""Generate Devanagari GSUB features: ccmp (consonant mapping + vowel decomposition), nukt, akhn, half, vatu, pres, blws, rphf."""
|
||||
features = []
|
||||
|
||||
# --- ccmp: Map Unicode consonants to internal PUA presentation forms ---
|
||||
# This is the critical first step: U+0915-0939 have width=0 in the sheet,
|
||||
# the actual glyph bitmaps live at their PUA forms (0xF0140+).
|
||||
# This mirrors the Kotlin normalise() pass 0.
|
||||
ccmp_subs = []
|
||||
for uni_cp in range(0x0915, 0x093A):
|
||||
@@ -422,13 +420,42 @@ def _generate_devanagari(glyphs, has):
|
||||
)
|
||||
except ValueError:
|
||||
pass
|
||||
if ccmp_subs:
|
||||
features.append(
|
||||
"feature ccmp {\n script dev2;\n"
|
||||
" lookup DevaConsonantMap {\n"
|
||||
+ '\n'.join(" " + s for s in ccmp_subs)
|
||||
+ "\n } DevaConsonantMap;\n} ccmp;"
|
||||
)
|
||||
|
||||
# --- ccmp: Devanagari vowel decompositions ---
|
||||
# Independent vowels like U+0910 (AI) decompose into base + matra.
|
||||
# These must be in the dev2 ccmp so HarfBuzz applies them during
|
||||
# Devanagari shaping (DFLT ccmp is not used when dev2 is present).
|
||||
vowel_decomp_subs = []
|
||||
if replacewith_subs:
|
||||
for src_cp, target_cps in replacewith_subs:
|
||||
if not (0x0900 <= src_cp <= 0x097F):
|
||||
continue
|
||||
# Skip consonants (already handled above as single subs)
|
||||
if 0x0915 <= src_cp <= 0x0939 or 0x0958 <= src_cp <= 0x095F:
|
||||
continue
|
||||
if len(target_cps) < 2:
|
||||
continue
|
||||
if not has(src_cp):
|
||||
continue
|
||||
if not all(has(t) for t in target_cps):
|
||||
continue
|
||||
targets = ' '.join(glyph_name(t) for t in target_cps)
|
||||
vowel_decomp_subs.append(
|
||||
f" sub {glyph_name(src_cp)} by {targets};"
|
||||
)
|
||||
|
||||
if ccmp_subs or vowel_decomp_subs:
|
||||
ccmp_parts = ["feature ccmp {", " script dev2;"]
|
||||
if ccmp_subs:
|
||||
ccmp_parts.append(" lookup DevaConsonantMap {")
|
||||
ccmp_parts.extend(" " + s for s in ccmp_subs)
|
||||
ccmp_parts.append(" } DevaConsonantMap;")
|
||||
if vowel_decomp_subs:
|
||||
ccmp_parts.append(" lookup DevaVowelDecomp {")
|
||||
ccmp_parts.extend(" " + s for s in vowel_decomp_subs)
|
||||
ccmp_parts.append(" } DevaVowelDecomp;")
|
||||
ccmp_parts.append("} ccmp;")
|
||||
features.append('\n'.join(ccmp_parts))
|
||||
|
||||
# --- nukt: consonant + nukta -> nukta form ---
|
||||
# Now operates on PUA forms (after ccmp)
|
||||
|
||||
Reference in New Issue
Block a user