Coverage for /usr/lib/python3/dist-packages/fontTools/ttLib/tables/C_P_A_L_.py: 14%
208 statements
« prev ^ index » next coverage.py v7.9.1, created at 2025-06-14 15:55 +0200
« prev ^ index » next coverage.py v7.9.1, created at 2025-06-14 15:55 +0200
1# Copyright 2013 Google, Inc. All Rights Reserved.
2#
3# Google Author(s): Behdad Esfahbod
5from fontTools.misc.textTools import bytesjoin, safeEval
6from . import DefaultTable
7import array
8from collections import namedtuple
9import struct
10import sys
13class table_C_P_A_L_(DefaultTable.DefaultTable):
14 NO_NAME_ID = 0xFFFF
15 DEFAULT_PALETTE_TYPE = 0
17 def __init__(self, tag=None):
18 DefaultTable.DefaultTable.__init__(self, tag)
19 self.palettes = []
20 self.paletteTypes = []
21 self.paletteLabels = []
22 self.paletteEntryLabels = []
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 )
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()
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()
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)
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])
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)
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
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
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
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()
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
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)
277 def __repr__(self):
278 return self.hex()
280 def toXML(self, writer, ttFont, index=None):
281 writer.simpletag("color", value=self.hex(), index=index)
282 writer.newline()
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)
294 @classmethod
295 def fromRGBA(cls, red, green, blue, alpha):
296 return cls(red=red, green=green, blue=blue, alpha=alpha)