Merge a monochrome Noto Emoji subset into the text fonts so chat messages, the composer, contact names, and memos render emoji (😀 🎉 ❤ 🔥 👍 …) instead of tofu. - Enable IMGUI_USE_WCHAR32 (imconfig.h): emoji live above the BMP (U+1F300+), so 16-bit ImWchar literally can't address them. This widens ImWchar build-wide; the only ImWchar uses in-tree are glyph-range arrays and one BMP private-use codepoint, so nothing else is affected. Tests + full build pass. - Bundle res/fonts/NotoEmoji-Subset.ttf — the OFL monochrome Noto Emoji (color CBDT/COLR fonts can't be rasterized by ImGui's stb_truetype) pinned to wght=400 and subset to the emoji planes (1411 glyphs, 747 KB). Reproducible via scripts/build_emoji_subset.py. Embedded via INCBIN like the CJK subset. - Typography::loadFont merges it (MergeMode) only into the small text fonts (Body/Subtitle/Caption/Button) — not headers, which don't need 1400 emoji. The base font keeps precedence for U+2600–26FF, so text-style symbols stay. Limits: ImGui does no shaping, so single-codepoint emoji render but ZWJ sequences (family/profession) and regional-indicator flags won't compose; emoji are monochrome (the OS emoji picker still inputs them fine, and the composer byte counter already counts their 4-byte UTF-8 cost against the on-chain cap). Verified headless: sizeof(ImWchar)==4 and every probed emoji is in-font and bakes into the atlas. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
88 lines
3.5 KiB
Python
88 lines
3.5 KiB
Python
#!/usr/bin/env python3
|
||
"""
|
||
Build a monochrome Noto Emoji subset for the chat/message UI.
|
||
|
||
Dear ImGui rasterizes fonts with stb_truetype, which handles only monochrome
|
||
(outline `glyf`) fonts — NOT color emoji (CBDT/sbix/COLR). So we use Google's
|
||
*monochrome* Noto Emoji (github.com/google/fonts, ofl/notoemoji, OFL-licensed)
|
||
and merge it into the text fonts (see Typography::loadFont, the block after the
|
||
CJK merge). ImGui renders one glyph per codepoint with no shaping, so ZWJ
|
||
sequences / regional-indicator flags won't compose — single-codepoint emoji
|
||
(😀 🎉 ❤ 🔥 👍 …) render fine, which covers the overwhelming majority of use.
|
||
|
||
Source is the variable font pinned to wght=400 → static, then subset to the
|
||
emoji planes plus the higher symbol/star ranges the base UI font doesn't cover.
|
||
The base Ubuntu font already owns U+2600–26FF etc.; ImGui's MergeMode gives the
|
||
first-loaded glyph precedence, so those stay text-styled and only the codepoints
|
||
the base lacks fall through to this font.
|
||
|
||
Get the source once (OFL, redistributable):
|
||
curl -fsSL -o /tmp/NotoEmoji-VF.ttf \
|
||
'https://github.com/google/fonts/raw/main/ofl/notoemoji/NotoEmoji%5Bwght%5D.ttf'
|
||
|
||
Then: python3 scripts/build_emoji_subset.py
|
||
Output: res/fonts/NotoEmoji-Subset.ttf (committed; embedded via INCBIN)
|
||
"""
|
||
import os
|
||
from fontTools import ttLib, subset
|
||
from fontTools.varLib.instancer import instantiateVariableFont
|
||
|
||
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||
SOURCE_VF = '/tmp/NotoEmoji-VF.ttf'
|
||
STATIC = '/tmp/NotoEmoji-Static.ttf'
|
||
OUTPUT = os.path.join(ROOT, 'res', 'fonts', 'NotoEmoji-Subset.ttf')
|
||
|
||
# Emoji codepoints to keep. Ranges are inclusive.
|
||
RANGES = [
|
||
(0x1F000, 0x1FAFF), # all the main emoji planes (emoticons, pictographs, transport, supplement, extended)
|
||
(0x2600, 0x27BF), # Miscellaneous Symbols + Dingbats
|
||
(0x2B00, 0x2BFF), # stars (⭐ 2B50) and misc arrows
|
||
(0xFE00, 0xFE0F), # variation selectors (VS16 emoji-style)
|
||
(0x2194, 0x21AA), # arrows used as emoji
|
||
(0x231A, 0x231B), # ⌚ ⌛
|
||
(0x23E9, 0x23FA), # media-control emoji
|
||
(0x25AA, 0x25FE), # small squares
|
||
]
|
||
SINGLES = [0x200D, 0x2934, 0x2935, 0x3030, 0x303D, 0x3297, 0x3299,
|
||
0x00A9, 0x00AE, 0x2122, 0x2139, 0x24C2]
|
||
|
||
|
||
def main():
|
||
if not os.path.exists(SOURCE_VF):
|
||
raise SystemExit(f"missing source font {SOURCE_VF} — see the header for the curl command")
|
||
|
||
# 1. Pin the weight axis so stb_truetype rasterizes a clean static instance.
|
||
f = ttLib.TTFont(SOURCE_VF)
|
||
if 'fvar' in f:
|
||
instantiateVariableFont(f, {'wght': 400}, inplace=True)
|
||
f.save(STATIC)
|
||
|
||
unicodes = list(SINGLES)
|
||
for lo, hi in RANGES:
|
||
unicodes.extend(range(lo, hi + 1))
|
||
|
||
opts = subset.Options()
|
||
opts.layout_features = [] # ImGui does no shaping — drop GSUB/GPOS
|
||
opts.name_IDs = []
|
||
opts.notdef_outline = True
|
||
opts.glyph_names = False
|
||
opts.drop_tables = ['GSUB', 'GPOS', 'GDEF', 'morx', 'kern']
|
||
|
||
font = subset.load_font(STATIC, opts)
|
||
ss = subset.Subsetter(options=opts)
|
||
ss.populate(unicodes=unicodes)
|
||
ss.subset(font)
|
||
subset.save_font(font, OUTPUT, opts)
|
||
|
||
out = ttLib.TTFont(OUTPUT)
|
||
cmap = out.getBestCmap()
|
||
color = any(t in out.reader.keys() for t in ('CBDT', 'sbix', 'COLR'))
|
||
print(f"Output: {OUTPUT}")
|
||
print(f"Size: {os.path.getsize(OUTPUT)//1024} KB | glyphs: {len(cmap)} | color tables: {color}")
|
||
if color:
|
||
raise SystemExit("ERROR: subset has color tables — stb_truetype cannot render it")
|
||
|
||
|
||
if __name__ == '__main__':
|
||
main()
|