From 191a036f2676d756b3c0835a513e41597bf7ad63 Mon Sep 17 00:00:00 2001 From: Cosimo Lupo Date: Mon, 10 Feb 2020 17:31:14 +0000 Subject: [PATCH 1/8] colorLib: add buildCOLR and buildCPAL --- Lib/fontTools/colorLib/__init__.py | 0 Lib/fontTools/colorLib/builder.py | 54 ++++++++++++++++++++++++++++++ Lib/fontTools/colorLib/errors.py | 3 ++ 3 files changed, 57 insertions(+) create mode 100644 Lib/fontTools/colorLib/__init__.py create mode 100644 Lib/fontTools/colorLib/builder.py create mode 100644 Lib/fontTools/colorLib/errors.py diff --git a/Lib/fontTools/colorLib/__init__.py b/Lib/fontTools/colorLib/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/Lib/fontTools/colorLib/builder.py b/Lib/fontTools/colorLib/builder.py new file mode 100644 index 000000000..7141120e7 --- /dev/null +++ b/Lib/fontTools/colorLib/builder.py @@ -0,0 +1,54 @@ +from fontTools.ttLib import newTable +from .errors import ColorLibError + + +def buildCOLR(colorLayers): + """Build COLR table from color layers mapping. + + Args: + colorLayers: Dict[str, List[Tuple[str, int]]]: map of base glyph names (str) + to lists of layer glyph names (str) and palette indices (int) tuples. + + Return: + A new COLRv0 table. + """ + from fontTools.ttLib.tables.C_O_L_R_ import LayerRecord + + colorLayerLists = {} + for baseGlyphName, layers in colorLayers.items(): + colorLayerLists[baseGlyphName] = [ + LayerRecord(layerGlyphName, colorID) for layerGlyphName, colorID in layers + ] + + colr = newTable("COLR") + colr.version = 0 + colr.ColorLayers = colorLayerLists + return colr + + +def buildCPAL(palettes): + """Build CPAL table from list of color palettes. + + Args: + palettes: List[List[Tuple[float, float, float, float]]]: list of lists + colors encoded as tuples of (R, G, B, A) floats. + + Return: + A new CPALv0 table. + """ + from fontTools.ttLib.tables.C_P_A_L_ import Color + + if len({len(p) for p in palettes}) != 1: + raise ColorLibError("color palettes have different lengths") + cpal = newTable("CPAL") + # TODO(anthotype): Support version 1 with palette types, labels and entry labels. + cpal.version = 0 + cpal.numPaletteEntries = len(palettes[0]) + cpal.palettes = [ + [ + Color(*(round(v * 255) for v in (blue, green, red, alpha))) + for red, green, blue, alpha in palette + ] + for palette in palettes + ] + return cpal diff --git a/Lib/fontTools/colorLib/errors.py b/Lib/fontTools/colorLib/errors.py new file mode 100644 index 000000000..a0bdda174 --- /dev/null +++ b/Lib/fontTools/colorLib/errors.py @@ -0,0 +1,3 @@ + +class ColorLibError(Exception): + pass From f8b9887f856f62e325ba31c54ea55ac5f55a43f7 Mon Sep 17 00:00:00 2001 From: Cosimo Lupo Date: Mon, 10 Feb 2020 17:35:05 +0000 Subject: [PATCH 2/8] fontBuilder: add setupCOLR and setupCPAL methods --- Lib/fontTools/fontBuilder.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/Lib/fontTools/fontBuilder.py b/Lib/fontTools/fontBuilder.py index 6e72dd520..f46db6f87 100644 --- a/Lib/fontTools/fontBuilder.py +++ b/Lib/fontTools/fontBuilder.py @@ -768,6 +768,24 @@ class FontBuilder(object): self.font, conditionalSubstitutions, featureTag=featureTag ) + def setupCOLR(self, colorLayers): + """Build new COLR table using color layers dictionary. + + Cf. `fontTools.colorLib.builder.buildCOLR`. + """ + from fontTools.colorLib.builder import buildCOLR + + self.font["COLR"] = buildCOLR(colorLayers) + + def setupCPAL(self, palettes): + """Build new CPAL table using list of palettes. + + Cf. `fontTools.colorLib.builder.buildCPAL`. + """ + from fontTools.colorLib.builder import buildCPAL + + self.font["CPAL"] = buildCPAL(palettes) + def buildCmapSubTable(cmapping, format, platformID, platEncID): subTable = cmap_classes[format](format) From acfae6721b868de490180aff7a7cdd8c74a6ca9c Mon Sep 17 00:00:00 2001 From: Cosimo Lupo Date: Mon, 17 Feb 2020 11:58:29 +0000 Subject: [PATCH 3/8] add tests for buildCORL and buildCPAL --- Tests/colorLib/__init__.py | 0 Tests/colorLib/builder_test.py | 56 ++++++++++++++++++++++++++++++++++ 2 files changed, 56 insertions(+) create mode 100644 Tests/colorLib/__init__.py create mode 100644 Tests/colorLib/builder_test.py diff --git a/Tests/colorLib/__init__.py b/Tests/colorLib/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/Tests/colorLib/builder_test.py b/Tests/colorLib/builder_test.py new file mode 100644 index 000000000..bd3898a8b --- /dev/null +++ b/Tests/colorLib/builder_test.py @@ -0,0 +1,56 @@ +from fontTools.colorLib import builder +from fontTools.colorLib.errors import ColorLibError +import pytest + + +def test_buildCOLR_v0(): + color_layer_lists = { + "a": [("a.color0", 0), ("a.color1", 1)], + "b": [("b.color1", 1), ("b.color0", 0)], + } + + colr = builder.buildCOLR(color_layer_lists) + + assert colr.tableTag == "COLR" + assert colr.version == 0 + assert colr.ColorLayers["a"][0].name == "a.color0" + assert colr.ColorLayers["a"][0].colorID == 0 + assert colr.ColorLayers["a"][1].name == "a.color1" + assert colr.ColorLayers["a"][1].colorID == 1 + assert colr.ColorLayers["b"][0].name == "b.color1" + assert colr.ColorLayers["b"][0].colorID == 1 + assert colr.ColorLayers["b"][1].name == "b.color0" + assert colr.ColorLayers["b"][1].colorID == 0 + + +def test_buildCPAL_v0(): + palettes = [ + [(0.68, 0.20, 0.32, 1.0), (0.45, 0.68, 0.21, 1.0)], + [(0.68, 0.20, 0.32, 0.6), (0.45, 0.68, 0.21, 0.6)], + [(0.68, 0.20, 0.32, 0.3), (0.45, 0.68, 0.21, 0.3)], + ] + + cpal = builder.buildCPAL(palettes) + + assert cpal.tableTag == "CPAL" + assert cpal.version == 0 + assert cpal.numPaletteEntries == 2 + + assert len(cpal.palettes) == 3 + assert [tuple(c) for c in cpal.palettes[0]] == [ + (82, 51, 173, 255), + (54, 173, 115, 255), + ] + assert [tuple(c) for c in cpal.palettes[1]] == [ + (82, 51, 173, 153), + (54, 173, 115, 153), + ] + assert [tuple(c) for c in cpal.palettes[2]] == [ + (82, 51, 173, 76), + (54, 173, 115, 76), + ] + + +def test_buildCPAL_palettes_different_lengths(): + with pytest.raises(ColorLibError, match="have different lengths"): + builder.buildCPAL([[(1, 1, 1, 1)], [(0, 0, 0, 1), (0.5, 0.5, 0.5, 1)]]) From 7a2f68a317781e8d1ed7873c301042c2432cd92b Mon Sep 17 00:00:00 2001 From: Cosimo Lupo Date: Mon, 17 Feb 2020 12:11:32 +0000 Subject: [PATCH 4/8] colorLib: add type annotations --- Lib/fontTools/colorLib/builder.py | 25 ++++++++++++------------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/Lib/fontTools/colorLib/builder.py b/Lib/fontTools/colorLib/builder.py index 7141120e7..59a650aed 100644 --- a/Lib/fontTools/colorLib/builder.py +++ b/Lib/fontTools/colorLib/builder.py @@ -1,46 +1,45 @@ -from fontTools.ttLib import newTable +from typing import Dict, List, Tuple +from fontTools.ttLib.tables.C_O_L_R_ import LayerRecord, table_C_O_L_R_ +from fontTools.ttLib.tables.C_P_A_L_ import Color, table_C_P_A_L_ from .errors import ColorLibError -def buildCOLR(colorLayers): +def buildCOLR(colorLayers: Dict[str, List[Tuple[str, int]]]) -> table_C_O_L_R_: """Build COLR table from color layers mapping. Args: - colorLayers: Dict[str, List[Tuple[str, int]]]: map of base glyph names (str) - to lists of layer glyph names (str) and palette indices (int) tuples. + colorLayers: : map of base glyph names to lists of (layer glyph names, + palette indices) tuples. Return: A new COLRv0 table. """ - from fontTools.ttLib.tables.C_O_L_R_ import LayerRecord - colorLayerLists = {} for baseGlyphName, layers in colorLayers.items(): colorLayerLists[baseGlyphName] = [ LayerRecord(layerGlyphName, colorID) for layerGlyphName, colorID in layers ] - colr = newTable("COLR") + colr = table_C_O_L_R_() colr.version = 0 colr.ColorLayers = colorLayerLists return colr -def buildCPAL(palettes): +def buildCPAL( + palettes: List[List[Tuple[float, float, float, float]]] +) -> table_C_P_A_L_: """Build CPAL table from list of color palettes. Args: - palettes: List[List[Tuple[float, float, float, float]]]: list of lists - colors encoded as tuples of (R, G, B, A) floats. + palettes: : list of lists of colors encoded as tuples of (R, G, B, A) floats. Return: A new CPALv0 table. """ - from fontTools.ttLib.tables.C_P_A_L_ import Color - if len({len(p) for p in palettes}) != 1: raise ColorLibError("color palettes have different lengths") - cpal = newTable("CPAL") + cpal = table_C_P_A_L_() # TODO(anthotype): Support version 1 with palette types, labels and entry labels. cpal.version = 0 cpal.numPaletteEntries = len(palettes[0]) From f60bcc2c5a7f404fa5592afd672d89c577509c1f Mon Sep 17 00:00:00 2001 From: Cosimo Lupo Date: Mon, 17 Feb 2020 17:02:58 +0000 Subject: [PATCH 5/8] [CPAL] the absence of a color palette label nameID is 0xFFFF, not 0 --- Lib/fontTools/ttLib/tables/C_P_A_L_.py | 44 ++++++++++++++------------ Tests/ttLib/tables/C_P_A_L_test.py | 17 +++------- 2 files changed, 29 insertions(+), 32 deletions(-) diff --git a/Lib/fontTools/ttLib/tables/C_P_A_L_.py b/Lib/fontTools/ttLib/tables/C_P_A_L_.py index f0415600c..a7b4ad2bb 100644 --- a/Lib/fontTools/ttLib/tables/C_P_A_L_.py +++ b/Lib/fontTools/ttLib/tables/C_P_A_L_.py @@ -13,6 +13,9 @@ import sys class table_C_P_A_L_(DefaultTable.DefaultTable): + NO_NAME_ID = 0xFFFF + DEFAULT_PALETTE_TYPE = 0 + def __init__(self, tag=None): DefaultTable.DefaultTable.__init__(self, tag) self.palettes = [] @@ -45,24 +48,25 @@ class table_C_P_A_L_(DefaultTable.DefaultTable): offsetToPaletteEntryLabelArray) = ( struct.unpack(">LLL", data[pos:pos+12])) self.paletteTypes = self._decompileUInt32Array( - data, offsetToPaletteTypeArray, numPalettes) + data, offsetToPaletteTypeArray, numPalettes, + default=self.DEFAULT_PALETTE_TYPE) self.paletteLabels = self._decompileUInt16Array( - data, offsetToPaletteLabelArray, numPalettes) + data, offsetToPaletteLabelArray, numPalettes, default=self.NO_NAME_ID) self.paletteEntryLabels = self._decompileUInt16Array( data, offsetToPaletteEntryLabelArray, - self.numPaletteEntries) + self.numPaletteEntries, default=self.NO_NAME_ID) - def _decompileUInt16Array(self, data, offset, numElements): + def _decompileUInt16Array(self, data, offset, numElements, default=0): if offset == 0: - return [0] * numElements + return [default] * numElements result = array.array("H", data[offset : offset + 2 * numElements]) if sys.byteorder != "big": result.byteswap() assert len(result) == numElements, result return result.tolist() - def _decompileUInt32Array(self, data, offset, numElements): + def _decompileUInt32Array(self, data, offset, numElements, default=0): if offset == 0: - return [0] * numElements + return [default] * numElements result = array.array("I", data[offset : offset + 4 * numElements]) if sys.byteorder != "big": result.byteswap() assert len(result) == numElements, result @@ -136,7 +140,7 @@ class table_C_P_A_L_(DefaultTable.DefaultTable): return result def _compilePaletteLabels(self): - if self.version == 0 or not any(self.paletteLabels): + if self.version == 0 or all(l == self.NO_NAME_ID for l in self.paletteLabels): return b'' assert len(self.paletteLabels) == len(self.palettes) result = bytesjoin([struct.pack(">H", label) @@ -145,7 +149,7 @@ class table_C_P_A_L_(DefaultTable.DefaultTable): return result def _compilePaletteEntryLabels(self): - if self.version == 0 or not any(self.paletteEntryLabels): + if self.version == 0 or all(l == self.NO_NAME_ID for l in self.paletteEntryLabels): return b'' assert len(self.paletteEntryLabels) == self.numPaletteEntries result = bytesjoin([struct.pack(">H", label) @@ -165,15 +169,15 @@ class table_C_P_A_L_(DefaultTable.DefaultTable): writer.newline() for index, palette in enumerate(self.palettes): attrs = {"index": index} - paletteType = paletteTypes.get(index) - paletteLabel = paletteLabels.get(index) - if self.version > 0 and paletteLabel is not None: + paletteType = paletteTypes.get(index, self.DEFAULT_PALETTE_TYPE) + paletteLabel = paletteLabels.get(index, self.NO_NAME_ID) + if self.version > 0 and paletteLabel != self.NO_NAME_ID: attrs["label"] = paletteLabel - if self.version > 0 and paletteType is not None: + if self.version > 0 and paletteType != self.DEFAULT_PALETTE_TYPE: attrs["type"] = paletteType writer.begintag("palette", **attrs) writer.newline() - if (self.version > 0 and paletteLabel and + if (self.version > 0 and paletteLabel != self.NO_NAME_ID and ttFont and "name" in ttFont): name = ttFont["name"].getDebugName(paletteLabel) if name is not None: @@ -184,11 +188,11 @@ class table_C_P_A_L_(DefaultTable.DefaultTable): color.toXML(writer, ttFont, cindex) writer.endtag("palette") writer.newline() - if self.version > 0 and any(self.paletteEntryLabels): + if self.version > 0 and not all(l == self.NO_NAME_ID for l in self.paletteEntryLabels): writer.begintag("paletteEntryLabels") writer.newline() for index, label in enumerate(self.paletteEntryLabels): - if label: + if label != self.NO_NAME_ID: writer.simpletag("label", index=index, value=label) if (self.version > 0 and label and ttFont and "name" in ttFont): name = ttFont["name"].getDebugName(label) @@ -200,8 +204,8 @@ class table_C_P_A_L_(DefaultTable.DefaultTable): def fromXML(self, name, attrs, content, ttFont): if name == "palette": - self.paletteLabels.append(int(attrs.get("label", "0"))) - self.paletteTypes.append(int(attrs.get("type", "0"))) + self.paletteLabels.append(int(attrs.get("label", self.NO_NAME_ID))) + self.paletteTypes.append(int(attrs.get("type", self.DEFAULT_PALETTE_TYPE))) palette = [] for element in content: if isinstance(element, basestring): @@ -221,13 +225,13 @@ class table_C_P_A_L_(DefaultTable.DefaultTable): nameID = safeEval(elementAttr["value"]) colorLabels[labelIndex] = nameID self.paletteEntryLabels = [ - colorLabels.get(i, 0) + colorLabels.get(i, self.NO_NAME_ID) for i in range(self.numPaletteEntries)] elif "value" in attrs: value = safeEval(attrs["value"]) setattr(self, name, value) if name == "numPaletteEntries": - self.paletteEntryLabels = [0] * self.numPaletteEntries + self.paletteEntryLabels = [self.NO_NAME_ID] * self.numPaletteEntries class Color(namedtuple("Color", "blue green red alpha")): diff --git a/Tests/ttLib/tables/C_P_A_L_test.py b/Tests/ttLib/tables/C_P_A_L_test.py index 68009874c..b018a5247 100644 --- a/Tests/ttLib/tables/C_P_A_L_test.py +++ b/Tests/ttLib/tables/C_P_A_L_test.py @@ -66,9 +66,6 @@ class CPALTest(unittest.TestCase): self.assertEqual(cpal.numPaletteEntries, 2) self.assertEqual(repr(cpal.palettes), '[[#000000FF, #66CCFFFF], [#000000FF, #800000FF]]') - self.assertEqual(cpal.paletteLabels, [0, 0]) - self.assertEqual(cpal.paletteTypes, [0, 0]) - self.assertEqual(cpal.paletteEntryLabels, [0, 0]) def test_decompile_v0_sharingColors(self): cpal = newTable('CPAL') @@ -80,9 +77,6 @@ class CPALTest(unittest.TestCase): '[#223344FF, #99887711, #55555555]', '[#223344FF, #99887711, #FFFFFFFF]', '[#223344FF, #99887711, #55555555]']) - self.assertEqual(cpal.paletteLabels, [0, 0, 0, 0]) - self.assertEqual(cpal.paletteTypes, [0, 0, 0, 0]) - self.assertEqual(cpal.paletteEntryLabels, [0, 0, 0]) def test_decompile_v1_noLabelsNoTypes(self): cpal = newTable('CPAL') @@ -92,9 +86,10 @@ class CPALTest(unittest.TestCase): self.assertEqual([repr(p) for p in cpal.palettes], [ '[#CAFECAFE, #22110033, #66554477]', # RGBA '[#59413127, #42424242, #13330037]']) - self.assertEqual(cpal.paletteLabels, [0, 0]) + self.assertEqual(cpal.paletteLabels, [cpal.NO_NAME_ID] * len(cpal.palettes)) self.assertEqual(cpal.paletteTypes, [0, 0]) - self.assertEqual(cpal.paletteEntryLabels, [0, 0, 0]) + self.assertEqual(cpal.paletteEntryLabels, + [cpal.NO_NAME_ID] * cpal.numPaletteEntries) def test_decompile_v1(self): cpal = newTable('CPAL') @@ -194,9 +189,6 @@ class CPALTest(unittest.TestCase): self.assertEqual(cpal.version, 0) self.assertEqual(cpal.numPaletteEntries, 2) self.assertEqual(repr(cpal.palettes), '[[#12345678, #FEDCBA98]]') - self.assertEqual(cpal.paletteLabels, [0]) - self.assertEqual(cpal.paletteTypes, [0]) - self.assertEqual(cpal.paletteEntryLabels, [0, 0]) def test_fromXML_v1(self): cpal = newTable('CPAL') @@ -218,7 +210,8 @@ class CPALTest(unittest.TestCase): '[[#12345678, #FEDCBA98, #CAFECAFE]]') self.assertEqual(cpal.paletteLabels, [259]) self.assertEqual(cpal.paletteTypes, [2]) - self.assertEqual(cpal.paletteEntryLabels, [0, 262, 0]) + self.assertEqual(cpal.paletteEntryLabels, + [cpal.NO_NAME_ID, 262, cpal.NO_NAME_ID]) if __name__ == "__main__": From bb46604ec24ea98c158940c2e513a945b0458d98 Mon Sep 17 00:00:00 2001 From: Cosimo Lupo Date: Mon, 17 Feb 2020 18:25:45 +0000 Subject: [PATCH 6/8] colorLib: allow to build CPAL version=1 --- Lib/fontTools/colorLib/builder.py | 120 ++++++++++++++++++++++++++---- 1 file changed, 107 insertions(+), 13 deletions(-) diff --git a/Lib/fontTools/colorLib/builder.py b/Lib/fontTools/colorLib/builder.py index 59a650aed..486909e9e 100644 --- a/Lib/fontTools/colorLib/builder.py +++ b/Lib/fontTools/colorLib/builder.py @@ -1,6 +1,8 @@ -from typing import Dict, List, Tuple +import enum +from typing import Dict, Iterable, List, Optional, Tuple, Union from fontTools.ttLib.tables.C_O_L_R_ import LayerRecord, table_C_O_L_R_ from fontTools.ttLib.tables.C_P_A_L_ import Color, table_C_P_A_L_ +from fontTools.ttLib.tables._n_a_m_e import table__n_a_m_e from .errors import ColorLibError @@ -26,28 +28,120 @@ def buildCOLR(colorLayers: Dict[str, List[Tuple[str, int]]]) -> table_C_O_L_R_: return colr +class ColorPaletteType(enum.IntFlag): + USABLE_WITH_LIGHT_BACKGROUND = 0x0001 + USABLE_WITH_DARK_BACKGROUND = 0x0002 + + @classmethod + def _missing_(cls, value): + # enforce reserved bits + if isinstance(value, int) and (value < 0 or value & 0xFFFC != 0): + raise ValueError(f"{value} is not a valid {cls.__name__}") + return super()._missing_(value) + + +# None, 'abc' or {'en': 'abc', 'de': 'xyz'} +_OptionalLocalizedString = Union[None, str, Dict[str, str]] + + +def buildPaletteLabels( + labels: List[_OptionalLocalizedString], nameTable: table__n_a_m_e +) -> List[Optional[int]]: + return [ + nameTable.addMultilingualName(l, mac=False) + if isinstance(l, dict) + else table_C_P_A_L_.NO_NAME_ID + if l is None + else nameTable.addMultilingualName({"en": l}, mac=False) + for l in labels + ] + + def buildCPAL( - palettes: List[List[Tuple[float, float, float, float]]] + palettes: List[List[Tuple[float, float, float, float]]], + paletteTypes: Optional[List[ColorPaletteType]] = None, + paletteLabels: Optional[List[_OptionalLocalizedString]] = None, + paletteEntryLabels: Optional[List[_OptionalLocalizedString]] = None, + nameTable: Optional[table__n_a_m_e] = None, ) -> table_C_P_A_L_: """Build CPAL table from list of color palettes. Args: - palettes: : list of lists of colors encoded as tuples of (R, G, B, A) floats. + palettes: list of lists of colors encoded as tuples of (R, G, B, A) floats + in the range [0..1]. + paletteTypes: optional list of ColorPaletteType, one for each palette. + paletteLabels: optional list of palette labels. Each lable can be either: + None (no label), a string (for for default English labels), or a + localized string (as a dict keyed with BCP47 language codes). + paletteEntryLabels: optional list of palette entry labels, one for each + palette entry (see paletteLabels). + nameTable: optional name table where to store palette and palette entry + labels. Required if either paletteLabels or paletteEntryLabels is set. Return: - A new CPALv0 table. + A new CPAL v0 or v1 table, if custom palette types or labels are specified. """ if len({len(p) for p in palettes}) != 1: raise ColorLibError("color palettes have different lengths") + + if (paletteLabels or paletteEntryLabels) and not nameTable: + raise TypeError( + "nameTable is required if palette or palette entries have labels" + ) + cpal = table_C_P_A_L_() - # TODO(anthotype): Support version 1 with palette types, labels and entry labels. - cpal.version = 0 cpal.numPaletteEntries = len(palettes[0]) - cpal.palettes = [ - [ - Color(*(round(v * 255) for v in (blue, green, red, alpha))) - for red, green, blue, alpha in palette - ] - for palette in palettes - ] + + cpal.palettes = [] + for i, palette in enumerate(palettes): + colors = [] + for j, color in enumerate(palette): + if not isinstance(color, tuple) or len(color) != 4: + raise ColorLibError( + f"In palette[{i}][{j}]: expected (R, G, B, A) tuple, got {color!r}" + ) + if any(v > 1 or v < 0 for v in color): + raise ColorLibError( + f"palette[{i}][{j}] has invalid out-of-range [0..1] color: {color!r}" + ) + # input colors are RGBA, CPAL encodes them as BGRA + red, green, blue, alpha = color + colors.append(Color(*(round(v * 255) for v in (blue, green, red, alpha)))) + cpal.palettes.append(colors) + + if any(v is not None for v in (paletteTypes, paletteLabels, paletteEntryLabels)): + cpal.version = 1 + + if paletteTypes is not None: + if len(paletteTypes) != len(palettes): + raise ColorLibError( + f"Expected {len(palettes)} paletteTypes, got {len(paletteTypes)}" + ) + cpal.paletteTypes = [ColorPaletteType(t).value for t in paletteTypes] + else: + cpal.paletteTypes = [table_C_P_A_L_.DEFAULT_PALETTE_TYPE] * len(palettes) + + if paletteLabels is not None: + if len(paletteLabels) != len(palettes): + raise ColorLibError( + f"Expected {len(palettes)} paletteLabels, got {len(paletteLabels)}" + ) + cpal.paletteLabels = buildPaletteLabels(paletteLabels, nameTable) + else: + cpal.paletteLabels = [table_C_P_A_L_.NO_NAME_ID] * len(palettes) + + if paletteEntryLabels is not None: + if len(paletteEntryLabels) != cpal.numPaletteEntries: + raise ColorLibError( + f"Expected {cpal.numPaletteEntries} paletteEntryLabels, " + f"got {len(paletteEntryLabels)}" + ) + cpal.paletteEntryLabels = buildPaletteLabels(paletteEntryLabels, nameTable) + else: + cpal.paletteEntryLabels = [ + table_C_P_A_L_.NO_NAME_ID + ] * cpal.numPaletteEntries + else: + cpal.version = 0 + return cpal From a0a4901a5ef22d25aab5d81a464fc696f6e9ee8b Mon Sep 17 00:00:00 2001 From: Cosimo Lupo Date: Mon, 17 Feb 2020 18:28:24 +0000 Subject: [PATCH 7/8] colorLib_test: add tests for buildCPAL v1 --- Tests/colorLib/builder_test.py | 131 +++++++++++++++++++++++++++++++++ 1 file changed, 131 insertions(+) diff --git a/Tests/colorLib/builder_test.py b/Tests/colorLib/builder_test.py index bd3898a8b..c51718790 100644 --- a/Tests/colorLib/builder_test.py +++ b/Tests/colorLib/builder_test.py @@ -1,3 +1,4 @@ +from fontTools.ttLib import newTable from fontTools.colorLib import builder from fontTools.colorLib.errors import ColorLibError import pytest @@ -54,3 +55,133 @@ def test_buildCPAL_v0(): def test_buildCPAL_palettes_different_lengths(): with pytest.raises(ColorLibError, match="have different lengths"): builder.buildCPAL([[(1, 1, 1, 1)], [(0, 0, 0, 1), (0.5, 0.5, 0.5, 1)]]) + + +def test_buildPaletteLabels(): + name_table = newTable("name") + name_table.names = [] + + name_ids = builder.buildPaletteLabels( + [None, "hi", {"en": "hello", "de": "hallo"}], name_table + ) + + assert name_ids == [0xFFFF, 256, 257] + + assert len(name_table.names) == 3 + assert str(name_table.names[0]) == "hi" + assert name_table.names[0].nameID == 256 + + assert str(name_table.names[1]) == "hallo" + assert name_table.names[1].nameID == 257 + + assert str(name_table.names[2]) == "hello" + assert name_table.names[2].nameID == 257 + + +def test_build_CPAL_v1_types_no_labels(): + palettes = [ + [(0.1, 0.2, 0.3, 1.0), (0.4, 0.5, 0.6, 1.0)], + [(0.1, 0.2, 0.3, 0.6), (0.4, 0.5, 0.6, 0.6)], + [(0.1, 0.2, 0.3, 0.3), (0.4, 0.5, 0.6, 0.3)], + ] + paletteTypes = [ + builder.ColorPaletteType.USABLE_WITH_LIGHT_BACKGROUND, + builder.ColorPaletteType.USABLE_WITH_DARK_BACKGROUND, + builder.ColorPaletteType.USABLE_WITH_LIGHT_BACKGROUND + | builder.ColorPaletteType.USABLE_WITH_DARK_BACKGROUND, + ] + + cpal = builder.buildCPAL(palettes, paletteTypes=paletteTypes) + + assert cpal.tableTag == "CPAL" + assert cpal.version == 1 + assert cpal.numPaletteEntries == 2 + assert len(cpal.palettes) == 3 + + assert cpal.paletteTypes == paletteTypes + assert cpal.paletteLabels == [cpal.NO_NAME_ID] * len(palettes) + assert cpal.paletteEntryLabels == [cpal.NO_NAME_ID] * cpal.numPaletteEntries + + +def test_build_CPAL_v1_labels(): + palettes = [ + [(0.1, 0.2, 0.3, 1.0), (0.4, 0.5, 0.6, 1.0)], + [(0.1, 0.2, 0.3, 0.6), (0.4, 0.5, 0.6, 0.6)], + [(0.1, 0.2, 0.3, 0.3), (0.4, 0.5, 0.6, 0.3)], + ] + paletteLabels = ["First", {"en": "Second", "it": "Seconda"}, None] + paletteEntryLabels = ["Foo", "Bar"] + + with pytest.raises(TypeError, match="nameTable is required"): + builder.buildCPAL(palettes, paletteLabels=paletteLabels) + with pytest.raises(TypeError, match="nameTable is required"): + builder.buildCPAL(palettes, paletteEntryLabels=paletteEntryLabels) + + name_table = newTable("name") + name_table.names = [] + + cpal = builder.buildCPAL( + palettes, + paletteLabels=paletteLabels, + paletteEntryLabels=paletteEntryLabels, + nameTable=name_table, + ) + + assert cpal.tableTag == "CPAL" + assert cpal.version == 1 + assert cpal.numPaletteEntries == 2 + assert len(cpal.palettes) == 3 + + assert cpal.paletteTypes == [cpal.DEFAULT_PALETTE_TYPE] * len(palettes) + assert cpal.paletteLabels == [256, 257, cpal.NO_NAME_ID] + assert cpal.paletteEntryLabels == [258, 259] + + assert name_table.getDebugName(256) == "First" + assert name_table.getDebugName(257) == "Second" + assert name_table.getDebugName(258) == "Foo" + assert name_table.getDebugName(259) == "Bar" + + +def test_invalid_ColorPaletteType(): + with pytest.raises(ValueError, match="not a valid ColorPaletteType"): + builder.ColorPaletteType(-1) + with pytest.raises(ValueError, match="not a valid ColorPaletteType"): + builder.ColorPaletteType(4) + with pytest.raises(ValueError, match="not a valid ColorPaletteType"): + builder.ColorPaletteType("abc") + + +def test_buildCPAL_v1_invalid_args_length(): + with pytest.raises(ColorLibError, match="Expected 2 paletteTypes, got 1"): + builder.buildCPAL([[(0, 0, 0, 0)], [(1, 1, 1, 1)]], paletteTypes=[1]) + + with pytest.raises(ColorLibError, match="Expected 2 paletteLabels, got 1"): + builder.buildCPAL( + [[(0, 0, 0, 0)], [(1, 1, 1, 1)]], + paletteLabels=["foo"], + nameTable=newTable("name"), + ) + + with pytest.raises(ColorLibError, match="Expected 1 paletteEntryLabels, got 0"): + cpal = builder.buildCPAL( + [[(0, 0, 0, 0)], [(1, 1, 1, 1)]], + paletteEntryLabels=[], + nameTable=newTable("name"), + ) + + +def test_buildCPAL_invalid_color(): + with pytest.raises( + ColorLibError, + match=r"In palette\[0\]\[1\]: expected \(R, G, B, A\) tuple, got \(1, 1, 1\)", + ): + builder.buildCPAL([[(1, 1, 1, 1), (1, 1, 1)]]) + + with pytest.raises( + ColorLibError, + match=( + r"palette\[1\]\[0\] has invalid out-of-range " + r"\[0..1\] color: \(1, 1, -1, 2\)" + ), + ): + builder.buildCPAL([[(0, 0, 0, 0)], [(1, 1, -1, 2)]]) From d9250ddcf59f223f9f9fb239f938cc16be5ffdfd Mon Sep 17 00:00:00 2001 From: Cosimo Lupo Date: Mon, 17 Feb 2020 18:35:15 +0000 Subject: [PATCH 8/8] fontBuilder: allow to build v1 from setupCPAL method --- Lib/fontTools/fontBuilder.py | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/Lib/fontTools/fontBuilder.py b/Lib/fontTools/fontBuilder.py index f46db6f87..a08442abf 100644 --- a/Lib/fontTools/fontBuilder.py +++ b/Lib/fontTools/fontBuilder.py @@ -777,14 +777,29 @@ class FontBuilder(object): self.font["COLR"] = buildCOLR(colorLayers) - def setupCPAL(self, palettes): + def setupCPAL( + self, + palettes, + paletteTypes=None, + paletteLabels=None, + paletteEntryLabels=None, + ): """Build new CPAL table using list of palettes. + Optionally build CPAL v1 table using paletteTypes, paletteLabels and + paletteEntryLabels. + Cf. `fontTools.colorLib.builder.buildCPAL`. """ from fontTools.colorLib.builder import buildCPAL - self.font["CPAL"] = buildCPAL(palettes) + self.font["CPAL"] = buildCPAL( + palettes, + paletteTypes=paletteTypes, + paletteLabels=paletteLabels, + paletteEntryLabels=paletteEntryLabels, + nameTable=self.font.get("name") + ) def buildCmapSubTable(cmapping, format, platformID, platEncID):