def _inherit_matrix_multiply(attrib, child, attr_name): group_transform = Affine2D.fromstring(attrib[attr_name]) if attr_name in child.attrib: transform = Affine2D.fromstring(child.attrib[attr_name]) transform = Affine2D.product(transform, group_transform) else: transform = group_transform if transform != Affine2D.identity(): child.attrib[attr_name] = transform.tostring() else: del child.attrib[attr_name]
def apply_transforms(self, inplace=False): """Naively transforms to shapes and removes the transform attribute. Naive: just applies any transform on a parent element. """ if not inplace: svg = SVG(copy.deepcopy(self.svg_root)) svg.apply_transforms(inplace=True) return svg self._update_etree() # figure out the sequence of transforms, if any, for each shape new_shapes = [] for idx, (el, (shape, )) in enumerate(self._elements()): transform = Affine2D.identity() while el is not None: if "transform" in el.attrib: transform = Affine2D.product( transform, Affine2D.fromstring(el.attrib["transform"])) el = el.getparent() if transform == Affine2D.identity(): continue new_shapes.append((idx, shape.apply_transform(transform))) for el_idx, new_shape in new_shapes: el, _ = self.elements[el_idx] self._set_element(el_idx, el, (new_shape, )) # destroy all transform attributes self.remove_attributes(["transform"], xpath="//svg:*[@transform]", inplace=True) return self
def skewY(self, x: float) -> Affine2D: # Get linear transform that approximates the flag's (non-linear) warp around # the given x coordinate ("Jacobian matrix"?). if x <= self.minx: tangent = cubic_tangent(0.0, *self.warp) else: tangent = cubic_tangent(1.0, *self._seg_ending_at(x)) angle = degrees(asin(tangent.y)) return Affine2D.fromstring( f"translate({x}) skewY({angle}) translate({-x})")
def _get_gradient_transform(grad_el, shape_bbox, view_box, upem) -> Affine2D: transform = map_viewbox_to_font_emsquare(view_box, upem) gradient_units = grad_el.attrib.get("gradientUnits", "objectBoundingBox") if gradient_units == "objectBoundingBox": bbox_space = Rect(0, 0, 1, 1) bbox_transform = Affine2D.rect_to_rect(bbox_space, shape_bbox) transform = Affine2D.product(bbox_transform, transform) if "gradientTransform" in grad_el.attrib: gradient_transform = Affine2D.fromstring( grad_el.attrib["gradientTransform"]) transform = Affine2D.product(gradient_transform, transform) return transform
def _get_gradient_transform( config: FontConfig, grad_el: etree.Element, shape_bbox: Rect, view_box: Rect, glyph_width: int, ) -> Affine2D: transform = map_viewbox_to_font_space( view_box, config.ascender, config.descender, glyph_width, config.transform ) gradient_units = grad_el.attrib.get("gradientUnits", "objectBoundingBox") if gradient_units == "objectBoundingBox": bbox_space = Rect(0, 0, 1, 1) bbox_transform = Affine2D.rect_to_rect(bbox_space, shape_bbox) transform = Affine2D.compose_ltr((bbox_transform, transform)) if "gradientTransform" in grad_el.attrib: gradient_transform = Affine2D.fromstring(grad_el.attrib["gradientTransform"]) transform = Affine2D.compose_ltr((gradient_transform, transform)) return transform
def _unnest_svg(self, svg: etree.Element, parent_width: float, parent_height: float) -> Tuple[etree.Element, ...]: x = float(svg.attrib.get("x", 0)) y = float(svg.attrib.get("y", 0)) width = float(svg.attrib.get("width", parent_width)) height = float(svg.attrib.get("height", parent_height)) viewport = viewbox = Rect(x, y, width, height) if "viewBox" in svg.attrib: viewbox = parse_view_box(svg.attrib["viewBox"]) # first recurse to un-nest any nested nested SVGs self._swap_elements((el, self._unnest_svg(el, viewbox.w, viewbox.h)) for el in self._iter_nested_svgs(svg)) g = etree.Element(f"{{{svgns()}}}g") g.extend(svg) if viewport != viewbox: preserve_aspect_ratio = svg.attrib.get("preserveAspectRatio", "xMidYMid") transform = Affine2D.rect_to_rect(viewbox, viewport, preserve_aspect_ratio) else: transform = Affine2D.identity().translate(x, y) if "transform" in svg.attrib: transform = Affine2D.compose_ltr( (transform, Affine2D.fromstring(svg.attrib["transform"]))) if transform != Affine2D.identity(): g.attrib["transform"] = transform.tostring() # TODO Define a viewport-sized clipPath once transform+clip-path issue is fixed # https://github.com/googlefonts/picosvg/issues/200 return (g, )
def _resolve_use(self, scope_el): attrib_not_copied = { "x", "y", "width", "height", "transform", _xlink_href_attr_name(), } # capture elements by id so even if we change it they remain stable el_by_id = {el.attrib["id"]: el for el in self.xpath(".//svg:*[@id]")} while True: swaps = [] use_els = list(self.xpath(".//svg:use", el=scope_el)) if not use_els: break for use_el in use_els: ref = use_el.attrib.get(_xlink_href_attr_name(), "") if not ref.startswith("#"): raise ValueError( f"Only use #fragment supported, reject {ref}") target = el_by_id.get(ref[1:], None) if target is None: raise ValueError(f"No element has id '{ref[1:]}'") new_el = copy.deepcopy(target) # leaving id's on <use> instantiated content is a path to duplicate ids for el in new_el.getiterator("*"): if "id" in el.attrib: del el.attrib["id"] group = etree.Element(f"{{{svgns()}}}g", nsmap=self.svg_root.nsmap) affine = Affine2D.identity().translate( float(use_el.attrib.get("x", 0)), float(use_el.attrib.get("y", 0))) if "transform" in use_el.attrib: affine = Affine2D.compose_ltr(( affine, Affine2D.fromstring(use_el.attrib["transform"]), )) if affine != Affine2D.identity(): group.attrib["transform"] = affine.tostring() for attr_name in use_el.attrib: if attr_name in attrib_not_copied: continue group.attrib[attr_name] = use_el.attrib[attr_name] if len(group.attrib): group.append(new_el) swaps.append((use_el, group)) else: swaps.append((use_el, new_el)) for old_el, new_el in swaps: old_el.getparent().replace(old_el, new_el)
def load(config_file: Optional[Path] = None, additional_srcs: Optional[Tuple[Path]] = None) -> FontConfig: config_dir, config = _resolve_config(config_file) # CLI flags will take precedence over the config file family = _pop_flag(config, "family") output_file = _pop_flag(config, "output_file") color_format = _pop_flag(config, "color_format") upem = int(_pop_flag(config, "upem")) width = int(_pop_flag(config, "width")) ascender = int(_pop_flag(config, "ascender")) descender = int(_pop_flag(config, "descender")) linegap = int(_pop_flag(config, "linegap")) transform = _pop_flag(config, "transform") if not isinstance(transform, Affine2D): assert isinstance(transform, str) transform = Affine2D.fromstring(transform) version_major = int(_pop_flag(config, "version_major")) version_minor = int(_pop_flag(config, "version_minor")) reuse_tolerance = float(_pop_flag(config, "reuse_tolerance")) ignore_reuse_error = _pop_flag(config, "ignore_reuse_error") keep_glyph_names = _pop_flag(config, "keep_glyph_names") clip_to_viewbox = _pop_flag(config, "clip_to_viewbox") clipbox_quantization = _pop_flag(config, "clipbox_quantization") pretty_print = _pop_flag(config, "pretty_print") fea_file = _pop_flag(config, "fea_file") glyphmap_generator = _pop_flag(config, "glyphmap_generator") axes = [] for axis_tag, axis_config in config.pop("axis").items(): axes.append( Axis( axis_tag, axis_config.pop("name"), axis_config.pop("default"), )) if axis_config: raise ValueError(f"Unexpected '{axis_tag}' config: {axis_config}") masters = [] source_names = set() for master_name, master_config in config.pop("master").items(): positions = tuple( sorted( AxisPosition(k, v) for k, v in master_config.pop("position").items())) srcs = set() if "srcs" in master_config: for src in master_config.pop("srcs"): srcs.update(_resolve_src(config_dir, src)) if additional_srcs is not None: srcs.update(additional_srcs) srcs = tuple(sorted(p.resolve() for p in srcs)) master = MasterConfig( master_name, master_config.pop("style_name"), ".".join(( Path(output_file).stem, master_name, "ufo", )), positions, srcs, ) if master_config: raise ValueError( f"Unexpected '{master_name}' config: {master_config}") masters.append(master) master_source_names = {s.name for s in master.sources} if len(master_source_names) != len(master.sources): raise ValueError( f"Input svgs for {master_name} must have unique names") if not source_names: source_names = master_source_names elif source_names != master_source_names: raise ValueError( f"{fonts[i].name} srcs don't match {fonts[0].name}") if not masters: raise ValueError("Must have at least one master") if config: raise ValueError(f"Unexpected config: {config}") return FontConfig( family=family, output_file=output_file, color_format=color_format, upem=upem, width=width, ascender=ascender, descender=descender, linegap=linegap, transform=transform, version_major=version_major, version_minor=version_minor, reuse_tolerance=reuse_tolerance, ignore_reuse_error=ignore_reuse_error, keep_glyph_names=keep_glyph_names, clip_to_viewbox=clip_to_viewbox, clipbox_quantization=clipbox_quantization, pretty_print=pretty_print, fea_file=fea_file, glyphmap_generator=glyphmap_generator, axes=tuple(axes), masters=tuple(masters), source_names=tuple(sorted(source_names)), ).validate()
( ("reused_shape_with_gradient.svg", ), "reused_shape_with_gradient.ttx", { "color_format": "glyf_colr_1" }, ), # Confirm we can apply a user transform, override some basic metrics ( ("one_rect.svg", ), "one_rect_transformed.ttx", { "color_format": "glyf_colr_1", "transform": Affine2D.fromstring( "scale(0.5, 0.75) translate(50) rotate(45)"), "width": 120, }, ), # Check that we use xlink:href to reuse shapes with <use> elements # https://github.com/googlefonts/nanoemoji/issues/266 ( ("reused_shape_2.svg", ), "reused_shape_2_picosvg.ttx", { "color_format": "picosvg", "pretty_print": True }, ), # Safari can't deal with gradientTransform where matrix.inverse() == self,