Module fpdf.drawing
Expand source code
import copy, decimal, math, re
from collections import OrderedDict
from contextlib import contextmanager
from typing import Optional, NamedTuple, Union
from .enums import (
BlendMode,
ClippingPathIntersectionRule,
IntersectionRule,
PathPaintRule,
StrokeCapStyle,
StrokeJoinStyle,
PDFStyleKeys,
)
from .syntax import Name, Raw
from .util import escape_parens
__pdoc__ = {"force_nodocument": False}
def force_nodocument(item):
"""A decorator that forces pdoc not to document the decorated item (class or method)"""
__pdoc__[item.__qualname__] = False
return item
@force_nodocument
def force_document(item):
"""A decorator that forces pdoc to document the decorated item (class or method)"""
__pdoc__[item.__qualname__] = True
return item
# type alias:
Number = Union[int, float, decimal.Decimal]
NumberClass = (int, float, decimal.Decimal)
WHITESPACE = frozenset("\0\t\n\f\r ")
"""Characters PDF considers to be whitespace."""
EOL_CHARS = frozenset("\n\r")
"""Characters PDF considers to mark the end of a line."""
DELIMITERS = frozenset("()<>[]{}/%")
"""Special delimiter characters"""
STR_ESC = re.compile(r"[\n\r\t\b\f()\\]")
STR_ESC_MAP = {
"\n": r"\n",
"\r": r"\r",
"\t": r"\t",
"\b": r"\b",
"\f": r"\f",
"(": r"\(",
")": r"\)",
"\\": r"\\",
}
class GraphicsStateDictRegistry(OrderedDict):
"""
A container providing deduplication of graphics state dictionaries across a PDF.
"""
def register_style(self, style: "GraphicsStyle"):
sdict = style.serialize()
# empty style does not need a dictionary
if not sdict:
return None
try:
return self[sdict]
except KeyError:
pass
name = Name(f"GS{len(self)}")
self[sdict] = name
return name
def _check_range(value, minimum=0.0, maximum=1.0):
if not minimum <= value <= maximum:
raise ValueError(f"{value} not in range [{minimum}, {maximum}]")
return value
def number_to_str(number):
"""
Convert a decimal number to a minimal string representation (no trailing 0 or .).
Args:
number (Number): the number to be converted to a string.
Returns:
The number's string representation.
"""
# this approach tries to produce minimal representations of floating point numbers
# but can also produce "-0".
return f"{number:.4f}".rstrip("0").rstrip(".")
# this maybe should live in fpdf.syntax
def render_pdf_primitive(primitive):
"""
Render a Python value as a PDF primitive type.
Container types (tuples/lists and dicts) are rendered recursively. This supports
values of the type Name, str, bytes, numbers, booleans, list/tuple, and dict.
Any custom type can be passed in as long as it provides a `serialize` method that
takes no arguments and returns a string. The primitive object is returned directly
if it is an instance of the `Raw` class. Otherwise, The existence of the `serialize`
method is checked before any other type checking is performed, so, for example, a
`dict` subclass with a `serialize` method would be converted using its `pdf_repr`
method rather than the built-in `dict` conversion process.
Args:
primitive: the primitive value to convert to its PDF representation.
Returns:
Raw-wrapped str of the PDF representation.
Raises:
ValueError: if a dictionary key is not a Name.
TypeError: if `primitive` does not have a known conversion to a PDF
representation.
"""
if isinstance(primitive, Raw):
return primitive
if callable(getattr(primitive, "serialize", None)):
output = primitive.serialize()
elif primitive is None:
output = "null"
elif isinstance(primitive, str):
output = f"({escape_parens(primitive)})"
elif isinstance(primitive, bytes):
output = f"<{primitive.hex()}>"
elif isinstance(primitive, bool): # has to come before number check
output = ["false", "true"][primitive]
elif isinstance(primitive, NumberClass):
output = number_to_str(primitive)
elif isinstance(primitive, (list, tuple)):
output = "[" + " ".join(render_pdf_primitive(val) for val in primitive) + "]"
elif isinstance(primitive, dict):
item_list = []
for key, val in primitive.items():
if not isinstance(key, Name):
raise ValueError("dict keys must be Names")
item_list.append(
render_pdf_primitive(key) + " " + render_pdf_primitive(val)
)
output = "<< " + "\n".join(item_list) + " >>"
else:
raise TypeError(f"cannot produce PDF representation for value {primitive!r}")
return Raw(output)
# We allow passing alpha in as None instead of a numeric quantity, which signals to the
# rendering procedure not to emit an explicit alpha field for this graphics state,
# causing it to be inherited from the parent.
# this weird inheritance is used because for some reason normal NamedTuple usage doesn't
# allow overriding __new__, even though it works just as expected this way.
class DeviceRGB(
NamedTuple(
"DeviceRGB",
[("r", Number), ("g", Number), ("b", Number), ("a", Optional[Number])],
)
):
"""A class representing a PDF DeviceRGB color."""
# This follows a common PDF drawing operator convention where the operand is upcased
# to apply to stroke and downcased to apply to fill.
# This could be more manually specified by `CS`/`cs` to set the color space(e.g. to
# `/DeviceRGB`) and `SC`/`sc` to set the color parameters. The documentation isn't
# perfectly clear on this front, but it appears that these cannot be set in the
# current graphics state dictionary and instead is set in the current page resource
# dictionary. fpdf appears to only generate a single resource dictionary for the
# entire document, and even if it created one per page, it would still be a lot
# clunkier to try to use that.
# Because PDF hates me, personally, the opacity of the drawing HAS to be specified
# in the current graphics state dictionary and does not exist as a standalone
# directive.
OPERATOR = "rg"
"""The PDF drawing operator used to specify this type of color."""
def __new__(cls, r, g, b, a=None):
if a is not None:
_check_range(a)
return super().__new__(
cls, _check_range(r), _check_range(g), _check_range(b), a
)
@property
def colors(self):
"""The color components as a tuple in order `(r, g, b)` with alpha omitted."""
return self[:-1]
def serialize(self) -> str:
return " ".join(number_to_str(val) for val in self.colors) + f" {self.OPERATOR}"
__pdoc__["DeviceRGB.OPERATOR"] = False
__pdoc__["DeviceRGB.r"] = "The red color component. Must be in the interval [0, 1]."
__pdoc__["DeviceRGB.g"] = "The green color component. Must be in the interval [0, 1]."
__pdoc__["DeviceRGB.b"] = "The blue color component. Must be in the interval [0, 1]."
__pdoc__[
"DeviceRGB.a"
] = """
The alpha color component (i.e. opacity). Must be `None` or in the interval [0, 1].
An alpha value of 0 makes the color fully transparent, and a value of 1 makes it fully
opaque. If `None`, the color will be interpreted as not specifying a particular
transparency rather than specifying fully transparent or fully opaque.
"""
# this weird inheritance is used because for some reason normal NamedTuple usage doesn't
# allow overriding __new__, even though it works just as expected this way.
class DeviceGray(
NamedTuple(
"DeviceGray",
[("g", Number), ("a", Optional[Number])],
)
):
"""A class representing a PDF DeviceGray color."""
OPERATOR = "g"
"""The PDF drawing operator used to specify this type of color."""
def __new__(cls, g, a=None):
if a is not None:
_check_range(a)
return super().__new__(cls, _check_range(g), a)
@property
def colors(self):
"""The color components as a tuple in order (g,) with alpha omitted."""
return self[:-1]
def serialize(self) -> str:
return " ".join(number_to_str(val) for val in self.colors) + f" {self.OPERATOR}"
__pdoc__["DeviceGray.OPERATOR"] = False
__pdoc__[
"DeviceGray.g"
] = """
The gray color component. Must be in the interval [0, 1].
A value of 0 represents black and a value of 1 represents white.
"""
__pdoc__[
"DeviceGray.a"
] = """
The alpha color component (i.e. opacity). Must be `None` or in the interval [0, 1].
An alpha value of 0 makes the color fully transparent, and a value of 1 makes it fully
opaque. If `None`, the color will be interpreted as not specifying a particular
transparency rather than specifying fully transparent or fully opaque.
"""
# this weird inheritance is used because for some reason normal NamedTuple usage doesn't
# allow overriding __new__, even though it works just as expected this way.
class DeviceCMYK(
NamedTuple(
"DeviceCMYK",
[
("c", Number),
("m", Number),
("y", Number),
("k", Number),
("a", Optional[Number]),
],
)
):
"""A class representing a PDF DeviceCMYK color."""
OPERATOR = "k"
"""The PDF drawing operator used to specify this type of color."""
def __new__(cls, c, m, y, k, a=None):
if a is not None:
_check_range(a)
return super().__new__(
cls, _check_range(c), _check_range(m), _check_range(y), _check_range(k), a
)
@property
def colors(self):
"""The color components as a tuple in order (c, m, y, k) with alpha omitted."""
return self[:-1]
def serialize(self) -> str:
return " ".join(number_to_str(val) for val in self.colors) + f" {self.OPERATOR}"
__pdoc__["DeviceCMYK.OPERATOR"] = False
__pdoc__["DeviceCMYK.c"] = "The cyan color component. Must be in the interval [0, 1]."
__pdoc__[
"DeviceCMYK.m"
] = "The magenta color component. Must be in the interval [0, 1]."
__pdoc__["DeviceCMYK.y"] = "The yellow color component. Must be in the interval [0, 1]."
__pdoc__["DeviceCMYK.k"] = "The black color component. Must be in the interval [0, 1]."
__pdoc__[
"DeviceCMYK.a"
] = """
The alpha color component (i.e. opacity). Must be `None` or in the interval [0, 1].
An alpha value of 0 makes the color fully transparent, and a value of 1 makes it fully
opaque. If `None`, the color will be interpreted as not specifying a particular
transparency rather than specifying fully transparent or fully opaque.
"""
def rgb8(r, g, b, a=None):
"""
Produce a DeviceRGB color from the given 8-bit RGB values.
Args:
r (Number): red color component. Must be in the interval [0, 255].
g (Number): green color component. Must be in the interval [0, 255].
b (Number): blue color component. Must be in the interval [0, 255].
a (Optional[Number]): alpha component. Must be `None` or in the interval
[0, 255]. 0 is fully transparent, 255 is fully opaque
Returns:
DeviceRGB color representation.
Raises:
ValueError: if any components are not in their valid interval.
"""
if a is not None:
a /= 255.0
return DeviceRGB(r / 255.0, g / 255.0, b / 255.0, a)
def gray8(g, a=None):
"""
Produce a DeviceGray color from the given 8-bit gray value.
Args:
g (Number): gray color component. Must be in the interval [0, 255]. 0 is black,
255 is white.
a (Optional[Number]): alpha component. Must be `None` or in the interval
[0, 255]. 0 is fully transparent, 255 is fully opaque
Returns:
DeviceGray color representation.
Raises:
ValueError: if any components are not in their valid interval.
"""
if a is not None:
a /= 255.0
return DeviceGray(g / 255.0, a)
def cmyk8(c, m, y, k, a=None):
"""
Produce a DeviceCMYK color from the given 8-bit CMYK values.
Args:
c (Number): red color component. Must be in the interval [0, 255].
m (Number): green color component. Must be in the interval [0, 255].
y (Number): blue color component. Must be in the interval [0, 255].
k (Number): blue color component. Must be in the interval [0, 255].
a (Optional[Number]): alpha component. Must be `None` or in the interval
[0, 255]. 0 is fully transparent, 255 is fully opaque
Returns:
DeviceCMYK color representation.
Raises:
ValueError: if any components are not in their valid interval.
"""
if a is not None:
a /= 255.0
return DeviceCMYK(c / 255.0, m / 255.0, y / 255.0, k / 255.0, a)
def color_from_hex_string(hexstr):
"""
Parse an RGB color from a css-style 8-bit hexadecimal color string.
Args:
hexstr (str): of the form `#RGB`, `#RGBA`, `#RRGGBB`, or `#RRGGBBAA`. Must
include the leading octothorp. Forms omitting the alpha field are
interpreted as not specifying the opacity, so it will not be explicitly set.
An alpha value of `00` is fully transparent and `FF` is fully opaque.
Returns:
DeviceRGB representation of the color.
"""
if not isinstance(hexstr, str):
raise TypeError(f"{hexstr} is not of type str")
if not hexstr.startswith("#"):
raise ValueError(f"{hexstr} does not start with #")
hlen = len(hexstr)
if hlen == 4:
return rgb8(*[int(char * 2, base=16) for char in hexstr[1:]], a=None)
if hlen == 5:
return rgb8(*[int(char * 2, base=16) for char in hexstr[1:]])
if hlen == 7:
return rgb8(
*[int(hexstr[idx : idx + 2], base=16) for idx in range(1, hlen, 2)], a=None
)
if hlen == 9:
return rgb8(*[int(hexstr[idx : idx + 2], base=16) for idx in range(1, hlen, 2)])
raise ValueError(f"{hexstr} could not be interpreted as a RGB(A) hex string")
def color_from_rgb_string(rgbstr):
"""
Parse an RGB color from a css-style rgb(R, G, B, A) color string.
Args:
rgbstr (str): of the form `rgb(R, G, B)` or `rgb(R, G, B, A)`.
Returns:
DeviceRGB representation of the color.
"""
if not isinstance(rgbstr, str):
raise TypeError(f"{rgbstr} is not of type str")
rgbstr = rgbstr.replace(" ", "")
if not rgbstr.startswith("rgb(") or not rgbstr.endswith(")"):
raise ValueError(f"{rgbstr} does not follow the expected rgb(...) format")
rgbstr = rgbstr[4:-1]
colors = rgbstr.split(",")
if len(colors) == 3:
return rgb8(*[int(c) for c in colors], a=None)
if len(colors) == 4:
return rgb8(*[int(c) for c in colors])
raise ValueError(f"{rgbstr} could not be interpreted as a rgb(R, G, B[, A]) color")
class Point(NamedTuple):
"""
An x-y coordinate pair within the two-dimensional coordinate frame.
"""
x: Number
"""The abscissa of the point."""
y: Number
"""The ordinate of the point."""
def render(self):
"""Render the point to the string `"x y"` for emitting to a PDF."""
return f"{number_to_str(self.x)} {number_to_str(self.y)}"
def dot(self, other):
"""
Compute the dot product of two points.
Args:
other (Point): the point with which to compute the dot product.
Returns:
The scalar result of the dot product computation.
Raises:
TypeError: if `other` is not a `Point`.
"""
if not isinstance(other, Point):
raise TypeError(f"cannot dot with {other!r}")
return self.x * other.x + self.y * other.y
def angle(self, other):
"""
Compute the angle between two points (interpreted as vectors from the origin).
The return value is in the interval (-pi, pi]. Sign is dependent on ordering,
with clockwise angle travel considered to be positive due to the orientation of
the coordinate frame basis vectors (i.e. the angle between `(1, 0)` and `(0, 1)`
is `+pi/2`, the angle between `(1, 0)` and `(0, -1)` is `-pi/2`, and the angle
between `(0, -1)` and `(1, 0)` is `+pi/2`).
Args:
other (Point): the point to compute the angle sweep toward.
Returns:
The scalar angle between the two points **in radians**.
Raises:
TypeError: if `other` is not a `Point`.
"""
if not isinstance(other, Point):
raise TypeError(f"cannot compute angle with {other!r}")
signifier = (self.x * other.y) - (self.y * other.x)
sign = (signifier >= 0) - (signifier < 0)
return sign * math.acos(round(self.dot(other) / (self.mag() * other.mag()), 8))
def mag(self):
"""
Compute the Cartesian distance from this point to the origin
This is the same as computing the magnitude of the vector represented by this
point.
Returns:
The scalar result of the distance computation.
"""
return (self.x**2 + self.y**2) ** 0.5
@force_document
def __add__(self, other):
"""
Produce the sum of two points.
Adding two points is the same as translating the source point by interpreting
the other point's x and y coordinates as distances.
Args:
other (Point): right-hand side of the infix addition operation
Returns:
A Point which is the sum of the two source points.
"""
if isinstance(other, Point):
return Point(x=self.x + other.x, y=self.y + other.y)
return NotImplemented
@force_document
def __sub__(self, other):
"""
Produce the difference between two points.
Unlike addition, this is not a commutative operation!
Args:
other (Point): right-hand side of the infix subtraction operation
Returns:
A Point which is the difference of the two source points.
"""
if isinstance(other, Point):
return Point(x=self.x - other.x, y=self.y - other.y)
return NotImplemented
@force_document
def __neg__(self):
"""
Produce a point by negating this point's coordinates.
Returns:
A Point whose coordinates are this points coordinates negated.
"""
return Point(x=-self.x, y=-self.y)
@force_document
def __mul__(self, other):
"""
Multiply a point by a scalar value.
Args:
other (Number): the scalar value by which to multiply the point's
coordinates.
Returns:
A Point whose coordinates are the result of the multiplication.
"""
if isinstance(other, NumberClass):
return Point(self.x * other, self.y * other)
return NotImplemented
__rmul__ = __mul__
@force_document
def __truediv__(self, other):
"""
Divide a point by a scalar value.
.. note::
Because division is not commutative, `Point / scalar` is implemented, but
`scalar / Point` is nonsensical and not implemented.
Args:
other (Number): the scalar value by which to divide the point's coordinates.
Returns:
A Point whose coordinates are the result of the division.
"""
if isinstance(other, NumberClass):
return Point(self.x / other, self.y / other)
return NotImplemented
@force_document
def __floordiv__(self, other):
"""
Divide a point by a scalar value using integer division.
.. note::
Because division is not commutative, `Point // scalar` is implemented, but
`scalar // Point` is nonsensical and not implemented.
Args:
other (Number): the scalar value by which to divide the point's coordinates.
Returns:
A Point whose coordinates are the result of the division.
"""
if isinstance(other, NumberClass):
return Point(self.x // other, self.y // other)
return NotImplemented
# no __r(true|floor)div__ because division is not commutative!
@force_document
def __matmul__(self, other):
"""
Transform a point with the given transform matrix.
.. note::
This operator is only implemented for Transforms. This transform is not
commutative, so `Point @ Transform` is implemented, but `Transform @ Point`
is not implemented (technically speaking, the current implementation is
commutative because of the way points and transforms are represented, but
if that representation were to change this operation could stop being
commutative)
Args:
other (Transform): the transform to apply to the point
Returns:
A Point whose coordinates are the result of applying the transform.
"""
if isinstance(other, Transform):
return Point(
x=other.a * self.x + other.c * self.y + other.e,
y=other.b * self.x + other.d * self.y + other.f,
)
return NotImplemented
def __str__(self):
return f"(x={number_to_str(self.x)}, y={number_to_str(self.y)})"
class Transform(NamedTuple):
"""
A representation of an affine transformation matrix for 2D shapes.
The actual matrix is:
```
[ a b 0 ]
[x' y' 1] = [x y 1] [ c d 0 ]
[ e f 1 ]
```
Complex transformation operations can be composed via a sequence of simple
transformations by performing successive matrix multiplication of the simple
transformations.
For example, scaling a set of points around a specific center point can be
represented by a translation-scale-translation sequence, where the first
translation translates the center to the origin, the scale transform scales the
points relative to the origin, and the second translation translates the points
back to the specified center point. Transform multiplication is performed using
python's dedicated matrix multiplication operator, `@`
The semantics of this representation mean composed transformations are specified
left-to-right in order of application (some other systems provide transposed
representations, in which case the application order is right-to-left).
For example, to rotate the square `(1,1) (1,3) (3,3) (3,1)` 45 degrees clockwise
about its center point (which is `(2,2)`) , the translate-rotate-translate
process described above may be applied:
```python
rotate_centered = (
Transform.translation(-2, -2)
@ Transform.rotation_d(45)
@ Transform.translation(2, 2)
)
```
Instances of this class provide a chaining API, so the above transform could also be
constructed as follows:
```python
rotate_centered = Transform.translation(-2, -2).rotate_d(45).translate(2, 2)
```
Or, because the particular operation of performing some transformations about a
specific point is pretty common,
```python
rotate_centered = Transform.rotation_d(45).about(2, 2)
```
By convention, this class provides class method constructors following noun-ish
naming (`translation`, `scaling`, `rotation`, `shearing`) and instance method
manipulations following verb-ish naming (`translate`, `scale`, `rotate`, `shear`).
"""
a: Number
b: Number
c: Number
d: Number
e: Number
f: Number
# compact representation of an affine transformation matrix for 2D shapes.
# The actual matrix is:
# [ A B 0 ]
# [x' y' 1] = [x y 1] [ C D 0 ]
# [ E F 1 ]
# The identity transform is 1 0 0 1 0 0
@classmethod
def identity(cls):
"""
Create a transform representing the identity transform.
The identity transform is a no-op.
"""
return cls(1, 0, 0, 1, 0, 0)
@classmethod
def translation(cls, x, y):
"""
Create a transform that performs translation.
Args:
x (Number): distance to translate points along the x (horizontal) axis.
y (Number): distance to translate points along the y (vertical) axis.
Returns:
A Transform representing the specified translation.
"""
return cls(1, 0, 0, 1, x, y)
@classmethod
def scaling(cls, x, y=None):
"""
Create a transform that performs scaling.
Args:
x (Number): scaling ratio in the x (horizontal) axis. A value of 1
results in no scale change in the x axis.
y (Number): optional scaling ratio in the y (vertical) axis. A value of 1
results in no scale change in the y axis. If this value is omitted, it
defaults to the value provided to the `x` argument.
Returns:
A Transform representing the specified scaling.
"""
if y is None:
y = x
return cls(x, 0, 0, y, 0, 0)
@classmethod
def rotation(cls, theta):
"""
Create a transform that performs rotation.
Args:
theta (Number): the angle **in radians** by which to rotate. Positive
values represent clockwise rotations.
Returns:
A Transform representing the specified rotation.
"""
return cls(
math.cos(theta), math.sin(theta), -math.sin(theta), math.cos(theta), 0, 0
)
@classmethod
def rotation_d(cls, theta_d):
"""
Create a transform that performs rotation **in degrees**.
Args:
theta_d (Number): the angle **in degrees** by which to rotate. Positive
values represent clockwise rotations.
Returns:
A Transform representing the specified rotation.
"""
return cls.rotation(math.radians(theta_d))
@classmethod
def shearing(cls, x, y=None):
"""
Create a transform that performs shearing (not of sheep).
Args:
x (Number): The amount to shear along the x (horizontal) axis.
y (Number): Optional amount to shear along the y (vertical) axis. If omitted,
this defaults to the value provided to the `x` argument.
Returns:
A Transform representing the specified shearing.
"""
if y is None:
y = x
return cls(1, y, x, 1, 0, 0)
def translate(self, x, y):
"""
Produce a transform by composing the current transform with a translation.
.. note::
Transforms are immutable, so this returns a new transform rather than
mutating self.
Args:
x (Number): distance to translate points along the x (horizontal) axis.
y (Number): distance to translate points along the y (vertical) axis.
Returns:
A Transform representing the composed transform.
"""
return self @ Transform.translation(x, y)
def scale(self, x, y=None):
"""
Produce a transform by composing the current transform with a scaling.
.. note::
Transforms are immutable, so this returns a new transform rather than
mutating self.
Args:
x (Number): scaling ratio in the x (horizontal) axis. A value of 1
results in no scale change in the x axis.
y (Number): optional scaling ratio in the y (vertical) axis. A value of 1
results in no scale change in the y axis. If this value is omitted, it
defaults to the value provided to the `x` argument.
Returns:
A Transform representing the composed transform.
"""
return self @ Transform.scaling(x, y)
def rotate(self, theta):
"""
Produce a transform by composing the current transform with a rotation.
.. note::
Transforms are immutable, so this returns a new transform rather than
mutating self.
Args:
theta (Number): the angle **in radians** by which to rotate. Positive
values represent clockwise rotations.
Returns:
A Transform representing the composed transform.
"""
return self @ Transform.rotation(theta)
def rotate_d(self, theta_d):
"""
Produce a transform by composing the current transform with a rotation
**in degrees**.
.. note::
Transforms are immutable, so this returns a new transform rather than
mutating self.
Args:
theta_d (Number): the angle **in degrees** by which to rotate. Positive
values represent clockwise rotations.
Returns:
A Transform representing the composed transform.
"""
return self @ Transform.rotation_d(theta_d)
def shear(self, x, y=None):
"""
Produce a transform by composing the current transform with a shearing.
.. note::
Transforms are immutable, so this returns a new transform rather than
mutating self.
Args:
x (Number): The amount to shear along the x (horizontal) axis.
y (Number): Optional amount to shear along the y (vertical) axis. If omitted,
this defaults to the value provided to the `x` argument.
Returns:
A Transform representing the composed transform.
"""
return self @ Transform.shearing(x, y)
def about(self, x, y):
"""
Bracket the given transform in a pair of translations to make it appear about a
point that isn't the origin.
This is a useful shorthand for performing a transform like a rotation around the
center point of an object that isn't centered at the origin.
.. note::
Transforms are immutable, so this returns a new transform rather than
mutating self.
Args:
x (Number): the point along the x (horizontal) axis about which to transform.
y (Number): the point along the y (vertical) axis about which to transform.
Returns:
A Transform representing the composed transform.
"""
return Transform.translation(-x, -y) @ self @ Transform.translation(x, y)
@force_document
def __mul__(self, other):
"""
Multiply the individual transform parameters by a scalar value.
Args:
other (Number): the scalar value by which to multiply the parameters
Returns:
A Transform with the modified parameters.
"""
if isinstance(other, NumberClass):
return Transform(
a=self.a * other,
b=self.b * other,
c=self.c * other,
d=self.d * other,
e=self.e * other,
f=self.f * other,
)
return NotImplemented
# scalar multiplication is commutative
__rmul__ = __mul__
@force_document
def __matmul__(self, other):
"""
Compose two transforms into a single transform.
Args:
other (Transform): the right-hand side transform of the infix operator.
Returns:
A Transform representing the composed transform.
"""
if isinstance(other, Transform):
return self.__class__(
a=self.a * other.a + self.b * other.c,
b=self.a * other.b + self.b * other.d,
c=self.c * other.a + self.d * other.c,
d=self.c * other.b + self.d * other.d,
e=self.e * other.a + self.f * other.c + other.e,
f=self.e * other.b + self.f * other.d + other.f,
)
return NotImplemented
def render(self, last_item):
"""
Render the transform to its PDF output representation.
Args:
last_item: the last path element this transform applies to
Returns:
A tuple of `(str, last_item)`. `last_item` is returned unchanged.
"""
return (
f"{number_to_str(self.a)} {number_to_str(self.b)} "
f"{number_to_str(self.c)} {number_to_str(self.d)} "
f"{number_to_str(self.e)} {number_to_str(self.f)} cm",
last_item,
)
def __str__(self):
return (
f"transform: ["
f"{number_to_str(self.a)} {number_to_str(self.b)} 0; "
f"{number_to_str(self.c)} {number_to_str(self.d)} 0; "
f"{number_to_str(self.e)} {number_to_str(self.f)} 1]"
)
__pdoc__["Transform.a"] = False
__pdoc__["Transform.b"] = False
__pdoc__["Transform.c"] = False
__pdoc__["Transform.d"] = False
__pdoc__["Transform.e"] = False
__pdoc__["Transform.f"] = False
class GraphicsStyle:
"""
A class representing various style attributes that determine drawing appearance.
This class uses the convention that the global Python singleton ellipsis (`...`) is
exclusively used to represent values that are inherited from the parent style. This
is to disambiguate the value None which is used for several values to signal an
explicitly disabled style. An example of this is the fill/stroke color styles,
which use None as hints to the auto paint style detection code.
"""
INHERIT = ...
"""Singleton specifying a style parameter should be inherited from the parent context."""
# order is be important here because some of these properties are entangled, e.g.
# fill_color and fill_opacity
MERGE_PROPERTIES = (
"paint_rule",
"allow_transparency",
"auto_close",
"intersection_rule",
"fill_color",
"fill_opacity",
"stroke_color",
"stroke_opacity",
"blend_mode",
"stroke_width",
"stroke_cap_style",
"stroke_join_style",
"stroke_miter_limit",
"stroke_dash_pattern",
"stroke_dash_phase",
)
"""An ordered collection of properties to use when merging two GraphicsStyles."""
TRANSPARENCY_KEYS = (
PDFStyleKeys.FILL_ALPHA.value,
PDFStyleKeys.STROKE_ALPHA.value,
PDFStyleKeys.BLEND_MODE.value,
)
"""An ordered collection of attributes not to emit in no transparency mode."""
PDF_STYLE_KEYS = (
*(k.value for k in PDFStyleKeys if k is not PDFStyleKeys.STROKE_DASH_PATTERN),
)
"""An ordered collection of keys to directly emit when serializing the style."""
_PAINT_RULE_LOOKUP = {
frozenset({}): PathPaintRule.DONT_PAINT,
frozenset({"stroke"}): PathPaintRule.STROKE,
frozenset({"fill", IntersectionRule.NONZERO}): PathPaintRule.FILL_NONZERO,
frozenset({"fill", IntersectionRule.EVENODD}): PathPaintRule.FILL_EVENODD,
frozenset(
{"stroke", "fill", IntersectionRule.NONZERO}
): PathPaintRule.STROKE_FILL_NONZERO,
frozenset(
{"stroke", "fill", IntersectionRule.EVENODD}
): PathPaintRule.STROKE_FILL_EVENODD,
}
"""A dictionary for resolving `PathPaintRule.AUTO`"""
@classmethod
def merge(cls, parent, child):
"""
Merge parent and child into a single GraphicsStyle.
The result contains the properties of the parent as overridden by any properties
explicitly set on the child. If both the parent and the child specify to
inherit a given property, that property will preserve the inherit value.
"""
new = cls()
for prop in cls.MERGE_PROPERTIES:
cval = getattr(child, prop)
if cval is cls.INHERIT:
setattr(new, prop, getattr(parent, prop))
else:
setattr(new, prop, cval)
return new
def __init__(self):
self.allow_transparency = self.INHERIT
self.paint_rule = self.INHERIT
self.auto_close = self.INHERIT
self.intersection_rule = self.INHERIT
self.fill_color = self.INHERIT
self.fill_opacity = self.INHERIT
self.stroke_color = self.INHERIT
self.stroke_opacity = self.INHERIT
self.blend_mode = self.INHERIT
self.stroke_width = self.INHERIT
self.stroke_cap_style = self.INHERIT
self.stroke_join_style = self.INHERIT
self.stroke_miter_limit = self.INHERIT
self.stroke_dash_pattern = self.INHERIT
self.stroke_dash_phase = self.INHERIT
def __deepcopy__(self, memo):
copied = self.__class__()
for prop in self.MERGE_PROPERTIES:
setattr(copied, prop, getattr(self, prop))
return copied
def __setattr__(self, name, value):
if not hasattr(self.__class__, name):
raise AttributeError(
f'{self.__class__} does not have style "{name}" (a typo?)'
)
super().__setattr__(name, value)
# at some point it probably makes sense to turn this into a general compliance
# property, but for now this is the simple approach.
@property
def allow_transparency(self):
return self._allow_transparency # pylint: disable=no-member
@allow_transparency.setter
def allow_transparency(self, new):
return super().__setattr__("_allow_transparency", new)
# If these are used in a nested graphics context inside of a painting path
# operation, they are no-ops. However, they can be used for outer GraphicsContexts
# that painting paths inherit from.
@property
def paint_rule(self):
"""The paint rule to use for this path/group."""
return self._paint_rule # pylint: disable=no-member
@paint_rule.setter
def paint_rule(self, new):
if new is None:
super().__setattr__("_paint_rule", PathPaintRule.DONT_PAINT)
elif new is self.INHERIT:
super().__setattr__("_paint_rule", new)
else:
super().__setattr__("_paint_rule", PathPaintRule.coerce(new))
@property
def auto_close(self):
"""If True, unclosed paths will be automatically closed before stroking."""
return self._auto_close # pylint: disable=no-member
@auto_close.setter
def auto_close(self, new):
if new not in {True, False, self.INHERIT}:
raise TypeError(f"auto_close must be a bool or self.INHERIT, not {new}")
super().__setattr__("_auto_close", new)
@property
def intersection_rule(self):
"""The desired intersection rule for this path/group."""
return self._intersection_rule # pylint: disable=no-member
@intersection_rule.setter
def intersection_rule(self, new):
# don't allow None for this one.
if new is self.INHERIT:
super().__setattr__("_intersection_rule", new)
else:
super().__setattr__("_intersection_rule", IntersectionRule.coerce(new))
@property
def fill_color(self):
"""
The desired fill color for this path/group.
When setting this property, if the color specifies an opacity value, that will
be used to set the fill_opacity property as well.
"""
return self._fill_color # pylint: disable=no-member
@fill_color.setter
def fill_color(self, color):
if isinstance(color, str):
color = color_from_hex_string(color)
if isinstance(color, (DeviceRGB, DeviceGray, DeviceCMYK)):
super().__setattr__("_fill_color", color)
if color.a is not None:
self.fill_opacity = color.a
elif (color is None) or (color is self.INHERIT):
super().__setattr__("_fill_color", color)
else:
raise TypeError(f"{color} doesn't look like a drawing color")
@property
def fill_opacity(self):
"""The desired fill opacity for this path/group."""
return getattr(self, PDFStyleKeys.FILL_ALPHA.value)
@fill_opacity.setter
def fill_opacity(self, new):
if new not in {None, self.INHERIT}:
_check_range(new)
super().__setattr__(PDFStyleKeys.FILL_ALPHA.value, new)
@property
def stroke_color(self):
"""
The desired stroke color for this path/group.
When setting this property, if the color specifies an opacity value, that will
be used to set the fill_opacity property as well.
"""
return self._stroke_color # pylint: disable=no-member
@stroke_color.setter
def stroke_color(self, color):
if isinstance(color, str):
color = color_from_hex_string(color)
if isinstance(color, (DeviceRGB, DeviceGray, DeviceCMYK)):
super().__setattr__("_stroke_color", color)
if color.a is not None:
self.stroke_opacity = color.a
if self.stroke_width is self.INHERIT:
self.stroke_width = 1
elif (color is None) or (color is self.INHERIT):
super().__setattr__("_stroke_color", color)
else:
raise TypeError(f"{color} doesn't look like a drawing color")
@property
def stroke_opacity(self):
"""The desired stroke opacity for this path/group."""
return getattr(self, PDFStyleKeys.STROKE_ALPHA.value)
@stroke_opacity.setter
def stroke_opacity(self, new):
if new not in {None, self.INHERIT}:
_check_range(new)
super().__setattr__(PDFStyleKeys.STROKE_ALPHA.value, new)
@property
def blend_mode(self):
"""The desired blend mode for this path/group."""
return getattr(self, PDFStyleKeys.BLEND_MODE.value)
@blend_mode.setter
def blend_mode(self, value):
if value is self.INHERIT:
super().__setattr__(PDFStyleKeys.BLEND_MODE.value, value)
else:
super().__setattr__(
PDFStyleKeys.BLEND_MODE.value, BlendMode.coerce(value).value
)
@property
def stroke_width(self):
"""The desired stroke width for this path/group."""
return getattr(self, PDFStyleKeys.STROKE_WIDTH.value)
@stroke_width.setter
def stroke_width(self, width):
if not isinstance(
width,
(int, float, decimal.Decimal, type(None), type(self.INHERIT)),
):
raise TypeError(f"stroke_width must be a number, not {type(width)}")
super().__setattr__(PDFStyleKeys.STROKE_WIDTH.value, width)
@property
def stroke_cap_style(self):
"""The desired stroke cap style for this path/group."""
return getattr(self, PDFStyleKeys.STROKE_CAP_STYLE.value)
@stroke_cap_style.setter
def stroke_cap_style(self, value):
if value is self.INHERIT:
super().__setattr__(PDFStyleKeys.STROKE_CAP_STYLE.value, value)
else:
super().__setattr__(
PDFStyleKeys.STROKE_CAP_STYLE.value, StrokeCapStyle.coerce(value)
)
@property
def stroke_join_style(self):
"""The desired stroke join style for this path/group."""
return getattr(self, PDFStyleKeys.STROKE_JOIN_STYLE.value)
@stroke_join_style.setter
def stroke_join_style(self, value):
if value is self.INHERIT:
super().__setattr__(PDFStyleKeys.STROKE_JOIN_STYLE.value, value)
else:
super().__setattr__(
PDFStyleKeys.STROKE_JOIN_STYLE.value,
StrokeJoinStyle.coerce(value),
)
@property
def stroke_miter_limit(self):
"""The desired stroke miter limit for this path/group."""
return getattr(self, PDFStyleKeys.STROKE_MITER_LIMIT.value)
@stroke_miter_limit.setter
def stroke_miter_limit(self, value):
if (value is self.INHERIT) or isinstance(value, NumberClass):
super().__setattr__(PDFStyleKeys.STROKE_MITER_LIMIT.value, value)
else:
raise TypeError(f"{value} is not a number")
@property
def stroke_dash_pattern(self):
"""The desired stroke dash pattern for this path/group."""
return self._stroke_dash_pattern # pylint: disable=no-member
@stroke_dash_pattern.setter
def stroke_dash_pattern(self, value):
if value is None:
result = ()
elif value is self.INHERIT:
result = value
elif isinstance(value, NumberClass):
result = (value,)
else:
try:
accum = []
for item in value:
if not isinstance(item, NumberClass):
raise TypeError(
f"stroke_dash_pattern {value} sequence has non-numeric value"
)
accum.append(item)
except TypeError:
raise TypeError(
f"stroke_dash_pattern {value} must be a number or sequence of numbers"
) from None
result = (*accum,)
super().__setattr__("_stroke_dash_pattern", result)
@property
def stroke_dash_phase(self):
"""The desired stroke dash pattern phase offset for this path/group."""
return self._stroke_dash_phase # pylint: disable=no-member
@stroke_dash_phase.setter
def stroke_dash_phase(self, value):
if value is self.INHERIT or isinstance(value, NumberClass):
return super().__setattr__("_stroke_dash_phase", value)
raise TypeError(f"{value} isn't a number or GraphicsStyle.INHERIT")
def serialize(self):
"""
Convert this style object to a PDF dictionary with appropriate style keys.
Only explicitly specified values are emitted.
"""
result = OrderedDict()
for key in self.PDF_STYLE_KEYS:
value = getattr(self, key, self.INHERIT)
if (value is not self.INHERIT) and (value is not None):
# None is used for out-of-band signaling on these, e.g. a stroke_width
# of None doesn't need to land here because it signals the
# PathPaintRule auto resolution only.
result[key] = value
# There is additional logic in GraphicsContext to ensure that this will work
if (self.stroke_dash_pattern is not self.INHERIT) and (
self.stroke_dash_pattern is not None
):
result[PDFStyleKeys.STROKE_DASH_PATTERN.value] = [
self.stroke_dash_pattern,
self.stroke_dash_phase,
]
if self.allow_transparency is False:
for key in self.TRANSPARENCY_KEYS:
if key in result:
del result[key]
if result:
# Only insert this key if there is at least one other item in the result so
# that we don't junk up the output PDF with empty ExtGState dictionaries.
type_name = Name("Type")
result[type_name] = Name("ExtGState")
result.move_to_end(type_name, last=False)
return render_pdf_primitive(result)
# this signals to the GraphicsStateDictRegistry that there is nothing to
# register. This is a success case.
return None
@force_nodocument
def resolve_paint_rule(self):
"""
Resolve `PathPaintRule.AUTO` to a real paint rule based on this style.
Returns:
the resolved `PathPaintRule`.
"""
if self.paint_rule is PathPaintRule.AUTO:
want = set()
if self.stroke_width is not None and self.stroke_color is not None:
want.add("stroke")
if self.fill_color is not None:
want.add("fill")
# we need to guarantee that this will not be None. The default will
# be "nonzero".
assert self.intersection_rule is not None
want.add(self.intersection_rule)
try:
rule = self._PAINT_RULE_LOOKUP[frozenset(want)]
except KeyError:
# don't default to DONT_PAINT because that's almost certainly not a very
# good default.
rule = PathPaintRule.STROKE_FILL_NONZERO
elif self.paint_rule is self.INHERIT:
# this shouldn't happen under normal usage, but certain API (ab)use can end
# up in this state. We can't resolve anything meaningful, so fall back to a
# sane(?) default.
rule = PathPaintRule.STROKE_FILL_NONZERO
else:
rule = self.paint_rule
return rule
def _render_move(pt):
return f"{pt.render()} m"
def _render_line(pt):
return f"{pt.render()} l"
def _render_curve(ctrl1, ctrl2, end):
return f"{ctrl1.render()} {ctrl2.render()} {end.render()} c"
class Move(NamedTuple):
"""
A path move element.
If a path has been created but not yet painted, this will create a new subpath.
See: `PaintedPath.move_to`
"""
pt: Point
"""The point to which to move."""
@property
def end_point(self):
"""The end point of this path element."""
return self.pt
@force_nodocument
def render(self, gsd_registry, style, last_item, initial_point):
"""
Render this path element to its PDF representation.
Args:
gsd_registry (GraphicsStateDictRegistry): the owner's graphics state
dictionary registry.
style (GraphicsStyle): the current resolved graphics style
last_item: the previous path element.
initial_point: last position set by a "M" or "m" command
Returns:
a tuple of `(str, new_last_item)`, where `new_last_item` is `self`
"""
# pylint: disable=unused-argument
return _render_move(self.pt), self, self.pt
@force_nodocument
def render_debug(
self, gsd_registry, style, last_item, initial_point, debug_stream, pfx
):
"""
Render this path element to its PDF representation and produce debug
information.
Args:
gsd_registry (GraphicsStateDictRegistry): the owner's graphics state
dictionary registry.
style (GraphicsStyle): the current resolved graphics style
last_item: the previous path element.
initial_point: last position set by a "M" or "m" command
debug_stream (io.TextIO): the stream to which the debug output should be
written. This is not guaranteed to be seekable (e.g. it may be stdout or
stderr).
pfx (str): the current debug output prefix string (only needed if emitting
more than one line).
Returns:
The same tuple as `Move.render`.
"""
# pylint: disable=unused-argument
rendered, resolved, initial_point = self.render(
gsd_registry, style, last_item, initial_point
)
debug_stream.write(str(self) + "\n")
return rendered, resolved, initial_point
class RelativeMove(NamedTuple):
"""
A path move element with an end point relative to the end of the previous path
element.
If a path has been created but not yet painted, this will create a new subpath.
See: `PaintedPath.move_relative`
"""
pt: Point
"""The offset by which to move."""
@force_nodocument
def render(self, gsd_registry, style, last_item, initial_point):
"""
Render this path element to its PDF representation.
Args:
gsd_registry (GraphicsStateDictRegistry): the owner's graphics state
dictionary registry.
style (GraphicsStyle): the current resolved graphics style
last_item: the previous path element.
initial_point: last position set by a "M" or "m" command
Returns:
a tuple of `(str, new_last_item)`, where `new_last_item` is the resolved
`Move`
"""
# pylint: disable=unused-argument
point = last_item.end_point + self.pt
return _render_move(point), Move(point), point
@force_nodocument
def render_debug(
self, gsd_registry, style, last_item, initial_point, debug_stream, pfx
):
"""
Render this path element to its PDF representation and produce debug
information.
Args:
gsd_registry (GraphicsStateDictRegistry): the owner's graphics state
dictionary registry.
style (GraphicsStyle): the current resolved graphics style
last_item: the previous path element.
initial_point: last position set by a "M" or "m" command
debug_stream (io.TextIO): the stream to which the debug output should be
written. This is not guaranteed to be seekable (e.g. it may be stdout or
stderr).
pfx (str): the current debug output prefix string (only needed if emitting
more than one line).
Returns:
The same tuple as `RelativeMove.render`.
"""
# pylint: disable=unused-argument
rendered, resolved, initial_point = self.render(
gsd_registry, style, last_item, initial_point
)
debug_stream.write(f"{self} resolved to {resolved}\n")
return rendered, resolved, initial_point
class Line(NamedTuple):
"""
A path line element.
This draws a straight line from the end point of the previous path element to the
point specified by `pt`.
See: `PaintedPath.line_to`
"""
pt: Point
"""The point to which the line is drawn."""
@property
def end_point(self):
"""The end point of this path element."""
return self.pt
@force_nodocument
def render(self, gsd_registry, style, last_item, initial_point):
"""
Render this path element to its PDF representation.
Args:
gsd_registry (GraphicsStateDictRegistry): the owner's graphics state
dictionary registry.
style (GraphicsStyle): the current resolved graphics style
last_item: the previous path element.
initial_point: last position set by a "M" or "m" command
Returns:
a tuple of `(str, new_last_item)`, where `new_last_item` is `self`
"""
# pylint: disable=unused-argument
return _render_line(self.pt), self, initial_point
@force_nodocument
def render_debug(
self, gsd_registry, style, last_item, initial_point, debug_stream, pfx
):
"""
Render this path element to its PDF representation and produce debug
information.
Args:
gsd_registry (GraphicsStateDictRegistry): the owner's graphics state
dictionary registry.
style (GraphicsStyle): the current resolved graphics style
last_item: the previous path element.
initial_point: last position set by a "M" or "m" command
debug_stream (io.TextIO): the stream to which the debug output should be
written. This is not guaranteed to be seekable (e.g. it may be stdout or
stderr).
pfx (str): the current debug output prefix string (only needed if emitting
more than one line).
Returns:
The same tuple as `Line.render`.
"""
# pylint: disable=unused-argument
rendered, resolved, initial_point = self.render(
gsd_registry, style, last_item, initial_point
)
debug_stream.write(str(self) + "\n")
return rendered, resolved, initial_point
class RelativeLine(NamedTuple):
"""
A path line element with an endpoint relative to the end of the previous element.
This draws a straight line from the end point of the previous path element to the
point specified by `last_item.end_point + pt`. The absolute coordinates of the end
point are resolved during the rendering process.
See: `PaintedPath.line_relative`
"""
pt: Point
"""The endpoint of the line relative to the previous path element."""
@force_nodocument
def render(self, gsd_registry, style, last_item, initial_point):
"""
Render this path element to its PDF representation.
Args:
gsd_registry (GraphicsStateDictRegistry): the owner's graphics state
dictionary registry.
style (GraphicsStyle): the current resolved graphics style
last_item: the previous path element.
initial_point: last position set by a "M" or "m" command
Returns:
a tuple of `(str, new_last_item)`, where `new_last_item` is the resolved
`Line`.
"""
# pylint: disable=unused-argument
point = last_item.end_point + self.pt
return _render_line(point), Line(point), initial_point
@force_nodocument
def render_debug(
self, gsd_registry, style, last_item, initial_point, debug_stream, pfx
):
"""
Render this path element to its PDF representation and produce debug
information.
Args:
gsd_registry (GraphicsStateDictRegistry): the owner's graphics state
dictionary registry.
style (GraphicsStyle): the current resolved graphics style
last_item: the previous path element.
initial_point: last position set by a "M" or "m" command
debug_stream (io.TextIO): the stream to which the debug output should be
written. This is not guaranteed to be seekable (e.g. it may be stdout or
stderr).
pfx (str): the current debug output prefix string (only needed if emitting
more than one line).
Returns:
The same tuple as `RelativeLine.render`.
"""
# pylint: disable=unused-argument
rendered, resolved, initial_point = self.render(
gsd_registry, style, last_item, initial_point
)
debug_stream.write(f"{self} resolved to {resolved}\n")
return rendered, resolved, initial_point
class HorizontalLine(NamedTuple):
"""
A path line element that takes its ordinate from the end of the previous element.
See: `PaintedPath.horizontal_line_to`
"""
x: Number
"""The abscissa of the horizontal line's end point."""
@force_nodocument
def render(self, gsd_registry, style, last_item, initial_point):
"""
Render this path element to its PDF representation.
Args:
gsd_registry (GraphicsStateDictRegistry): the owner's graphics state
dictionary registry.
style (GraphicsStyle): the current resolved graphics style
last_item: the previous path element.
initial_point: last position set by a "M" or "m" command
Returns:
a tuple of `(str, new_last_item)`, where `new_last_item` is the resolved
`Line`.
"""
# pylint: disable=unused-argument
end_point = Point(x=self.x, y=last_item.end_point.y)
return _render_line(end_point), Line(end_point), initial_point
@force_nodocument
def render_debug(
self, gsd_registry, style, last_item, initial_point, debug_stream, pfx
):
"""
Render this path element to its PDF representation and produce debug
information.
Args:
gsd_registry (GraphicsStateDictRegistry): the owner's graphics state
dictionary registry.
style (GraphicsStyle): the current resolved graphics style
last_item: the previous path element.
initial_point: last position set by a "M" or "m" command
debug_stream (io.TextIO): the stream to which the debug output should be
written. This is not guaranteed to be seekable (e.g. it may be stdout or
stderr).
pfx (str): the current debug output prefix string (only needed if emitting
more than one line).
Returns:
The same tuple as `HorizontalLine.render`.
"""
# pylint: disable=unused-argument
rendered, resolved, initial_point = self.render(
gsd_registry, style, last_item, initial_point
)
debug_stream.write(f"{self} resolved to {resolved}\n")
return rendered, resolved, initial_point
class RelativeHorizontalLine(NamedTuple):
"""
A path line element that takes its ordinate from the end of the previous element and
computes its abscissa offset from the end of that element.
See: `PaintedPath.horizontal_line_relative`
"""
x: Number
"""
The abscissa of the horizontal line's end point relative to the abscissa of the
previous path element.
"""
@force_nodocument
def render(self, gsd_registry, style, last_item, initial_point):
"""
Render this path element to its PDF representation.
Args:
gsd_registry (GraphicsStateDictRegistry): the owner's graphics state
dictionary registry.
style (GraphicsStyle): the current resolved graphics style
last_item: the previous path element.
initial_point: last position set by a "M" or "m" command
Returns:
a tuple of `(str, new_last_item)`, where `new_last_item` is the resolved
`Line`.
"""
# pylint: disable=unused-argument
end_point = Point(x=last_item.end_point.x + self.x, y=last_item.end_point.y)
return _render_line(end_point), Line(end_point), initial_point
@force_nodocument
def render_debug(
self, gsd_registry, style, last_item, initial_point, debug_stream, pfx
):
"""
Render this path element to its PDF representation and produce debug
information.
Args:
gsd_registry (GraphicsStateDictRegistry): the owner's graphics state
dictionary registry.
style (GraphicsStyle): the current resolved graphics style
last_item: the previous path element.
initial_point: last position set by a "M" or "m" command
debug_stream (io.TextIO): the stream to which the debug output should be
written. This is not guaranteed to be seekable (e.g. it may be stdout or
stderr).
pfx (str): the current debug output prefix string (only needed if emitting
more than one line).
Returns:
The same tuple as `RelativeHorizontalLine.render`.
"""
# pylint: disable=unused-argument
rendered, resolved, initial_point = self.render(
gsd_registry, style, last_item, initial_point
)
debug_stream.write(f"{self} resolved to {resolved}\n")
return rendered, resolved, initial_point
class VerticalLine(NamedTuple):
"""
A path line element that takes its abscissa from the end of the previous element.
See: `PaintedPath.vertical_line_to`
"""
y: Number
"""The ordinate of the vertical line's end point."""
@force_nodocument
def render(self, gsd_registry, style, last_item, initial_point):
"""
Render this path element to its PDF representation.
Args:
gsd_registry (GraphicsStateDictRegistry): the owner's graphics state
dictionary registry.
style (GraphicsStyle): the current resolved graphics style
last_item: the previous path element.
initial_point: last position set by a "M" or "m" command
Returns:
a tuple of `(str, new_last_item)`, where `new_last_item` is the resolved
`Line`.
"""
# pylint: disable=unused-argument
end_point = Point(x=last_item.end_point.x, y=self.y)
return _render_line(end_point), Line(end_point), initial_point
@force_nodocument
def render_debug(
self, gsd_registry, style, last_item, initial_point, debug_stream, pfx
):
"""
Render this path element to its PDF representation and produce debug
information.
Args:
gsd_registry (GraphicsStateDictRegistry): the owner's graphics state
dictionary registry.
style (GraphicsStyle): the current resolved graphics style
last_item: the previous path element.
initial_point: last position set by a "M" or "m" command
debug_stream (io.TextIO): the stream to which the debug output should be
written. This is not guaranteed to be seekable (e.g. it may be stdout or
stderr).
pfx (str): the current debug output prefix string (only needed if emitting
more than one line).
Returns:
The same tuple as `VerticalLine.render`.
"""
# pylint: disable=unused-argument
rendered, resolved, initial_point = self.render(
gsd_registry, style, last_item, initial_point
)
debug_stream.write(f"{self} resolved to {resolved}\n")
return rendered, resolved, initial_point
class RelativeVerticalLine(NamedTuple):
"""
A path line element that takes its abscissa from the end of the previous element and
computes its ordinate offset from the end of that element.
See: `PaintedPath.vertical_line_relative`
"""
y: Number
"""
The ordinate of the vertical line's end point relative to the ordinate of the
previous path element.
"""
@force_nodocument
def render(self, gsd_registry, style, last_item, initial_point):
"""
Render this path element to its PDF representation.
Args:
gsd_registry (GraphicsStateDictRegistry): the owner's graphics state
dictionary registry.
style (GraphicsStyle): the current resolved graphics style
last_item: the previous path element.
initial_point: last position set by a "M" or "m" command
Returns:
a tuple of `(str, new_last_item)`, where `new_last_item` is the resolved
`Line`.
"""
# pylint: disable=unused-argument
end_point = Point(x=last_item.end_point.x, y=last_item.end_point.y + self.y)
return _render_line(end_point), Line(end_point), initial_point
@force_nodocument
def render_debug(
self, gsd_registry, style, last_item, initial_point, debug_stream, pfx
):
"""
Render this path element to its PDF representation and produce debug
information.
Args:
gsd_registry (GraphicsStateDictRegistry): the owner's graphics state
dictionary registry.
style (GraphicsStyle): the current resolved graphics style
last_item: the previous path element.
initial_point: last position set by a "M" or "m" command
debug_stream (io.TextIO): the stream to which the debug output should be
written. This is not guaranteed to be seekable (e.g. it may be stdout or
stderr).
pfx (str): the current debug output prefix string (only needed if emitting
more than one line).
Returns:
The same tuple as `RelativeVerticalLine.render`.
"""
# pylint: disable=unused-argument
rendered, resolved, initial_point = self.render(
gsd_registry, style, last_item, initial_point
)
debug_stream.write(f"{self} resolved to {resolved}\n")
return rendered, resolved, initial_point
class BezierCurve(NamedTuple):
"""
A cubic Bézier curve path element.
This draws a Bézier curve parameterized by the end point of the previous path
element, two off-curve control points, and an end point.
See: `PaintedPath.curve_to`
"""
c1: Point
"""The curve's first control point."""
c2: Point
"""The curve's second control point."""
end: Point
"""The curve's end point."""
@property
def end_point(self):
"""The end point of this path element."""
return self.end
@force_nodocument
def render(self, gsd_registry, style, last_item, initial_point):
"""
Render this path element to its PDF representation.
Args:
gsd_registry (GraphicsStateDictRegistry): the owner's graphics state
dictionary registry.
style (GraphicsStyle): the current resolved graphics style
last_item: the previous path element.
initial_point: last position set by a "M" or "m" command
Returns:
a tuple of `(str, new_last_item)`, where `new_last_item` is `self`
"""
# pylint: disable=unused-argument
return _render_curve(self.c1, self.c2, self.end), self, initial_point
@force_nodocument
def render_debug(
self, gsd_registry, style, last_item, initial_point, debug_stream, pfx
):
"""
Render this path element to its PDF representation and produce debug
information.
Args:
gsd_registry (GraphicsStateDictRegistry): the owner's graphics state
dictionary registry.
style (GraphicsStyle): the current resolved graphics style
last_item: the previous path element.
initial_point: last position set by a "M" or "m" command
debug_stream (io.TextIO): the stream to which the debug output should be
written. This is not guaranteed to be seekable (e.g. it may be stdout or
stderr).
pfx (str): the current debug output prefix string (only needed if emitting
more than one line).
Returns:
The same tuple as `BezierCurve.render`.
"""
# pylint: disable=unused-argument
rendered, resolved, initial_point = self.render(
gsd_registry, style, last_item, initial_point
)
debug_stream.write(str(self) + "\n")
return rendered, resolved, initial_point
class RelativeBezierCurve(NamedTuple):
"""
A cubic Bézier curve path element whose points are specified relative to the end
point of the previous path element.
See: `PaintedPath.curve_relative`
"""
c1: Point
"""
The curve's first control point relative to the end of the previous path element.
"""
c2: Point
"""
The curve's second control point relative to the end of the previous path element.
"""
end: Point
"""The curve's end point relative to the end of the previous path element."""
@force_nodocument
def render(self, gsd_registry, style, last_item, initial_point):
"""
Render this path element to its PDF representation.
Args:
gsd_registry (GraphicsStateDictRegistry): the owner's graphics state
dictionary registry.
style (GraphicsStyle): the current resolved graphics style
last_item: the previous path element.
initial_point: last position set by a "M" or "m" command
Returns:
a tuple of `(str, new_last_item)`, where `new_last_item` is the resolved
`BezierCurve`.
"""
# pylint: disable=unused-argument
last_point = last_item.end_point
c1 = last_point + self.c1
c2 = last_point + self.c2
end = last_point + self.end
return (
_render_curve(c1, c2, end),
BezierCurve(c1=c1, c2=c2, end=end),
initial_point,
)
@force_nodocument
def render_debug(
self, gsd_registry, style, last_item, initial_point, debug_stream, pfx
):
"""
Render this path element to its PDF representation and produce debug
information.
Args:
gsd_registry (GraphicsStateDictRegistry): the owner's graphics state
dictionary registry.
style (GraphicsStyle): the current resolved graphics style
last_item: the previous path element.
initial_point: last position set by a "M" or "m" command
debug_stream (io.TextIO): the stream to which the debug output should be
written. This is not guaranteed to be seekable (e.g. it may be stdout or
stderr).
pfx (str): the current debug output prefix string (only needed if emitting
more than one line).
Returns:
The same tuple as `RelativeBezierCurve.render`.
"""
# pylint: disable=unused-argument
rendered, resolved, initial_point = self.render(
gsd_registry, style, last_item, initial_point
)
debug_stream.write(f"{self} resolved to {resolved}\n")
return rendered, resolved, initial_point
class QuadraticBezierCurve(NamedTuple):
"""
A quadratic Bézier curve path element.
This draws a Bézier curve parameterized by the end point of the previous path
element, one off-curve control point, and an end point.
See: `PaintedPath.quadratic_curve_to`
"""
ctrl: Point
"""The curve's control point."""
end: Point
"""The curve's end point."""
@property
def end_point(self):
"""The end point of this path element."""
return self.end
def to_cubic_curve(self, start_point):
ctrl = self.ctrl
end = self.end
ctrl1 = Point(
x=start_point.x + 2 * (ctrl.x - start_point.x) / 3,
y=start_point.y + 2 * (ctrl.y - start_point.y) / 3,
)
ctrl2 = Point(
x=end.x + 2 * (ctrl.x - end.x) / 3,
y=end.y + 2 * (ctrl.y - end.y) / 3,
)
return BezierCurve(ctrl1, ctrl2, end)
@force_nodocument
def render(self, gsd_registry, style, last_item, initial_point):
"""
Render this path element to its PDF representation.
Args:
gsd_registry (GraphicsStateDictRegistry): the owner's graphics state
dictionary registry.
style (GraphicsStyle): the current resolved graphics style
last_item: the previous path element.
initial_point: last position set by a "M" or "m" command
Returns:
a tuple of `(str, new_last_item)`, where `new_last_item` is `self`.
"""
# pylint: disable=unused-argument
return (
self.to_cubic_curve(last_item.end_point).render(
gsd_registry, style, last_item, initial_point
)[0],
self,
initial_point,
)
@force_nodocument
def render_debug(
self, gsd_registry, style, last_item, initial_point, debug_stream, pfx
):
"""
Render this path element to its PDF representation and produce debug
information.
Args:
gsd_registry (GraphicsStateDictRegistry): the owner's graphics state
dictionary registry.
style (GraphicsStyle): the current resolved graphics style
last_item: the previous path element.
initial_point: last position set by a "M" or "m" command
debug_stream (io.TextIO): the stream to which the debug output should be
written. This is not guaranteed to be seekable (e.g. it may be stdout or
stderr).
pfx (str): the current debug output prefix string (only needed if emitting
more than one line).
Returns:
The same tuple as `QuadraticBezierCurve.render`.
"""
# pylint: disable=unused-argument
rendered, resolved, initial_point = self.render(
gsd_registry, style, last_item, initial_point
)
debug_stream.write(
f"{self} resolved to {self.to_cubic_curve(last_item.end_point)}\n"
)
return rendered, resolved, initial_point
class RelativeQuadraticBezierCurve(NamedTuple):
"""
A quadratic Bézier curve path element whose points are specified relative to the end
point of the previous path element.
See: `PaintedPath.quadratic_curve_relative`
"""
ctrl: Point
"""The curve's control point relative to the end of the previous path element."""
end: Point
"""The curve's end point relative to the end of the previous path element."""
@force_nodocument
def render(self, gsd_registry, style, last_item, initial_point):
"""
Render this path element to its PDF representation.
Args:
gsd_registry (GraphicsStateDictRegistry): the owner's graphics state
dictionary registry.
style (GraphicsStyle): the current resolved graphics style
last_item: the previous path element.
initial_point: last position set by a "M" or "m" command
Returns:
a tuple of `(str, new_last_item)`, where `new_last_item` is the resolved
`QuadraticBezierCurve`.
"""
# pylint: disable=unused-argument
last_point = last_item.end_point
ctrl = last_point + self.ctrl
end = last_point + self.end
absolute = QuadraticBezierCurve(ctrl=ctrl, end=end)
return absolute.render(gsd_registry, style, last_item, initial_point)
@force_nodocument
def render_debug(
self, gsd_registry, style, last_item, initial_point, debug_stream, pfx
):
"""
Render this path element to its PDF representation and produce debug
information.
Args:
gsd_registry (GraphicsStateDictRegistry): the owner's graphics state
dictionary registry.
style (GraphicsStyle): the current resolved graphics style
last_item: the previous path element.
initial_point: last position set by a "M" or "m" command
debug_stream (io.TextIO): the stream to which the debug output should be
written. This is not guaranteed to be seekable (e.g. it may be stdout or
stderr).
pfx (str): the current debug output prefix string (only needed if emitting
more than one line).
Returns:
The same tuple as `RelativeQuadraticBezierCurve.render`.
"""
# pylint: disable=unused-argument
rendered, resolved, initial_point = self.render(
gsd_registry, style, last_item, initial_point
)
debug_stream.write(
f"{self} resolved to {resolved} "
f"then to {resolved.to_cubic_curve(last_item.end_point)}\n"
)
return rendered, resolved, initial_point
class Arc(NamedTuple):
"""
An elliptical arc path element.
The arc is drawn from the end of the current path element to its specified end point
using a number of parameters to determine how it is constructed.
See: `PaintedPath.arc_to`
"""
radii: Point
"""
The x- and y-radii of the arc. If `radii.x == radii.y` the arc will be circular.
"""
rotation: Number
"""The rotation of the arc's major/minor axes relative to the coordinate frame."""
large: bool
"""If True, sweep the arc over an angle greater than or equal to 180 degrees."""
sweep: bool
"""If True, the arc is swept in the positive angular direction."""
end: Point
"""The end point of the arc."""
@staticmethod
@force_nodocument
def subdivde_sweep(sweep_angle):
"""
A generator that subdivides a swept angle into segments no larger than a quarter
turn.
Any sweep that is larger than a quarter turn is subdivided into as many equally
sized segments as necessary to prevent any individual segment from being larger
than a quarter turn.
This is used for approximating a circular curve segment using cubic Bézier
curves. This computes the parameters used for the Bézier approximation up
front, as well as the transform necessary to place the segment in the correct
position.
Args:
sweep_angle (Number): the angle to subdivide.
Yields:
A tuple of (ctrl1, ctrl2, end) representing the control and end points of
the cubic Bézier curve approximating the segment as a unit circle centered
at the origin.
"""
sweep_angle = abs(sweep_angle)
sweep_left = sweep_angle
quarterturn = math.pi / 2
chunks = math.ceil(sweep_angle / quarterturn)
sweep_segment = sweep_angle / chunks
cos_t = math.cos(sweep_segment)
sin_t = math.sin(sweep_segment)
kappa = 4 / 3 * math.tan(sweep_segment / 4)
ctrl1 = Point(1, kappa)
ctrl2 = Point(cos_t + kappa * sin_t, sin_t - kappa * cos_t)
end = Point(cos_t, sin_t)
for _ in range(chunks):
offset = sweep_angle - sweep_left
transform = Transform.rotation(offset)
yield ctrl1 @ transform, ctrl2 @ transform, end @ transform
sweep_left -= sweep_segment
def _approximate_arc(self, last_item):
"""
Approximate this arc with a sequence of `BezierCurve`.
Args:
last_item: the previous path element (used for its end point)
Returns:
a list of `BezierCurve`.
"""
radii = self.radii
reverse = Transform.rotation(-self.rotation)
forward = Transform.rotation(self.rotation)
prime = ((last_item.end_point - self.end) * 0.5) @ reverse
lam_da = (prime.x / radii.x) ** 2 + (prime.y / radii.y) ** 2
if lam_da > 1:
radii = Point(x=(lam_da**0.5) * radii.x, y=(lam_da**0.5) * radii.y)
sign = (self.large != self.sweep) - (self.large == self.sweep)
rxry2 = (radii.x * radii.y) ** 2
rxpy2 = (radii.x * prime.y) ** 2
rypx2 = (radii.y * prime.x) ** 2
centerprime = (
sign
* math.sqrt(round(rxry2 - rxpy2 - rypx2, 8) / (rxpy2 + rypx2))
* Point(
x=radii.x * prime.y / radii.y,
y=-radii.y * prime.x / radii.x,
)
)
center = (centerprime @ forward) + ((last_item.end_point + self.end) * 0.5)
arcstart = Point(
x=(prime.x - centerprime.x) / radii.x,
y=(prime.y - centerprime.y) / radii.y,
)
arcend = Point(
x=(-prime.x - centerprime.x) / radii.x,
y=(-prime.y - centerprime.y) / radii.y,
)
theta = Point(1, 0).angle(arcstart)
deltatheta = arcstart.angle(arcend)
if (self.sweep is False) and (deltatheta > 0):
deltatheta -= math.tau
elif (self.sweep is True) and (deltatheta < 0):
deltatheta += math.tau
sweep_sign = (deltatheta >= 0) - (deltatheta < 0)
final_tf = (
Transform.scaling(x=1, y=sweep_sign) # flip negative sweeps
.rotate(theta) # rotate start of arc to correct position
.scale(radii.x, radii.y) # scale unit circle into the final ellipse shape
.rotate(self.rotation) # rotate the ellipse the specified angle
.translate(center.x, center.y) # translate to the final coordinates
)
curves = []
for ctrl1, ctrl2, end in self.subdivde_sweep(deltatheta):
curves.append(
BezierCurve(ctrl1 @ final_tf, ctrl2 @ final_tf, end @ final_tf)
)
return curves
@force_nodocument
def render(self, gsd_registry, style, last_item, initial_point):
"""
Render this path element to its PDF representation.
Args:
gsd_registry (GraphicsStateDictRegistry): the owner's graphics state
dictionary registry.
style (GraphicsStyle): the current resolved graphics style
last_item: the previous path element.
initial_point: last position set by a "M" or "m" command
Returns:
a tuple of `(str, new_last_item)`, where `new_last_item` is a resolved
`BezierCurve`.
"""
# pylint: disable=unused-argument
curves = self._approximate_arc(last_item)
if not curves:
return "", last_item
return (
" ".join(
curve.render(gsd_registry, style, prev, initial_point)[0]
for prev, curve in zip([last_item, *curves[:-1]], curves)
),
curves[-1],
initial_point,
)
@force_nodocument
def render_debug(
self, gsd_registry, style, last_item, initial_point, debug_stream, pfx
):
"""
Render this path element to its PDF representation and produce debug
information.
Args:
gsd_registry (GraphicsStateDictRegistry): the owner's graphics state
dictionary registry.
style (GraphicsStyle): the current resolved graphics style
last_item: the previous path element.
initial_point: last position set by a "M" or "m" command
debug_stream (io.TextIO): the stream to which the debug output should be
written. This is not guaranteed to be seekable (e.g. it may be stdout or
stderr).
pfx (str): the current debug output prefix string (only needed if emitting
more than one line).
Returns:
The same tuple as `Arc.render`.
"""
# pylint: disable=unused-argument
curves = self._approximate_arc(last_item)
debug_stream.write(f"{self} resolved to:\n")
if not curves:
debug_stream.write(pfx + " └─ nothing\n")
return "", last_item
previous = [last_item]
for curve in curves[:-1]:
previous.append(curve)
debug_stream.write(pfx + f" ├─ {curve}\n")
debug_stream.write(pfx + f" └─ {curves[-1]}\n")
return (
" ".join(
curve.render(gsd_registry, style, prev, initial_point)[0]
for prev, curve in zip(previous, curves)
),
curves[-1],
initial_point,
)
class RelativeArc(NamedTuple):
"""
An elliptical arc path element.
The arc is drawn from the end of the current path element to its specified end point
using a number of parameters to determine how it is constructed.
See: `PaintedPath.arc_relative`
"""
radii: Point
"""
The x- and y-radii of the arc. If `radii.x == radii.y` the arc will be circular.
"""
rotation: Number
"""The rotation of the arc's major/minor axes relative to the coordinate frame."""
large: bool
"""If True, sweep the arc over an angle greater than or equal to 180 degrees."""
sweep: bool
"""If True, the arc is swept in the positive angular direction."""
end: Point
"""The end point of the arc relative to the end of the previous path element."""
@force_nodocument
def render(self, gsd_registry, style, last_item, initial_point):
"""
Render this path element to its PDF representation.
Args:
gsd_registry (GraphicsStateDictRegistry): the owner's graphics state
dictionary registry.
style (GraphicsStyle): the current resolved graphics style
last_item: the previous path element.
initial_point: last position set by a "M" or "m" command
Returns:
a tuple of `(str, new_last_item)`, where `new_last_item` is a resolved
`BezierCurve`.
"""
# pylint: disable=unused-argument
return Arc(
self.radii,
self.rotation,
self.large,
self.sweep,
last_item.end_point + self.end,
).render(gsd_registry, style, last_item, initial_point)
@force_nodocument
def render_debug(
self, gsd_registry, style, last_item, initial_point, debug_stream, pfx
):
"""
Render this path element to its PDF representation and produce debug
information.
Args:
gsd_registry (GraphicsStateDictRegistry): the owner's graphics state
dictionary registry.
style (GraphicsStyle): the current resolved graphics style
last_item: the previous path element.
initial_point: last position set by a "M" or "m" command
debug_stream (io.TextIO): the stream to which the debug output should be
written. This is not guaranteed to be seekable (e.g. it may be stdout or
stderr).
pfx (str): the current debug output prefix string (only needed if emitting
more than one line).
Returns:
The same tuple as `RelativeArc.render`.
"""
# pylint: disable=unused-argument
# newline is intentionally missing here
debug_stream.write(f"{self} resolved to ")
return Arc(
self.radii,
self.rotation,
self.large,
self.sweep,
last_item.end_point + self.end,
).render_debug(gsd_registry, style, last_item, initial_point, debug_stream, pfx)
class Rectangle(NamedTuple):
"""A pdf primitive rectangle."""
org: Point
"""The top-left corner of the rectangle."""
size: Point
"""The width and height of the rectangle."""
@force_nodocument
def render(self, gsd_registry, style, last_item, initial_point):
"""
Render this path element to its PDF representation.
Args:
gsd_registry (GraphicsStateDictRegistry): the owner's graphics state
dictionary registry.
style (GraphicsStyle): the current resolved graphics style
last_item: the previous path element.
initial_point: last position set by a "M" or "m" command
Returns:
a tuple of `(str, new_last_item)`, where `new_last_item` is a `Line` back to
the rectangle's origin.
"""
# pylint: disable=unused-argument
return (
f"{self.org.render()} {self.size.render()} re",
Line(self.org),
initial_point,
)
@force_nodocument
def render_debug(
self, gsd_registry, style, last_item, initial_point, debug_stream, pfx
):
"""
Render this path element to its PDF representation and produce debug
information.
Args:
gsd_registry (GraphicsStateDictRegistry): the owner's graphics state
dictionary registry.
style (GraphicsStyle): the current resolved graphics style
last_item: the previous path element.
initial_point: last position set by a "M" or "m" command
debug_stream (io.TextIO): the stream to which the debug output should be
written. This is not guaranteed to be seekable (e.g. it may be stdout or
stderr).
pfx (str): the current debug output prefix string (only needed if emitting
more than one line).
Returns:
The same tuple as `Rectangle.render`.
"""
# pylint: disable=unused-argument
rendered, resolved, initial_point = self.render(
gsd_registry, style, last_item, initial_point
)
debug_stream.write(f"{self} resolved to {rendered}\n")
return rendered, resolved, initial_point
class RoundedRectangle(NamedTuple):
"""
A rectangle with rounded corners.
See: `PaintedPath.rectangle`
"""
org: Point
"""The top-left corner of the rectangle."""
size: Point
"""The width and height of the rectangle."""
corner_radii: Point
"""The x- and y-radius of the corners."""
def _decompose(self):
items = []
if (self.size.x == 0) and (self.size.y == 0):
pass
elif (self.size.x == 0) or (self.size.y == 0):
items.append(Move(self.org))
items.append(Line(self.org + self.size))
items.append(Close())
elif (self.corner_radii.x == 0) or (self.corner_radii.y == 0):
items.append(Rectangle(self.org, self.size))
else:
x, y = self.org
w, h = self.size
rx, ry = self.corner_radii
sign_width = (self.size.x >= 0) - (self.size.x < 0)
sign_height = (self.size.y >= 0) - (self.size.y < 0)
if abs(rx) > abs(w):
rx = self.size.x
if abs(ry) > abs(h):
ry = self.size.y
rx = sign_width * abs(rx)
ry = sign_height * abs(ry)
arc_rad = Point(rx, ry)
items.append(Move(Point(x + rx, y)))
items.append(Line(Point(x + w - rx, y)))
items.append(Arc(arc_rad, 0, False, True, Point(x + w, y + ry)))
items.append(Line(Point(x + w, y + h - ry)))
items.append(Arc(arc_rad, 0, False, True, Point(x + w - rx, y + h)))
items.append(Line(Point(x + rx, y + h)))
items.append(Arc(arc_rad, 0, False, True, Point(x, y + h - ry)))
items.append(Line(Point(x, y + ry)))
items.append(Arc(arc_rad, 0, False, True, Point(x + rx, y)))
items.append(Close())
return items
@force_nodocument
def render(self, gsd_registry, style, last_item, initial_point):
"""
Render this path element to its PDF representation.
Args:
gsd_registry (GraphicsStateDictRegistry): the owner's graphics state
dictionary registry.
style (GraphicsStyle): the current resolved graphics style
last_item: the previous path element.
initial_point: last position set by a "M" or "m" command
Returns:
a tuple of `(str, new_last_item)`, where `new_last_item` is a resolved
`Line`.
"""
# pylint: disable=unused-argument
components = self._decompose()
if not components:
return "", last_item
render_list = []
for item in components:
rendered, last_item, initial_point = item.render(
gsd_registry, style, last_item, initial_point
)
render_list.append(rendered)
return " ".join(render_list), Line(self.org), initial_point
@force_nodocument
def render_debug(
self, gsd_registry, style, last_item, initial_point, debug_stream, pfx
):
"""
Render this path element to its PDF representation and produce debug
information.
Args:
gsd_registry (GraphicsStateDictRegistry): the owner's graphics state
dictionary registry.
style (GraphicsStyle): the current resolved graphics style
last_item: the previous path element.
initial_point: last position set by a "M" or "m" command
debug_stream (io.TextIO): the stream to which the debug output should be
written. This is not guaranteed to be seekable (e.g. it may be stdout or
stderr).
pfx (str): the current debug output prefix string (only needed if emitting
more than one line).
Returns:
The same tuple as `RoundedRectangle.render`.
"""
# pylint: disable=unused-argument
components = self._decompose()
debug_stream.write(f"{self} resolved to:\n")
if not components:
debug_stream.write(pfx + " └─ nothing\n")
return "", last_item
render_list = []
for item in components[:-1]:
rendered, last_item, initial_point = item.render(
gsd_registry, style, last_item, initial_point
)
debug_stream.write(pfx + f" ├─ {item}\n")
render_list.append(rendered)
rendered, last_item, initial_point = components[-1].render(
gsd_registry, style, last_item, initial_point
)
debug_stream.write(pfx + f" └─ {components[-1]}\n")
render_list.append(rendered)
return " ".join(render_list), Line(self.org), initial_point
class Ellipse(NamedTuple):
"""
An ellipse.
See: `PaintedPath.ellipse`
"""
radii: Point
"""The x- and y-radii of the ellipse"""
center: Point
"""The abscissa and ordinate of the center of the ellipse"""
def _decompose(self):
items = []
rx = abs(self.radii.x)
ry = abs(self.radii.y)
cx, cy = self.center
arc_rad = Point(rx, ry)
# this isn't the most efficient way to do this, computationally, but it's
# internally consistent.
if (rx != 0) and (ry != 0):
items.append(Move(Point(cx + rx, cy)))
items.append(Arc(arc_rad, 0, False, True, Point(cx, cy + ry)))
items.append(Arc(arc_rad, 0, False, True, Point(cx - rx, cy)))
items.append(Arc(arc_rad, 0, False, True, Point(cx, cy - ry)))
items.append(Arc(arc_rad, 0, False, True, Point(cx + rx, cy)))
items.append(Close())
return items
@force_nodocument
def render(self, gsd_registry, style, last_item, initial_point):
"""
Render this path element to its PDF representation.
Args:
gsd_registry (GraphicsStateDictRegistry): the owner's graphics state
dictionary registry.
style (GraphicsStyle): the current resolved graphics style
last_item: the previous path element.
initial_point: last position set by a "M" or "m" command
Returns:
a tuple of `(str, new_last_item)`, where `new_last_item` is a resolved
`Move` to the center of the ellipse.
"""
# pylint: disable=unused-argument
components = self._decompose()
if not components:
return "", last_item
render_list = []
for item in components:
rendered, last_item, initial_point = item.render(
gsd_registry, style, last_item, initial_point
)
render_list.append(rendered)
return " ".join(render_list), Move(self.center), initial_point
@force_nodocument
def render_debug(
self, gsd_registry, style, last_item, initial_point, debug_stream, pfx
):
"""
Render this path element to its PDF representation and produce debug
information.
Args:
gsd_registry (GraphicsStateDictRegistry): the owner's graphics state
dictionary registry.
style (GraphicsStyle): the current resolved graphics style
last_item: the previous path element.
initial_point: last position set by a "M" or "m" command
debug_stream (io.TextIO): the stream to which the debug output should be
written. This is not guaranteed to be seekable (e.g. it may be stdout or
stderr).
pfx (str): the current debug output prefix string (only needed if emitting
more than one line).
Returns:
The same tuple as `Ellipse.render`.
"""
# pylint: disable=unused-argument
components = self._decompose()
debug_stream.write(f"{self} resolved to:\n")
if not components:
debug_stream.write(pfx + " └─ nothing\n")
return "", last_item
render_list = []
for item in components[:-1]:
rendered, last_item, initial_point = item.render(
gsd_registry, style, last_item, initial_point
)
debug_stream.write(pfx + f" ├─ {item}\n")
render_list.append(rendered)
rendered, last_item, initial_point = components[-1].render(
gsd_registry, style, last_item, initial_point
)
debug_stream.write(pfx + f" └─ {components[-1]}\n")
render_list.append(rendered)
return " ".join(render_list), Move(self.center), initial_point
class ImplicitClose(NamedTuple):
"""
A path close element that is conditionally rendered depending on the value of
`GraphicsStyle.auto_close`.
"""
@force_nodocument
def render(self, gsd_registry, style, last_item, initial_point):
"""
Render this path element to its PDF representation.
Args:
gsd_registry (GraphicsStateDictRegistry): the owner's graphics state
dictionary registry.
style (GraphicsStyle): the current resolved graphics style
last_item: the previous path element.
initial_point: last position set by a "M" or "m" command
Returns:
a tuple of `(str, new_last_item)`, where `new_last_item` is whatever the old
last_item was.
"""
# pylint: disable=unused-argument
if style.auto_close:
return "h", last_item, initial_point
return "", last_item, initial_point
@force_nodocument
def render_debug(
self, gsd_registry, style, last_item, initial_point, debug_stream, pfx
):
"""
Render this path element to its PDF representation and produce debug
information.
Args:
gsd_registry (GraphicsStateDictRegistry): the owner's graphics state
dictionary registry.
style (GraphicsStyle): the current resolved graphics style
last_item: the previous path element.
initial_point: last position set by a "M" or "m" command
debug_stream (io.TextIO): the stream to which the debug output should be
written. This is not guaranteed to be seekable (e.g. it may be stdout or
stderr).
pfx (str): the current debug output prefix string (only needed if emitting
more than one line).
Returns:
The same tuple as `ImplicitClose.render`.
"""
# pylint: disable=unused-argument
rendered, resolved, initial_point = self.render(
gsd_registry, style, last_item, initial_point
)
debug_stream.write(f"{self} resolved to {rendered}\n")
return rendered, resolved, initial_point
class Close(NamedTuple):
"""
A path close element.
Instructs the renderer to draw a straight line from the end of the last path element
to the start of the current path.
See: `PaintedPath.close`
"""
@force_nodocument
def render(self, gsd_registry, style, last_item, initial_point):
"""
Render this path element to its PDF representation.
Args:
gsd_registry (GraphicsStateDictRegistry): the owner's graphics state
dictionary registry.
style (GraphicsStyle): the current resolved graphics style
last_item: the previous path element.
initial_point: last position set by a "M" or "m" command
Returns:
a tuple of `(str, new_last_item)`, where `new_last_item` is whatever the old
last_item was.
"""
# pylint: disable=unused-argument
return "h", Move(initial_point), initial_point
@force_nodocument
def render_debug(
self, gsd_registry, style, last_item, initial_point, debug_stream, pfx
):
"""
Render this path element to its PDF representation and produce debug
information.
Args:
gsd_registry (GraphicsStateDictRegistry): the owner's graphics state
dictionary registry.
style (GraphicsStyle): the current resolved graphics style
last_item: the previous path element.
initial_point: last position set by a "M" or "m" command
debug_stream (io.TextIO): the stream to which the debug output should be
written. This is not guaranteed to be seekable (e.g. it may be stdout or
stderr).
pfx (str): the current debug output prefix string (only needed if emitting
more than one line).
Returns:
The same tuple as `Close.render`.
"""
# pylint: disable=unused-argument
rendered, resolved, initial_point = self.render(
gsd_registry, style, last_item, initial_point
)
debug_stream.write(str(self) + "\n")
return rendered, resolved, initial_point
class DrawingContext:
"""
Base context for a drawing in a PDF
This context is not stylable and is mainly responsible for transforming path
drawing coordinates into user coordinates (i.e. it ensures that the output drawing
is correctly scaled).
"""
def __init__(self):
self._subitems = []
def add_item(self, item, _copy=True):
"""
Append an item to this drawing context
Args:
item (GraphicsContext, PaintedPath): the item to be appended.
_copy (bool): if true (the default), the item will be copied before being
appended. This prevents modifications to a referenced object from
"retroactively" altering its style/shape and should be disabled with
caution.
"""
if not isinstance(item, (GraphicsContext, PaintedPath)):
raise TypeError(f"{item} doesn't belong in a DrawingContext")
if _copy:
item = copy.deepcopy(item)
self._subitems.append(item)
@staticmethod
def _setup_render_prereqs(style, first_point, scale, height):
style.auto_close = True
style.paint_rule = PathPaintRule.AUTO
style.intersection_rule = IntersectionRule.NONZERO
last_item = Move(first_point)
scale, last_item = (
Transform.scaling(x=1, y=-1)
.about(x=0, y=height / 2)
.scale(scale)
.render(last_item)
)
render_list = ["q", scale]
return render_list, style, last_item
def render(self, gsd_registry, first_point, scale, height, starting_style):
"""
Render the drawing context to PDF format.
Args:
gsd_registry (GraphicsStateDictRegistry): the parent document's graphics
state registry.
first_point (Point): the starting point to use if the first path element is
a relative element.
scale (Number): the scale factor to convert from PDF pt units into the
document's semantic units (e.g. mm or in).
height (Number): the page height. This is used to remap the coordinates to
be from the top-left corner of the page (matching fpdf's behavior)
instead of the PDF native behavior of bottom-left.
starting_style (GraphicsStyle): the base style for this drawing context,
derived from the document's current style defaults.
Returns:
A string composed of the PDF representation of all the paths and groups in
this context (an empty string is returned if there are no paths or groups)
"""
if not self._subitems:
return ""
render_list, style, last_item = self._setup_render_prereqs(
starting_style, first_point, scale, height
)
for item in self._subitems:
rendered, last_item, first_point = item.render(
gsd_registry, style, last_item, first_point
)
if rendered:
render_list.append(rendered)
# there was nothing to render: the only items are the start group and scale
# transform.
if len(render_list) == 2:
return ""
style_dict_name = gsd_registry.register_style(style)
if style_dict_name is not None:
render_list.insert(2, f"{render_pdf_primitive(style_dict_name)} gs")
render_list.insert(
3,
render_pdf_primitive(style.stroke_dash_pattern)
+ f" {number_to_str(style.stroke_dash_phase)} d",
)
render_list.append("Q")
return " ".join(render_list)
def render_debug(
self, gsd_registry, first_point, scale, height, starting_style, debug_stream
):
"""
Render the drawing context to PDF format.
Args:
gsd_registry (GraphicsStateDictRegistry): the parent document's graphics
state registry.
first_point (Point): the starting point to use if the first path element is
a relative element.
scale (Number): the scale factor to convert from PDF pt units into the
document's semantic units (e.g. mm or in).
height (Number): the page height. This is used to remap the coordinates to
be from the top-left corner of the page (matching fpdf's behavior)
instead of the PDF native behavior of bottom-left.
starting_style (GraphicsStyle): the base style for this drawing context,
derived from the document's current style defaults.
debug_stream (TextIO): a text stream to which a debug representation of the
drawing structure will be written.
Returns:
A string composed of the PDF representation of all the paths and groups in
this context (an empty string is returned if there are no paths or groups)
"""
render_list, style, last_item = self._setup_render_prereqs(
starting_style, first_point, scale, height
)
debug_stream.write("ROOT\n")
for child in self._subitems[:-1]:
debug_stream.write(" ├─ ")
rendered, last_item = child.render_debug(
gsd_registry, style, last_item, debug_stream, " │ "
)
if rendered:
render_list.append(rendered)
if self._subitems:
debug_stream.write(" └─ ")
rendered, last_item, first_point = self._subitems[-1].render_debug(
gsd_registry, style, last_item, first_point, debug_stream, " "
)
if rendered:
render_list.append(rendered)
# there was nothing to render: the only items are the start group and scale
# transform.
if len(render_list) == 2:
return ""
style_dict_name = gsd_registry.register_style(style)
if style_dict_name is not None:
render_list.insert(2, f"{render_pdf_primitive(style_dict_name)} gs")
render_list.insert(
3,
render_pdf_primitive(style.stroke_dash_pattern)
+ f" {number_to_str(style.stroke_dash_phase)} d",
)
render_list.append("Q")
return " ".join(render_list)
return ""
class PaintedPath:
"""
A path to be drawn by the PDF renderer.
A painted path is defined by a style and an arbitrary sequence of path elements,
which include the primitive path elements (`Move`, `Line`, `BezierCurve`, ...) as
well as arbitrarily nested `GraphicsContext` containing their own sequence of
primitive path elements and `GraphicsContext`.
"""
def __init__(self, x=0, y=0):
self._root_graphics_context = GraphicsContext()
self._graphics_context = self._root_graphics_context
self._closed = True
self._close_context = self._graphics_context
self._starter_move = Move(Point(x, y))
def __deepcopy__(self, memo):
# there's no real way to recover the matching current _graphics_context after
# copying the root context, but that's ok because we can just disallow copying
# of paths under modification as that is almost certainly wrong usage.
if self._graphics_context is not self._root_graphics_context:
raise RuntimeError(f"cannot copy path {self} while it is being modified")
copied = self.__class__()
copied._root_graphics_context = copy.deepcopy(self._root_graphics_context, memo)
copied._graphics_context = copied._root_graphics_context
copied._closed = self._closed
copied._close_context = copied._graphics_context
return copied
@property
def style(self):
"""The `GraphicsStyle` applied to all elements of this path."""
return self._root_graphics_context.style
@property
def transform(self):
"""The `Transform` that applies to all of the elements of this path."""
return self._root_graphics_context.transform
@transform.setter
def transform(self, tf):
self._root_graphics_context.transform = tf
@property
def auto_close(self):
"""If true, the path should automatically close itself before painting."""
return self.style.auto_close
@auto_close.setter
def auto_close(self, should):
self.style.auto_close = should
@property
def paint_rule(self):
"""Manually specify the `PathPaintRule` to use for rendering the path."""
return self.style.paint_rule
@paint_rule.setter
def paint_rule(self, style):
self.style.paint_rule = style
@property
def clipping_path(self):
"""Set the clipping path for this path."""
return self._root_graphics_context.clipping_path
@clipping_path.setter
def clipping_path(self, new_clipath):
self._root_graphics_context.clipping_path = new_clipath
@contextmanager
def _new_graphics_context(self, _attach=True):
old_graphics_context = self._graphics_context
new_graphics_context = GraphicsContext()
self._graphics_context = new_graphics_context
try:
yield new_graphics_context
if _attach:
old_graphics_context.add_item(new_graphics_context)
finally:
self._graphics_context = old_graphics_context
@contextmanager
def transform_group(self, transform):
"""
Apply the provided `Transform` to all points added within this context.
"""
with self._new_graphics_context() as ctxt:
ctxt.transform = transform
yield self
def add_path_element(self, item, _copy=True):
"""
Add the given element as a path item of this path.
Args:
item: the item to add to this path.
_copy (bool): if true (the default), the item will be copied before being
appended. This prevents modifications to a referenced object from
"retroactively" altering its style/shape and should be disabled with
caution.
"""
if self._starter_move is not None:
self._closed = False
self._graphics_context.add_item(self._starter_move, _copy=False)
self._close_context = self._graphics_context
self._starter_move = None
self._graphics_context.add_item(item, _copy=_copy)
def remove_last_path_element(self):
self._graphics_context.remove_last_item()
def rectangle(self, x, y, w, h, rx=0, ry=0):
"""
Append a rectangle as a closed subpath to the current path.
If the width or the height are 0, the rectangle will be collapsed to a line
(unless they're both 0, in which case it's collapsed to nothing).
Args:
x (Number): the abscissa of the starting corner of the rectangle.
y (Number): the ordinate of the starting corner of the rectangle.
w (Number): the width of the rectangle (if 0, the rectangle will be
rendered as a vertical line).
h (Number): the height of the rectangle (if 0, the rectangle will be
rendered as a horizontal line).
rx (Number): the x-radius of the rectangle rounded corner (if 0 the corners
will not be rounded).
ry (Number): the y-radius of the rectangle rounded corner (if 0 the corners
will not be rounded).
Returns:
The path, to allow chaining method calls.
"""
self._insert_implicit_close_if_open()
self.add_path_element(
RoundedRectangle(Point(x, y), Point(w, h), Point(rx, ry)), _copy=False
)
self._closed = True
self.move_to(x, y)
return self
def circle(self, cx, cy, r):
"""
Append a circle as a closed subpath to the current path.
Args:
cx (Number): the abscissa of the circle's center point.
cy (Number): the ordinate of the circle's center point.
r (Number): the radius of the circle.
Returns:
The path, to allow chaining method calls.
"""
return self.ellipse(cx, cy, r, r)
def ellipse(self, cx, cy, rx, ry):
"""
Append an ellipse as a closed subpath to the current path.
Args:
cx (Number): the abscissa of the ellipse's center point.
cy (Number): the ordinate of the ellipse's center point.
rx (Number): the x-radius of the ellipse.
ry (Number): the y-radius of the ellipse.
Returns:
The path, to allow chaining method calls.
"""
self._insert_implicit_close_if_open()
self.add_path_element(Ellipse(Point(rx, ry), Point(cx, cy)), _copy=False)
self._closed = True
self.move_to(cx, cy)
return self
def move_to(self, x, y):
"""
Start a new subpath or move the path starting point.
If no path elements have been added yet, this will change the path starting
point. If path elements have been added, this will insert an implicit close in
order to start a new subpath.
Args:
x (Number): abscissa of the (sub)path starting point.
y (Number): ordinate of the (sub)path starting point.
Returns:
The path, to allow chaining method calls.
"""
self._insert_implicit_close_if_open()
self._starter_move = Move(Point(x, y))
return self
def move_relative(self, x, y):
"""
Start a new subpath or move the path start point relative to the previous point.
If no path elements have been added yet, this will change the path starting
point. If path elements have been added, this will insert an implicit close in
order to start a new subpath.
This will overwrite an absolute move_to as long as no non-move path items have
been appended. The relative position is resolved from the previous item when
the path is being rendered, or from 0, 0 if it is the first item.
Args:
x (Number): abscissa of the (sub)path starting point relative to the.
y (Number): ordinate of the (sub)path starting point relative to the.
"""
self._insert_implicit_close_if_open()
if self._starter_move is not None:
self._closed = False
self._graphics_context.add_item(self._starter_move, _copy=False)
self._close_context = self._graphics_context
self._starter_move = RelativeMove(Point(x, y))
return self
def line_to(self, x, y):
"""
Append a straight line to this path.
Args:
x (Number): abscissa the line's end point.
y (Number): ordinate of the line's end point.
Returns:
The path, to allow chaining method calls.
"""
self.add_path_element(Line(Point(x, y)), _copy=False)
return self
def line_relative(self, dx, dy):
"""
Append a straight line whose end is computed as an offset from the end of the
previous path element.
Args:
x (Number): abscissa the line's end point relative to the end point of the
previous path element.
y (Number): ordinate of the line's end point relative to the end point of
the previous path element.
Returns:
The path, to allow chaining method calls.
"""
self.add_path_element(RelativeLine(Point(dx, dy)), _copy=False)
return self
def horizontal_line_to(self, x):
"""
Append a straight horizontal line to the given abscissa. The ordinate is
retrieved from the end point of the previous path element.
Args:
x (Number): abscissa of the line's end point.
Returns:
The path, to allow chaining method calls.
"""
self.add_path_element(HorizontalLine(x), _copy=False)
return self
def horizontal_line_relative(self, dx):
"""
Append a straight horizontal line to the given offset from the previous path
element. The ordinate is retrieved from the end point of the previous path
element.
Args:
x (Number): abscissa of the line's end point relative to the end point of
the previous path element.
Returns:
The path, to allow chaining method calls.
"""
self.add_path_element(RelativeHorizontalLine(dx), _copy=False)
return self
def vertical_line_to(self, y):
"""
Append a straight vertical line to the given ordinate. The abscissa is
retrieved from the end point of the previous path element.
Args:
y (Number): ordinate of the line's end point.
Returns:
The path, to allow chaining method calls.
"""
self.add_path_element(VerticalLine(y), _copy=False)
return self
def vertical_line_relative(self, dy):
"""
Append a straight vertical line to the given offset from the previous path
element. The abscissa is retrieved from the end point of the previous path
element.
Args:
y (Number): ordinate of the line's end point relative to the end point of
the previous path element.
Returns:
The path, to allow chaining method calls.
"""
self.add_path_element(RelativeVerticalLine(dy), _copy=False)
return self
def curve_to(self, x1, y1, x2, y2, x3, y3):
"""
Append a cubic Bézier curve to this path.
Args:
x1 (Number): abscissa of the first control point
y1 (Number): ordinate of the first control point
x2 (Number): abscissa of the second control point
y2 (Number): ordinate of the second control point
x3 (Number): abscissa of the end point
y3 (Number): ordinate of the end point
Returns:
The path, to allow chaining method calls.
"""
ctrl1 = Point(x1, y1)
ctrl2 = Point(x2, y2)
end = Point(x3, y3)
self.add_path_element(BezierCurve(ctrl1, ctrl2, end), _copy=False)
return self
def curve_relative(self, dx1, dy1, dx2, dy2, dx3, dy3):
"""
Append a cubic Bézier curve whose points are expressed relative to the
end point of the previous path element.
E.g. with a start point of (0, 0), given (1, 1), (2, 2), (3, 3), the output
curve would have the points:
(0, 0) c1 (1, 1) c2 (3, 3) e (6, 6)
Args:
dx1 (Number): abscissa of the first control point relative to the end point
of the previous path element
dy1 (Number): ordinate of the first control point relative to the end point
of the previous path element
dx2 (Number): abscissa offset of the second control point relative to the
end point of the previous path element
dy2 (Number): ordinate offset of the second control point relative to the
end point of the previous path element
dx3 (Number): abscissa offset of the end point relative to the end point of
the previous path element
dy3 (Number): ordinate offset of the end point relative to the end point of
the previous path element
Returns:
The path, to allow chaining method calls.
"""
c1d = Point(dx1, dy1)
c2d = Point(dx2, dy2)
end = Point(dx3, dy3)
self.add_path_element(RelativeBezierCurve(c1d, c2d, end), _copy=False)
return self
def quadratic_curve_to(self, x1, y1, x2, y2):
"""
Append a cubic Bézier curve mimicking the specified quadratic Bézier curve.
Args:
x1 (Number): abscissa of the control point
y1 (Number): ordinate of the control point
x2 (Number): abscissa of the end point
y2 (Number): ordinate of the end point
Returns:
The path, to allow chaining method calls.
"""
ctrl = Point(x1, y1)
end = Point(x2, y2)
self.add_path_element(QuadraticBezierCurve(ctrl, end), _copy=False)
return self
def quadratic_curve_relative(self, dx1, dy1, dx2, dy2):
"""
Append a cubic Bézier curve mimicking the specified quadratic Bézier curve.
Args:
dx1 (Number): abscissa of the control point relative to the end point of
the previous path element
dy1 (Number): ordinate of the control point relative to the end point of
the previous path element
dx2 (Number): abscissa offset of the end point relative to the end point of
the previous path element
dy2 (Number): ordinate offset of the end point relative to the end point of
the previous path element
Returns:
The path, to allow chaining method calls.
"""
ctrl = Point(dx1, dy1)
end = Point(dx2, dy2)
self.add_path_element(RelativeQuadraticBezierCurve(ctrl, end), _copy=False)
return self
def arc_to(self, rx, ry, rotation, large_arc, positive_sweep, x, y):
"""
Append an elliptical arc from the end of the previous path point to the
specified end point.
The arc is approximated using Bézier curves, so it is not perfectly accurate.
However, the error is small enough to not be noticeable at any reasonable
(and even most unreasonable) scales, with a worst-case deviation of around 3‱.
Notes:
- The signs of the radii arguments (`rx` and `ry`) are ignored (i.e. their
absolute values are used instead).
- If either radius is 0, then a straight line will be emitted instead of an
arc.
- If the radii are too small for the arc to reach from the current point to
the specified end point (`x` and `y`), then they will be proportionally
scaled up until they are big enough, which will always result in a
half-ellipse arc (i.e. an 180 degree sweep)
Args:
rx (Number): radius in the x-direction.
ry (Number): radius in the y-direction.
rotation (Number): angle (in degrees) that the arc should be rotated
clockwise from the principle axes. This parameter does not have
a visual effect in the case that `rx == ry`.
large_arc (bool): if True, the arc will cover a sweep angle of at least 180
degrees. Otherwise, the sweep angle will be at most 180 degrees.
positive_sweep (bool): if True, the arc will be swept over a positive angle,
i.e. clockwise. Otherwise, the arc will be swept over a negative
angle.
x (Number): abscissa of the arc's end point.
y (Number): ordinate of the arc's end point.
"""
if rx == 0 or ry == 0:
return self.line_to(x, y)
radii = Point(abs(rx), abs(ry))
large_arc = bool(large_arc)
rotation = math.radians(rotation)
positive_sweep = bool(positive_sweep)
end = Point(x, y)
self.add_path_element(
Arc(radii, rotation, large_arc, positive_sweep, end), _copy=False
)
return self
def arc_relative(self, rx, ry, rotation, large_arc, positive_sweep, dx, dy):
"""
Append an elliptical arc from the end of the previous path point to an offset
point.
The arc is approximated using Bézier curves, so it is not perfectly accurate.
However, the error is small enough to not be noticeable at any reasonable
(and even most unreasonable) scales, with a worst-case deviation of around 3‱.
Notes:
- The signs of the radii arguments (`rx` and `ry`) are ignored (i.e. their
absolute values are used instead).
- If either radius is 0, then a straight line will be emitted instead of an
arc.
- If the radii are too small for the arc to reach from the current point to
the specified end point (`x` and `y`), then they will be proportionally
scaled up until they are big enough, which will always result in a
half-ellipse arc (i.e. an 180 degree sweep)
Args:
rx (Number): radius in the x-direction.
ry (Number): radius in the y-direction.
rotation (Number): angle (in degrees) that the arc should be rotated
clockwise from the principle axes. This parameter does not have
a visual effect in the case that `rx == ry`.
large_arc (bool): if True, the arc will cover a sweep angle of at least 180
degrees. Otherwise, the sweep angle will be at most 180 degrees.
positive_sweep (bool): if True, the arc will be swept over a positive angle,
i.e. clockwise. Otherwise, the arc will be swept over a negative
angle.
dx (Number): abscissa of the arc's end point relative to the end point of
the previous path element.
dy (Number): ordinate of the arc's end point relative to the end point of
the previous path element.
"""
if rx == 0 or ry == 0:
return self.line_relative(dx, dy)
radii = Point(abs(rx), abs(ry))
large_arc = bool(large_arc)
rotation = math.radians(rotation)
positive_sweep = bool(positive_sweep)
end = Point(dx, dy)
self.add_path_element(
RelativeArc(radii, rotation, large_arc, positive_sweep, end), _copy=False
)
return self
def close(self):
"""
Explicitly close the current (sub)path.
"""
self.add_path_element(Close(), _copy=False)
self._closed = True
self.move_relative(0, 0)
def _insert_implicit_close_if_open(self):
if not self._closed:
self._close_context.add_item(ImplicitClose(), _copy=False)
self._close_context = self._graphics_context
self._closed = True
def render(
self, gsd_registry, style, last_item, initial_point, debug_stream=None, pfx=None
):
self._insert_implicit_close_if_open()
(
render_list,
last_item,
initial_point,
) = self._root_graphics_context.build_render_list(
gsd_registry, style, last_item, initial_point, debug_stream, pfx
)
paint_rule = GraphicsStyle.merge(style, self.style).resolve_paint_rule()
render_list.insert(-1, paint_rule.value)
return " ".join(render_list), last_item, initial_point
def render_debug(
self, gsd_registry, style, last_item, initial_point, debug_stream, pfx
):
"""
Render this path element to its PDF representation and produce debug
information.
Args:
gsd_registry (GraphicsStateDictRegistry): the owner's graphics state
dictionary registry.
style (GraphicsStyle): the current resolved graphics style
last_item: the previous path element.
initial_point: last position set by a "M" or "m" command
debug_stream (io.TextIO): the stream to which the debug output should be
written. This is not guaranteed to be seekable (e.g. it may be stdout or
stderr).
pfx (str): the current debug output prefix string (only needed if emitting
more than one line).
Returns:
The same tuple as `PaintedPath.render`.
"""
return self.render(
gsd_registry, style, last_item, initial_point, debug_stream, pfx
)
class ClippingPath(PaintedPath):
"""
The PaintedPath API but to be used to create clipping paths.
.. warning::
Unless you really know what you're doing, changing attributes of the clipping
path style is likely to produce unexpected results. This is because the
clipping path styles override implicit style inheritance of the `PaintedPath`
it applies to.
For example, `clippath.style.stroke_width = 2` can unexpectedly override
`paintpath.style.stroke_width = GraphicsStyle.INHERIT` and cause the painted
path to be rendered with a stroke of 2 instead of what it would have normally
inherited. Because a `ClippingPath` can be painted like a normal `PaintedPath`,
it would be overly restrictive to remove the ability to style it, so instead
this warning is here.
"""
# because clipping paths can be painted, we inherit from PaintedPath. However, when
# setting the styling on the clipping path, those values will also be applied to
# the PaintedPath the ClippingPath is applied to unless they are explicitly set for
# that painted path. This is not ideal, but there's no way to really fix it from
# the PDF rendering model, and trying to track the appropriate state/defaults seems
# similarly error prone.
# In general, the expectation is that painted clipping paths are likely to be very
# uncommon, so it's an edge case that isn't worth worrying too much about.
def __init__(self, x=0, y=0):
super().__init__(x=x, y=y)
self.paint_rule = PathPaintRule.DONT_PAINT
def render(
self, gsd_registry, style, last_item, initial_point, debug_stream=None, pfx=None
):
# painting the clipping path outside of its root graphics context allows it to
# be transformed without affecting the transform of the graphics context of the
# path it is being used to clip. This is because, unlike all of the other style
# settings, transformations immediately affect the points following them,
# rather than only affecting them at painting time. stroke settings and color
# settings are applied only at paint time.
if debug_stream:
debug_stream.write("<ClippingPath> ")
(
render_list,
last_item,
initial_point,
) = self._root_graphics_context.build_render_list(
gsd_registry,
style,
last_item,
initial_point,
debug_stream,
pfx,
_push_stack=False,
)
merged_style = GraphicsStyle.merge(style, self.style)
# we should never get a collision error here
intersection_rule = merged_style.intersection_rule
if intersection_rule is merged_style.INHERIT:
intersection_rule = ClippingPathIntersectionRule.NONZERO
else:
intersection_rule = ClippingPathIntersectionRule[
intersection_rule.name # pylint: disable=no-member
]
paint_rule = merged_style.resolve_paint_rule()
render_list.append(intersection_rule.value)
render_list.append(paint_rule.value)
return " ".join(render_list), last_item, initial_point
def render_debug(
self, gsd_registry, style, last_item, initial_point, debug_stream, pfx
):
"""
Render this path element to its PDF representation and produce debug
information.
Args:
gsd_registry (GraphicsStateDictRegistry): the owner's graphics state
dictionary registry.
style (GraphicsStyle): the current resolved graphics style
last_item: the previous path element.
debug_stream (io.TextIO): the stream to which the debug output should be
written. This is not guaranteed to be seekable (e.g. it may be stdout or
stderr).
pfx (str): the current debug output prefix string (only needed if emitting
more than one line).
Returns:
The same tuple as `ClippingPath.render`.
"""
return self.render(
gsd_registry, style, last_item, initial_point, debug_stream, pfx
)
class GraphicsContext:
def __init__(self):
self.style = GraphicsStyle()
self.path_items = []
self._transform = None
self._clipping_path = None
def __deepcopy__(self, memo):
copied = self.__class__()
copied.style = copy.deepcopy(self.style, memo)
copied.path_items = copy.deepcopy(self.path_items, memo)
copied._transform = copy.deepcopy(self.transform, memo)
copied._clipping_path = copy.deepcopy(self.clipping_path, memo)
return copied
@property
def transform(self):
return self._transform
@transform.setter
def transform(self, tf):
self._transform = tf
@property
def clipping_path(self):
"""The `ClippingPath` for this graphics context."""
return self._clipping_path
@clipping_path.setter
def clipping_path(self, new_clipath):
self._clipping_path = new_clipath
def add_item(self, item, _copy=True):
"""
Add a path element to this graphics context.
Args:
item: the path element to add. May be a primitive element or another
`GraphicsContext` or a `PaintedPath`.
_copy (bool): if true (the default), the item will be copied before being
appended. This prevents modifications to a referenced object from
"retroactively" altering its style/shape and should be disabled with
caution.
"""
if _copy:
item = copy.deepcopy(item)
self.path_items.append(item)
def remove_last_item(self):
del self.path_items[-1]
def merge(self, other_context):
"""Copy another `GraphicsContext`'s path items into this one."""
self.path_items.extend(other_context.path_items)
@force_nodocument
def build_render_list(
self,
gsd_registry,
style,
last_item,
initial_point,
debug_stream=None,
pfx=None,
_push_stack=True,
):
"""
Build a list composed of all all the individual elements rendered.
This is used by `PaintedPath` and `ClippingPath` to reuse the `GraphicsContext`
rendering process while still being able to inject some path specific items
(e.g. the painting directive) before the render is collapsed into a single
string.
Args:
gsd_registry (GraphicsStateDictRegistry): the owner's graphics state
dictionary registry.
style (GraphicsStyle): the current resolved graphics style
last_item: the previous path element.
initial_point: last position set by a "M" or "m" command
debug_stream (io.TextIO): the stream to which the debug output should be
written. This is not guaranteed to be seekable (e.g. it may be stdout or
stderr).
pfx (str): the current debug output prefix string (only needed if emitting
more than one line).
_push_stack (bool): if True, wrap the resulting render list in a push/pop
graphics stack directive pair.
Returns:
`tuple[list[str], last_item]` where `last_item` is the past path element in
this `GraphicsContext`
"""
render_list = []
if self.path_items:
if debug_stream is not None:
debug_stream.write(f"{self.__class__.__name__}")
merged_style = style.__class__.merge(style, self.style)
if debug_stream is not None:
if self._transform:
debug_stream.write(f"({self._transform})")
styles_dbg = []
for attr in merged_style.MERGE_PROPERTIES:
val = getattr(merged_style, attr)
if val is not merged_style.INHERIT:
if getattr(self.style, attr) is merged_style.INHERIT:
inh = " (inherited)"
else:
inh = ""
styles_dbg.append(f"{attr}: {val}{inh}")
if styles_dbg:
debug_stream.write(" {\n")
for style_dbg_line in styles_dbg:
debug_stream.write(pfx + " ")
debug_stream.write(style_dbg_line)
debug_stream.write("\n")
debug_stream.write(pfx + "}┐\n")
else:
debug_stream.write("\n")
NO_EMIT_SET = {None, merged_style.INHERIT}
emit_style = self.style
if merged_style.allow_transparency != self.style.allow_transparency:
emit_style = copy.deepcopy(self.style)
emit_style.allow_transparency = merged_style.allow_transparency
# in order to decouple the dash pattern and the dash phase at the API layer,
# we have to perform additional logic here to recombine them. We can rely
# on these being serializable because we always get a sane style on the
# drawing context.
dash_pattern = merged_style.stroke_dash_pattern
dash_phase = merged_style.stroke_dash_phase
if (dash_pattern != style.stroke_dash_pattern) or (
dash_phase != style.stroke_dash_phase
):
if emit_style is self.style:
emit_style = copy.deepcopy(emit_style)
emit_style.stroke_dash_pattern = dash_pattern
emit_style.stroke_dash_phase = dash_phase
emit_dash = (dash_pattern, dash_phase)
else:
emit_dash = None
style_dict_name = gsd_registry.register_style(emit_style)
if style_dict_name is not None:
render_list.append(f"{render_pdf_primitive(style_dict_name)} gs")
# we can't set color in the graphics state context dictionary, so we have to
# manually inherit it and emit it here.
fill_color = self.style.fill_color
stroke_color = self.style.stroke_color
if fill_color not in NO_EMIT_SET:
render_list.append(fill_color.serialize().lower())
if stroke_color not in NO_EMIT_SET:
render_list.append(stroke_color.serialize().upper())
if emit_dash is not None:
render_list.append(
render_pdf_primitive(emit_dash[0])
+ f" {number_to_str(emit_dash[1])} d"
)
if debug_stream:
if self.clipping_path is not None:
debug_stream.write(pfx + " ├─ ")
rendered_cpath, _, __ = self.clipping_path.render_debug(
gsd_registry,
merged_style,
last_item,
initial_point,
debug_stream,
pfx + " │ ",
)
if rendered_cpath:
render_list.append(rendered_cpath)
for item in self.path_items[:-1]:
debug_stream.write(pfx + " ├─ ")
rendered, last_item, initial_point = item.render_debug(
gsd_registry,
merged_style,
last_item,
initial_point,
debug_stream,
pfx + " │ ",
)
if rendered:
render_list.append(rendered)
debug_stream.write(pfx + " └─ ")
rendered, last_item, initial_point = self.path_items[-1].render_debug(
gsd_registry,
merged_style,
last_item,
initial_point,
debug_stream,
pfx + " ",
)
if rendered:
render_list.append(rendered)
else:
if self.clipping_path is not None:
rendered_cpath, _, __ = self.clipping_path.render(
gsd_registry, merged_style, last_item, initial_point
)
if rendered_cpath:
render_list.append(rendered_cpath)
for item in self.path_items:
rendered, last_item, initial_point = item.render(
gsd_registry, merged_style, last_item, initial_point
)
if rendered:
render_list.append(rendered)
# insert transform before points
if self.transform is not None:
render_list.insert(0, self.transform.render(last_item)[0])
if _push_stack:
render_list.insert(0, "q")
render_list.append("Q")
return render_list, last_item, initial_point
def render(
self,
gsd_registry,
style: DrawingContext,
last_item,
initial_point,
debug_stream=None,
pfx=None,
_push_stack=True,
):
render_list, last_item, initial_point = self.build_render_list(
gsd_registry,
style,
last_item,
initial_point,
debug_stream,
pfx,
_push_stack=_push_stack,
)
return " ".join(render_list), last_item, initial_point
def render_debug(
self,
gsd_registry,
style: DrawingContext,
last_item,
initial_point,
debug_stream,
pfx,
_push_stack=True,
):
return self.render(
gsd_registry,
style,
last_item,
initial_point,
debug_stream,
pfx,
_push_stack=_push_stack,
)
Global variables
var DELIMITERS
-
Special delimiter characters
var EOL_CHARS
-
Characters PDF considers to mark the end of a line.
var WHITESPACE
-
Characters PDF considers to be whitespace.
Functions
def cmyk8(c, m, y, k, a=None)
-
Produce a DeviceCMYK color from the given 8-bit CMYK values.
Args
c
:Number
- red color component. Must be in the interval [0, 255].
m
:Number
- green color component. Must be in the interval [0, 255].
y
:Number
- blue color component. Must be in the interval [0, 255].
k
:Number
- blue color component. Must be in the interval [0, 255].
a
:Optional[Number]
- alpha component. Must be
None
or in the interval [0, 255]. 0 is fully transparent, 255 is fully opaque
Returns
DeviceCMYK color representation.
Raises
ValueError
- if any components are not in their valid interval.
Expand source code
def cmyk8(c, m, y, k, a=None): """ Produce a DeviceCMYK color from the given 8-bit CMYK values. Args: c (Number): red color component. Must be in the interval [0, 255]. m (Number): green color component. Must be in the interval [0, 255]. y (Number): blue color component. Must be in the interval [0, 255]. k (Number): blue color component. Must be in the interval [0, 255]. a (Optional[Number]): alpha component. Must be `None` or in the interval [0, 255]. 0 is fully transparent, 255 is fully opaque Returns: DeviceCMYK color representation. Raises: ValueError: if any components are not in their valid interval. """ if a is not None: a /= 255.0 return DeviceCMYK(c / 255.0, m / 255.0, y / 255.0, k / 255.0, a)
def color_from_hex_string(hexstr)
-
Parse an RGB color from a css-style 8-bit hexadecimal color string.
Args
hexstr
:str
-
of the form
#RGB
,#RGBA
,#RRGGBB
, or#RRGGBBAA
. Must include the leading octothorp. Forms omitting the alpha field are interpreted as not specifying the opacity, so it will not be explicitly set.An alpha value of
00
is fully transparent andFF
is fully opaque.
Returns
DeviceRGB representation of the color.
Expand source code
def color_from_hex_string(hexstr): """ Parse an RGB color from a css-style 8-bit hexadecimal color string. Args: hexstr (str): of the form `#RGB`, `#RGBA`, `#RRGGBB`, or `#RRGGBBAA`. Must include the leading octothorp. Forms omitting the alpha field are interpreted as not specifying the opacity, so it will not be explicitly set. An alpha value of `00` is fully transparent and `FF` is fully opaque. Returns: DeviceRGB representation of the color. """ if not isinstance(hexstr, str): raise TypeError(f"{hexstr} is not of type str") if not hexstr.startswith("#"): raise ValueError(f"{hexstr} does not start with #") hlen = len(hexstr) if hlen == 4: return rgb8(*[int(char * 2, base=16) for char in hexstr[1:]], a=None) if hlen == 5: return rgb8(*[int(char * 2, base=16) for char in hexstr[1:]]) if hlen == 7: return rgb8( *[int(hexstr[idx : idx + 2], base=16) for idx in range(1, hlen, 2)], a=None ) if hlen == 9: return rgb8(*[int(hexstr[idx : idx + 2], base=16) for idx in range(1, hlen, 2)]) raise ValueError(f"{hexstr} could not be interpreted as a RGB(A) hex string")
def color_from_rgb_string(rgbstr)
-
Parse an RGB color from a css-style rgb(R, G, B, A) color string.
Args
rgbstr
:str
- of the form
rgb(R, G, B)
orrgb(R, G, B, A)
.
Returns
DeviceRGB representation of the color.
Expand source code
def color_from_rgb_string(rgbstr): """ Parse an RGB color from a css-style rgb(R, G, B, A) color string. Args: rgbstr (str): of the form `rgb(R, G, B)` or `rgb(R, G, B, A)`. Returns: DeviceRGB representation of the color. """ if not isinstance(rgbstr, str): raise TypeError(f"{rgbstr} is not of type str") rgbstr = rgbstr.replace(" ", "") if not rgbstr.startswith("rgb(") or not rgbstr.endswith(")"): raise ValueError(f"{rgbstr} does not follow the expected rgb(...) format") rgbstr = rgbstr[4:-1] colors = rgbstr.split(",") if len(colors) == 3: return rgb8(*[int(c) for c in colors], a=None) if len(colors) == 4: return rgb8(*[int(c) for c in colors]) raise ValueError(f"{rgbstr} could not be interpreted as a rgb(R, G, B[, A]) color")
def gray8(g, a=None)
-
Produce a DeviceGray color from the given 8-bit gray value.
Args
g
:Number
- gray color component. Must be in the interval [0, 255]. 0 is black, 255 is white.
a
:Optional[Number]
- alpha component. Must be
None
or in the interval [0, 255]. 0 is fully transparent, 255 is fully opaque
Returns
DeviceGray color representation.
Raises
ValueError
- if any components are not in their valid interval.
Expand source code
def gray8(g, a=None): """ Produce a DeviceGray color from the given 8-bit gray value. Args: g (Number): gray color component. Must be in the interval [0, 255]. 0 is black, 255 is white. a (Optional[Number]): alpha component. Must be `None` or in the interval [0, 255]. 0 is fully transparent, 255 is fully opaque Returns: DeviceGray color representation. Raises: ValueError: if any components are not in their valid interval. """ if a is not None: a /= 255.0 return DeviceGray(g / 255.0, a)
def number_to_str(number)
-
Convert a decimal number to a minimal string representation (no trailing 0 or .).
Args
number
:Number
- the number to be converted to a string.
Returns
The number's string representation.
Expand source code
def number_to_str(number): """ Convert a decimal number to a minimal string representation (no trailing 0 or .). Args: number (Number): the number to be converted to a string. Returns: The number's string representation. """ # this approach tries to produce minimal representations of floating point numbers # but can also produce "-0". return f"{number:.4f}".rstrip("0").rstrip(".")
def render_pdf_primitive(primitive)
-
Render a Python value as a PDF primitive type.
Container types (tuples/lists and dicts) are rendered recursively. This supports values of the type Name, str, bytes, numbers, booleans, list/tuple, and dict.
Any custom type can be passed in as long as it provides a
serialize
method that takes no arguments and returns a string. The primitive object is returned directly if it is an instance of theRaw
class. Otherwise, The existence of theserialize
method is checked before any other type checking is performed, so, for example, adict
subclass with aserialize
method would be converted using itspdf_repr
method rather than the built-indict
conversion process.Args
primitive
- the primitive value to convert to its PDF representation.
Returns
Raw-wrapped str of the PDF representation.
Raises
ValueError
- if a dictionary key is not a Name.
TypeError
- if
primitive
does not have a known conversion to a PDF representation.
Expand source code
def render_pdf_primitive(primitive): """ Render a Python value as a PDF primitive type. Container types (tuples/lists and dicts) are rendered recursively. This supports values of the type Name, str, bytes, numbers, booleans, list/tuple, and dict. Any custom type can be passed in as long as it provides a `serialize` method that takes no arguments and returns a string. The primitive object is returned directly if it is an instance of the `Raw` class. Otherwise, The existence of the `serialize` method is checked before any other type checking is performed, so, for example, a `dict` subclass with a `serialize` method would be converted using its `pdf_repr` method rather than the built-in `dict` conversion process. Args: primitive: the primitive value to convert to its PDF representation. Returns: Raw-wrapped str of the PDF representation. Raises: ValueError: if a dictionary key is not a Name. TypeError: if `primitive` does not have a known conversion to a PDF representation. """ if isinstance(primitive, Raw): return primitive if callable(getattr(primitive, "serialize", None)): output = primitive.serialize() elif primitive is None: output = "null" elif isinstance(primitive, str): output = f"({escape_parens(primitive)})" elif isinstance(primitive, bytes): output = f"<{primitive.hex()}>" elif isinstance(primitive, bool): # has to come before number check output = ["false", "true"][primitive] elif isinstance(primitive, NumberClass): output = number_to_str(primitive) elif isinstance(primitive, (list, tuple)): output = "[" + " ".join(render_pdf_primitive(val) for val in primitive) + "]" elif isinstance(primitive, dict): item_list = [] for key, val in primitive.items(): if not isinstance(key, Name): raise ValueError("dict keys must be Names") item_list.append( render_pdf_primitive(key) + " " + render_pdf_primitive(val) ) output = "<< " + "\n".join(item_list) + " >>" else: raise TypeError(f"cannot produce PDF representation for value {primitive!r}") return Raw(output)
def rgb8(r, g, b, a=None)
-
Produce a DeviceRGB color from the given 8-bit RGB values.
Args
r
:Number
- red color component. Must be in the interval [0, 255].
g
:Number
- green color component. Must be in the interval [0, 255].
b
:Number
- blue color component. Must be in the interval [0, 255].
a
:Optional[Number]
- alpha component. Must be
None
or in the interval [0, 255]. 0 is fully transparent, 255 is fully opaque
Returns
DeviceRGB color representation.
Raises
ValueError
- if any components are not in their valid interval.
Expand source code
def rgb8(r, g, b, a=None): """ Produce a DeviceRGB color from the given 8-bit RGB values. Args: r (Number): red color component. Must be in the interval [0, 255]. g (Number): green color component. Must be in the interval [0, 255]. b (Number): blue color component. Must be in the interval [0, 255]. a (Optional[Number]): alpha component. Must be `None` or in the interval [0, 255]. 0 is fully transparent, 255 is fully opaque Returns: DeviceRGB color representation. Raises: ValueError: if any components are not in their valid interval. """ if a is not None: a /= 255.0 return DeviceRGB(r / 255.0, g / 255.0, b / 255.0, a)
Classes
class Arc (radii: Point, rotation: Union[int, float, decimal.Decimal], large: bool, sweep: bool, end: Point)
-
An elliptical arc path element.
The arc is drawn from the end of the current path element to its specified end point using a number of parameters to determine how it is constructed.
See:
PaintedPath.arc_to()
Expand source code
class Arc(NamedTuple): """ An elliptical arc path element. The arc is drawn from the end of the current path element to its specified end point using a number of parameters to determine how it is constructed. See: `PaintedPath.arc_to` """ radii: Point """ The x- and y-radii of the arc. If `radii.x == radii.y` the arc will be circular. """ rotation: Number """The rotation of the arc's major/minor axes relative to the coordinate frame.""" large: bool """If True, sweep the arc over an angle greater than or equal to 180 degrees.""" sweep: bool """If True, the arc is swept in the positive angular direction.""" end: Point """The end point of the arc.""" @staticmethod @force_nodocument def subdivde_sweep(sweep_angle): """ A generator that subdivides a swept angle into segments no larger than a quarter turn. Any sweep that is larger than a quarter turn is subdivided into as many equally sized segments as necessary to prevent any individual segment from being larger than a quarter turn. This is used for approximating a circular curve segment using cubic Bézier curves. This computes the parameters used for the Bézier approximation up front, as well as the transform necessary to place the segment in the correct position. Args: sweep_angle (Number): the angle to subdivide. Yields: A tuple of (ctrl1, ctrl2, end) representing the control and end points of the cubic Bézier curve approximating the segment as a unit circle centered at the origin. """ sweep_angle = abs(sweep_angle) sweep_left = sweep_angle quarterturn = math.pi / 2 chunks = math.ceil(sweep_angle / quarterturn) sweep_segment = sweep_angle / chunks cos_t = math.cos(sweep_segment) sin_t = math.sin(sweep_segment) kappa = 4 / 3 * math.tan(sweep_segment / 4) ctrl1 = Point(1, kappa) ctrl2 = Point(cos_t + kappa * sin_t, sin_t - kappa * cos_t) end = Point(cos_t, sin_t) for _ in range(chunks): offset = sweep_angle - sweep_left transform = Transform.rotation(offset) yield ctrl1 @ transform, ctrl2 @ transform, end @ transform sweep_left -= sweep_segment def _approximate_arc(self, last_item): """ Approximate this arc with a sequence of `BezierCurve`. Args: last_item: the previous path element (used for its end point) Returns: a list of `BezierCurve`. """ radii = self.radii reverse = Transform.rotation(-self.rotation) forward = Transform.rotation(self.rotation) prime = ((last_item.end_point - self.end) * 0.5) @ reverse lam_da = (prime.x / radii.x) ** 2 + (prime.y / radii.y) ** 2 if lam_da > 1: radii = Point(x=(lam_da**0.5) * radii.x, y=(lam_da**0.5) * radii.y) sign = (self.large != self.sweep) - (self.large == self.sweep) rxry2 = (radii.x * radii.y) ** 2 rxpy2 = (radii.x * prime.y) ** 2 rypx2 = (radii.y * prime.x) ** 2 centerprime = ( sign * math.sqrt(round(rxry2 - rxpy2 - rypx2, 8) / (rxpy2 + rypx2)) * Point( x=radii.x * prime.y / radii.y, y=-radii.y * prime.x / radii.x, ) ) center = (centerprime @ forward) + ((last_item.end_point + self.end) * 0.5) arcstart = Point( x=(prime.x - centerprime.x) / radii.x, y=(prime.y - centerprime.y) / radii.y, ) arcend = Point( x=(-prime.x - centerprime.x) / radii.x, y=(-prime.y - centerprime.y) / radii.y, ) theta = Point(1, 0).angle(arcstart) deltatheta = arcstart.angle(arcend) if (self.sweep is False) and (deltatheta > 0): deltatheta -= math.tau elif (self.sweep is True) and (deltatheta < 0): deltatheta += math.tau sweep_sign = (deltatheta >= 0) - (deltatheta < 0) final_tf = ( Transform.scaling(x=1, y=sweep_sign) # flip negative sweeps .rotate(theta) # rotate start of arc to correct position .scale(radii.x, radii.y) # scale unit circle into the final ellipse shape .rotate(self.rotation) # rotate the ellipse the specified angle .translate(center.x, center.y) # translate to the final coordinates ) curves = [] for ctrl1, ctrl2, end in self.subdivde_sweep(deltatheta): curves.append( BezierCurve(ctrl1 @ final_tf, ctrl2 @ final_tf, end @ final_tf) ) return curves @force_nodocument def render(self, gsd_registry, style, last_item, initial_point): """ Render this path element to its PDF representation. Args: gsd_registry (GraphicsStateDictRegistry): the owner's graphics state dictionary registry. style (GraphicsStyle): the current resolved graphics style last_item: the previous path element. initial_point: last position set by a "M" or "m" command Returns: a tuple of `(str, new_last_item)`, where `new_last_item` is a resolved `BezierCurve`. """ # pylint: disable=unused-argument curves = self._approximate_arc(last_item) if not curves: return "", last_item return ( " ".join( curve.render(gsd_registry, style, prev, initial_point)[0] for prev, curve in zip([last_item, *curves[:-1]], curves) ), curves[-1], initial_point, ) @force_nodocument def render_debug( self, gsd_registry, style, last_item, initial_point, debug_stream, pfx ): """ Render this path element to its PDF representation and produce debug information. Args: gsd_registry (GraphicsStateDictRegistry): the owner's graphics state dictionary registry. style (GraphicsStyle): the current resolved graphics style last_item: the previous path element. initial_point: last position set by a "M" or "m" command debug_stream (io.TextIO): the stream to which the debug output should be written. This is not guaranteed to be seekable (e.g. it may be stdout or stderr). pfx (str): the current debug output prefix string (only needed if emitting more than one line). Returns: The same tuple as `Arc.render`. """ # pylint: disable=unused-argument curves = self._approximate_arc(last_item) debug_stream.write(f"{self} resolved to:\n") if not curves: debug_stream.write(pfx + " └─ nothing\n") return "", last_item previous = [last_item] for curve in curves[:-1]: previous.append(curve) debug_stream.write(pfx + f" ├─ {curve}\n") debug_stream.write(pfx + f" └─ {curves[-1]}\n") return ( " ".join( curve.render(gsd_registry, style, prev, initial_point)[0] for prev, curve in zip(previous, curves) ), curves[-1], initial_point, )
Ancestors
- builtins.tuple
Instance variables
var end : Point
-
The end point of the arc.
var large : bool
-
If True, sweep the arc over an angle greater than or equal to 180 degrees.
var radii : Point
-
The x- and y-radii of the arc. If
radii.x == radii.y
the arc will be circular. var rotation : Union[int, float, decimal.Decimal]
-
The rotation of the arc's major/minor axes relative to the coordinate frame.
var sweep : bool
-
If True, the arc is swept in the positive angular direction.
class BezierCurve (c1: Point, c2: Point, end: Point)
-
A cubic Bézier curve path element.
This draws a Bézier curve parameterized by the end point of the previous path element, two off-curve control points, and an end point.
Expand source code
class BezierCurve(NamedTuple): """ A cubic Bézier curve path element. This draws a Bézier curve parameterized by the end point of the previous path element, two off-curve control points, and an end point. See: `PaintedPath.curve_to` """ c1: Point """The curve's first control point.""" c2: Point """The curve's second control point.""" end: Point """The curve's end point.""" @property def end_point(self): """The end point of this path element.""" return self.end @force_nodocument def render(self, gsd_registry, style, last_item, initial_point): """ Render this path element to its PDF representation. Args: gsd_registry (GraphicsStateDictRegistry): the owner's graphics state dictionary registry. style (GraphicsStyle): the current resolved graphics style last_item: the previous path element. initial_point: last position set by a "M" or "m" command Returns: a tuple of `(str, new_last_item)`, where `new_last_item` is `self` """ # pylint: disable=unused-argument return _render_curve(self.c1, self.c2, self.end), self, initial_point @force_nodocument def render_debug( self, gsd_registry, style, last_item, initial_point, debug_stream, pfx ): """ Render this path element to its PDF representation and produce debug information. Args: gsd_registry (GraphicsStateDictRegistry): the owner's graphics state dictionary registry. style (GraphicsStyle): the current resolved graphics style last_item: the previous path element. initial_point: last position set by a "M" or "m" command debug_stream (io.TextIO): the stream to which the debug output should be written. This is not guaranteed to be seekable (e.g. it may be stdout or stderr). pfx (str): the current debug output prefix string (only needed if emitting more than one line). Returns: The same tuple as `BezierCurve.render`. """ # pylint: disable=unused-argument rendered, resolved, initial_point = self.render( gsd_registry, style, last_item, initial_point ) debug_stream.write(str(self) + "\n") return rendered, resolved, initial_point
Ancestors
- builtins.tuple
Instance variables
var c1 : Point
-
The curve's first control point.
var c2 : Point
-
The curve's second control point.
var end : Point
-
The curve's end point.
var end_point
-
The end point of this path element.
Expand source code
@property def end_point(self): """The end point of this path element.""" return self.end
class ClippingPath (x=0, y=0)
-
The PaintedPath API but to be used to create clipping paths.
Warning
Unless you really know what you're doing, changing attributes of the clipping path style is likely to produce unexpected results. This is because the clipping path styles override implicit style inheritance of the
PaintedPath
it applies to.For example,
clippath.style.stroke_width = 2
can unexpectedly overridepaintpath.style.stroke_width = GraphicsStyle.INHERIT
and cause the painted path to be rendered with a stroke of 2 instead of what it would have normally inherited. Because aClippingPath
can be painted like a normalPaintedPath
, it would be overly restrictive to remove the ability to style it, so instead this warning is here.Expand source code
class ClippingPath(PaintedPath): """ The PaintedPath API but to be used to create clipping paths. .. warning:: Unless you really know what you're doing, changing attributes of the clipping path style is likely to produce unexpected results. This is because the clipping path styles override implicit style inheritance of the `PaintedPath` it applies to. For example, `clippath.style.stroke_width = 2` can unexpectedly override `paintpath.style.stroke_width = GraphicsStyle.INHERIT` and cause the painted path to be rendered with a stroke of 2 instead of what it would have normally inherited. Because a `ClippingPath` can be painted like a normal `PaintedPath`, it would be overly restrictive to remove the ability to style it, so instead this warning is here. """ # because clipping paths can be painted, we inherit from PaintedPath. However, when # setting the styling on the clipping path, those values will also be applied to # the PaintedPath the ClippingPath is applied to unless they are explicitly set for # that painted path. This is not ideal, but there's no way to really fix it from # the PDF rendering model, and trying to track the appropriate state/defaults seems # similarly error prone. # In general, the expectation is that painted clipping paths are likely to be very # uncommon, so it's an edge case that isn't worth worrying too much about. def __init__(self, x=0, y=0): super().__init__(x=x, y=y) self.paint_rule = PathPaintRule.DONT_PAINT def render( self, gsd_registry, style, last_item, initial_point, debug_stream=None, pfx=None ): # painting the clipping path outside of its root graphics context allows it to # be transformed without affecting the transform of the graphics context of the # path it is being used to clip. This is because, unlike all of the other style # settings, transformations immediately affect the points following them, # rather than only affecting them at painting time. stroke settings and color # settings are applied only at paint time. if debug_stream: debug_stream.write("<ClippingPath> ") ( render_list, last_item, initial_point, ) = self._root_graphics_context.build_render_list( gsd_registry, style, last_item, initial_point, debug_stream, pfx, _push_stack=False, ) merged_style = GraphicsStyle.merge(style, self.style) # we should never get a collision error here intersection_rule = merged_style.intersection_rule if intersection_rule is merged_style.INHERIT: intersection_rule = ClippingPathIntersectionRule.NONZERO else: intersection_rule = ClippingPathIntersectionRule[ intersection_rule.name # pylint: disable=no-member ] paint_rule = merged_style.resolve_paint_rule() render_list.append(intersection_rule.value) render_list.append(paint_rule.value) return " ".join(render_list), last_item, initial_point def render_debug( self, gsd_registry, style, last_item, initial_point, debug_stream, pfx ): """ Render this path element to its PDF representation and produce debug information. Args: gsd_registry (GraphicsStateDictRegistry): the owner's graphics state dictionary registry. style (GraphicsStyle): the current resolved graphics style last_item: the previous path element. debug_stream (io.TextIO): the stream to which the debug output should be written. This is not guaranteed to be seekable (e.g. it may be stdout or stderr). pfx (str): the current debug output prefix string (only needed if emitting more than one line). Returns: The same tuple as `ClippingPath.render`. """ return self.render( gsd_registry, style, last_item, initial_point, debug_stream, pfx )
Ancestors
Methods
def render(self, gsd_registry, style, last_item, initial_point, debug_stream=None, pfx=None)
-
Expand source code
def render( self, gsd_registry, style, last_item, initial_point, debug_stream=None, pfx=None ): # painting the clipping path outside of its root graphics context allows it to # be transformed without affecting the transform of the graphics context of the # path it is being used to clip. This is because, unlike all of the other style # settings, transformations immediately affect the points following them, # rather than only affecting them at painting time. stroke settings and color # settings are applied only at paint time. if debug_stream: debug_stream.write("<ClippingPath> ") ( render_list, last_item, initial_point, ) = self._root_graphics_context.build_render_list( gsd_registry, style, last_item, initial_point, debug_stream, pfx, _push_stack=False, ) merged_style = GraphicsStyle.merge(style, self.style) # we should never get a collision error here intersection_rule = merged_style.intersection_rule if intersection_rule is merged_style.INHERIT: intersection_rule = ClippingPathIntersectionRule.NONZERO else: intersection_rule = ClippingPathIntersectionRule[ intersection_rule.name # pylint: disable=no-member ] paint_rule = merged_style.resolve_paint_rule() render_list.append(intersection_rule.value) render_list.append(paint_rule.value) return " ".join(render_list), last_item, initial_point
def render_debug(self, gsd_registry, style, last_item, initial_point, debug_stream, pfx)
-
Render this path element to its PDF representation and produce debug information.
Args
gsd_registry
:GraphicsStateDictRegistry
- the owner's graphics state dictionary registry.
style
:GraphicsStyle
- the current resolved graphics style
last_item
- the previous path element.
debug_stream
:io.TextIO
- the stream to which the debug output should be written. This is not guaranteed to be seekable (e.g. it may be stdout or stderr).
pfx
:str
- the current debug output prefix string (only needed if emitting more than one line).
Returns
The same tuple as
ClippingPath.render()
.Expand source code
def render_debug( self, gsd_registry, style, last_item, initial_point, debug_stream, pfx ): """ Render this path element to its PDF representation and produce debug information. Args: gsd_registry (GraphicsStateDictRegistry): the owner's graphics state dictionary registry. style (GraphicsStyle): the current resolved graphics style last_item: the previous path element. debug_stream (io.TextIO): the stream to which the debug output should be written. This is not guaranteed to be seekable (e.g. it may be stdout or stderr). pfx (str): the current debug output prefix string (only needed if emitting more than one line). Returns: The same tuple as `ClippingPath.render`. """ return self.render( gsd_registry, style, last_item, initial_point, debug_stream, pfx )
Inherited members
PaintedPath
:add_path_element
arc_relative
arc_to
auto_close
circle
clipping_path
close
curve_relative
curve_to
ellipse
horizontal_line_relative
horizontal_line_to
line_relative
line_to
move_relative
move_to
paint_rule
quadratic_curve_relative
quadratic_curve_to
rectangle
style
transform
transform_group
vertical_line_relative
vertical_line_to
class Close
-
A path close element.
Instructs the renderer to draw a straight line from the end of the last path element to the start of the current path.
See:
PaintedPath.close()
Expand source code
class Close(NamedTuple): """ A path close element. Instructs the renderer to draw a straight line from the end of the last path element to the start of the current path. See: `PaintedPath.close` """ @force_nodocument def render(self, gsd_registry, style, last_item, initial_point): """ Render this path element to its PDF representation. Args: gsd_registry (GraphicsStateDictRegistry): the owner's graphics state dictionary registry. style (GraphicsStyle): the current resolved graphics style last_item: the previous path element. initial_point: last position set by a "M" or "m" command Returns: a tuple of `(str, new_last_item)`, where `new_last_item` is whatever the old last_item was. """ # pylint: disable=unused-argument return "h", Move(initial_point), initial_point @force_nodocument def render_debug( self, gsd_registry, style, last_item, initial_point, debug_stream, pfx ): """ Render this path element to its PDF representation and produce debug information. Args: gsd_registry (GraphicsStateDictRegistry): the owner's graphics state dictionary registry. style (GraphicsStyle): the current resolved graphics style last_item: the previous path element. initial_point: last position set by a "M" or "m" command debug_stream (io.TextIO): the stream to which the debug output should be written. This is not guaranteed to be seekable (e.g. it may be stdout or stderr). pfx (str): the current debug output prefix string (only needed if emitting more than one line). Returns: The same tuple as `Close.render`. """ # pylint: disable=unused-argument rendered, resolved, initial_point = self.render( gsd_registry, style, last_item, initial_point ) debug_stream.write(str(self) + "\n") return rendered, resolved, initial_point
Ancestors
- builtins.tuple
class DeviceCMYK (c, m, y, k, a=None)
-
A class representing a PDF DeviceCMYK color.
Expand source code
class DeviceCMYK( NamedTuple( "DeviceCMYK", [ ("c", Number), ("m", Number), ("y", Number), ("k", Number), ("a", Optional[Number]), ], ) ): """A class representing a PDF DeviceCMYK color.""" OPERATOR = "k" """The PDF drawing operator used to specify this type of color.""" def __new__(cls, c, m, y, k, a=None): if a is not None: _check_range(a) return super().__new__( cls, _check_range(c), _check_range(m), _check_range(y), _check_range(k), a ) @property def colors(self): """The color components as a tuple in order (c, m, y, k) with alpha omitted.""" return self[:-1] def serialize(self) -> str: return " ".join(number_to_str(val) for val in self.colors) + f" {self.OPERATOR}"
Ancestors
- builtins.tuple
Instance variables
var colors
-
The color components as a tuple in order (c, m, y, k) with alpha omitted.
Expand source code
@property def colors(self): """The color components as a tuple in order (c, m, y, k) with alpha omitted.""" return self[:-1]
Methods
def serialize(self) ‑> str
-
Expand source code
def serialize(self) -> str: return " ".join(number_to_str(val) for val in self.colors) + f" {self.OPERATOR}"
class DeviceGray (g, a=None)
-
A class representing a PDF DeviceGray color.
Expand source code
class DeviceGray( NamedTuple( "DeviceGray", [("g", Number), ("a", Optional[Number])], ) ): """A class representing a PDF DeviceGray color.""" OPERATOR = "g" """The PDF drawing operator used to specify this type of color.""" def __new__(cls, g, a=None): if a is not None: _check_range(a) return super().__new__(cls, _check_range(g), a) @property def colors(self): """The color components as a tuple in order (g,) with alpha omitted.""" return self[:-1] def serialize(self) -> str: return " ".join(number_to_str(val) for val in self.colors) + f" {self.OPERATOR}"
Ancestors
- builtins.tuple
Instance variables
var colors
-
The color components as a tuple in order (g,) with alpha omitted.
Expand source code
@property def colors(self): """The color components as a tuple in order (g,) with alpha omitted.""" return self[:-1]
Methods
def serialize(self) ‑> str
-
Expand source code
def serialize(self) -> str: return " ".join(number_to_str(val) for val in self.colors) + f" {self.OPERATOR}"
class DeviceRGB (r, g, b, a=None)
-
A class representing a PDF DeviceRGB color.
Expand source code
class DeviceRGB( NamedTuple( "DeviceRGB", [("r", Number), ("g", Number), ("b", Number), ("a", Optional[Number])], ) ): """A class representing a PDF DeviceRGB color.""" # This follows a common PDF drawing operator convention where the operand is upcased # to apply to stroke and downcased to apply to fill. # This could be more manually specified by `CS`/`cs` to set the color space(e.g. to # `/DeviceRGB`) and `SC`/`sc` to set the color parameters. The documentation isn't # perfectly clear on this front, but it appears that these cannot be set in the # current graphics state dictionary and instead is set in the current page resource # dictionary. fpdf appears to only generate a single resource dictionary for the # entire document, and even if it created one per page, it would still be a lot # clunkier to try to use that. # Because PDF hates me, personally, the opacity of the drawing HAS to be specified # in the current graphics state dictionary and does not exist as a standalone # directive. OPERATOR = "rg" """The PDF drawing operator used to specify this type of color.""" def __new__(cls, r, g, b, a=None): if a is not None: _check_range(a) return super().__new__( cls, _check_range(r), _check_range(g), _check_range(b), a ) @property def colors(self): """The color components as a tuple in order `(r, g, b)` with alpha omitted.""" return self[:-1] def serialize(self) -> str: return " ".join(number_to_str(val) for val in self.colors) + f" {self.OPERATOR}"
Ancestors
- builtins.tuple
Instance variables
var colors
-
The color components as a tuple in order
(r, g, b)
with alpha omitted.Expand source code
@property def colors(self): """The color components as a tuple in order `(r, g, b)` with alpha omitted.""" return self[:-1]
Methods
def serialize(self) ‑> str
-
Expand source code
def serialize(self) -> str: return " ".join(number_to_str(val) for val in self.colors) + f" {self.OPERATOR}"
class DrawingContext
-
Base context for a drawing in a PDF
This context is not stylable and is mainly responsible for transforming path drawing coordinates into user coordinates (i.e. it ensures that the output drawing is correctly scaled).
Expand source code
class DrawingContext: """ Base context for a drawing in a PDF This context is not stylable and is mainly responsible for transforming path drawing coordinates into user coordinates (i.e. it ensures that the output drawing is correctly scaled). """ def __init__(self): self._subitems = [] def add_item(self, item, _copy=True): """ Append an item to this drawing context Args: item (GraphicsContext, PaintedPath): the item to be appended. _copy (bool): if true (the default), the item will be copied before being appended. This prevents modifications to a referenced object from "retroactively" altering its style/shape and should be disabled with caution. """ if not isinstance(item, (GraphicsContext, PaintedPath)): raise TypeError(f"{item} doesn't belong in a DrawingContext") if _copy: item = copy.deepcopy(item) self._subitems.append(item) @staticmethod def _setup_render_prereqs(style, first_point, scale, height): style.auto_close = True style.paint_rule = PathPaintRule.AUTO style.intersection_rule = IntersectionRule.NONZERO last_item = Move(first_point) scale, last_item = ( Transform.scaling(x=1, y=-1) .about(x=0, y=height / 2) .scale(scale) .render(last_item) ) render_list = ["q", scale] return render_list, style, last_item def render(self, gsd_registry, first_point, scale, height, starting_style): """ Render the drawing context to PDF format. Args: gsd_registry (GraphicsStateDictRegistry): the parent document's graphics state registry. first_point (Point): the starting point to use if the first path element is a relative element. scale (Number): the scale factor to convert from PDF pt units into the document's semantic units (e.g. mm or in). height (Number): the page height. This is used to remap the coordinates to be from the top-left corner of the page (matching fpdf's behavior) instead of the PDF native behavior of bottom-left. starting_style (GraphicsStyle): the base style for this drawing context, derived from the document's current style defaults. Returns: A string composed of the PDF representation of all the paths and groups in this context (an empty string is returned if there are no paths or groups) """ if not self._subitems: return "" render_list, style, last_item = self._setup_render_prereqs( starting_style, first_point, scale, height ) for item in self._subitems: rendered, last_item, first_point = item.render( gsd_registry, style, last_item, first_point ) if rendered: render_list.append(rendered) # there was nothing to render: the only items are the start group and scale # transform. if len(render_list) == 2: return "" style_dict_name = gsd_registry.register_style(style) if style_dict_name is not None: render_list.insert(2, f"{render_pdf_primitive(style_dict_name)} gs") render_list.insert( 3, render_pdf_primitive(style.stroke_dash_pattern) + f" {number_to_str(style.stroke_dash_phase)} d", ) render_list.append("Q") return " ".join(render_list) def render_debug( self, gsd_registry, first_point, scale, height, starting_style, debug_stream ): """ Render the drawing context to PDF format. Args: gsd_registry (GraphicsStateDictRegistry): the parent document's graphics state registry. first_point (Point): the starting point to use if the first path element is a relative element. scale (Number): the scale factor to convert from PDF pt units into the document's semantic units (e.g. mm or in). height (Number): the page height. This is used to remap the coordinates to be from the top-left corner of the page (matching fpdf's behavior) instead of the PDF native behavior of bottom-left. starting_style (GraphicsStyle): the base style for this drawing context, derived from the document's current style defaults. debug_stream (TextIO): a text stream to which a debug representation of the drawing structure will be written. Returns: A string composed of the PDF representation of all the paths and groups in this context (an empty string is returned if there are no paths or groups) """ render_list, style, last_item = self._setup_render_prereqs( starting_style, first_point, scale, height ) debug_stream.write("ROOT\n") for child in self._subitems[:-1]: debug_stream.write(" ├─ ") rendered, last_item = child.render_debug( gsd_registry, style, last_item, debug_stream, " │ " ) if rendered: render_list.append(rendered) if self._subitems: debug_stream.write(" └─ ") rendered, last_item, first_point = self._subitems[-1].render_debug( gsd_registry, style, last_item, first_point, debug_stream, " " ) if rendered: render_list.append(rendered) # there was nothing to render: the only items are the start group and scale # transform. if len(render_list) == 2: return "" style_dict_name = gsd_registry.register_style(style) if style_dict_name is not None: render_list.insert(2, f"{render_pdf_primitive(style_dict_name)} gs") render_list.insert( 3, render_pdf_primitive(style.stroke_dash_pattern) + f" {number_to_str(style.stroke_dash_phase)} d", ) render_list.append("Q") return " ".join(render_list) return ""
Methods
def add_item(self, item)
-
Append an item to this drawing context
Args
item
:GraphicsContext, PaintedPath
- the item to be appended.
_copy
:bool
- if true (the default), the item will be copied before being appended. This prevents modifications to a referenced object from "retroactively" altering its style/shape and should be disabled with caution.
Expand source code
def add_item(self, item, _copy=True): """ Append an item to this drawing context Args: item (GraphicsContext, PaintedPath): the item to be appended. _copy (bool): if true (the default), the item will be copied before being appended. This prevents modifications to a referenced object from "retroactively" altering its style/shape and should be disabled with caution. """ if not isinstance(item, (GraphicsContext, PaintedPath)): raise TypeError(f"{item} doesn't belong in a DrawingContext") if _copy: item = copy.deepcopy(item) self._subitems.append(item)
def render(self, gsd_registry, first_point, scale, height, starting_style)
-
Render the drawing context to PDF format.
Args
gsd_registry
:GraphicsStateDictRegistry
- the parent document's graphics state registry.
first_point
:Point
- the starting point to use if the first path element is a relative element.
scale
:Number
- the scale factor to convert from PDF pt units into the document's semantic units (e.g. mm or in).
height
:Number
- the page height. This is used to remap the coordinates to be from the top-left corner of the page (matching fpdf's behavior) instead of the PDF native behavior of bottom-left.
starting_style
:GraphicsStyle
- the base style for this drawing context, derived from the document's current style defaults.
Returns
A string composed of the PDF representation of all the paths and groups in this context (an empty string is returned if there are no paths or groups)
Expand source code
def render(self, gsd_registry, first_point, scale, height, starting_style): """ Render the drawing context to PDF format. Args: gsd_registry (GraphicsStateDictRegistry): the parent document's graphics state registry. first_point (Point): the starting point to use if the first path element is a relative element. scale (Number): the scale factor to convert from PDF pt units into the document's semantic units (e.g. mm or in). height (Number): the page height. This is used to remap the coordinates to be from the top-left corner of the page (matching fpdf's behavior) instead of the PDF native behavior of bottom-left. starting_style (GraphicsStyle): the base style for this drawing context, derived from the document's current style defaults. Returns: A string composed of the PDF representation of all the paths and groups in this context (an empty string is returned if there are no paths or groups) """ if not self._subitems: return "" render_list, style, last_item = self._setup_render_prereqs( starting_style, first_point, scale, height ) for item in self._subitems: rendered, last_item, first_point = item.render( gsd_registry, style, last_item, first_point ) if rendered: render_list.append(rendered) # there was nothing to render: the only items are the start group and scale # transform. if len(render_list) == 2: return "" style_dict_name = gsd_registry.register_style(style) if style_dict_name is not None: render_list.insert(2, f"{render_pdf_primitive(style_dict_name)} gs") render_list.insert( 3, render_pdf_primitive(style.stroke_dash_pattern) + f" {number_to_str(style.stroke_dash_phase)} d", ) render_list.append("Q") return " ".join(render_list)
def render_debug(self, gsd_registry, first_point, scale, height, starting_style, debug_stream)
-
Render the drawing context to PDF format.
Args
gsd_registry
:GraphicsStateDictRegistry
- the parent document's graphics state registry.
first_point
:Point
- the starting point to use if the first path element is a relative element.
scale
:Number
- the scale factor to convert from PDF pt units into the document's semantic units (e.g. mm or in).
height
:Number
- the page height. This is used to remap the coordinates to be from the top-left corner of the page (matching fpdf's behavior) instead of the PDF native behavior of bottom-left.
starting_style
:GraphicsStyle
- the base style for this drawing context, derived from the document's current style defaults.
debug_stream
:TextIO
- a text stream to which a debug representation of the drawing structure will be written.
Returns
A string composed of the PDF representation of all the paths and groups in this context (an empty string is returned if there are no paths or groups)
Expand source code
def render_debug( self, gsd_registry, first_point, scale, height, starting_style, debug_stream ): """ Render the drawing context to PDF format. Args: gsd_registry (GraphicsStateDictRegistry): the parent document's graphics state registry. first_point (Point): the starting point to use if the first path element is a relative element. scale (Number): the scale factor to convert from PDF pt units into the document's semantic units (e.g. mm or in). height (Number): the page height. This is used to remap the coordinates to be from the top-left corner of the page (matching fpdf's behavior) instead of the PDF native behavior of bottom-left. starting_style (GraphicsStyle): the base style for this drawing context, derived from the document's current style defaults. debug_stream (TextIO): a text stream to which a debug representation of the drawing structure will be written. Returns: A string composed of the PDF representation of all the paths and groups in this context (an empty string is returned if there are no paths or groups) """ render_list, style, last_item = self._setup_render_prereqs( starting_style, first_point, scale, height ) debug_stream.write("ROOT\n") for child in self._subitems[:-1]: debug_stream.write(" ├─ ") rendered, last_item = child.render_debug( gsd_registry, style, last_item, debug_stream, " │ " ) if rendered: render_list.append(rendered) if self._subitems: debug_stream.write(" └─ ") rendered, last_item, first_point = self._subitems[-1].render_debug( gsd_registry, style, last_item, first_point, debug_stream, " " ) if rendered: render_list.append(rendered) # there was nothing to render: the only items are the start group and scale # transform. if len(render_list) == 2: return "" style_dict_name = gsd_registry.register_style(style) if style_dict_name is not None: render_list.insert(2, f"{render_pdf_primitive(style_dict_name)} gs") render_list.insert( 3, render_pdf_primitive(style.stroke_dash_pattern) + f" {number_to_str(style.stroke_dash_phase)} d", ) render_list.append("Q") return " ".join(render_list) return ""
class Ellipse (radii: Point, center: Point)
-
An ellipse.
Expand source code
class Ellipse(NamedTuple): """ An ellipse. See: `PaintedPath.ellipse` """ radii: Point """The x- and y-radii of the ellipse""" center: Point """The abscissa and ordinate of the center of the ellipse""" def _decompose(self): items = [] rx = abs(self.radii.x) ry = abs(self.radii.y) cx, cy = self.center arc_rad = Point(rx, ry) # this isn't the most efficient way to do this, computationally, but it's # internally consistent. if (rx != 0) and (ry != 0): items.append(Move(Point(cx + rx, cy))) items.append(Arc(arc_rad, 0, False, True, Point(cx, cy + ry))) items.append(Arc(arc_rad, 0, False, True, Point(cx - rx, cy))) items.append(Arc(arc_rad, 0, False, True, Point(cx, cy - ry))) items.append(Arc(arc_rad, 0, False, True, Point(cx + rx, cy))) items.append(Close()) return items @force_nodocument def render(self, gsd_registry, style, last_item, initial_point): """ Render this path element to its PDF representation. Args: gsd_registry (GraphicsStateDictRegistry): the owner's graphics state dictionary registry. style (GraphicsStyle): the current resolved graphics style last_item: the previous path element. initial_point: last position set by a "M" or "m" command Returns: a tuple of `(str, new_last_item)`, where `new_last_item` is a resolved `Move` to the center of the ellipse. """ # pylint: disable=unused-argument components = self._decompose() if not components: return "", last_item render_list = [] for item in components: rendered, last_item, initial_point = item.render( gsd_registry, style, last_item, initial_point ) render_list.append(rendered) return " ".join(render_list), Move(self.center), initial_point @force_nodocument def render_debug( self, gsd_registry, style, last_item, initial_point, debug_stream, pfx ): """ Render this path element to its PDF representation and produce debug information. Args: gsd_registry (GraphicsStateDictRegistry): the owner's graphics state dictionary registry. style (GraphicsStyle): the current resolved graphics style last_item: the previous path element. initial_point: last position set by a "M" or "m" command debug_stream (io.TextIO): the stream to which the debug output should be written. This is not guaranteed to be seekable (e.g. it may be stdout or stderr). pfx (str): the current debug output prefix string (only needed if emitting more than one line). Returns: The same tuple as `Ellipse.render`. """ # pylint: disable=unused-argument components = self._decompose() debug_stream.write(f"{self} resolved to:\n") if not components: debug_stream.write(pfx + " └─ nothing\n") return "", last_item render_list = [] for item in components[:-1]: rendered, last_item, initial_point = item.render( gsd_registry, style, last_item, initial_point ) debug_stream.write(pfx + f" ├─ {item}\n") render_list.append(rendered) rendered, last_item, initial_point = components[-1].render( gsd_registry, style, last_item, initial_point ) debug_stream.write(pfx + f" └─ {components[-1]}\n") render_list.append(rendered) return " ".join(render_list), Move(self.center), initial_point
Ancestors
- builtins.tuple
Instance variables
var center : Point
-
The abscissa and ordinate of the center of the ellipse
var radii : Point
-
The x- and y-radii of the ellipse
class GraphicsContext
-
Expand source code
class GraphicsContext: def __init__(self): self.style = GraphicsStyle() self.path_items = [] self._transform = None self._clipping_path = None def __deepcopy__(self, memo): copied = self.__class__() copied.style = copy.deepcopy(self.style, memo) copied.path_items = copy.deepcopy(self.path_items, memo) copied._transform = copy.deepcopy(self.transform, memo) copied._clipping_path = copy.deepcopy(self.clipping_path, memo) return copied @property def transform(self): return self._transform @transform.setter def transform(self, tf): self._transform = tf @property def clipping_path(self): """The `ClippingPath` for this graphics context.""" return self._clipping_path @clipping_path.setter def clipping_path(self, new_clipath): self._clipping_path = new_clipath def add_item(self, item, _copy=True): """ Add a path element to this graphics context. Args: item: the path element to add. May be a primitive element or another `GraphicsContext` or a `PaintedPath`. _copy (bool): if true (the default), the item will be copied before being appended. This prevents modifications to a referenced object from "retroactively" altering its style/shape and should be disabled with caution. """ if _copy: item = copy.deepcopy(item) self.path_items.append(item) def remove_last_item(self): del self.path_items[-1] def merge(self, other_context): """Copy another `GraphicsContext`'s path items into this one.""" self.path_items.extend(other_context.path_items) @force_nodocument def build_render_list( self, gsd_registry, style, last_item, initial_point, debug_stream=None, pfx=None, _push_stack=True, ): """ Build a list composed of all all the individual elements rendered. This is used by `PaintedPath` and `ClippingPath` to reuse the `GraphicsContext` rendering process while still being able to inject some path specific items (e.g. the painting directive) before the render is collapsed into a single string. Args: gsd_registry (GraphicsStateDictRegistry): the owner's graphics state dictionary registry. style (GraphicsStyle): the current resolved graphics style last_item: the previous path element. initial_point: last position set by a "M" or "m" command debug_stream (io.TextIO): the stream to which the debug output should be written. This is not guaranteed to be seekable (e.g. it may be stdout or stderr). pfx (str): the current debug output prefix string (only needed if emitting more than one line). _push_stack (bool): if True, wrap the resulting render list in a push/pop graphics stack directive pair. Returns: `tuple[list[str], last_item]` where `last_item` is the past path element in this `GraphicsContext` """ render_list = [] if self.path_items: if debug_stream is not None: debug_stream.write(f"{self.__class__.__name__}") merged_style = style.__class__.merge(style, self.style) if debug_stream is not None: if self._transform: debug_stream.write(f"({self._transform})") styles_dbg = [] for attr in merged_style.MERGE_PROPERTIES: val = getattr(merged_style, attr) if val is not merged_style.INHERIT: if getattr(self.style, attr) is merged_style.INHERIT: inh = " (inherited)" else: inh = "" styles_dbg.append(f"{attr}: {val}{inh}") if styles_dbg: debug_stream.write(" {\n") for style_dbg_line in styles_dbg: debug_stream.write(pfx + " ") debug_stream.write(style_dbg_line) debug_stream.write("\n") debug_stream.write(pfx + "}┐\n") else: debug_stream.write("\n") NO_EMIT_SET = {None, merged_style.INHERIT} emit_style = self.style if merged_style.allow_transparency != self.style.allow_transparency: emit_style = copy.deepcopy(self.style) emit_style.allow_transparency = merged_style.allow_transparency # in order to decouple the dash pattern and the dash phase at the API layer, # we have to perform additional logic here to recombine them. We can rely # on these being serializable because we always get a sane style on the # drawing context. dash_pattern = merged_style.stroke_dash_pattern dash_phase = merged_style.stroke_dash_phase if (dash_pattern != style.stroke_dash_pattern) or ( dash_phase != style.stroke_dash_phase ): if emit_style is self.style: emit_style = copy.deepcopy(emit_style) emit_style.stroke_dash_pattern = dash_pattern emit_style.stroke_dash_phase = dash_phase emit_dash = (dash_pattern, dash_phase) else: emit_dash = None style_dict_name = gsd_registry.register_style(emit_style) if style_dict_name is not None: render_list.append(f"{render_pdf_primitive(style_dict_name)} gs") # we can't set color in the graphics state context dictionary, so we have to # manually inherit it and emit it here. fill_color = self.style.fill_color stroke_color = self.style.stroke_color if fill_color not in NO_EMIT_SET: render_list.append(fill_color.serialize().lower()) if stroke_color not in NO_EMIT_SET: render_list.append(stroke_color.serialize().upper()) if emit_dash is not None: render_list.append( render_pdf_primitive(emit_dash[0]) + f" {number_to_str(emit_dash[1])} d" ) if debug_stream: if self.clipping_path is not None: debug_stream.write(pfx + " ├─ ") rendered_cpath, _, __ = self.clipping_path.render_debug( gsd_registry, merged_style, last_item, initial_point, debug_stream, pfx + " │ ", ) if rendered_cpath: render_list.append(rendered_cpath) for item in self.path_items[:-1]: debug_stream.write(pfx + " ├─ ") rendered, last_item, initial_point = item.render_debug( gsd_registry, merged_style, last_item, initial_point, debug_stream, pfx + " │ ", ) if rendered: render_list.append(rendered) debug_stream.write(pfx + " └─ ") rendered, last_item, initial_point = self.path_items[-1].render_debug( gsd_registry, merged_style, last_item, initial_point, debug_stream, pfx + " ", ) if rendered: render_list.append(rendered) else: if self.clipping_path is not None: rendered_cpath, _, __ = self.clipping_path.render( gsd_registry, merged_style, last_item, initial_point ) if rendered_cpath: render_list.append(rendered_cpath) for item in self.path_items: rendered, last_item, initial_point = item.render( gsd_registry, merged_style, last_item, initial_point ) if rendered: render_list.append(rendered) # insert transform before points if self.transform is not None: render_list.insert(0, self.transform.render(last_item)[0]) if _push_stack: render_list.insert(0, "q") render_list.append("Q") return render_list, last_item, initial_point def render( self, gsd_registry, style: DrawingContext, last_item, initial_point, debug_stream=None, pfx=None, _push_stack=True, ): render_list, last_item, initial_point = self.build_render_list( gsd_registry, style, last_item, initial_point, debug_stream, pfx, _push_stack=_push_stack, ) return " ".join(render_list), last_item, initial_point def render_debug( self, gsd_registry, style: DrawingContext, last_item, initial_point, debug_stream, pfx, _push_stack=True, ): return self.render( gsd_registry, style, last_item, initial_point, debug_stream, pfx, _push_stack=_push_stack, )
Instance variables
var clipping_path
-
The
ClippingPath
for this graphics context.Expand source code
@property def clipping_path(self): """The `ClippingPath` for this graphics context.""" return self._clipping_path
var transform
-
Expand source code
@property def transform(self): return self._transform
Methods
def add_item(self, item)
-
Add a path element to this graphics context.
Args
item
- the path element to add. May be a primitive element or another
GraphicsContext
or aPaintedPath
. _copy
:bool
- if true (the default), the item will be copied before being appended. This prevents modifications to a referenced object from "retroactively" altering its style/shape and should be disabled with caution.
Expand source code
def add_item(self, item, _copy=True): """ Add a path element to this graphics context. Args: item: the path element to add. May be a primitive element or another `GraphicsContext` or a `PaintedPath`. _copy (bool): if true (the default), the item will be copied before being appended. This prevents modifications to a referenced object from "retroactively" altering its style/shape and should be disabled with caution. """ if _copy: item = copy.deepcopy(item) self.path_items.append(item)
def merge(self, other_context)
-
Copy another
GraphicsContext
's path items into this one.Expand source code
def merge(self, other_context): """Copy another `GraphicsContext`'s path items into this one.""" self.path_items.extend(other_context.path_items)
def remove_last_item(self)
-
Expand source code
def remove_last_item(self): del self.path_items[-1]
def render(self, gsd_registry, style: DrawingContext, last_item, initial_point, debug_stream=None, pfx=None)
-
Expand source code
def render( self, gsd_registry, style: DrawingContext, last_item, initial_point, debug_stream=None, pfx=None, _push_stack=True, ): render_list, last_item, initial_point = self.build_render_list( gsd_registry, style, last_item, initial_point, debug_stream, pfx, _push_stack=_push_stack, ) return " ".join(render_list), last_item, initial_point
def render_debug(self, gsd_registry, style: DrawingContext, last_item, initial_point, debug_stream, pfx)
-
Expand source code
def render_debug( self, gsd_registry, style: DrawingContext, last_item, initial_point, debug_stream, pfx, _push_stack=True, ): return self.render( gsd_registry, style, last_item, initial_point, debug_stream, pfx, _push_stack=_push_stack, )
class GraphicsStateDictRegistry (*args, **kwargs)
-
A container providing deduplication of graphics state dictionaries across a PDF.
Expand source code
class GraphicsStateDictRegistry(OrderedDict): """ A container providing deduplication of graphics state dictionaries across a PDF. """ def register_style(self, style: "GraphicsStyle"): sdict = style.serialize() # empty style does not need a dictionary if not sdict: return None try: return self[sdict] except KeyError: pass name = Name(f"GS{len(self)}") self[sdict] = name return name
Ancestors
- collections.OrderedDict
- builtins.dict
Methods
def register_style(self, style: GraphicsStyle)
-
Expand source code
def register_style(self, style: "GraphicsStyle"): sdict = style.serialize() # empty style does not need a dictionary if not sdict: return None try: return self[sdict] except KeyError: pass name = Name(f"GS{len(self)}") self[sdict] = name return name
class GraphicsStyle
-
A class representing various style attributes that determine drawing appearance.
This class uses the convention that the global Python singleton ellipsis (
…
) is exclusively used to represent values that are inherited from the parent style. This is to disambiguate the value None which is used for several values to signal an explicitly disabled style. An example of this is the fill/stroke color styles, which use None as hints to the auto paint style detection code.Expand source code
class GraphicsStyle: """ A class representing various style attributes that determine drawing appearance. This class uses the convention that the global Python singleton ellipsis (`...`) is exclusively used to represent values that are inherited from the parent style. This is to disambiguate the value None which is used for several values to signal an explicitly disabled style. An example of this is the fill/stroke color styles, which use None as hints to the auto paint style detection code. """ INHERIT = ... """Singleton specifying a style parameter should be inherited from the parent context.""" # order is be important here because some of these properties are entangled, e.g. # fill_color and fill_opacity MERGE_PROPERTIES = ( "paint_rule", "allow_transparency", "auto_close", "intersection_rule", "fill_color", "fill_opacity", "stroke_color", "stroke_opacity", "blend_mode", "stroke_width", "stroke_cap_style", "stroke_join_style", "stroke_miter_limit", "stroke_dash_pattern", "stroke_dash_phase", ) """An ordered collection of properties to use when merging two GraphicsStyles.""" TRANSPARENCY_KEYS = ( PDFStyleKeys.FILL_ALPHA.value, PDFStyleKeys.STROKE_ALPHA.value, PDFStyleKeys.BLEND_MODE.value, ) """An ordered collection of attributes not to emit in no transparency mode.""" PDF_STYLE_KEYS = ( *(k.value for k in PDFStyleKeys if k is not PDFStyleKeys.STROKE_DASH_PATTERN), ) """An ordered collection of keys to directly emit when serializing the style.""" _PAINT_RULE_LOOKUP = { frozenset({}): PathPaintRule.DONT_PAINT, frozenset({"stroke"}): PathPaintRule.STROKE, frozenset({"fill", IntersectionRule.NONZERO}): PathPaintRule.FILL_NONZERO, frozenset({"fill", IntersectionRule.EVENODD}): PathPaintRule.FILL_EVENODD, frozenset( {"stroke", "fill", IntersectionRule.NONZERO} ): PathPaintRule.STROKE_FILL_NONZERO, frozenset( {"stroke", "fill", IntersectionRule.EVENODD} ): PathPaintRule.STROKE_FILL_EVENODD, } """A dictionary for resolving `PathPaintRule.AUTO`""" @classmethod def merge(cls, parent, child): """ Merge parent and child into a single GraphicsStyle. The result contains the properties of the parent as overridden by any properties explicitly set on the child. If both the parent and the child specify to inherit a given property, that property will preserve the inherit value. """ new = cls() for prop in cls.MERGE_PROPERTIES: cval = getattr(child, prop) if cval is cls.INHERIT: setattr(new, prop, getattr(parent, prop)) else: setattr(new, prop, cval) return new def __init__(self): self.allow_transparency = self.INHERIT self.paint_rule = self.INHERIT self.auto_close = self.INHERIT self.intersection_rule = self.INHERIT self.fill_color = self.INHERIT self.fill_opacity = self.INHERIT self.stroke_color = self.INHERIT self.stroke_opacity = self.INHERIT self.blend_mode = self.INHERIT self.stroke_width = self.INHERIT self.stroke_cap_style = self.INHERIT self.stroke_join_style = self.INHERIT self.stroke_miter_limit = self.INHERIT self.stroke_dash_pattern = self.INHERIT self.stroke_dash_phase = self.INHERIT def __deepcopy__(self, memo): copied = self.__class__() for prop in self.MERGE_PROPERTIES: setattr(copied, prop, getattr(self, prop)) return copied def __setattr__(self, name, value): if not hasattr(self.__class__, name): raise AttributeError( f'{self.__class__} does not have style "{name}" (a typo?)' ) super().__setattr__(name, value) # at some point it probably makes sense to turn this into a general compliance # property, but for now this is the simple approach. @property def allow_transparency(self): return self._allow_transparency # pylint: disable=no-member @allow_transparency.setter def allow_transparency(self, new): return super().__setattr__("_allow_transparency", new) # If these are used in a nested graphics context inside of a painting path # operation, they are no-ops. However, they can be used for outer GraphicsContexts # that painting paths inherit from. @property def paint_rule(self): """The paint rule to use for this path/group.""" return self._paint_rule # pylint: disable=no-member @paint_rule.setter def paint_rule(self, new): if new is None: super().__setattr__("_paint_rule", PathPaintRule.DONT_PAINT) elif new is self.INHERIT: super().__setattr__("_paint_rule", new) else: super().__setattr__("_paint_rule", PathPaintRule.coerce(new)) @property def auto_close(self): """If True, unclosed paths will be automatically closed before stroking.""" return self._auto_close # pylint: disable=no-member @auto_close.setter def auto_close(self, new): if new not in {True, False, self.INHERIT}: raise TypeError(f"auto_close must be a bool or self.INHERIT, not {new}") super().__setattr__("_auto_close", new) @property def intersection_rule(self): """The desired intersection rule for this path/group.""" return self._intersection_rule # pylint: disable=no-member @intersection_rule.setter def intersection_rule(self, new): # don't allow None for this one. if new is self.INHERIT: super().__setattr__("_intersection_rule", new) else: super().__setattr__("_intersection_rule", IntersectionRule.coerce(new)) @property def fill_color(self): """ The desired fill color for this path/group. When setting this property, if the color specifies an opacity value, that will be used to set the fill_opacity property as well. """ return self._fill_color # pylint: disable=no-member @fill_color.setter def fill_color(self, color): if isinstance(color, str): color = color_from_hex_string(color) if isinstance(color, (DeviceRGB, DeviceGray, DeviceCMYK)): super().__setattr__("_fill_color", color) if color.a is not None: self.fill_opacity = color.a elif (color is None) or (color is self.INHERIT): super().__setattr__("_fill_color", color) else: raise TypeError(f"{color} doesn't look like a drawing color") @property def fill_opacity(self): """The desired fill opacity for this path/group.""" return getattr(self, PDFStyleKeys.FILL_ALPHA.value) @fill_opacity.setter def fill_opacity(self, new): if new not in {None, self.INHERIT}: _check_range(new) super().__setattr__(PDFStyleKeys.FILL_ALPHA.value, new) @property def stroke_color(self): """ The desired stroke color for this path/group. When setting this property, if the color specifies an opacity value, that will be used to set the fill_opacity property as well. """ return self._stroke_color # pylint: disable=no-member @stroke_color.setter def stroke_color(self, color): if isinstance(color, str): color = color_from_hex_string(color) if isinstance(color, (DeviceRGB, DeviceGray, DeviceCMYK)): super().__setattr__("_stroke_color", color) if color.a is not None: self.stroke_opacity = color.a if self.stroke_width is self.INHERIT: self.stroke_width = 1 elif (color is None) or (color is self.INHERIT): super().__setattr__("_stroke_color", color) else: raise TypeError(f"{color} doesn't look like a drawing color") @property def stroke_opacity(self): """The desired stroke opacity for this path/group.""" return getattr(self, PDFStyleKeys.STROKE_ALPHA.value) @stroke_opacity.setter def stroke_opacity(self, new): if new not in {None, self.INHERIT}: _check_range(new) super().__setattr__(PDFStyleKeys.STROKE_ALPHA.value, new) @property def blend_mode(self): """The desired blend mode for this path/group.""" return getattr(self, PDFStyleKeys.BLEND_MODE.value) @blend_mode.setter def blend_mode(self, value): if value is self.INHERIT: super().__setattr__(PDFStyleKeys.BLEND_MODE.value, value) else: super().__setattr__( PDFStyleKeys.BLEND_MODE.value, BlendMode.coerce(value).value ) @property def stroke_width(self): """The desired stroke width for this path/group.""" return getattr(self, PDFStyleKeys.STROKE_WIDTH.value) @stroke_width.setter def stroke_width(self, width): if not isinstance( width, (int, float, decimal.Decimal, type(None), type(self.INHERIT)), ): raise TypeError(f"stroke_width must be a number, not {type(width)}") super().__setattr__(PDFStyleKeys.STROKE_WIDTH.value, width) @property def stroke_cap_style(self): """The desired stroke cap style for this path/group.""" return getattr(self, PDFStyleKeys.STROKE_CAP_STYLE.value) @stroke_cap_style.setter def stroke_cap_style(self, value): if value is self.INHERIT: super().__setattr__(PDFStyleKeys.STROKE_CAP_STYLE.value, value) else: super().__setattr__( PDFStyleKeys.STROKE_CAP_STYLE.value, StrokeCapStyle.coerce(value) ) @property def stroke_join_style(self): """The desired stroke join style for this path/group.""" return getattr(self, PDFStyleKeys.STROKE_JOIN_STYLE.value) @stroke_join_style.setter def stroke_join_style(self, value): if value is self.INHERIT: super().__setattr__(PDFStyleKeys.STROKE_JOIN_STYLE.value, value) else: super().__setattr__( PDFStyleKeys.STROKE_JOIN_STYLE.value, StrokeJoinStyle.coerce(value), ) @property def stroke_miter_limit(self): """The desired stroke miter limit for this path/group.""" return getattr(self, PDFStyleKeys.STROKE_MITER_LIMIT.value) @stroke_miter_limit.setter def stroke_miter_limit(self, value): if (value is self.INHERIT) or isinstance(value, NumberClass): super().__setattr__(PDFStyleKeys.STROKE_MITER_LIMIT.value, value) else: raise TypeError(f"{value} is not a number") @property def stroke_dash_pattern(self): """The desired stroke dash pattern for this path/group.""" return self._stroke_dash_pattern # pylint: disable=no-member @stroke_dash_pattern.setter def stroke_dash_pattern(self, value): if value is None: result = () elif value is self.INHERIT: result = value elif isinstance(value, NumberClass): result = (value,) else: try: accum = [] for item in value: if not isinstance(item, NumberClass): raise TypeError( f"stroke_dash_pattern {value} sequence has non-numeric value" ) accum.append(item) except TypeError: raise TypeError( f"stroke_dash_pattern {value} must be a number or sequence of numbers" ) from None result = (*accum,) super().__setattr__("_stroke_dash_pattern", result) @property def stroke_dash_phase(self): """The desired stroke dash pattern phase offset for this path/group.""" return self._stroke_dash_phase # pylint: disable=no-member @stroke_dash_phase.setter def stroke_dash_phase(self, value): if value is self.INHERIT or isinstance(value, NumberClass): return super().__setattr__("_stroke_dash_phase", value) raise TypeError(f"{value} isn't a number or GraphicsStyle.INHERIT") def serialize(self): """ Convert this style object to a PDF dictionary with appropriate style keys. Only explicitly specified values are emitted. """ result = OrderedDict() for key in self.PDF_STYLE_KEYS: value = getattr(self, key, self.INHERIT) if (value is not self.INHERIT) and (value is not None): # None is used for out-of-band signaling on these, e.g. a stroke_width # of None doesn't need to land here because it signals the # PathPaintRule auto resolution only. result[key] = value # There is additional logic in GraphicsContext to ensure that this will work if (self.stroke_dash_pattern is not self.INHERIT) and ( self.stroke_dash_pattern is not None ): result[PDFStyleKeys.STROKE_DASH_PATTERN.value] = [ self.stroke_dash_pattern, self.stroke_dash_phase, ] if self.allow_transparency is False: for key in self.TRANSPARENCY_KEYS: if key in result: del result[key] if result: # Only insert this key if there is at least one other item in the result so # that we don't junk up the output PDF with empty ExtGState dictionaries. type_name = Name("Type") result[type_name] = Name("ExtGState") result.move_to_end(type_name, last=False) return render_pdf_primitive(result) # this signals to the GraphicsStateDictRegistry that there is nothing to # register. This is a success case. return None @force_nodocument def resolve_paint_rule(self): """ Resolve `PathPaintRule.AUTO` to a real paint rule based on this style. Returns: the resolved `PathPaintRule`. """ if self.paint_rule is PathPaintRule.AUTO: want = set() if self.stroke_width is not None and self.stroke_color is not None: want.add("stroke") if self.fill_color is not None: want.add("fill") # we need to guarantee that this will not be None. The default will # be "nonzero". assert self.intersection_rule is not None want.add(self.intersection_rule) try: rule = self._PAINT_RULE_LOOKUP[frozenset(want)] except KeyError: # don't default to DONT_PAINT because that's almost certainly not a very # good default. rule = PathPaintRule.STROKE_FILL_NONZERO elif self.paint_rule is self.INHERIT: # this shouldn't happen under normal usage, but certain API (ab)use can end # up in this state. We can't resolve anything meaningful, so fall back to a # sane(?) default. rule = PathPaintRule.STROKE_FILL_NONZERO else: rule = self.paint_rule return rule
Class variables
var INHERIT
-
Singleton specifying a style parameter should be inherited from the parent context.
var MERGE_PROPERTIES
-
An ordered collection of properties to use when merging two GraphicsStyles.
var PDF_STYLE_KEYS
-
An ordered collection of keys to directly emit when serializing the style.
var TRANSPARENCY_KEYS
-
An ordered collection of attributes not to emit in no transparency mode.
Static methods
def merge(parent, child)
-
Merge parent and child into a single GraphicsStyle.
The result contains the properties of the parent as overridden by any properties explicitly set on the child. If both the parent and the child specify to inherit a given property, that property will preserve the inherit value.
Expand source code
@classmethod def merge(cls, parent, child): """ Merge parent and child into a single GraphicsStyle. The result contains the properties of the parent as overridden by any properties explicitly set on the child. If both the parent and the child specify to inherit a given property, that property will preserve the inherit value. """ new = cls() for prop in cls.MERGE_PROPERTIES: cval = getattr(child, prop) if cval is cls.INHERIT: setattr(new, prop, getattr(parent, prop)) else: setattr(new, prop, cval) return new
Instance variables
var allow_transparency
-
Expand source code
@property def allow_transparency(self): return self._allow_transparency # pylint: disable=no-member
var auto_close
-
If True, unclosed paths will be automatically closed before stroking.
Expand source code
@property def auto_close(self): """If True, unclosed paths will be automatically closed before stroking.""" return self._auto_close # pylint: disable=no-member
var blend_mode
-
The desired blend mode for this path/group.
Expand source code
@property def blend_mode(self): """The desired blend mode for this path/group.""" return getattr(self, PDFStyleKeys.BLEND_MODE.value)
var fill_color
-
The desired fill color for this path/group.
When setting this property, if the color specifies an opacity value, that will be used to set the fill_opacity property as well.
Expand source code
@property def fill_color(self): """ The desired fill color for this path/group. When setting this property, if the color specifies an opacity value, that will be used to set the fill_opacity property as well. """ return self._fill_color # pylint: disable=no-member
var fill_opacity
-
The desired fill opacity for this path/group.
Expand source code
@property def fill_opacity(self): """The desired fill opacity for this path/group.""" return getattr(self, PDFStyleKeys.FILL_ALPHA.value)
var intersection_rule
-
The desired intersection rule for this path/group.
Expand source code
@property def intersection_rule(self): """The desired intersection rule for this path/group.""" return self._intersection_rule # pylint: disable=no-member
var paint_rule
-
The paint rule to use for this path/group.
Expand source code
@property def paint_rule(self): """The paint rule to use for this path/group.""" return self._paint_rule # pylint: disable=no-member
var stroke_cap_style
-
The desired stroke cap style for this path/group.
Expand source code
@property def stroke_cap_style(self): """The desired stroke cap style for this path/group.""" return getattr(self, PDFStyleKeys.STROKE_CAP_STYLE.value)
var stroke_color
-
The desired stroke color for this path/group.
When setting this property, if the color specifies an opacity value, that will be used to set the fill_opacity property as well.
Expand source code
@property def stroke_color(self): """ The desired stroke color for this path/group. When setting this property, if the color specifies an opacity value, that will be used to set the fill_opacity property as well. """ return self._stroke_color # pylint: disable=no-member
var stroke_dash_pattern
-
The desired stroke dash pattern for this path/group.
Expand source code
@property def stroke_dash_pattern(self): """The desired stroke dash pattern for this path/group.""" return self._stroke_dash_pattern # pylint: disable=no-member
var stroke_dash_phase
-
The desired stroke dash pattern phase offset for this path/group.
Expand source code
@property def stroke_dash_phase(self): """The desired stroke dash pattern phase offset for this path/group.""" return self._stroke_dash_phase # pylint: disable=no-member
var stroke_join_style
-
The desired stroke join style for this path/group.
Expand source code
@property def stroke_join_style(self): """The desired stroke join style for this path/group.""" return getattr(self, PDFStyleKeys.STROKE_JOIN_STYLE.value)
var stroke_miter_limit
-
The desired stroke miter limit for this path/group.
Expand source code
@property def stroke_miter_limit(self): """The desired stroke miter limit for this path/group.""" return getattr(self, PDFStyleKeys.STROKE_MITER_LIMIT.value)
var stroke_opacity
-
The desired stroke opacity for this path/group.
Expand source code
@property def stroke_opacity(self): """The desired stroke opacity for this path/group.""" return getattr(self, PDFStyleKeys.STROKE_ALPHA.value)
var stroke_width
-
The desired stroke width for this path/group.
Expand source code
@property def stroke_width(self): """The desired stroke width for this path/group.""" return getattr(self, PDFStyleKeys.STROKE_WIDTH.value)
Methods
def serialize(self)
-
Convert this style object to a PDF dictionary with appropriate style keys.
Only explicitly specified values are emitted.
Expand source code
def serialize(self): """ Convert this style object to a PDF dictionary with appropriate style keys. Only explicitly specified values are emitted. """ result = OrderedDict() for key in self.PDF_STYLE_KEYS: value = getattr(self, key, self.INHERIT) if (value is not self.INHERIT) and (value is not None): # None is used for out-of-band signaling on these, e.g. a stroke_width # of None doesn't need to land here because it signals the # PathPaintRule auto resolution only. result[key] = value # There is additional logic in GraphicsContext to ensure that this will work if (self.stroke_dash_pattern is not self.INHERIT) and ( self.stroke_dash_pattern is not None ): result[PDFStyleKeys.STROKE_DASH_PATTERN.value] = [ self.stroke_dash_pattern, self.stroke_dash_phase, ] if self.allow_transparency is False: for key in self.TRANSPARENCY_KEYS: if key in result: del result[key] if result: # Only insert this key if there is at least one other item in the result so # that we don't junk up the output PDF with empty ExtGState dictionaries. type_name = Name("Type") result[type_name] = Name("ExtGState") result.move_to_end(type_name, last=False) return render_pdf_primitive(result) # this signals to the GraphicsStateDictRegistry that there is nothing to # register. This is a success case. return None
class HorizontalLine (x: Union[int, float, decimal.Decimal])
-
A path line element that takes its ordinate from the end of the previous element.
Expand source code
class HorizontalLine(NamedTuple): """ A path line element that takes its ordinate from the end of the previous element. See: `PaintedPath.horizontal_line_to` """ x: Number """The abscissa of the horizontal line's end point.""" @force_nodocument def render(self, gsd_registry, style, last_item, initial_point): """ Render this path element to its PDF representation. Args: gsd_registry (GraphicsStateDictRegistry): the owner's graphics state dictionary registry. style (GraphicsStyle): the current resolved graphics style last_item: the previous path element. initial_point: last position set by a "M" or "m" command Returns: a tuple of `(str, new_last_item)`, where `new_last_item` is the resolved `Line`. """ # pylint: disable=unused-argument end_point = Point(x=self.x, y=last_item.end_point.y) return _render_line(end_point), Line(end_point), initial_point @force_nodocument def render_debug( self, gsd_registry, style, last_item, initial_point, debug_stream, pfx ): """ Render this path element to its PDF representation and produce debug information. Args: gsd_registry (GraphicsStateDictRegistry): the owner's graphics state dictionary registry. style (GraphicsStyle): the current resolved graphics style last_item: the previous path element. initial_point: last position set by a "M" or "m" command debug_stream (io.TextIO): the stream to which the debug output should be written. This is not guaranteed to be seekable (e.g. it may be stdout or stderr). pfx (str): the current debug output prefix string (only needed if emitting more than one line). Returns: The same tuple as `HorizontalLine.render`. """ # pylint: disable=unused-argument rendered, resolved, initial_point = self.render( gsd_registry, style, last_item, initial_point ) debug_stream.write(f"{self} resolved to {resolved}\n") return rendered, resolved, initial_point
Ancestors
- builtins.tuple
Instance variables
var x : Union[int, float, decimal.Decimal]
-
The abscissa of the horizontal line's end point.
class ImplicitClose
-
A path close element that is conditionally rendered depending on the value of
GraphicsStyle.auto_close
.Expand source code
class ImplicitClose(NamedTuple): """ A path close element that is conditionally rendered depending on the value of `GraphicsStyle.auto_close`. """ @force_nodocument def render(self, gsd_registry, style, last_item, initial_point): """ Render this path element to its PDF representation. Args: gsd_registry (GraphicsStateDictRegistry): the owner's graphics state dictionary registry. style (GraphicsStyle): the current resolved graphics style last_item: the previous path element. initial_point: last position set by a "M" or "m" command Returns: a tuple of `(str, new_last_item)`, where `new_last_item` is whatever the old last_item was. """ # pylint: disable=unused-argument if style.auto_close: return "h", last_item, initial_point return "", last_item, initial_point @force_nodocument def render_debug( self, gsd_registry, style, last_item, initial_point, debug_stream, pfx ): """ Render this path element to its PDF representation and produce debug information. Args: gsd_registry (GraphicsStateDictRegistry): the owner's graphics state dictionary registry. style (GraphicsStyle): the current resolved graphics style last_item: the previous path element. initial_point: last position set by a "M" or "m" command debug_stream (io.TextIO): the stream to which the debug output should be written. This is not guaranteed to be seekable (e.g. it may be stdout or stderr). pfx (str): the current debug output prefix string (only needed if emitting more than one line). Returns: The same tuple as `ImplicitClose.render`. """ # pylint: disable=unused-argument rendered, resolved, initial_point = self.render( gsd_registry, style, last_item, initial_point ) debug_stream.write(f"{self} resolved to {rendered}\n") return rendered, resolved, initial_point
Ancestors
- builtins.tuple
class Line (pt: Point)
-
A path line element.
This draws a straight line from the end point of the previous path element to the point specified by
pt
.Expand source code
class Line(NamedTuple): """ A path line element. This draws a straight line from the end point of the previous path element to the point specified by `pt`. See: `PaintedPath.line_to` """ pt: Point """The point to which the line is drawn.""" @property def end_point(self): """The end point of this path element.""" return self.pt @force_nodocument def render(self, gsd_registry, style, last_item, initial_point): """ Render this path element to its PDF representation. Args: gsd_registry (GraphicsStateDictRegistry): the owner's graphics state dictionary registry. style (GraphicsStyle): the current resolved graphics style last_item: the previous path element. initial_point: last position set by a "M" or "m" command Returns: a tuple of `(str, new_last_item)`, where `new_last_item` is `self` """ # pylint: disable=unused-argument return _render_line(self.pt), self, initial_point @force_nodocument def render_debug( self, gsd_registry, style, last_item, initial_point, debug_stream, pfx ): """ Render this path element to its PDF representation and produce debug information. Args: gsd_registry (GraphicsStateDictRegistry): the owner's graphics state dictionary registry. style (GraphicsStyle): the current resolved graphics style last_item: the previous path element. initial_point: last position set by a "M" or "m" command debug_stream (io.TextIO): the stream to which the debug output should be written. This is not guaranteed to be seekable (e.g. it may be stdout or stderr). pfx (str): the current debug output prefix string (only needed if emitting more than one line). Returns: The same tuple as `Line.render`. """ # pylint: disable=unused-argument rendered, resolved, initial_point = self.render( gsd_registry, style, last_item, initial_point ) debug_stream.write(str(self) + "\n") return rendered, resolved, initial_point
Ancestors
- builtins.tuple
Instance variables
var end_point
-
The end point of this path element.
Expand source code
@property def end_point(self): """The end point of this path element.""" return self.pt
var pt : Point
-
The point to which the line is drawn.
class Move (pt: Point)
-
A path move element.
If a path has been created but not yet painted, this will create a new subpath.
Expand source code
class Move(NamedTuple): """ A path move element. If a path has been created but not yet painted, this will create a new subpath. See: `PaintedPath.move_to` """ pt: Point """The point to which to move.""" @property def end_point(self): """The end point of this path element.""" return self.pt @force_nodocument def render(self, gsd_registry, style, last_item, initial_point): """ Render this path element to its PDF representation. Args: gsd_registry (GraphicsStateDictRegistry): the owner's graphics state dictionary registry. style (GraphicsStyle): the current resolved graphics style last_item: the previous path element. initial_point: last position set by a "M" or "m" command Returns: a tuple of `(str, new_last_item)`, where `new_last_item` is `self` """ # pylint: disable=unused-argument return _render_move(self.pt), self, self.pt @force_nodocument def render_debug( self, gsd_registry, style, last_item, initial_point, debug_stream, pfx ): """ Render this path element to its PDF representation and produce debug information. Args: gsd_registry (GraphicsStateDictRegistry): the owner's graphics state dictionary registry. style (GraphicsStyle): the current resolved graphics style last_item: the previous path element. initial_point: last position set by a "M" or "m" command debug_stream (io.TextIO): the stream to which the debug output should be written. This is not guaranteed to be seekable (e.g. it may be stdout or stderr). pfx (str): the current debug output prefix string (only needed if emitting more than one line). Returns: The same tuple as `Move.render`. """ # pylint: disable=unused-argument rendered, resolved, initial_point = self.render( gsd_registry, style, last_item, initial_point ) debug_stream.write(str(self) + "\n") return rendered, resolved, initial_point
Ancestors
- builtins.tuple
Instance variables
var end_point
-
The end point of this path element.
Expand source code
@property def end_point(self): """The end point of this path element.""" return self.pt
var pt : Point
-
The point to which to move.
class PaintedPath (x=0, y=0)
-
A path to be drawn by the PDF renderer.
A painted path is defined by a style and an arbitrary sequence of path elements, which include the primitive path elements (
Move
,Line
,BezierCurve
, …) as well as arbitrarily nestedGraphicsContext
containing their own sequence of primitive path elements andGraphicsContext
.Expand source code
class PaintedPath: """ A path to be drawn by the PDF renderer. A painted path is defined by a style and an arbitrary sequence of path elements, which include the primitive path elements (`Move`, `Line`, `BezierCurve`, ...) as well as arbitrarily nested `GraphicsContext` containing their own sequence of primitive path elements and `GraphicsContext`. """ def __init__(self, x=0, y=0): self._root_graphics_context = GraphicsContext() self._graphics_context = self._root_graphics_context self._closed = True self._close_context = self._graphics_context self._starter_move = Move(Point(x, y)) def __deepcopy__(self, memo): # there's no real way to recover the matching current _graphics_context after # copying the root context, but that's ok because we can just disallow copying # of paths under modification as that is almost certainly wrong usage. if self._graphics_context is not self._root_graphics_context: raise RuntimeError(f"cannot copy path {self} while it is being modified") copied = self.__class__() copied._root_graphics_context = copy.deepcopy(self._root_graphics_context, memo) copied._graphics_context = copied._root_graphics_context copied._closed = self._closed copied._close_context = copied._graphics_context return copied @property def style(self): """The `GraphicsStyle` applied to all elements of this path.""" return self._root_graphics_context.style @property def transform(self): """The `Transform` that applies to all of the elements of this path.""" return self._root_graphics_context.transform @transform.setter def transform(self, tf): self._root_graphics_context.transform = tf @property def auto_close(self): """If true, the path should automatically close itself before painting.""" return self.style.auto_close @auto_close.setter def auto_close(self, should): self.style.auto_close = should @property def paint_rule(self): """Manually specify the `PathPaintRule` to use for rendering the path.""" return self.style.paint_rule @paint_rule.setter def paint_rule(self, style): self.style.paint_rule = style @property def clipping_path(self): """Set the clipping path for this path.""" return self._root_graphics_context.clipping_path @clipping_path.setter def clipping_path(self, new_clipath): self._root_graphics_context.clipping_path = new_clipath @contextmanager def _new_graphics_context(self, _attach=True): old_graphics_context = self._graphics_context new_graphics_context = GraphicsContext() self._graphics_context = new_graphics_context try: yield new_graphics_context if _attach: old_graphics_context.add_item(new_graphics_context) finally: self._graphics_context = old_graphics_context @contextmanager def transform_group(self, transform): """ Apply the provided `Transform` to all points added within this context. """ with self._new_graphics_context() as ctxt: ctxt.transform = transform yield self def add_path_element(self, item, _copy=True): """ Add the given element as a path item of this path. Args: item: the item to add to this path. _copy (bool): if true (the default), the item will be copied before being appended. This prevents modifications to a referenced object from "retroactively" altering its style/shape and should be disabled with caution. """ if self._starter_move is not None: self._closed = False self._graphics_context.add_item(self._starter_move, _copy=False) self._close_context = self._graphics_context self._starter_move = None self._graphics_context.add_item(item, _copy=_copy) def remove_last_path_element(self): self._graphics_context.remove_last_item() def rectangle(self, x, y, w, h, rx=0, ry=0): """ Append a rectangle as a closed subpath to the current path. If the width or the height are 0, the rectangle will be collapsed to a line (unless they're both 0, in which case it's collapsed to nothing). Args: x (Number): the abscissa of the starting corner of the rectangle. y (Number): the ordinate of the starting corner of the rectangle. w (Number): the width of the rectangle (if 0, the rectangle will be rendered as a vertical line). h (Number): the height of the rectangle (if 0, the rectangle will be rendered as a horizontal line). rx (Number): the x-radius of the rectangle rounded corner (if 0 the corners will not be rounded). ry (Number): the y-radius of the rectangle rounded corner (if 0 the corners will not be rounded). Returns: The path, to allow chaining method calls. """ self._insert_implicit_close_if_open() self.add_path_element( RoundedRectangle(Point(x, y), Point(w, h), Point(rx, ry)), _copy=False ) self._closed = True self.move_to(x, y) return self def circle(self, cx, cy, r): """ Append a circle as a closed subpath to the current path. Args: cx (Number): the abscissa of the circle's center point. cy (Number): the ordinate of the circle's center point. r (Number): the radius of the circle. Returns: The path, to allow chaining method calls. """ return self.ellipse(cx, cy, r, r) def ellipse(self, cx, cy, rx, ry): """ Append an ellipse as a closed subpath to the current path. Args: cx (Number): the abscissa of the ellipse's center point. cy (Number): the ordinate of the ellipse's center point. rx (Number): the x-radius of the ellipse. ry (Number): the y-radius of the ellipse. Returns: The path, to allow chaining method calls. """ self._insert_implicit_close_if_open() self.add_path_element(Ellipse(Point(rx, ry), Point(cx, cy)), _copy=False) self._closed = True self.move_to(cx, cy) return self def move_to(self, x, y): """ Start a new subpath or move the path starting point. If no path elements have been added yet, this will change the path starting point. If path elements have been added, this will insert an implicit close in order to start a new subpath. Args: x (Number): abscissa of the (sub)path starting point. y (Number): ordinate of the (sub)path starting point. Returns: The path, to allow chaining method calls. """ self._insert_implicit_close_if_open() self._starter_move = Move(Point(x, y)) return self def move_relative(self, x, y): """ Start a new subpath or move the path start point relative to the previous point. If no path elements have been added yet, this will change the path starting point. If path elements have been added, this will insert an implicit close in order to start a new subpath. This will overwrite an absolute move_to as long as no non-move path items have been appended. The relative position is resolved from the previous item when the path is being rendered, or from 0, 0 if it is the first item. Args: x (Number): abscissa of the (sub)path starting point relative to the. y (Number): ordinate of the (sub)path starting point relative to the. """ self._insert_implicit_close_if_open() if self._starter_move is not None: self._closed = False self._graphics_context.add_item(self._starter_move, _copy=False) self._close_context = self._graphics_context self._starter_move = RelativeMove(Point(x, y)) return self def line_to(self, x, y): """ Append a straight line to this path. Args: x (Number): abscissa the line's end point. y (Number): ordinate of the line's end point. Returns: The path, to allow chaining method calls. """ self.add_path_element(Line(Point(x, y)), _copy=False) return self def line_relative(self, dx, dy): """ Append a straight line whose end is computed as an offset from the end of the previous path element. Args: x (Number): abscissa the line's end point relative to the end point of the previous path element. y (Number): ordinate of the line's end point relative to the end point of the previous path element. Returns: The path, to allow chaining method calls. """ self.add_path_element(RelativeLine(Point(dx, dy)), _copy=False) return self def horizontal_line_to(self, x): """ Append a straight horizontal line to the given abscissa. The ordinate is retrieved from the end point of the previous path element. Args: x (Number): abscissa of the line's end point. Returns: The path, to allow chaining method calls. """ self.add_path_element(HorizontalLine(x), _copy=False) return self def horizontal_line_relative(self, dx): """ Append a straight horizontal line to the given offset from the previous path element. The ordinate is retrieved from the end point of the previous path element. Args: x (Number): abscissa of the line's end point relative to the end point of the previous path element. Returns: The path, to allow chaining method calls. """ self.add_path_element(RelativeHorizontalLine(dx), _copy=False) return self def vertical_line_to(self, y): """ Append a straight vertical line to the given ordinate. The abscissa is retrieved from the end point of the previous path element. Args: y (Number): ordinate of the line's end point. Returns: The path, to allow chaining method calls. """ self.add_path_element(VerticalLine(y), _copy=False) return self def vertical_line_relative(self, dy): """ Append a straight vertical line to the given offset from the previous path element. The abscissa is retrieved from the end point of the previous path element. Args: y (Number): ordinate of the line's end point relative to the end point of the previous path element. Returns: The path, to allow chaining method calls. """ self.add_path_element(RelativeVerticalLine(dy), _copy=False) return self def curve_to(self, x1, y1, x2, y2, x3, y3): """ Append a cubic Bézier curve to this path. Args: x1 (Number): abscissa of the first control point y1 (Number): ordinate of the first control point x2 (Number): abscissa of the second control point y2 (Number): ordinate of the second control point x3 (Number): abscissa of the end point y3 (Number): ordinate of the end point Returns: The path, to allow chaining method calls. """ ctrl1 = Point(x1, y1) ctrl2 = Point(x2, y2) end = Point(x3, y3) self.add_path_element(BezierCurve(ctrl1, ctrl2, end), _copy=False) return self def curve_relative(self, dx1, dy1, dx2, dy2, dx3, dy3): """ Append a cubic Bézier curve whose points are expressed relative to the end point of the previous path element. E.g. with a start point of (0, 0), given (1, 1), (2, 2), (3, 3), the output curve would have the points: (0, 0) c1 (1, 1) c2 (3, 3) e (6, 6) Args: dx1 (Number): abscissa of the first control point relative to the end point of the previous path element dy1 (Number): ordinate of the first control point relative to the end point of the previous path element dx2 (Number): abscissa offset of the second control point relative to the end point of the previous path element dy2 (Number): ordinate offset of the second control point relative to the end point of the previous path element dx3 (Number): abscissa offset of the end point relative to the end point of the previous path element dy3 (Number): ordinate offset of the end point relative to the end point of the previous path element Returns: The path, to allow chaining method calls. """ c1d = Point(dx1, dy1) c2d = Point(dx2, dy2) end = Point(dx3, dy3) self.add_path_element(RelativeBezierCurve(c1d, c2d, end), _copy=False) return self def quadratic_curve_to(self, x1, y1, x2, y2): """ Append a cubic Bézier curve mimicking the specified quadratic Bézier curve. Args: x1 (Number): abscissa of the control point y1 (Number): ordinate of the control point x2 (Number): abscissa of the end point y2 (Number): ordinate of the end point Returns: The path, to allow chaining method calls. """ ctrl = Point(x1, y1) end = Point(x2, y2) self.add_path_element(QuadraticBezierCurve(ctrl, end), _copy=False) return self def quadratic_curve_relative(self, dx1, dy1, dx2, dy2): """ Append a cubic Bézier curve mimicking the specified quadratic Bézier curve. Args: dx1 (Number): abscissa of the control point relative to the end point of the previous path element dy1 (Number): ordinate of the control point relative to the end point of the previous path element dx2 (Number): abscissa offset of the end point relative to the end point of the previous path element dy2 (Number): ordinate offset of the end point relative to the end point of the previous path element Returns: The path, to allow chaining method calls. """ ctrl = Point(dx1, dy1) end = Point(dx2, dy2) self.add_path_element(RelativeQuadraticBezierCurve(ctrl, end), _copy=False) return self def arc_to(self, rx, ry, rotation, large_arc, positive_sweep, x, y): """ Append an elliptical arc from the end of the previous path point to the specified end point. The arc is approximated using Bézier curves, so it is not perfectly accurate. However, the error is small enough to not be noticeable at any reasonable (and even most unreasonable) scales, with a worst-case deviation of around 3‱. Notes: - The signs of the radii arguments (`rx` and `ry`) are ignored (i.e. their absolute values are used instead). - If either radius is 0, then a straight line will be emitted instead of an arc. - If the radii are too small for the arc to reach from the current point to the specified end point (`x` and `y`), then they will be proportionally scaled up until they are big enough, which will always result in a half-ellipse arc (i.e. an 180 degree sweep) Args: rx (Number): radius in the x-direction. ry (Number): radius in the y-direction. rotation (Number): angle (in degrees) that the arc should be rotated clockwise from the principle axes. This parameter does not have a visual effect in the case that `rx == ry`. large_arc (bool): if True, the arc will cover a sweep angle of at least 180 degrees. Otherwise, the sweep angle will be at most 180 degrees. positive_sweep (bool): if True, the arc will be swept over a positive angle, i.e. clockwise. Otherwise, the arc will be swept over a negative angle. x (Number): abscissa of the arc's end point. y (Number): ordinate of the arc's end point. """ if rx == 0 or ry == 0: return self.line_to(x, y) radii = Point(abs(rx), abs(ry)) large_arc = bool(large_arc) rotation = math.radians(rotation) positive_sweep = bool(positive_sweep) end = Point(x, y) self.add_path_element( Arc(radii, rotation, large_arc, positive_sweep, end), _copy=False ) return self def arc_relative(self, rx, ry, rotation, large_arc, positive_sweep, dx, dy): """ Append an elliptical arc from the end of the previous path point to an offset point. The arc is approximated using Bézier curves, so it is not perfectly accurate. However, the error is small enough to not be noticeable at any reasonable (and even most unreasonable) scales, with a worst-case deviation of around 3‱. Notes: - The signs of the radii arguments (`rx` and `ry`) are ignored (i.e. their absolute values are used instead). - If either radius is 0, then a straight line will be emitted instead of an arc. - If the radii are too small for the arc to reach from the current point to the specified end point (`x` and `y`), then they will be proportionally scaled up until they are big enough, which will always result in a half-ellipse arc (i.e. an 180 degree sweep) Args: rx (Number): radius in the x-direction. ry (Number): radius in the y-direction. rotation (Number): angle (in degrees) that the arc should be rotated clockwise from the principle axes. This parameter does not have a visual effect in the case that `rx == ry`. large_arc (bool): if True, the arc will cover a sweep angle of at least 180 degrees. Otherwise, the sweep angle will be at most 180 degrees. positive_sweep (bool): if True, the arc will be swept over a positive angle, i.e. clockwise. Otherwise, the arc will be swept over a negative angle. dx (Number): abscissa of the arc's end point relative to the end point of the previous path element. dy (Number): ordinate of the arc's end point relative to the end point of the previous path element. """ if rx == 0 or ry == 0: return self.line_relative(dx, dy) radii = Point(abs(rx), abs(ry)) large_arc = bool(large_arc) rotation = math.radians(rotation) positive_sweep = bool(positive_sweep) end = Point(dx, dy) self.add_path_element( RelativeArc(radii, rotation, large_arc, positive_sweep, end), _copy=False ) return self def close(self): """ Explicitly close the current (sub)path. """ self.add_path_element(Close(), _copy=False) self._closed = True self.move_relative(0, 0) def _insert_implicit_close_if_open(self): if not self._closed: self._close_context.add_item(ImplicitClose(), _copy=False) self._close_context = self._graphics_context self._closed = True def render( self, gsd_registry, style, last_item, initial_point, debug_stream=None, pfx=None ): self._insert_implicit_close_if_open() ( render_list, last_item, initial_point, ) = self._root_graphics_context.build_render_list( gsd_registry, style, last_item, initial_point, debug_stream, pfx ) paint_rule = GraphicsStyle.merge(style, self.style).resolve_paint_rule() render_list.insert(-1, paint_rule.value) return " ".join(render_list), last_item, initial_point def render_debug( self, gsd_registry, style, last_item, initial_point, debug_stream, pfx ): """ Render this path element to its PDF representation and produce debug information. Args: gsd_registry (GraphicsStateDictRegistry): the owner's graphics state dictionary registry. style (GraphicsStyle): the current resolved graphics style last_item: the previous path element. initial_point: last position set by a "M" or "m" command debug_stream (io.TextIO): the stream to which the debug output should be written. This is not guaranteed to be seekable (e.g. it may be stdout or stderr). pfx (str): the current debug output prefix string (only needed if emitting more than one line). Returns: The same tuple as `PaintedPath.render`. """ return self.render( gsd_registry, style, last_item, initial_point, debug_stream, pfx )
Subclasses
Instance variables
var auto_close
-
If true, the path should automatically close itself before painting.
Expand source code
@property def auto_close(self): """If true, the path should automatically close itself before painting.""" return self.style.auto_close
var clipping_path
-
Set the clipping path for this path.
Expand source code
@property def clipping_path(self): """Set the clipping path for this path.""" return self._root_graphics_context.clipping_path
var paint_rule
-
Manually specify the
PathPaintRule
to use for rendering the path.Expand source code
@property def paint_rule(self): """Manually specify the `PathPaintRule` to use for rendering the path.""" return self.style.paint_rule
var style
-
The
GraphicsStyle
applied to all elements of this path.Expand source code
@property def style(self): """The `GraphicsStyle` applied to all elements of this path.""" return self._root_graphics_context.style
var transform
-
The
Transform
that applies to all of the elements of this path.Expand source code
@property def transform(self): """The `Transform` that applies to all of the elements of this path.""" return self._root_graphics_context.transform
Methods
def add_path_element(self, item)
-
Add the given element as a path item of this path.
Args
item
- the item to add to this path.
_copy
:bool
- if true (the default), the item will be copied before being appended. This prevents modifications to a referenced object from "retroactively" altering its style/shape and should be disabled with caution.
Expand source code
def add_path_element(self, item, _copy=True): """ Add the given element as a path item of this path. Args: item: the item to add to this path. _copy (bool): if true (the default), the item will be copied before being appended. This prevents modifications to a referenced object from "retroactively" altering its style/shape and should be disabled with caution. """ if self._starter_move is not None: self._closed = False self._graphics_context.add_item(self._starter_move, _copy=False) self._close_context = self._graphics_context self._starter_move = None self._graphics_context.add_item(item, _copy=_copy)
def arc_relative(self, rx, ry, rotation, large_arc, positive_sweep, dx, dy)
-
Append an elliptical arc from the end of the previous path point to an offset point.
The arc is approximated using Bézier curves, so it is not perfectly accurate. However, the error is small enough to not be noticeable at any reasonable (and even most unreasonable) scales, with a worst-case deviation of around 3‱.
Notes
- The signs of the radii arguments (
rx
andry
) are ignored (i.e. their absolute values are used instead). - If either radius is 0, then a straight line will be emitted instead of an arc.
- If the radii are too small for the arc to reach from the current point to
the specified end point (
x
andy
), then they will be proportionally scaled up until they are big enough, which will always result in a half-ellipse arc (i.e. an 180 degree sweep)
Args
rx
:Number
- radius in the x-direction.
ry
:Number
- radius in the y-direction.
rotation
:Number
- angle (in degrees) that the arc should be rotated
clockwise from the principle axes. This parameter does not have
a visual effect in the case that
rx == ry
. large_arc
:bool
- if True, the arc will cover a sweep angle of at least 180 degrees. Otherwise, the sweep angle will be at most 180 degrees.
positive_sweep
:bool
- if True, the arc will be swept over a positive angle, i.e. clockwise. Otherwise, the arc will be swept over a negative angle.
dx
:Number
- abscissa of the arc's end point relative to the end point of the previous path element.
dy
:Number
- ordinate of the arc's end point relative to the end point of the previous path element.
Expand source code
def arc_relative(self, rx, ry, rotation, large_arc, positive_sweep, dx, dy): """ Append an elliptical arc from the end of the previous path point to an offset point. The arc is approximated using Bézier curves, so it is not perfectly accurate. However, the error is small enough to not be noticeable at any reasonable (and even most unreasonable) scales, with a worst-case deviation of around 3‱. Notes: - The signs of the radii arguments (`rx` and `ry`) are ignored (i.e. their absolute values are used instead). - If either radius is 0, then a straight line will be emitted instead of an arc. - If the radii are too small for the arc to reach from the current point to the specified end point (`x` and `y`), then they will be proportionally scaled up until they are big enough, which will always result in a half-ellipse arc (i.e. an 180 degree sweep) Args: rx (Number): radius in the x-direction. ry (Number): radius in the y-direction. rotation (Number): angle (in degrees) that the arc should be rotated clockwise from the principle axes. This parameter does not have a visual effect in the case that `rx == ry`. large_arc (bool): if True, the arc will cover a sweep angle of at least 180 degrees. Otherwise, the sweep angle will be at most 180 degrees. positive_sweep (bool): if True, the arc will be swept over a positive angle, i.e. clockwise. Otherwise, the arc will be swept over a negative angle. dx (Number): abscissa of the arc's end point relative to the end point of the previous path element. dy (Number): ordinate of the arc's end point relative to the end point of the previous path element. """ if rx == 0 or ry == 0: return self.line_relative(dx, dy) radii = Point(abs(rx), abs(ry)) large_arc = bool(large_arc) rotation = math.radians(rotation) positive_sweep = bool(positive_sweep) end = Point(dx, dy) self.add_path_element( RelativeArc(radii, rotation, large_arc, positive_sweep, end), _copy=False ) return self
- The signs of the radii arguments (
def arc_to(self, rx, ry, rotation, large_arc, positive_sweep, x, y)
-
Append an elliptical arc from the end of the previous path point to the specified end point.
The arc is approximated using Bézier curves, so it is not perfectly accurate. However, the error is small enough to not be noticeable at any reasonable (and even most unreasonable) scales, with a worst-case deviation of around 3‱.
Notes
- The signs of the radii arguments (
rx
andry
) are ignored (i.e. their absolute values are used instead). - If either radius is 0, then a straight line will be emitted instead of an arc.
- If the radii are too small for the arc to reach from the current point to
the specified end point (
x
andy
), then they will be proportionally scaled up until they are big enough, which will always result in a half-ellipse arc (i.e. an 180 degree sweep)
Args
rx
:Number
- radius in the x-direction.
ry
:Number
- radius in the y-direction.
rotation
:Number
- angle (in degrees) that the arc should be rotated
clockwise from the principle axes. This parameter does not have
a visual effect in the case that
rx == ry
. large_arc
:bool
- if True, the arc will cover a sweep angle of at least 180 degrees. Otherwise, the sweep angle will be at most 180 degrees.
positive_sweep
:bool
- if True, the arc will be swept over a positive angle, i.e. clockwise. Otherwise, the arc will be swept over a negative angle.
x
:Number
- abscissa of the arc's end point.
y
:Number
- ordinate of the arc's end point.
Expand source code
def arc_to(self, rx, ry, rotation, large_arc, positive_sweep, x, y): """ Append an elliptical arc from the end of the previous path point to the specified end point. The arc is approximated using Bézier curves, so it is not perfectly accurate. However, the error is small enough to not be noticeable at any reasonable (and even most unreasonable) scales, with a worst-case deviation of around 3‱. Notes: - The signs of the radii arguments (`rx` and `ry`) are ignored (i.e. their absolute values are used instead). - If either radius is 0, then a straight line will be emitted instead of an arc. - If the radii are too small for the arc to reach from the current point to the specified end point (`x` and `y`), then they will be proportionally scaled up until they are big enough, which will always result in a half-ellipse arc (i.e. an 180 degree sweep) Args: rx (Number): radius in the x-direction. ry (Number): radius in the y-direction. rotation (Number): angle (in degrees) that the arc should be rotated clockwise from the principle axes. This parameter does not have a visual effect in the case that `rx == ry`. large_arc (bool): if True, the arc will cover a sweep angle of at least 180 degrees. Otherwise, the sweep angle will be at most 180 degrees. positive_sweep (bool): if True, the arc will be swept over a positive angle, i.e. clockwise. Otherwise, the arc will be swept over a negative angle. x (Number): abscissa of the arc's end point. y (Number): ordinate of the arc's end point. """ if rx == 0 or ry == 0: return self.line_to(x, y) radii = Point(abs(rx), abs(ry)) large_arc = bool(large_arc) rotation = math.radians(rotation) positive_sweep = bool(positive_sweep) end = Point(x, y) self.add_path_element( Arc(radii, rotation, large_arc, positive_sweep, end), _copy=False ) return self
- The signs of the radii arguments (
def circle(self, cx, cy, r)
-
Append a circle as a closed subpath to the current path.
Args
cx
:Number
- the abscissa of the circle's center point.
cy
:Number
- the ordinate of the circle's center point.
r
:Number
- the radius of the circle.
Returns
The path, to allow chaining method calls.
Expand source code
def circle(self, cx, cy, r): """ Append a circle as a closed subpath to the current path. Args: cx (Number): the abscissa of the circle's center point. cy (Number): the ordinate of the circle's center point. r (Number): the radius of the circle. Returns: The path, to allow chaining method calls. """ return self.ellipse(cx, cy, r, r)
def close(self)
-
Explicitly close the current (sub)path.
Expand source code
def close(self): """ Explicitly close the current (sub)path. """ self.add_path_element(Close(), _copy=False) self._closed = True self.move_relative(0, 0)
def curve_relative(self, dx1, dy1, dx2, dy2, dx3, dy3)
-
Append a cubic Bézier curve whose points are expressed relative to the end point of the previous path element.
E.g. with a start point of (0, 0), given (1, 1), (2, 2), (3, 3), the output curve would have the points:
(0, 0) c1 (1, 1) c2 (3, 3) e (6, 6)
Args
dx1
:Number
- abscissa of the first control point relative to the end point of the previous path element
dy1
:Number
- ordinate of the first control point relative to the end point of the previous path element
dx2
:Number
- abscissa offset of the second control point relative to the end point of the previous path element
dy2
:Number
- ordinate offset of the second control point relative to the end point of the previous path element
dx3
:Number
- abscissa offset of the end point relative to the end point of the previous path element
dy3
:Number
- ordinate offset of the end point relative to the end point of the previous path element
Returns
The path, to allow chaining method calls.
Expand source code
def curve_relative(self, dx1, dy1, dx2, dy2, dx3, dy3): """ Append a cubic Bézier curve whose points are expressed relative to the end point of the previous path element. E.g. with a start point of (0, 0), given (1, 1), (2, 2), (3, 3), the output curve would have the points: (0, 0) c1 (1, 1) c2 (3, 3) e (6, 6) Args: dx1 (Number): abscissa of the first control point relative to the end point of the previous path element dy1 (Number): ordinate of the first control point relative to the end point of the previous path element dx2 (Number): abscissa offset of the second control point relative to the end point of the previous path element dy2 (Number): ordinate offset of the second control point relative to the end point of the previous path element dx3 (Number): abscissa offset of the end point relative to the end point of the previous path element dy3 (Number): ordinate offset of the end point relative to the end point of the previous path element Returns: The path, to allow chaining method calls. """ c1d = Point(dx1, dy1) c2d = Point(dx2, dy2) end = Point(dx3, dy3) self.add_path_element(RelativeBezierCurve(c1d, c2d, end), _copy=False) return self
def curve_to(self, x1, y1, x2, y2, x3, y3)
-
Append a cubic Bézier curve to this path.
Args
x1
:Number
- abscissa of the first control point
y1
:Number
- ordinate of the first control point
x2
:Number
- abscissa of the second control point
y2
:Number
- ordinate of the second control point
x3
:Number
- abscissa of the end point
y3
:Number
- ordinate of the end point
Returns
The path, to allow chaining method calls.
Expand source code
def curve_to(self, x1, y1, x2, y2, x3, y3): """ Append a cubic Bézier curve to this path. Args: x1 (Number): abscissa of the first control point y1 (Number): ordinate of the first control point x2 (Number): abscissa of the second control point y2 (Number): ordinate of the second control point x3 (Number): abscissa of the end point y3 (Number): ordinate of the end point Returns: The path, to allow chaining method calls. """ ctrl1 = Point(x1, y1) ctrl2 = Point(x2, y2) end = Point(x3, y3) self.add_path_element(BezierCurve(ctrl1, ctrl2, end), _copy=False) return self
def ellipse(self, cx, cy, rx, ry)
-
Append an ellipse as a closed subpath to the current path.
Args
cx
:Number
- the abscissa of the ellipse's center point.
cy
:Number
- the ordinate of the ellipse's center point.
rx
:Number
- the x-radius of the ellipse.
ry
:Number
- the y-radius of the ellipse.
Returns
The path, to allow chaining method calls.
Expand source code
def ellipse(self, cx, cy, rx, ry): """ Append an ellipse as a closed subpath to the current path. Args: cx (Number): the abscissa of the ellipse's center point. cy (Number): the ordinate of the ellipse's center point. rx (Number): the x-radius of the ellipse. ry (Number): the y-radius of the ellipse. Returns: The path, to allow chaining method calls. """ self._insert_implicit_close_if_open() self.add_path_element(Ellipse(Point(rx, ry), Point(cx, cy)), _copy=False) self._closed = True self.move_to(cx, cy) return self
def horizontal_line_relative(self, dx)
-
Append a straight horizontal line to the given offset from the previous path element. The ordinate is retrieved from the end point of the previous path element.
Args
x
:Number
- abscissa of the line's end point relative to the end point of the previous path element.
Returns
The path, to allow chaining method calls.
Expand source code
def horizontal_line_relative(self, dx): """ Append a straight horizontal line to the given offset from the previous path element. The ordinate is retrieved from the end point of the previous path element. Args: x (Number): abscissa of the line's end point relative to the end point of the previous path element. Returns: The path, to allow chaining method calls. """ self.add_path_element(RelativeHorizontalLine(dx), _copy=False) return self
def horizontal_line_to(self, x)
-
Append a straight horizontal line to the given abscissa. The ordinate is retrieved from the end point of the previous path element.
Args
x
:Number
- abscissa of the line's end point.
Returns
The path, to allow chaining method calls.
Expand source code
def horizontal_line_to(self, x): """ Append a straight horizontal line to the given abscissa. The ordinate is retrieved from the end point of the previous path element. Args: x (Number): abscissa of the line's end point. Returns: The path, to allow chaining method calls. """ self.add_path_element(HorizontalLine(x), _copy=False) return self
def line_relative(self, dx, dy)
-
Append a straight line whose end is computed as an offset from the end of the previous path element.
Args
x
:Number
- abscissa the line's end point relative to the end point of the previous path element.
y
:Number
- ordinate of the line's end point relative to the end point of the previous path element.
Returns
The path, to allow chaining method calls.
Expand source code
def line_relative(self, dx, dy): """ Append a straight line whose end is computed as an offset from the end of the previous path element. Args: x (Number): abscissa the line's end point relative to the end point of the previous path element. y (Number): ordinate of the line's end point relative to the end point of the previous path element. Returns: The path, to allow chaining method calls. """ self.add_path_element(RelativeLine(Point(dx, dy)), _copy=False) return self
def line_to(self, x, y)
-
Append a straight line to this path.
Args
x
:Number
- abscissa the line's end point.
y
:Number
- ordinate of the line's end point.
Returns
The path, to allow chaining method calls.
Expand source code
def line_to(self, x, y): """ Append a straight line to this path. Args: x (Number): abscissa the line's end point. y (Number): ordinate of the line's end point. Returns: The path, to allow chaining method calls. """ self.add_path_element(Line(Point(x, y)), _copy=False) return self
def move_relative(self, x, y)
-
Start a new subpath or move the path start point relative to the previous point.
If no path elements have been added yet, this will change the path starting point. If path elements have been added, this will insert an implicit close in order to start a new subpath.
This will overwrite an absolute move_to as long as no non-move path items have been appended. The relative position is resolved from the previous item when the path is being rendered, or from 0, 0 if it is the first item.
Args
x
:Number
- abscissa of the (sub)path starting point relative to the.
y
:Number
- ordinate of the (sub)path starting point relative to the.
Expand source code
def move_relative(self, x, y): """ Start a new subpath or move the path start point relative to the previous point. If no path elements have been added yet, this will change the path starting point. If path elements have been added, this will insert an implicit close in order to start a new subpath. This will overwrite an absolute move_to as long as no non-move path items have been appended. The relative position is resolved from the previous item when the path is being rendered, or from 0, 0 if it is the first item. Args: x (Number): abscissa of the (sub)path starting point relative to the. y (Number): ordinate of the (sub)path starting point relative to the. """ self._insert_implicit_close_if_open() if self._starter_move is not None: self._closed = False self._graphics_context.add_item(self._starter_move, _copy=False) self._close_context = self._graphics_context self._starter_move = RelativeMove(Point(x, y)) return self
def move_to(self, x, y)
-
Start a new subpath or move the path starting point.
If no path elements have been added yet, this will change the path starting point. If path elements have been added, this will insert an implicit close in order to start a new subpath.
Args
x
:Number
- abscissa of the (sub)path starting point.
y
:Number
- ordinate of the (sub)path starting point.
Returns
The path, to allow chaining method calls.
Expand source code
def move_to(self, x, y): """ Start a new subpath or move the path starting point. If no path elements have been added yet, this will change the path starting point. If path elements have been added, this will insert an implicit close in order to start a new subpath. Args: x (Number): abscissa of the (sub)path starting point. y (Number): ordinate of the (sub)path starting point. Returns: The path, to allow chaining method calls. """ self._insert_implicit_close_if_open() self._starter_move = Move(Point(x, y)) return self
def quadratic_curve_relative(self, dx1, dy1, dx2, dy2)
-
Append a cubic Bézier curve mimicking the specified quadratic Bézier curve.
Args
dx1
:Number
- abscissa of the control point relative to the end point of the previous path element
dy1
:Number
- ordinate of the control point relative to the end point of the previous path element
dx2
:Number
- abscissa offset of the end point relative to the end point of the previous path element
dy2
:Number
- ordinate offset of the end point relative to the end point of the previous path element
Returns
The path, to allow chaining method calls.
Expand source code
def quadratic_curve_relative(self, dx1, dy1, dx2, dy2): """ Append a cubic Bézier curve mimicking the specified quadratic Bézier curve. Args: dx1 (Number): abscissa of the control point relative to the end point of the previous path element dy1 (Number): ordinate of the control point relative to the end point of the previous path element dx2 (Number): abscissa offset of the end point relative to the end point of the previous path element dy2 (Number): ordinate offset of the end point relative to the end point of the previous path element Returns: The path, to allow chaining method calls. """ ctrl = Point(dx1, dy1) end = Point(dx2, dy2) self.add_path_element(RelativeQuadraticBezierCurve(ctrl, end), _copy=False) return self
def quadratic_curve_to(self, x1, y1, x2, y2)
-
Append a cubic Bézier curve mimicking the specified quadratic Bézier curve.
Args
x1
:Number
- abscissa of the control point
y1
:Number
- ordinate of the control point
x2
:Number
- abscissa of the end point
y2
:Number
- ordinate of the end point
Returns
The path, to allow chaining method calls.
Expand source code
def quadratic_curve_to(self, x1, y1, x2, y2): """ Append a cubic Bézier curve mimicking the specified quadratic Bézier curve. Args: x1 (Number): abscissa of the control point y1 (Number): ordinate of the control point x2 (Number): abscissa of the end point y2 (Number): ordinate of the end point Returns: The path, to allow chaining method calls. """ ctrl = Point(x1, y1) end = Point(x2, y2) self.add_path_element(QuadraticBezierCurve(ctrl, end), _copy=False) return self
def rectangle(self, x, y, w, h, rx=0, ry=0)
-
Append a rectangle as a closed subpath to the current path.
If the width or the height are 0, the rectangle will be collapsed to a line (unless they're both 0, in which case it's collapsed to nothing).
Args
x
:Number
- the abscissa of the starting corner of the rectangle.
y
:Number
- the ordinate of the starting corner of the rectangle.
w
:Number
- the width of the rectangle (if 0, the rectangle will be rendered as a vertical line).
h
:Number
- the height of the rectangle (if 0, the rectangle will be rendered as a horizontal line).
rx
:Number
- the x-radius of the rectangle rounded corner (if 0 the corners will not be rounded).
ry
:Number
- the y-radius of the rectangle rounded corner (if 0 the corners will not be rounded).
Returns
The path, to allow chaining method calls.
Expand source code
def rectangle(self, x, y, w, h, rx=0, ry=0): """ Append a rectangle as a closed subpath to the current path. If the width or the height are 0, the rectangle will be collapsed to a line (unless they're both 0, in which case it's collapsed to nothing). Args: x (Number): the abscissa of the starting corner of the rectangle. y (Number): the ordinate of the starting corner of the rectangle. w (Number): the width of the rectangle (if 0, the rectangle will be rendered as a vertical line). h (Number): the height of the rectangle (if 0, the rectangle will be rendered as a horizontal line). rx (Number): the x-radius of the rectangle rounded corner (if 0 the corners will not be rounded). ry (Number): the y-radius of the rectangle rounded corner (if 0 the corners will not be rounded). Returns: The path, to allow chaining method calls. """ self._insert_implicit_close_if_open() self.add_path_element( RoundedRectangle(Point(x, y), Point(w, h), Point(rx, ry)), _copy=False ) self._closed = True self.move_to(x, y) return self
def remove_last_path_element(self)
-
Expand source code
def remove_last_path_element(self): self._graphics_context.remove_last_item()
def render(self, gsd_registry, style, last_item, initial_point, debug_stream=None, pfx=None)
-
Expand source code
def render( self, gsd_registry, style, last_item, initial_point, debug_stream=None, pfx=None ): self._insert_implicit_close_if_open() ( render_list, last_item, initial_point, ) = self._root_graphics_context.build_render_list( gsd_registry, style, last_item, initial_point, debug_stream, pfx ) paint_rule = GraphicsStyle.merge(style, self.style).resolve_paint_rule() render_list.insert(-1, paint_rule.value) return " ".join(render_list), last_item, initial_point
def render_debug(self, gsd_registry, style, last_item, initial_point, debug_stream, pfx)
-
Render this path element to its PDF representation and produce debug information.
Args
gsd_registry
:GraphicsStateDictRegistry
- the owner's graphics state dictionary registry.
style
:GraphicsStyle
- the current resolved graphics style
last_item
- the previous path element.
initial_point
- last position set by a "M" or "m" command
debug_stream
:io.TextIO
- the stream to which the debug output should be written. This is not guaranteed to be seekable (e.g. it may be stdout or stderr).
pfx
:str
- the current debug output prefix string (only needed if emitting more than one line).
Returns
The same tuple as
PaintedPath.render()
.Expand source code
def render_debug( self, gsd_registry, style, last_item, initial_point, debug_stream, pfx ): """ Render this path element to its PDF representation and produce debug information. Args: gsd_registry (GraphicsStateDictRegistry): the owner's graphics state dictionary registry. style (GraphicsStyle): the current resolved graphics style last_item: the previous path element. initial_point: last position set by a "M" or "m" command debug_stream (io.TextIO): the stream to which the debug output should be written. This is not guaranteed to be seekable (e.g. it may be stdout or stderr). pfx (str): the current debug output prefix string (only needed if emitting more than one line). Returns: The same tuple as `PaintedPath.render`. """ return self.render( gsd_registry, style, last_item, initial_point, debug_stream, pfx )
def transform_group(self, transform)
-
Apply the provided
Transform
to all points added within this context.Expand source code
@contextmanager def transform_group(self, transform): """ Apply the provided `Transform` to all points added within this context. """ with self._new_graphics_context() as ctxt: ctxt.transform = transform yield self
def vertical_line_relative(self, dy)
-
Append a straight vertical line to the given offset from the previous path element. The abscissa is retrieved from the end point of the previous path element.
Args
y
:Number
- ordinate of the line's end point relative to the end point of the previous path element.
Returns
The path, to allow chaining method calls.
Expand source code
def vertical_line_relative(self, dy): """ Append a straight vertical line to the given offset from the previous path element. The abscissa is retrieved from the end point of the previous path element. Args: y (Number): ordinate of the line's end point relative to the end point of the previous path element. Returns: The path, to allow chaining method calls. """ self.add_path_element(RelativeVerticalLine(dy), _copy=False) return self
def vertical_line_to(self, y)
-
Append a straight vertical line to the given ordinate. The abscissa is retrieved from the end point of the previous path element.
Args
y
:Number
- ordinate of the line's end point.
Returns
The path, to allow chaining method calls.
Expand source code
def vertical_line_to(self, y): """ Append a straight vertical line to the given ordinate. The abscissa is retrieved from the end point of the previous path element. Args: y (Number): ordinate of the line's end point. Returns: The path, to allow chaining method calls. """ self.add_path_element(VerticalLine(y), _copy=False) return self
class Point (x: Union[int, float, decimal.Decimal], y: Union[int, float, decimal.Decimal])
-
An x-y coordinate pair within the two-dimensional coordinate frame.
Expand source code
class Point(NamedTuple): """ An x-y coordinate pair within the two-dimensional coordinate frame. """ x: Number """The abscissa of the point.""" y: Number """The ordinate of the point.""" def render(self): """Render the point to the string `"x y"` for emitting to a PDF.""" return f"{number_to_str(self.x)} {number_to_str(self.y)}" def dot(self, other): """ Compute the dot product of two points. Args: other (Point): the point with which to compute the dot product. Returns: The scalar result of the dot product computation. Raises: TypeError: if `other` is not a `Point`. """ if not isinstance(other, Point): raise TypeError(f"cannot dot with {other!r}") return self.x * other.x + self.y * other.y def angle(self, other): """ Compute the angle between two points (interpreted as vectors from the origin). The return value is in the interval (-pi, pi]. Sign is dependent on ordering, with clockwise angle travel considered to be positive due to the orientation of the coordinate frame basis vectors (i.e. the angle between `(1, 0)` and `(0, 1)` is `+pi/2`, the angle between `(1, 0)` and `(0, -1)` is `-pi/2`, and the angle between `(0, -1)` and `(1, 0)` is `+pi/2`). Args: other (Point): the point to compute the angle sweep toward. Returns: The scalar angle between the two points **in radians**. Raises: TypeError: if `other` is not a `Point`. """ if not isinstance(other, Point): raise TypeError(f"cannot compute angle with {other!r}") signifier = (self.x * other.y) - (self.y * other.x) sign = (signifier >= 0) - (signifier < 0) return sign * math.acos(round(self.dot(other) / (self.mag() * other.mag()), 8)) def mag(self): """ Compute the Cartesian distance from this point to the origin This is the same as computing the magnitude of the vector represented by this point. Returns: The scalar result of the distance computation. """ return (self.x**2 + self.y**2) ** 0.5 @force_document def __add__(self, other): """ Produce the sum of two points. Adding two points is the same as translating the source point by interpreting the other point's x and y coordinates as distances. Args: other (Point): right-hand side of the infix addition operation Returns: A Point which is the sum of the two source points. """ if isinstance(other, Point): return Point(x=self.x + other.x, y=self.y + other.y) return NotImplemented @force_document def __sub__(self, other): """ Produce the difference between two points. Unlike addition, this is not a commutative operation! Args: other (Point): right-hand side of the infix subtraction operation Returns: A Point which is the difference of the two source points. """ if isinstance(other, Point): return Point(x=self.x - other.x, y=self.y - other.y) return NotImplemented @force_document def __neg__(self): """ Produce a point by negating this point's coordinates. Returns: A Point whose coordinates are this points coordinates negated. """ return Point(x=-self.x, y=-self.y) @force_document def __mul__(self, other): """ Multiply a point by a scalar value. Args: other (Number): the scalar value by which to multiply the point's coordinates. Returns: A Point whose coordinates are the result of the multiplication. """ if isinstance(other, NumberClass): return Point(self.x * other, self.y * other) return NotImplemented __rmul__ = __mul__ @force_document def __truediv__(self, other): """ Divide a point by a scalar value. .. note:: Because division is not commutative, `Point / scalar` is implemented, but `scalar / Point` is nonsensical and not implemented. Args: other (Number): the scalar value by which to divide the point's coordinates. Returns: A Point whose coordinates are the result of the division. """ if isinstance(other, NumberClass): return Point(self.x / other, self.y / other) return NotImplemented @force_document def __floordiv__(self, other): """ Divide a point by a scalar value using integer division. .. note:: Because division is not commutative, `Point // scalar` is implemented, but `scalar // Point` is nonsensical and not implemented. Args: other (Number): the scalar value by which to divide the point's coordinates. Returns: A Point whose coordinates are the result of the division. """ if isinstance(other, NumberClass): return Point(self.x // other, self.y // other) return NotImplemented # no __r(true|floor)div__ because division is not commutative! @force_document def __matmul__(self, other): """ Transform a point with the given transform matrix. .. note:: This operator is only implemented for Transforms. This transform is not commutative, so `Point @ Transform` is implemented, but `Transform @ Point` is not implemented (technically speaking, the current implementation is commutative because of the way points and transforms are represented, but if that representation were to change this operation could stop being commutative) Args: other (Transform): the transform to apply to the point Returns: A Point whose coordinates are the result of applying the transform. """ if isinstance(other, Transform): return Point( x=other.a * self.x + other.c * self.y + other.e, y=other.b * self.x + other.d * self.y + other.f, ) return NotImplemented def __str__(self): return f"(x={number_to_str(self.x)}, y={number_to_str(self.y)})"
Ancestors
- builtins.tuple
Instance variables
var x : Union[int, float, decimal.Decimal]
-
The abscissa of the point.
var y : Union[int, float, decimal.Decimal]
-
The ordinate of the point.
Methods
def __add__(self, other)
-
Produce the sum of two points.
Adding two points is the same as translating the source point by interpreting the other point's x and y coordinates as distances.
Args
other
:Point
- right-hand side of the infix addition operation
Returns
A Point which is the sum of the two source points.
Expand source code
@force_document def __add__(self, other): """ Produce the sum of two points. Adding two points is the same as translating the source point by interpreting the other point's x and y coordinates as distances. Args: other (Point): right-hand side of the infix addition operation Returns: A Point which is the sum of the two source points. """ if isinstance(other, Point): return Point(x=self.x + other.x, y=self.y + other.y) return NotImplemented
def __floordiv__(self, other)
-
Divide a point by a scalar value using integer division.
Note
Because division is not commutative,
Point // scalar
is implemented, butscalar // Point
is nonsensical and not implemented.Args
other
:Number
- the scalar value by which to divide the point's coordinates.
Returns
A Point whose coordinates are the result of the division.
Expand source code
@force_document def __floordiv__(self, other): """ Divide a point by a scalar value using integer division. .. note:: Because division is not commutative, `Point // scalar` is implemented, but `scalar // Point` is nonsensical and not implemented. Args: other (Number): the scalar value by which to divide the point's coordinates. Returns: A Point whose coordinates are the result of the division. """ if isinstance(other, NumberClass): return Point(self.x // other, self.y // other) return NotImplemented
def __matmul__(self, other)
-
Transform a point with the given transform matrix.
Note
This operator is only implemented for Transforms. This transform is not commutative, so
Point @ Transform
is implemented, butTransform @ Point
is not implemented (technically speaking, the current implementation is commutative because of the way points and transforms are represented, but if that representation were to change this operation could stop being commutative)Args
other
:Transform
- the transform to apply to the point
Returns
A Point whose coordinates are the result of applying the transform.
Expand source code
@force_document def __matmul__(self, other): """ Transform a point with the given transform matrix. .. note:: This operator is only implemented for Transforms. This transform is not commutative, so `Point @ Transform` is implemented, but `Transform @ Point` is not implemented (technically speaking, the current implementation is commutative because of the way points and transforms are represented, but if that representation were to change this operation could stop being commutative) Args: other (Transform): the transform to apply to the point Returns: A Point whose coordinates are the result of applying the transform. """ if isinstance(other, Transform): return Point( x=other.a * self.x + other.c * self.y + other.e, y=other.b * self.x + other.d * self.y + other.f, ) return NotImplemented
def __mul__(self, other)
-
Multiply a point by a scalar value.
Args
other
:Number
- the scalar value by which to multiply the point's coordinates.
Returns
A Point whose coordinates are the result of the multiplication.
Expand source code
@force_document def __mul__(self, other): """ Multiply a point by a scalar value. Args: other (Number): the scalar value by which to multiply the point's coordinates. Returns: A Point whose coordinates are the result of the multiplication. """ if isinstance(other, NumberClass): return Point(self.x * other, self.y * other) return NotImplemented
def __neg__(self)
-
Produce a point by negating this point's coordinates.
Returns
A Point whose coordinates are this points coordinates negated.
Expand source code
@force_document def __neg__(self): """ Produce a point by negating this point's coordinates. Returns: A Point whose coordinates are this points coordinates negated. """ return Point(x=-self.x, y=-self.y)
def __sub__(self, other)
-
Produce the difference between two points.
Unlike addition, this is not a commutative operation!
Args
other
:Point
- right-hand side of the infix subtraction operation
Returns
A Point which is the difference of the two source points.
Expand source code
@force_document def __sub__(self, other): """ Produce the difference between two points. Unlike addition, this is not a commutative operation! Args: other (Point): right-hand side of the infix subtraction operation Returns: A Point which is the difference of the two source points. """ if isinstance(other, Point): return Point(x=self.x - other.x, y=self.y - other.y) return NotImplemented
def __truediv__(self, other)
-
Divide a point by a scalar value.
Note
Because division is not commutative,
Point / scalar
is implemented, butscalar / Point
is nonsensical and not implemented.Args
other
:Number
- the scalar value by which to divide the point's coordinates.
Returns
A Point whose coordinates are the result of the division.
Expand source code
@force_document def __truediv__(self, other): """ Divide a point by a scalar value. .. note:: Because division is not commutative, `Point / scalar` is implemented, but `scalar / Point` is nonsensical and not implemented. Args: other (Number): the scalar value by which to divide the point's coordinates. Returns: A Point whose coordinates are the result of the division. """ if isinstance(other, NumberClass): return Point(self.x / other, self.y / other) return NotImplemented
def angle(self, other)
-
Compute the angle between two points (interpreted as vectors from the origin).
The return value is in the interval (-pi, pi]. Sign is dependent on ordering, with clockwise angle travel considered to be positive due to the orientation of the coordinate frame basis vectors (i.e. the angle between
(1, 0)
and(0, 1)
is+pi/2
, the angle between(1, 0)
and(0, -1)
is-pi/2
, and the angle between(0, -1)
and(1, 0)
is+pi/2
).Args
other
:Point
- the point to compute the angle sweep toward.
Returns
The scalar angle between the two points in radians.
Raises
TypeError
- if
other
is not aPoint
.
Expand source code
def angle(self, other): """ Compute the angle between two points (interpreted as vectors from the origin). The return value is in the interval (-pi, pi]. Sign is dependent on ordering, with clockwise angle travel considered to be positive due to the orientation of the coordinate frame basis vectors (i.e. the angle between `(1, 0)` and `(0, 1)` is `+pi/2`, the angle between `(1, 0)` and `(0, -1)` is `-pi/2`, and the angle between `(0, -1)` and `(1, 0)` is `+pi/2`). Args: other (Point): the point to compute the angle sweep toward. Returns: The scalar angle between the two points **in radians**. Raises: TypeError: if `other` is not a `Point`. """ if not isinstance(other, Point): raise TypeError(f"cannot compute angle with {other!r}") signifier = (self.x * other.y) - (self.y * other.x) sign = (signifier >= 0) - (signifier < 0) return sign * math.acos(round(self.dot(other) / (self.mag() * other.mag()), 8))
def dot(self, other)
-
Compute the dot product of two points.
Args
other
:Point
- the point with which to compute the dot product.
Returns
The scalar result of the dot product computation.
Raises
TypeError
- if
other
is not aPoint
.
Expand source code
def dot(self, other): """ Compute the dot product of two points. Args: other (Point): the point with which to compute the dot product. Returns: The scalar result of the dot product computation. Raises: TypeError: if `other` is not a `Point`. """ if not isinstance(other, Point): raise TypeError(f"cannot dot with {other!r}") return self.x * other.x + self.y * other.y
def mag(self)
-
Compute the Cartesian distance from this point to the origin
This is the same as computing the magnitude of the vector represented by this point.
Returns
The scalar result of the distance computation.
Expand source code
def mag(self): """ Compute the Cartesian distance from this point to the origin This is the same as computing the magnitude of the vector represented by this point. Returns: The scalar result of the distance computation. """ return (self.x**2 + self.y**2) ** 0.5
def render(self)
-
Render the point to the string
"x y"
for emitting to a PDF.Expand source code
def render(self): """Render the point to the string `"x y"` for emitting to a PDF.""" return f"{number_to_str(self.x)} {number_to_str(self.y)}"
class QuadraticBezierCurve (ctrl: Point, end: Point)
-
A quadratic Bézier curve path element.
This draws a Bézier curve parameterized by the end point of the previous path element, one off-curve control point, and an end point.
Expand source code
class QuadraticBezierCurve(NamedTuple): """ A quadratic Bézier curve path element. This draws a Bézier curve parameterized by the end point of the previous path element, one off-curve control point, and an end point. See: `PaintedPath.quadratic_curve_to` """ ctrl: Point """The curve's control point.""" end: Point """The curve's end point.""" @property def end_point(self): """The end point of this path element.""" return self.end def to_cubic_curve(self, start_point): ctrl = self.ctrl end = self.end ctrl1 = Point( x=start_point.x + 2 * (ctrl.x - start_point.x) / 3, y=start_point.y + 2 * (ctrl.y - start_point.y) / 3, ) ctrl2 = Point( x=end.x + 2 * (ctrl.x - end.x) / 3, y=end.y + 2 * (ctrl.y - end.y) / 3, ) return BezierCurve(ctrl1, ctrl2, end) @force_nodocument def render(self, gsd_registry, style, last_item, initial_point): """ Render this path element to its PDF representation. Args: gsd_registry (GraphicsStateDictRegistry): the owner's graphics state dictionary registry. style (GraphicsStyle): the current resolved graphics style last_item: the previous path element. initial_point: last position set by a "M" or "m" command Returns: a tuple of `(str, new_last_item)`, where `new_last_item` is `self`. """ # pylint: disable=unused-argument return ( self.to_cubic_curve(last_item.end_point).render( gsd_registry, style, last_item, initial_point )[0], self, initial_point, ) @force_nodocument def render_debug( self, gsd_registry, style, last_item, initial_point, debug_stream, pfx ): """ Render this path element to its PDF representation and produce debug information. Args: gsd_registry (GraphicsStateDictRegistry): the owner's graphics state dictionary registry. style (GraphicsStyle): the current resolved graphics style last_item: the previous path element. initial_point: last position set by a "M" or "m" command debug_stream (io.TextIO): the stream to which the debug output should be written. This is not guaranteed to be seekable (e.g. it may be stdout or stderr). pfx (str): the current debug output prefix string (only needed if emitting more than one line). Returns: The same tuple as `QuadraticBezierCurve.render`. """ # pylint: disable=unused-argument rendered, resolved, initial_point = self.render( gsd_registry, style, last_item, initial_point ) debug_stream.write( f"{self} resolved to {self.to_cubic_curve(last_item.end_point)}\n" ) return rendered, resolved, initial_point
Ancestors
- builtins.tuple
Instance variables
var ctrl : Point
-
The curve's control point.
var end : Point
-
The curve's end point.
var end_point
-
The end point of this path element.
Expand source code
@property def end_point(self): """The end point of this path element.""" return self.end
Methods
def to_cubic_curve(self, start_point)
-
Expand source code
def to_cubic_curve(self, start_point): ctrl = self.ctrl end = self.end ctrl1 = Point( x=start_point.x + 2 * (ctrl.x - start_point.x) / 3, y=start_point.y + 2 * (ctrl.y - start_point.y) / 3, ) ctrl2 = Point( x=end.x + 2 * (ctrl.x - end.x) / 3, y=end.y + 2 * (ctrl.y - end.y) / 3, ) return BezierCurve(ctrl1, ctrl2, end)
class Rectangle (org: Point, size: Point)
-
A pdf primitive rectangle.
Expand source code
class Rectangle(NamedTuple): """A pdf primitive rectangle.""" org: Point """The top-left corner of the rectangle.""" size: Point """The width and height of the rectangle.""" @force_nodocument def render(self, gsd_registry, style, last_item, initial_point): """ Render this path element to its PDF representation. Args: gsd_registry (GraphicsStateDictRegistry): the owner's graphics state dictionary registry. style (GraphicsStyle): the current resolved graphics style last_item: the previous path element. initial_point: last position set by a "M" or "m" command Returns: a tuple of `(str, new_last_item)`, where `new_last_item` is a `Line` back to the rectangle's origin. """ # pylint: disable=unused-argument return ( f"{self.org.render()} {self.size.render()} re", Line(self.org), initial_point, ) @force_nodocument def render_debug( self, gsd_registry, style, last_item, initial_point, debug_stream, pfx ): """ Render this path element to its PDF representation and produce debug information. Args: gsd_registry (GraphicsStateDictRegistry): the owner's graphics state dictionary registry. style (GraphicsStyle): the current resolved graphics style last_item: the previous path element. initial_point: last position set by a "M" or "m" command debug_stream (io.TextIO): the stream to which the debug output should be written. This is not guaranteed to be seekable (e.g. it may be stdout or stderr). pfx (str): the current debug output prefix string (only needed if emitting more than one line). Returns: The same tuple as `Rectangle.render`. """ # pylint: disable=unused-argument rendered, resolved, initial_point = self.render( gsd_registry, style, last_item, initial_point ) debug_stream.write(f"{self} resolved to {rendered}\n") return rendered, resolved, initial_point
Ancestors
- builtins.tuple
Instance variables
var org : Point
-
The top-left corner of the rectangle.
var size : Point
-
The width and height of the rectangle.
class RelativeArc (radii: Point, rotation: Union[int, float, decimal.Decimal], large: bool, sweep: bool, end: Point)
-
An elliptical arc path element.
The arc is drawn from the end of the current path element to its specified end point using a number of parameters to determine how it is constructed.
Expand source code
class RelativeArc(NamedTuple): """ An elliptical arc path element. The arc is drawn from the end of the current path element to its specified end point using a number of parameters to determine how it is constructed. See: `PaintedPath.arc_relative` """ radii: Point """ The x- and y-radii of the arc. If `radii.x == radii.y` the arc will be circular. """ rotation: Number """The rotation of the arc's major/minor axes relative to the coordinate frame.""" large: bool """If True, sweep the arc over an angle greater than or equal to 180 degrees.""" sweep: bool """If True, the arc is swept in the positive angular direction.""" end: Point """The end point of the arc relative to the end of the previous path element.""" @force_nodocument def render(self, gsd_registry, style, last_item, initial_point): """ Render this path element to its PDF representation. Args: gsd_registry (GraphicsStateDictRegistry): the owner's graphics state dictionary registry. style (GraphicsStyle): the current resolved graphics style last_item: the previous path element. initial_point: last position set by a "M" or "m" command Returns: a tuple of `(str, new_last_item)`, where `new_last_item` is a resolved `BezierCurve`. """ # pylint: disable=unused-argument return Arc( self.radii, self.rotation, self.large, self.sweep, last_item.end_point + self.end, ).render(gsd_registry, style, last_item, initial_point) @force_nodocument def render_debug( self, gsd_registry, style, last_item, initial_point, debug_stream, pfx ): """ Render this path element to its PDF representation and produce debug information. Args: gsd_registry (GraphicsStateDictRegistry): the owner's graphics state dictionary registry. style (GraphicsStyle): the current resolved graphics style last_item: the previous path element. initial_point: last position set by a "M" or "m" command debug_stream (io.TextIO): the stream to which the debug output should be written. This is not guaranteed to be seekable (e.g. it may be stdout or stderr). pfx (str): the current debug output prefix string (only needed if emitting more than one line). Returns: The same tuple as `RelativeArc.render`. """ # pylint: disable=unused-argument # newline is intentionally missing here debug_stream.write(f"{self} resolved to ") return Arc( self.radii, self.rotation, self.large, self.sweep, last_item.end_point + self.end, ).render_debug(gsd_registry, style, last_item, initial_point, debug_stream, pfx)
Ancestors
- builtins.tuple
Instance variables
var end : Point
-
The end point of the arc relative to the end of the previous path element.
var large : bool
-
If True, sweep the arc over an angle greater than or equal to 180 degrees.
var radii : Point
-
The x- and y-radii of the arc. If
radii.x == radii.y
the arc will be circular. var rotation : Union[int, float, decimal.Decimal]
-
The rotation of the arc's major/minor axes relative to the coordinate frame.
var sweep : bool
-
If True, the arc is swept in the positive angular direction.
class RelativeBezierCurve (c1: Point, c2: Point, end: Point)
-
A cubic Bézier curve path element whose points are specified relative to the end point of the previous path element.
Expand source code
class RelativeBezierCurve(NamedTuple): """ A cubic Bézier curve path element whose points are specified relative to the end point of the previous path element. See: `PaintedPath.curve_relative` """ c1: Point """ The curve's first control point relative to the end of the previous path element. """ c2: Point """ The curve's second control point relative to the end of the previous path element. """ end: Point """The curve's end point relative to the end of the previous path element.""" @force_nodocument def render(self, gsd_registry, style, last_item, initial_point): """ Render this path element to its PDF representation. Args: gsd_registry (GraphicsStateDictRegistry): the owner's graphics state dictionary registry. style (GraphicsStyle): the current resolved graphics style last_item: the previous path element. initial_point: last position set by a "M" or "m" command Returns: a tuple of `(str, new_last_item)`, where `new_last_item` is the resolved `BezierCurve`. """ # pylint: disable=unused-argument last_point = last_item.end_point c1 = last_point + self.c1 c2 = last_point + self.c2 end = last_point + self.end return ( _render_curve(c1, c2, end), BezierCurve(c1=c1, c2=c2, end=end), initial_point, ) @force_nodocument def render_debug( self, gsd_registry, style, last_item, initial_point, debug_stream, pfx ): """ Render this path element to its PDF representation and produce debug information. Args: gsd_registry (GraphicsStateDictRegistry): the owner's graphics state dictionary registry. style (GraphicsStyle): the current resolved graphics style last_item: the previous path element. initial_point: last position set by a "M" or "m" command debug_stream (io.TextIO): the stream to which the debug output should be written. This is not guaranteed to be seekable (e.g. it may be stdout or stderr). pfx (str): the current debug output prefix string (only needed if emitting more than one line). Returns: The same tuple as `RelativeBezierCurve.render`. """ # pylint: disable=unused-argument rendered, resolved, initial_point = self.render( gsd_registry, style, last_item, initial_point ) debug_stream.write(f"{self} resolved to {resolved}\n") return rendered, resolved, initial_point
Ancestors
- builtins.tuple
Instance variables
var c1 : Point
-
The curve's first control point relative to the end of the previous path element.
var c2 : Point
-
The curve's second control point relative to the end of the previous path element.
var end : Point
-
The curve's end point relative to the end of the previous path element.
class RelativeHorizontalLine (x: Union[int, float, decimal.Decimal])
-
A path line element that takes its ordinate from the end of the previous element and computes its abscissa offset from the end of that element.
Expand source code
class RelativeHorizontalLine(NamedTuple): """ A path line element that takes its ordinate from the end of the previous element and computes its abscissa offset from the end of that element. See: `PaintedPath.horizontal_line_relative` """ x: Number """ The abscissa of the horizontal line's end point relative to the abscissa of the previous path element. """ @force_nodocument def render(self, gsd_registry, style, last_item, initial_point): """ Render this path element to its PDF representation. Args: gsd_registry (GraphicsStateDictRegistry): the owner's graphics state dictionary registry. style (GraphicsStyle): the current resolved graphics style last_item: the previous path element. initial_point: last position set by a "M" or "m" command Returns: a tuple of `(str, new_last_item)`, where `new_last_item` is the resolved `Line`. """ # pylint: disable=unused-argument end_point = Point(x=last_item.end_point.x + self.x, y=last_item.end_point.y) return _render_line(end_point), Line(end_point), initial_point @force_nodocument def render_debug( self, gsd_registry, style, last_item, initial_point, debug_stream, pfx ): """ Render this path element to its PDF representation and produce debug information. Args: gsd_registry (GraphicsStateDictRegistry): the owner's graphics state dictionary registry. style (GraphicsStyle): the current resolved graphics style last_item: the previous path element. initial_point: last position set by a "M" or "m" command debug_stream (io.TextIO): the stream to which the debug output should be written. This is not guaranteed to be seekable (e.g. it may be stdout or stderr). pfx (str): the current debug output prefix string (only needed if emitting more than one line). Returns: The same tuple as `RelativeHorizontalLine.render`. """ # pylint: disable=unused-argument rendered, resolved, initial_point = self.render( gsd_registry, style, last_item, initial_point ) debug_stream.write(f"{self} resolved to {resolved}\n") return rendered, resolved, initial_point
Ancestors
- builtins.tuple
Instance variables
var x : Union[int, float, decimal.Decimal]
-
The abscissa of the horizontal line's end point relative to the abscissa of the previous path element.
class RelativeLine (pt: Point)
-
A path line element with an endpoint relative to the end of the previous element.
This draws a straight line from the end point of the previous path element to the point specified by
last_item.end_point + pt
. The absolute coordinates of the end point are resolved during the rendering process.Expand source code
class RelativeLine(NamedTuple): """ A path line element with an endpoint relative to the end of the previous element. This draws a straight line from the end point of the previous path element to the point specified by `last_item.end_point + pt`. The absolute coordinates of the end point are resolved during the rendering process. See: `PaintedPath.line_relative` """ pt: Point """The endpoint of the line relative to the previous path element.""" @force_nodocument def render(self, gsd_registry, style, last_item, initial_point): """ Render this path element to its PDF representation. Args: gsd_registry (GraphicsStateDictRegistry): the owner's graphics state dictionary registry. style (GraphicsStyle): the current resolved graphics style last_item: the previous path element. initial_point: last position set by a "M" or "m" command Returns: a tuple of `(str, new_last_item)`, where `new_last_item` is the resolved `Line`. """ # pylint: disable=unused-argument point = last_item.end_point + self.pt return _render_line(point), Line(point), initial_point @force_nodocument def render_debug( self, gsd_registry, style, last_item, initial_point, debug_stream, pfx ): """ Render this path element to its PDF representation and produce debug information. Args: gsd_registry (GraphicsStateDictRegistry): the owner's graphics state dictionary registry. style (GraphicsStyle): the current resolved graphics style last_item: the previous path element. initial_point: last position set by a "M" or "m" command debug_stream (io.TextIO): the stream to which the debug output should be written. This is not guaranteed to be seekable (e.g. it may be stdout or stderr). pfx (str): the current debug output prefix string (only needed if emitting more than one line). Returns: The same tuple as `RelativeLine.render`. """ # pylint: disable=unused-argument rendered, resolved, initial_point = self.render( gsd_registry, style, last_item, initial_point ) debug_stream.write(f"{self} resolved to {resolved}\n") return rendered, resolved, initial_point
Ancestors
- builtins.tuple
Instance variables
var pt : Point
-
The endpoint of the line relative to the previous path element.
class RelativeMove (pt: Point)
-
A path move element with an end point relative to the end of the previous path element.
If a path has been created but not yet painted, this will create a new subpath.
Expand source code
class RelativeMove(NamedTuple): """ A path move element with an end point relative to the end of the previous path element. If a path has been created but not yet painted, this will create a new subpath. See: `PaintedPath.move_relative` """ pt: Point """The offset by which to move.""" @force_nodocument def render(self, gsd_registry, style, last_item, initial_point): """ Render this path element to its PDF representation. Args: gsd_registry (GraphicsStateDictRegistry): the owner's graphics state dictionary registry. style (GraphicsStyle): the current resolved graphics style last_item: the previous path element. initial_point: last position set by a "M" or "m" command Returns: a tuple of `(str, new_last_item)`, where `new_last_item` is the resolved `Move` """ # pylint: disable=unused-argument point = last_item.end_point + self.pt return _render_move(point), Move(point), point @force_nodocument def render_debug( self, gsd_registry, style, last_item, initial_point, debug_stream, pfx ): """ Render this path element to its PDF representation and produce debug information. Args: gsd_registry (GraphicsStateDictRegistry): the owner's graphics state dictionary registry. style (GraphicsStyle): the current resolved graphics style last_item: the previous path element. initial_point: last position set by a "M" or "m" command debug_stream (io.TextIO): the stream to which the debug output should be written. This is not guaranteed to be seekable (e.g. it may be stdout or stderr). pfx (str): the current debug output prefix string (only needed if emitting more than one line). Returns: The same tuple as `RelativeMove.render`. """ # pylint: disable=unused-argument rendered, resolved, initial_point = self.render( gsd_registry, style, last_item, initial_point ) debug_stream.write(f"{self} resolved to {resolved}\n") return rendered, resolved, initial_point
Ancestors
- builtins.tuple
Instance variables
var pt : Point
-
The offset by which to move.
class RelativeQuadraticBezierCurve (ctrl: Point, end: Point)
-
A quadratic Bézier curve path element whose points are specified relative to the end point of the previous path element.
Expand source code
class RelativeQuadraticBezierCurve(NamedTuple): """ A quadratic Bézier curve path element whose points are specified relative to the end point of the previous path element. See: `PaintedPath.quadratic_curve_relative` """ ctrl: Point """The curve's control point relative to the end of the previous path element.""" end: Point """The curve's end point relative to the end of the previous path element.""" @force_nodocument def render(self, gsd_registry, style, last_item, initial_point): """ Render this path element to its PDF representation. Args: gsd_registry (GraphicsStateDictRegistry): the owner's graphics state dictionary registry. style (GraphicsStyle): the current resolved graphics style last_item: the previous path element. initial_point: last position set by a "M" or "m" command Returns: a tuple of `(str, new_last_item)`, where `new_last_item` is the resolved `QuadraticBezierCurve`. """ # pylint: disable=unused-argument last_point = last_item.end_point ctrl = last_point + self.ctrl end = last_point + self.end absolute = QuadraticBezierCurve(ctrl=ctrl, end=end) return absolute.render(gsd_registry, style, last_item, initial_point) @force_nodocument def render_debug( self, gsd_registry, style, last_item, initial_point, debug_stream, pfx ): """ Render this path element to its PDF representation and produce debug information. Args: gsd_registry (GraphicsStateDictRegistry): the owner's graphics state dictionary registry. style (GraphicsStyle): the current resolved graphics style last_item: the previous path element. initial_point: last position set by a "M" or "m" command debug_stream (io.TextIO): the stream to which the debug output should be written. This is not guaranteed to be seekable (e.g. it may be stdout or stderr). pfx (str): the current debug output prefix string (only needed if emitting more than one line). Returns: The same tuple as `RelativeQuadraticBezierCurve.render`. """ # pylint: disable=unused-argument rendered, resolved, initial_point = self.render( gsd_registry, style, last_item, initial_point ) debug_stream.write( f"{self} resolved to {resolved} " f"then to {resolved.to_cubic_curve(last_item.end_point)}\n" ) return rendered, resolved, initial_point
Ancestors
- builtins.tuple
Instance variables
var ctrl : Point
-
The curve's control point relative to the end of the previous path element.
var end : Point
-
The curve's end point relative to the end of the previous path element.
class RelativeVerticalLine (y: Union[int, float, decimal.Decimal])
-
A path line element that takes its abscissa from the end of the previous element and computes its ordinate offset from the end of that element.
Expand source code
class RelativeVerticalLine(NamedTuple): """ A path line element that takes its abscissa from the end of the previous element and computes its ordinate offset from the end of that element. See: `PaintedPath.vertical_line_relative` """ y: Number """ The ordinate of the vertical line's end point relative to the ordinate of the previous path element. """ @force_nodocument def render(self, gsd_registry, style, last_item, initial_point): """ Render this path element to its PDF representation. Args: gsd_registry (GraphicsStateDictRegistry): the owner's graphics state dictionary registry. style (GraphicsStyle): the current resolved graphics style last_item: the previous path element. initial_point: last position set by a "M" or "m" command Returns: a tuple of `(str, new_last_item)`, where `new_last_item` is the resolved `Line`. """ # pylint: disable=unused-argument end_point = Point(x=last_item.end_point.x, y=last_item.end_point.y + self.y) return _render_line(end_point), Line(end_point), initial_point @force_nodocument def render_debug( self, gsd_registry, style, last_item, initial_point, debug_stream, pfx ): """ Render this path element to its PDF representation and produce debug information. Args: gsd_registry (GraphicsStateDictRegistry): the owner's graphics state dictionary registry. style (GraphicsStyle): the current resolved graphics style last_item: the previous path element. initial_point: last position set by a "M" or "m" command debug_stream (io.TextIO): the stream to which the debug output should be written. This is not guaranteed to be seekable (e.g. it may be stdout or stderr). pfx (str): the current debug output prefix string (only needed if emitting more than one line). Returns: The same tuple as `RelativeVerticalLine.render`. """ # pylint: disable=unused-argument rendered, resolved, initial_point = self.render( gsd_registry, style, last_item, initial_point ) debug_stream.write(f"{self} resolved to {resolved}\n") return rendered, resolved, initial_point
Ancestors
- builtins.tuple
Instance variables
var y : Union[int, float, decimal.Decimal]
-
The ordinate of the vertical line's end point relative to the ordinate of the previous path element.
class RoundedRectangle (org: Point, size: Point, corner_radii: Point)
-
A rectangle with rounded corners.
Expand source code
class RoundedRectangle(NamedTuple): """ A rectangle with rounded corners. See: `PaintedPath.rectangle` """ org: Point """The top-left corner of the rectangle.""" size: Point """The width and height of the rectangle.""" corner_radii: Point """The x- and y-radius of the corners.""" def _decompose(self): items = [] if (self.size.x == 0) and (self.size.y == 0): pass elif (self.size.x == 0) or (self.size.y == 0): items.append(Move(self.org)) items.append(Line(self.org + self.size)) items.append(Close()) elif (self.corner_radii.x == 0) or (self.corner_radii.y == 0): items.append(Rectangle(self.org, self.size)) else: x, y = self.org w, h = self.size rx, ry = self.corner_radii sign_width = (self.size.x >= 0) - (self.size.x < 0) sign_height = (self.size.y >= 0) - (self.size.y < 0) if abs(rx) > abs(w): rx = self.size.x if abs(ry) > abs(h): ry = self.size.y rx = sign_width * abs(rx) ry = sign_height * abs(ry) arc_rad = Point(rx, ry) items.append(Move(Point(x + rx, y))) items.append(Line(Point(x + w - rx, y))) items.append(Arc(arc_rad, 0, False, True, Point(x + w, y + ry))) items.append(Line(Point(x + w, y + h - ry))) items.append(Arc(arc_rad, 0, False, True, Point(x + w - rx, y + h))) items.append(Line(Point(x + rx, y + h))) items.append(Arc(arc_rad, 0, False, True, Point(x, y + h - ry))) items.append(Line(Point(x, y + ry))) items.append(Arc(arc_rad, 0, False, True, Point(x + rx, y))) items.append(Close()) return items @force_nodocument def render(self, gsd_registry, style, last_item, initial_point): """ Render this path element to its PDF representation. Args: gsd_registry (GraphicsStateDictRegistry): the owner's graphics state dictionary registry. style (GraphicsStyle): the current resolved graphics style last_item: the previous path element. initial_point: last position set by a "M" or "m" command Returns: a tuple of `(str, new_last_item)`, where `new_last_item` is a resolved `Line`. """ # pylint: disable=unused-argument components = self._decompose() if not components: return "", last_item render_list = [] for item in components: rendered, last_item, initial_point = item.render( gsd_registry, style, last_item, initial_point ) render_list.append(rendered) return " ".join(render_list), Line(self.org), initial_point @force_nodocument def render_debug( self, gsd_registry, style, last_item, initial_point, debug_stream, pfx ): """ Render this path element to its PDF representation and produce debug information. Args: gsd_registry (GraphicsStateDictRegistry): the owner's graphics state dictionary registry. style (GraphicsStyle): the current resolved graphics style last_item: the previous path element. initial_point: last position set by a "M" or "m" command debug_stream (io.TextIO): the stream to which the debug output should be written. This is not guaranteed to be seekable (e.g. it may be stdout or stderr). pfx (str): the current debug output prefix string (only needed if emitting more than one line). Returns: The same tuple as `RoundedRectangle.render`. """ # pylint: disable=unused-argument components = self._decompose() debug_stream.write(f"{self} resolved to:\n") if not components: debug_stream.write(pfx + " └─ nothing\n") return "", last_item render_list = [] for item in components[:-1]: rendered, last_item, initial_point = item.render( gsd_registry, style, last_item, initial_point ) debug_stream.write(pfx + f" ├─ {item}\n") render_list.append(rendered) rendered, last_item, initial_point = components[-1].render( gsd_registry, style, last_item, initial_point ) debug_stream.write(pfx + f" └─ {components[-1]}\n") render_list.append(rendered) return " ".join(render_list), Line(self.org), initial_point
Ancestors
- builtins.tuple
Instance variables
var corner_radii : Point
-
The x- and y-radius of the corners.
var org : Point
-
The top-left corner of the rectangle.
var size : Point
-
The width and height of the rectangle.
class Transform (a: Union[int, float, decimal.Decimal], b: Union[int, float, decimal.Decimal], c: Union[int, float, decimal.Decimal], d: Union[int, float, decimal.Decimal], e: Union[int, float, decimal.Decimal], f: Union[int, float, decimal.Decimal])
-
A representation of an affine transformation matrix for 2D shapes.
The actual matrix is:
[ a b 0 ] [x' y' 1] = [x y 1] [ c d 0 ] [ e f 1 ]
Complex transformation operations can be composed via a sequence of simple transformations by performing successive matrix multiplication of the simple transformations.
For example, scaling a set of points around a specific center point can be represented by a translation-scale-translation sequence, where the first translation translates the center to the origin, the scale transform scales the points relative to the origin, and the second translation translates the points back to the specified center point. Transform multiplication is performed using python's dedicated matrix multiplication operator,
@
The semantics of this representation mean composed transformations are specified left-to-right in order of application (some other systems provide transposed representations, in which case the application order is right-to-left).
For example, to rotate the square
(1,1) (1,3) (3,3) (3,1)
45 degrees clockwise about its center point (which is(2,2)
) , the translate-rotate-translate process described above may be applied:rotate_centered = ( Transform.translation(-2, -2) @ Transform.rotation_d(45) @ Transform.translation(2, 2) )
Instances of this class provide a chaining API, so the above transform could also be constructed as follows:
rotate_centered = Transform.translation(-2, -2).rotate_d(45).translate(2, 2)
Or, because the particular operation of performing some transformations about a specific point is pretty common,
rotate_centered = Transform.rotation_d(45).about(2, 2)
By convention, this class provides class method constructors following noun-ish naming (
translation
,scaling
,rotation
,shearing
) and instance method manipulations following verb-ish naming (translate
,scale
,rotate
,shear
).Expand source code
class Transform(NamedTuple): """ A representation of an affine transformation matrix for 2D shapes. The actual matrix is: ``` [ a b 0 ] [x' y' 1] = [x y 1] [ c d 0 ] [ e f 1 ] ``` Complex transformation operations can be composed via a sequence of simple transformations by performing successive matrix multiplication of the simple transformations. For example, scaling a set of points around a specific center point can be represented by a translation-scale-translation sequence, where the first translation translates the center to the origin, the scale transform scales the points relative to the origin, and the second translation translates the points back to the specified center point. Transform multiplication is performed using python's dedicated matrix multiplication operator, `@` The semantics of this representation mean composed transformations are specified left-to-right in order of application (some other systems provide transposed representations, in which case the application order is right-to-left). For example, to rotate the square `(1,1) (1,3) (3,3) (3,1)` 45 degrees clockwise about its center point (which is `(2,2)`) , the translate-rotate-translate process described above may be applied: ```python rotate_centered = ( Transform.translation(-2, -2) @ Transform.rotation_d(45) @ Transform.translation(2, 2) ) ``` Instances of this class provide a chaining API, so the above transform could also be constructed as follows: ```python rotate_centered = Transform.translation(-2, -2).rotate_d(45).translate(2, 2) ``` Or, because the particular operation of performing some transformations about a specific point is pretty common, ```python rotate_centered = Transform.rotation_d(45).about(2, 2) ``` By convention, this class provides class method constructors following noun-ish naming (`translation`, `scaling`, `rotation`, `shearing`) and instance method manipulations following verb-ish naming (`translate`, `scale`, `rotate`, `shear`). """ a: Number b: Number c: Number d: Number e: Number f: Number # compact representation of an affine transformation matrix for 2D shapes. # The actual matrix is: # [ A B 0 ] # [x' y' 1] = [x y 1] [ C D 0 ] # [ E F 1 ] # The identity transform is 1 0 0 1 0 0 @classmethod def identity(cls): """ Create a transform representing the identity transform. The identity transform is a no-op. """ return cls(1, 0, 0, 1, 0, 0) @classmethod def translation(cls, x, y): """ Create a transform that performs translation. Args: x (Number): distance to translate points along the x (horizontal) axis. y (Number): distance to translate points along the y (vertical) axis. Returns: A Transform representing the specified translation. """ return cls(1, 0, 0, 1, x, y) @classmethod def scaling(cls, x, y=None): """ Create a transform that performs scaling. Args: x (Number): scaling ratio in the x (horizontal) axis. A value of 1 results in no scale change in the x axis. y (Number): optional scaling ratio in the y (vertical) axis. A value of 1 results in no scale change in the y axis. If this value is omitted, it defaults to the value provided to the `x` argument. Returns: A Transform representing the specified scaling. """ if y is None: y = x return cls(x, 0, 0, y, 0, 0) @classmethod def rotation(cls, theta): """ Create a transform that performs rotation. Args: theta (Number): the angle **in radians** by which to rotate. Positive values represent clockwise rotations. Returns: A Transform representing the specified rotation. """ return cls( math.cos(theta), math.sin(theta), -math.sin(theta), math.cos(theta), 0, 0 ) @classmethod def rotation_d(cls, theta_d): """ Create a transform that performs rotation **in degrees**. Args: theta_d (Number): the angle **in degrees** by which to rotate. Positive values represent clockwise rotations. Returns: A Transform representing the specified rotation. """ return cls.rotation(math.radians(theta_d)) @classmethod def shearing(cls, x, y=None): """ Create a transform that performs shearing (not of sheep). Args: x (Number): The amount to shear along the x (horizontal) axis. y (Number): Optional amount to shear along the y (vertical) axis. If omitted, this defaults to the value provided to the `x` argument. Returns: A Transform representing the specified shearing. """ if y is None: y = x return cls(1, y, x, 1, 0, 0) def translate(self, x, y): """ Produce a transform by composing the current transform with a translation. .. note:: Transforms are immutable, so this returns a new transform rather than mutating self. Args: x (Number): distance to translate points along the x (horizontal) axis. y (Number): distance to translate points along the y (vertical) axis. Returns: A Transform representing the composed transform. """ return self @ Transform.translation(x, y) def scale(self, x, y=None): """ Produce a transform by composing the current transform with a scaling. .. note:: Transforms are immutable, so this returns a new transform rather than mutating self. Args: x (Number): scaling ratio in the x (horizontal) axis. A value of 1 results in no scale change in the x axis. y (Number): optional scaling ratio in the y (vertical) axis. A value of 1 results in no scale change in the y axis. If this value is omitted, it defaults to the value provided to the `x` argument. Returns: A Transform representing the composed transform. """ return self @ Transform.scaling(x, y) def rotate(self, theta): """ Produce a transform by composing the current transform with a rotation. .. note:: Transforms are immutable, so this returns a new transform rather than mutating self. Args: theta (Number): the angle **in radians** by which to rotate. Positive values represent clockwise rotations. Returns: A Transform representing the composed transform. """ return self @ Transform.rotation(theta) def rotate_d(self, theta_d): """ Produce a transform by composing the current transform with a rotation **in degrees**. .. note:: Transforms are immutable, so this returns a new transform rather than mutating self. Args: theta_d (Number): the angle **in degrees** by which to rotate. Positive values represent clockwise rotations. Returns: A Transform representing the composed transform. """ return self @ Transform.rotation_d(theta_d) def shear(self, x, y=None): """ Produce a transform by composing the current transform with a shearing. .. note:: Transforms are immutable, so this returns a new transform rather than mutating self. Args: x (Number): The amount to shear along the x (horizontal) axis. y (Number): Optional amount to shear along the y (vertical) axis. If omitted, this defaults to the value provided to the `x` argument. Returns: A Transform representing the composed transform. """ return self @ Transform.shearing(x, y) def about(self, x, y): """ Bracket the given transform in a pair of translations to make it appear about a point that isn't the origin. This is a useful shorthand for performing a transform like a rotation around the center point of an object that isn't centered at the origin. .. note:: Transforms are immutable, so this returns a new transform rather than mutating self. Args: x (Number): the point along the x (horizontal) axis about which to transform. y (Number): the point along the y (vertical) axis about which to transform. Returns: A Transform representing the composed transform. """ return Transform.translation(-x, -y) @ self @ Transform.translation(x, y) @force_document def __mul__(self, other): """ Multiply the individual transform parameters by a scalar value. Args: other (Number): the scalar value by which to multiply the parameters Returns: A Transform with the modified parameters. """ if isinstance(other, NumberClass): return Transform( a=self.a * other, b=self.b * other, c=self.c * other, d=self.d * other, e=self.e * other, f=self.f * other, ) return NotImplemented # scalar multiplication is commutative __rmul__ = __mul__ @force_document def __matmul__(self, other): """ Compose two transforms into a single transform. Args: other (Transform): the right-hand side transform of the infix operator. Returns: A Transform representing the composed transform. """ if isinstance(other, Transform): return self.__class__( a=self.a * other.a + self.b * other.c, b=self.a * other.b + self.b * other.d, c=self.c * other.a + self.d * other.c, d=self.c * other.b + self.d * other.d, e=self.e * other.a + self.f * other.c + other.e, f=self.e * other.b + self.f * other.d + other.f, ) return NotImplemented def render(self, last_item): """ Render the transform to its PDF output representation. Args: last_item: the last path element this transform applies to Returns: A tuple of `(str, last_item)`. `last_item` is returned unchanged. """ return ( f"{number_to_str(self.a)} {number_to_str(self.b)} " f"{number_to_str(self.c)} {number_to_str(self.d)} " f"{number_to_str(self.e)} {number_to_str(self.f)} cm", last_item, ) def __str__(self): return ( f"transform: [" f"{number_to_str(self.a)} {number_to_str(self.b)} 0; " f"{number_to_str(self.c)} {number_to_str(self.d)} 0; " f"{number_to_str(self.e)} {number_to_str(self.f)} 1]" )
Ancestors
- builtins.tuple
Static methods
def identity()
-
Create a transform representing the identity transform.
The identity transform is a no-op.
Expand source code
@classmethod def identity(cls): """ Create a transform representing the identity transform. The identity transform is a no-op. """ return cls(1, 0, 0, 1, 0, 0)
def rotation(theta)
-
Create a transform that performs rotation.
Args
theta
:Number
- the angle in radians by which to rotate. Positive values represent clockwise rotations.
Returns
A Transform representing the specified rotation.
Expand source code
@classmethod def rotation(cls, theta): """ Create a transform that performs rotation. Args: theta (Number): the angle **in radians** by which to rotate. Positive values represent clockwise rotations. Returns: A Transform representing the specified rotation. """ return cls( math.cos(theta), math.sin(theta), -math.sin(theta), math.cos(theta), 0, 0 )
def rotation_d(theta_d)
-
Create a transform that performs rotation in degrees.
Args
theta_d
:Number
- the angle in degrees by which to rotate. Positive values represent clockwise rotations.
Returns
A Transform representing the specified rotation.
Expand source code
@classmethod def rotation_d(cls, theta_d): """ Create a transform that performs rotation **in degrees**. Args: theta_d (Number): the angle **in degrees** by which to rotate. Positive values represent clockwise rotations. Returns: A Transform representing the specified rotation. """ return cls.rotation(math.radians(theta_d))
def scaling(x, y=None)
-
Create a transform that performs scaling.
Args
x
:Number
- scaling ratio in the x (horizontal) axis. A value of 1 results in no scale change in the x axis.
y
:Number
- optional scaling ratio in the y (vertical) axis. A value of 1
results in no scale change in the y axis. If this value is omitted, it
defaults to the value provided to the
x
argument.
Returns
A Transform representing the specified scaling.
Expand source code
@classmethod def scaling(cls, x, y=None): """ Create a transform that performs scaling. Args: x (Number): scaling ratio in the x (horizontal) axis. A value of 1 results in no scale change in the x axis. y (Number): optional scaling ratio in the y (vertical) axis. A value of 1 results in no scale change in the y axis. If this value is omitted, it defaults to the value provided to the `x` argument. Returns: A Transform representing the specified scaling. """ if y is None: y = x return cls(x, 0, 0, y, 0, 0)
def shearing(x, y=None)
-
Create a transform that performs shearing (not of sheep).
Args
x
:Number
- The amount to shear along the x (horizontal) axis.
y
:Number
- Optional amount to shear along the y (vertical) axis. If omitted,
this defaults to the value provided to the
x
argument.
Returns
A Transform representing the specified shearing.
Expand source code
@classmethod def shearing(cls, x, y=None): """ Create a transform that performs shearing (not of sheep). Args: x (Number): The amount to shear along the x (horizontal) axis. y (Number): Optional amount to shear along the y (vertical) axis. If omitted, this defaults to the value provided to the `x` argument. Returns: A Transform representing the specified shearing. """ if y is None: y = x return cls(1, y, x, 1, 0, 0)
def translation(x, y)
-
Create a transform that performs translation.
Args
x
:Number
- distance to translate points along the x (horizontal) axis.
y
:Number
- distance to translate points along the y (vertical) axis.
Returns
A Transform representing the specified translation.
Expand source code
@classmethod def translation(cls, x, y): """ Create a transform that performs translation. Args: x (Number): distance to translate points along the x (horizontal) axis. y (Number): distance to translate points along the y (vertical) axis. Returns: A Transform representing the specified translation. """ return cls(1, 0, 0, 1, x, y)
Methods
def __matmul__(self, other)
-
Compose two transforms into a single transform.
Args
other
:Transform
- the right-hand side transform of the infix operator.
Returns
A Transform representing the composed transform.
Expand source code
@force_document def __matmul__(self, other): """ Compose two transforms into a single transform. Args: other (Transform): the right-hand side transform of the infix operator. Returns: A Transform representing the composed transform. """ if isinstance(other, Transform): return self.__class__( a=self.a * other.a + self.b * other.c, b=self.a * other.b + self.b * other.d, c=self.c * other.a + self.d * other.c, d=self.c * other.b + self.d * other.d, e=self.e * other.a + self.f * other.c + other.e, f=self.e * other.b + self.f * other.d + other.f, ) return NotImplemented
def __mul__(self, other)
-
Multiply the individual transform parameters by a scalar value.
Args
other
:Number
- the scalar value by which to multiply the parameters
Returns
A Transform with the modified parameters.
Expand source code
@force_document def __mul__(self, other): """ Multiply the individual transform parameters by a scalar value. Args: other (Number): the scalar value by which to multiply the parameters Returns: A Transform with the modified parameters. """ if isinstance(other, NumberClass): return Transform( a=self.a * other, b=self.b * other, c=self.c * other, d=self.d * other, e=self.e * other, f=self.f * other, ) return NotImplemented
def about(self, x, y)
-
Bracket the given transform in a pair of translations to make it appear about a point that isn't the origin.
This is a useful shorthand for performing a transform like a rotation around the center point of an object that isn't centered at the origin.
Note
Transforms are immutable, so this returns a new transform rather than mutating self.
Args
x
:Number
- the point along the x (horizontal) axis about which to transform.
y
:Number
- the point along the y (vertical) axis about which to transform.
Returns
A Transform representing the composed transform.
Expand source code
def about(self, x, y): """ Bracket the given transform in a pair of translations to make it appear about a point that isn't the origin. This is a useful shorthand for performing a transform like a rotation around the center point of an object that isn't centered at the origin. .. note:: Transforms are immutable, so this returns a new transform rather than mutating self. Args: x (Number): the point along the x (horizontal) axis about which to transform. y (Number): the point along the y (vertical) axis about which to transform. Returns: A Transform representing the composed transform. """ return Transform.translation(-x, -y) @ self @ Transform.translation(x, y)
def render(self, last_item)
-
Render the transform to its PDF output representation.
Args
last_item
- the last path element this transform applies to
Returns
A tuple of
(str, last_item)
.last_item
is returned unchanged.Expand source code
def render(self, last_item): """ Render the transform to its PDF output representation. Args: last_item: the last path element this transform applies to Returns: A tuple of `(str, last_item)`. `last_item` is returned unchanged. """ return ( f"{number_to_str(self.a)} {number_to_str(self.b)} " f"{number_to_str(self.c)} {number_to_str(self.d)} " f"{number_to_str(self.e)} {number_to_str(self.f)} cm", last_item, )
def rotate(self, theta)
-
Produce a transform by composing the current transform with a rotation.
Note
Transforms are immutable, so this returns a new transform rather than mutating self.
Args
theta
:Number
- the angle in radians by which to rotate. Positive values represent clockwise rotations.
Returns
A Transform representing the composed transform.
Expand source code
def rotate(self, theta): """ Produce a transform by composing the current transform with a rotation. .. note:: Transforms are immutable, so this returns a new transform rather than mutating self. Args: theta (Number): the angle **in radians** by which to rotate. Positive values represent clockwise rotations. Returns: A Transform representing the composed transform. """ return self @ Transform.rotation(theta)
def rotate_d(self, theta_d)
-
Produce a transform by composing the current transform with a rotation in degrees.
Note
Transforms are immutable, so this returns a new transform rather than mutating self.
Args
theta_d
:Number
- the angle in degrees by which to rotate. Positive values represent clockwise rotations.
Returns
A Transform representing the composed transform.
Expand source code
def rotate_d(self, theta_d): """ Produce a transform by composing the current transform with a rotation **in degrees**. .. note:: Transforms are immutable, so this returns a new transform rather than mutating self. Args: theta_d (Number): the angle **in degrees** by which to rotate. Positive values represent clockwise rotations. Returns: A Transform representing the composed transform. """ return self @ Transform.rotation_d(theta_d)
def scale(self, x, y=None)
-
Produce a transform by composing the current transform with a scaling.
Note
Transforms are immutable, so this returns a new transform rather than mutating self.
Args
x
:Number
- scaling ratio in the x (horizontal) axis. A value of 1 results in no scale change in the x axis.
y
:Number
- optional scaling ratio in the y (vertical) axis. A value of 1
results in no scale change in the y axis. If this value is omitted, it
defaults to the value provided to the
x
argument.
Returns
A Transform representing the composed transform.
Expand source code
def scale(self, x, y=None): """ Produce a transform by composing the current transform with a scaling. .. note:: Transforms are immutable, so this returns a new transform rather than mutating self. Args: x (Number): scaling ratio in the x (horizontal) axis. A value of 1 results in no scale change in the x axis. y (Number): optional scaling ratio in the y (vertical) axis. A value of 1 results in no scale change in the y axis. If this value is omitted, it defaults to the value provided to the `x` argument. Returns: A Transform representing the composed transform. """ return self @ Transform.scaling(x, y)
def shear(self, x, y=None)
-
Produce a transform by composing the current transform with a shearing.
Note
Transforms are immutable, so this returns a new transform rather than mutating self.
Args
x
:Number
- The amount to shear along the x (horizontal) axis.
y
:Number
- Optional amount to shear along the y (vertical) axis. If omitted,
this defaults to the value provided to the
x
argument.
Returns
A Transform representing the composed transform.
Expand source code
def shear(self, x, y=None): """ Produce a transform by composing the current transform with a shearing. .. note:: Transforms are immutable, so this returns a new transform rather than mutating self. Args: x (Number): The amount to shear along the x (horizontal) axis. y (Number): Optional amount to shear along the y (vertical) axis. If omitted, this defaults to the value provided to the `x` argument. Returns: A Transform representing the composed transform. """ return self @ Transform.shearing(x, y)
def translate(self, x, y)
-
Produce a transform by composing the current transform with a translation.
Note
Transforms are immutable, so this returns a new transform rather than mutating self.
Args
x
:Number
- distance to translate points along the x (horizontal) axis.
y
:Number
- distance to translate points along the y (vertical) axis.
Returns
A Transform representing the composed transform.
Expand source code
def translate(self, x, y): """ Produce a transform by composing the current transform with a translation. .. note:: Transforms are immutable, so this returns a new transform rather than mutating self. Args: x (Number): distance to translate points along the x (horizontal) axis. y (Number): distance to translate points along the y (vertical) axis. Returns: A Transform representing the composed transform. """ return self @ Transform.translation(x, y)
class VerticalLine (y: Union[int, float, decimal.Decimal])
-
A path line element that takes its abscissa from the end of the previous element.
Expand source code
class VerticalLine(NamedTuple): """ A path line element that takes its abscissa from the end of the previous element. See: `PaintedPath.vertical_line_to` """ y: Number """The ordinate of the vertical line's end point.""" @force_nodocument def render(self, gsd_registry, style, last_item, initial_point): """ Render this path element to its PDF representation. Args: gsd_registry (GraphicsStateDictRegistry): the owner's graphics state dictionary registry. style (GraphicsStyle): the current resolved graphics style last_item: the previous path element. initial_point: last position set by a "M" or "m" command Returns: a tuple of `(str, new_last_item)`, where `new_last_item` is the resolved `Line`. """ # pylint: disable=unused-argument end_point = Point(x=last_item.end_point.x, y=self.y) return _render_line(end_point), Line(end_point), initial_point @force_nodocument def render_debug( self, gsd_registry, style, last_item, initial_point, debug_stream, pfx ): """ Render this path element to its PDF representation and produce debug information. Args: gsd_registry (GraphicsStateDictRegistry): the owner's graphics state dictionary registry. style (GraphicsStyle): the current resolved graphics style last_item: the previous path element. initial_point: last position set by a "M" or "m" command debug_stream (io.TextIO): the stream to which the debug output should be written. This is not guaranteed to be seekable (e.g. it may be stdout or stderr). pfx (str): the current debug output prefix string (only needed if emitting more than one line). Returns: The same tuple as `VerticalLine.render`. """ # pylint: disable=unused-argument rendered, resolved, initial_point = self.render( gsd_registry, style, last_item, initial_point ) debug_stream.write(f"{self} resolved to {resolved}\n") return rendered, resolved, initial_point
Ancestors
- builtins.tuple
Instance variables
var y : Union[int, float, decimal.Decimal]
-
The ordinate of the vertical line's end point.