"""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 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 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 """ 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") 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" ) def toXML(self, writer: XMLWriter, ttFont: TTFont) -> None: """Emit pretty-printed JSON inside a 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() def fromXML( self, name: str, attrs: dict[str, str], content, ttFont: TTFont ) -> None: """Read JSON from the 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}"