diff options
Diffstat (limited to 'Lib/fontTools/colorLib/builder.py')
-rw-r--r-- | Lib/fontTools/colorLib/builder.py | 207 |
1 files changed, 123 insertions, 84 deletions
diff --git a/Lib/fontTools/colorLib/builder.py b/Lib/fontTools/colorLib/builder.py index 821244af..2577fa76 100644 --- a/Lib/fontTools/colorLib/builder.py +++ b/Lib/fontTools/colorLib/builder.py @@ -21,25 +21,16 @@ from typing import ( TypeVar, Union, ) +from fontTools.misc.arrayTools import intRect from fontTools.misc.fixedTools import fixedToFloat from fontTools.ttLib.tables import C_O_L_R_ from fontTools.ttLib.tables import C_P_A_L_ from fontTools.ttLib.tables import _n_a_m_e from fontTools.ttLib.tables import otTables as ot -from fontTools.ttLib.tables.otTables import ( - ExtendMode, - CompositeMode, - VariableValue, - VariableFloat, - VariableInt, -) +from fontTools.ttLib.tables.otTables import ExtendMode, CompositeMode from .errors import ColorLibError from .geometry import round_start_circle_stable_containment -from .table_builder import ( - convertTupleClass, - BuildCallback, - TableBuilder, -) +from .table_builder import BuildCallback, TableBuilder # TODO move type aliases to colorLib.types? @@ -49,56 +40,54 @@ _PaintInput = Union[int, _Kwargs, ot.Paint, Tuple[str, "_PaintInput"]] _PaintInputList = Sequence[_PaintInput] _ColorGlyphsDict = Dict[str, Union[_PaintInputList, _PaintInput]] _ColorGlyphsV0Dict = Dict[str, Sequence[Tuple[str, int]]] +_ClipBoxInput = Union[ + Tuple[int, int, int, int, int], # format 1, variable + Tuple[int, int, int, int], # format 0, non-variable + ot.ClipBox, +] MAX_PAINT_COLR_LAYER_COUNT = 255 -_DEFAULT_ALPHA = VariableFloat(1.0) +_DEFAULT_ALPHA = 1.0 _MAX_REUSE_LEN = 32 -def _beforeBuildPaintVarRadialGradient(paint, source, srcMapFn=lambda v: v): - # normalize input types (which may or may not specify a varIdx) - x0 = convertTupleClass(VariableFloat, source["x0"]) - y0 = convertTupleClass(VariableFloat, source["y0"]) - r0 = convertTupleClass(VariableFloat, source["r0"]) - x1 = convertTupleClass(VariableFloat, source["x1"]) - y1 = convertTupleClass(VariableFloat, source["y1"]) - r1 = convertTupleClass(VariableFloat, source["r1"]) +def _beforeBuildPaintRadialGradient(paint, source): + x0 = source["x0"] + y0 = source["y0"] + r0 = source["r0"] + x1 = source["x1"] + y1 = source["y1"] + r1 = source["r1"] # TODO apparently no builder_test confirms this works (?) # avoid abrupt change after rounding when c0 is near c1's perimeter - c = round_start_circle_stable_containment( - (x0.value, y0.value), r0.value, (x1.value, y1.value), r1.value - ) - x0, y0 = x0._replace(value=c.centre[0]), y0._replace(value=c.centre[1]) - r0 = r0._replace(value=c.radius) + c = round_start_circle_stable_containment((x0, y0), r0, (x1, y1), r1) + x0, y0 = c.centre + r0 = c.radius # update source to ensure paint is built with corrected values - source["x0"] = srcMapFn(x0) - source["y0"] = srcMapFn(y0) - source["r0"] = srcMapFn(r0) - source["x1"] = srcMapFn(x1) - source["y1"] = srcMapFn(y1) - source["r1"] = srcMapFn(r1) + source["x0"] = x0 + source["y0"] = y0 + source["r0"] = r0 + source["x1"] = x1 + source["y1"] = y1 + source["r1"] = r1 return paint, source -def _beforeBuildPaintRadialGradient(paint, source): - return _beforeBuildPaintVarRadialGradient(paint, source, lambda v: v.value) - - -def _defaultColorIndex(): - colorIndex = ot.ColorIndex() - colorIndex.Alpha = _DEFAULT_ALPHA.value - return colorIndex +def _defaultColorStop(): + colorStop = ot.ColorStop() + colorStop.Alpha = _DEFAULT_ALPHA + return colorStop -def _defaultVarColorIndex(): - colorIndex = ot.VarColorIndex() - colorIndex.Alpha = _DEFAULT_ALPHA - return colorIndex +def _defaultVarColorStop(): + colorStop = ot.VarColorStop() + colorStop.Alpha = _DEFAULT_ALPHA + return colorStop def _defaultColorLine(): @@ -113,6 +102,12 @@ def _defaultVarColorLine(): return colorLine +def _defaultPaintSolid(): + paint = ot.Paint() + paint.Alpha = _DEFAULT_ALPHA + return paint + + def _buildPaintCallbacks(): return { ( @@ -124,11 +119,21 @@ def _buildPaintCallbacks(): BuildCallback.BEFORE_BUILD, ot.Paint, ot.PaintFormat.PaintVarRadialGradient, - ): _beforeBuildPaintVarRadialGradient, - (BuildCallback.CREATE_DEFAULT, ot.ColorIndex): _defaultColorIndex, - (BuildCallback.CREATE_DEFAULT, ot.VarColorIndex): _defaultVarColorIndex, + ): _beforeBuildPaintRadialGradient, + (BuildCallback.CREATE_DEFAULT, ot.ColorStop): _defaultColorStop, + (BuildCallback.CREATE_DEFAULT, ot.VarColorStop): _defaultVarColorStop, (BuildCallback.CREATE_DEFAULT, ot.ColorLine): _defaultColorLine, (BuildCallback.CREATE_DEFAULT, ot.VarColorLine): _defaultVarColorLine, + ( + BuildCallback.CREATE_DEFAULT, + ot.Paint, + ot.PaintFormat.PaintSolid, + ): _defaultPaintSolid, + ( + BuildCallback.CREATE_DEFAULT, + ot.Paint, + ot.PaintFormat.PaintVarSolid, + ): _defaultPaintSolid, } @@ -140,11 +145,11 @@ def populateCOLRv0( """Build v0 color layers and add to existing COLR table. Args: - table: a raw otTables.COLR() object (not ttLib's table_C_O_L_R_). + table: a raw ``otTables.COLR()`` object (not ttLib's ``table_C_O_L_R_``). colorGlyphsV0: map of base glyph names to lists of (layer glyph names, - color palette index) tuples. + color palette index) tuples. Can be empty. glyphMap: a map from glyph names to glyph indices, as returned from - TTFont.getReverseGlyphMap(), to optionally sort base records by GID. + ``TTFont.getReverseGlyphMap()``, to optionally sort base records by GID. """ if glyphMap is not None: colorGlyphItems = sorted( @@ -167,11 +172,14 @@ def populateCOLRv0( layerRec.PaletteIndex = paletteIndex layerRecords.append(layerRec) + table.BaseGlyphRecordArray = table.LayerRecordArray = None + if baseGlyphRecords: + table.BaseGlyphRecordArray = ot.BaseGlyphRecordArray() + table.BaseGlyphRecordArray.BaseGlyphRecord = baseGlyphRecords + if layerRecords: + table.LayerRecordArray = ot.LayerRecordArray() + table.LayerRecordArray.LayerRecord = layerRecords table.BaseGlyphRecordCount = len(baseGlyphRecords) - table.BaseGlyphRecordArray = ot.BaseGlyphRecordArray() - table.BaseGlyphRecordArray.BaseGlyphRecord = baseGlyphRecords - table.LayerRecordArray = ot.LayerRecordArray() - table.LayerRecordArray.LayerRecord = layerRecords table.LayerRecordCount = len(layerRecords) @@ -180,12 +188,16 @@ def buildCOLR( version: Optional[int] = None, glyphMap: Optional[Mapping[str, int]] = None, varStore: Optional[ot.VarStore] = None, + varIndexMap: Optional[ot.DeltaSetIndexMap] = None, + clipBoxes: Optional[Dict[str, _ClipBoxInput]] = None, ) -> C_O_L_R_.table_C_O_L_R_: """Build COLR table from color layers mapping. + Args: + colorGlyphs: map of base glyph name to, either list of (layer glyph name, - color palette index) tuples for COLRv0; or a single Paint (dict) or - list of Paint for COLRv1. + color palette index) tuples for COLRv0; or a single ``Paint`` (dict) or + list of ``Paint`` for COLRv1. version: the version of COLR table. If None, the version is determined by the presence of COLRv1 paints or variation data (varStore), which require version 1; otherwise, if all base glyphs use only simple color @@ -193,7 +205,11 @@ def buildCOLR( glyphMap: a map from glyph names to glyph indices, as returned from TTFont.getReverseGlyphMap(), to optionally sort base records by GID. varStore: Optional ItemVarationStore for deltas associated with v1 layer. - Return: + varIndexMap: Optional DeltaSetIndexMap for deltas associated with v1 layer. + clipBoxes: Optional map of base glyph name to clip box 4- or 5-tuples: + (xMin, yMin, xMax, yMax) or (xMin, yMin, xMax, yMax, varIndexBase). + + Returns: A new COLR table. """ self = C_O_L_R_.table_C_O_L_R_() @@ -209,18 +225,13 @@ def buildCOLR( else: # unless explicitly requested for v1 or have variations, in which case # we encode all color glyph as v1 - colorGlyphsV0, colorGlyphsV1 = None, colorGlyphs + colorGlyphsV0, colorGlyphsV1 = {}, colorGlyphs colr = ot.COLR() - if colorGlyphsV0: - populateCOLRv0(colr, colorGlyphsV0, glyphMap) - else: - colr.BaseGlyphRecordCount = colr.LayerRecordCount = 0 - colr.BaseGlyphRecordArray = colr.LayerRecordArray = None + populateCOLRv0(colr, colorGlyphsV0, glyphMap) - if colorGlyphsV1: - colr.LayerV1List, colr.BaseGlyphV1List = buildColrV1(colorGlyphsV1, glyphMap) + colr.LayerList, colr.BaseGlyphList = buildColrV1(colorGlyphsV1, glyphMap) if version is None: version = 1 if (varStore or colorGlyphsV1) else 0 @@ -231,12 +242,38 @@ def buildCOLR( if version == 0: self.ColorLayers = self._decompileColorLayersV0(colr) else: + clipBoxes = { + name: clipBoxes[name] for name in clipBoxes or {} if name in colorGlyphsV1 + } + colr.ClipList = buildClipList(clipBoxes) if clipBoxes else None + colr.VarIndexMap = varIndexMap colr.VarStore = varStore self.table = colr return self +def buildClipList(clipBoxes: Dict[str, _ClipBoxInput]) -> ot.ClipList: + clipList = ot.ClipList() + clipList.Format = 1 + clipList.clips = {name: buildClipBox(box) for name, box in clipBoxes.items()} + return clipList + + +def buildClipBox(clipBox: _ClipBoxInput) -> ot.ClipBox: + if isinstance(clipBox, ot.ClipBox): + return clipBox + n = len(clipBox) + clip = ot.ClipBox() + if n not in (4, 5): + raise ValueError(f"Invalid ClipBox: expected 4 or 5 values, found {n}") + clip.xMin, clip.yMin, clip.xMax, clip.yMax = intRect(clipBox[:4]) + clip.Format = int(n == 5) + 1 + if n == 5: + clip.VarIndexBase = int(clipBox[4]) + return clip + + class ColorPaletteType(enum.IntFlag): USABLE_WITH_LIGHT_BACKGROUND = 0x0001 USABLE_WITH_DARK_BACKGROUND = 0x0002 @@ -406,15 +443,13 @@ def _reuse_ranges(num_layers: int) -> Generator[Tuple[int, int], None, None]: yield (lbound, ubound) -class LayerV1ListBuilder: - slices: List[ot.Paint] +class LayerListBuilder: layers: List[ot.Paint] reusePool: Mapping[Tuple[Any, ...], int] tuples: Mapping[int, Tuple[Any, ...]] keepAlive: List[ot.Paint] # we need id to remain valid def __init__(self): - self.slices = [] self.layers = [] self.reusePool = {} self.tuples = {} @@ -459,10 +494,6 @@ class LayerV1ListBuilder: # COLR layers is unusual in that it modifies shared state # so we need a callback into an object def _beforeBuildPaintColrLayers(self, dest, source): - paint = ot.Paint() - paint.Format = int(ot.PaintFormat.PaintColrLayers) - self.slices.append(paint) - # Sketchy gymnastics: a sequence input will have dropped it's layers # into NumLayers; get it back if isinstance(source.get("NumLayers", None), collections.abc.Sequence): @@ -520,6 +551,12 @@ class LayerV1ListBuilder: layers = [listToColrLayers(l) for l in layers] + # No reason to have a colr layers with just one entry + if len(layers) == 1: + return layers[0], {} + + paint = ot.Paint() + paint.Format = int(ot.PaintFormat.PaintColrLayers) paint.NumLayers = len(layers) paint.FirstLayerIndex = len(self.layers) self.layers.extend(layers) @@ -538,17 +575,19 @@ class LayerV1ListBuilder: def buildPaint(self, paint: _PaintInput) -> ot.Paint: return self.tableBuilder.build(ot.Paint, paint) - def build(self) -> ot.LayerV1List: - layers = ot.LayerV1List() + def build(self) -> Optional[ot.LayerList]: + if not self.layers: + return None + layers = ot.LayerList() layers.LayerCount = len(self.layers) layers.Paint = self.layers return layers -def buildBaseGlyphV1Record( - baseGlyph: str, layerBuilder: LayerV1ListBuilder, paint: _PaintInput -) -> ot.BaseGlyphV1List: - self = ot.BaseGlyphV1Record() +def buildBaseGlyphPaintRecord( + baseGlyph: str, layerBuilder: LayerListBuilder, paint: _PaintInput +) -> ot.BaseGlyphList: + self = ot.BaseGlyphPaintRecord() self.BaseGlyph = baseGlyph self.Paint = layerBuilder.buildPaint(paint) return self @@ -564,7 +603,7 @@ def _format_glyph_errors(errors: Mapping[str, Exception]) -> str: def buildColrV1( colorGlyphs: _ColorGlyphsDict, glyphMap: Optional[Mapping[str, int]] = None, -) -> Tuple[ot.LayerV1List, ot.BaseGlyphV1List]: +) -> Tuple[Optional[ot.LayerList], ot.BaseGlyphList]: if glyphMap is not None: colorGlyphItems = sorted( colorGlyphs.items(), key=lambda item: glyphMap[item[0]] @@ -574,24 +613,24 @@ def buildColrV1( errors = {} baseGlyphs = [] - layerBuilder = LayerV1ListBuilder() + layerBuilder = LayerListBuilder() for baseGlyph, paint in colorGlyphItems: try: - baseGlyphs.append(buildBaseGlyphV1Record(baseGlyph, layerBuilder, paint)) + baseGlyphs.append(buildBaseGlyphPaintRecord(baseGlyph, layerBuilder, paint)) except (ColorLibError, OverflowError, ValueError, TypeError) as e: errors[baseGlyph] = e if errors: failed_glyphs = _format_glyph_errors(errors) - exc = ColorLibError(f"Failed to build BaseGlyphV1List:\n{failed_glyphs}") + exc = ColorLibError(f"Failed to build BaseGlyphList:\n{failed_glyphs}") exc.errors = errors raise exc from next(iter(errors.values())) layers = layerBuilder.build() - glyphs = ot.BaseGlyphV1List() + glyphs = ot.BaseGlyphList() glyphs.BaseGlyphCount = len(baseGlyphs) - glyphs.BaseGlyphV1Record = baseGlyphs + glyphs.BaseGlyphPaintRecord = baseGlyphs return (layers, glyphs) |