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