1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
|
"""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
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 <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()
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}"
|