"""This code is taken from by Alexandre Carlier, Martin Danelljan, Alexandre Alahi and Radu Timofte from the paper >https://arxiv.org/pdf/2007.11301.pdf> """ from __future__ import annotations from .geom import * from src.preprocessing.deepsvg.deepsvg_difflib.tensor import SVGTensor from .util_fns import get_roots from enum import Enum import torch import math from typing import List, Union Num = Union[int, float] class SVGCmdEnum(Enum): MOVE_TO = "m" LINE_TO = "l" CUBIC_BEZIER = "c" CLOSE_PATH = "z" ELLIPTIC_ARC = "a" QUAD_BEZIER = "q" LINE_TO_HORIZONTAL = "h" LINE_TO_VERTICAL = "v" CUBIC_BEZIER_REFL = "s" QUAD_BEZIER_REFL = "t" svgCmdArgTypes = { SVGCmdEnum.MOVE_TO.value: [Point], SVGCmdEnum.LINE_TO.value: [Point], SVGCmdEnum.CUBIC_BEZIER.value: [Point, Point, Point], SVGCmdEnum.CLOSE_PATH.value: [], SVGCmdEnum.ELLIPTIC_ARC.value: [Radius, Angle, Flag, Flag, Point], SVGCmdEnum.QUAD_BEZIER.value: [Point, Point], SVGCmdEnum.LINE_TO_HORIZONTAL.value: [XCoord], SVGCmdEnum.LINE_TO_VERTICAL.value: [YCoord], SVGCmdEnum.CUBIC_BEZIER_REFL.value: [Point, Point], SVGCmdEnum.QUAD_BEZIER_REFL.value: [Point], } class SVGCommand: def __init__(self, command: SVGCmdEnum, args: List[Geom], start_pos: Point, end_pos: Point): self.command = command self.args = args self.start_pos = start_pos self.end_pos = end_pos def copy(self): raise NotImplementedError @staticmethod def from_str(cmd_str: str, args_str: List[Num], pos=None, initial_pos=None, prev_command: SVGCommand = None): if pos is None: pos = Point(0.) if initial_pos is None: initial_pos = Point(0.) cmd = SVGCmdEnum(cmd_str.lower()) # Implicit MoveTo commands are treated as LineTo if cmd is SVGCmdEnum.MOVE_TO and len(args_str) > 2: l_cmd_str = SVGCmdEnum.LINE_TO.value if cmd_str.isupper(): l_cmd_str = l_cmd_str.upper() l1, pos, initial_pos = SVGCommand.from_str(cmd_str, args_str[:2], pos, initial_pos) l2, pos, initial_pos = SVGCommand.from_str(l_cmd_str, args_str[2:], pos, initial_pos) return [*l1, *l2], pos, initial_pos nb_args = len(args_str) if cmd is SVGCmdEnum.CLOSE_PATH: assert nb_args == 0, f"Expected no argument for command {cmd_str}: {nb_args} given" return [SVGCommandClose(pos, initial_pos)], initial_pos, initial_pos expected_nb_args = sum([ArgType.num_args for ArgType in svgCmdArgTypes[cmd.value]]) assert nb_args % expected_nb_args == 0, f"Expected {expected_nb_args} arguments for command {cmd_str}: {nb_args} given" l = [] i = 0 for _ in range(nb_args // expected_nb_args): args = [] for ArgType in svgCmdArgTypes[cmd.value]: num_args = ArgType.num_args arg = ArgType(*args_str[i:i+num_args]) if cmd_str.islower(): arg.translate(pos) if isinstance(arg, Coord): arg = arg.to_point(pos) args.append(arg) i += num_args if cmd is SVGCmdEnum.LINE_TO or cmd is SVGCmdEnum.LINE_TO_VERTICAL or cmd is SVGCmdEnum.LINE_TO_HORIZONTAL: cmd_parsed = SVGCommandLine(pos, *args) elif cmd is SVGCmdEnum.MOVE_TO: cmd_parsed = SVGCommandMove(pos, *args) elif cmd is SVGCmdEnum.ELLIPTIC_ARC: cmd_parsed = SVGCommandArc(pos, *args) elif cmd is SVGCmdEnum.CUBIC_BEZIER: cmd_parsed = SVGCommandBezier(pos, *args) elif cmd is SVGCmdEnum.QUAD_BEZIER: cmd_parsed = SVGCommandBezier(pos, args[0], args[0], args[1]) elif cmd is SVGCmdEnum.QUAD_BEZIER_REFL or cmd is SVGCmdEnum.CUBIC_BEZIER_REFL: if isinstance(prev_command, SVGCommandBezier): control1 = pos * 2 - prev_command.control2 else: control1 = pos control2 = args[0] if cmd is SVGCmdEnum.CUBIC_BEZIER_REFL else control1 cmd_parsed = SVGCommandBezier(pos, control1, control2, args[-1]) prev_command = cmd_parsed pos = cmd_parsed.end_pos if cmd is SVGCmdEnum.MOVE_TO: initial_pos = pos l.append(cmd_parsed) return l, pos, initial_pos def __repr__(self): cmd = self.command.value.upper() return f"{cmd}{self.get_geoms()}" def to_str(self): cmd = self.command.value.upper() return f"{cmd}{' '.join([arg.to_str() for arg in self.args])}" def to_tensor(self, PAD_VAL=-1): raise NotImplementedError @staticmethod def from_tensor(vector: torch.Tensor): cmd_index, args = int(vector[0]), vector[1:] cmd = SVGCmdEnum(SVGTensor.COMMANDS_SIMPLIFIED[cmd_index]) radius = Radius(*args[:2].tolist()) x_axis_rotation = Angle(*args[2:3].tolist()) large_arc_flag = Flag(args[3].item()) sweep_flag = Flag(args[4].item()) start_pos = Point(*args[5:7].tolist()) control1 = Point(*args[7:9].tolist()) control2 = Point(*args[9:11].tolist()) end_pos = Point(*args[11:].tolist()) return SVGCommand.from_args(cmd, radius, x_axis_rotation, large_arc_flag, sweep_flag, start_pos, control1, control2, end_pos) @staticmethod def from_args(command: SVGCmdEnum, radius: Radius, x_axis_rotation: Angle, large_arc_flag: Flag, sweep_flag: Flag, start_pos: Point, control1: Point, control2: Point, end_pos: Point): if command is SVGCmdEnum.MOVE_TO: return SVGCommandMove(start_pos, end_pos) elif command is SVGCmdEnum.LINE_TO: return SVGCommandLine(start_pos, end_pos) elif command is SVGCmdEnum.CUBIC_BEZIER: return SVGCommandBezier(start_pos, control1, control2, end_pos) elif command is SVGCmdEnum.CLOSE_PATH: return SVGCommandClose(start_pos, end_pos) elif command is SVGCmdEnum.ELLIPTIC_ARC: return SVGCommandArc(start_pos, radius, x_axis_rotation, large_arc_flag, sweep_flag, end_pos) def draw(self, *args, **kwargs): from .svg_path import SVGPath return SVGPath([self]).draw(*args, **kwargs) def reverse(self): raise NotImplementedError def is_left_to(self, other: SVGCommand): p1, p2 = self.start_pos, other.start_pos if p1.y == p2.y: return p1.x < p2.x return p1.y < p2.y or (np.isclose(p1.norm(), p2.norm()) and p1.x < p2.x) def numericalize(self, n=256): raise NotImplementedError def get_geoms(self): return [self.start_pos, self.end_pos] def get_points_viz(self, first=False, last=False): from .svg_primitive import SVGCircle color = "red" if first else "purple" if last else "deepskyblue" # "#C4C4C4" opacity = 0.75 if first or last else 1.0 return [SVGCircle(self.end_pos, radius=Radius(0.4), color=color, fill=True, stroke_width=".1", opacity=opacity)] def get_handles_viz(self): return [] def sample_points(self, n=10, return_array=False): return [] def split(self, n=2): raise NotImplementedError def length(self): raise NotImplementedError def bbox(self): raise NotImplementedError class SVGCommandLinear(SVGCommand): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) def to_tensor(self, PAD_VAL=-1): cmd_index = SVGTensor.COMMANDS_SIMPLIFIED.index(self.command.value) return torch.tensor([cmd_index, *([PAD_VAL] * 5), *self.start_pos.to_tensor(), *([PAD_VAL] * 4), *self.end_pos.to_tensor()]) def numericalize(self, n=256): self.start_pos.numericalize(n) self.end_pos.numericalize(n) def copy(self): return self.__class__(self.start_pos.copy(), self.end_pos.copy()) def reverse(self): return self.__class__(self.end_pos, self.start_pos) def split(self, n=2): return [self] def bbox(self): return Bbox(self.start_pos, self.end_pos) class SVGCommandMove(SVGCommandLinear): def __init__(self, start_pos: Point, end_pos: Point=None): if end_pos is None: start_pos, end_pos = Point(0.), start_pos super().__init__(SVGCmdEnum.MOVE_TO, [end_pos], start_pos, end_pos) def get_points_viz(self, first=False, last=False): from .svg_primitive import SVGLine points_viz = super().get_points_viz(first, last) points_viz.append(SVGLine(self.start_pos, self.end_pos, color="red", dasharray=0.5)) return points_viz def bbox(self): return Bbox(self.end_pos, self.end_pos) class SVGCommandLine(SVGCommandLinear): def __init__(self, start_pos: Point, end_pos: Point): super().__init__(SVGCmdEnum.LINE_TO, [end_pos], start_pos, end_pos) def sample_points(self, n=10, return_array=False): z = np.linspace(0., 1., n) if return_array: points = (1-z)[:, None] * self.start_pos.pos[None] + z[:, None] * self.end_pos.pos[None] return points points = [(1 - alpha) * self.start_pos + alpha * self.end_pos for alpha in z] return points def split(self, n=2): points = self.sample_points(n+1) return [SVGCommandLine(p1, p2) for p1, p2 in zip(points[:-1], points[1:])] def length(self): return self.start_pos.dist(self.end_pos) class SVGCommandClose(SVGCommandLinear): def __init__(self, start_pos: Point, end_pos: Point): super().__init__(SVGCmdEnum.CLOSE_PATH, [], start_pos, end_pos) def get_points_viz(self, first=False, last=False): return [] class SVGCommandBezier(SVGCommand): def __init__(self, start_pos: Point, control1: Point, control2: Point, end_pos: Point): if control2 is None: control2 = control1.copy() super().__init__(SVGCmdEnum.CUBIC_BEZIER, [control1, control2, end_pos], start_pos, end_pos) self.control1 = control1 self.control2 = control2 @property def p1(self): return self.start_pos @property def p2(self): return self.end_pos @property def q1(self): return self.control1 @property def q2(self): return self.control2 def copy(self): return SVGCommandBezier(self.start_pos.copy(), self.control1.copy(), self.control2.copy(), self.end_pos.copy()) def to_tensor(self, PAD_VAL=-1): cmd_index = SVGTensor.COMMANDS_SIMPLIFIED.index(SVGCmdEnum.CUBIC_BEZIER.value) return torch.tensor([cmd_index, *([PAD_VAL] * 5), *self.start_pos.to_tensor(), *self.control1.to_tensor(), *self.control2.to_tensor(), *self.end_pos.to_tensor()]) def to_vector(self): return np.array([ self.start_pos.tolist(), self.control1.tolist(), self.control2.tolist(), self.end_pos.tolist() ]) @staticmethod def from_vector(vector): return SVGCommandBezier(Point(vector[0]), Point(vector[1]), Point(vector[2]), Point(vector[3])) def reverse(self): return SVGCommandBezier(self.end_pos, self.control2, self.control1, self.start_pos) def numericalize(self, n=256): self.start_pos.numericalize(n) self.control1.numericalize(n) self.control2.numericalize(n) self.end_pos.numericalize(n) def get_geoms(self): return [self.start_pos, self.control1, self.control2, self.end_pos] def get_handles_viz(self): from .svg_primitive import SVGLine, SVGCircle anchor_1 = SVGCircle(self.control1, radius=Radius(0.4), color="lime", fill=True, stroke_width=".1") anchor_2 = SVGCircle(self.control2, radius=Radius(0.4), color="lime", fill=True, stroke_width=".1") handle_1 = SVGLine(self.start_pos, self.control1, color="grey", dasharray=0.5, stroke_width=".1") handle_2 = SVGLine(self.end_pos, self.control2, color="grey", dasharray=0.5, stroke_width=".1") return [handle_1, handle_2, anchor_1, anchor_2] def eval(self, t): return (1 - t)**3 * self.start_pos + 3 * (1 - t)**2 * t * self.control1 + 3 * (1 - t) * t**2 * self.control2 + t**3 * self.end_pos def derivative(self, t, n=1): if n == 1: return 3 * (1 - t)**2 * (self.control1 - self.start_pos) + 6 * (1 - t) * t * (self.control2 - self.control1) + 3 * t**2 * (self.end_pos - self.control2) elif n == 2: return 6 * (1 - t) * (self.control2 - 2 * self.control1 + self.start_pos) + 6 * t * (self.end_pos - 2 * self.control2 + self.control1) raise NotImplementedError def angle(self, other: SVGCommandBezier): t1, t2 = self.derivative(1.), -other.derivative(0.) if np.isclose(t1.norm(), 0.) or np.isclose(t2.norm(), 0.): return 0. angle = np.arccos(np.clip(t1.normalize().dot(t2.normalize()), -1., 1.)) return np.rad2deg(angle) def sample_points(self, n=10, return_array=False): b = self.to_vector() z = np.linspace(0., 1., n) Z = np.stack([np.ones_like(z), z, z**2, z**3], axis=1) Q = np.array([[1., 0., 0., 0.], [-3, 3., 0., 0.], [3., -6, 3., 0.], [-1, 3., -3, 1]]) points = Z @ Q @ b if return_array: return points return [Point(p) for p in points] def _split_two(self, z=.5): b = self.to_vector() Q1 = np.array([[1, 0, 0, 0], [-(z - 1), z, 0, 0], [(z - 1) ** 2, -2 * (z - 1) * z, z ** 2, 0], [-(z - 1) ** 3, 3 * (z - 1) ** 2 * z, -3 * (z - 1) * z ** 2, z ** 3]]) Q2 = np.array([[-(z - 1) ** 3, 3 * (z - 1) ** 2 * z, -3 * (z - 1) * z ** 2, z ** 3], [0, (z - 1) ** 2, -2 * (z - 1) * z, z ** 2], [0, 0, -(z - 1), z], [0, 0, 0, 1]]) return SVGCommandBezier.from_vector(Q1 @ b), SVGCommandBezier.from_vector(Q2 @ b) def split(self, n=2): b_list = [] b = self for i in range(n - 1): z = 1. / (n - i) b1, b = b._split_two(z) b_list.append(b1) b_list.append(b) return b_list def length(self): p = self.sample_points(n=100, return_array=True) return np.linalg.norm(p[1:] - p[:-1], axis=-1).sum() def bbox(self): return Bbox.from_points(self.find_extrema()) def find_roots(self): a = 3 * (-self.p1 + 3 * self.q1 - 3 * self.q2 + self.p2) b = 6 * (self.p1 - 2 * self.q1 + self.q2) c = 3 * (self.q1 - self.p1) x_roots, y_roots = get_roots(a.x, b.x, c.x), get_roots(a.y, b.y, c.y) roots_cat = [*x_roots, *y_roots] roots = [root for root in roots_cat if 0 <= root <= 1] return roots def find_extrema(self): points = [self.start_pos, self.end_pos] points.extend([self.eval(root) for root in self.find_roots()]) return points class SVGCommandArc(SVGCommand): def __init__(self, start_pos: Point, radius: Radius, x_axis_rotation: Angle, large_arc_flag: Flag, sweep_flag: Flag, end_pos: Point): super().__init__(SVGCmdEnum.ELLIPTIC_ARC, [radius, x_axis_rotation, large_arc_flag, sweep_flag, end_pos], start_pos, end_pos) self.radius = radius self.x_axis_rotation = x_axis_rotation self.large_arc_flag = large_arc_flag self.sweep_flag = sweep_flag def copy(self): return SVGCommandArc(self.start_pos.copy(), self.radius.copy(), self.x_axis_rotation.copy(), self.large_arc_flag.copy(), self.sweep_flag.copy(), self.end_pos.copy()) def to_tensor(self, PAD_VAL=-1): cmd_index = SVGTensor.COMMANDS_SIMPLIFIED.index(SVGCmdEnum.ELLIPTIC_ARC.value) return torch.tensor([cmd_index, *self.radius.to_tensor(), *self.x_axis_rotation.to_tensor(), *self.large_arc_flag.to_tensor(), *self.sweep_flag.to_tensor(), *self.start_pos.to_tensor(), *([PAD_VAL] * 4), *self.end_pos.to_tensor()]) def _get_center_parametrization(self): r = self.radius p1, p2 = self.start_pos, self.end_pos h, m = 0.5 * (p1 - p2), 0.5 * (p1 + p2) p1_trans = h.rotate(-self.x_axis_rotation) sign = -1 if self.large_arc_flag.flag == self.sweep_flag.flag else 1 x2, y2, rx2, ry2 = p1_trans.x**2, p1_trans.y**2, r.x**2, r.y**2 sqrt = math.sqrt(max((rx2*ry2 - rx2*y2 - ry2*x2) / (rx2*y2 + ry2*x2), 0.)) c_trans = sign * sqrt * Point(r.x * p1_trans.y / r.y, -r.y * p1_trans.x / r.x) c = c_trans.rotate(self.x_axis_rotation) + m d, ns = (p1_trans - c_trans) / r, -(p1_trans + c_trans) / r theta_1 = Point(1, 0).angle(d, signed=True) delta_theta = d.angle(ns, signed=True) delta_theta.deg %= 360 if self.sweep_flag.flag == 0 and delta_theta.deg > 0: delta_theta = delta_theta - Angle(360) if self.sweep_flag == 1 and delta_theta.deg < 0: delta_theta = delta_theta + Angle(360) return c, theta_1, delta_theta def _get_point(self, c: Point, t: float_type): r = self.radius return c + Point(r.x * np.cos(t), r.y * np.sin(t)).rotate(self.x_axis_rotation) def _get_derivative(self, t: float_type): r = self.radius return Point(-r.x * np.sin(t), r.y * np.cos(t)).rotate(self.x_axis_rotation) def to_beziers(self): """ References: https://www.w3.org/TR/2018/CR-SVG2-20180807/implnote.html https://mortoray.com/2017/02/16/rendering-an-svg-elliptical-arc-as-bezier-curves/ http://www.spaceroots.org/documents/ellipse/elliptical-arc.pdf """ beziers = [] c, theta_1, delta_theta = self._get_center_parametrization() nb_curves = max(int(abs(delta_theta.deg) // 45), 1) etas = [theta_1 + i * delta_theta / nb_curves for i in range(nb_curves+1)] for eta_1, eta_2 in zip(etas[:-1], etas[1:]): e1, e2 = eta_1.rad, eta_2.rad alpha = np.sin(e2 - e1) * (math.sqrt(4 + 3 * np.tan(0.5 * (e2 - e1))**2) - 1) / 3 p1, p2 = self._get_point(c, e1), self._get_point(c, e2) q1 = p1 + alpha * self._get_derivative(e1) q2 = p2 - alpha * self._get_derivative(e2) beziers.append(SVGCommandBezier(p1, q1, q2, p2)) return beziers def reverse(self): return SVGCommandArc(self.end_pos, self.radius, self.x_axis_rotation, self.large_arc_flag, ~self.sweep_flag, self.start_pos) def numericalize(self, n=256): raise NotImplementedError def get_geoms(self): return [self.start_pos, self.radius, self.x_axis_rotation, self.large_arc_flag, self.sweep_flag, self.end_pos] def split(self, n=2): raise NotImplementedError def sample_points(self, n=10, return_array=False): raise NotImplementedError