Fallback Fonts

Most operating systems have the concept of a fallback font, so if a glyph can’t be rendered by the selected font, it will fall back to a different font. This feature is missing in Roku OS, so if I’m using the default system fonts or a custom font with only Latin-1 glyphs and I try to render some non Latin-1 characters, the missing glyphs are rendered as empty rectangles. It would be a valuable feature to be able to specify a list of fallback fonts so we can install a single font to handle non Latin-1 glyphs rather than expanding all of the fonts with a larger set of glyphs.

We could potentially inspect every string’s entire set of codepoints to ensure that the font will render it, but that would be detrimental to performance. If ifFont was extended to add a method that would return the list of codepoints in a string that can’t be rendered, we could load a different font for that string.

This feature actually already exists at a lower level in the Roku OS, as we use this to provide additional Roku OS-specific glyphs for all fonts, but we don’t currently have support for adding a second level of font searching. To support this in SceneGraph, we’d need to extend the Font RSG node ( Font ) to take a fallback font specification in addition to the main one. Do you think that it would also need its own size parameter or could always use the one from the main font?

I do worry that this could lead to many application packages including something like the Google Noto font, increasing the size of every app and making less room to store other apps. We’d looked at putting something like that in firmware, but the size is prohibitive. A single weight of Noto Sans is around 630K, which isn’t that big on its own, but adds up if it gets duplicated, and that’s for a font that doesn’t cover any eastern Asian languages. Our text rendering code has only been validated for left-to-right languages too.

One interesting potential use case I’ve seen is using something like GitHub - hfg-gmuend/openmoji: Open source emojis for designers, developers and everyone else! · GitHub to fallback for emojis. I know there is likely more then just dropping one of these files in and it working for the colored versions. I’ve not yet tested myself. But this is just an example.

Yes, I assume it would use the font nodes size. But others can chime in if they have more experience working with fallback fonts and what the norm would be.

Also what some other platforms do is they can fallback to the system in the cases where the custom font does not have the correct glyph before showing the missing glyph.

To the point about package size. I think thats really up to the developer to manage since they already need to account for the 4mb package limit. That shouldn’t stop us from being able to do it if we have the space for it.

Overall I am in favor of this change.

Hi, @Chris-DP!

We actually have our own special symbol font in Roku OS (“RokuFont01”) that’s already taking up the fallback slot in our low-level renderer. Details on what glyphs are in that are part of the new TS SDK that publishes next month - mostly it’s things like remote control glyphs

To support this request, we need to change the single-slot fallback implementation into a multi-level one, as we want the choice of which glyph is used to happen down at the level where we’re generating text metrics. I’ll make an internal proposal for it, but can’t promise anything.

My use case is that we support 3rd party content with some titles containing emojis, non-Latin-1 characters or “fun” glyphs Lɪᴋᴇ ᴛʜɪs, ᵒʳ ᵗʰᶦˢ, ₒᵣ ₑᵥₑₙ ₜₕᵢₛ.

Right now, we end up displaying empty rectangles. If we could at least know that some glyphs won’t render, we could find an alternate title. My other suggestion was that iFont be extended to support a fast way of checking if all of the glyphs in a string can be rendered. Currently, we’d have to use Asc() to check every character of every title string to make sure they are within the supported range, and I know that would be very slow.

What if we provided a way to render this efficiently using the MultiStyleLabel control? That can already tag different parts of a string in a different font, so you’d just need a way to identify what parts of your string need to be mapped into which glyph set?

I’d still need a way to identify which parts of the string wouldn’t render with a given font.

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]

I have the opposite problem. I don’t know what code points are in the title strings, so I won’t know if the string will render properly with a given font. If I can’t solve it with fallback fonts like I do on every other platform, I would at least like to know if the font can render the string so I can select the appropriate font or find an alternate title. I could scan through each string with Asc() to validate the code points, but that would be too slow.

Right, but effectively by using a fallback font, you’re asking the OS code to do that same character-by-character scan through the string. Ultimately, when the string is drawn to the string, the code takes the string apart, maps each character to a glyph structure inside of Freetype, then uses that draw from a cached bitmap or to follow the TrueType instructions to draw the character to the cache, then from there to the screen.

Doing the scan in BrightScript using generated code with output for a MultiStyleLabel is something you can do in your code today. Having us add a new feature is something that would have to get designed, then planned for a release, then justified through our API review process, etc, and likely wouldn’t be available on a device for you to use for at least nine months.