From 8f66a1e81306e8e71c01e737fff4c8681574e356 Mon Sep 17 00:00:00 2001 From: Cosimo Lupo Date: Wed, 3 Feb 2021 16:47:59 +0000 Subject: [PATCH] COLRv1: add functions to un-build COLR otTables to raw dicts This adds an unbuildColrV1 which does the inverse of colorLib.builder.buildColrV1. Takes a LayerV1List and BaseGlypV1List and returns a map of base glyphs to raw data structures (list, dict, float, str, etc.). Useful not only for debugging purpose, but also for implementing COLRv1 subsetting (where we need to drop whole chunks of paints which may be reused by multiple glyphs). --- Lib/fontTools/colorLib/unbuilder.py | 201 ++++++++++++++++++++++++++++ Tests/colorLib/unbuilder_test.py | 141 +++++++++++++++++++ 2 files changed, 342 insertions(+) create mode 100644 Lib/fontTools/colorLib/unbuilder.py create mode 100644 Tests/colorLib/unbuilder_test.py 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