summaryrefslogtreecommitdiffstats
path: root/contrib/python/fonttools/fontTools/svgLib/path/parser.py
diff options
context:
space:
mode:
authorzverevgeny <[email protected]>2025-05-13 19:00:02 +0300
committerzverevgeny <[email protected]>2025-05-13 19:13:54 +0300
commit92e06374736aa28637dc0e706455b65c8268a5e6 (patch)
tree3df370c199ae25d308e542f02af20f43eab78f8a /contrib/python/fonttools/fontTools/svgLib/path/parser.py
parentdc63d5794da99c2ebe3f32914d0351d9707660b0 (diff)
Import matplotlib
commit_hash:d59c2338025ef8fd1e1f961ed9d8d5fd52d0bd96
Diffstat (limited to 'contrib/python/fonttools/fontTools/svgLib/path/parser.py')
-rw-r--r--contrib/python/fonttools/fontTools/svgLib/path/parser.py322
1 files changed, 322 insertions, 0 deletions
diff --git a/contrib/python/fonttools/fontTools/svgLib/path/parser.py b/contrib/python/fonttools/fontTools/svgLib/path/parser.py
new file mode 100644
index 00000000000..18c8e77f7f7
--- /dev/null
+++ b/contrib/python/fonttools/fontTools/svgLib/path/parser.py
@@ -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()