diff --git a/Lib/fontTools/colorLib/unbuilder.py b/Lib/fontTools/colorLib/unbuilder.py new file mode 100644 index 000000000..6b7a09b30 --- /dev/null +++ b/Lib/fontTools/colorLib/unbuilder.py @@ -0,0 +1,201 @@ +from fontTools.ttLib.tables import otTables as ot + + +def unbuildColrV1(layerV1List, baseGlyphV1List, ignoreVarIdx=False): + unbuilder = LayerV1ListUnbuilder(layerV1List.Paint, ignoreVarIdx=ignoreVarIdx) + return { + rec.BaseGlyph: unbuilder.unbuildPaint(rec.Paint) + for rec in baseGlyphV1List.BaseGlyphV1Record + } + + +def _unbuildVariableValue(v, ignoreVarIdx=False): + return v.value if ignoreVarIdx else (v.value, v.varIdx) + + +def unbuildColorStop(colorStop, ignoreVarIdx=False): + return { + "offset": _unbuildVariableValue( + colorStop.StopOffset, ignoreVarIdx=ignoreVarIdx + ), + "paletteIndex": colorStop.Color.PaletteIndex, + "alpha": _unbuildVariableValue( + colorStop.Color.Alpha, ignoreVarIdx=ignoreVarIdx + ), + } + + +def unbuildColorLine(colorLine, ignoreVarIdx=False): + return { + "stops": [ + unbuildColorStop(stop, ignoreVarIdx=ignoreVarIdx) + for stop in colorLine.ColorStop + ], + "extend": colorLine.Extend.name.lower(), + } + + +def unbuildAffine2x3(transform, ignoreVarIdx=False): + return tuple( + _unbuildVariableValue(getattr(transform, attr), ignoreVarIdx=ignoreVarIdx) + for attr in ("xx", "yx", "xy", "yy", "dx", "dy") + ) + + +def _flatten(lst): + for el in lst: + if isinstance(el, list): + yield from _flatten(el) + else: + yield el + + +class LayerV1ListUnbuilder: + def __init__(self, layers, ignoreVarIdx=False): + self.layers = layers + self.ignoreVarIdx = ignoreVarIdx + + def unbuildPaint(self, paint): + try: + return self._unbuildFunctions[paint.Format](self, paint) + except KeyError: + raise ValueError(f"Unrecognized paint format: {paint.Format}") + + def unbuildVariableValue(self, value): + return _unbuildVariableValue(value, ignoreVarIdx=self.ignoreVarIdx) + + def unbuildPaintColrLayers(self, paint): + return list( + _flatten( + [ + self.unbuildPaint(childPaint) + for childPaint in self.layers[ + paint.FirstLayerIndex : paint.FirstLayerIndex + paint.NumLayers + ] + ] + ) + ) + + def unbuildPaintSolid(self, paint): + return { + "format": int(paint.Format), + "paletteIndex": paint.Color.PaletteIndex, + "alpha": self.unbuildVariableValue(paint.Color.Alpha), + } + + def unbuildPaintLinearGradient(self, paint): + p0 = (self.unbuildVariableValue(paint.x0), self.unbuildVariableValue(paint.y0)) + p1 = (self.unbuildVariableValue(paint.x1), self.unbuildVariableValue(paint.y1)) + p2 = (self.unbuildVariableValue(paint.x2), self.unbuildVariableValue(paint.y2)) + return { + "format": int(ot.Paint.Format.PaintLinearGradient), + "colorLine": unbuildColorLine( + paint.ColorLine, ignoreVarIdx=self.ignoreVarIdx + ), + "p0": p0, + "p1": p1, + "p2": p2, + } + + def unbuildPaintRadialGradient(self, paint): + c0 = (self.unbuildVariableValue(paint.x0), self.unbuildVariableValue(paint.y0)) + r0 = self.unbuildVariableValue(paint.r0) + c1 = (self.unbuildVariableValue(paint.x1), self.unbuildVariableValue(paint.y1)) + r1 = self.unbuildVariableValue(paint.r1) + return { + "format": int(ot.Paint.Format.PaintRadialGradient), + "colorLine": unbuildColorLine( + paint.ColorLine, ignoreVarIdx=self.ignoreVarIdx + ), + "c0": c0, + "r0": r0, + "c1": c1, + "r1": r1, + } + + def unbuildPaintGlyph(self, paint): + return { + "format": int(ot.Paint.Format.PaintGlyph), + "glyph": paint.Glyph, + "paint": self.unbuildPaint(paint.Paint), + } + + def unbuildPaintColrGlyph(self, paint): + return { + "format": int(ot.Paint.Format.PaintColrGlyph), + "glyph": paint.Glyph, + } + + def unbuildPaintTransform(self, paint): + return { + "format": int(ot.Paint.Format.PaintTransform), + "transform": unbuildAffine2x3( + paint.Transform, ignoreVarIdx=self.ignoreVarIdx + ), + "paint": self.unbuildPaint(paint.Paint), + } + + def unbuildPaintTranslate(self, paint): + return { + "format": int(ot.Paint.Format.PaintTranslate), + "dx": self.unbuildVariableValue(paint.dx), + "dy": self.unbuildVariableValue(paint.dy), + "paint": self.unbuildPaint(paint.Paint), + } + + def unbuildPaintRotate(self, paint): + return { + "format": int(ot.Paint.Format.PaintRotate), + "angle": self.unbuildVariableValue(paint.angle), + "centerX": self.unbuildVariableValue(paint.centerX), + "centerY": self.unbuildVariableValue(paint.centerY), + "paint": self.unbuildPaint(paint.Paint), + } + + def unbuildPaintSkew(self, paint): + return { + "format": int(ot.Paint.Format.PaintSkew), + "xSkewAngle": self.unbuildVariableValue(paint.xSkewAngle), + "ySkewAngle": self.unbuildVariableValue(paint.ySkewAngle), + "centerX": self.unbuildVariableValue(paint.centerX), + "centerY": self.unbuildVariableValue(paint.centerY), + "paint": self.unbuildPaint(paint.Paint), + } + + def unbuildPaintComposite(self, paint): + return { + "format": int(ot.Paint.Format.PaintComposite), + "mode": paint.CompositeMode.name.lower(), + "source": self.unbuildPaint(paint.SourcePaint), + "backdrop": self.unbuildPaint(paint.BackdropPaint), + } + + +LayerV1ListUnbuilder._unbuildFunctions = { + pf.value: getattr(LayerV1ListUnbuilder, "unbuild" + pf.name) + for pf in ot.Paint.Format +} + + +if __name__ == "__main__": + from pprint import pprint + import sys + from fontTools.ttLib import TTFont + + try: + fontfile = sys.argv[1] + except IndexError: + sys.exit("usage: fonttools colorLib.unbuilder FONTFILE") + + font = TTFont(fontfile) + colr = font["COLR"] + if colr.version < 1: + sys.exit(f"error: No COLR table version=1 found in {fontfile}") + + colorGlyphs = unbuildColrV1( + colr.table.LayerV1List, + colr.table.BaseGlyphV1List, + ignoreVarIdx=not colr.table.VarStore, + ) + + pprint(colorGlyphs) diff --git a/Tests/colorLib/unbuilder_test.py b/Tests/colorLib/unbuilder_test.py new file mode 100644 index 000000000..9d115b415 --- /dev/null +++ b/Tests/colorLib/unbuilder_test.py @@ -0,0 +1,141 @@ +from fontTools.ttLib.tables import otTables as ot +from fontTools.colorLib.builder import buildColrV1 +from fontTools.colorLib.unbuilder import unbuildColrV1 +import pytest + + +TEST_COLOR_GLYPHS = { + "glyph00010": [ + { + "format": int(ot.Paint.Format.PaintGlyph), + "glyph": "glyph00011", + "paint": { + "format": int(ot.Paint.Format.PaintSolid), + "paletteIndex": 2, + "alpha": 0.5, + }, + }, + { + "format": int(ot.Paint.Format.PaintGlyph), + "glyph": "glyph00012", + "paint": { + "format": int(ot.Paint.Format.PaintLinearGradient), + "colorLine": { + "stops": [ + {"offset": 0.0, "paletteIndex": 3, "alpha": 1.0}, + {"offset": 0.5, "paletteIndex": 4, "alpha": 1.0}, + {"offset": 1.0, "paletteIndex": 5, "alpha": 1.0}, + ], + "extend": "repeat", + }, + "p0": (1, 2), + "p1": (-3, -4), + "p2": (5, 6), + }, + }, + { + "format": int(ot.Paint.Format.PaintGlyph), + "glyph": "glyph00013", + "paint": { + "format": int(ot.Paint.Format.PaintTransform), + "transform": (-13.0, 14.0, 15.0, -17.0, 18.0, 19.0), + "paint": { + "format": int(ot.Paint.Format.PaintRadialGradient), + "colorLine": { + "stops": [ + {"offset": 0.0, "paletteIndex": 6, "alpha": 1.0}, + { + "offset": 1.0, + "paletteIndex": 7, + "alpha": 0.4, + }, + ], + "extend": "pad", + }, + "c0": (7, 8), + "r0": 9, + "c1": (10, 11), + "r1": 12, + }, + }, + }, + { + "format": int(ot.Paint.Format.PaintTranslate), + "dx": 257.0, + "dy": 258.0, + "paint": { + "format": int(ot.Paint.Format.PaintRotate), + "angle": 45.0, + "centerX": 255.0, + "centerY": 256.0, + "paint": { + "format": int(ot.Paint.Format.PaintSkew), + "xSkewAngle": -11.0, + "ySkewAngle": 5.0, + "centerX": 253.0, + "centerY": 254.0, + "paint": { + "format": int(ot.Paint.Format.PaintGlyph), + "glyph": "glyph00011", + "paint": { + "format": int(ot.Paint.Format.PaintSolid), + "paletteIndex": 2, + "alpha": 0.5, + }, + }, + }, + }, + }, + ], + "glyph00014": { + "format": int(ot.Paint.Format.PaintComposite), + "mode": "src_over", + "source": { + "format": int(ot.Paint.Format.PaintColrGlyph), + "glyph": "glyph00010", + }, + "backdrop": { + "format": int(ot.Paint.Format.PaintTransform), + "transform": (1.0, 0.0, 0.0, 1.0, 300.0, 0.0), + "paint": { + "format": int(ot.Paint.Format.PaintColrGlyph), + "glyph": "glyph00010", + }, + }, + }, + "glyph00015": [ + { + "format": int(ot.Paint.Format.PaintGlyph), + "glyph": "glyph00011", + "paint": { + "format": int(ot.Paint.Format.PaintSolid), + "paletteIndex": 2, + "alpha": 0.5, + }, + }, + { + "format": int(ot.Paint.Format.PaintGlyph), + "glyph": "glyph00012", + "paint": { + "format": int(ot.Paint.Format.PaintLinearGradient), + "colorLine": { + "stops": [ + {"offset": 0.0, "paletteIndex": 3, "alpha": 1.0}, + {"offset": 0.5, "paletteIndex": 4, "alpha": 1.0}, + {"offset": 1.0, "paletteIndex": 5, "alpha": 1.0}, + ], + "extend": "repeat", + }, + "p0": (1, 2), + "p1": (-3, -4), + "p2": (5, 6), + }, + }, + ], +} + + +def test_unbuildColrV1(): + layersV1, baseGlyphsV1 = buildColrV1(TEST_COLOR_GLYPHS) + colorGlyphs = unbuildColrV1(layersV1, baseGlyphsV1, ignoreVarIdx=True) + assert colorGlyphs == TEST_COLOR_GLYPHS