Exemplo n.º 1
0
def _decompose_uniform_transform(transform: Affine2D) -> Tuple[Affine2D, Affine2D]:
    scale, remaining_transform = transform.decompose_scale()
    s = max(*scale.getscale())
    # most transforms will contain a Y-flip component as result of mapping from SVG to
    # font coordinate space. Here we keep this negative Y sign as part of the uniform
    # transform since it does not affect the circle-ness, and also makes so that the
    # font-mapped gradient geometry is more likely to be in the +x,+y quadrant like
    # the path geometry it is applied to.
    uniform_scale = Affine2D(s, 0, 0, copysign(s, transform.d), 0, 0)
    remaining_transform = Affine2D.compose_ltr(
        (uniform_scale.inverse(), scale, remaining_transform)
    )

    translate, remaining_transform = remaining_transform.decompose_translation()
    # round away very small float-math noise, so we get clean 0s and 1s for the special
    # case of identity matrix which implies no wrapping transform
    remaining_transform = remaining_transform.round(9)

    logging.debug(
        "Decomposing %r:\n\tscale: %r\n\ttranslate: %r\n\tremaining: %r",
        transform,
        uniform_scale,
        translate,
        remaining_transform,
    )

    uniform_transform = Affine2D.compose_ltr((uniform_scale, translate))
    return uniform_transform, remaining_transform
Exemplo n.º 2
0
def _apply_gradient_ot_paint(
        svg_defs: etree.Element,
        svg_path: etree.Element,
        ttfont: ttLib.TTFont,
        font_to_vbox: Affine2D,
        ot_paint: otTables.Paint,
        reuse_cache: ReuseCache,
        transform: Affine2D = Affine2D.identity(),
):
    paint = _gradient_paint(ttfont, ot_paint)
    # For radial gradients we want to keep cirlces as such, so we must decompose into
    # a uniform scale+translate plus a remainder to encode as gradientTransform.
    # Whereas for linear gradients, we can simply apply the whole combined transform to
    # start/end points and omit gradientTransform attribute.
    coord_transform = Affine2D.compose_ltr((transform, font_to_vbox))
    remaining_transform = Affine2D.identity()
    if paint.format == PaintRadialGradient.format:
        coord_transform, remaining_transform = _decompose_uniform_transform(
            coord_transform)
    paint = _map_gradient_coordinates(paint, coord_transform)
    _apply_gradient_paint(svg_defs,
                          svg_path,
                          paint,
                          reuse_cache,
                          transform=remaining_transform)
Exemplo n.º 3
0
def _apply_paint(
        svg_defs: etree.Element,
        el: etree.Element,
        paint: Paint,
        upem_to_vbox: Affine2D,
        reuse_cache: ReuseCache,
        transform: Affine2D = Affine2D.identity(),
):
    # If you modify the attributes _apply_paint can set also modify _PAINT_ATTRIB_APPLY_PAINT_MAY_SET
    if isinstance(paint, PaintSolid):
        _apply_solid_paint(el, paint)
    elif isinstance(paint, (PaintLinearGradient, PaintRadialGradient)):
        # Gradient paint coordinates are in UPEM space, we want them in SVG viewBox
        # so that they match the SVGPath.d coordinates (that we copy unmodified).
        paint = _map_gradient_coordinates(paint, upem_to_vbox)
        # Likewise transforms refer to UPEM so they must be adjusted for SVG
        if transform != Affine2D.identity():
            transform = Affine2D.compose_ltr(
                (upem_to_vbox.inverse(), transform, upem_to_vbox))
        _apply_gradient_paint(svg_defs, el, paint, reuse_cache, transform)
    elif is_transform(paint):
        transform @= paint.gettransform()
        child = paint.paint  # pytype: disable=attribute-error
        _apply_paint(svg_defs, el, child, upem_to_vbox, reuse_cache, transform)
    else:
        raise NotImplementedError(type(paint))
Exemplo n.º 4
0
    def _update_paint_glyph(paint):
        if paint.format != PaintGlyph.format:
            return paint

        if glyph_cache.is_known_glyph(paint.glyph):
            return paint

        assert paint.glyph.startswith(
            "M"), f"{paint.glyph} doesn't look like a path"
        path_in_font_space = (SVGPath(
            d=paint.glyph).apply_transform(svg_units_to_font_units).d)

        reuse_result = glyph_cache.try_reuse(path_in_font_space)
        if reuse_result is not None:
            # TODO: when is it more compact to use a new transforming glyph?
            child_transform = Affine2D.identity()
            child_paint = paint.paint
            if is_transform(child_paint):
                child_transform = child_paint.gettransform()
                child_paint = child_paint.paint

            # sanity check: GlyphReuseCache.try_reuse would return None if overflowed
            assert fixed_safe(*reuse_result.transform)
            overflows = False

            # TODO: handle gradient anywhere in subtree, not only as direct child of
            # PaintGlyph or PaintTransform
            if is_gradient(child_paint):
                # We have a gradient so we need to reverse the effect of the
                # reuse_result.transform. First we try to apply the combined transform
                # to the gradient's geometry; but this may overflow OT integer bounds,
                # in which case we pass through gradient unscaled
                transform = Affine2D.compose_ltr(
                    (child_transform, reuse_result.transform.inverse()))
                # skip reuse if combined transform overflows OT int bounds
                overflows = not fixed_safe(*transform)
                if not overflows:
                    try:
                        child_paint = child_paint.apply_transform(transform)
                    except OverflowError:
                        child_paint = transformed(transform, child_paint)

            if not overflows:
                return transformed(
                    reuse_result.transform,
                    PaintGlyph(
                        glyph=reuse_result.glyph_name,
                        paint=child_paint,
                    ),
                )

        glyph = _create_glyph(color_glyph, paint, path_in_font_space)
        glyph_cache.add_glyph(glyph.name, path_in_font_space)

        return dataclasses.replace(paint, glyph=glyph.name)
Exemplo n.º 5
0
def map_viewbox_to_otsvg_space(
    view_box: Rect, ascender: int, descender: int, width: int, user_transform: Affine2D
) -> Affine2D:
    return Affine2D.compose_ltr(
        [
            _scale_viewbox_to_font_metrics(view_box, ascender, descender, width),
            # shift things in the [+x,-y] quadrant where OT-SVG expects them
            Affine2D(1, 0, 0, 1, 0, -ascender),
            user_transform,
        ]
    )
Exemplo n.º 6
0
def map_viewbox_to_font_space(
    view_box: Rect, ascender: int, descender: int, width: int, user_transform: Affine2D
) -> Affine2D:
    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,
        ]
    )
Exemplo n.º 7
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
Exemplo n.º 8
0
def _scale_viewbox_to_font_metrics(
    view_box: Rect, ascender: int, descender: int, width: int
):
    assert descender <= 0
    # scale height to (ascender - descender)
    scale = (ascender - descender) / view_box.h
    # shift so width is centered
    dx = (width - scale * view_box.w) / 2
    return Affine2D.compose_ltr(
        (
            # first normalize viewbox origin
            Affine2D(1, 0, 0, 1, -view_box.x, -view_box.y),
            Affine2D(scale, 0, 0, scale, dx, 0),
        )
    )
Exemplo n.º 9
0
def _picosvg_transform(self, affine):
    for idx, (el, (shape, )) in enumerate(self._elements()):
        self.elements[idx] = (el, (shape.apply_transform(affine), ))

    for el in self._select_gradients():
        gradient = _GRADIENT_CLASSES[strip_ns(el.tag)].from_element(
            el, self.view_box())
        gradient_transform = Affine2D.compose_ltr(
            (gradient.gradientTransform, affine))

        if gradient_transform != Affine2D.identity():
            el.attrib["gradientTransform"] = gradient_transform.tostring()
        elif "gradientTransform" in el.attrib:
            del el.attrib["gradientTransform"]
    return self
Exemplo n.º 10
0
def _affine_vec2vec(initial: Vector, target: Vector) -> Affine2D:
    affine = Affine2D.identity()

    # rotate initial to have the same angle as target (may have different magnitude)
    angle = _angle(target) - _angle(initial)
    affine = Affine2D.identity().rotate(angle)
    vec = affine.map_vector(initial)

    # scale to target magnitude
    s = 0
    if vec.norm() != 0:
        s = target.norm() / vec.norm()

    affine = Affine2D.compose_ltr((affine, Affine2D.identity().scale(s, s)))

    return affine
Exemplo n.º 11
0
def _transform_gradients(self, warp):
    for el in self._select_gradients():
        gradient = _GRADIENT_CLASSES[strip_ns(el.tag)].from_element(
            el, self.view_box())

        mid_pt = _gradient_mid_point(gradient)
        mid_pt = gradient.gradientTransform.map_point(mid_pt)
        translate = Affine2D.identity().translate(*warp.vec(mid_pt))
        mid_pt = translate.map_point(mid_pt)

        gradient_transform = Affine2D.compose_ltr(
            (gradient.gradientTransform, translate, warp.skewY(mid_pt.x)))

        if gradient_transform != Affine2D.identity():
            el.attrib["gradientTransform"] = gradient_transform.tostring()
        elif "gradientTransform" in el.attrib:
            del el.attrib["gradientTransform"]
    return self
Exemplo n.º 12
0
 def breadth_first(self) -> Generator[PaintTraverseContext, None, None]:
     frontier = [PaintTraverseContext((), self, Affine2D.identity())]
     while frontier:
         context = frontier.pop(0)
         yield context
         transform = context.transform
         paint_transform = context.paint.gettransform()
         transform = Affine2D.compose_ltr(
             (
                 transform,
                 paint_transform,
             )
         )
         for paint in context.paint.children():
             frontier.append(
                 PaintTraverseContext(
                     context.path + (context.paint,), paint, transform
                 )
             )
Exemplo n.º 13
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, )
Exemplo n.º 14
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)
Exemplo n.º 15
0
def _colr_v1_paint_to_svg(
        ttfont: ttLib.TTFont,
        glyph_set: Mapping[str, Any],
        parent_el: etree.Element,
        svg_defs: etree.Element,
        font_to_vbox: Affine2D,
        ot_paint: otTables.Paint,
        reuse_cache: ReuseCache,
        transform: Affine2D = Affine2D.identity(),
):
    def descend(parent: etree.Element, paint: otTables.Paint):
        _colr_v1_paint_to_svg(
            ttfont,
            glyph_set,
            parent,
            svg_defs,
            font_to_vbox,
            paint,
            reuse_cache,
            transform=transform,
        )

    if ot_paint.Format == PaintSolid.format:
        _apply_solid_ot_paint(parent_el, ttfont, ot_paint)
    elif ot_paint.Format in _GRADIENT_PAINT_FORMATS:
        _apply_gradient_ot_paint(svg_defs, parent_el, ttfont, font_to_vbox,
                                 ot_paint, reuse_cache, transform)
    elif ot_paint.Format == PaintGlyph.format:
        layer_glyph = ot_paint.Glyph
        svg_path = etree.SubElement(parent_el, "path")

        # This only occurs if path is reused; we could wire up use. But for now ... not.
        if transform != Affine2D.identity():
            svg_transform = Affine2D.compose_ltr(
                (font_to_vbox.inverse(), transform, font_to_vbox))
            svg_path.attrib["transform"] = _svg_matrix(svg_transform)
            # we must reset the current user space when setting the 'transform'
            # attribute on a <path>, since that already affects the gradients used
            # and we don't want the transform to be applied twice to gradients:
            # https://github.com/googlefonts/nanoemoji/issues/334
            transform = Affine2D.identity()

        descend(svg_path, ot_paint.Paint)

        _draw_svg_path(svg_path, glyph_set, layer_glyph, font_to_vbox)
    elif is_transform(ot_paint.Format):
        paint = Paint.from_ot(ot_paint)
        transform @= paint.gettransform()
        descend(parent_el, ot_paint.Paint)
    elif ot_paint.Format == PaintColrLayers.format:
        layerList = ttfont["COLR"].table.LayerList.Paint
        assert layerList, "Paint layers without a layer list :("
        for child_paint in layerList[ot_paint.
                                     FirstLayerIndex:ot_paint.FirstLayerIndex +
                                     ot_paint.NumLayers]:
            descend(parent_el, child_paint)

    elif ot_paint.Format == PaintComposite.format and (
            ot_paint.CompositeMode == CompositeMode.SRC_IN
            and ot_paint.BackdropPaint.Format == PaintSolid.format):
        # Only simple group opacity for now
        color = _color(
            ttfont,
            ot_paint.BackdropPaint.PaletteIndex,
            ot_paint.BackdropPaint.Alpha,
        )
        if color[:3] != (0, 0, 0):
            raise NotImplementedError(color)
        g = etree.SubElement(parent_el, "g")
        g.attrib["opacity"] = ntos(color.alpha)
        descend(g, ot_paint.SourcePaint)

    else:
        raise NotImplementedError(ot_paint.Format)
Exemplo n.º 16
0
def affine_between(s1: SVGShape, s2: SVGShape,
                   tolerance: float) -> Optional[Affine2D]:
    """Returns the Affine2D to change s1 into s2 or None if no solution was found.

    Intended use is to call this only when the normalized versions of the shapes
    are the same, in which case finding a solution is typical

    """
    s1 = dataclasses.replace(s1, id="")
    s2 = dataclasses.replace(s2, id="")

    if s1.almost_equals(s2, tolerance):
        return Affine2D.identity()

    s1 = _affine_friendly(s1)
    s2 = _affine_friendly(s2)

    s1x, s1y = _first_move(s1)
    s2x, s2y = _first_move(s2)

    affine = Affine2D.identity().translate(s2x - s1x, s2y - s1y)
    if _try_affine(affine, s1, s2, tolerance):
        return affine

    # Normalize first edge.
    # Fixes rotation, x-scale, and uniform scaling.
    s1_vec1 = _nth_vector(s1, 1)
    s2_vec1 = _nth_vector(s2, 1)

    s1_to_origin = Affine2D.identity().translate(-s1x, -s1y)
    s2_to_origin = Affine2D.identity().translate(-s2x, -s2y)
    s1_vec1_to_s2_vec1 = _affine_vec2vec(s1_vec1, s2_vec1)

    # Move to s2 start
    origin_to_s2 = Affine2D.identity().translate(s2x, s2y)

    affine = Affine2D.compose_ltr(
        (s1_to_origin, s1_vec1_to_s2_vec1, origin_to_s2))
    if _try_affine(affine, s1, s2, tolerance):
        return _round(affine, s1, s2, tolerance)

    # Could be non-uniform scaling and/or mirroring
    # Scale first y movement (after matching up vec1) to match

    # Rotate first edge to lie on x axis
    s2_vec1_angle = _angle(s2_vec1)
    rotate_s2vec1_onto_x = Affine2D.identity().rotate(-s2_vec1_angle)
    rotate_s2vec1_off_x = Affine2D.identity().rotate(s2_vec1_angle)

    affine = Affine2D.compose_ltr(
        (s1_to_origin, s1_vec1_to_s2_vec1, rotate_s2vec1_onto_x))
    s1_prime = _apply_affine(affine, s1)

    affine = Affine2D.compose_ltr((s2_to_origin, rotate_s2vec1_onto_x))
    s2_prime = _apply_affine(affine, s2)

    s1_vecy = _first_y(_vectors(s1_prime), tolerance)
    s2_vecy = _first_y(_vectors(s2_prime), tolerance)

    if s1_vecy and s2_vecy:
        affine = Affine2D.compose_ltr((
            s1_to_origin,
            s1_vec1_to_s2_vec1,
            # lie vec1 along x axis
            rotate_s2vec1_onto_x,
            # scale first y-vectors to match; x-parts should already match
            Affine2D.identity().scale(1.0, s2_vecy.y / s1_vecy.y),
            # restore the rotation we removed
            rotate_s2vec1_off_x,
            # drop into final position
            origin_to_s2,
        ))
        if _try_affine(affine, s1, s2, tolerance):
            return _round(affine, s1, s2, tolerance)

    # If we still aren't the same give up
    return None