From bb46604ec24ea98c158940c2e513a945b0458d98 Mon Sep 17 00:00:00 2001 From: Cosimo Lupo Date: Mon, 17 Feb 2020 18:25:45 +0000 Subject: [PATCH] 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