diff --git a/OTFbuild/bitmap_tracer.py b/OTFbuild/bitmap_tracer.py index 346a3bc..ffae93e 100644 --- a/OTFbuild/bitmap_tracer.py +++ b/OTFbuild/bitmap_tracer.py @@ -77,15 +77,21 @@ def trace_bitmap(bitmap, glyph_width_px): return contours -def draw_glyph_to_pen(contours, pen): +def draw_glyph_to_pen(contours, pen, x_offset=0, y_offset=0): """ Draw rectangle contours to a TTGlyphPen or similar pen. Each rectangle is drawn as a clockwise closed contour (4 on-curve points). + + x_offset/y_offset shift all contours (used for alignment positioning). """ for x0, y0, x1, y1 in contours: + ax0 = x0 + x_offset + ax1 = x1 + x_offset + ay0 = y0 + y_offset + ay1 = y1 + y_offset # Clockwise: bottom-left -> top-left -> top-right -> bottom-right - pen.moveTo((x0, y0)) - pen.lineTo((x0, y1)) - pen.lineTo((x1, y1)) - pen.lineTo((x1, y0)) + pen.moveTo((ax0, ay0)) + pen.lineTo((ax0, ay1)) + pen.lineTo((ax1, ay1)) + pen.lineTo((ax1, ay0)) pen.closePath() diff --git a/OTFbuild/font_builder.py b/OTFbuild/font_builder.py index de65f93..141da9a 100644 --- a/OTFbuild/font_builder.py +++ b/OTFbuild/font_builder.py @@ -168,11 +168,28 @@ def build_font(assets_dir, output_path, no_bitmap=False, no_features=False): continue advance = g.props.width * SCALE + + # Compute alignment offset (lsb shift). + # The Kotlin code draws the full cell at an offset position: + # ALIGN_LEFT: offset = 0 + # ALIGN_RIGHT: offset = width - W_VAR_INIT (negative) + # ALIGN_CENTRE: offset = ceil((width - W_VAR_INIT) / 2) (negative) + # ALIGN_BEFORE: offset = 0 + # The bitmap cell width depends on the sheet type. + 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: + x_offset = (g.props.width - bm_cols) * SCALE + elif g.props.align_where == SC.ALIGN_CENTRE: + x_offset = math.ceil((g.props.width - bm_cols) / 2) * SCALE + else: + x_offset = 0 + contours = trace_bitmap(g.bitmap, g.props.width) pen = T2CharStringPen(advance, None) if contours: - draw_glyph_to_pen(contours, pen) + draw_glyph_to_pen(contours, pen, x_offset=x_offset) traced_count += 1 charstrings[name] = pen.getCharString() diff --git a/OTFbuild/glyph_parser.py b/OTFbuild/glyph_parser.py index 07ecb0e..e31b568 100644 --- a/OTFbuild/glyph_parser.py +++ b/OTFbuild/glyph_parser.py @@ -191,17 +191,30 @@ def parse_variable_sheet(image, sheet_index, cell_w, cell_h, cols, is_xy_swapped info |= (1 << y) ext_info[x] = info - # Extract glyph bitmap: only pixels within the glyph's declared width. - # The tag column and any padding beyond width must be stripped. - bitmap_w = min(width, cell_w - 1) if width > 0 else 0 + # Extract glyph bitmap: full cell minus the tag column. + # The Kotlin code draws the ENTIRE cell at a computed position; + # the tag column is the only thing excluded. + # Alignment and width only affect advance/positioning, not the bitmap. + max_w = cell_w - 1 # exclude tag column + bitmap = [] for row in range(cell_h): row_data = [] - for col in range(bitmap_w): + for col in range(max_w): px = image.get_pixel(cell_x + col, cell_y + row) row_data.append(1 if (px & 0xFF) != 0 else 0) bitmap.append(row_data) + # Now strip the tag column pixels that may have leaked into + # the glyph area. Tag data lives at column (cell_w - 1) which + # we already excluded, but extInfo columns 0..6 at the LEFT + # edge of the cell also contain tag data for replacewith glyphs. + # Clean those columns if they were used for extInfo. + if ext_count > 0: + for col_idx in range(min(ext_count, max_w)): + for row in range(cell_h): + bitmap[row][col_idx] = 0 + result[code] = ExtractedGlyph(code, props, bitmap) return result diff --git a/OTFbuild/opentype_features.py b/OTFbuild/opentype_features.py index 95eea8b..3320477 100644 --- a/OTFbuild/opentype_features.py +++ b/OTFbuild/opentype_features.py @@ -398,59 +398,103 @@ def _generate_locl(glyphs, has): def _generate_devanagari(glyphs, has): - """Generate Devanagari GSUB features: nukt, akhn, half, vatu, pres, blws, rphf.""" + """Generate Devanagari GSUB features: ccmp (consonant mapping), 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): + internal = SC.to_deva_internal(uni_cp) + if has(uni_cp) and has(internal): + ccmp_subs.append( + f" sub {glyph_name(uni_cp)} by {glyph_name(internal)};" + ) + # Also map nukta-forms U+0958-095F to their PUA equivalents + for uni_cp in range(0x0958, 0x0960): + try: + internal = SC.to_deva_internal(uni_cp) + if has(uni_cp) and has(internal): + ccmp_subs.append( + f" sub {glyph_name(uni_cp)} by {glyph_name(internal)};" + ) + 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;" + ) + # --- nukt: consonant + nukta -> nukta form --- + # Now operates on PUA forms (after ccmp) nukt_subs = [] for uni_cp in range(0x0915, 0x093A): internal = SC.to_deva_internal(uni_cp) nukta_form = internal + 48 - if has(uni_cp) and has(0x093C) and has(nukta_form): + if has(internal) and has(0x093C) and has(nukta_form): nukt_subs.append( - f" sub {glyph_name(uni_cp)} {glyph_name(0x093C)} by {glyph_name(nukta_form)};" + f" sub {glyph_name(internal)} {glyph_name(0x093C)} by {glyph_name(nukta_form)};" ) if nukt_subs: features.append("feature nukt {\n script dev2;\n" + '\n'.join(nukt_subs) + "\n} nukt;") # --- akhn: akhand ligatures --- + # Must reference PUA forms after ccmp akhn_subs = [] - if has(0x0915) and has(SC.DEVANAGARI_VIRAMA) and has(0x0937) and has(SC.DEVANAGARI_LIG_K_SS): + ka_int = SC.to_deva_internal(0x0915) + ssa_int = SC.to_deva_internal(0x0937) + ja_int = SC.to_deva_internal(0x091C) + nya_int = SC.to_deva_internal(0x091E) + if has(ka_int) and has(SC.DEVANAGARI_VIRAMA) and has(ssa_int) and has(SC.DEVANAGARI_LIG_K_SS): akhn_subs.append( - f" sub {glyph_name(0x0915)} {glyph_name(SC.DEVANAGARI_VIRAMA)} {glyph_name(0x0937)} by {glyph_name(SC.DEVANAGARI_LIG_K_SS)};" + f" sub {glyph_name(ka_int)} {glyph_name(SC.DEVANAGARI_VIRAMA)} {glyph_name(ssa_int)} by {glyph_name(SC.DEVANAGARI_LIG_K_SS)};" ) - if has(0x091C) and has(SC.DEVANAGARI_VIRAMA) and has(0x091E) and has(SC.DEVANAGARI_LIG_J_NY): + if has(ja_int) and has(SC.DEVANAGARI_VIRAMA) and has(nya_int) and has(SC.DEVANAGARI_LIG_J_NY): akhn_subs.append( - f" sub {glyph_name(0x091C)} {glyph_name(SC.DEVANAGARI_VIRAMA)} {glyph_name(0x091E)} by {glyph_name(SC.DEVANAGARI_LIG_J_NY)};" + f" sub {glyph_name(ja_int)} {glyph_name(SC.DEVANAGARI_VIRAMA)} {glyph_name(nya_int)} by {glyph_name(SC.DEVANAGARI_LIG_J_NY)};" ) if akhn_subs: features.append("feature akhn {\n script dev2;\n" + '\n'.join(akhn_subs) + "\n} akhn;") - # --- half: consonant + virama -> half form --- + # --- half: consonant (PUA) + virama -> half form --- + # After ccmp, consonants are in PUA form, so reference PUA here half_subs = [] for uni_cp in range(0x0915, 0x093A): internal = SC.to_deva_internal(uni_cp) half_form = internal + 240 - if has(uni_cp) and has(SC.DEVANAGARI_VIRAMA) and has(half_form): + if has(internal) and has(SC.DEVANAGARI_VIRAMA) and has(half_form): half_subs.append( - f" sub {glyph_name(uni_cp)} {glyph_name(SC.DEVANAGARI_VIRAMA)} by {glyph_name(half_form)};" + f" sub {glyph_name(internal)} {glyph_name(SC.DEVANAGARI_VIRAMA)} by {glyph_name(half_form)};" ) if half_subs: features.append("feature half {\n script dev2;\n" + '\n'.join(half_subs) + "\n} half;") - # --- vatu: consonant + virama + RA -> RA-appended form --- + # --- vatu: consonant (PUA) + virama + RA (PUA) -> RA-appended form --- + ra_int = SC.to_deva_internal(0x0930) vatu_subs = [] for uni_cp in range(0x0915, 0x093A): internal = SC.to_deva_internal(uni_cp) ra_form = internal + 480 - if has(uni_cp) and has(SC.DEVANAGARI_VIRAMA) and has(0x0930) and has(ra_form): + if has(internal) and has(SC.DEVANAGARI_VIRAMA) and has(ra_int) and has(ra_form): vatu_subs.append( - f" sub {glyph_name(uni_cp)} {glyph_name(SC.DEVANAGARI_VIRAMA)} {glyph_name(0x0930)} by {glyph_name(ra_form)};" + f" sub {glyph_name(internal)} {glyph_name(SC.DEVANAGARI_VIRAMA)} {glyph_name(ra_int)} by {glyph_name(ra_form)};" ) if vatu_subs: features.append("feature vatu {\n script dev2;\n" + '\n'.join(vatu_subs) + "\n} vatu;") - # --- pres: named conjunct ligatures --- + # --- pres: named conjunct ligatures (using PUA forms) --- + def _di(u): + """Convert Unicode Devanagari consonant to internal PUA form.""" + try: + return SC.to_deva_internal(u) + except ValueError: + return u # already PUA or non-consonant + pres_subs = [] _conjuncts = [ (0x0915, 0x0924, SC.DEVANAGARI_LIG_K_T, "K.T"), @@ -494,7 +538,9 @@ def _generate_devanagari(glyphs, has): (0x0939, 0x0932, 0xF01CA, "H.L"), (0x0939, 0x0935, 0xF01CB, "H.V"), ] - for c1, c2, result, name in _conjuncts: + for c1_uni, c2_uni, result, name in _conjuncts: + c1 = _di(c1_uni) + c2 = _di(c2_uni) if has(c1) and has(SC.DEVANAGARI_VIRAMA) and has(c2) and has(result): pres_subs.append( f" sub {glyph_name(c1)} {glyph_name(SC.DEVANAGARI_VIRAMA)} {glyph_name(c2)} by {glyph_name(result)}; # {name}" @@ -502,15 +548,15 @@ def _generate_devanagari(glyphs, has): if pres_subs: features.append("feature pres {\n script dev2;\n" + '\n'.join(pres_subs) + "\n} pres;") - # --- blws: RA/RRA/HA + U/UU -> special syllables --- + # --- blws: RA/RRA/HA (PUA) + U/UU -> special syllables --- blws_subs = [] _blws_rules = [ - (0x0930, SC.DEVANAGARI_U, SC.DEVANAGARI_SYLL_RU, "Ru"), - (0x0930, SC.DEVANAGARI_UU, SC.DEVANAGARI_SYLL_RUU, "Ruu"), - (0x0931, SC.DEVANAGARI_U, SC.DEVANAGARI_SYLL_RRU, "RRu"), - (0x0931, SC.DEVANAGARI_UU, SC.DEVANAGARI_SYLL_RRUU, "RRuu"), - (0x0939, SC.DEVANAGARI_U, SC.DEVANAGARI_SYLL_HU, "Hu"), - (0x0939, SC.DEVANAGARI_UU, SC.DEVANAGARI_SYLL_HUU, "Huu"), + (_di(0x0930), SC.DEVANAGARI_U, SC.DEVANAGARI_SYLL_RU, "Ru"), + (_di(0x0930), SC.DEVANAGARI_UU, SC.DEVANAGARI_SYLL_RUU, "Ruu"), + (_di(0x0931), SC.DEVANAGARI_U, SC.DEVANAGARI_SYLL_RRU, "RRu"), + (_di(0x0931), SC.DEVANAGARI_UU, SC.DEVANAGARI_SYLL_RRUU, "RRuu"), + (_di(0x0939), SC.DEVANAGARI_U, SC.DEVANAGARI_SYLL_HU, "Hu"), + (_di(0x0939), SC.DEVANAGARI_UU, SC.DEVANAGARI_SYLL_HUU, "Huu"), ] for c1, c2, result, name in _blws_rules: if has(c1) and has(c2) and has(result): @@ -520,12 +566,12 @@ def _generate_devanagari(glyphs, has): if blws_subs: features.append("feature blws {\n script dev2;\n" + '\n'.join(blws_subs) + "\n} blws;") - # --- rphf: RA + virama -> reph --- - if has(0x0930) and has(SC.DEVANAGARI_VIRAMA) and has(SC.DEVANAGARI_RA_SUPER): + # --- rphf: RA (PUA) + virama -> reph --- + if has(ra_int) and has(SC.DEVANAGARI_VIRAMA) and has(SC.DEVANAGARI_RA_SUPER): rphf_code = ( f"feature rphf {{\n" f" script dev2;\n" - f" sub {glyph_name(0x0930)} {glyph_name(SC.DEVANAGARI_VIRAMA)} by {glyph_name(SC.DEVANAGARI_RA_SUPER)};\n" + f" sub {glyph_name(ra_int)} {glyph_name(SC.DEVANAGARI_VIRAMA)} by {glyph_name(SC.DEVANAGARI_RA_SUPER)};\n" f"}} rphf;" ) features.append(rphf_code)