• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1# Copyright 2013 Google, Inc. All Rights Reserved.
2#
3# Google Author(s): Behdad Esfahbod
4
5from fontTools.misc.textTools import bytesjoin, safeEval
6from . import DefaultTable
7import array
8from collections import namedtuple
9import struct
10import sys
11
12
13class table_C_P_A_L_(DefaultTable.DefaultTable):
14    NO_NAME_ID = 0xFFFF
15    DEFAULT_PALETTE_TYPE = 0
16
17    def __init__(self, tag=None):
18        DefaultTable.DefaultTable.__init__(self, tag)
19        self.palettes = []
20        self.paletteTypes = []
21        self.paletteLabels = []
22        self.paletteEntryLabels = []
23
24    def decompile(self, data, ttFont):
25        (
26            self.version,
27            self.numPaletteEntries,
28            numPalettes,
29            numColorRecords,
30            goffsetFirstColorRecord,
31        ) = struct.unpack(">HHHHL", data[:12])
32        assert (
33            self.version <= 1
34        ), "Version of CPAL table is higher than I know how to handle"
35        self.palettes = []
36        pos = 12
37        for i in range(numPalettes):
38            startIndex = struct.unpack(">H", data[pos : pos + 2])[0]
39            assert startIndex + self.numPaletteEntries <= numColorRecords
40            pos += 2
41            palette = []
42            ppos = goffsetFirstColorRecord + startIndex * 4
43            for j in range(self.numPaletteEntries):
44                palette.append(Color(*struct.unpack(">BBBB", data[ppos : ppos + 4])))
45                ppos += 4
46            self.palettes.append(palette)
47        if self.version == 0:
48            offsetToPaletteTypeArray = 0
49            offsetToPaletteLabelArray = 0
50            offsetToPaletteEntryLabelArray = 0
51        else:
52            pos = 12 + numPalettes * 2
53            (
54                offsetToPaletteTypeArray,
55                offsetToPaletteLabelArray,
56                offsetToPaletteEntryLabelArray,
57            ) = struct.unpack(">LLL", data[pos : pos + 12])
58        self.paletteTypes = self._decompileUInt32Array(
59            data,
60            offsetToPaletteTypeArray,
61            numPalettes,
62            default=self.DEFAULT_PALETTE_TYPE,
63        )
64        self.paletteLabels = self._decompileUInt16Array(
65            data, offsetToPaletteLabelArray, numPalettes, default=self.NO_NAME_ID
66        )
67        self.paletteEntryLabels = self._decompileUInt16Array(
68            data,
69            offsetToPaletteEntryLabelArray,
70            self.numPaletteEntries,
71            default=self.NO_NAME_ID,
72        )
73
74    def _decompileUInt16Array(self, data, offset, numElements, default=0):
75        if offset == 0:
76            return [default] * numElements
77        result = array.array("H", data[offset : offset + 2 * numElements])
78        if sys.byteorder != "big":
79            result.byteswap()
80        assert len(result) == numElements, result
81        return result.tolist()
82
83    def _decompileUInt32Array(self, data, offset, numElements, default=0):
84        if offset == 0:
85            return [default] * numElements
86        result = array.array("I", data[offset : offset + 4 * numElements])
87        if sys.byteorder != "big":
88            result.byteswap()
89        assert len(result) == numElements, result
90        return result.tolist()
91
92    def compile(self, ttFont):
93        colorRecordIndices, colorRecords = self._compileColorRecords()
94        paletteTypes = self._compilePaletteTypes()
95        paletteLabels = self._compilePaletteLabels()
96        paletteEntryLabels = self._compilePaletteEntryLabels()
97        numColorRecords = len(colorRecords) // 4
98        offsetToFirstColorRecord = 12 + len(colorRecordIndices)
99        if self.version >= 1:
100            offsetToFirstColorRecord += 12
101        header = struct.pack(
102            ">HHHHL",
103            self.version,
104            self.numPaletteEntries,
105            len(self.palettes),
106            numColorRecords,
107            offsetToFirstColorRecord,
108        )
109        if self.version == 0:
110            dataList = [header, colorRecordIndices, colorRecords]
111        else:
112            pos = offsetToFirstColorRecord + len(colorRecords)
113            if len(paletteTypes) == 0:
114                offsetToPaletteTypeArray = 0
115            else:
116                offsetToPaletteTypeArray = pos
117                pos += len(paletteTypes)
118            if len(paletteLabels) == 0:
119                offsetToPaletteLabelArray = 0
120            else:
121                offsetToPaletteLabelArray = pos
122                pos += len(paletteLabels)
123            if len(paletteEntryLabels) == 0:
124                offsetToPaletteEntryLabelArray = 0
125            else:
126                offsetToPaletteEntryLabelArray = pos
127                pos += len(paletteLabels)
128            header1 = struct.pack(
129                ">LLL",
130                offsetToPaletteTypeArray,
131                offsetToPaletteLabelArray,
132                offsetToPaletteEntryLabelArray,
133            )
134            dataList = [
135                header,
136                colorRecordIndices,
137                header1,
138                colorRecords,
139                paletteTypes,
140                paletteLabels,
141                paletteEntryLabels,
142            ]
143        return bytesjoin(dataList)
144
145    def _compilePalette(self, palette):
146        assert len(palette) == self.numPaletteEntries
147        pack = lambda c: struct.pack(">BBBB", c.blue, c.green, c.red, c.alpha)
148        return bytesjoin([pack(color) for color in palette])
149
150    def _compileColorRecords(self):
151        colorRecords, colorRecordIndices, pool = [], [], {}
152        for palette in self.palettes:
153            packedPalette = self._compilePalette(palette)
154            if packedPalette in pool:
155                index = pool[packedPalette]
156            else:
157                index = len(colorRecords)
158                colorRecords.append(packedPalette)
159                pool[packedPalette] = index
160            colorRecordIndices.append(struct.pack(">H", index * self.numPaletteEntries))
161        return bytesjoin(colorRecordIndices), bytesjoin(colorRecords)
162
163    def _compilePaletteTypes(self):
164        if self.version == 0 or not any(self.paletteTypes):
165            return b""
166        assert len(self.paletteTypes) == len(self.palettes)
167        result = bytesjoin([struct.pack(">I", ptype) for ptype in self.paletteTypes])
168        assert len(result) == 4 * len(self.palettes)
169        return result
170
171    def _compilePaletteLabels(self):
172        if self.version == 0 or all(l == self.NO_NAME_ID for l in self.paletteLabels):
173            return b""
174        assert len(self.paletteLabels) == len(self.palettes)
175        result = bytesjoin([struct.pack(">H", label) for label in self.paletteLabels])
176        assert len(result) == 2 * len(self.palettes)
177        return result
178
179    def _compilePaletteEntryLabels(self):
180        if self.version == 0 or all(
181            l == self.NO_NAME_ID for l in self.paletteEntryLabels
182        ):
183            return b""
184        assert len(self.paletteEntryLabels) == self.numPaletteEntries
185        result = bytesjoin(
186            [struct.pack(">H", label) for label in self.paletteEntryLabels]
187        )
188        assert len(result) == 2 * self.numPaletteEntries
189        return result
190
191    def toXML(self, writer, ttFont):
192        numPalettes = len(self.palettes)
193        paletteLabels = {i: nameID for (i, nameID) in enumerate(self.paletteLabels)}
194        paletteTypes = {i: typ for (i, typ) in enumerate(self.paletteTypes)}
195        writer.simpletag("version", value=self.version)
196        writer.newline()
197        writer.simpletag("numPaletteEntries", value=self.numPaletteEntries)
198        writer.newline()
199        for index, palette in enumerate(self.palettes):
200            attrs = {"index": index}
201            paletteType = paletteTypes.get(index, self.DEFAULT_PALETTE_TYPE)
202            paletteLabel = paletteLabels.get(index, self.NO_NAME_ID)
203            if self.version > 0 and paletteLabel != self.NO_NAME_ID:
204                attrs["label"] = paletteLabel
205            if self.version > 0 and paletteType != self.DEFAULT_PALETTE_TYPE:
206                attrs["type"] = paletteType
207            writer.begintag("palette", **attrs)
208            writer.newline()
209            if (
210                self.version > 0
211                and paletteLabel != self.NO_NAME_ID
212                and ttFont
213                and "name" in ttFont
214            ):
215                name = ttFont["name"].getDebugName(paletteLabel)
216                if name is not None:
217                    writer.comment(name)
218                    writer.newline()
219            assert len(palette) == self.numPaletteEntries
220            for cindex, color in enumerate(palette):
221                color.toXML(writer, ttFont, cindex)
222            writer.endtag("palette")
223            writer.newline()
224        if self.version > 0 and not all(
225            l == self.NO_NAME_ID for l in self.paletteEntryLabels
226        ):
227            writer.begintag("paletteEntryLabels")
228            writer.newline()
229            for index, label in enumerate(self.paletteEntryLabels):
230                if label != self.NO_NAME_ID:
231                    writer.simpletag("label", index=index, value=label)
232                    if self.version > 0 and label and ttFont and "name" in ttFont:
233                        name = ttFont["name"].getDebugName(label)
234                        if name is not None:
235                            writer.comment(name)
236                    writer.newline()
237            writer.endtag("paletteEntryLabels")
238            writer.newline()
239
240    def fromXML(self, name, attrs, content, ttFont):
241        if name == "palette":
242            self.paletteLabels.append(int(attrs.get("label", self.NO_NAME_ID)))
243            self.paletteTypes.append(int(attrs.get("type", self.DEFAULT_PALETTE_TYPE)))
244            palette = []
245            for element in content:
246                if isinstance(element, str):
247                    continue
248                attrs = element[1]
249                color = Color.fromHex(attrs["value"])
250                palette.append(color)
251            self.palettes.append(palette)
252        elif name == "paletteEntryLabels":
253            colorLabels = {}
254            for element in content:
255                if isinstance(element, str):
256                    continue
257                elementName, elementAttr, _ = element
258                if elementName == "label":
259                    labelIndex = safeEval(elementAttr["index"])
260                    nameID = safeEval(elementAttr["value"])
261                    colorLabels[labelIndex] = nameID
262            self.paletteEntryLabels = [
263                colorLabels.get(i, self.NO_NAME_ID)
264                for i in range(self.numPaletteEntries)
265            ]
266        elif "value" in attrs:
267            value = safeEval(attrs["value"])
268            setattr(self, name, value)
269            if name == "numPaletteEntries":
270                self.paletteEntryLabels = [self.NO_NAME_ID] * self.numPaletteEntries
271
272
273class Color(namedtuple("Color", "blue green red alpha")):
274    def hex(self):
275        return "#%02X%02X%02X%02X" % (self.red, self.green, self.blue, self.alpha)
276
277    def __repr__(self):
278        return self.hex()
279
280    def toXML(self, writer, ttFont, index=None):
281        writer.simpletag("color", value=self.hex(), index=index)
282        writer.newline()
283
284    @classmethod
285    def fromHex(cls, value):
286        if value[0] == "#":
287            value = value[1:]
288        red = int(value[0:2], 16)
289        green = int(value[2:4], 16)
290        blue = int(value[4:6], 16)
291        alpha = int(value[6:8], 16) if len(value) >= 8 else 0xFF
292        return cls(red=red, green=green, blue=blue, alpha=alpha)
293
294    @classmethod
295    def fromRGBA(cls, red, green, blue, alpha):
296        return cls(red=red, green=green, blue=blue, alpha=alpha)
297