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).
This commit is contained in:
parent
728258d66f
commit
8f66a1e813
201
Lib/fontTools/colorLib/unbuilder.py
Normal file
201
Lib/fontTools/colorLib/unbuilder.py
Normal file
@ -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)
|
141
Tests/colorLib/unbuilder_test.py
Normal file
141
Tests/colorLib/unbuilder_test.py
Normal file
@ -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
|
Loading…
x
Reference in New Issue
Block a user