diff --git a/src/nanoemoji/color_glyph.py b/src/nanoemoji/color_glyph.py
index 05c2d399..6dec3c54 100644
--- a/src/nanoemoji/color_glyph.py
+++ b/src/nanoemoji/color_glyph.py
@@ -475,29 +475,6 @@ 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):
- print()
- print(
- self.svg_filename,
- self.svg.view_box(),
- self._transform(map_viewbox_to_font_space),
- )
- return self._transform(map_viewbox_to_font_space)
-
@property
def ufo_glyph(self) -> UfoGlyph:
return self.ufo[self.ufo_glyph_name]
diff --git a/src/nanoemoji/glyph_reuse.py b/src/nanoemoji/glyph_reuse.py
index cfb1b1f9..df748c93 100644
--- a/src/nanoemoji/glyph_reuse.py
+++ b/src/nanoemoji/glyph_reuse.py
@@ -45,7 +45,6 @@ def try_reuse(self, path: str, path_view_box: Rect) -> ReuseResult:
assert path[0].upper() == "M", path
path = SVGPath(d=path)
- path_view_box = ReusableParts.view_box_for((path_view_box,))
if path_view_box != self._reusable_parts.view_box:
print(path, path_view_box, self._reusable_parts.view_box)
path = path.apply_transform(
diff --git a/src/nanoemoji/nanoemoji.py b/src/nanoemoji/nanoemoji.py
index 1292ee89..12cd88e3 100644
--- a/src/nanoemoji/nanoemoji.py
+++ b/src/nanoemoji/nanoemoji.py
@@ -261,7 +261,7 @@ def write_preamble(nw):
module_rule(
nw,
"write_part_file",
- f"--reuse_tolerance $reuse_tolerance --output_file $out $in",
+ f"--reuse_tolerance $reuse_tolerance --upem $upem --output_file $out $in",
)
nw.newline()
@@ -360,19 +360,18 @@ def master_part_file_dest() -> Path:
def write_picosvg_builds(
picosvg_builds: Set[Path],
nw: NinjaWriter,
- clipped: bool,
- reuse_tolerance: float,
+ font_config: FontConfig,
master: MasterConfig,
) -> Tuple[Set[Path], Set[Path]]:
rule_name = "picosvg_unclipped"
- if clipped:
+ if font_config.clip_to_viewbox:
rule_name = "picosvg_clipped"
picosvgs = set()
part_files = set()
for svg_file in master.sources:
svg_file = abspath(svg_file)
- dest = picosvg_dest(clipped, svg_file)
+ dest = picosvg_dest(font_config.clip_to_viewbox, svg_file)
if svg_file in picosvg_builds:
continue
picosvg_builds.add(svg_file)
@@ -383,7 +382,7 @@ def write_picosvg_builds(
part_dest,
"write_part_file",
dest,
- variables={"reuse_tolerance": reuse_tolerance},
+ variables={"reuse_tolerance": font_config.reuse_tolerance, "upem": font_config.upem},
)
picosvgs.add(dest)
@@ -667,8 +666,7 @@ def _run(argv):
_, parts = write_picosvg_builds(
picosvg_builds,
nw,
- font_config.clip_to_viewbox,
- font_config.reuse_tolerance,
+ font_config,
master,
)
part_files |= parts
diff --git a/src/nanoemoji/parts.py b/src/nanoemoji/parts.py
index 5f0e3a0b..f78b5612 100644
--- a/src/nanoemoji/parts.py
+++ b/src/nanoemoji/parts.py
@@ -14,15 +14,20 @@
"""A cache of reusable parts, esp paths, for whatever purpose you see fit.
Intended to be used as a building block for glyph reuse.
+
+We always apply nop transforms to ensure any command type flips, such as arcs
+to cubics, occur. This ensures that if we merge a part file with no transform
+with one that has transformation the command types still align.
"""
import dataclasses
-from functools import lru_cache
+from functools import lru_cache, partial, reduce
import json
from nanoemoji.config import FontConfig
from pathlib import Path
from picosvg.geometric_types import Rect
from picosvg.svg import SVG
+from picosvg.svg_meta import cmd_coords
from picosvg.svg_reuse import affine_between, normalize
from picosvg.svg_transform import Affine2D
from picosvg.svg_types import SVGPath, SVGShape
@@ -86,15 +91,17 @@ def _bbox_area(shape: Shape) -> float:
return bbox.w * bbox.h
-def _round(path: SVGShape) -> SVGPath:
- return path.as_path().round_floats(_DEFAULT_ROUND_NDIGITS)
+def _round(shape: SVGShape) -> SVGPath:
+ return shape.as_path().round_floats(_DEFAULT_ROUND_NDIGITS)
-def as_shape(shape: SVGPath) -> Shape:
- return Shape(_round(shape).d)
+def as_shape(path: SVGPath) -> Shape:
+ # apply a nop transform because some things still change, like arcs to cubics
+ path = path.apply_transform(Affine2D.identity())
+ return Shape(_round(path).d)
-# TODO: create a parts builder and a frozen parts to more explicitly model the add/use cycle
+# TODO: create a parts builder and a frozen parts from compute_donors() to more explicitly model the add/use cycle
@dataclasses.dataclass
@@ -112,7 +119,13 @@ class ReusableParts:
def normalize(self, path: str) -> NormalizedShape:
if self.reuse_tolerance != -1:
# normalize handles it's own rounding
- norm = NormalizedShape(normalize(SVGPath(d=path), self.reuse_tolerance).d)
+ # apply a nop transform because some things still change, like arcs to cubics
+ norm = NormalizedShape(
+ normalize(
+ SVGPath(d=path).apply_transform(Affine2D.identity()),
+ self.reuse_tolerance,
+ ).d
+ )
else:
norm = NormalizedShape(path)
return norm
@@ -131,24 +144,23 @@ def add(self, source: PathSource):
"""Combine two sets of parts. Source shapes will be scaled to dest viewbox."""
if isinstance(source, ReusableParts):
transform = Affine2D.rect_to_rect(source.view_box, self.view_box)
- for normalized, shape_set in source.shape_sets.items():
- for shape in shape_set:
- if transform != Affine2D.identity():
- shape = as_shape(SVGPath(d=shape).apply_transform(transform))
- self._add_norm_path(normalized, shape)
+ shapes = tuple(
+ reduce(lambda a, c: a | c, source.shape_sets.values(), set())
+ )
elif isinstance(source, SVG):
source.checkpicosvg()
- transform = Affine2D.rect_to_rect(
- ReusableParts.view_box_for((source.view_box(),)), self.view_box
- )
- for svg_shape in source.shapes():
- svg_shape = svg_shape.as_path()
- if transform != Affine2D.identity():
- svg_shape = svg_shape.apply_transform(transform)
- self._add(as_shape(svg_shape))
+ transform = Affine2D.rect_to_rect(source.view_box(), self.view_box)
+ shapes = tuple(s.as_path() for s in source.shapes())
else:
raise ValueError(f"Unknown part source: {type(source)}")
+ for shape in shapes:
+ if isinstance(shape, str):
+ shape = SVGPath(d=shape)
+ if transform != Affine2D.identity():
+ shape = shape.apply_transform(transform)
+ self._add(as_shape(shape))
+
def _compute_donor(self, norm: NormalizedShape):
self._donor_cache[norm] = None # no solution
@@ -157,7 +169,7 @@ def _compute_donor(self, norm: NormalizedShape):
# shrinking a big thing is more likely to result in small #s that fit into
# more compact PaintTransform variants so try biggest first
- # TODO there are cases where this picks a suboptimal transform, e.g. a 2x3
+ # NOTE there are cases where this picks a suboptimal transform, e.g. a 2x3
# downscale be used when a scale uniform around center upscale might work
# Ex SVGPath(d="M8,13 A3 3 0 1 1 2,13 A3 3 0 1 1 8,13 Z")
# SVGPath(d="M11,5 A2 2 0 1 1 7,5 A2 2 0 1 1 11,5 Z")
@@ -249,52 +261,38 @@ def to_json(self):
return json.dumps(json_dict, indent=2)
@classmethod
- def fromstring(cls, string) -> "ReusableParts":
- first = string.strip()[0]
- parts = cls()
- if first == "<":
- svg = SVG.fromstring(string).topicosvg()
- parts.view_box = ReusableParts.view_box_for((svg.view_box(),))
- for svg_shape in svg.shapes():
- parts._add(as_shape(svg_shape))
- elif first == "{":
- json_dict = json.loads(string)
- parts.version = tuple(int(v) for v in json_dict.pop("version").split("."))
- assert parts.version == (1, 0, 0), f"Bad version {parts.version}"
- parts.view_box = Rect(
- *(int(v) for v in json_dict.pop("view_box").split(" "))
- )
- assert parts.view_box[:2] == (
- 0,
- 0,
- ), f"Must be a viewbox from 0,0 {parts.view_box}"
- parts.reuse_tolerance = float(json_dict.pop("reuse_tolerance"))
- for shape_set_json in json_dict.pop("shape_sets"):
- norm = NormalizedShape(shape_set_json.pop("normalized"))
- shapes = ShapeSet({Shape(s) for s in shape_set_json.pop("shapes")})
- donor = shape_set_json.pop("donor")
- if donor and donor not in shapes:
- raise ValueError("Donor must be in group")
- if shape_set_json:
- raise ValueError(f"Unconsumed input {shape_set_json}")
- parts.shape_sets[norm] = shapes
- if donor != "":
- parts._donor_cache[norm] = donor
- if json_dict:
- raise ValueError(f"Unconsumed input {json_dict}")
-
- else:
- raise ValueError(f"Unrecognized start sequence {string[:16]}")
+ def from_json(cls, string: str) -> "ReusableParts":
+ json_dict = json.loads(string)
+ parts = ReusableParts()
+ parts.version = tuple(int(v) for v in json_dict.pop("version").split("."))
+ assert parts.version == (1, 0, 0), f"Bad version {parts.version}"
+ parts.view_box = Rect(
+ *(int(v) for v in json_dict.pop("view_box").split(" "))
+ )
+ assert parts.view_box[:2] == (
+ 0,
+ 0,
+ ), f"Must be a viewbox from 0,0 {parts.view_box}"
+ parts.reuse_tolerance = float(json_dict.pop("reuse_tolerance"))
+ for shape_set_json in json_dict.pop("shape_sets"):
+ norm = NormalizedShape(shape_set_json.pop("normalized"))
+ shapes = ShapeSet({Shape(s) for s in shape_set_json.pop("shapes")})
+ donor = shape_set_json.pop("donor")
+ if donor and donor not in shapes:
+ raise ValueError("Donor must be in group")
+ if shape_set_json:
+ raise ValueError(f"Unconsumed input {shape_set_json}")
+ parts.shape_sets[norm] = shapes
+ if donor != "":
+ parts._donor_cache[norm] = donor
+ if json_dict:
+ raise ValueError(f"Unconsumed input {json_dict}")
return parts
@classmethod
- def load(cls, input_file: Path) -> "ReusableParts":
+ def loadjson(cls, input_file: Path) -> "ReusableParts":
ext = input_file.suffix.lower()
- if ext not in {".svg", ".json"}:
+ if ext != ".json":
raise ValueError(f"Unknown format {input_file}")
- return cls.fromstring(input_file.read_text(encoding="utf-8"))
+ return cls.from_json(input_file.read_text(encoding="utf-8"))
- @staticmethod
- def view_box_for(view_boxes: Iterable[Rect]) -> Rect:
- max_h = max((v.h for v in view_boxes))
- return Rect(0, 0, max_h, max_h)
diff --git a/src/nanoemoji/svg.py b/src/nanoemoji/svg.py
index 6dd0001e..52a36551 100644
--- a/src/nanoemoji/svg.py
+++ b/src/nanoemoji/svg.py
@@ -21,7 +21,7 @@
from functools import reduce
from lxml import etree # pytype: disable=import-error
from nanoemoji.colors import Color
-from nanoemoji.color_glyph import ColorGlyph
+from nanoemoji.color_glyph import map_viewbox_to_otsvg_space, ColorGlyph
from nanoemoji.config import FontConfig
from nanoemoji.disjoint_set import DisjointSet
from nanoemoji.glyph_reuse import GlyphReuseCache
@@ -457,7 +457,27 @@ def _create_use_element(
return svg_use
-def _add_glyph(svg: SVG, color_glyph: ColorGlyph, reuse_cache: ReuseCache):
+def _font_units_to_svg_units(view_box: Rect, config: FontConfig, glyph_width: int) -> Affine2D:
+ return map_viewbox_to_otsvg_space(
+ view_box,
+ config.ascender,
+ config.descender,
+ glyph_width,
+ config.transform,
+ )
+
+
+def _svg_units_to_font_units(view_box: Rect, config: FontConfig, glyph_width: int) -> Affine2D:
+ return map_viewbox_to_otsvg_space(
+ view_box,
+ config.ascender,
+ config.descender,
+ glyph_width,
+ config.transform,
+ )
+
+
+def _add_glyph(config: FontConfig, svg: SVG, color_glyph: ColorGlyph, reuse_cache: ReuseCache):
svg_defs = svg.xpath_one("//svg:defs")
# each glyph gets a group of its very own
@@ -469,11 +489,11 @@ def _add_glyph(svg: SVG, color_glyph: ColorGlyph, reuse_cache: ReuseCache):
raise ValueError(f"{color_glyph.svg_filename} must declare view box")
# https://github.com/googlefonts/nanoemoji/issues/58: group needs transform
- transform = color_glyph.transform_for_otsvg_space()
+ transform = _font_units_to_svg_units(reuse_cache.glyph_cache.view_box(), config, color_glyph.ufo_glyph.width)
if not transform.almost_equals(Affine2D.identity()):
svg_g.attrib["transform"] = _svg_matrix(transform)
- vbox_to_upem = color_glyph.transform_for_font_space()
+ vbox_to_upem = _svg_units_to_font_units(reuse_cache.glyph_cache.view_box(), config, color_glyph.ufo_glyph.width)
upem_to_vbox = vbox_to_upem.inverse()
# copy the shapes into our svg
@@ -726,7 +746,7 @@ def _picosvg_docs(
for color_glyph in (color_glyphs[g] for g in group):
if color_glyph.painted_layers:
- _add_glyph(svg, color_glyph, reuse_cache)
+ _add_glyph(config, svg, color_glyph, reuse_cache)
# tidy use elements, they may emerge from _add_glyph with unnecessary attributes
_tidy_use_elements(svg)
@@ -767,7 +787,7 @@ def _rawsvg_docs(
# Map gid => svg doc
"id": f"glyph{color_glyph.glyph_id}",
# map viewBox to OT-SVG space (+x,-y)
- "transform": _svg_matrix(color_glyph.transform_for_otsvg_space()),
+ "transform": _svg_matrix(_font_units_to_svg_units(color_glyph.svg.view_box(), config, color_glyph.ufo_glyph.width)),
},
)
# move all the elements under the new group
diff --git a/src/nanoemoji/write_combined_part_files.py b/src/nanoemoji/write_combined_part_files.py
index b20917ac..76b83882 100644
--- a/src/nanoemoji/write_combined_part_files.py
+++ b/src/nanoemoji/write_combined_part_files.py
@@ -29,14 +29,14 @@ def main(argv):
input_files = util.expand_ninja_response_files(argv[1:])
combined_parts = ReusableParts()
- individual_parts = [ReusableParts.load(Path(p)) for p in input_files]
+ individual_parts = [ReusableParts.loadjson(Path(p)) for p in input_files]
if individual_parts:
combined_parts.version = util.only({p.version for p in individual_parts})
combined_parts.reuse_tolerance = util.only(
{p.reuse_tolerance for p in individual_parts}
)
- combined_parts.view_box = ReusableParts.view_box_for(
- (p.view_box for p in individual_parts)
+ combined_parts.view_box = util.only(
+ {p.view_box for p in individual_parts}
)
for parts in individual_parts:
diff --git a/src/nanoemoji/write_font.py b/src/nanoemoji/write_font.py
index e18a61e0..af76b54d 100644
--- a/src/nanoemoji/write_font.py
+++ b/src/nanoemoji/write_font.py
@@ -272,6 +272,10 @@ def _create_glyph(color_glyph: ColorGlyph, path_in_font_space: str) -> Glyph:
ufo = color_glyph.ufo
draw_svg_path(SVGPath(d=path_in_font_space), glyph.getPen())
ufo.glyphOrder += [glyph.name]
+ print(glyph)
+ for contour in glyph.contours:
+ for i, pt in enumerate(contour.points):
+ print(" ", f"{i}:", pt)
return glyph
@@ -289,19 +293,23 @@ def _svg_transform_in_font_space(
)
+def _svg_units_to_font_units(glyph_cache: GlyphReuseCache, config: FontConfig, glyph_width: int) -> Affine2D:
+ return map_viewbox_to_font_space(
+ glyph_cache.view_box(),
+ config.ascender,
+ config.descender,
+ glyph_width,
+ config.transform,
+ )
+
+
def _create_glyph_for_svg_path(
config: FontConfig,
color_glyph: ColorGlyph,
glyph_cache: GlyphReuseCache,
path_in_svg_space: str,
) -> Glyph:
- svg_units_to_font_units = map_viewbox_to_font_space(
- glyph_cache.view_box(),
- config.ascender,
- config.descender,
- color_glyph.ufo_glyph.width,
- config.transform,
- )
+ svg_units_to_font_units = _svg_units_to_font_units(glyph_cache, config, color_glyph.ufo_glyph.width)
path_in_font_space = (
SVGPath(d=path_in_svg_space).apply_transform(svg_units_to_font_units).d
)
@@ -368,6 +376,8 @@ def _create_glyphs_for_svg_paths(
assert paint.glyph.startswith("M"), f"{paint.glyph} doesn't look like a path"
path_in_svg_space = paint.glyph
+ print("_create_glyphs_for_svg_paths")
+
maybe_reuse = glyph_cache.try_reuse(path_in_svg_space, color_glyph.svg.view_box())
# if we have a glyph for the target already, use that
@@ -378,7 +388,7 @@ def _create_glyphs_for_svg_paths(
else:
# TODO: when is it more compact to use a new transforming glyph?
# otherwise, create a glyph for the target and use it
- print("create glyph for", maybe_reuse.shape)
+ print(" ", "create glyph for", maybe_reuse.shape)
glyph = _create_glyph_for_svg_path(
config, color_glyph, glyph_cache, maybe_reuse.shape
)
@@ -387,10 +397,14 @@ def _create_glyphs_for_svg_paths(
if not maybe_reuse.transform.almost_equals(Affine2D.identity()):
# TODO: when is it more compact to use a new transforming glyph?
- svg_units_to_font_units = color_glyph.transform_for_font_space()
+ svg_units_to_font_units = _svg_units_to_font_units(glyph_cache, config, color_glyph.ufo_glyph.width)
reuse_transform = _svg_transform_in_font_space(
svg_units_to_font_units, maybe_reuse.transform
)
+
+ print(" ", "reuse, maybe_reuse.transform", maybe_reuse.transform)
+ print(" ", "reuse, svg_units_to_font_units", svg_units_to_font_units)
+ print(" ", "reuse, reuse_transform", reuse_transform)
# assert fixed_safe(*reuse_transform), f"{color_glyph.svg_filename} {color_glyph.ufo_glyph_name} fixed unsafe {reuse_transform} to reuse {maybe_reuse.shape} for {path_in_svg_space}"
# might need to adjust a gradient
@@ -468,7 +482,6 @@ def _glyf_ufo(
"%s %s %s",
ufo.info.familyName,
color_glyph.ufo_glyph_name,
- color_glyph.transform_for_font_space(),
)
parent_glyph = color_glyph.ufo_glyph
@@ -527,6 +540,7 @@ def _create_transformed_glyph(
glyph = _init_glyph(color_glyph)
glyph.components.append(Component(baseGlyph=paint.glyph, transformation=transform))
color_glyph.ufo.glyphOrder += [glyph.name]
+ print("_create_transformed_glyph", glyph.name, transform)
return glyph
@@ -688,10 +702,9 @@ def _colr_ufo(
quantization = round(config.upem * 0.02)
for i, color_glyph in enumerate(color_glyphs):
logging.debug(
- "%s %s %s",
+ "%s %s",
ufo.info.familyName,
color_glyph.ufo_glyph_name,
- color_glyph.transform_for_font_space(),
)
# generate glyphs for PaintGlyph's and assign glyph names
@@ -793,6 +806,7 @@ def _generate_color_font(
config: FontConfig, reusable_parts: ReusableParts, inputs: Iterable[InputGlyph]
):
"""Make a UFO and optionally a TTFont from svgs."""
+ print("_generate_color_font", "upem", config.upem)
ufo = _ufo(config)
_ensure_codepoints_will_have_glyphs(ufo, inputs)
@@ -905,7 +919,7 @@ def main(argv):
reusable_parts = ReusableParts()
if FLAGS.part_file:
- reusable_parts = ReusableParts.load(Path(FLAGS.part_file))
+ reusable_parts = ReusableParts.loadjson(Path(FLAGS.part_file))
ufo, ttfont = _generate_color_font(font_config, reusable_parts, inputs)
_write(ufo, ttfont, font_config.output_file)
diff --git a/src/nanoemoji/write_part_file.py b/src/nanoemoji/write_part_file.py
index a00a86b9..f92209da 100644
--- a/src/nanoemoji/write_part_file.py
+++ b/src/nanoemoji/write_part_file.py
@@ -21,6 +21,8 @@
from nanoemoji.parts import ReusableParts
from nanoemoji import util
from pathlib import Path
+from picosvg.geometric_types import Rect
+from picosvg.svg import SVG
FLAGS = flags.FLAGS
@@ -32,7 +34,12 @@
def main(argv):
if len(argv) != 2:
raise ValueError("Specify exactly one input")
- parts = ReusableParts.load(Path(argv[1]))
+
+ view_box = Rect(0, 0, FLAGS.upem, FLAGS.upem)
+ parts = ReusableParts(view_box=view_box, reuse_tolerance=FLAGS.reuse_tolerance)
+
+ svg = SVG.parse(Path(argv[1]))
+ parts.add(svg)
if FLAGS.compute_donors:
parts.compute_donors()
@@ -42,4 +49,6 @@ def main(argv):
if __name__ == "__main__":
+ flags.mark_flag_as_required("upem")
+ flags.mark_flag_as_required("reuse_tolerance")
app.run(main)
diff --git a/tests/circle.svg b/tests/circle.svg
new file mode 100644
index 00000000..1c39dab6
--- /dev/null
+++ b/tests/circle.svg
@@ -0,0 +1,3 @@
+
diff --git a/tests/circle_10x.svg b/tests/circle_10x.svg
new file mode 100644
index 00000000..11c9c7b9
--- /dev/null
+++ b/tests/circle_10x.svg
@@ -0,0 +1,3 @@
+
diff --git a/tests/clocks_picosvg.ttx b/tests/clocks_picosvg.ttx
index c79c3a5c..02c200f9 100644
--- a/tests/clocks_picosvg.ttx
+++ b/tests/clocks_picosvg.ttx
@@ -67,36 +67,36 @@
-
-
-
-
+
+
+
+
-
+
-
-
-
-
-
-
-
+
+
+
+
+
+
+
-
+
-
-
-
-
-
-
-
+
+
+
+
+
+
+
diff --git a/tests/clocks_rects_picosvg.ttx b/tests/clocks_rects_picosvg.ttx
index d0c1548f..41987f3c 100644
--- a/tests/clocks_rects_picosvg.ttx
+++ b/tests/clocks_rects_picosvg.ttx
@@ -79,7 +79,7 @@
-
+
diff --git a/tests/parts_test.py b/tests/parts_test.py
index afdcd1b2..c8cef3b2 100644
--- a/tests/parts_test.py
+++ b/tests/parts_test.py
@@ -16,10 +16,13 @@
from nanoemoji.util import only
from picosvg.geometric_types import Rect
from picosvg.svg import SVG
+from picosvg import svg_meta
from picosvg.svg_types import SVGCircle, SVGPath, SVGRect
from picosvg.svg_reuse import affine_between
+from pathlib import Path
import pprint
import pytest
+import re
from test_helper import cleanup_temp_dirs, locate_test_file, mkdtemp
@@ -37,14 +40,45 @@ def _cleanup_temporary_dirs():
# TODO we get pointless precision, e.g. 1.2000000000000002
+def _svg_commands(path: str) -> str:
+ print(path)
+ svg_cmds = "".join(svg_meta.cmds())
+ return re.sub(f"[^{svg_cmds}]+", "", path)
+
+
def check_num_shapes(parts: ReusableParts, expected_shape_sets: int):
assert len(parts.shape_sets) == expected_shape_sets, ",".join(
sorted(str(p) for p in parts.shape_sets.keys())
)
+def _from_svg(svg, view_box=None) -> ReusableParts:
+ if isinstance(svg, str):
+ svg = SVG.fromstring(svg)
+ elif isinstance(svg, Path):
+ svg = SVG.parse(svg)
+ if view_box is None:
+ view_box = svg.view_box()
+ parts = ReusableParts(view_box=view_box)
+ parts.add(svg)
+ return parts
+
+
+def test_add_svg():
+ parts = _from_svg(
+ """
+
+ """
+ )
+ check_num_shapes(parts, 1)
+
+
def test_collects_normalized_shapes():
- parts = ReusableParts.fromstring(
+ parts = _from_svg(
"""