Esempio n. 1
0
 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]
Esempio n. 2
0
    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
Esempio n. 3
0
 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})")
Esempio n. 4
0
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
Esempio n. 5
0
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
Esempio n. 6
0
    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, )
Esempio n. 7
0
    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)
Esempio n. 8
0
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()
Esempio n. 9
0
 (
     ("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,