Skip to content

Commit

Permalink
Narrow the scope
Browse files Browse the repository at this point in the history
  • Loading branch information
rsheeter committed Jun 10, 2022

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
1 parent 8373448 commit 6d34eda
Showing 40 changed files with 844 additions and 1,030 deletions.
22 changes: 18 additions & 4 deletions src/nanoemoji/color_glyph.py
Original file line number Diff line number Diff line change
@@ -69,17 +69,14 @@ def _scale_viewbox_to_font_metrics(
def map_viewbox_to_font_space(
view_box: Rect, ascender: int, descender: int, width: int, user_transform: Affine2D
) -> Affine2D:
# print("map_viewbox_to_font_space", view_box, ascender, descender, width, user_transform)
transform = Affine2D.compose_ltr(
return Affine2D.compose_ltr(
[
_scale_viewbox_to_font_metrics(view_box, ascender, descender, width),
# flip y axis and shift so things are in the right place
Affine2D(1, 0, 0, -1, 0, ascender),
user_transform,
]
)
# print("map_viewbox_to_font_space", transform)
return transform


# https://docs.microsoft.com/en-us/typography/opentype/spec/svg#coordinate-systems-and-glyph-metrics
@@ -475,6 +472,23 @@ def _has_viewbox_for_transform(self) -> bool:
)
return view_box is not None

def _transform(self, map_fn):
if not self._has_viewbox_for_transform():
return Affine2D.identity()
return map_fn(
self.svg.view_box(),
self.ufo.info.ascender,
self.ufo.info.descender,
self.ufo_glyph.width,
self.user_transform,
)

def transform_for_otsvg_space(self):
return self._transform(map_viewbox_to_otsvg_space)

def transform_for_font_space(self):
return self._transform(map_viewbox_to_font_space)

@property
def ufo_glyph(self) -> UfoGlyph:
return self.ufo[self.ufo_glyph_name]
75 changes: 19 additions & 56 deletions src/nanoemoji/colr_to_svg.py
Original file line number Diff line number Diff line change
@@ -31,7 +31,6 @@
is_transform,
_decompose_uniform_transform,
)
from nanoemoji.parts import ReusableParts
from nanoemoji.svg import (
_svg_matrix,
_apply_solid_paint,
@@ -183,12 +182,11 @@ def _apply_gradient_ot_paint(
def _colr_v0_glyph_to_svg(
ttfont: ttLib.TTFont,
glyph_set: ttLib.ttFont._TTGlyphSet,
shape_view_box_callback: ViewboxCallback,
dest_view_box_callback: ViewboxCallback,
view_box_callback: ViewboxCallback,
glyph_name: str,
) -> etree.Element:
view_box, font_to_vbox = _view_box_and_transform(
ttfont, shape_view_box_callback, dest_view_box_callback, glyph_name
ttfont, view_box_callback, glyph_name
)
svg_root = _svg_root(view_box)
for glyph_layer in ttfont["COLR"].ColorLayers[glyph_name]:
@@ -325,41 +323,28 @@ def glyph_region(ttfont: ttLib.TTFont, glyph_name: str) -> Rect:


def _view_box_and_transform(
ttfont: ttLib.TTFont,
shape_view_box_callback: ViewboxCallback,
dest_view_box_callback: ViewboxCallback,
glyph_name: str,
ttfont: ttLib.TTFont, view_box_callback: ViewboxCallback, glyph_name: str
) -> Tuple[Rect, Affine2D]:

dest_view_box = dest_view_box_callback(glyph_name)
assert dest_view_box.w > 0, f"0-width viewBox for {glyph_name}?!"
view_box = view_box_callback(glyph_name)
assert view_box.w > 0, f"0-width viewBox for {glyph_name}?!"

parts_view_box = shape_view_box_callback(glyph_name)
region = glyph_region(ttfont, glyph_name)
assert region.w > 0, f"0-width region for {glyph_name}?!"

width = ttfont["hmtx"][glyph_name][0]
if width == 0:
width = ttfont["glyf"][glyph_name].xMax
# TODO we lost user transform?
svg_units_to_font_units = color_glyph.map_viewbox_to_font_space(
parts_view_box,
ttfont["OS/2"].sTypoAscender,
ttfont["OS/2"].sTypoDescender,
width,
Affine2D.identity(),
)
font_to_vbox = map_font_space_to_viewbox(view_box, region)

return (dest_view_box, svg_units_to_font_units.inverse())
return (view_box, font_to_vbox)


def _colr_v1_glyph_to_svg(
ttfont: ttLib.TTFont,
glyph_set: ttLib.ttFont._TTGlyphSet,
shape_view_box_callback: ViewboxCallback,
view_box_callback: ViewboxCallback,
glyph: otTables.BaseGlyphRecord,
) -> etree.Element:
view_box, font_to_vbox = _view_box_and_transform(
ttfont, shape_view_box_callback, view_box_callback, glyph.BaseGlyph
ttfont, view_box_callback, glyph.BaseGlyph
)
svg_root = _svg_root(view_box)
svg_defs = svg_root[0]
@@ -373,7 +358,7 @@ def _colr_v1_glyph_to_svg(


def _new_reuse_cache() -> ReuseCache:
return ReuseCache(GlyphReuseCache(ReusableParts(reuse_tolerance=0.1)))
return ReuseCache(0.1, GlyphReuseCache(0.1))


def colr_glyphs(font: ttLib.TTFont) -> Iterable[int]:
@@ -389,68 +374,46 @@ def colr_glyphs(font: ttLib.TTFont) -> Iterable[int]:


def _colr_v0_to_svgs(
shape_view_box_callback: ViewboxCallback,
dest_view_box_callback: ViewboxCallback,
ttfont: ttLib.TTFont,
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,
shape_view_box_callback,
dest_view_box_callback,
g,
)
_colr_v0_glyph_to_svg(ttfont, glyph_set, view_box_callback, g)
)
)
for g in ttfont["COLR"].ColorLayers
}


def _colr_v1_to_svgs(
shape_view_box_callback: ViewboxCallback,
dest_view_box_callback: ViewboxCallback,
ttfont: ttLib.TTFont,
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,
shape_view_box_callback,
dest_view_box_callback,
g,
)
_colr_v1_glyph_to_svg(ttfont, glyph_set, view_box_callback, g)
)
)
for g in ttfont["COLR"].table.BaseGlyphList.BaseGlyphPaintRecord
}


def colr_to_svg(
shape_view_box_callback: ViewboxCallback,
dest_view_box_callback: ViewboxCallback,
view_box_callback: ViewboxCallback,
ttfont: ttLib.TTFont,
rounding_ndigits: Optional[int] = None,
) -> Dict[str, SVG]:
"""
Creates a glyph name => SVG dict from a COLR table.
shape_view_box_callback: function to get the space in which shapes for a glyph were defined, such as the parts view box.
dest_view_box_callback: function to get the view box of the destination
"""
"""For testing only, don't use for real!"""
assert len(ttfont["CPAL"].palettes) == 1, "We assume one palette"

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

2 changes: 1 addition & 1 deletion src/nanoemoji/config.py
Original file line number Diff line number Diff line change
@@ -158,7 +158,7 @@ class FontConfig(NamedTuple):
transform: Affine2D = Affine2D.identity()
version_major: int = 1
version_minor: int = 0
reuse_tolerance: float = 0.05
reuse_tolerance: float = 0.1
ignore_reuse_error: bool = True
keep_glyph_names: bool = False
clip_to_viewbox: bool = True
1 change: 1 addition & 0 deletions src/nanoemoji/extract_svgs_from_otsvg.py
Original file line number Diff line number Diff line change
@@ -19,6 +19,7 @@
from fontTools import ttLib
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
4 changes: 1 addition & 3 deletions src/nanoemoji/generate_svgs_from_colr.py
Original file line number Diff line number Diff line change
@@ -53,9 +53,7 @@ def main(argv):
assert "COLR" in font, f"No COLR table in {font_file}"
logging.debug("Writing svgs from %s to %s", font_file, out_dir)

region_callback = lambda gn: _view_box(font, gn)

for glyph_name, svg in colr_to_svg(region_callback, region_callback, font).items():
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:
127 changes: 53 additions & 74 deletions src/nanoemoji/glyph_reuse.py
Original file line number Diff line number Diff line change
@@ -16,97 +16,76 @@


from absl import logging
import dataclasses
from nanoemoji import parts
from nanoemoji.parts import ReuseResult, ReusableParts
from picosvg.geometric_types import Rect
from picosvg.svg_reuse import normalize, affine_between
from picosvg.svg_transform import Affine2D
from picosvg.svg_types import SVGPath
from typing import (
MutableMapping,
NamedTuple,
Optional,
Set,
)
from .fixed import fixed_safe


@dataclasses.dataclass
class ReuseResult(NamedTuple):
glyph_name: str
transform: Affine2D


class GlyphReuseCache:
_reusable_parts: ReusableParts
_shape_to_glyph: MutableMapping[parts.Shape, str] = dataclasses.field(
default_factory=dict
)
_glyph_to_shape: MutableMapping[str, parts.Shape] = dataclasses.field(
default_factory=dict
)

def try_reuse(self, path: str, path_view_box: Rect) -> ReuseResult:
assert path[0].upper() == "M", path

path = SVGPath(d=path)
if path_view_box != self._reusable_parts.view_box:
print(path, path_view_box, self._reusable_parts.view_box)
path = path.apply_transform(
Affine2D.rect_to_rect(path_view_box, self._reusable_parts.view_box)
)
def __init__(self, reuse_tolerance: float):
self._reuse_tolerance = reuse_tolerance
self._known_glyphs = set()
self._reusable_paths = {}

maybe_reuse = self._reusable_parts.try_reuse(path)
# normalize tries to remap first two significant vectors to [1 0], [0 1]
# reuse tolerence is relative to viewbox, which is typically much larger
# than the space normalize operates in. TODO: better default.
self._normalize_tolerance = self._reuse_tolerance / 10

# https://github.com/googlefonts/nanoemoji/issues/313 avoid out of bounds affines
if maybe_reuse is not None and not fixed_safe(*maybe_reuse.transform):
logging.warning(
"affine_between overflows Fixed: %s %s, %s",
path,
maybe_reuse.shape,
maybe_reuse.transform,
)
maybe_reuse = None
if maybe_reuse is None:
maybe_reuse = ReuseResult(Affine2D.identity(), parts.as_shape(path))
return maybe_reuse

def set_glyph_for_path(self, glyph_name: str, path: str):
norm = self._reusable_parts.normalize(path)
assert norm in self._reusable_parts.shape_sets, f"No shape set for {path}"
shape = parts.as_shape(SVGPath(d=path))
assert (
shape in self._reusable_parts.shape_sets[norm]
), f"Not present in shape set: {path}"
def try_reuse(self, path: str) -> Optional[ReuseResult]:
"""Try to reproduce path as the transformation of another glyph.
Path is expected to be in font units.
if self._shape_to_glyph.get(shape, glyph_name) != glyph_name:
raise ValueError(f"{shape} cannot be associated with glyphs")
if self._glyph_to_shape.get(glyph_name, shape) != shape:
raise ValueError(f"{glyph_name} cannot be associated with multiple shapes")
Returns (glyph name, transform) if possible, None if not.
"""
assert (
not path in self._known_glyphs
), f"{path} isn't a path, it's a glyph name we've seen before"
assert path.startswith("M"), f"{path} doesn't look like a path"

self._shape_to_glyph[shape] = glyph_name
self._glyph_to_shape[glyph_name] = shape
if self._reuse_tolerance == -1:
return None

def get_glyph_for_path(self, path: str) -> str:
return self._shape_to_glyph[parts.as_shape(SVGPath(d=path))]
norm_path = normalize(SVGPath(d=path), self._normalize_tolerance).d
if norm_path not in self._reusable_paths:
return None

def forget_glyph_path_associations(self):
self._shape_to_glyph.clear()
self._glyph_to_shape.clear()
glyph_name, glyph_path = self._reusable_paths[norm_path]
affine = affine_between(
SVGPath(d=glyph_path), SVGPath(d=path), self._reuse_tolerance
)
if affine is None:
logging.warning("affine_between failed: %s %s ", glyph_path, path)
return None

def consuming_glyphs(self, path: str) -> Set[str]:
norm = self._reusable_parts.normalize(path)
assert (
norm in self._reusable_parts.shape_sets
), f"{path} not associated with any parts!"
return {
self._shape_to_glyph[shape]
for shape in self._reusable_parts.shape_sets[norm]
}
# https://github.com/googlefonts/nanoemoji/issues/313 avoid out of bounds affines
if not fixed_safe(*affine):
logging.warning(
"affine_between overflows Fixed: %s %s, %s", glyph_path, path, affine
)
return None

def is_known_glyph(self, glyph_name: str):
return glyph_name in self._glyph_to_shape
return ReuseResult(glyph_name, affine)

def is_known_path(self, path: str):
return parts.as_shape(SVGPath(d=path)) in self._shape_to_glyph
def add_glyph(self, glyph_name, glyph_path):
assert glyph_path.startswith("M"), f"{glyph_path} doesn't look like a path"
if self._reuse_tolerance != -1:
norm_path = normalize(SVGPath(d=glyph_path), self._normalize_tolerance).d
else:
norm_path = glyph_path
self._reusable_paths[norm_path] = (glyph_name, glyph_path)
self._known_glyphs.add(glyph_name)

def view_box(self) -> Rect:
"""
The box within which the shapes in this cache exist.
"""
return self._reusable_parts.view_box
def is_known_glyph(self, glyph_name):
return glyph_name in self._known_glyphs
Loading

0 comments on commit 6d34eda

Please sign in to comment.