Skip to content

Commit

Permalink
[COLRv2] Implement PaintGlyphSelf and PaintGlyphDelta
Browse files Browse the repository at this point in the history
Implements extended version of:
googlefonts/colr-gradients-spec#370
  • Loading branch information
behdad committed Aug 6, 2023
1 parent 9dd6dd3 commit 87ce67b
Show file tree
Hide file tree
Showing 3 changed files with 110 additions and 19 deletions.
@@ -1,7 +1,5 @@
"""
Loads a COLRv1 font and tries to detect paint graphs that can
use templatization, and proceeds to produce a font with such
templates.
Loads a COLRv1 font and upgrades it to COLRv2.
"""

from fontTools.ttLib.tables.otBase import OTTableWriter
Expand All @@ -16,7 +14,7 @@
"main",
]

log = logging.getLogger("fontTools.colorLib.templatize")
log = logging.getLogger("fontTools.colorLib.COLRv1ToCOLRv2")


def objectToTuple(obj, layerList):
Expand All @@ -43,6 +41,55 @@ def objectToTuple(obj, layerList):
)


def replaceSelfAndDeltaPaintGlyphs(glyphName, obj, reverseGlyphMap=None):
if isinstance(obj, (int, float, str)):
return obj

Check warning on line 46 in Lib/fontTools/colorLib/COLRv1ToCOLRv2.py

View check run for this annotation

Codecov / codecov/patch

Lib/fontTools/colorLib/COLRv1ToCOLRv2.py#L46

Added line #L46 was not covered by tests

if obj[0] in ("list", "PaintColrLayers"):
return tuple(
replaceSelfAndDeltaPaintGlyphs(glyphName, o, reverseGlyphMap) for o in obj
)

if obj[0] != "Paint":
return obj

Check warning on line 54 in Lib/fontTools/colorLib/COLRv1ToCOLRv2.py

View check run for this annotation

Codecov / codecov/patch

Lib/fontTools/colorLib/COLRv1ToCOLRv2.py#L54

Added line #L54 was not covered by tests

paintFormat = None
paintGlyphName = None
paintPaint = None

Check warning on line 58 in Lib/fontTools/colorLib/COLRv1ToCOLRv2.py

View check run for this annotation

Codecov / codecov/patch

Lib/fontTools/colorLib/COLRv1ToCOLRv2.py#L56-L58

Added lines #L56 - L58 were not covered by tests
for attr, val in obj[1:]:
if attr == "Format":
paintFormat = val

Check warning on line 61 in Lib/fontTools/colorLib/COLRv1ToCOLRv2.py

View check run for this annotation

Codecov / codecov/patch

Lib/fontTools/colorLib/COLRv1ToCOLRv2.py#L61

Added line #L61 was not covered by tests
if attr == "Glyph":
paintGlyphName = val

Check warning on line 63 in Lib/fontTools/colorLib/COLRv1ToCOLRv2.py

View check run for this annotation

Codecov / codecov/patch

Lib/fontTools/colorLib/COLRv1ToCOLRv2.py#L63

Added line #L63 was not covered by tests
elif attr == "Paint":
paintPaint = val

Check warning on line 65 in Lib/fontTools/colorLib/COLRv1ToCOLRv2.py

View check run for this annotation

Codecov / codecov/patch

Lib/fontTools/colorLib/COLRv1ToCOLRv2.py#L65

Added line #L65 was not covered by tests

if paintFormat != PaintFormat.PaintGlyph:
return ("Paint",) + tuple(
(k, replaceSelfAndDeltaPaintGlyphs(glyphName, v, reverseGlyphMap))
for k, v in obj[1:]
)

# PaintGlyph

if glyphName == paintGlyphName:
return ("Paint", ("Format", PaintFormat.PaintGlyphSelf), ("Paint", paintPaint))

Check warning on line 76 in Lib/fontTools/colorLib/COLRv1ToCOLRv2.py

View check run for this annotation

Codecov / codecov/patch

Lib/fontTools/colorLib/COLRv1ToCOLRv2.py#L76

Added line #L76 was not covered by tests

if reverseGlyphMap is not None:
glyphID = reverseGlyphMap.get(glyphName)
paintGlyphID = reverseGlyphMap.get(paintGlyphName)
delta = (paintGlyphID - glyphID) % 65536

Check warning on line 81 in Lib/fontTools/colorLib/COLRv1ToCOLRv2.py

View check run for this annotation

Codecov / codecov/patch

Lib/fontTools/colorLib/COLRv1ToCOLRv2.py#L79-L81

Added lines #L79 - L81 were not covered by tests
if delta < 65536 and ((glyphID + delta) % 65536 == paintGlyphID):
return (

Check warning on line 83 in Lib/fontTools/colorLib/COLRv1ToCOLRv2.py

View check run for this annotation

Codecov / codecov/patch

Lib/fontTools/colorLib/COLRv1ToCOLRv2.py#L83

Added line #L83 was not covered by tests
"Paint",
("Format", PaintFormat.PaintGlyphDelta),
("DeltaGlyphID", delta),
("Paint", paintPaint),
)

return obj

Check warning on line 90 in Lib/fontTools/colorLib/COLRv1ToCOLRv2.py

View check run for this annotation

Codecov / codecov/patch

Lib/fontTools/colorLib/COLRv1ToCOLRv2.py#L90

Added line #L90 was not covered by tests


def templateForObjectTuple(objTuple):
if not isinstance(objTuple, tuple):
return objTuple

Check warning on line 95 in Lib/fontTools/colorLib/COLRv1ToCOLRv2.py

View check run for this annotation

Codecov / codecov/patch

Lib/fontTools/colorLib/COLRv1ToCOLRv2.py#L95

Added line #L95 was not covered by tests
Expand Down Expand Up @@ -316,12 +363,12 @@ def rebuildColr(font, paintTuples):
colr.compile(writer, font)
data = writer.getAllData()
l = len(data)
log.info("Reconstructed COLR table is %d bytes", l)
log.info("Constructed COLRv2 table is %d bytes", l)
return l

Check warning on line 367 in Lib/fontTools/colorLib/COLRv1ToCOLRv2.py

View check run for this annotation

Codecov / codecov/patch

Lib/fontTools/colorLib/COLRv1ToCOLRv2.py#L362-L367

Added lines #L362 - L367 were not covered by tests


def main(args=None):
"""Templatize a COLRv1 color font"""
"""Convert a COLRv1 color font to COLRv2"""
from fontTools import configLogger

Check warning on line 372 in Lib/fontTools/colorLib/COLRv1ToCOLRv2.py

View check run for this annotation

Codecov / codecov/patch

Lib/fontTools/colorLib/COLRv1ToCOLRv2.py#L372

Added line #L372 was not covered by tests

if args is None:
Expand All @@ -333,8 +380,8 @@ def main(args=None):
import argparse

Check warning on line 380 in Lib/fontTools/colorLib/COLRv1ToCOLRv2.py

View check run for this annotation

Codecov / codecov/patch

Lib/fontTools/colorLib/COLRv1ToCOLRv2.py#L379-L380

Added lines #L379 - L380 were not covered by tests

parser = argparse.ArgumentParser(

Check warning on line 382 in Lib/fontTools/colorLib/COLRv1ToCOLRv2.py

View check run for this annotation

Codecov / codecov/patch

Lib/fontTools/colorLib/COLRv1ToCOLRv2.py#L382

Added line #L382 was not covered by tests
"fonttools colorLib.templatize",
description="Templatize a COLRv1 color font.",
"fonttools colorLib.COLRv1ToCOLRv2",
description="Convert a COLRv1 color font to COLRv2.",
)
parser.add_argument("font", metavar="font.ttf", help="Font file.")
parser.add_argument(

Check warning on line 387 in Lib/fontTools/colorLib/COLRv1ToCOLRv2.py

View check run for this annotation

Codecov / codecov/patch

Lib/fontTools/colorLib/COLRv1ToCOLRv2.py#L386-L387

Added lines #L386 - L387 were not covered by tests
Expand Down Expand Up @@ -378,18 +425,27 @@ def main(args=None):
paintTuple = objectToTuple(paint, layerList)
paintTuples[glyphName] = paintTuple

Check warning on line 426 in Lib/fontTools/colorLib/COLRv1ToCOLRv2.py

View check run for this annotation

Codecov / codecov/patch

Lib/fontTools/colorLib/COLRv1ToCOLRv2.py#L425-L426

Added lines #L425 - L426 were not covered by tests

originalSize = None
v1Size = None

Check warning on line 428 in Lib/fontTools/colorLib/COLRv1ToCOLRv2.py

View check run for this annotation

Codecov / codecov/patch

Lib/fontTools/colorLib/COLRv1ToCOLRv2.py#L428

Added line #L428 was not covered by tests
if log.isEnabledFor(logging.INFO):
writer = OTTableWriter()
colr.compile(writer, font)
data = writer.getAllData()
originalSize = len(data)
log.info("Original COLR table is %d bytes", originalSize)
v1Size = len(data)
log.info("Original COLRv1 table is %d bytes", v1Size)

Check warning on line 434 in Lib/fontTools/colorLib/COLRv1ToCOLRv2.py

View check run for this annotation

Codecov / codecov/patch

Lib/fontTools/colorLib/COLRv1ToCOLRv2.py#L430-L434

Added lines #L430 - L434 were not covered by tests

if False:

Check warning on line 436 in Lib/fontTools/colorLib/COLRv1ToCOLRv2.py

View check run for this annotation

Codecov / codecov/patch

Lib/fontTools/colorLib/COLRv1ToCOLRv2.py#L436

Added line #L436 was not covered by tests
log.info("Rebuilding original font.")
rebuildColr(font, paintTuples)

log.info("Detecting self/delta glyph paints.")
reverseGlyphMap = font.getReverseGlyphMap()

Check warning on line 441 in Lib/fontTools/colorLib/COLRv1ToCOLRv2.py

View check run for this annotation

Codecov / codecov/patch

Lib/fontTools/colorLib/COLRv1ToCOLRv2.py#L440-L441

Added lines #L440 - L441 were not covered by tests
paintTuples = {
k: replaceSelfAndDeltaPaintGlyphs(k, v, reverseGlyphMap)
for k, v in paintTuples.items()
}

log.info("Templatizing paints.")

Check warning on line 447 in Lib/fontTools/colorLib/COLRv1ToCOLRv2.py

View check run for this annotation

Codecov / codecov/patch

Lib/fontTools/colorLib/COLRv1ToCOLRv2.py#L447

Added line #L447 was not covered by tests

genericTemplates = defaultdict(list)

Check warning on line 449 in Lib/fontTools/colorLib/COLRv1ToCOLRv2.py

View check run for this annotation

Codecov / codecov/patch

Lib/fontTools/colorLib/COLRv1ToCOLRv2.py#L449

Added line #L449 was not covered by tests
for glyphName, paintTuple in paintTuples.items():
paintTemplate = templateForObjectTuple(paintTuple)

Check warning on line 451 in Lib/fontTools/colorLib/COLRv1ToCOLRv2.py

View check run for this annotation

Codecov / codecov/patch

Lib/fontTools/colorLib/COLRv1ToCOLRv2.py#L451

Added line #L451 was not covered by tests
Expand Down Expand Up @@ -470,19 +526,17 @@ def main(args=None):
paintTuples[glyphName] = paintTuple
log.info("Skipped %d templates as they didn't save space", skipped)

Check warning on line 527 in Lib/fontTools/colorLib/COLRv1ToCOLRv2.py

View check run for this annotation

Codecov / codecov/patch

Lib/fontTools/colorLib/COLRv1ToCOLRv2.py#L526-L527

Added lines #L526 - L527 were not covered by tests

log.info("Building templatized font")
templatizedSize = rebuildColr(font, paintTuples)
log.info("Building COLRv2 font")
v2Size = rebuildColr(font, paintTuples)

Check warning on line 530 in Lib/fontTools/colorLib/COLRv1ToCOLRv2.py

View check run for this annotation

Codecov / codecov/patch

Lib/fontTools/colorLib/COLRv1ToCOLRv2.py#L529-L530

Added lines #L529 - L530 were not covered by tests

if originalSize is not None:
if v1Size is not None:
log.info(

Check warning on line 533 in Lib/fontTools/colorLib/COLRv1ToCOLRv2.py

View check run for this annotation

Codecov / codecov/patch

Lib/fontTools/colorLib/COLRv1ToCOLRv2.py#L533

Added line #L533 was not covered by tests
"Templatized COLR table is %.3g%% smaller.",
100 * (1 - templatizedSize / originalSize),
"COLRv2 table is %.3g%% smaller.",
100 * (1 - v2Size / v1Size),
)

if options.output_file is None:
outfile = makeOutputFileName(
options.font, overWrite=True, suffix=".templatized"
)
outfile = makeOutputFileName(options.font, overWrite=True, suffix=".COLRv2")

Check warning on line 539 in Lib/fontTools/colorLib/COLRv1ToCOLRv2.py

View check run for this annotation

Codecov / codecov/patch

Lib/fontTools/colorLib/COLRv1ToCOLRv2.py#L539

Added line #L539 was not covered by tests
else:
outfile = options.output_file

Check warning on line 541 in Lib/fontTools/colorLib/COLRv1ToCOLRv2.py

View check run for this annotation

Codecov / codecov/patch

Lib/fontTools/colorLib/COLRv1ToCOLRv2.py#L541

Added line #L541 was not covered by tests
if outfile:
Expand Down
35 changes: 35 additions & 0 deletions Lib/fontTools/ttLib/tables/otData.py
Expand Up @@ -6187,6 +6187,41 @@
("uint8", "ArgumentIndex", None, None, ""),
],
),
# PaintGlyphSelf
(
"PaintFormat35",
[
("uint8", "PaintFormat", None, None, "Format identifier-format = 35"),
(
"Offset24",
"Paint",
None,
None,
"Offset (from beginning of PaintGlyphSelf table) to Paint subtable.",
),
],
),
# PaintGlyphDelta
(
"PaintFormat36",
[
("uint8", "PaintFormat", None, None, "Format identifier-format = 36"),
(
"Offset24",
"Paint",
None,
None,
"Offset (from beginning of PaintGlyphDelta table) to Paint subtable.",
),
(
"uint16",
"DeltaGlyphID",
None,
None,
"Add to original GlyphID modulo 65536 to get substitute GlyphID",
),
],
),
#
# avar
#
Expand Down
2 changes: 2 additions & 0 deletions Lib/fontTools/ttLib/tables/otTables.py
Expand Up @@ -1564,6 +1564,8 @@ class PaintFormat(IntEnum):
PaintComposite = 32
PaintTemplateInstance = 33
PaintTemplateArgument = 34
PaintGlyphSelf = 35
PaintGlyphDelta = 36

def is_variable(self):
return self.name.startswith("PaintVar")
Expand Down

0 comments on commit 87ce67b

Please sign in to comment.