That’s something you can do with local tooling, assuming you’re using a font bundled with your package. You can had an AI generate a quick tool to scan the TTF and output ranges of glyphs that are supported, then build a function to map those ranges to styles and convert a string. You only need to supply styles for the fallbacks, using your default font.
In fact, here’s a Python3 script to do that:
#!/usr/bin/env python3
"""
ttf_unicode_ranges.py
Read a TrueType/OpenType font and print the ranges of Unicode code points
that are encoded in its 'cmap' table, as a single flat JSON array of
alternating begin/end values, where each end is exclusive:
[begin0, end0, begin1, end1, ...]
Usage:
python3 ttf_unicode_ranges.py FONT.ttf [--hex]
Requires:
pip install fonttools
"""
import argparse
import json
import sys
from fontTools.ttLib import TTFont
def get_encoded_codepoints(font_path):
"""Return a sorted set of all Unicode code points encoded by the font."""
font = TTFont(font_path, fontNumber=0, lazy=True)
if "cmap" not in font:
raise ValueError("Font has no 'cmap' table; cannot determine encoding.")
codepoints = set()
best = font.getBestCmap()
if best:
codepoints.update(best.keys())
for table in font["cmap"].tables:
if table.isUnicode():
codepoints.update(table.cmap.keys())
font.close()
return sorted(codepoints)
def to_flat_ranges(sorted_codepoints):
"""Collapse sorted ints into a flat [begin0, end0, begin1, end1, ...] list.
Each end is exclusive (one past the last code point in the range).
"""
flat = []
start = prev = None
for cp in sorted_codepoints:
if start is None:
start = prev = cp
elif cp == prev + 1:
prev = cp
else:
flat.extend((start, prev + 1)) # end is exclusive
start = prev = cp
if start is not None:
flat.extend((start, prev + 1))
return flat
def main(argv=None):
parser = argparse.ArgumentParser(
description="Output the encoded Unicode ranges of a font as a flat "
"JSON array of alternating begin/end values."
)
parser.add_argument("font", help="Path to the .ttf or .otf font file")
parser.add_argument("--hex", action="store_true",
help='Emit bounds as hex strings (e.g. "U+0041") '
"instead of integers")
args = parser.parse_args(argv)
try:
codepoints = get_encoded_codepoints(args.font)
except Exception as exc: # noqa: BLE001
print(f"Error: {exc}", file=sys.stderr)
return 1
flat = to_flat_ranges(codepoints)
if args.hex:
flat = [f"U+{v:04X}" for v in flat]
# Compact, single-line JSON with no nesting.
print(json.dumps(flat, separators=(",", ":"), ensure_ascii=False))
return 0
if __name__ == "__main__":
sys.exit(main())
which outputs like
❯ python3 ttf_unicode_ranges.py assets/fonts/Inconsolata-Regular.ttf
[0,1,13,14,32,127,160,306,308,329,330,383,399,400,402,403,408,409,416,418,431,433,440,442,455,458,486,488,490,492,506,540,554,558,560,564,567,568,589,590,601,602,635,636,664,665,666,667,697,701,702,704,710,717,728,734,768,773,774,781,783,784,785,787,795,796,803,805,806,809,814,815,817,818,821,823,885,886,7688,7690,7692,7696,7700,7704,7708,7710,7712,7714,7716,7718,7722,7724,7726,7728,7734,7736,7738,7740,7746,7754,7756,7764,7770,7772,7774,7786,7788,7792,7800,7804,7808,7814,7822,7824,7826,7828,7831,7832,7838,7839,7840,7930,8199,8204,8208,8209,8210,8214,8216,8219,8220,8223,8224,8227,8230,8231,8240,8241,8242,8244,8249,8251,8260,8261,8304,8305,8308,8314,8315,8316,8319,8330,8353,8354,8355,8357,8358,8360,8361,8362,8363,8366,8369,8371,8373,8374,8377,8379,8380,8382,8467,8468,8470,8471,8482,8483,8484,8485,8486,8487,8494,8495,8592,8602,8678,8683,8706,8707,8709,8711,8712,8713,8719,8720,8721,8723,8725,8726,8727,8728,8729,8731,8734,8735,8747,8748,8776,8777,8800,8801,8804,8806,8853,8854,8962,8963,8984,8985,8997,9000,9003,9004,9099,9100,9166,9167,9251,9252,9472,9632,9670,9672,9674,9676,9679,9680,9785,9788,9824,9825,9827,9828,9829,9831,10003,10006,10007,10009,11013,11016,11157,11158,11794,11795,65533,65534]