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:
Cosimo Lupo 2021-02-03 16:47:59 +00:00
parent 728258d66f
commit 8f66a1e813
No known key found for this signature in database
GPG Key ID: 179A8F0895A02F4F
2 changed files with 342 additions and 0 deletions

View 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)

View 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