asd
This commit is contained in:
		| @ -0,0 +1,65 @@ | ||||
| from fontTools.pens.transformPen import TransformPen | ||||
| from fontTools.misc import etree | ||||
| from fontTools.misc.textTools import tostr | ||||
| from .parser import parse_path | ||||
| from .shapes import PathBuilder | ||||
|  | ||||
|  | ||||
| __all__ = [tostr(s) for s in ("SVGPath", "parse_path")] | ||||
|  | ||||
|  | ||||
| class SVGPath(object): | ||||
|     """Parse SVG ``path`` elements from a file or string, and draw them | ||||
|     onto a glyph object that supports the FontTools Pen protocol. | ||||
|  | ||||
|     For example, reading from an SVG file and drawing to a Defcon Glyph: | ||||
|  | ||||
|     .. code-block:: | ||||
|  | ||||
|         import defcon | ||||
|         glyph = defcon.Glyph() | ||||
|         pen = glyph.getPen() | ||||
|         svg = SVGPath("path/to/a.svg") | ||||
|         svg.draw(pen) | ||||
|  | ||||
|     Or reading from a string containing SVG data, using the alternative | ||||
|     'fromstring' (a class method): | ||||
|  | ||||
|     .. code-block:: | ||||
|  | ||||
|         data = '<?xml version="1.0" ...' | ||||
|         svg = SVGPath.fromstring(data) | ||||
|         svg.draw(pen) | ||||
|  | ||||
|     Both constructors can optionally take a 'transform' matrix (6-float | ||||
|     tuple, or a FontTools Transform object) to modify the draw output. | ||||
|     """ | ||||
|  | ||||
|     def __init__(self, filename=None, transform=None): | ||||
|         if filename is None: | ||||
|             self.root = etree.ElementTree() | ||||
|         else: | ||||
|             tree = etree.parse(filename) | ||||
|             self.root = tree.getroot() | ||||
|         self.transform = transform | ||||
|  | ||||
|     @classmethod | ||||
|     def fromstring(cls, data, transform=None): | ||||
|         self = cls(transform=transform) | ||||
|         self.root = etree.fromstring(data) | ||||
|         return self | ||||
|  | ||||
|     def draw(self, pen): | ||||
|         if self.transform: | ||||
|             pen = TransformPen(pen, self.transform) | ||||
|         pb = PathBuilder() | ||||
|         # xpath | doesn't seem to reliable work so just walk it | ||||
|         for el in self.root.iter(): | ||||
|             pb.add_path_from_element(el) | ||||
|         original_pen = pen | ||||
|         for path, transform in zip(pb.paths, pb.transforms): | ||||
|             if transform: | ||||
|                 pen = TransformPen(original_pen, transform) | ||||
|             else: | ||||
|                 pen = original_pen | ||||
|             parse_path(path, pen) | ||||
							
								
								
									
										154
									
								
								venv/lib/python3.12/site-packages/fontTools/svgLib/path/arc.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										154
									
								
								venv/lib/python3.12/site-packages/fontTools/svgLib/path/arc.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,154 @@ | ||||
| """Convert SVG Path's elliptical arcs to Bezier curves. | ||||
|  | ||||
| The code is mostly adapted from Blink's SVGPathNormalizer::DecomposeArcToCubic | ||||
| https://github.com/chromium/chromium/blob/93831f2/third_party/ | ||||
| blink/renderer/core/svg/svg_path_parser.cc#L169-L278 | ||||
| """ | ||||
|  | ||||
| from fontTools.misc.transform import Identity, Scale | ||||
| from math import atan2, ceil, cos, fabs, isfinite, pi, radians, sin, sqrt, tan | ||||
|  | ||||
|  | ||||
| TWO_PI = 2 * pi | ||||
| PI_OVER_TWO = 0.5 * pi | ||||
|  | ||||
|  | ||||
| def _map_point(matrix, pt): | ||||
|     # apply Transform matrix to a point represented as a complex number | ||||
|     r = matrix.transformPoint((pt.real, pt.imag)) | ||||
|     return r[0] + r[1] * 1j | ||||
|  | ||||
|  | ||||
| class EllipticalArc(object): | ||||
|     def __init__(self, current_point, rx, ry, rotation, large, sweep, target_point): | ||||
|         self.current_point = current_point | ||||
|         self.rx = rx | ||||
|         self.ry = ry | ||||
|         self.rotation = rotation | ||||
|         self.large = large | ||||
|         self.sweep = sweep | ||||
|         self.target_point = target_point | ||||
|  | ||||
|         # SVG arc's rotation angle is expressed in degrees, whereas Transform.rotate | ||||
|         # uses radians | ||||
|         self.angle = radians(rotation) | ||||
|  | ||||
|         # these derived attributes are computed by the _parametrize method | ||||
|         self.center_point = self.theta1 = self.theta2 = self.theta_arc = None | ||||
|  | ||||
|     def _parametrize(self): | ||||
|         # convert from endopoint to center parametrization: | ||||
|         # https://www.w3.org/TR/SVG/implnote.html#ArcConversionEndpointToCenter | ||||
|  | ||||
|         # If rx = 0 or ry = 0 then this arc is treated as a straight line segment (a | ||||
|         # "lineto") joining the endpoints. | ||||
|         # http://www.w3.org/TR/SVG/implnote.html#ArcOutOfRangeParameters | ||||
|         rx = fabs(self.rx) | ||||
|         ry = fabs(self.ry) | ||||
|         if not (rx and ry): | ||||
|             return False | ||||
|  | ||||
|         # If the current point and target point for the arc are identical, it should | ||||
|         # be treated as a zero length path. This ensures continuity in animations. | ||||
|         if self.target_point == self.current_point: | ||||
|             return False | ||||
|  | ||||
|         mid_point_distance = (self.current_point - self.target_point) * 0.5 | ||||
|  | ||||
|         point_transform = Identity.rotate(-self.angle) | ||||
|  | ||||
|         transformed_mid_point = _map_point(point_transform, mid_point_distance) | ||||
|         square_rx = rx * rx | ||||
|         square_ry = ry * ry | ||||
|         square_x = transformed_mid_point.real * transformed_mid_point.real | ||||
|         square_y = transformed_mid_point.imag * transformed_mid_point.imag | ||||
|  | ||||
|         # Check if the radii are big enough to draw the arc, scale radii if not. | ||||
|         # http://www.w3.org/TR/SVG/implnote.html#ArcCorrectionOutOfRangeRadii | ||||
|         radii_scale = square_x / square_rx + square_y / square_ry | ||||
|         if radii_scale > 1: | ||||
|             rx *= sqrt(radii_scale) | ||||
|             ry *= sqrt(radii_scale) | ||||
|             self.rx, self.ry = rx, ry | ||||
|  | ||||
|         point_transform = Scale(1 / rx, 1 / ry).rotate(-self.angle) | ||||
|  | ||||
|         point1 = _map_point(point_transform, self.current_point) | ||||
|         point2 = _map_point(point_transform, self.target_point) | ||||
|         delta = point2 - point1 | ||||
|  | ||||
|         d = delta.real * delta.real + delta.imag * delta.imag | ||||
|         scale_factor_squared = max(1 / d - 0.25, 0.0) | ||||
|  | ||||
|         scale_factor = sqrt(scale_factor_squared) | ||||
|         if self.sweep == self.large: | ||||
|             scale_factor = -scale_factor | ||||
|  | ||||
|         delta *= scale_factor | ||||
|         center_point = (point1 + point2) * 0.5 | ||||
|         center_point += complex(-delta.imag, delta.real) | ||||
|         point1 -= center_point | ||||
|         point2 -= center_point | ||||
|  | ||||
|         theta1 = atan2(point1.imag, point1.real) | ||||
|         theta2 = atan2(point2.imag, point2.real) | ||||
|  | ||||
|         theta_arc = theta2 - theta1 | ||||
|         if theta_arc < 0 and self.sweep: | ||||
|             theta_arc += TWO_PI | ||||
|         elif theta_arc > 0 and not self.sweep: | ||||
|             theta_arc -= TWO_PI | ||||
|  | ||||
|         self.theta1 = theta1 | ||||
|         self.theta2 = theta1 + theta_arc | ||||
|         self.theta_arc = theta_arc | ||||
|         self.center_point = center_point | ||||
|  | ||||
|         return True | ||||
|  | ||||
|     def _decompose_to_cubic_curves(self): | ||||
|         if self.center_point is None and not self._parametrize(): | ||||
|             return | ||||
|  | ||||
|         point_transform = Identity.rotate(self.angle).scale(self.rx, self.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(self.theta_arc / (PI_OVER_TWO + 0.001)))) | ||||
|         for i in range(num_segments): | ||||
|             start_theta = self.theta1 + i * self.theta_arc / num_segments | ||||
|             end_theta = self.theta1 + (i + 1) * self.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 = complex( | ||||
|                 cos_start_theta - t * sin_start_theta, | ||||
|                 sin_start_theta + t * cos_start_theta, | ||||
|             ) | ||||
|             point1 += self.center_point | ||||
|             target_point = complex(cos_end_theta, sin_end_theta) | ||||
|             target_point += self.center_point | ||||
|             point2 = target_point | ||||
|             point2 += complex(t * sin_end_theta, -t * cos_end_theta) | ||||
|  | ||||
|             point1 = _map_point(point_transform, point1) | ||||
|             point2 = _map_point(point_transform, point2) | ||||
|             target_point = _map_point(point_transform, target_point) | ||||
|  | ||||
|             yield point1, point2, target_point | ||||
|  | ||||
|     def draw(self, pen): | ||||
|         for point1, point2, target_point in self._decompose_to_cubic_curves(): | ||||
|             pen.curveTo( | ||||
|                 (point1.real, point1.imag), | ||||
|                 (point2.real, point2.imag), | ||||
|                 (target_point.real, target_point.imag), | ||||
|             ) | ||||
| @ -0,0 +1,322 @@ | ||||
| # SVG Path specification parser. | ||||
| # This is an adaptation from 'svg.path' by Lennart Regebro (@regebro), | ||||
| # modified so that the parser takes a FontTools Pen object instead of | ||||
| # returning a list of svg.path Path objects. | ||||
| # The original code can be found at: | ||||
| # https://github.com/regebro/svg.path/blob/4f9b6e3/src/svg/path/parser.py | ||||
| # Copyright (c) 2013-2014 Lennart Regebro | ||||
| # License: MIT | ||||
|  | ||||
| from .arc import EllipticalArc | ||||
| import re | ||||
|  | ||||
|  | ||||
| COMMANDS = set("MmZzLlHhVvCcSsQqTtAa") | ||||
| ARC_COMMANDS = set("Aa") | ||||
| UPPERCASE = set("MZLHVCSQTA") | ||||
|  | ||||
| COMMAND_RE = re.compile("([MmZzLlHhVvCcSsQqTtAa])") | ||||
|  | ||||
| # https://www.w3.org/TR/css-syntax-3/#number-token-diagram | ||||
| #   but -6.e-5 will be tokenized as "-6" then "-5" and confuse parsing | ||||
| FLOAT_RE = re.compile( | ||||
|     r"[-+]?"  # optional sign | ||||
|     r"(?:" | ||||
|     r"(?:0|[1-9][0-9]*)(?:\.[0-9]+)?(?:[eE][-+]?[0-9]+)?"  # int/float | ||||
|     r"|" | ||||
|     r"(?:\.[0-9]+(?:[eE][-+]?[0-9]+)?)"  # float with leading dot (e.g. '.42') | ||||
|     r")" | ||||
| ) | ||||
| BOOL_RE = re.compile("^[01]") | ||||
| SEPARATOR_RE = re.compile(f"[, \t]") | ||||
|  | ||||
|  | ||||
| def _tokenize_path(pathdef): | ||||
|     arc_cmd = None | ||||
|     for x in COMMAND_RE.split(pathdef): | ||||
|         if x in COMMANDS: | ||||
|             arc_cmd = x if x in ARC_COMMANDS else None | ||||
|             yield x | ||||
|             continue | ||||
|  | ||||
|         if arc_cmd: | ||||
|             try: | ||||
|                 yield from _tokenize_arc_arguments(x) | ||||
|             except ValueError as e: | ||||
|                 raise ValueError(f"Invalid arc command: '{arc_cmd}{x}'") from e | ||||
|         else: | ||||
|             for token in FLOAT_RE.findall(x): | ||||
|                 yield token | ||||
|  | ||||
|  | ||||
| ARC_ARGUMENT_TYPES = ( | ||||
|     ("rx", FLOAT_RE), | ||||
|     ("ry", FLOAT_RE), | ||||
|     ("x-axis-rotation", FLOAT_RE), | ||||
|     ("large-arc-flag", BOOL_RE), | ||||
|     ("sweep-flag", BOOL_RE), | ||||
|     ("x", FLOAT_RE), | ||||
|     ("y", FLOAT_RE), | ||||
| ) | ||||
|  | ||||
|  | ||||
| def _tokenize_arc_arguments(arcdef): | ||||
|     raw_args = [s for s in SEPARATOR_RE.split(arcdef) if s] | ||||
|     if not raw_args: | ||||
|         raise ValueError(f"Not enough arguments: '{arcdef}'") | ||||
|     raw_args.reverse() | ||||
|  | ||||
|     i = 0 | ||||
|     while raw_args: | ||||
|         arg = raw_args.pop() | ||||
|  | ||||
|         name, pattern = ARC_ARGUMENT_TYPES[i] | ||||
|         match = pattern.search(arg) | ||||
|         if not match: | ||||
|             raise ValueError(f"Invalid argument for '{name}' parameter: {arg!r}") | ||||
|  | ||||
|         j, k = match.span() | ||||
|         yield arg[j:k] | ||||
|         arg = arg[k:] | ||||
|  | ||||
|         if arg: | ||||
|             raw_args.append(arg) | ||||
|  | ||||
|         # wrap around every 7 consecutive arguments | ||||
|         if i == 6: | ||||
|             i = 0 | ||||
|         else: | ||||
|             i += 1 | ||||
|  | ||||
|     if i != 0: | ||||
|         raise ValueError(f"Not enough arguments: '{arcdef}'") | ||||
|  | ||||
|  | ||||
| def parse_path(pathdef, pen, current_pos=(0, 0), arc_class=EllipticalArc): | ||||
|     """Parse SVG path definition (i.e. "d" attribute of <path> elements) | ||||
|     and call a 'pen' object's moveTo, lineTo, curveTo, qCurveTo and closePath | ||||
|     methods. | ||||
|  | ||||
|     If 'current_pos' (2-float tuple) is provided, the initial moveTo will | ||||
|     be relative to that instead being absolute. | ||||
|  | ||||
|     If the pen has an "arcTo" method, it is called with the original values | ||||
|     of the elliptical arc curve commands: | ||||
|  | ||||
|     .. code-block:: | ||||
|  | ||||
|         pen.arcTo(rx, ry, rotation, arc_large, arc_sweep, (x, y)) | ||||
|  | ||||
|     Otherwise, the arcs are approximated by series of cubic Bezier segments | ||||
|     ("curveTo"), one every 90 degrees. | ||||
|     """ | ||||
|     # In the SVG specs, initial movetos are absolute, even if | ||||
|     # specified as 'm'. This is the default behavior here as well. | ||||
|     # But if you pass in a current_pos variable, the initial moveto | ||||
|     # will be relative to that current_pos. This is useful. | ||||
|     current_pos = complex(*current_pos) | ||||
|  | ||||
|     elements = list(_tokenize_path(pathdef)) | ||||
|     # Reverse for easy use of .pop() | ||||
|     elements.reverse() | ||||
|  | ||||
|     start_pos = None | ||||
|     command = None | ||||
|     last_control = None | ||||
|  | ||||
|     have_arcTo = hasattr(pen, "arcTo") | ||||
|  | ||||
|     while elements: | ||||
|         if elements[-1] in COMMANDS: | ||||
|             # New command. | ||||
|             last_command = command  # Used by S and T | ||||
|             command = elements.pop() | ||||
|             absolute = command in UPPERCASE | ||||
|             command = command.upper() | ||||
|         else: | ||||
|             # If this element starts with numbers, it is an implicit command | ||||
|             # and we don't change the command. Check that it's allowed: | ||||
|             if command is None: | ||||
|                 raise ValueError( | ||||
|                     "Unallowed implicit command in %s, position %s" | ||||
|                     % (pathdef, len(pathdef.split()) - len(elements)) | ||||
|                 ) | ||||
|             last_command = command  # Used by S and T | ||||
|  | ||||
|         if command == "M": | ||||
|             # Moveto command. | ||||
|             x = elements.pop() | ||||
|             y = elements.pop() | ||||
|             pos = float(x) + float(y) * 1j | ||||
|             if absolute: | ||||
|                 current_pos = pos | ||||
|             else: | ||||
|                 current_pos += pos | ||||
|  | ||||
|             # M is not preceded by Z; it's an open subpath | ||||
|             if start_pos is not None: | ||||
|                 pen.endPath() | ||||
|  | ||||
|             pen.moveTo((current_pos.real, current_pos.imag)) | ||||
|  | ||||
|             # when M is called, reset start_pos | ||||
|             # This behavior of Z is defined in svg spec: | ||||
|             # http://www.w3.org/TR/SVG/paths.html#PathDataClosePathCommand | ||||
|             start_pos = current_pos | ||||
|  | ||||
|             # Implicit moveto commands are treated as lineto commands. | ||||
|             # So we set command to lineto here, in case there are | ||||
|             # further implicit commands after this moveto. | ||||
|             command = "L" | ||||
|  | ||||
|         elif command == "Z": | ||||
|             # Close path | ||||
|             if current_pos != start_pos: | ||||
|                 pen.lineTo((start_pos.real, start_pos.imag)) | ||||
|             pen.closePath() | ||||
|             current_pos = start_pos | ||||
|             start_pos = None | ||||
|             command = None  # You can't have implicit commands after closing. | ||||
|  | ||||
|         elif command == "L": | ||||
|             x = elements.pop() | ||||
|             y = elements.pop() | ||||
|             pos = float(x) + float(y) * 1j | ||||
|             if not absolute: | ||||
|                 pos += current_pos | ||||
|             pen.lineTo((pos.real, pos.imag)) | ||||
|             current_pos = pos | ||||
|  | ||||
|         elif command == "H": | ||||
|             x = elements.pop() | ||||
|             pos = float(x) + current_pos.imag * 1j | ||||
|             if not absolute: | ||||
|                 pos += current_pos.real | ||||
|             pen.lineTo((pos.real, pos.imag)) | ||||
|             current_pos = pos | ||||
|  | ||||
|         elif command == "V": | ||||
|             y = elements.pop() | ||||
|             pos = current_pos.real + float(y) * 1j | ||||
|             if not absolute: | ||||
|                 pos += current_pos.imag * 1j | ||||
|             pen.lineTo((pos.real, pos.imag)) | ||||
|             current_pos = pos | ||||
|  | ||||
|         elif command == "C": | ||||
|             control1 = float(elements.pop()) + float(elements.pop()) * 1j | ||||
|             control2 = float(elements.pop()) + float(elements.pop()) * 1j | ||||
|             end = float(elements.pop()) + float(elements.pop()) * 1j | ||||
|  | ||||
|             if not absolute: | ||||
|                 control1 += current_pos | ||||
|                 control2 += current_pos | ||||
|                 end += current_pos | ||||
|  | ||||
|             pen.curveTo( | ||||
|                 (control1.real, control1.imag), | ||||
|                 (control2.real, control2.imag), | ||||
|                 (end.real, end.imag), | ||||
|             ) | ||||
|             current_pos = end | ||||
|             last_control = control2 | ||||
|  | ||||
|         elif command == "S": | ||||
|             # Smooth curve. First control point is the "reflection" of | ||||
|             # the second control point in the previous path. | ||||
|  | ||||
|             if last_command not in "CS": | ||||
|                 # If there is no previous command or if the previous command | ||||
|                 # was not an C, c, S or s, assume the first control point is | ||||
|                 # coincident with the current point. | ||||
|                 control1 = current_pos | ||||
|             else: | ||||
|                 # The first control point is assumed to be the reflection of | ||||
|                 # the second control point on the previous command relative | ||||
|                 # to the current point. | ||||
|                 control1 = current_pos + current_pos - last_control | ||||
|  | ||||
|             control2 = float(elements.pop()) + float(elements.pop()) * 1j | ||||
|             end = float(elements.pop()) + float(elements.pop()) * 1j | ||||
|  | ||||
|             if not absolute: | ||||
|                 control2 += current_pos | ||||
|                 end += current_pos | ||||
|  | ||||
|             pen.curveTo( | ||||
|                 (control1.real, control1.imag), | ||||
|                 (control2.real, control2.imag), | ||||
|                 (end.real, end.imag), | ||||
|             ) | ||||
|             current_pos = end | ||||
|             last_control = control2 | ||||
|  | ||||
|         elif command == "Q": | ||||
|             control = float(elements.pop()) + float(elements.pop()) * 1j | ||||
|             end = float(elements.pop()) + float(elements.pop()) * 1j | ||||
|  | ||||
|             if not absolute: | ||||
|                 control += current_pos | ||||
|                 end += current_pos | ||||
|  | ||||
|             pen.qCurveTo((control.real, control.imag), (end.real, end.imag)) | ||||
|             current_pos = end | ||||
|             last_control = control | ||||
|  | ||||
|         elif command == "T": | ||||
|             # Smooth curve. Control point is the "reflection" of | ||||
|             # the second control point in the previous path. | ||||
|  | ||||
|             if last_command not in "QT": | ||||
|                 # If there is no previous command or if the previous command | ||||
|                 # was not an Q, q, T or t, assume the first control point is | ||||
|                 # coincident with the current point. | ||||
|                 control = current_pos | ||||
|             else: | ||||
|                 # The control point is assumed to be the reflection of | ||||
|                 # the control point on the previous command relative | ||||
|                 # to the current point. | ||||
|                 control = current_pos + current_pos - last_control | ||||
|  | ||||
|             end = float(elements.pop()) + float(elements.pop()) * 1j | ||||
|  | ||||
|             if not absolute: | ||||
|                 end += current_pos | ||||
|  | ||||
|             pen.qCurveTo((control.real, control.imag), (end.real, end.imag)) | ||||
|             current_pos = end | ||||
|             last_control = control | ||||
|  | ||||
|         elif command == "A": | ||||
|             rx = abs(float(elements.pop())) | ||||
|             ry = abs(float(elements.pop())) | ||||
|             rotation = float(elements.pop()) | ||||
|             arc_large = bool(int(elements.pop())) | ||||
|             arc_sweep = bool(int(elements.pop())) | ||||
|             end = float(elements.pop()) + float(elements.pop()) * 1j | ||||
|  | ||||
|             if not absolute: | ||||
|                 end += current_pos | ||||
|  | ||||
|             # if the pen supports arcs, pass the values unchanged, otherwise | ||||
|             # approximate the arc with a series of cubic bezier curves | ||||
|             if have_arcTo: | ||||
|                 pen.arcTo( | ||||
|                     rx, | ||||
|                     ry, | ||||
|                     rotation, | ||||
|                     arc_large, | ||||
|                     arc_sweep, | ||||
|                     (end.real, end.imag), | ||||
|                 ) | ||||
|             else: | ||||
|                 arc = arc_class( | ||||
|                     current_pos, rx, ry, rotation, arc_large, arc_sweep, end | ||||
|                 ) | ||||
|                 arc.draw(pen) | ||||
|  | ||||
|             current_pos = end | ||||
|  | ||||
|     # no final Z command, it's an open path | ||||
|     if start_pos is not None: | ||||
|         pen.endPath() | ||||
| @ -0,0 +1,183 @@ | ||||
| import re | ||||
|  | ||||
|  | ||||
| def _prefer_non_zero(*args): | ||||
|     for arg in args: | ||||
|         if arg != 0: | ||||
|             return arg | ||||
|     return 0.0 | ||||
|  | ||||
|  | ||||
| def _ntos(n): | ||||
|     # %f likes to add unnecessary 0's, %g isn't consistent about # decimals | ||||
|     return ("%.3f" % n).rstrip("0").rstrip(".") | ||||
|  | ||||
|  | ||||
| def _strip_xml_ns(tag): | ||||
|     # ElementTree API doesn't provide a way to ignore XML namespaces in tags | ||||
|     # so we here strip them ourselves: cf. https://bugs.python.org/issue18304 | ||||
|     return tag.split("}", 1)[1] if "}" in tag else tag | ||||
|  | ||||
|  | ||||
| def _transform(raw_value): | ||||
|     # TODO assumes a 'matrix' transform. | ||||
|     # No other transform functions are supported at the moment. | ||||
|     # https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/transform | ||||
|     # start simple: if you aren't exactly matrix(...) then no love | ||||
|     match = re.match(r"matrix\((.*)\)", raw_value) | ||||
|     if not match: | ||||
|         raise NotImplementedError | ||||
|     matrix = tuple(float(p) for p in re.split(r"\s+|,", match.group(1))) | ||||
|     if len(matrix) != 6: | ||||
|         raise ValueError("wrong # of terms in %s" % raw_value) | ||||
|     return matrix | ||||
|  | ||||
|  | ||||
| class PathBuilder(object): | ||||
|     def __init__(self): | ||||
|         self.paths = [] | ||||
|         self.transforms = [] | ||||
|  | ||||
|     def _start_path(self, initial_path=""): | ||||
|         self.paths.append(initial_path) | ||||
|         self.transforms.append(None) | ||||
|  | ||||
|     def _end_path(self): | ||||
|         self._add("z") | ||||
|  | ||||
|     def _add(self, path_snippet): | ||||
|         path = self.paths[-1] | ||||
|         if path: | ||||
|             path += " " + path_snippet | ||||
|         else: | ||||
|             path = path_snippet | ||||
|         self.paths[-1] = path | ||||
|  | ||||
|     def _move(self, c, x, y): | ||||
|         self._add("%s%s,%s" % (c, _ntos(x), _ntos(y))) | ||||
|  | ||||
|     def M(self, x, y): | ||||
|         self._move("M", x, y) | ||||
|  | ||||
|     def m(self, x, y): | ||||
|         self._move("m", x, y) | ||||
|  | ||||
|     def _arc(self, c, rx, ry, x, y, large_arc): | ||||
|         self._add( | ||||
|             "%s%s,%s 0 %d 1 %s,%s" | ||||
|             % (c, _ntos(rx), _ntos(ry), large_arc, _ntos(x), _ntos(y)) | ||||
|         ) | ||||
|  | ||||
|     def A(self, rx, ry, x, y, large_arc=0): | ||||
|         self._arc("A", rx, ry, x, y, large_arc) | ||||
|  | ||||
|     def a(self, rx, ry, x, y, large_arc=0): | ||||
|         self._arc("a", rx, ry, x, y, large_arc) | ||||
|  | ||||
|     def _vhline(self, c, x): | ||||
|         self._add("%s%s" % (c, _ntos(x))) | ||||
|  | ||||
|     def H(self, x): | ||||
|         self._vhline("H", x) | ||||
|  | ||||
|     def h(self, x): | ||||
|         self._vhline("h", x) | ||||
|  | ||||
|     def V(self, y): | ||||
|         self._vhline("V", y) | ||||
|  | ||||
|     def v(self, y): | ||||
|         self._vhline("v", y) | ||||
|  | ||||
|     def _line(self, c, x, y): | ||||
|         self._add("%s%s,%s" % (c, _ntos(x), _ntos(y))) | ||||
|  | ||||
|     def L(self, x, y): | ||||
|         self._line("L", x, y) | ||||
|  | ||||
|     def l(self, x, y): | ||||
|         self._line("l", x, y) | ||||
|  | ||||
|     def _parse_line(self, line): | ||||
|         x1 = float(line.attrib.get("x1", 0)) | ||||
|         y1 = float(line.attrib.get("y1", 0)) | ||||
|         x2 = float(line.attrib.get("x2", 0)) | ||||
|         y2 = float(line.attrib.get("y2", 0)) | ||||
|  | ||||
|         self._start_path() | ||||
|         self.M(x1, y1) | ||||
|         self.L(x2, y2) | ||||
|  | ||||
|     def _parse_rect(self, rect): | ||||
|         x = float(rect.attrib.get("x", 0)) | ||||
|         y = float(rect.attrib.get("y", 0)) | ||||
|         w = float(rect.attrib.get("width")) | ||||
|         h = float(rect.attrib.get("height")) | ||||
|         rx = float(rect.attrib.get("rx", 0)) | ||||
|         ry = float(rect.attrib.get("ry", 0)) | ||||
|  | ||||
|         rx = _prefer_non_zero(rx, ry) | ||||
|         ry = _prefer_non_zero(ry, rx) | ||||
|         # TODO there are more rules for adjusting rx, ry | ||||
|  | ||||
|         self._start_path() | ||||
|         self.M(x + rx, y) | ||||
|         self.H(x + w - rx) | ||||
|         if rx > 0: | ||||
|             self.A(rx, ry, x + w, y + ry) | ||||
|         self.V(y + h - ry) | ||||
|         if rx > 0: | ||||
|             self.A(rx, ry, x + w - rx, y + h) | ||||
|         self.H(x + rx) | ||||
|         if rx > 0: | ||||
|             self.A(rx, ry, x, y + h - ry) | ||||
|         self.V(y + ry) | ||||
|         if rx > 0: | ||||
|             self.A(rx, ry, x + rx, y) | ||||
|         self._end_path() | ||||
|  | ||||
|     def _parse_path(self, path): | ||||
|         if "d" in path.attrib: | ||||
|             self._start_path(initial_path=path.attrib["d"]) | ||||
|  | ||||
|     def _parse_polygon(self, poly): | ||||
|         if "points" in poly.attrib: | ||||
|             self._start_path("M" + poly.attrib["points"]) | ||||
|             self._end_path() | ||||
|  | ||||
|     def _parse_polyline(self, poly): | ||||
|         if "points" in poly.attrib: | ||||
|             self._start_path("M" + poly.attrib["points"]) | ||||
|  | ||||
|     def _parse_circle(self, circle): | ||||
|         cx = float(circle.attrib.get("cx", 0)) | ||||
|         cy = float(circle.attrib.get("cy", 0)) | ||||
|         r = float(circle.attrib.get("r")) | ||||
|  | ||||
|         # arc doesn't seem to like being a complete shape, draw two halves | ||||
|         self._start_path() | ||||
|         self.M(cx - r, cy) | ||||
|         self.A(r, r, cx + r, cy, large_arc=1) | ||||
|         self.A(r, r, cx - r, cy, large_arc=1) | ||||
|  | ||||
|     def _parse_ellipse(self, ellipse): | ||||
|         cx = float(ellipse.attrib.get("cx", 0)) | ||||
|         cy = float(ellipse.attrib.get("cy", 0)) | ||||
|         rx = float(ellipse.attrib.get("rx")) | ||||
|         ry = float(ellipse.attrib.get("ry")) | ||||
|  | ||||
|         # arc doesn't seem to like being a complete shape, draw two halves | ||||
|         self._start_path() | ||||
|         self.M(cx - rx, cy) | ||||
|         self.A(rx, ry, cx + rx, cy, large_arc=1) | ||||
|         self.A(rx, ry, cx - rx, cy, large_arc=1) | ||||
|  | ||||
|     def add_path_from_element(self, el): | ||||
|         tag = _strip_xml_ns(el.tag) | ||||
|         parse_fn = getattr(self, "_parse_%s" % tag.lower(), None) | ||||
|         if not callable(parse_fn): | ||||
|             return False | ||||
|         parse_fn(el) | ||||
|         if "transform" in el.attrib: | ||||
|             self.transforms[-1] = _transform(el.attrib["transform"]) | ||||
|         return True | ||||
		Reference in New Issue
	
	Block a user