picosvgx 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- picosvgx/__init__.py +15 -0
- picosvgx/_version.py +34 -0
- picosvgx/arc_to_cubic.py +210 -0
- picosvgx/geometric_types.py +170 -0
- picosvgx/picosvgx.py +85 -0
- picosvgx/py.typed +0 -0
- picosvgx/svg.py +1697 -0
- picosvgx/svg_meta.py +307 -0
- picosvgx/svg_path_iter.py +110 -0
- picosvgx/svg_pathops.py +194 -0
- picosvgx/svg_reuse.py +384 -0
- picosvgx/svg_transform.py +373 -0
- picosvgx/svg_types.py +1031 -0
- picosvgx-0.1.0.dist-info/METADATA +114 -0
- picosvgx-0.1.0.dist-info/RECORD +19 -0
- picosvgx-0.1.0.dist-info/WHEEL +5 -0
- picosvgx-0.1.0.dist-info/entry_points.txt +2 -0
- picosvgx-0.1.0.dist-info/licenses/LICENSE +202 -0
- picosvgx-0.1.0.dist-info/top_level.txt +1 -0
picosvgx/__init__.py
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# Copyright 2020 Google LLC
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
__version__ = "0.1.0"
|
picosvgx/_version.py
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# file generated by setuptools-scm
|
|
2
|
+
# don't change, don't track in version control
|
|
3
|
+
|
|
4
|
+
__all__ = [
|
|
5
|
+
"__version__",
|
|
6
|
+
"__version_tuple__",
|
|
7
|
+
"version",
|
|
8
|
+
"version_tuple",
|
|
9
|
+
"__commit_id__",
|
|
10
|
+
"commit_id",
|
|
11
|
+
]
|
|
12
|
+
|
|
13
|
+
TYPE_CHECKING = False
|
|
14
|
+
if TYPE_CHECKING:
|
|
15
|
+
from typing import Tuple
|
|
16
|
+
from typing import Union
|
|
17
|
+
|
|
18
|
+
VERSION_TUPLE = Tuple[Union[int, str], ...]
|
|
19
|
+
COMMIT_ID = Union[str, None]
|
|
20
|
+
else:
|
|
21
|
+
VERSION_TUPLE = object
|
|
22
|
+
COMMIT_ID = object
|
|
23
|
+
|
|
24
|
+
version: str
|
|
25
|
+
__version__: str
|
|
26
|
+
__version_tuple__: VERSION_TUPLE
|
|
27
|
+
version_tuple: VERSION_TUPLE
|
|
28
|
+
commit_id: COMMIT_ID
|
|
29
|
+
__commit_id__: COMMIT_ID
|
|
30
|
+
|
|
31
|
+
__version__ = version = '0.1.dev659+g00f0aab4b.d20260206'
|
|
32
|
+
__version_tuple__ = version_tuple = (0, 1, 'dev659', 'g00f0aab4b.d20260206')
|
|
33
|
+
|
|
34
|
+
__commit_id__ = commit_id = 'g00f0aab4b'
|
picosvgx/arc_to_cubic.py
ADDED
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
# Copyright 2020 Google LLC
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
"""Convert SVG Path's elliptical arcs to Bezier curves.
|
|
16
|
+
|
|
17
|
+
The code is adapted from FontTools fontTools/svgLib/path/arc.py, which in turn
|
|
18
|
+
is adapted from Blink's SVGPathNormalizer::DecomposeArcToCubic:
|
|
19
|
+
https://github.com/chromium/chromium/blob/93831f2/third_party/blink/renderer/core/svg/svg_path_parser.cc#L169-L278
|
|
20
|
+
"""
|
|
21
|
+
from math import atan2, ceil, cos, fabs, isfinite, pi, radians, sin, sqrt, tan
|
|
22
|
+
from typing import Iterator, NamedTuple, Optional, Tuple
|
|
23
|
+
from picosvgx.geometric_types import Point, Vector
|
|
24
|
+
from picosvgx.svg_transform import Affine2D
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
TWO_PI = 2 * pi
|
|
28
|
+
PI_OVER_TWO = 0.5 * pi
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class CenterParametrization(NamedTuple):
|
|
32
|
+
theta1: float
|
|
33
|
+
theta_arc: float
|
|
34
|
+
center_point: Point
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class EllipticalArc(NamedTuple):
|
|
38
|
+
start_point: Point
|
|
39
|
+
rx: float
|
|
40
|
+
ry: float
|
|
41
|
+
rotation: float
|
|
42
|
+
large: int
|
|
43
|
+
sweep: int
|
|
44
|
+
end_point: Point
|
|
45
|
+
|
|
46
|
+
def is_straight_line(self) -> bool:
|
|
47
|
+
# If rx = 0 or ry = 0 then this arc is treated as a straight line segment (a
|
|
48
|
+
# "lineto") joining the endpoints.
|
|
49
|
+
# http://www.w3.org/TR/SVG/implnote.html#ArcOutOfRangeParameters
|
|
50
|
+
rx = fabs(self.rx)
|
|
51
|
+
ry = fabs(self.ry)
|
|
52
|
+
if not (rx and ry):
|
|
53
|
+
return True
|
|
54
|
+
return False
|
|
55
|
+
|
|
56
|
+
def is_zero_length(self):
|
|
57
|
+
return self.end_point == self.start_point
|
|
58
|
+
|
|
59
|
+
def correct_out_of_range_radii(self) -> "EllipticalArc":
|
|
60
|
+
# Check if the radii are big enough to draw the arc, scale radii if not.
|
|
61
|
+
# http://www.w3.org/TR/SVG/implnote.html#ArcCorrectionOutOfRangeRadii
|
|
62
|
+
if self.is_straight_line() or self.is_zero_length():
|
|
63
|
+
return self
|
|
64
|
+
|
|
65
|
+
mid_point_distance = (self.start_point - self.end_point) * 0.5
|
|
66
|
+
|
|
67
|
+
# SVG rotation is expressed in degrees, whereas Affin2D.rotate uses radians
|
|
68
|
+
angle = radians(self.rotation)
|
|
69
|
+
point_transform = Affine2D.identity().rotate(-angle)
|
|
70
|
+
|
|
71
|
+
transformed_mid_point = point_transform.map_vector(mid_point_distance)
|
|
72
|
+
rx = self.rx
|
|
73
|
+
ry = self.ry
|
|
74
|
+
square_rx = rx * rx
|
|
75
|
+
square_ry = ry * ry
|
|
76
|
+
square_x = transformed_mid_point.x * transformed_mid_point.x
|
|
77
|
+
square_y = transformed_mid_point.y * transformed_mid_point.y
|
|
78
|
+
|
|
79
|
+
radii_scale = square_x / square_rx + square_y / square_ry
|
|
80
|
+
if radii_scale > 1:
|
|
81
|
+
rx *= sqrt(radii_scale)
|
|
82
|
+
ry *= sqrt(radii_scale)
|
|
83
|
+
return self._replace(rx=rx, ry=ry)
|
|
84
|
+
|
|
85
|
+
return self
|
|
86
|
+
|
|
87
|
+
# https://www.w3.org/TR/SVG/implnote.html#ArcConversionEndpointToCenter
|
|
88
|
+
def end_to_center_parametrization(self) -> CenterParametrization:
|
|
89
|
+
if self.is_straight_line() or self.is_zero_length():
|
|
90
|
+
raise ValueError(f"Can't compute center parametrization for {self}")
|
|
91
|
+
|
|
92
|
+
angle = radians(self.rotation)
|
|
93
|
+
point_transform = (
|
|
94
|
+
Affine2D.identity().scale(1 / self.rx, 1 / self.ry).rotate(-angle)
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
point1 = point_transform.map_point(self.start_point)
|
|
98
|
+
point2 = point_transform.map_point(self.end_point)
|
|
99
|
+
delta = point2 - point1
|
|
100
|
+
|
|
101
|
+
d = delta.x * delta.x + delta.y * delta.y
|
|
102
|
+
scale_factor_squared = max(1 / d - 0.25, 0.0)
|
|
103
|
+
|
|
104
|
+
scale_factor = sqrt(scale_factor_squared)
|
|
105
|
+
if self.sweep == self.large:
|
|
106
|
+
scale_factor = -scale_factor
|
|
107
|
+
|
|
108
|
+
delta *= scale_factor
|
|
109
|
+
center_point = point1 + (point2 - point1) * 0.5 + Vector(-delta.y, delta.x)
|
|
110
|
+
v1 = point1 - center_point
|
|
111
|
+
v2 = point2 - center_point
|
|
112
|
+
|
|
113
|
+
theta1 = atan2(v1.y, v1.x)
|
|
114
|
+
theta2 = atan2(v2.y, v2.x)
|
|
115
|
+
|
|
116
|
+
theta_arc = theta2 - theta1
|
|
117
|
+
if theta_arc < 0 and self.sweep:
|
|
118
|
+
theta_arc += TWO_PI
|
|
119
|
+
elif theta_arc > 0 and not self.sweep:
|
|
120
|
+
theta_arc -= TWO_PI
|
|
121
|
+
|
|
122
|
+
center_point = point_transform.inverse().map_point(center_point)
|
|
123
|
+
|
|
124
|
+
return CenterParametrization(theta1, theta_arc, center_point)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def _arc_to_cubic(arc: EllipticalArc) -> Iterator[Tuple[Point, Point, Point]]:
|
|
128
|
+
arc = arc.correct_out_of_range_radii()
|
|
129
|
+
arc_params = arc.end_to_center_parametrization()
|
|
130
|
+
|
|
131
|
+
point_transform = (
|
|
132
|
+
Affine2D.identity()
|
|
133
|
+
.translate(arc_params.center_point.x, arc_params.center_point.y)
|
|
134
|
+
.rotate(radians(arc.rotation))
|
|
135
|
+
.scale(arc.rx, arc.ry)
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
# Some results of atan2 on some platform implementations are not exact
|
|
139
|
+
# enough. So that we get more cubic curves than expected here. Adding 0.001f
|
|
140
|
+
# reduces the count of sgements to the correct count.
|
|
141
|
+
num_segments = int(ceil(fabs(arc_params.theta_arc / (PI_OVER_TWO + 0.001))))
|
|
142
|
+
for i in range(num_segments):
|
|
143
|
+
start_theta = arc_params.theta1 + i * arc_params.theta_arc / num_segments
|
|
144
|
+
end_theta = arc_params.theta1 + (i + 1) * arc_params.theta_arc / num_segments
|
|
145
|
+
|
|
146
|
+
t = (4 / 3) * tan(0.25 * (end_theta - start_theta))
|
|
147
|
+
if not isfinite(t):
|
|
148
|
+
return
|
|
149
|
+
|
|
150
|
+
sin_start_theta = sin(start_theta)
|
|
151
|
+
cos_start_theta = cos(start_theta)
|
|
152
|
+
sin_end_theta = sin(end_theta)
|
|
153
|
+
cos_end_theta = cos(end_theta)
|
|
154
|
+
|
|
155
|
+
point1 = Point(
|
|
156
|
+
cos_start_theta - t * sin_start_theta, sin_start_theta + t * cos_start_theta
|
|
157
|
+
)
|
|
158
|
+
end_point = Point(cos_end_theta, sin_end_theta)
|
|
159
|
+
point2 = end_point + Vector(t * sin_end_theta, -t * cos_end_theta)
|
|
160
|
+
|
|
161
|
+
point1 = point_transform.map_point(point1)
|
|
162
|
+
point2 = point_transform.map_point(point2)
|
|
163
|
+
|
|
164
|
+
# by definition, the last bezier's end point == the arc end point
|
|
165
|
+
# by directly taking the end point we avoid floating point imprecision
|
|
166
|
+
if i == num_segments - 1:
|
|
167
|
+
end_point = arc.end_point
|
|
168
|
+
else:
|
|
169
|
+
end_point = point_transform.map_point(end_point)
|
|
170
|
+
|
|
171
|
+
yield point1, point2, end_point
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def arc_to_cubic(
|
|
175
|
+
start_point: Tuple[float, float],
|
|
176
|
+
rx: float,
|
|
177
|
+
ry: float,
|
|
178
|
+
rotation: float,
|
|
179
|
+
large: int,
|
|
180
|
+
sweep: int,
|
|
181
|
+
end_point: Tuple[float, float],
|
|
182
|
+
) -> Iterator[Tuple[Optional[Point], Optional[Point], Point]]:
|
|
183
|
+
"""Convert arc to cubic(s).
|
|
184
|
+
|
|
185
|
+
start/end point are (x,y) tuples with absolute coordinates.
|
|
186
|
+
See https://skia.org/user/api/SkPath_Reference#SkPath_arcTo_4
|
|
187
|
+
Note in particular:
|
|
188
|
+
SVG sweep-flag value is opposite the integer value of sweep;
|
|
189
|
+
SVG sweep-flag uses 1 for clockwise, while kCW_Direction cast to int is zero.
|
|
190
|
+
|
|
191
|
+
Yields 3-tuples of Points for each Cubic bezier, i.e. two off-curve points and
|
|
192
|
+
one on-curve end point.
|
|
193
|
+
|
|
194
|
+
If either rx or ry is 0, the arc is treated as a straight line joining the end
|
|
195
|
+
points, and a (None, None, arc.end_point) tuple is yielded.
|
|
196
|
+
|
|
197
|
+
Yields empty iterator if arc has zero length.
|
|
198
|
+
"""
|
|
199
|
+
if not isinstance(start_point, Point):
|
|
200
|
+
start_point = Point(*start_point)
|
|
201
|
+
if not isinstance(end_point, Point):
|
|
202
|
+
end_point = Point(*end_point)
|
|
203
|
+
|
|
204
|
+
arc = EllipticalArc(start_point, rx, ry, rotation, large, sweep, end_point)
|
|
205
|
+
if arc.is_zero_length():
|
|
206
|
+
return
|
|
207
|
+
elif arc.is_straight_line():
|
|
208
|
+
yield None, None, arc.end_point
|
|
209
|
+
else:
|
|
210
|
+
yield from _arc_to_cubic(arc)
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
# Copyright 2020 Google LLC
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
import math
|
|
16
|
+
from typing import NamedTuple, Optional, Union
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
DEFAULT_ALMOST_EQUAL_TOLERANCE = 1e-9
|
|
20
|
+
_PointOrVec = Union["Point", "Vector"]
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def almost_equal(c1, c2, tolerance=DEFAULT_ALMOST_EQUAL_TOLERANCE) -> bool:
|
|
24
|
+
return abs(c1 - c2) <= tolerance
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class Point(NamedTuple):
|
|
28
|
+
x: float = 0
|
|
29
|
+
y: float = 0
|
|
30
|
+
|
|
31
|
+
def _sub_pt(self, other: "Point") -> "Vector":
|
|
32
|
+
return Vector(self.x - other.x, self.y - other.y)
|
|
33
|
+
|
|
34
|
+
def _sub_vec(self, other: "Vector") -> "Point":
|
|
35
|
+
return self.__class__(self.x - other.x, self.y - other.y)
|
|
36
|
+
|
|
37
|
+
def __sub__(self, other: _PointOrVec) -> _PointOrVec:
|
|
38
|
+
"""Return a Point or Vector based on the type of other.
|
|
39
|
+
|
|
40
|
+
If other is a Point, return Vector from other to self.
|
|
41
|
+
If other is a Vector, return Point translated by -other Vector.
|
|
42
|
+
"""
|
|
43
|
+
if isinstance(other, Point):
|
|
44
|
+
return self._sub_pt(other)
|
|
45
|
+
elif isinstance(other, Vector):
|
|
46
|
+
return self._sub_vec(other)
|
|
47
|
+
return NotImplemented # pytype: disable=bad-return-type
|
|
48
|
+
|
|
49
|
+
def __add__(self, other: "Vector") -> "Point":
|
|
50
|
+
"""Return Point translated by other Vector"""
|
|
51
|
+
if isinstance(other, Vector):
|
|
52
|
+
return self.__class__(self.x + other.x, self.y + other.y)
|
|
53
|
+
return NotImplemented # pytype: disable=bad-return-type
|
|
54
|
+
|
|
55
|
+
def round(self, digits: int) -> "Point":
|
|
56
|
+
return Point(round(self.x, digits), round(self.y, digits))
|
|
57
|
+
|
|
58
|
+
def almost_equals(
|
|
59
|
+
self, other: "Point", tolerance=DEFAULT_ALMOST_EQUAL_TOLERANCE
|
|
60
|
+
) -> bool:
|
|
61
|
+
return almost_equal(self.x, other.x, tolerance) and almost_equal(
|
|
62
|
+
self.y, other.y, tolerance
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class Vector(NamedTuple):
|
|
67
|
+
x: float = 0
|
|
68
|
+
y: float = 0
|
|
69
|
+
|
|
70
|
+
def __add__(self, other: "Vector") -> "Vector":
|
|
71
|
+
return self.__class__(self.x + other.x, self.y + other.y)
|
|
72
|
+
|
|
73
|
+
def __sub__(self, other: "Vector") -> "Vector":
|
|
74
|
+
return self.__class__(self.x - other.x, self.y - other.y)
|
|
75
|
+
|
|
76
|
+
def __neg__(self) -> "Vector":
|
|
77
|
+
return self * -1.0
|
|
78
|
+
|
|
79
|
+
def __mul__(self, scalar: float) -> "Vector":
|
|
80
|
+
"""Multiply vector by a scalar value."""
|
|
81
|
+
if not isinstance(scalar, (int, float)):
|
|
82
|
+
return NotImplemented
|
|
83
|
+
return self.__class__(self.x * scalar, self.y * scalar)
|
|
84
|
+
|
|
85
|
+
__rmul__ = __mul__
|
|
86
|
+
|
|
87
|
+
def perpendicular(self, clockwise: bool = False) -> "Vector":
|
|
88
|
+
"""Return Vector rotated 90 degrees counter-clockwise from self.
|
|
89
|
+
|
|
90
|
+
If clockwise is True, return the other perpendicular vector.
|
|
91
|
+
"""
|
|
92
|
+
# https://mathworld.wolfram.com/PerpendicularVector.html
|
|
93
|
+
if clockwise:
|
|
94
|
+
return self.__class__(self.y, -self.x)
|
|
95
|
+
else:
|
|
96
|
+
return self.__class__(-self.y, self.x)
|
|
97
|
+
|
|
98
|
+
def norm(self) -> float:
|
|
99
|
+
"""Return the vector Euclidean norm (or length or magnitude)."""
|
|
100
|
+
return math.sqrt(self.x * self.x + self.y * self.y)
|
|
101
|
+
|
|
102
|
+
def unit(self) -> Optional["Vector"]:
|
|
103
|
+
"""Return the Unit Vector (of length 1), or None if self is a zero vector."""
|
|
104
|
+
norm = self.norm()
|
|
105
|
+
if norm != 0:
|
|
106
|
+
return self.__class__(self.x / norm, self.y / norm)
|
|
107
|
+
return None
|
|
108
|
+
|
|
109
|
+
def dot(self, other: "Vector") -> float:
|
|
110
|
+
"""Return the Dot Product of self with other vector."""
|
|
111
|
+
return self.x * other.x + self.y * other.y
|
|
112
|
+
|
|
113
|
+
def projection(self, other: "Vector") -> "Vector":
|
|
114
|
+
"""Return the vector projection of self onto other vector."""
|
|
115
|
+
norm = other.norm()
|
|
116
|
+
if norm == 0:
|
|
117
|
+
# it is more helpful for projection onto 0 to be 0 than an error
|
|
118
|
+
return Vector()
|
|
119
|
+
return self.dot(other) / norm * other.unit()
|
|
120
|
+
|
|
121
|
+
def almost_equals(
|
|
122
|
+
self, other: "Vector", tolerance=DEFAULT_ALMOST_EQUAL_TOLERANCE
|
|
123
|
+
) -> bool:
|
|
124
|
+
return almost_equal(self.x, other.x, tolerance) and almost_equal(
|
|
125
|
+
self.y, other.y, tolerance
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
class Rect(NamedTuple):
|
|
130
|
+
x: float = 0
|
|
131
|
+
y: float = 0
|
|
132
|
+
w: float = 0
|
|
133
|
+
h: float = 0
|
|
134
|
+
|
|
135
|
+
def empty(self) -> bool:
|
|
136
|
+
"""Return True if the Rect's width or height is 0."""
|
|
137
|
+
return self.w == 0 or self.h == 0
|
|
138
|
+
|
|
139
|
+
def intersection(self, other: "Rect") -> Optional["Rect"]:
|
|
140
|
+
def _overlap(start1, end1, start2, end2):
|
|
141
|
+
start = max(start1, start2)
|
|
142
|
+
end = min(end1, end2)
|
|
143
|
+
if start >= end:
|
|
144
|
+
return (0.0, 0.0)
|
|
145
|
+
return (start, end)
|
|
146
|
+
|
|
147
|
+
x1, x2 = _overlap(self.x, self.x + self.w, other.x, other.x + other.w)
|
|
148
|
+
y1, y2 = _overlap(self.y, self.y + self.h, other.y, other.y + other.h)
|
|
149
|
+
|
|
150
|
+
if x1 != x2 and y1 != y2:
|
|
151
|
+
return Rect(x1, y1, x2 - x1, y2 - y1)
|
|
152
|
+
return None
|
|
153
|
+
|
|
154
|
+
@property
|
|
155
|
+
def x_max(self) -> float:
|
|
156
|
+
return self.x + self.w
|
|
157
|
+
|
|
158
|
+
@property
|
|
159
|
+
def y_max(self) -> float:
|
|
160
|
+
return self.y + self.h
|
|
161
|
+
|
|
162
|
+
def union(self, other: "Rect") -> "Rect":
|
|
163
|
+
x, y = min(self.x, other.x), min(self.y, other.y)
|
|
164
|
+
x_max, y_max = max(self.x_max, other.x_max), max(self.y_max, other.y_max)
|
|
165
|
+
return Rect(x=x, y=y, w=x_max - x, h=y_max - y)
|
|
166
|
+
|
|
167
|
+
def normalized_diagonal(self):
|
|
168
|
+
# used for computing percentages of lengths relative to the SVG viewport:
|
|
169
|
+
# https://www.w3.org/TR/SVG2/coords.html#Units
|
|
170
|
+
return math.hypot(self.w, self.h) / math.sqrt(2)
|
picosvgx/picosvgx.py
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# Copyright 2020 Google LLC
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
"""Simplify svg.
|
|
16
|
+
|
|
17
|
+
Usage:
|
|
18
|
+
picosvg.py emoji_u1f469_1f3fd_200d_1f91d_200d_1f468_1f3fb.svg
|
|
19
|
+
<simplified svg dumped to stdout>
|
|
20
|
+
"""
|
|
21
|
+
from absl import app
|
|
22
|
+
from absl import flags
|
|
23
|
+
from lxml import etree # pytype: disable=import-error
|
|
24
|
+
from picosvgx.svg import SVG
|
|
25
|
+
from picosvgx.svg_meta import svgns
|
|
26
|
+
import sys
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
FLAGS = flags.FLAGS
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
flags.DEFINE_bool(
|
|
33
|
+
"drop_unsupported",
|
|
34
|
+
False,
|
|
35
|
+
"Whether to blindly discard all elements we don't understand. Likely unwise.",
|
|
36
|
+
)
|
|
37
|
+
flags.DEFINE_bool("clip_to_viewbox", False, "Whether to clip content outside viewbox")
|
|
38
|
+
flags.DEFINE_string("output_file", "-", "Output SVG file ('-' means stdout)")
|
|
39
|
+
flags.DEFINE_bool(
|
|
40
|
+
"allow_text",
|
|
41
|
+
False,
|
|
42
|
+
"Whether to allow text elements. Note that they will not be converted to paths, just pass through to the output.",
|
|
43
|
+
)
|
|
44
|
+
flags.DEFINE_bool(
|
|
45
|
+
"allow_all_defs",
|
|
46
|
+
False,
|
|
47
|
+
"Allow all defs elements (filter, mask, pattern, etc.) and root-level switch/symbol to pass through.",
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _run(argv):
|
|
52
|
+
try:
|
|
53
|
+
input_file = argv[1]
|
|
54
|
+
except IndexError:
|
|
55
|
+
input_file = None
|
|
56
|
+
|
|
57
|
+
if input_file:
|
|
58
|
+
svg = SVG.parse(input_file)
|
|
59
|
+
else:
|
|
60
|
+
svg = SVG.fromstring(sys.stdin.read())
|
|
61
|
+
|
|
62
|
+
# Do the needful
|
|
63
|
+
svg = svg.topicosvg(
|
|
64
|
+
allow_text=FLAGS.allow_text, allow_all_defs=FLAGS.allow_all_defs, drop_unsupported=FLAGS.drop_unsupported
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
if FLAGS.clip_to_viewbox:
|
|
68
|
+
svg.clip_to_viewbox(inplace=True)
|
|
69
|
+
|
|
70
|
+
output = svg.tostring(pretty_print=True)
|
|
71
|
+
|
|
72
|
+
if FLAGS.output_file == "-":
|
|
73
|
+
print(output)
|
|
74
|
+
else:
|
|
75
|
+
with open(FLAGS.output_file, "w") as f:
|
|
76
|
+
f.write(output)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def main(argv=None):
|
|
80
|
+
# We don't seem to be __main__ when run as cli tool installed by setuptools
|
|
81
|
+
app.run(_run, argv=argv)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
if __name__ == "__main__":
|
|
85
|
+
main()
|
picosvgx/py.typed
ADDED
|
File without changes
|