Skip to content

Commit

Permalink
Ninja runner for addition of COLR to ot-svg, svg glyph reduction.
Browse files Browse the repository at this point in the history
  • Loading branch information
rsheeter committed Mar 1, 2022
1 parent d7b7d70 commit dd041ba
Show file tree
Hide file tree
Showing 14 changed files with 612 additions and 147 deletions.
49 changes: 49 additions & 0 deletions src/nanoemoji/extract_svgs.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# 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 extracting svg files from the SVG table."""


import copy
from fontTools import ttLib
from picosvg.svg import SVG
from picosvg.svg_meta import strip_ns
from typing import Iterable, Tuple


def _remove_glyph_elements(svg: SVG, gids_to_remove: Iterable[int]) -> SVG:
"""Strip out unwanted glyph roots.
We do NOT try to strip unused shared content; that is left for others,
e.g. picosvg.
"""
svg = copy.deepcopy(svg)
for gid in gids_to_remove:
results = svg.xpath(f"svg:*[@id='glyph{gid}']")
for result in results:
parent = result.getparent()
if parent is not None:
parent.remove(result)
return svg


def svg_glyphs(font: ttLib.TTFont) -> Iterable[Tuple[int, SVG]]:
for raw_svg, min_gid, max_gid in font["SVG "].docList:
gids = set(range(min_gid, max_gid + 1))
svg = SVG.fromstring(raw_svg)
for gid in gids:
svg_for_gid = svg
if len(gids) > 1:
svg_for_gid = _remove_glyph_elements(svg_for_gid, gids - {gid})
yield (gid, svg_for_gid)
19 changes: 9 additions & 10 deletions src/nanoemoji/extract_svgs_from_otsvg.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
from lxml import etree
from nanoemoji import codepoints
from nanoemoji.color_glyph import map_viewbox_to_otsvg_space
from nanoemoji.extract_svgs import svg_glyphs
from nanoemoji import util
import os
import shutil
Expand Down Expand Up @@ -62,8 +63,7 @@ def main(argv):
# So use the font height (global) and width (per glyph) as the svg viewbox
height = ascender - descender

for raw_svg, min_gid, max_gid in font["SVG "].docList:
svg = SVG.fromstring(raw_svg)
for gid, svg in svg_glyphs(font):
svg_defs = etree.Element("defs")
svg_g = etree.Element("g")
svg_g.attrib["transform"] = f"translate(0, {ascender})"
Expand All @@ -77,14 +77,13 @@ def main(argv):
svg.svg_root.append(svg_defs)
svg.svg_root.append(svg_g)

for gid in range(min_gid, max_gid + 1):
glyph_name = font.getGlyphName(gid)
width, _ = metrics[glyph_name]
svg.svg_root.attrib["viewBox"] = f"0 0 {width} {height}"
dest_file = out_dir / f"{gid}.svg"
with open(dest_file, "w") as f:
f.write(svg.tostring(pretty_print=True))
logging.debug("Wrote %s", dest_file)
glyph_name = font.getGlyphName(gid)
width, _ = metrics[glyph_name]
svg.svg_root.attrib["viewBox"] = f"0 0 {width} {height}"
dest_file = out_dir / f"{gid}.svg"
with open(dest_file, "w") as f:
f.write(svg.tostring(pretty_print=True))
logging.debug("Wrote %s", dest_file)


if __name__ == "__main__":
Expand Down
1 change: 0 additions & 1 deletion src/nanoemoji/glue_together.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,6 @@ def _collect_glyphs(paint):
record.Paint.traverse(donor["COLR"].table, _collect_glyphs)

for glyph_name in _glyphs_to_copy:
print("Copy glyph", glyph_name)
target["glyf"].glyphs[glyph_name] = donor["glyf"].glyphs[glyph_name]
target["hmtx"].metrics[glyph_name] = donor["hmtx"].metrics[glyph_name]

Expand Down
179 changes: 179 additions & 0 deletions src/nanoemoji/maximum_color.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
# 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.

"""Generates color tables to provide support for a wide range of browsers.
Requires input font have vector color capability, that is either an SVG table
or COLR/CPAL.
https://www.youtube.com/watch?v=HLddvNiXym4
Sample usage:
python -m nanoemoji.maximum_color MySvgFont.ttf"""
from absl import app
from absl import logging
from fontTools import ttLib
from nanoemoji.extract_svgs import svg_glyphs
from nanoemoji.ninja import (
build_dir,
gen_ninja,
maybe_run_ninja,
module_rule,
rel_build,
NinjaWriter,
)
from nanoemoji.util import only
from pathlib import Path


_SVG2COLR_GLYPHMAP = "svg2colr.glyphmap"
_SVG2COLR_CONFIG = "svg2colr.toml"


def _vector_color_table(font: ttLib.TTFont) -> str:
has_svg = "SVG " in font
has_colr = "COLR" in font
if has_svg == has_colr:
raise ValueError("Must have one of COLR, SVG")

if has_svg:
return "SVG "
if has_colr:
return "COLR"

raise ValueError("Impossible")


def svg_extract_dir() -> Path:
return build_dir() / "svg_dump"


def picosvg_dir() -> Path:
return build_dir() / "picosvg"


def picosvg_dest(input_svg: Path) -> Path:
return picosvg_dir() / input_svg.name


def _write_preamble(nw: NinjaWriter, input_font: Path):
module_rule(
nw,
"extract_svgs_from_otsvg",
f"--output_dir {rel_build(svg_extract_dir())} $in $out",
)
nw.newline()

module_rule(nw, "write_glyphmap_for_glyph_svgs", "$in > $out")
nw.newline()

module_rule(nw, "write_config_for_glyph_svgs", "$in $out")
nw.newline()

module_rule(
nw,
"write_font",
f"--glyphmap_file {_SVG2COLR_GLYPHMAP} --config_file {_SVG2COLR_CONFIG} --output_file $out",
rule_name="write_colr_font_from_svg_dump",
)
nw.newline()

nw.rule(
f"picosvg",
f"picosvg --output_file $out $in",
)
nw.newline()

module_rule(
nw,
"glue_together",
f"--color_table COLR --target_font {rel_build(input_font)} --donor_font $in --output_file $out",
rule_name="copy_colr_from_svg2colr",
)
nw.newline()


def _write_svg_extract(nw: NinjaWriter, input_font: Path, font: ttLib.TTFont):
# extract the svgs
svg_extracts = [
rel_build(svg_extract_dir() / f"{gid}.svg") for gid, _ in svg_glyphs(font)
]
nw.build(svg_extracts, "extract_svgs_from_otsvg", input_font)
nw.newline()

# picosvg them
picosvgs = [rel_build(picosvg_dest(s)) for s in svg_extracts]
for svg_extract, picosvg in zip(svg_extracts, picosvgs):
nw.build(picosvg, "picosvg", svg_extract)
nw.newline()

# make a glyphmap
nw.build(
_SVG2COLR_GLYPHMAP, "write_glyphmap_for_glyph_svgs", picosvgs + [input_font]
)
nw.newline()

# make a config
nw.build(_SVG2COLR_CONFIG, "write_config_for_glyph_svgs", input_font)
nw.newline()

# generate a new font with COLR glyphs that use the same names as the original
nw.build(
"colr_from_svg.ttf",
"write_colr_font_from_svg_dump",
[_SVG2COLR_GLYPHMAP, _SVG2COLR_CONFIG],
)
nw.newline()

# stick our shiny new COLR table onto the input font and declare victory
nw.build(
input_font.name,
"copy_colr_from_svg2colr",
"colr_from_svg.ttf",
)
nw.newline()


def main(argv):
if len(argv) != 2:
raise ValueError("Must have one argument, a font file")

input_font = Path(argv[1])
assert input_font.is_file()
font = ttLib.TTFont(input_font)

build_file = build_dir() / "build.ninja"
build_dir().mkdir(parents=True, exist_ok=True)

print(build_dir()) # TEMPORARY

color_table = _vector_color_table(font)

if gen_ninja():
logging.info(f"Generating {build_file.relative_to(build_dir())}")
with open(build_file, "w") as f:
nw = NinjaWriter(f)
_write_preamble(nw, input_font)

if color_table == "COLR":
raise NotImplementedError("Conversion from COLR coming soon")
else:
_write_svg_extract(nw, input_font, font)

maybe_run_ninja(build_file)


if __name__ == "__main__":
app.run(main)
Loading

0 comments on commit dd041ba

Please sign in to comment.