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 def buildCOLR(colorLayers: Dict[str, List[Tuple[str, int]]]) -> table_C_O_L_R_: """Build COLR table from color layers mapping. Args: colorLayers: : map of base glyph names to lists of (layer glyph names, palette indices) tuples. Return: A new COLRv0 table. """ colorLayerLists = {} for baseGlyphName, layers in colorLayers.items(): colorLayerLists[baseGlyphName] = [ LayerRecord(layerGlyphName, colorID) for layerGlyphName, colorID in layers ] colr = table_C_O_L_R_() colr.version = 0 colr.ColorLayers = colorLayerLists 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]]], 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 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 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_() cpal.numPaletteEntries = len(palettes[0]) 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