Beispiel #1
0
def _parse_linear_gradient(grad_el, shape_bbox, view_box, upem):
    width, height = _get_gradient_units_relative_scale(grad_el, view_box)

    x1 = _number_or_percentage(grad_el.attrib.get("x1", "0%"), width)
    y1 = _number_or_percentage(grad_el.attrib.get("y1", "0%"), height)
    x2 = _number_or_percentage(grad_el.attrib.get("x2", "100%"), width)
    y2 = _number_or_percentage(grad_el.attrib.get("y2", "0%"), height)

    p0 = Point(x1, y1)
    p1 = Point(x2, y2)

    # compute the vector n perpendicular to vector v from P1 to P0
    v = p0 - p1
    n = v.perpendicular()

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

    # apply transformations to points and perpendicular vector
    p0 = transform.map_point(p0)
    p1 = transform.map_point(p1)
    n = transform.map_vector(n)

    # P2 is equal to P1 translated by the orthogonal projection of the transformed
    # vector v' (from P1' to P0') onto the transformed vector n'; before the
    # transform the vector n is perpendicular to v, so the projection of v onto n
    # is zero and P2 == P1; if the transform has a skew or a scale that doesn't
    # preserve aspect ratio, the projection of v' onto n' is non-zero and P2 != P1
    v = p0 - p1
    p2 = p1 + v.projection(n)

    return {"p0": p0, "p1": p1, "p2": p2}
Beispiel #2
0
def cubic_deriv(p0, p1, p2, p3):
    # derivative is a quad
    return (
        Point(3 * (p1[0] - p0[0]), 3 * (p1[1] - p0[1])),
        Point(3 * (p2[0] - p1[0]), 3 * (p2[1] - p1[1])),
        Point(3 * (p3[0] - p2[0]), 3 * (p3[1] - p2[1])),
    )
Beispiel #3
0
def _gradient_paint(ttfont: ttLib.TTFont,
                    ot_paint: otTables.Paint) -> _GradientPaint:
    stops = tuple(
        ColorStop(
            stop.StopOffset,
            _color(ttfont, stop.PaletteIndex, stop.Alpha),
        ) for stop in ot_paint.ColorLine.ColorStop)
    extend = Extend((ot_paint.ColorLine.Extend, ))
    if ot_paint.Format == PaintLinearGradient.format:
        return PaintLinearGradient(
            stops=stops,
            extend=extend,
            p0=Point(ot_paint.x0, ot_paint.y0),
            p1=Point(ot_paint.x1, ot_paint.y1),
            p2=Point(ot_paint.x2, ot_paint.y2),
        )
    elif ot_paint.Format == PaintRadialGradient.format:
        return PaintRadialGradient(
            stops=stops,
            extend=extend,
            c0=Point(ot_paint.x0, ot_paint.y0),
            c1=Point(ot_paint.x1, ot_paint.y1),
            r0=ot_paint.r0,
            r1=ot_paint.r1,
        )
    else:
        raise ValueError(
            f"Expected one of Paint formats {_GRADIENT_PAINT_FORMATS}; "
            f"found {ot_paint.Format}")
Beispiel #4
0
def _parse_linear_gradient(
    config: FontConfig,
    grad_el: etree.Element,
    shape_bbox: Rect,
    view_box: Rect,
    glyph_width: int,
    shape_opacity: float = 1.0,
):
    gradient = SVGLinearGradient.from_element(grad_el, view_box)

    p0 = Point(gradient.x1, gradient.y1)
    p1 = Point(gradient.x2, gradient.y2)

    # Set P2 to P1 rotated 90 degrees counter-clockwise around P0
    p2 = p0 + (p1 - p0).perpendicular()

    common_args = _common_gradient_parts(grad_el, shape_opacity)

    transform = _get_gradient_transform(
        config, grad_el, shape_bbox, view_box, glyph_width
    )

    return PaintLinearGradient(  # pytype: disable=wrong-arg-types
        p0=p0, p1=p1, p2=p2, **common_args
    ).apply_transform(transform)
Beispiel #5
0
def _parse_radial_gradient(
    config: FontConfig,
    grad_el: etree.Element,
    shape_bbox: Rect,
    view_box: Rect,
    glyph_width: int,
    shape_opacity: float = 1.0,
):
    gradient = SVGRadialGradient.from_element(grad_el, view_box)

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

    gradient_args = {"c0": c0, "c1": c1, "r0": r0, "r1": r1}
    gradient_args.update(_common_gradient_parts(grad_el, shape_opacity))

    transform = _get_gradient_transform(
        config, grad_el, shape_bbox, view_box, glyph_width
    )

    return PaintRadialGradient(  # pytype: disable=wrong-arg-types
        **gradient_args
    ).apply_transform(transform)
Beispiel #6
0
class PaintLinearGradient(Paint):
    format: ClassVar[int] = 2
    extend: Extend = Extend.PAD
    stops: Tuple[ColorStop, ...] = tuple()
    p0: Point = Point()
    p1: Point = Point()
    p2: Point = None  # if undefined, default to p1

    def __post_init__(self):
        # use object.__setattr__ as the dataclass is frozen
        if self.p2 is None:
            object.__setattr__(self, "p2", self.p1)

    def colors(self):
        for stop in self.stops:
            yield stop.color

    def to_ufo_paint(self, colors):
        return {
            "format": self.format,
            "colorLine": _ufoColorLine(self, colors),
            "p0": self.p0,
            "p1": self.p1,
            "p2": self.p2,
        }
Beispiel #7
0
class PaintRadialGradient(Paint):
    format: ClassVar[int] = 3
    extend: Extend = Extend.PAD
    stops: Tuple[ColorStop] = tuple()
    c0: Point = Point()
    c1: Point = Point()
    r0: float = 0.0
    r1: float = 0.0
    affine2x2: Optional[Tuple[float, float, float, float]] = None

    def colors(self):
        for stop in self.stops:
            yield stop.color

    def to_ufo_paint(self, colors):
        result = {
            "format": self.format,
            "colorLine": _ufoColorLine(self, colors),
            "c0": self.c0,
            "c1": self.c1,
            "r0": self.r0,
            "r1": self.r1,
        }
        if self.affine2x2:
            result["transform"] = self.affine2x2
        return result
Beispiel #8
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
Beispiel #9
0
class PaintLinearGradient(Paint):
    format: ClassVar[int] = int(ot.PaintFormat.PaintLinearGradient)
    extend: Extend = Extend.PAD
    stops: Tuple[ColorStop, ...] = tuple()
    p0: Point = Point()
    p1: Point = Point()
    p2: Point = None  # if normal undefined, default to P1 rotated 90° cc'wise

    def __post_init__(self):
        if self.p2 is None:
            p0, p1 = Point(*self.p0), Point(*self.p1)
            # use object.__setattr__ as the dataclass is frozen
            object.__setattr__(self, "p2", p0 + (p1 - p0).perpendicular())

    def colors(self):
        for stop in self.stops:
            yield stop.color

    def to_ufo_paint(self, colors):
        return {
            "Format": self.format,
            "ColorLine": _ufoColorLine(self, colors),
            "x0": self.p0[0],
            "y0": self.p0[1],
            "x1": self.p1[0],
            "y1": self.p1[1],
            "x2": self.p2[0],
            "y2": self.p2[1],
        }

    def check_overflows(self) -> "PaintLinearGradient":
        for i in range(3):
            attr_name = f"p{i}"
            value = getattr(self, attr_name)
            for j in range(2):
                if not (MIN_INT16 <= value[j] <= MAX_INT16):
                    raise OverflowError(
                        f"{self.__class__.__name__}.{attr_name}[{j}] ({value[j]}) is "
                        f"out of bounds: [{MIN_INT16}...{MAX_INT16}]"
                    )
        return self

    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()
Beispiel #10
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))
Beispiel #11
0
class PaintScaleAroundCenter(Paint):
    format: ClassVar[int] = int(ot.PaintFormat.PaintScaleAroundCenter)
    paint: Paint
    scaleX: float = 1.0
    scaleY: float = 1.0
    center: Point = Point()

    def colors(self):
        yield from self.paint.colors()

    def to_ufo_paint(self, colors):
        paint = {
            "Format": self.format,
            "Paint": self.paint.to_ufo_paint(colors),
            "scaleX": self.scaleX,
            "scaleY": self.scaleY,
            "centerX": self.center[0],
            "centerY": self.center[1],
        }
        return paint

    def children(self) -> Iterable[Paint]:
        return (self.paint,)

    def gettransform(self) -> Affine2D:
        return (
            Affine2D.identity()
            .translate(self.center[0], self.center[1])
            .scale(self.scaleX, self.scaleY)
            .translate(-self.center[0], -self.center[1])
        )
Beispiel #12
0
class PaintRotateAroundCenter(Paint):
    format: ClassVar[int] = int(ot.PaintFormat.PaintRotateAroundCenter)
    paint: Paint
    angle: float = 0.0
    center: Point = Point()

    def colors(self):
        yield from self.paint.colors()

    def to_ufo_paint(self, colors):
        paint = {
            "Format": self.format,
            "Paint": self.paint.to_ufo_paint(colors),
            "angle": self.angle,
            "centerX": self.center[0],
            "centerY": self.center[1],
        }
        return paint

    def children(self) -> Iterable[Paint]:
        return (self.paint,)

    def gettransform(self) -> Affine2D:
        return Affine2D.identity().rotate(
            radians(self.angle), self.center[0], self.center[1]
        )
Beispiel #13
0
class PaintSkewAroundCenter(Paint):
    format: ClassVar[int] = int(ot.PaintFormat.PaintSkewAroundCenter)
    paint: Paint
    xSkewAngle: float = 0.0
    ySkewAngle: float = 0.0
    center: Point = Point()

    def colors(self):
        yield from self.paint.colors()

    def to_ufo_paint(self, colors):
        paint = {
            "Format": self.format,
            "Paint": self.paint.to_ufo_paint(colors),
            "xSkewAngle": self.xSkewAngle,
            "ySkewAngle": self.ySkewAngle,
            "centerX": self.center[0],
            "centerY": self.center[1],
        }
        return paint

    def children(self) -> Iterable[Paint]:
        return (self.paint,)

    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])
        )
Beispiel #14
0
        def arc_to_cubic_callback(subpath_start, curr_pos, cmd, args, *_):
            del subpath_start
            if cmd not in {"a", "A"}:
                # no work to do
                return ((cmd, args), )

            (rx, ry, x_rotation, large, sweep, end_x, end_y) = args

            if cmd == "a":
                end_x += curr_pos.x
                end_y += curr_pos.y
            end_pt = Point(end_x, end_y)

            result = []
            for p1, p2, target in arc_to_cubic(curr_pos, rx, ry, x_rotation,
                                               large, sweep, end_pt):
                x, y = target
                if p1 is not None:
                    assert p2 is not None
                    x1, y1 = p1
                    x2, y2 = p2
                    result.append(("C", (x1, y1, x2, y2, x, y)))
                else:
                    result.append(("L", (x, y)))

            return tuple(result)
Beispiel #15
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
Beispiel #16
0
def test_point_subtraction_and_addition():
    p0 = Point(1, 3)
    p1 = Point(-2, 4)

    v = p1 - p0

    assert isinstance(v, Vector)
    assert v.x == -3
    assert v.y == 1

    p2 = p1 - v

    assert isinstance(p2, Point)
    assert p2 == p0

    p3 = p0 + v
    assert isinstance(p3, Point)
    assert p3 == p1
Beispiel #17
0
def cubic_tangent(t, p0, p1, p2, p3) -> Vector:
    # Returns the unit vector defining the cubic bezier curve's direction at t
    if t == 0.0:
        tangent = Point(*p1) - Point(*p0)
    elif t == 1.0:
        tangent = Point(*p3) - Point(*p2)
    else:
        tangent = Vector(*cubic_deriv_pos(t, p0, p1, p2, p3))
    tangent = tangent.unit()
    if tangent is not None:
        return tangent
    return Vector()
Beispiel #18
0
def _cubic_callback(subpath_start, curr_xy, cmd, args, prev_xy, prev_cmd,
                    prev_args):
    if cmd.upper() == "M":
        return ((cmd, args), )

    # Convert to cubic if needed
    if cmd.upper() == "Z":
        # a line back to subpath start ... unless we are there already
        if curr_xy == subpath_start:
            return ()
        cmd = "L"
        args = subpath_start

    if cmd == "L":
        # line from curr_xy to args
        assert len(args) == 2
        end_xy = args

        # cubic ctl points 1/3rd and 2/3rds along
        cmd = "C"
        args = (
            *line_pos(0.33, curr_xy, end_xy),
            *line_pos(0.66, curr_xy, end_xy),
            *end_xy,
        )

    if cmd == "Q":
        assert len(args) == 4
        cmd = "C"
        p0 = Point(*curr_xy)
        p1 = Point(*args[0:2])
        p2 = Point(*args[2:4])
        args = (*(p0 + 2 / 3 * (p1 - p0)), *(p2 + 2 / 3 * (p1 - p2)), *p2)

    if cmd != "C":
        raise ValueError(f"How do you cubic {cmd}, {args}")

    assert len(args) == 6

    return (("C", args), )
Beispiel #19
0
def arc_to_cubic(
    start_point: Tuple[float, float],
    rx: float,
    ry: float,
    rotation: float,
    large: int,
    sweep: int,
    end_point: Tuple[float, float],
) -> Iterator[Tuple[Optional[Point], Optional[Point], Point]]:
    """Convert arc to cubic(s).

    start/end point are (x,y) tuples with absolute coordinates.
    See https://skia.org/user/api/SkPath_Reference#SkPath_arcTo_4
    Note in particular:
        SVG sweep-flag value is opposite the integer value of sweep;
        SVG sweep-flag uses 1 for clockwise, while kCW_Direction cast to int is zero.

    Yields 3-tuples of Points for each Cubic bezier, i.e. two off-curve points and
    one on-curve end point.

    If either rx or ry is 0, the arc is treated as a straight line joining the end
    points, and a (None, None, arc.end_point) tuple is yielded.

    Yields empty iterator if arc has zero length.
    """
    if not isinstance(start_point, Point):
        start_point = Point(*start_point)
    if not isinstance(end_point, Point):
        end_point = Point(*end_point)

    arc = EllipticalArc(start_point, rx, ry, rotation, large, sweep, end_point)
    if arc.is_zero_length():
        return
    elif arc.is_straight_line():
        yield None, None, arc.end_point
    else:
        yield from _arc_to_cubic(arc)
Beispiel #20
0
def _is_almost_line(curve, flatness=1.0):
    # Returns True if the bezier curve is equivalent to a line.
    # A 'flat' bezier curve is one such that the sum of the distances between
    # consecutive control points equals the distance from start to end points.
    # That's because if a control point isn't on the line from start to end then
    # the length would exceed the direct path start => end.
    # The 'flatness' factor of 1.0 means exactly flat, anything greater than 1.0
    # proportionately means flat "enough". Less than 1.0 means never flat (i.e.
    # keep all flat curves as curves, FWIW).
    points = [Point(*p) for p in curve]
    length = 0
    for i in range(len(curve) - 1):
        length += (points[i + 1] - points[i]).norm()
    max_length = (points[-1] - points[0]).norm()
    return length <= flatness * max_length
Beispiel #21
0
def _next_pos(curr_pos, cmd, cmd_args):
    # update current position
    x_coord_idxs, y_coord_idxs = svg_meta.cmd_coords(cmd)
    new_x, new_y = curr_pos
    if cmd.isupper():
        if x_coord_idxs:
            new_x = 0
        if y_coord_idxs:
            new_y = 0

    if x_coord_idxs:
        new_x += cmd_args[x_coord_idxs[-1]]
    if y_coord_idxs:
        new_y += cmd_args[y_coord_idxs[-1]]

    return Point(new_x, new_y)
Beispiel #22
0
        def expand_shorthand_callback(_, curr_pos, cmd, args, prev_pos,
                                      prev_cmd, prev_args):
            short_to_long = {"S": "C", "T": "Q"}
            if not cmd.upper() in short_to_long:
                return ((cmd, args), )

            if cmd.islower():
                cmd, args = _relative_to_absolute(curr_pos, cmd, args)

            # if there is no prev, or a bad prev, control point coincident current
            new_cp = (curr_pos.x, curr_pos.y)
            if prev_cmd:
                if prev_cmd.islower():
                    prev_cmd, prev_args = _relative_to_absolute(
                        prev_pos, prev_cmd, prev_args)
                if prev_cmd in short_to_long.values():
                    # reflect 2nd-last x,y pair over curr_pos and make it our first arg
                    prev_cp = Point(prev_args[-4], prev_args[-3])
                    new_cp = (2 * curr_pos.x - prev_cp.x,
                              2 * curr_pos.y - prev_cp.y)

            return ((short_to_long[cmd], new_cp + args), )
Beispiel #23
0
    def walk(self, callback):
        """Walk path and call callback to build potentially new commands.

        https://www.w3.org/TR/SVG11/paths.html

        def callback(subpath_start, curr_xy, cmd, args, prev_xy, prev_cmd, prev_args)
          prev_* None if there was no previous
          returns sequence of (new_cmd, new_args) that replace cmd, args
        """
        curr_pos = Point()
        subpath_start_pos = curr_pos  # where a z will take you
        new_cmds = []

        # iteration gives us exploded commands
        for idx, (cmd, args) in enumerate(self):
            svg_meta.check_cmd(cmd, args)
            if idx == 0 and cmd == "m":
                cmd = "M"

            prev = (None, None, None)
            if new_cmds:
                prev = new_cmds[-1]
            for (new_cmd, new_cmd_args) in callback(subpath_start_pos,
                                                    curr_pos, cmd, args,
                                                    *prev):
                if new_cmd.lower() != "z":
                    next_pos = _next_pos(curr_pos, new_cmd, new_cmd_args)
                else:
                    next_pos = subpath_start_pos

                prev_pos, curr_pos = curr_pos, next_pos
                if new_cmd.upper() == "M":
                    subpath_start_pos = curr_pos
                new_cmds.append((prev_pos, new_cmd, new_cmd_args))

        self.d = ""
        for _, cmd, args in new_cmds:
            self._add_cmd(cmd, *args)
Beispiel #24
0
def _colr_v1_glyph_to_svg(ttfont: ttLib.TTFont, view_box: Rect,
                          glyph: otTables.BaseGlyphRecord) -> etree.Element:
    glyph_set = ttfont.getGlyphSet()
    svg_root = _svg_root(view_box)
    defs = svg_root[0]
    for glyph_layer in glyph.LayerV1List.LayerV1Record:
        svg_path = etree.SubElement(svg_root, "path")

        # TODO care about variations, such as for alpha
        paint = glyph_layer.Paint
        if paint.Format == _PAINT_SOLID:
            _solid_paint(svg_path, ttfont, paint.Color.PaletteIndex,
                         paint.Color.Alpha.value)
        elif paint.Format == _PAINT_LINEAR_GRADIENT:
            _linear_gradient_paint(
                defs,
                svg_path,
                ttfont,
                view_box,
                stops=[
                    _ColorStop(
                        stop.StopOffset.value,
                        stop.Color.PaletteIndex,
                        stop.Color.Alpha.value,
                    ) for stop in paint.ColorLine.ColorStop
                ],
                extend=Extend((paint.ColorLine.Extend.value, )),
                p0=Point(paint.x0.value, paint.y0.value),
                p1=Point(paint.x1.value, paint.y1.value),
                p2=Point(paint.x2.value, paint.y2.value),
            )
        elif paint.Format == _PAINT_RADIAL_GRADIENT:
            _radial_gradient_paint(
                defs,
                svg_path,
                ttfont,
                view_box,
                stops=[
                    _ColorStop(
                        stop.StopOffset.value,
                        stop.Color.PaletteIndex,
                        stop.Color.Alpha.value,
                    ) for stop in paint.ColorLine.ColorStop
                ],
                extend=Extend((paint.ColorLine.Extend.value, )),
                c0=Point(paint.x0.value, paint.y0.value),
                c1=Point(paint.x1.value, paint.y1.value),
                r0=paint.r0.value,
                r1=paint.r1.value,
                transform=(Affine2D.identity()
                           if not paint.Transform else Affine2D(
                               paint.Transform.xx.value,
                               paint.Transform.xy.value,
                               paint.Transform.yx.value,
                               paint.Transform.yy.value,
                               0,
                               0,
                           )),
            )

        _draw_svg_path(svg_path, view_box, ttfont, glyph_layer.LayerGlyph,
                       glyph_set)

    return svg_root
Beispiel #25
0
 def test_add_vec(self):
     assert Vector(1, 2) + Vector(2, 3) == Vector(3, 5)
     assert Vector(1, 2) + Point(2, 3) == Point(3, 5)
     assert Point(2, 3) + Vector(1, 2) == Point(3, 5)
Beispiel #26
0
def _mid_point(p1, p2):
    x1, y1 = p1
    x2, y2 = p2
    return Point(x1, y1) + (Point(x2, y2) - Point(x1, y1)) * 0.5
Beispiel #27
0
 def map_point(self, pt: Tuple[float, float]) -> Point:
     """Return Point (x, y) multiplied by Affine2D."""
     x, y = pt
     return Point(self.a * x + self.c * y + self.e,
                  self.b * x + self.d * y + self.f)
Beispiel #28
0
class PaintRadialGradient(Paint):
    format: ClassVar[int] = int(ot.PaintFormat.PaintRadialGradient)
    extend: Extend = Extend.PAD
    stops: Tuple[ColorStop, ...] = tuple()
    c0: Point = Point()
    c1: Point = Point()
    r0: float = 0.0
    r1: float = 0.0

    def colors(self):
        for stop in self.stops:
            yield stop.color

    def to_ufo_paint(self, colors):
        paint = {
            "Format": self.format,
            "ColorLine": _ufoColorLine(self, colors),
            "x0": self.c0[0],
            "y0": self.c0[1],
            "r0": self.r0,
            "x1": self.c1[0],
            "y1": self.c1[1],
            "r1": self.r1,
        }
        return paint

    def check_overflows(self) -> "PaintRadialGradient":
        int_bounds = {
            "c": (MIN_INT16, MAX_INT16),
            "r": (MIN_UINT16, MAX_UINT16),
        }
        attrs = []
        for prefix in ("c", "r"):
            for i in range(2):
                attr_name = f"{prefix}{i}"
                value = getattr(self, attr_name)
                if prefix == "c":
                    attrs.extend((f"{attr_name}[{j}]", value[j]) for j in range(2))
                else:
                    attrs.append((f"{attr_name}", value))
        for attr_name, value in attrs:
            min_value, max_value = int_bounds[attr_name[0]]
            if not (min_value <= value <= max_value):
                raise OverflowError(
                    f"{self.__class__.__name__}.{attr_name} ({value}) is "
                    f"out of bounds: [{min_value}...{max_value}]"
                )
        return self

    def apply_transform(self, transform: Affine2D) -> Paint:
        # if gradientUnits="objectBoundingBox" and the bbox is not square, or there's some
        # gradientTransform, we may end up with a transformation that does not keep the
        # aspect ratio of the gradient circles and turns them into ellipses, but CORLv1
        # PaintRadialGradient by itself can only define circles. Thus we only apply the
        # uniform scale and translate components of the original transform to the circles,
        # then encode any remaining non-uniform transformation as a COLRv1 transform
        # that wraps the PaintRadialGradient (see further below).
        uniform_transform, remaining_transform = _decompose_uniform_transform(transform)

        c0 = uniform_transform.map_point(self.c0)
        c1 = uniform_transform.map_point(self.c1)

        sx, _ = uniform_transform.getscale()
        r0 = self.r0 * sx
        r1 = self.r1 * sx

        # TODO handle degenerate cases, fallback to solid, w/e
        return transformed(
            remaining_transform,
            dataclasses.replace(self, c0=c0, c1=c1, r0=r0, r1=r1).check_overflows(),
        )
Beispiel #29
0
 def __post_init__(self):
     if self.p2 is None:
         p0, p1 = Point(*self.p0), Point(*self.p1)
         # use object.__setattr__ as the dataclass is frozen
         object.__setattr__(self, "p2", p0 + (p1 - p0).perpendicular())