diff --git a/Lib/fontTools/colorLib/builder.py b/Lib/fontTools/colorLib/builder.py index 76d4bff64..c78d85bea 100644 --- a/Lib/fontTools/colorLib/builder.py +++ b/Lib/fontTools/colorLib/builder.py @@ -9,6 +9,7 @@ from functools import partial from typing import ( Any, Dict, + Generator, Iterable, List, Mapping, @@ -23,6 +24,7 @@ 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.otBase import BaseTable from fontTools.ttLib.tables import otTables as ot from fontTools.ttLib.tables.otTables import ( ExtendMode, @@ -111,8 +113,8 @@ def buildCOLR( Args: colorGlyphs: map of base glyph name to, either list of (layer glyph name, - color palette index) tuples for COLRv0; or list of Paints (dicts) - 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 @@ -148,7 +150,8 @@ def buildCOLR( colr.BaseGlyphRecordArray = colr.LayerRecordArray = None if colorGlyphsV1: - colr.BaseGlyphV1List = buildBaseGlyphV1List(colorGlyphsV1, glyphMap) + colr.LayerV1List, colr.BaseGlyphV1List = buildColrV1(colorGlyphsV1, glyphMap) + if version is None: version = 1 if (varStore or colorGlyphsV1) else 0 @@ -430,6 +433,111 @@ def _to_color_line(obj): raise TypeError(obj) +_PAINT_BUILDERS = { + 1: lambda _, kwargs: buildPaintSolid(**kwargs), + 2: lambda _, kwargs: buildPaintLinearGradient(**kwargs), + 3: lambda _, kwargs: buildPaintRadialGradient(**kwargs), + 4: lambda builder, kwargs: buildPaintGlyph(builder, **kwargs), + 5: lambda _, kwargs: buildPaintColrGlyph(**kwargs), + 6: lambda builder, kwargs: buildPaintTransform(builder, **kwargs), + 7: lambda builder, kwargs: buildPaintComposite(builder, **kwargs), +} + + +def _as_tuple(obj) -> Tuple[Any, ...]: + # start simple, who even cares about cyclic graphs or interesting field types + def _tuple_safe(value): + if isinstance(value, enum.Enum): + return value + elif hasattr(value, "__dict__"): + return tuple((k, _tuple_safe(v)) for k, v in value.__dict__.items()) + elif isinstance(value, collections.abc.MutableSequence): + return tuple(_tuple_safe(e) for e in value) + return value + return tuple(_tuple_safe(obj)) + + +def _reuse_ranges(num_layers: int) -> Generator[Tuple[int, int], None, None]: + # TODO feels like something itertools might have already + for lbound in range(num_layers): + # TODO may want a max length to limit scope of search + # Reuse of very large #s of layers is relatively unlikely + # +2: we want sequences of at least 2 + # otData handles single-record duplication + for ubound in range(lbound + 2, num_layers + 1): + yield (lbound, ubound) + + +class LayerCollector: + Slices: List[ot.Paint] + Layers: List[ot.Paint] + ReusePool: Mapping[Tuple[Any, ...], int] + + def __init__(self): + self.Slices = [] + self.Layers = [] + self.ReusePool = {} + + def buildColrLayers(self, paints: List[_PaintInput]) -> ot.Paint: + paint = ot.Paint() + paint.Format = int(ot.Paint.Format.PaintColrLayers) + self.Slices.append(paint) + + paints = [self.build(p) for p in paints] + + # Look for reuse, with preference to longer sequences + found_reuse = True + while found_reuse: + found_reuse = False + + ranges = sorted(_reuse_ranges(len(paints)), + key=lambda t: (t[1] - t[0], t[1], t[0]), + reverse=True) + for lbound, ubound in ranges: + reuse_lbound = self.ReusePool.get(_as_tuple(paints[lbound:ubound]), -1) + if reuse_lbound == -1: + continue + found_reuse = True + new_slice = ot.Paint() + new_slice.Format = int(ot.Paint.Format.PaintColrLayers) + new_slice.NumLayers = ubound - lbound + new_slice.FirstLayerIndex = reuse_lbound + paints = paints[:lbound] + [new_slice] + paints[ubound:] + + paint.NumLayers = len(paints) + paint.FirstLayerIndex = len(self.Layers) + self.Layers.extend(paints) + + # Register our parts for reuse + for lbound, ubound in _reuse_ranges(len(paints)): + self.ReusePool[_as_tuple(paints[lbound:ubound])] = lbound + paint.FirstLayerIndex + + return paint + + def build(self, paint: _PaintInput) -> ot.Paint: + if isinstance(paint, ot.Paint): + return paint + elif isinstance(paint, int): + paletteIndex = paint + return buildPaintSolid(paletteIndex) + elif isinstance(paint, tuple): + layerGlyph, paint = paint + return buildPaintGlyph(self, layerGlyph, paint) + elif isinstance(paint, list): + # implicit PaintColrLayers + return self.buildColrLayers(paint) + elif isinstance(paint, collections.abc.Mapping): + kwargs = dict(paint) + fmt = kwargs.pop("format") + try: + return _PAINT_BUILDERS[fmt](self, kwargs) + except KeyError: + raise NotImplementedError(fmt) + raise TypeError( + f"expected int, Mapping or ot.Paint, found {type(paint).__name__}: {paint!r}" + ) + + def buildPaintLinearGradient( colorLine: _ColorLineInput, p0: _PointTuple, @@ -487,115 +595,50 @@ def buildPaintRadialGradient( return self -def buildPaintGlyph(glyph: str, paint: _PaintInput) -> ot.Paint: +def buildPaintGlyph(layerCollector: LayerCollector, glyph: str, paint: _PaintInput) -> ot.Paint: self = ot.Paint() self.Format = int(ot.Paint.Format.PaintGlyph) self.Glyph = glyph - self.Paint = buildPaint(paint) + self.Paint = layerCollector.build(paint) return self -def buildPaintColrSlice( - glyph: str, firstLayerIndex: int = 0, lastLayerIndex: int = 255 +def buildPaintColrGlyph( + glyph: str ) -> ot.Paint: self = ot.Paint() - self.Format = int(ot.Paint.Format.PaintColrSlice) + self.Format = int(ot.Paint.Format.PaintColrGlyph) self.Glyph = glyph - if firstLayerIndex > lastLayerIndex: - raise ValueError( - f"Expected first <= last index, found: {firstLayerIndex} > {lastLayerIndex}" - ) - for prefix in ("first", "last"): - indexName = f"{prefix}LayerIndex" - index = locals()[indexName] - if index < 0 or index > 255: - raise OverflowError(f"{indexName} ({index}) out of range [0..255]") - self.FirstLayerIndex = firstLayerIndex - self.LastLayerIndex = lastLayerIndex return self -def buildPaintTransform(transform: _AffineInput, paint: _PaintInput) -> ot.Paint: +def buildPaintTransform(layerCollector: LayerCollector, transform: _AffineInput, paint: _PaintInput) -> ot.Paint: self = ot.Paint() self.Format = int(ot.Paint.Format.PaintTransform) if not isinstance(transform, ot.Affine2x3): transform = buildAffine2x3(transform) self.Transform = transform - self.Paint = buildPaint(paint) + self.Paint = layerCollector.build(paint) return self def buildPaintComposite( - mode: _CompositeInput, source: _PaintInput, backdrop: _PaintInput + layerCollector: LayerCollector, mode: _CompositeInput, source: _PaintInput, backdrop: _PaintInput ): self = ot.Paint() self.Format = int(ot.Paint.Format.PaintComposite) - self.SourcePaint = buildPaint(source) + self.SourcePaint = layerCollector.build(source) self.CompositeMode = _to_composite_mode(mode) - self.BackdropPaint = buildPaint(backdrop) - return self - - -_PAINT_BUILDERS = { - 1: buildPaintSolid, - 2: buildPaintLinearGradient, - 3: buildPaintRadialGradient, - 4: buildPaintGlyph, - 5: buildPaintColrSlice, - 6: buildPaintTransform, - 7: buildPaintComposite, -} - - -def buildPaint(paint: _PaintInput) -> ot.Paint: - if isinstance(paint, ot.Paint): - return paint - elif isinstance(paint, int): - paletteIndex = paint - return buildPaintSolid(paletteIndex) - elif isinstance(paint, tuple): - layerGlyph, paint = paint - return buildPaintGlyph(layerGlyph, paint) - elif isinstance(paint, collections.abc.Mapping): - kwargs = dict(paint) - fmt = kwargs.pop("format") - try: - return _PAINT_BUILDERS[fmt](**kwargs) - except KeyError: - raise NotImplementedError(fmt) - raise TypeError( - f"expected int, Mapping or ot.Paint, found {type(paint).__name__}: {paint!r}" - ) - - -def buildLayerV1List(layers: _PaintInputList) -> ot.LayerV1List: - self = ot.LayerV1List() - layerCount = len(layers) - self.LayerCount = layerCount - self.Paint = [buildPaint(layer) for layer in layers] - return self - - -def buildPaintColrLayers(firstLayerIndex: int, numLayers: int) -> ot.Paint: - self = ot.Paint() - self.Format = int(ot.Paint.Format.PaintColrLayers) - if numLayers > MAX_PAINT_COLR_LAYER_COUNT: - raise OverflowError( - "PaintColrLayers.NumLayers: {numLayers} > {MAX_PAINT_COLR_LAYER_COUNT}" - ) - self.NumLayers = numLayers - self.FirstLayerIndex = firstLayerIndex + self.BackdropPaint = layerCollector.build(backdrop) return self def buildBaseGlyphV1Record( - baseGlyph: str, layers: Union[_PaintInputList, ot.LayerV1List] + baseGlyph: str, layerCollector: LayerCollector, paint: _PaintInput ) -> ot.BaseGlyphV1List: self = ot.BaseGlyphV1Record() self.BaseGlyph = baseGlyph - if not isinstance(layers, ot.LayerV1List): - layers = buildLayerV1List(layers) - self.LayerV1List = layers + self.Paint = layerCollector.build(paint) return self @@ -606,10 +649,10 @@ def _format_glyph_errors(errors: Mapping[str, Exception]) -> str: return "\n".join(lines) -def buildBaseGlyphV1List( +def buildColrV1( colorGlyphs: _ColorGlyphsDict, glyphMap: Optional[Mapping[str, int]] = None, -) -> ot.BaseGlyphV1List: +) -> Tuple[ot.LayerV1List, ot.BaseGlyphV1List]: if glyphMap is not None: colorGlyphItems = sorted( colorGlyphs.items(), key=lambda item: glyphMap[item[0]] @@ -618,10 +661,12 @@ def buildBaseGlyphV1List( colorGlyphItems = colorGlyphs.items() errors = {} - records = [] - for baseGlyph, layers in colorGlyphItems: + baseGlyphs = [] + layerCollector = LayerCollector() + for baseGlyph, paint in colorGlyphItems: try: - records.append(buildBaseGlyphV1Record(baseGlyph, layers)) + baseGlyphs.append(buildBaseGlyphV1Record(baseGlyph, layerCollector, paint)) + except (ColorLibError, OverflowError, ValueError, TypeError) as e: errors[baseGlyph] = e @@ -631,7 +676,10 @@ def buildBaseGlyphV1List( exc.errors = errors raise exc from next(iter(errors.values())) - self = ot.BaseGlyphV1List() - self.BaseGlyphCount = len(records) - self.BaseGlyphV1Record = records - return self + layers = ot.LayerV1List() + layers.LayerCount = len(layerCollector.Layers) + layers.Paint = layerCollector.Layers + glyphs = ot.BaseGlyphV1List() + glyphs.BaseGlyphCount = len(baseGlyphs) + glyphs.BaseGlyphV1Record = baseGlyphs + return (layers, glyphs) diff --git a/Lib/fontTools/ttLib/tables/otData.py b/Lib/fontTools/ttLib/tables/otData.py index 3f747e547..6487340b3 100755 --- a/Lib/fontTools/ttLib/tables/otData.py +++ b/Lib/fontTools/ttLib/tables/otData.py @@ -1647,8 +1647,6 @@ otData = [ ('PaintFormat5', [ ('uint8', 'PaintFormat', None, None, 'Format identifier-format = 5'), ('GlyphID', 'Glyph', None, None, 'Virtual glyph ID for a BaseGlyphV1List base glyph.'), - ('uint8', 'FirstLayerIndex', None, None, 'First layer index to reuse'), - ('uint8', 'LastLayerIndex', None, None, 'Last layer index to reuse, inclusive'), ]), ('PaintFormat6', [ ('uint8', 'PaintFormat', None, None, 'Format identifier-format = 6'), diff --git a/Lib/fontTools/ttLib/tables/otTables.py b/Lib/fontTools/ttLib/tables/otTables.py index 8f57f2e94..d3ea700ad 100644 --- a/Lib/fontTools/ttLib/tables/otTables.py +++ b/Lib/fontTools/ttLib/tables/otTables.py @@ -1331,7 +1331,7 @@ class Paint(getFormatSwitchingBaseTableClass("uint8")): PaintLinearGradient = 2 PaintRadialGradient = 3 PaintGlyph = 4 - PaintColrSlice = 5 + PaintColrGlyph = 5 PaintTransform = 6 PaintComposite = 7 PaintColrLayers = 8 diff --git a/Tests/colorLib/builder_test.py b/Tests/colorLib/builder_test.py index ac0193b49..73bccb02a 100644 --- a/Tests/colorLib/builder_test.py +++ b/Tests/colorLib/builder_test.py @@ -1,8 +1,10 @@ from fontTools.ttLib import newTable from fontTools.ttLib.tables import otTables as ot from fontTools.colorLib import builder +from fontTools.colorLib.builder import LayerCollector from fontTools.colorLib.errors import ColorLibError import pytest +from typing import List def test_buildCOLR_v0(): @@ -345,18 +347,22 @@ def test_buildPaintRadialGradient(): assert gradient.ColorLine.ColorStop == color_stops -def test_buildPaintGlyph(): - layer = builder.buildPaintGlyph("a", 2) +def test_buildPaintGlyph_Solid(): + collector = LayerCollector() + layer = builder.buildPaintGlyph(collector, "a", 2) assert layer.Glyph == "a" assert layer.Paint.Format == ot.Paint.Format.PaintSolid assert layer.Paint.Color.PaletteIndex == 2 - layer = builder.buildPaintGlyph("a", builder.buildPaintSolid(3, 0.9)) + layer = builder.buildPaintGlyph(collector, "a", builder.buildPaintSolid(3, 0.9)) assert layer.Paint.Format == ot.Paint.Format.PaintSolid assert layer.Paint.Color.PaletteIndex == 3 assert layer.Paint.Color.Alpha.value == 0.9 + +def test_buildPaintGlyph_LinearGradient(): layer = builder.buildPaintGlyph( + LayerCollector(), "a", builder.buildPaintLinearGradient( {"stops": [(0.0, 3), (1.0, 4)]}, (100, 200), (150, 250) @@ -372,7 +378,10 @@ def test_buildPaintGlyph(): assert layer.Paint.x1.value == 150 assert layer.Paint.y1.value == 250 + +def test_buildPaintGlyph_RadialGradient(): layer = builder.buildPaintGlyph( + LayerCollector(), "a", builder.buildPaintRadialGradient( { @@ -404,13 +413,16 @@ def test_buildPaintGlyph(): assert layer.Paint.r1.value == 10 -def test_buildPaintGlyph_from_dict(): - layer = builder.buildPaintGlyph("a", {"format": 1, "paletteIndex": 0}) +def test_buildPaintGlyph_Dict_Solid(): + layer = builder.buildPaintGlyph(LayerCollector(), "a", {"format": 1, "paletteIndex": 0}) assert layer.Glyph == "a" assert layer.Paint.Format == ot.Paint.Format.PaintSolid assert layer.Paint.Color.PaletteIndex == 0 + +def test_buildPaintGlyph_Dict_LinearGradient(): layer = builder.buildPaintGlyph( + LayerCollector(), "a", { "format": 2, @@ -422,7 +434,10 @@ def test_buildPaintGlyph_from_dict(): assert layer.Paint.Format == ot.Paint.Format.PaintLinearGradient assert layer.Paint.ColorLine.ColorStop[0].StopOffset.value == 0.0 + +def test_buildPaintGlyph_Dict_RadialGradient(): layer = builder.buildPaintGlyph( + LayerCollector(), "a", { "format": 3, @@ -437,29 +452,18 @@ def test_buildPaintGlyph_from_dict(): assert layer.Paint.r0.value == 4 -def test_buildPaintColrSlice(): - paint = builder.buildPaintColrSlice("a") - assert paint.Format == ot.Paint.Format.PaintColrSlice +def test_buildPaintColrGlyph(): + paint = builder.buildPaintColrGlyph("a") + assert paint.Format == ot.Paint.Format.PaintColrGlyph assert paint.Glyph == "a" - assert paint.FirstLayerIndex == 0 - assert paint.LastLayerIndex == 255 - - paint = builder.buildPaintColrSlice("a", firstLayerIndex=1, lastLayerIndex=254) - assert paint.FirstLayerIndex == 1 - assert paint.LastLayerIndex == 254 - - with pytest.raises(ValueError, match="Expected first <= last index"): - builder.buildPaintColrSlice("a", 255, 0) - with pytest.raises(OverflowError, match="firstLayerIndex .* out of range"): - builder.buildPaintColrSlice("a", -1, 255) - with pytest.raises(OverflowError, match="lastLayerIndex .* out of range"): - builder.buildPaintColrSlice("a", 0, 256) def test_buildPaintTransform(): paint = builder.buildPaintTransform( + layerCollector=LayerCollector(), transform=builder.buildAffine2x3((1, 2, 3, 4, 5, 6)), paint=builder.buildPaintGlyph( + layerCollector=LayerCollector(), glyph="a", paint=builder.buildPaintSolid(paletteIndex=0, alpha=1.0), ), @@ -475,6 +479,7 @@ def test_buildPaintTransform(): assert paint.Paint.Format == ot.Paint.Format.PaintGlyph paint = builder.buildPaintTransform( + LayerCollector(), (1, 0, 0, 0.3333, 10, 10), { "format": 3, @@ -497,7 +502,9 @@ def test_buildPaintTransform(): def test_buildPaintComposite(): + collector = LayerCollector() composite = builder.buildPaintComposite( + layerCollector=collector, mode=ot.CompositeMode.SRC_OVER, source={ "format": 7, @@ -506,7 +513,7 @@ def test_buildPaintComposite(): "backdrop": {"format": 4, "glyph": "b", "paint": 1}, }, backdrop=builder.buildPaintGlyph( - "a", builder.buildPaintSolid(paletteIndex=0, alpha=1.0) + collector, "a", builder.buildPaintSolid(paletteIndex=0, alpha=1.0) ), ) @@ -530,56 +537,7 @@ def test_buildPaintComposite(): assert composite.BackdropPaint.Paint.Color.PaletteIndex == 0 -def test_buildLayerV1List(): - layers = [ - ("a", 1), - ("b", {"format": 1, "paletteIndex": 2, "alpha": 0.5}), - ( - "c", - { - "format": 2, - "colorLine": {"stops": [(0.0, 3), (1.0, 4)], "extend": "repeat"}, - "p0": (100, 200), - "p1": (150, 250), - }, - ), - ( - "d", - { - "format": 3, - "colorLine": { - "stops": [ - {"offset": 0.0, "paletteIndex": 5}, - {"offset": 0.5, "paletteIndex": 6, "alpha": 0.8}, - {"offset": 1.0, "paletteIndex": 7}, - ] - }, - "c0": (50, 50), - "c1": (75, 75), - "r0": 30, - "r1": 10, - }, - ), - builder.buildPaintGlyph("e", builder.buildPaintSolid(8)), - ] - layers = builder.buildLayerV1List(layers) - - assert layers.LayerCount == len(layers.Paint) - assert all(isinstance(l, ot.Paint) for l in layers.Paint) - - -def test_buildBaseGlyphV1Record(): - baseGlyphRec = builder.buildBaseGlyphV1Record("a", [("b", 0), ("c", 1)]) - assert baseGlyphRec.BaseGlyph == "a" - assert isinstance(baseGlyphRec.LayerV1List, ot.LayerV1List) - - layers = builder.buildLayerV1List([("b", 0), ("c", 1)]) - baseGlyphRec = builder.buildBaseGlyphV1Record("a", layers) - assert baseGlyphRec.BaseGlyph == "a" - assert baseGlyphRec.LayerV1List == layers - - -def test_buildBaseGlyphV1List(): +def test_buildColrV1(): colorGlyphs = { "a": [("b", 0), ("c", 1)], "d": [ @@ -596,7 +554,7 @@ def test_buildBaseGlyphV1List(): }, ), ], - "g": builder.buildLayerV1List([("h", 5)]), + "g": [("h", 5)], } glyphMap = { ".notdef": 0, @@ -610,13 +568,14 @@ def test_buildBaseGlyphV1List(): "h": 8, } - baseGlyphs = builder.buildBaseGlyphV1List(colorGlyphs, glyphMap) + # TODO(anthrotype) should we split into two tests? - seems two distinct validations + layers, baseGlyphs = builder.buildColrV1(colorGlyphs, glyphMap) assert baseGlyphs.BaseGlyphCount == len(colorGlyphs) assert baseGlyphs.BaseGlyphV1Record[0].BaseGlyph == "d" assert baseGlyphs.BaseGlyphV1Record[1].BaseGlyph == "a" assert baseGlyphs.BaseGlyphV1Record[2].BaseGlyph == "g" - baseGlyphs = builder.buildBaseGlyphV1List(colorGlyphs) + layers, baseGlyphs = builder.buildColrV1(colorGlyphs) assert baseGlyphs.BaseGlyphCount == len(colorGlyphs) assert baseGlyphs.BaseGlyphV1Record[0].BaseGlyph == "a" assert baseGlyphs.BaseGlyphV1Record[1].BaseGlyph == "d" @@ -669,6 +628,173 @@ def test_split_color_glyphs_by_version(): assert len(colorGlyphsV1["c"]) == 2 +def assertIsColrV1(colr): + assert colr.version == 1 + assert not hasattr(colr, "ColorLayers") + assert hasattr(colr, "table") + assert isinstance(colr.table, ot.COLR) + + +def assertNoV0Content(colr): + assert colr.table.BaseGlyphRecordCount == 0 + assert colr.table.BaseGlyphRecordArray is None + assert colr.table.LayerRecordCount == 0 + assert colr.table.LayerRecordArray is None + + +def test_build_layerv1list_empty(): + # Nobody uses PaintColorLayers (format 8), no layerlist + colr = builder.buildCOLR( + { + "a": { + "format": 4, # PaintGlyph + "paint": {"format": 1, "paletteIndex": 2, "alpha": 0.8}, + "glyph": "b", + }, + "b": { + "format": 4, # PaintGlyph + "paint": { + "format": 2, + "colorLine": { + "stops": [(0.0, 2), (1.0, 3)], + "extend": "reflect", + }, + "p0": (1, 2), + "p1": (3, 4), + "p2": (2, 2), + }, + "glyph": "bb", + }, + } + ) + + assertIsColrV1(colr) + assertNoV0Content(colr) + + # 2 v1 glyphs, none in LayerV1List + assert colr.table.BaseGlyphV1List.BaseGlyphCount == 2 + assert len(colr.table.BaseGlyphV1List.BaseGlyphV1Record) == 2 + assert colr.table.LayerV1List.LayerCount == 0 + assert len(colr.table.LayerV1List.Paint) == 0 + + +def _paint_names(paints) -> List[str]: + # prints a predictable string from a paint list to enable + # semi-readable assertions on a LayerV1List order. + result = [] + for paint in paints: + if paint.Format == int(ot.Paint.Format.PaintGlyph): + result.append(paint.Glyph) + elif paint.Format == int(ot.Paint.Format.PaintColrLayers): + result.append(f"Layers[{paint.FirstLayerIndex}:{paint.FirstLayerIndex+paint.NumLayers}]") + return result + +def test_build_layerv1list_simple(): + # Two colr glyphs, each with two layers the first of which is common + # All layers use the same solid paint + solid_paint = {"format": 1, "paletteIndex": 2, "alpha": 0.8} + backdrop = { + "format": 4, # PaintGlyph + "paint": solid_paint, + "glyph": "back", + } + a_foreground = { + "format": 4, # PaintGlyph + "paint": solid_paint, + "glyph": "a_fore", + } + b_foreground = { + "format": 4, # PaintGlyph + "paint": solid_paint, + "glyph": "b_fore", + } + + # list => PaintColrLayers, which means contents should be in LayerV1List + colr = builder.buildCOLR( + { + "a": [ + backdrop, + a_foreground, + ], + "b": [ + backdrop, + b_foreground, + ], + } + ) + + assertIsColrV1(colr) + assertNoV0Content(colr) + + # 2 v1 glyphs, 4 paints in LayerV1List + # A single shared backdrop isn't worth accessing by slice + assert colr.table.BaseGlyphV1List.BaseGlyphCount == 2 + assert len(colr.table.BaseGlyphV1List.BaseGlyphV1Record) == 2 + assert colr.table.LayerV1List.LayerCount == 4 + assert _paint_names(colr.table.LayerV1List.Paint) == ["back", "a_fore", "back", "b_fore"] + + +def test_build_layerv1list_with_sharing(): + # Three colr glyphs, each with two layers in common + solid_paint = {"format": 1, "paletteIndex": 2, "alpha": 0.8} + backdrop = [ + { + "format": 4, # PaintGlyph + "paint": solid_paint, + "glyph": "back1", + }, + { + "format": 4, # PaintGlyph + "paint": solid_paint, + "glyph": "back2", + }, + ] + a_foreground = { + "format": 4, # PaintGlyph + "paint": solid_paint, + "glyph": "a_fore", + } + b_background = { + "format": 4, # PaintGlyph + "paint": solid_paint, + "glyph": "b_back", + } + b_foreground = { + "format": 4, # PaintGlyph + "paint": solid_paint, + "glyph": "b_fore", + } + c_background = { + "format": 4, # PaintGlyph + "paint": solid_paint, + "glyph": "c_back", + } + + # list => PaintColrLayers, which means contents should be in LayerV1List + colr = builder.buildCOLR( + { + "a": backdrop + [a_foreground], + "b": [b_background] + backdrop + [b_foreground], + "c": [c_background] + backdrop, + } + ) + + assertIsColrV1(colr) + assertNoV0Content(colr) + + # 2 v1 glyphs, 4 paints in LayerV1List + # A single shared backdrop isn't worth accessing by slice + baseGlyphs = colr.table.BaseGlyphV1List.BaseGlyphV1Record + assert colr.table.BaseGlyphV1List.BaseGlyphCount == 3 + assert len(baseGlyphs) == 3 + assert (_paint_names([b.Paint for b in baseGlyphs]) == + ["Layers[0:3]", "Layers[3:6]", "Layers[6:8]"]) + assert _paint_names([baseGlyphs[0].Paint]), ["Layers[0:4]"] + assert _paint_names([baseGlyphs[0].Paint]), ["Layers[0:4]"] + assert (_paint_names(colr.table.LayerV1List.Paint) == + ["back1", "back2", "a_fore", "b_back", "Layers[0:2]", "b_fore", "c_back", "Layers[0:2]"]) + assert colr.table.LayerV1List.LayerCount == 8 + class BuildCOLRTest(object): def test_automatic_version_all_solid_color_glyphs(self): colr = builder.buildCOLR({"a": [("b", 0), ("c", 1)]}) @@ -714,10 +840,7 @@ class BuildCOLRTest(object): ], } ) - assert colr.version == 1 - assert not hasattr(colr, "ColorLayers") - assert hasattr(colr, "table") - assert isinstance(colr.table, ot.COLR) + assertIsColrV1(colr) assert colr.table.BaseGlyphRecordCount == 0 assert colr.table.BaseGlyphRecordArray is None assert colr.table.LayerRecordCount == 0 @@ -741,10 +864,7 @@ class BuildCOLRTest(object): ], } ) - assert colr.version == 1 - assert not hasattr(colr, "ColorLayers") - assert hasattr(colr, "table") - assert isinstance(colr.table, ot.COLR) + assertIsColrV1(colr) assert colr.table.VarStore is None assert colr.table.BaseGlyphRecordCount == 1 @@ -759,12 +879,9 @@ class BuildCOLRTest(object): ) assert colr.table.BaseGlyphV1List.BaseGlyphV1Record[0].BaseGlyph == "d" assert isinstance( - colr.table.BaseGlyphV1List.BaseGlyphV1Record[0].LayerV1List, ot.LayerV1List - ) - assert ( - colr.table.BaseGlyphV1List.BaseGlyphV1Record[0].LayerV1List.Paint[0].Glyph - == "e" + colr.table.LayerV1List, ot.LayerV1List ) + assert colr.table.LayerV1List.Paint[0].Glyph == "e" def test_explicit_version_0(self): colr = builder.buildCOLR({"a": [("b", 0), ("c", 1)]}, version=0)