"""Support for the `bgcl` (Apple Color Emoji background) table.
This table stores a JSON blob. We decode it to a Python object on
decompile and emit human readable JSON in the TTX via a <json> element.
On compile we re-encode the JSON to UTF-8 bytes which is what Apple code
reads via CTFontCopyTable then JSONDecoder.
On iOS 16 and later the `bgcl` payload is used as the wallpaper
background when the user selects an emoji wallpaper.
Fields:
- ``colors``: list of palette entries. Each entry is an array of four
integers ``[R, G, B, A]``. R/G/B are 0-255. A is 0-1.
- ``emojicolors``: list of per-emoji palettes. Each item is an array of
three sublists: primary/dominant, accent, contextual
(names inferred). Each sublist contains integer indexes referencing
entries in ``colors``; the runtime uses these to assemble layered
background tints for an emoji.
- ``indexmap``: mapping (glyph identifier → palette index). The map
maps a glyph identity to an integer index selecting an entry in
``emojicolors``. The font/UI uses this to pick the correct palette
for a glyph when rendering wallpaper backgrounds.
- ``version``: integer table version used for parsing/compatibility.
Runtime usage summary:
- The system fetches the table bytes with ``CTFontCopyTable('bgcl')``,
decodes the bytes as UTF‑8 JSON and runs the JSON through the app's
decoder into an internal ``BgclTable`` structure. The app looks up a
glyph's entry in ``indexmap``, retrieves the corresponding
``emojicolors`` palette, then converts the referenced ``colors``
entries into color objects (normalizing channels/alpha as needed).
This resulting color(s) drive the wallpaper background appearance for
emoji wallpapers on supported iOS versions.
This implementation preserves the JSON payload and exposes convenience
attributes ``colors``, ``emojicolors``, ``indexmap`` and ``version``
when available.
"""
from __future__ import annotations
from typing import TYPE_CHECKING
from fontTools.misc.textTools import tostr, strjoin
from . import DefaultTable
import json
if TYPE_CHECKING:
from fontTools.misc.xmlWriter import XMLWriter
from fontTools.ttLib import TTFont
[docs]
class table__b_g_c_l(DefaultTable.DefaultTable):
"""bgcl table: stores a JSON blob describing background palettes.
The JSON structure typically contains the top-level keys:
- colors: [[R,G,B,A], ...]
- emojicolors: [[[dominant...],[accent...],[contextual...]], ...]
- indexmap: {"glyphIndex": emojicolors_index, ...}
- version: int
"""
[docs]
def decompile(self, data: bytes, ttFont: TTFont) -> None:
"""Store raw bytes and attempt to parse JSON.
The JSON commonly includes palette/lookup data used at runtime to
construct wallpaper/background colors for emoji wallpapers on
recent iOS releases.
"""
self.data = data
try:
text = tostr(data, "utf_8")
self.json = json.loads(text)
except Exception as e: # keep table decompilation robust
self.json = None
self.ERROR = f"bgcl JSON parse error: {e!r}"
return
# convenient attributes
self.colors = self.json.get("colors")
self.emojicolors = self.json.get("emojicolors")
self.indexmap = self.json.get("indexmap")
self.version = self.json.get("version")
[docs]
def compile(self, ttFont: TTFont) -> bytes:
"""Encode the JSON object to UTF-8 bytes for font binary storage."""
if getattr(self, "json", None) is None:
# fallback to raw bytes if parsing failed earlier
return getattr(self, "data", b"")
# use compact representation for binary table
return json.dumps(self.json, separators=(",", ":"), ensure_ascii=False).encode(
"utf_8"
)
[docs]
def toXML(self, writer: XMLWriter, ttFont: TTFont) -> None:
"""Emit pretty-printed JSON inside a <json> element for human inspection."""
if getattr(self, "json", None) is None:
# fallback to default hex output
DefaultTable.DefaultTable.toXML(self, writer, ttFont)
return
writer.begintag("json")
writer.newline()
pretty = json.dumps(self.json, indent=2, ensure_ascii=False)
writer.writecdata(pretty)
writer.newline()
writer.endtag("json")
writer.newline()
[docs]
def fromXML(
self, name: str, attrs: dict[str, str], content, ttFont: TTFont
) -> None:
"""Read JSON from the <json> element. `content` may be a list.
This mirrors SVG/other table `fromXML` handlers which accept a
list of content chunks.
"""
if name != "json":
# fall back to DefaultTable behavior for unknown elements
return DefaultTable.DefaultTable.fromXML(self, name, attrs, content, ttFont)
text = strjoin(content).strip()
try:
self.json = json.loads(text)
# keep raw bytes in sync
self.data = text.encode("utf_8")
self.colors = self.json.get("colors")
self.emojicolors = self.json.get("emojicolors")
self.indexmap = self.json.get("indexmap")
self.version = self.json.get("version")
except Exception as e:
# store error and fall back to raw
self.json = None
self.ERROR = f"bgcl JSON parse error in fromXML: {e!r}"