Ejemplo n.º 1
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))
Ejemplo 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)
Ejemplo n.º 3
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
Ejemplo n.º 4
0
def _apply_gradient_common_parts(
        gradient: etree.Element,
        paint: _GradientPaint,
        transform: Affine2D = Affine2D.identity(),
):
    gradient.attrib["gradientUnits"] = "userSpaceOnUse"
    for stop in paint.stops:
        stop_el = etree.SubElement(gradient, "stop")
        stop_el.attrib["offset"] = _ntos(stop.stopOffset)
        stop_el.attrib["stop-color"] = stop.color.opaque().to_string()
        if stop.color.alpha != 1.0:
            stop_el.attrib["stop-opacity"] = _ntos(stop.color.alpha)
    if paint.extend != Extend.PAD:
        gradient.attrib["spreadMethod"] = paint.extend.name.lower()

    transform = transform.round(_DEFAULT_ROUND_NDIGITS)
    if transform != Affine2D.identity():
        # Safari has a bug which makes it reject a gradient if gradientTransform
        # contains an 'involutory matrix' (i.e. matrix whose inverse equals itself,
        # such that M @ M == Identity, e.g. reflection), hence the following hack:
        # https://github.com/googlefonts/nanoemoji/issues/268
        # https://en.wikipedia.org/wiki/Involutory_matrix
        # TODO: Remove once the bug gets fixed
        if transform @ transform == Affine2D.identity():
            transform = transform._replace(a=transform.a + 0.00001)
            assert transform.inverse() != transform
        gradient.attrib["gradientTransform"] = transform.tostring()
Ejemplo n.º 5
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
Ejemplo n.º 6
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))
Ejemplo n.º 7
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
Ejemplo n.º 8
0
    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"]
Ejemplo n.º 9
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])
     )
Ejemplo n.º 10
0
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
Ejemplo n.º 11
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
Ejemplo n.º 12
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
Ejemplo n.º 13
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])
     )
Ejemplo n.º 14
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")
Ejemplo n.º 15
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
Ejemplo n.º 16
0
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
Ejemplo n.º 17
0
 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[self.glyph_name].width,
         self.user_transform,
     )
Ejemplo n.º 18
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)
Ejemplo n.º 19
0
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
Ejemplo n.º 20
0
def _define_gradient(
        svg_defs: etree.Element,
        paint: _GradientPaint,
        transform: Affine2D = Affine2D.identity(),
) -> str:
    if isinstance(paint, PaintLinearGradient):
        return _define_linear_gradient(svg_defs, paint, transform)
    elif isinstance(paint, PaintRadialGradient):
        return _define_radial_gradient(svg_defs, paint, transform)
    else:
        raise TypeError(type(paint))
Ejemplo n.º 21
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]
Ejemplo n.º 22
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})"
Ejemplo n.º 23
0
def _create_use_element(svg: SVG, parent_el: etree.Element,
                        reuse_result: ReuseResult) -> etree.Element:
    svg_use = etree.SubElement(parent_el, "use", nsmap=svg.svg_root.nsmap)
    svg_use.attrib[_XLINK_HREF_ATTR_NAME] = f"#{reuse_result.glyph_name}"
    transform = reuse_result.transform
    tx, ty = transform.gettranslate()
    if tx:
        svg_use.attrib["x"] = _ntos(tx)
    if ty:
        svg_use.attrib["y"] = _ntos(ty)
    transform = transform.translate(-tx, -ty)
    if transform != Affine2D.identity():
        svg_use.attrib["transform"] = _svg_matrix(transform)
    return svg_use
Ejemplo n.º 24
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
Ejemplo n.º 25
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
Ejemplo n.º 26
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, )
Ejemplo n.º 27
0
def _arc_to_cubic(arc: EllipticalArc) -> Iterator[Tuple[Point, Point, Point]]:
    arc = arc.correct_out_of_range_radii()
    arc_params = arc.end_to_center_parametrization()

    point_transform = (
        Affine2D.identity()
        .translate(arc_params.center_point.x, arc_params.center_point.y)
        .rotate(radians(arc.rotation))
        .scale(arc.rx, arc.ry)
    )

    # Some results of atan2 on some platform implementations are not exact
    # enough. So that we get more cubic curves than expected here. Adding 0.001f
    # reduces the count of sgements to the correct count.
    num_segments = int(ceil(fabs(arc_params.theta_arc / (PI_OVER_TWO + 0.001))))
    for i in range(num_segments):
        start_theta = arc_params.theta1 + i * arc_params.theta_arc / num_segments
        end_theta = arc_params.theta1 + (i + 1) * arc_params.theta_arc / num_segments

        t = (4 / 3) * tan(0.25 * (end_theta - start_theta))
        if not isfinite(t):
            return

        sin_start_theta = sin(start_theta)
        cos_start_theta = cos(start_theta)
        sin_end_theta = sin(end_theta)
        cos_end_theta = cos(end_theta)

        point1 = Point(
            cos_start_theta - t * sin_start_theta, sin_start_theta + t * cos_start_theta
        )
        end_point = Point(cos_end_theta, sin_end_theta)
        point2 = end_point + Vector(t * sin_end_theta, -t * cos_end_theta)

        point1 = point_transform.map_point(point1)
        point2 = point_transform.map_point(point2)

        # by definition, the last bezier's end point == the arc end point
        # by directly taking the end point we avoid floating point imprecision
        if i == num_segments - 1:
            end_point = arc.end_point
        else:
            end_point = point_transform.map_point(end_point)

        yield point1, point2, end_point
Ejemplo n.º 28
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
                 )
             )
Ejemplo n.º 29
0
def _add_glyph(svg: SVG, color_glyph: ColorGlyph, reuse_cache: ReuseCache):
    # each glyph gets a group of its very own
    svg_g = svg.append_to("/svg:svg", etree.Element("g"))
    svg_g.attrib["id"] = f"glyph{color_glyph.glyph_id}"
    # https://github.com/googlefonts/nanoemoji/issues/58: group needs transform
    svg_g.attrib["transform"] = _svg_matrix(
        color_glyph.transform_for_otsvg_space())

    # copy the shapes into our svg
    for painted_layer in color_glyph.as_painted_layers():
        view_box = color_glyph.picosvg.view_box()
        if view_box is None:
            raise ValueError(f"{color_glyph.filename} must declare view box")
        reuse_key = _inter_glyph_reuse_key(view_box, painted_layer)
        if reuse_key not in reuse_cache.shapes:
            el = to_element(painted_layer.path)
            match = regex.match(r"url\(#([^)]+)*\)", el.attrib.get("fill", ""))
            if match:
                el.attrib[
                    "fill"] = f"url(#{reuse_cache.old_to_new_id.get(match.group(1), match.group(1))})"
            svg_g.append(el)
            reuse_cache.shapes[reuse_key] = el
            for reuse in painted_layer.reuses:
                _ensure_has_id(el)
                svg_use = etree.SubElement(svg_g, "use")
                svg_use.attrib["href"] = f'#{el.attrib["id"]}'
                tx, ty = reuse.gettranslate()
                if tx:
                    svg_use.attrib["x"] = _ntos(tx)
                if ty:
                    svg_use.attrib["y"] = _ntos(ty)
                transform = reuse.translate(-tx, -ty)
                if transform != Affine2D.identity():
                    # TODO apply scale and rotation. Just slap a transform on the <use>?
                    raise NotImplementedError(
                        "TODO apply scale & rotation to use")

        else:
            el = reuse_cache.shapes[reuse_key]
            _ensure_has_id(el)
            svg_use = etree.SubElement(svg_g, "use")
            svg_use.attrib["href"] = f'#{el.attrib["id"]}'
Ejemplo n.º 30
0
def _colr0_layers(color_glyph: ColorGlyph, root: Paint,
                  palette: Sequence[Color]):
    # COLRv0: write out each PaintGlyph we see in it's first color
    # If we see a transformed glyph generate a component
    # Results for complex structures will be suboptimal :)
    ufo = color_glyph.ufo
    layers = []
    for context in root.breadth_first():
        if context.paint.format != PaintGlyph.format:  # pytype: disable=attribute-error
            continue
        paint_glyph: PaintGlyph = (context.paint)  # pytype: disable=annotation-type-mismatch
        color = next(paint_glyph.colors())
        glyph_name = paint_glyph.glyph

        if context.transform != Affine2D.identity():
            glyph_name = _create_transformed_glyph(color_glyph, paint_glyph,
                                                   context.transform).name

        layers.append((glyph_name, palette.index(color)))
    return layers