Skip to content

Commit

Permalink
[COLRv2] Implement PaintGlyphSelf and PaintGlyphNearby
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 6b9f2ef
Show file tree
Hide file tree
Showing 3 changed files with 89 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,45 @@ def objectToTuple(obj, layerList):
)


def replaceSelfAndNearbyPaintGlyphs(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(replaceSelfAndNearbyPaintGlyphs(glyphName, o, reverseGlyphMap) for o in obj)

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

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

View check run for this annotation

Codecov / codecov/patch

Lib/fontTools/colorLib/COLRv1ToCOLRv2.py#L52

Added line #L52 was not covered by tests

paintFormat = None
paintGlyphName = None
paintPaint = None

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

View check run for this annotation

Codecov / codecov/patch

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

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

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

View check run for this annotation

Codecov / codecov/patch

Lib/fontTools/colorLib/COLRv1ToCOLRv2.py#L59

Added line #L59 was not covered by tests
if attr == 'Glyph':
paintGlyphName = 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
elif attr == 'Paint':
paintPaint = 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

if paintFormat != PaintFormat.PaintGlyph:
return ('Paint',) + tuple(replaceSelfAndNearbyPaintGlyphs(glyphName, o, reverseGlyphMap) for o in obj[1:])

# PaintGlyph

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

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

View check run for this annotation

Codecov / codecov/patch

Lib/fontTools/colorLib/COLRv1ToCOLRv2.py#L71

Added line #L71 was not covered by tests

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

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#L74-L76

Added lines #L74 - L76 were not covered by tests
if -128 <= delta <= 127:
return ('Paint', ('Format', PaintFormat.PaintGlyphNearby), ('Delta', delta), ('Paint', paintPaint))

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

View check run for this annotation

Codecov / codecov/patch

Lib/fontTools/colorLib/COLRv1ToCOLRv2.py#L78

Added line #L78 was not covered by tests

return obj

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

View check run for this annotation

Codecov / codecov/patch

Lib/fontTools/colorLib/COLRv1ToCOLRv2.py#L80

Added line #L80 was not covered by tests


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

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

View check run for this annotation

Codecov / codecov/patch

Lib/fontTools/colorLib/COLRv1ToCOLRv2.py#L85

Added line #L85 was not covered by tests
Expand Down Expand Up @@ -316,12 +353,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 357 in Lib/fontTools/colorLib/COLRv1ToCOLRv2.py

View check run for this annotation

Codecov / codecov/patch

Lib/fontTools/colorLib/COLRv1ToCOLRv2.py#L352-L357

Added lines #L352 - L357 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 362 in Lib/fontTools/colorLib/COLRv1ToCOLRv2.py

View check run for this annotation

Codecov / codecov/patch

Lib/fontTools/colorLib/COLRv1ToCOLRv2.py#L362

Added line #L362 was not covered by tests

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

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

View check run for this annotation

Codecov / codecov/patch

Lib/fontTools/colorLib/COLRv1ToCOLRv2.py#L369-L370

Added lines #L369 - L370 were not covered by tests

parser = argparse.ArgumentParser(

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
"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 377 in Lib/fontTools/colorLib/COLRv1ToCOLRv2.py

View check run for this annotation

Codecov / codecov/patch

Lib/fontTools/colorLib/COLRv1ToCOLRv2.py#L376-L377

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

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

View check run for this annotation

Codecov / codecov/patch

Lib/fontTools/colorLib/COLRv1ToCOLRv2.py#L415-L416

Added lines #L415 - L416 were not covered by tests

originalSize = None
v1Size = None

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

View check run for this annotation

Codecov / codecov/patch

Lib/fontTools/colorLib/COLRv1ToCOLRv2.py#L418

Added line #L418 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 424 in Lib/fontTools/colorLib/COLRv1ToCOLRv2.py

View check run for this annotation

Codecov / codecov/patch

Lib/fontTools/colorLib/COLRv1ToCOLRv2.py#L420-L424

Added lines #L420 - L424 were not covered by tests

if False:

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#L426

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

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

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

View check run for this annotation

Codecov / codecov/patch

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

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

genericTemplates = defaultdict(list)

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#L434

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

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
Expand Down Expand Up @@ -470,19 +511,17 @@ def main(args=None):
paintTuples[glyphName] = paintTuple
log.info("Skipped %d templates as they didn't save space", skipped)

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

View check run for this annotation

Codecov / codecov/patch

Lib/fontTools/colorLib/COLRv1ToCOLRv2.py#L511-L512

Added lines #L511 - L512 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 515 in Lib/fontTools/colorLib/COLRv1ToCOLRv2.py

View check run for this annotation

Codecov / codecov/patch

Lib/fontTools/colorLib/COLRv1ToCOLRv2.py#L514-L515

Added lines #L514 - L515 were not covered by tests

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

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

View check run for this annotation

Codecov / codecov/patch

Lib/fontTools/colorLib/COLRv1ToCOLRv2.py#L518

Added line #L518 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 524 in Lib/fontTools/colorLib/COLRv1ToCOLRv2.py

View check run for this annotation

Codecov / codecov/patch

Lib/fontTools/colorLib/COLRv1ToCOLRv2.py#L524

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

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

View check run for this annotation

Codecov / codecov/patch

Lib/fontTools/colorLib/COLRv1ToCOLRv2.py#L526

Added line #L526 was not covered by tests
if outfile:
Expand Down
29 changes: 29 additions & 0 deletions Lib/fontTools/ttLib/tables/otData.py
Expand Up @@ -6187,6 +6187,35 @@
("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.",
),
],
),
# PaintGlyphNearby
(
"PaintFormat36",
[
("uint8", "PaintFormat", None, None, "Format identifier-format = 36"),
(
"Offset24",
"Paint",
None,
None,
"Offset (from beginning of PaintGlyphNearby table) to Paint subtable.",
),
("int8", "Delta", None, None, "Delta for Glyph ID for the source outline."),
],
),
#
# 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
PaintGlyphNearby = 36

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

0 comments on commit 6b9f2ef

Please sign in to comment.