Skip to content

Commit

Permalink
Starting to play with COLR => SVG
Browse files Browse the repository at this point in the history
  • Loading branch information
rsheeter committed Mar 6, 2022
1 parent dbd4de5 commit 12a82dd
Show file tree
Hide file tree
Showing 16 changed files with 348 additions and 41 deletions.
36 changes: 36 additions & 0 deletions src/nanoemoji/colr.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# Copyright 2022 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""Helpers for dealing with COLR."""


from fontTools.ttLib.tables import otTables as ot
from fontTools import ttLib
from typing import Iterable


def paints_of_type(
font: ttLib.TTFont, paint_format: ot.PaintFormat
) -> Iterable[ot.Paint]:
result = []

def _callback(paint):
if paint.Format == paint_format:
result.append(paint)

colr_table = font["COLR"].table
for record in colr_table.BaseGlyphList.BaseGlyphPaintRecord:
record.Paint.traverse(colr_table, _callback)

return tuple(result)
82 changes: 58 additions & 24 deletions tests/colr_to_svg.py → src/nanoemoji/colr_to_svg.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@
# See the License for the specific language governing permissions and
# limitations under the License.

try:
import importlib.resources as resources # pytype: disable=import-error
except ImportError:
import importlib_resources as resources # pytype: disable=import-error
from nanoemoji import colors
from nanoemoji import color_glyph
from nanoemoji.glyph_reuse import GlyphReuseCache
Expand Down Expand Up @@ -44,14 +48,14 @@
from picosvg.svg_transform import Affine2D
from fontTools import ttLib
from picosvg.geometric_types import Point, Rect
import test_helper
from lxml import etree
from typing import Any, Dict, Mapping, Optional
from typing import Any, Callable, Dict, Iterable, Mapping, Optional, Tuple
from fontTools.pens import transformPen
from fontTools.ttLib.tables import otTables


_GRADIENT_PAINT_FORMATS = (PaintLinearGradient.format, PaintRadialGradient.format)
ViewboxCallback = Callable[[str], Rect] # f(glyph_name) -> Rect


def _map_font_space_to_viewbox(
Expand All @@ -63,9 +67,8 @@ def _map_font_space_to_viewbox(


def _svg_root(view_box: Rect) -> etree.Element:
svg_tree = etree.parse(
str(test_helper.locate_test_file("colr_to_svg_template.svg"))
)
with resources.path("nanoemoji.data", "colr_to_svg_template.svg") as template_file:
svg_tree = etree.parse(str(template_file))
svg_root = svg_tree.getroot()
vbox = (view_box.x, view_box.y, view_box.w, view_box.h)
svg_root.attrib["viewBox"] = " ".join(ntos(v) for v in vbox)
Expand Down Expand Up @@ -170,15 +173,13 @@ def _apply_gradient_ot_paint(
def _colr_v0_glyph_to_svg(
ttfont: ttLib.TTFont,
glyph_set: ttLib.ttFont._TTGlyphSet,
view_box: Rect,
view_box_callback: ViewboxCallback,
glyph_name: str,
) -> etree.Element:
view_box, font_to_vbox = _view_box_and_transform(
ttfont, view_box_callback, glyph_name
)
svg_root = _svg_root(view_box)
ascender = ttfont["OS/2"].sTypoAscender
descender = ttfont["OS/2"].sTypoDescender
width = ttfont["hmtx"][glyph_name][0]
font_to_vbox = _map_font_space_to_viewbox(view_box, ascender, descender, width)

for glyph_layer in ttfont["COLR"].ColorLayers[glyph_name]:
svg_path = etree.SubElement(svg_root, "path")
paint = PaintSolid(_color(ttfont, glyph_layer.colorID))
Expand Down Expand Up @@ -267,20 +268,35 @@ def descend(parent: etree.Element, paint: otTables.Paint):
raise NotImplementedError(ot_paint.Format)


def _view_box_and_transform(
ttfont: ttLib.TTFont, view_box_callback: ViewboxCallback, glyph_name: str
) -> Tuple[Rect, Affine2D]:
ascender = ttfont["OS/2"].sTypoAscender
descender = ttfont["OS/2"].sTypoDescender
width = ttfont["hmtx"][glyph_name][0]
# view_box = Rect(
# 0, 0, width * font_to_svg_scale, (ascender - descender) * font_to_svg_scale
# )
view_box = view_box_callback(glyph_name)
font_to_vbox = _map_font_space_to_viewbox(view_box, ascender, descender, width)

return (view_box, font_to_vbox)


def _colr_v1_glyph_to_svg(
ttfont: ttLib.TTFont,
glyph_set: ttLib.ttFont._TTGlyphSet,
view_box: Rect,
view_box_callback: ViewboxCallback,
glyph: otTables.BaseGlyphRecord,
) -> etree.Element:
glyph_set = ttfont.getGlyphSet()
view_box, font_to_vbox = _view_box_and_transform(
ttfont, view_box_callback, glyph.BaseGlyph
)
svg_root = _svg_root(view_box)
svg_defs = svg_root[0]
ascender = ttfont["OS/2"].sTypoAscender
descender = ttfont["OS/2"].sTypoDescender
width = ttfont["hmtx"][glyph.BaseGlyph][0]
font_to_vbox = _map_font_space_to_viewbox(view_box, ascender, descender, width)

reuse_cache = _new_reuse_cache()
glyph_set = ttfont.getGlyphSet()
_colr_v1_paint_to_svg(
ttfont, glyph_set, svg_root, svg_defs, font_to_vbox, glyph.Paint, reuse_cache
)
Expand All @@ -291,28 +307,46 @@ def _new_reuse_cache() -> ReuseCache:
return ReuseCache(0.1, GlyphReuseCache(0.1))


def _colr_v0_to_svgs(view_box: Rect, ttfont: ttLib.TTFont) -> Dict[str, SVG]:
def colr_glyphs(font: ttLib.TTFont) -> Iterable[int]:
colr = font["COLR"]
if colr.version == 0:
for glyph_name in colr.ColorLayers:
yield font.getGlyphID(glyph_name)
else:
for base_glyph in font["COLR"].table.BaseGlyphList:
yield font.getGlyphID(base_glyph.Glyph)


def _colr_v0_to_svgs(
view_box_callback: ViewboxCallback, ttfont: ttLib.TTFont
) -> Dict[str, SVG]:
glyph_set = ttfont.getGlyphSet()
return {
g: SVG.fromstring(
etree.tostring(_colr_v0_glyph_to_svg(ttfont, glyph_set, view_box, g))
etree.tostring(
_colr_v0_glyph_to_svg(ttfont, glyph_set, view_box_callback, g)
)
)
for g in ttfont["COLR"].ColorLayers
}


def _colr_v1_to_svgs(view_box: Rect, ttfont: ttLib.TTFont) -> Dict[str, SVG]:
def _colr_v1_to_svgs(
view_box_callback: ViewboxCallback, ttfont: ttLib.TTFont
) -> Dict[str, SVG]:
glyph_set = ttfont.getGlyphSet()
return {
g.BaseGlyph: SVG.fromstring(
etree.tostring(_colr_v1_glyph_to_svg(ttfont, glyph_set, view_box, g))
etree.tostring(
_colr_v1_glyph_to_svg(ttfont, glyph_set, view_box_callback, g)
)
)
for g in ttfont["COLR"].table.BaseGlyphList.BaseGlyphPaintRecord
}


def colr_to_svg(
view_box: Rect,
view_box_callback: ViewboxCallback,
ttfont: ttLib.TTFont,
rounding_ndigits: Optional[int] = None,
) -> Dict[str, SVG]:
Expand All @@ -321,9 +355,9 @@ def colr_to_svg(

colr_version = ttfont["COLR"].version
if colr_version == 0:
svgs = _colr_v0_to_svgs(view_box, ttfont)
svgs = _colr_v0_to_svgs(view_box_callback, ttfont)
elif colr_version == 1:
svgs = _colr_v1_to_svgs(view_box, ttfont)
svgs = _colr_v1_to_svgs(view_box_callback, ttfont)
else:
raise NotImplementedError(colr_version)

Expand Down
File renamed without changes
62 changes: 62 additions & 0 deletions src/nanoemoji/generate_svgs_from_colr.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
# Copyright 2022 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""Creates svg files from a COLR table."""
from absl import app
from absl import flags
from absl import logging
from fontTools import ttLib
from nanoemoji.colr_to_svg import colr_to_svg
from nanoemoji import util
from pathlib import Path
from picosvg.geometric_types import Rect


FLAGS = flags.FLAGS


flags.DEFINE_string("output_dir", None, "Output dir. Files written to <glyph id>.svg.")
flags.DEFINE_string(
"log_level",
"INFO",
"The threshold for what messages will be logged. One of DEBUG, INFO, WARN, "
"ERROR, or FATAL.",
)


def _view_box(font: ttLib.TTFont, glyph_name: str) -> Rect:
pass


def main(argv):
logging.set_verbosity(FLAGS.log_level)

font_file = util.only(lambda a: a.endswith(".ttf"), argv)
out_dir = Path(FLAGS.output_dir)
assert out_dir.is_dir(), f"{FLAGS.output_dir} is not a directory"

font = ttLib.TTFont(font_file)
assert "COLR" in font, f"No COLR table in {font_file}"
logging.debug("Writing svgs from %s to %s", font_file, out_dir)

for glyph_name, svg in colr_to_svg(lambda gn: _view_box(font, gn), font).items():
gid = font.getGlyphID(glyph_name)
dest_file = out_dir / f"{gid:05d}.svg"
with open(dest_file, "w") as f:
f.write(svg.tostring(pretty_print=True))


if __name__ == "__main__":
flags.mark_flag_as_required("output_dir")
app.run(main)
16 changes: 8 additions & 8 deletions src/nanoemoji/glue_together.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
from absl import logging
from fontTools.ttLib.tables import otTables as ot
from fontTools import ttLib
from nanoemoji.colr import paints_of_type
import os


Expand All @@ -34,14 +35,9 @@

def _copy_colr(target: ttLib.TTFont, donor: ttLib.TTFont):
# Copy all glyphs used by COLR over
_glyphs_to_copy = set()

def _collect_glyphs(paint):
if paint.Format == ot.PaintFormat.PaintGlyph:
_glyphs_to_copy.add(paint.Glyph)

for record in donor["COLR"].table.BaseGlyphList.BaseGlyphPaintRecord:
record.Paint.traverse(donor["COLR"].table, _collect_glyphs)
_glyphs_to_copy = {
p.Glyph for p in paints_of_type(donor, ot.PaintFormat.PaintGlyph)
}

for glyph_name in _glyphs_to_copy:
target["glyf"][glyph_name] = donor["glyf"][glyph_name]
Expand All @@ -62,9 +58,13 @@ def main(argv):
target = ttLib.TTFont(FLAGS.target_font)
donor = ttLib.TTFont(FLAGS.donor_font)

# TODO lookup, guess fn name, etc
if FLAGS.color_table == "COLR":
_copy_colr(target, donor)
else:
# TODO: SVG support
# Note that nanoemoji svg reorders glyphs to pack svgs nicely
# The merged font may need to update to the donors glyph order for this to work
raise ValueError(f"Unsupported color table '{FLAGS.color_table}'")

target.save(FLAGS.output_file)
Expand Down
Loading

0 comments on commit 12a82dd

Please sign in to comment.