summaryrefslogtreecommitdiffstats
path: root/contrib/python/fonttools/fontTools/ttLib/tables/_b_g_c_l.py
blob: 7d4a2ffbfb292bbddf5ee65dd42bbc4f03eb7fe6 (plain) (blame)
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}"