From b106e1c1b014020c8ef2358c922070666cb43470 Mon Sep 17 00:00:00 2001 From: minjaesong Date: Wed, 4 Mar 2026 20:44:16 +0900 Subject: [PATCH] coloured font with COLRv0 --- OTFbuild/calligra_font_tests.odt | Bin 17233 -> 16677 bytes OTFbuild/font_builder.py | 134 +++++++++++++++++++- OTFbuild/glyph_parser.py | 45 ++++++- src/assets/devanagari_variable.tga | 2 +- src/assets/halfwidth_fullwidth_variable.tga | 2 +- work_files/devanagari_variable.psd | 2 +- work_files/halfwidth_fullwidth_variable.psd | 4 +- 7 files changed, 176 insertions(+), 13 deletions(-) diff --git a/OTFbuild/calligra_font_tests.odt b/OTFbuild/calligra_font_tests.odt index 0e47f490000d9d628c44f301d690026e78a40527..953010108cf8ba12d0815e25836ee99d2f523cbb 100644 GIT binary patch delta 15464 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 diff --git a/OTFbuild/font_builder.py b/OTFbuild/font_builder.py index db13b04..a894764 100644 --- a/OTFbuild/font_builder.py +++ b/OTFbuild/font_builder.py @@ -28,6 +28,7 @@ from keming_machine import generate_kerning_pairs from opentype_features import generate_features, glyph_name import sheet_config as SC +FONT_VERSION = "1.15" # Codepoints that get cmap entries (user-visible) # PUA forms used internally by GSUB get glyphs but NO cmap entries @@ -124,6 +125,7 @@ def build_font(assets_dir, output_path, no_bitmap=False, no_features=False): 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 + uni_g.color_bitmap = pua_g.color_bitmap deva_copied += 1 # Also copy nukta consonant forms U+0958-095F for uni_cp in range(0x0958, 0x0960): @@ -137,6 +139,7 @@ def build_font(assets_dir, output_path, no_bitmap=False, no_features=False): 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 + uni_g.color_bitmap = pua_g.color_bitmap deva_copied += 1 print(f" Copied {deva_copied} consonant glyphs from PUA forms") @@ -229,6 +232,79 @@ def build_font(assets_dir, output_path, no_bitmap=False, no_features=False): print(f" Glyph order: {len(glyph_order)} glyphs, cmap: {len(cmap)} entries") + # Step 4a: Detect coloured glyphs and prepare COLR layer data + print("Step 4a: Detecting coloured glyphs...") + colr_layer_data = {} # base_name -> list of (layer_name, colour_rgb) + palette_colours = {} # (r, g, b) -> palette_index + layer_bitmaps = {} # layer_name -> 1-bit bitmap + layer_insert = [] # (after_name, [layer_names]) for glyph_order insertion + + for cp in sorted_cps: + g = glyphs[cp] + if g.props.is_illegal or g.color_bitmap is None: + continue + name = glyph_name(cp) + if name == ".notdef" or name not in glyph_set: + continue + + # Group pixels by RGB value -> per-colour 1-bit masks + colour_pixels = {} # (r, g, b) -> set of (row, col) + cbm = g.color_bitmap + for row in range(len(cbm)): + for col in range(len(cbm[row])): + px = cbm[row][col] + a = px & 0xFF + if a == 0: + continue + r = (px >> 24) & 0xFF + g_ch = (px >> 16) & 0xFF + b = (px >> 8) & 0xFF + rgb = (r, g_ch, b) + if rgb not in colour_pixels: + colour_pixels[rgb] = set() + colour_pixels[rgb].add((row, col)) + + if not colour_pixels: + continue + if len(colour_pixels) == 1 and (255, 255, 255) in colour_pixels: + # Only white pixels — no colour layers needed + continue + + # Assign palette indices for each unique colour + for rgb in colour_pixels: + if rgb not in palette_colours: + palette_colours[rgb] = len(palette_colours) + + # Generate layer glyphs + h = len(cbm) + w = len(cbm[0]) if h > 0 else 0 + layers = [] + layer_names = [] + for i, (rgb, positions) in enumerate(sorted(colour_pixels.items())): + layer_name = f"{name}.clr{i}" + # Build 1-bit mask for this colour + mask = [[0] * w for _ in range(h)] + for (row, col) in positions: + mask[row][col] = 1 + layer_bitmaps[layer_name] = mask + layers.append((layer_name, rgb)) + layer_names.append(layer_name) + + colr_layer_data[name] = layers + layer_insert.append((name, layer_names)) + + # Insert layer glyph names into glyph_order immediately after their base glyph + for base_name, lnames in layer_insert: + idx = glyph_order.index(base_name) + for j, ln in enumerate(lnames): + glyph_order.insert(idx + 1 + j, ln) + glyph_set.add(ln) + + if colr_layer_data: + print(f" Found {len(colr_layer_data)} coloured glyphs, {len(palette_colours)} palette colours, {sum(len(v) for v in colr_layer_data.values())} layer glyphs") + else: + print(" No coloured glyphs found") + # Step 5: Build font with fonttools (CFF/OTF) print("Step 5: Building font tables...") fb = FontBuilder(SC.UNITS_PER_EM, isTTF=False) @@ -256,6 +332,7 @@ def build_font(assets_dir, output_path, no_bitmap=False, no_features=False): charstrings[".notdef"] = pen.getCharString() _unihan_cps = set(SC.CODE_RANGE[SC.SHEET_UNIHAN]) + _base_offsets = {} # glyph_name -> (x_offset, y_offset) for COLR layers traced_count = 0 for cp in sorted_cps: @@ -314,6 +391,10 @@ def build_font(assets_dir, output_path, no_bitmap=False, no_features=False): if 15 <= _pua_row <= 18: x_offset -= SC.W_HANGUL_BASE * SCALE + # Store offsets for COLR layer glyphs + if name in colr_layer_data: + _base_offsets[name] = (x_offset, y_offset) + contours = trace_bitmap(g.bitmap, g.props.width) pen = T2CharStringPen(advance, None) @@ -322,7 +403,22 @@ def build_font(assets_dir, output_path, no_bitmap=False, no_features=False): traced_count += 1 charstrings[name] = pen.getCharString() - print(f" Traced {traced_count} glyphs with outlines") + # Trace COLR layer glyphs + layer_traced = 0 + for base_name, layers in colr_layer_data.items(): + base_xoff, base_yoff = _base_offsets.get(base_name, (0, 0)) + for layer_name, _rgb in layers: + lbm = layer_bitmaps[layer_name] + # Find the effective glyph width from the base glyph's bitmap + lw = len(lbm[0]) if lbm and lbm[0] else 0 + contours = trace_bitmap(lbm, lw) + pen = T2CharStringPen(0, None) # advance width 0 for layers + if contours: + draw_glyph_to_pen(contours, pen, x_offset=base_xoff, y_offset=base_yoff) + layer_traced += 1 + charstrings[layer_name] = pen.getCharString() + + print(f" Traced {traced_count} glyphs with outlines" + (f" + {layer_traced} colour layers" if layer_traced else "")) fb.setupCFF( psName="TerrarumSansBitmap-Regular", @@ -346,6 +442,11 @@ def build_font(assets_dir, output_path, no_bitmap=False, no_features=False): advance = 0 if cp in mark_cps else g.props.width * SCALE metrics[name] = (advance, 0) + # Add zero-advance metrics for COLR layer glyphs + for _base_name, layers in colr_layer_data.items(): + for layer_name, _rgb in layers: + metrics[layer_name] = (0, 0) + fb.setupHorizontalMetrics(metrics) fb.setupHorizontalHeader( ascent=SC.ASCENT, @@ -353,15 +454,15 @@ def build_font(assets_dir, output_path, no_bitmap=False, no_features=False): ) fb.setupNameTable({ - "copyright": "CuriousTorvald", + "copyright": "Copyright (c) 2026 CuriousTorvald (curioustorvald.com), with Reserved Font Name Terrarum.", "familyName": "Terrarum Sans Bitmap", "styleName": "Regular", - "uniqueFontIdentifier": "TerrarumSansBitmap-Regular-1.15", + "uniqueFontIdentifier": "TerrarumSansBitmap-Regular-"+FONT_VERSION, "fullName": "Terrarum Sans Bitmap Regular", "psName": "TerrarumSansBitmap-Regular", - "version": "1.15", - "licenseDescription": "SIL Open Font License, Version 1.1", - "licenseInfoURL": "http://scripts.sil.org/OFL" + "version": FONT_VERSION, + "licenseDescription": "This Font Software is licensed under the SIL Open Font License, Version 1.1.", + "licenseInfoURL": "https://openfontlicense.org/" }) fb.setupOS2( @@ -387,6 +488,27 @@ def build_font(assets_dir, output_path, no_bitmap=False, no_features=False): font = fb.font + # Step 7a: Build COLR v0 / CPAL tables + if colr_layer_data: + print("Step 7a: Building COLR v0/CPAL tables...") + from fontTools.colorLib.builder import buildCOLR, buildCPAL + + # CPAL: single palette normalised to 0..1 + palette = [(0, 0, 0, 1.0)] * len(palette_colours) + for (r, g, b), idx in palette_colours.items(): + palette[idx] = (r / 255, g / 255, b / 255, 1.0) + font["CPAL"] = buildCPAL([palette]) + + # COLR v0: list of (layer_glyph_name, palette_index) per base glyph + colr_v0 = {} + for base_name, layers in colr_layer_data.items(): + colr_v0[base_name] = [ + (layer_name, palette_colours[rgb]) + for layer_name, rgb in layers + ] + font["COLR"] = buildCOLR(colr_v0, version=0) + print(f" COLR v0: {len(colr_v0)} base glyphs, {len(palette)} palette entries") + # Step 8: Generate and compile OpenType features if not no_features: print("Step 8: Generating OpenType features...") diff --git a/OTFbuild/glyph_parser.py b/OTFbuild/glyph_parser.py index e31b568..f9cb99f 100644 --- a/OTFbuild/glyph_parser.py +++ b/OTFbuild/glyph_parser.py @@ -64,6 +64,18 @@ class ExtractedGlyph: codepoint: int props: GlyphProps bitmap: List[List[int]] # [row][col], 0 or 1 + color_bitmap: Optional[List[List[int]]] = None # [row][col], RGBA8888 values + + +def _is_coloured_pixel(px): + """Return True if the pixel is visible (A > 0) and non-white (R+G+B < 765).""" + a = px & 0xFF + if a == 0: + return False + r = (px >> 24) & 0xFF + g = (px >> 16) & 0xFF + b = (px >> 8) & 0xFF + return (r + g + b) < 765 def _tagify(pixel): @@ -215,7 +227,28 @@ def parse_variable_sheet(image, sheet_index, cell_w, cell_h, cols, is_xy_swapped for row in range(cell_h): bitmap[row][col_idx] = 0 - result[code] = ExtractedGlyph(code, props, bitmap) + # Colour extraction: check if any visible pixel is non-white + has_colour = False + color_bitmap = [] + for row in range(cell_h): + row_data = [] + for col in range(max_w): + px = image.get_pixel(cell_x + col, cell_y + row) + row_data.append(px) + if not has_colour and _is_coloured_pixel(px): + has_colour = True + color_bitmap.append(row_data) + + if has_colour: + # Strip extInfo columns from color_bitmap too + if ext_count > 0: + for col_idx in range(min(ext_count, max_w)): + for row in range(cell_h): + color_bitmap[row][col_idx] = 0 + else: + color_bitmap = None + + result[code] = ExtractedGlyph(code, props, bitmap, color_bitmap) return result @@ -321,15 +354,23 @@ def parse_fixed_sheet(image, sheet_index, cell_w, cell_h, cols): cell_y = (index // cols) * cell_h bitmap = [] + has_colour = False + color_bitmap = [] for row in range(cell_h): row_data = [] + color_row = [] for col in range(cell_w): px = image.get_pixel(cell_x + col, cell_y + row) row_data.append(1 if (px & 0xFF) != 0 else 0) + color_row.append(px) + if not has_colour and _is_coloured_pixel(px): + has_colour = True bitmap.append(row_data) + color_bitmap.append(color_row) props = GlyphProps(width=fixed_width) - result[code] = ExtractedGlyph(code, props, bitmap) + result[code] = ExtractedGlyph(code, props, bitmap, + color_bitmap if has_colour else None) return result diff --git a/src/assets/devanagari_variable.tga b/src/assets/devanagari_variable.tga index 01c46b4..ae86b1d 100644 --- a/src/assets/devanagari_variable.tga +++ b/src/assets/devanagari_variable.tga @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:bab806c7e407feb3de23d28360d5a71c1bbf2ed4f524aacf9a4dcab939bcb712 +oid sha256:3f287df9be6928e213a959576fe1f306bd5e1ffe290c41d797e0e29e2843a259 size 1474578 diff --git a/src/assets/halfwidth_fullwidth_variable.tga b/src/assets/halfwidth_fullwidth_variable.tga index ae00a6a..85fc33b 100644 --- a/src/assets/halfwidth_fullwidth_variable.tga +++ b/src/assets/halfwidth_fullwidth_variable.tga @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:805b0b662b81146136313f3c685cedc583a0aeb96fd374f6450ca5968343478a +oid sha256:d861b883d2fd42df8499085c087eb85b75ad1388f2fb470a598e180aba36953f size 327698 diff --git a/work_files/devanagari_variable.psd b/work_files/devanagari_variable.psd index 66b7a6c..4ce4652 100644 --- a/work_files/devanagari_variable.psd +++ b/work_files/devanagari_variable.psd @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6171a566806ad2b2a803b6d479946a720eee4a2149df4ffb968c3b0454c22965 +oid sha256:eeb5fc60ac1700587642d856be6fb9650f07e1a6de4d8cb0a5251a53b52158e9 size 1453738 diff --git a/work_files/halfwidth_fullwidth_variable.psd b/work_files/halfwidth_fullwidth_variable.psd index 39c2924..123b30f 100644 --- a/work_files/halfwidth_fullwidth_variable.psd +++ b/work_files/halfwidth_fullwidth_variable.psd @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:13a06015fc6fd8afaccf1873c1fc2a3209cd24677754cf066f14823d8ab5a4db -size 365504 +oid sha256:293d473bf520c97c8f3af5d1ec12e77fa54471133c8d4cc8155ab20dcc5df105 +size 365776