コード例 #1
0
ファイル: svg.py プロジェクト: yisibl/picosvg
    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
コード例 #2
0
def affine_between(s1: SVGShape,
                   s2: SVGShape,
                   tolerance: int = DEFAULT_TOLERANCE) -> Optional[Affine2D]:
    """Returns the Affine2D to change s1 into s2 or None if no solution was found.

    Implementation starting *very* basic, can improve over time.
    """
    s1 = dataclasses.replace(s1, id="")
    s2 = dataclasses.replace(s2, id="")

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

    s1 = s1.as_path()
    s2 = s2.as_path()

    s1x, s1y = _first_move(s1)
    s2x, s2y = _first_move(s2)
    dx = s2x - s1x
    dy = s2y - s1y

    s1.move(dx, dy, inplace=True)

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

    return None
コード例 #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))
コード例 #4
0
ファイル: svg.py プロジェクト: yisibl/picosvg
    def _apply_gradient_translation(self, inplace=False):
        if not inplace:
            svg = SVG(copy.deepcopy(self.svg_root))
            svg._apply_gradient_translation(inplace=True)
            return svg

        for el in self._select_gradients():
            gradient = _GRADIENT_CLASSES[strip_ns(el.tag)].from_element(
                el, self.view_box())
            a, b, c, d, e, f = (round(v, _GRADIENT_TRANSFORM_NDIGITS)
                                for v in gradient.gradientTransform)
            affine = Affine2D(a, b, c, d, e, f)
            #  no translate? nop!
            if (e, f) == (0, 0):
                continue

            # split translation from rest of the transform and apply to gradient coords
            translate, affine_prime = affine.decompose_translation()
            for x_attr, y_attr in _GRADIENT_COORDS[strip_ns(el.tag)]:
                # if at default just ignore
                if x_attr not in el.attrib and y_attr not in el.attrib:
                    continue
                x = getattr(gradient, x_attr)
                y = getattr(gradient, y_attr)
                x_prime, y_prime = translate.map_point((x, y))
                el.attrib[x_attr] = ntos(
                    round(x_prime, _GRADIENT_TRANSFORM_NDIGITS))
                el.attrib[y_attr] = ntos(
                    round(y_prime, _GRADIENT_TRANSFORM_NDIGITS))

            if affine_prime != Affine2D.identity():
                el.attrib["gradientTransform"] = (
                    "matrix(" + " ".join(ntos(v) for v in affine_prime) + ")")
            else:
                del el.attrib["gradientTransform"]
コード例 #5
0
ファイル: colr_to_svg.py プロジェクト: yisibl/nanoemoji
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)
コード例 #6
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
コード例 #7
0
 def apply_transform(self, transform: Affine2D) -> Paint:
     return dataclasses.replace(
         self,
         p0=transform.map_point(self.p0),
         p1=transform.map_point(self.p1),
         p2=transform.map_point(self.p2),
     ).check_overflows()
コード例 #8
0
ファイル: write_font.py プロジェクト: yisibl/nanoemoji
def _transformed_glyph_bounds(
        ufo: ufoLib2.Font, glyph_name: str,
        transform: Affine2D) -> Optional[Tuple[float, float, float, float]]:
    glyph = ufo[glyph_name]
    pen = bounds_pen = ControlBoundsPen(ufo)
    if not transform.almost_equals(Affine2D.identity()):
        pen = TransformPen(bounds_pen, transform)
    glyph.draw(pen)
    return bounds_pen.bounds
コード例 #9
0
ファイル: write_font.py プロジェクト: yisibl/nanoemoji
    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)
コード例 #10
0
ファイル: color_glyph.py プロジェクト: yisibl/nanoemoji
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,
        ]
    )
コード例 #11
0
ファイル: color_glyph.py プロジェクト: yisibl/nanoemoji
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,
        ]
    )
コード例 #12
0
ファイル: color_glyph.py プロジェクト: mavit/nanoemoji
def _parse_radial_gradient(grad_el, shape_bbox, view_box, upem):
    width, height = _get_gradient_units_relative_scale(grad_el, view_box)

    cx = _number_or_percentage(grad_el.attrib.get("cx", "50%"), width)
    cy = _number_or_percentage(grad_el.attrib.get("cy", "50%"), height)
    r = _number_or_percentage(grad_el.attrib.get("r", "50%"), width)

    raw_fx = grad_el.attrib.get("fx")
    fx = _number_or_percentage(raw_fx, width) if raw_fx is not None else cx
    raw_fy = grad_el.attrib.get("fy")
    fy = _number_or_percentage(raw_fy, height) if raw_fy is not None else cy
    fr = _number_or_percentage(grad_el.attrib.get("fr", "0%"), width)

    c0 = Point(fx, fy)
    r0 = fr
    c1 = Point(cx, cy)
    r1 = r

    transform = _get_gradient_transform(grad_el, shape_bbox, view_box, upem)

    # The optional Affine2x2 matrix of COLRv1.RadialGradient is used to transform
    # the circles into ellipses "around their centres": i.e. centres coordinates
    # are _not_ transformed by it. Thus we apply the full transform to them.
    c0 = transform.map_point(c0)
    c1 = transform.map_point(c1)

    # As for the circle radii (which are affected by Affine2x2), we only scale them
    # by the maximum of the (absolute) scale or skew.
    # Then in Affine2x2 we only store a "fraction" of the original transform, i.e.
    # multiplied by the inverse of the scale that we've already applied to the radii.
    # Especially when gradientUnits="objectBoundingBox", where circle positions and
    # radii are expressed using small floats in the range [0..1], this pre-scaling
    # helps reducing the inevitable rounding errors that arise from storing these
    # values as integers in COLRv1 tables.
    s = max(abs(v) for v in transform[:4])

    rscale = Affine2D(s, 0, 0, s, 0, 0)
    r0 = rscale.map_vector((r0, 0)).x
    r1 = rscale.map_vector((r1, 0)).x

    affine2x2 = Affine2D.product(rscale.inverse(), transform)

    gradient = {
        "c0": c0,
        "c1": c1,
        "r0": r0,
        "r1": r1,
        "affine2x2":
        (affine2x2[:4] if affine2x2 != Affine2D.identity() else None),
    }

    # TODO handle degenerate cases, fallback to solid, w/e

    return gradient
コード例 #13
0
ファイル: svg.py プロジェクト: yisibl/picosvg
 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]
コード例 #14
0
def _radial_gradient_paint(
    svg_defs: etree.Element,
    svg_path: etree.Element,
    ttfont: ttLib.TTFont,
    view_box: Rect,
    stops: Sequence[_ColorStop],
    extend: Extend,
    c0: Point,
    c1: Point,
    r0: int,
    r1: int,
    transform: Affine2D,
):
    # map centres and radii from UPEM to SVG space
    upem_to_vbox = _emsquare_to_viewbox(ttfont["head"].unitsPerEm, view_box)
    c0 = upem_to_vbox.map_point(c0)
    c1 = upem_to_vbox.map_point(c1)
    # _emsquare_to_viewbox guarantees view_box is square so scaling radii is ok
    r0 = upem_to_vbox.map_point((r0, 0)).x
    r1 = upem_to_vbox.map_point((r1, 0)).x

    # COLRv1 centre points aren't affected by the gradient Affine2x2, whereas in SVG
    # gradientTransform applies to everything; to prevent that, we must also map
    # the centres with the inverse of gradientTransform, so they won't move.
    inverse_transform = transform.inverse()
    fx, fy = inverse_transform.map_point(c0)
    cx, cy = inverse_transform.map_point(c1)

    gradient = etree.SubElement(svg_defs, "radialGradient")
    gradient_id = gradient.attrib["id"] = f"g{len(svg_defs)}"
    gradient.attrib["gradientUnits"] = "userSpaceOnUse"
    gradient.attrib["fx"] = _ntos(fx)
    gradient.attrib["fy"] = _ntos(fy)
    gradient.attrib["fr"] = _ntos(r0)
    gradient.attrib["cx"] = _ntos(cx)
    gradient.attrib["cy"] = _ntos(cy)
    gradient.attrib["r"] = _ntos(r1)
    if transform != Affine2D.identity():
        gradient.attrib["gradientTransform"] = _svg_matrix(transform)
    if extend != Extend.PAD:
        gradient.attrib["spreadMethod"] = extend.name.lower()

    palette = ttfont["CPAL"].palettes[0]
    for stop in stops:
        stop_el = etree.SubElement(gradient, "stop")
        stop_el.attrib["offset"] = _ntos(stop.offset)
        cpal_color = palette[stop.palette_index]
        svg_color, svg_opacity = _svg_color_and_opacity(cpal_color, stop.alpha)
        stop_el.attrib["stop-color"] = svg_color
        if svg_opacity:
            stop_el.attrib["stop-opacity"] = svg_opacity

    svg_path.attrib["fill"] = f"url(#{gradient_id})"
コード例 #15
0
def _about_paint(paint):
    hashable = colr_builder._paint_tuple(paint)
    if hashable in visited:
        return
    visited.add(hashable)

    count_by_type[paint.getFormatName()] += 1

    if paint.getFormatName() == 'PaintTransform':
        tr = paint.Transform
        transform = (tr.xx, tr.yx, tr.xy, tr.yy, tr.dx, tr.dy)
        # just scale and/or translate?
        if tr.xy == 0 and tr.yx == 0:
            # Relationship between scale and translate leads to div 0?
            if ((tr.xx == 1) != (tr.dx == 0)) or ((tr.yy == 1) !=
                                                  (tr.dy == 0)):
                count_by_type[paint.getFormatName() + "::weird_scale"] += 1
            elif f2dot14_safe(tr.xx, tr.yy):
                cx = cy = 0
                if tr.dx != 0:
                    cx = tr.dx / (1 - tr.xx)
                if tr.dy != 0:
                    cy = tr.dy / (1 - tr.yy)

                if int16_safe(cx, cy):
                    if tr.dx == 0 and tr.dy == 0:
                        count_by_type[paint.getFormatName() +
                                      "::scale_origin"] += 1
                    else:
                        count_by_type[paint.getFormatName() +
                                      "::scale_around"] += 1
                    unique_dropped_affines.add(transform)
                else:
                    count_by_type[paint.getFormatName() +
                                  "::scale_around_non_int"] += 1
            else:
                count_by_type[paint.getFormatName() + "::large_scale"] += 1
        else:
            translate, other = Affine2D(*transform).decompose_translation()
            if _common_angle(*other[:4]):
                if translate.almost_equals(Affine2D.identity()):
                    count_by_type[paint.getFormatName() + "::pure_rotate"] += 1
                else:
                    count_by_type[paint.getFormatName() + "::move_rotate"] += 1
            elif (tr.dx, tr.dy) == (0, 0):
                count_by_type[paint.getFormatName() +
                              "::inexplicable_2x2"] += 1
            else:
                count_by_type[paint.getFormatName() +
                              "::inexplicable_2x3"] += 1
コード例 #16
0
ファイル: naive_warp.py プロジェクト: rsheeter/warp
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
コード例 #17
0
ファイル: color_glyph.py プロジェクト: yisibl/nanoemoji
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),
        )
    )
コード例 #18
0
ファイル: color_glyph.py プロジェクト: mavit/nanoemoji
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
コード例 #19
0
def transformed(transform: Affine2D, target: Paint) -> Paint:
    if transform == Affine2D.identity():
        return target

    sx, b, c, sy, dx, dy = transform

    # Int16 translation?
    if (dx, dy) != (0, 0) and Affine2D.identity().translate(dx, dy) == transform:
        if int16_safe(dx, dy):
            return PaintTranslate(paint=target, dx=dx, dy=dy)

    # Scale?
    # If all we have are scale and translation this is pure scaling
    # If b,c are present this is some sort of rotation or skew
    if (sx, sy) != (1, 1) and (b, c) == (0, 0) and f2dot14_safe(sx, sy):
        if (dx, dy) == (0, 0):
            if almost_equal(sx, sy):
                return PaintScaleUniform(paint=target, scale=sx)
            else:
                return PaintScale(paint=target, scaleX=sx, scaleY=sy)
        else:
            # translated scaling is the same as scale around a non-origin center
            # If you trace through translate, scale, translate you get:
            # dx = (1 - sx) * cx
            # cx = dx / (1 - sx)  # undefined if sx == 1; if so dx should == 0
            # so, as long as (1==sx) == (0 == dx) we're good
            if (1 == sx) == (0 == dx) and (1 == sy) == (0 == dy):
                cx = 0
                if sx != 1:
                    cx = dx / (1 - sx)
                cy = 0
                if sy != 1:
                    cy = dy / (1 - sy)
                if int16_safe(cx, cy):
                    if almost_equal(sx, sy):
                        return PaintScaleUniformAroundCenter(
                            paint=target, scale=sx, center=Point(cx, cy)
                        )
                    else:
                        return PaintScaleAroundCenter(
                            paint=target, scaleX=sx, scaleY=sy, center=Point(cx, cy)
                        )

    # TODO optimize rotations

    # TODO optimize, skew, rotate around center

    return PaintTransform(paint=target, transform=tuple(transform))
コード例 #20
0
ファイル: naive_warp.py プロジェクト: rsheeter/warp
def normalize_flag_aspect(svg, viewbox_size, width, height, right_margin,
                          top_margin):
    apply_viewbox_preserve_aspect_ratio(svg)

    current_box = _bbox(tuple(s.bounding_box() for s in svg.shapes()))

    # Try to keep overall proportions for the flags that are considerably
    # narrower or wider than the standard aspect ratio.
    aspect = current_box.w / current_box.h
    aspect /= STD_ASPECT
    aspect = sqrt(aspect)  # Discount the effect
    if 0.9 <= aspect <= 1.1:
        aspect = 1.0
    else:
        print("Non-standard aspect ratio:", aspect)

    xmin = _x_aspect(right_margin, aspect, viewbox_size)
    ymin = _y_aspect(top_margin, aspect, viewbox_size)
    xmax = _x_aspect(right_margin + width, aspect, viewbox_size)
    ymax = _y_aspect(top_margin + height, aspect, viewbox_size)
    new_box = Rect(xmin, ymin, xmax - xmin, ymax - ymin)

    affine = Affine2D.rect_to_rect(current_box, new_box)

    _picosvg_transform(svg, affine)

    square_viewbox = Rect(0, 0, viewbox_size, viewbox_size)
    svg.svg_root.attrib["viewBox"] = " ".join(ntos(v) for v in square_viewbox)
    for attr_name in ("width", "height"):
        if attr_name in svg.svg_root.attrib:
            del svg.svg_root.attrib[attr_name]
コード例 #21
0
ファイル: color_glyph.py プロジェクト: mavit/nanoemoji
def map_viewbox_to_otsvg_emsquare(view_box: Rect, upem: int) -> Affine2D:
    x_scale, y_scale = _scale_viewbox_to_emsquare(view_box, upem)
    dx, dy = _shift_origin_0_0(view_box, x_scale, y_scale)

    # shift so things are in the right place
    dy = dy - upem
    return Affine2D(x_scale, 0, 0, y_scale, dx, dy)
コード例 #22
0
    def correct_out_of_range_radii(self) -> "EllipticalArc":
        # Check if the radii are big enough to draw the arc, scale radii if not.
        # http://www.w3.org/TR/SVG/implnote.html#ArcCorrectionOutOfRangeRadii
        if self.is_straight_line() or self.is_zero_length():
            return self

        mid_point_distance = (self.start_point - self.end_point) * 0.5

        # SVG rotation is expressed in degrees, whereas Affin2D.rotate uses radians
        angle = radians(self.rotation)
        point_transform = Affine2D.identity().rotate(-angle)

        transformed_mid_point = point_transform.map_vector(mid_point_distance)
        rx = self.rx
        ry = self.ry
        square_rx = rx * rx
        square_ry = ry * ry
        square_x = transformed_mid_point.x * transformed_mid_point.x
        square_y = transformed_mid_point.y * transformed_mid_point.y

        radii_scale = square_x / square_rx + square_y / square_ry
        if radii_scale > 1:
            rx *= sqrt(radii_scale)
            ry *= sqrt(radii_scale)
            return self._replace(rx=rx, ry=ry)

        return self
コード例 #23
0
 def gettransform(self) -> Affine2D:
     return (
         Affine2D.identity()
         .translate(self.center[0], self.center[1])
         .skew(-radians(self.xSkewAngle), radians(self.ySkewAngle))
         .translate(-self.center[0], -self.center[1])
     )
コード例 #24
0
 def gettransform(self) -> Affine2D:
     return (
         Affine2D.identity()
         .translate(self.center[0], self.center[1])
         .scale(self.scale)
         .translate(-self.center[0], -self.center[1])
     )
コード例 #25
0
def _define_linear_gradient(
        svg_defs: etree.Element,
        paint: PaintLinearGradient,
        transform: Affine2D = Affine2D.identity(),
) -> str:
    gradient = etree.SubElement(svg_defs, "linearGradient")
    gradient_id = gradient.attrib["id"] = f"g{len(svg_defs)}"

    p0, p1, p2 = paint.p0, paint.p1, paint.p2
    # P2 allows to rotate the linear gradient independently of the end points P0 and P1.
    # Below we compute P3 which is the orthogonal projection of P1 onto a line passing
    # through P0 and perpendicular to the "normal" or "rotation vector" from P0 and P2.
    # The vector P3-P0 is the "effective" linear gradient vector after this rotation.
    # When vector P2-P0 is perpendicular to the gradient vector P1-P0, then P3
    # (projection of P1 onto perpendicular to normal) is == P1 itself thus no rotation.
    # When P2 is collinear to the P1-P0 gradient vector, then this projected P3 == P0
    # and the gradient degenerates to a solid paint (the last color stop).
    p3 = p0 + (p1 - p0).projection((p2 - p0).perpendicular())

    x1, y1 = p0
    x2, y2 = p3
    gradient.attrib["x1"] = _ntos(x1)
    gradient.attrib["y1"] = _ntos(y1)
    gradient.attrib["x2"] = _ntos(x2)
    gradient.attrib["y2"] = _ntos(y2)

    _apply_gradient_common_parts(gradient, paint, transform)

    return gradient_id
コード例 #26
0
ファイル: naive_warp.py プロジェクト: rsheeter/warp
def apply_viewbox_preserve_aspect_ratio(svg):
    """If viewport != viewBox apply the resulting transform and remove viewBox.
    Takes 'preserveAspectRatio' into account.
    E.g. The Qatar flag (QA.svg) needs this treatment.
    """
    svg_root = svg.svg_root
    width = svg_root.attrib.get("width")
    height = svg_root.attrib.get("height")
    if width is not None and height is not None and "viewBox" in svg_root.attrib:
        # ignore absolute length units; we're only interested in the relative size
        # of viewport vs viewbox here
        width = SVG_UNITS_RE.sub("", width)
        height = SVG_UNITS_RE.sub("", height)
        viewport = Rect(0, 0, float(width), float(height))
        viewbox = svg.view_box()
        if viewport != viewbox:
            transform = Affine2D.rect_to_rect(
                viewbox,
                viewport,
                svg_root.attrib.get("preserveAspectRatio", "xMidYMid"),
            )
            _picosvg_transform(svg, transform)
            del svg_root.attrib["viewBox"]
            if "preserveAspectRatio" in svg_root.attrib:
                del svg_root.attrib["preserveAspectRatio"]
コード例 #27
0
ファイル: svg_reuse.py プロジェクト: yisibl/picosvg
def normalize(shape: SVGShape, tolerance: float) -> SVGShape:
    """Build a version of shape that will compare == to other shapes even if offset,
    scaled, rotated, etc.

    Intended use is to normalize multiple shapes to identify opportunity for reuse."""

    path = _affine_friendly(dataclasses.replace(shape, id=""))

    # Make path relative, with first coord at 0,0
    x, y = _first_move(path)
    path.move(-x, -y, inplace=True)

    # Normlize vector 1 to [1 0]; eliminates rotation and uniform scaling
    vec1 = _nth_vector(path, 1)  # ignore M 0,0
    affine1 = _affine_vec2vec(vec1, Vector(1, 0))
    path.walk(lambda *args: _affine_callback(affine1, *args))

    # Scale first y movement to 1.0
    vecy = _first_y(_vectors(path), tolerance)
    if vecy and not almost_equal(vecy.y, 1.0):
        affine2 = Affine2D.identity().scale(1, 1 / vecy.y)
        path.walk(lambda *args: _affine_callback(affine2, *args))

    # TODO: what if shapes are the same but different start point
    # TODO: what if shapes are the same but different drawing cmds
    # This DOES happen in Noto; extent unclear

    path.round_multiple(tolerance, inplace=True)
    return path
コード例 #28
0
ファイル: svg_reuse.py プロジェクト: yisibl/picosvg
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
コード例 #29
0
def test_addComponent_decompose_with_transform():
    pen = SVGPathPen(glyphSet={"a": DummyGlyph()})
    pen.addComponent("a", Affine2D(2, 0, 0, 2, 0, 0))

    assert pen.path.d == (
        "M0,0 L0,20 L20,20 L20,0 Z "
        "M0,30 C0,40 20,40 20,30 Z "
        "M0,-10 Q0,-16 3,-18 Q6,-20 10,-20 Q14,-20 17,-18 Q20,-16 20,-10")
コード例 #30
0
def test_addComponent_decompose():
    pen = SVGPathPen(glyphSet={"a": DummyGlyph()})
    pen.addComponent("a", Affine2D.identity())

    assert pen.path.d == (
        "M0,0 L0,10 L10,10 L10,0 Z "
        "M0,15 C0,20 10,20 10,15 Z "
        "M0,-5 Q0,-8 1.5,-9 Q3,-10 5,-10 Q7,-10 8.5,-9 Q10,-8 10,-5")