aboutsummaryrefslogtreecommitdiff
path: root/Lib/fontTools/misc/transform.py
diff options
context:
space:
mode:
Diffstat (limited to 'Lib/fontTools/misc/transform.py')
-rw-r--r--Lib/fontTools/misc/transform.py739
1 files changed, 418 insertions, 321 deletions
diff --git a/Lib/fontTools/misc/transform.py b/Lib/fontTools/misc/transform.py
index 94e1f622..f85b54b7 100644
--- a/Lib/fontTools/misc/transform.py
+++ b/Lib/fontTools/misc/transform.py
@@ -19,6 +19,9 @@ Offset
Scale
Convenience function that returns a scaling transformation
+The DecomposedTransform class implements a transformation with separate
+translate, rotation, scale, skew, and transformation-center components.
+
:Example:
>>> t = Transform(2, 0, 0, 3, 0, 0)
@@ -49,10 +52,12 @@ Scale
>>>
"""
+import math
from typing import NamedTuple
+from dataclasses import dataclass
-__all__ = ["Transform", "Identity", "Offset", "Scale"]
+__all__ = ["Transform", "Identity", "Offset", "Scale", "DecomposedTransform"]
_EPSILON = 1e-15
@@ -61,338 +66,430 @@ _MINUS_ONE_EPSILON = -1 + _EPSILON
def _normSinCos(v):
- if abs(v) < _EPSILON:
- v = 0
- elif v > _ONE_EPSILON:
- v = 1
- elif v < _MINUS_ONE_EPSILON:
- v = -1
- return v
+ if abs(v) < _EPSILON:
+ v = 0
+ elif v > _ONE_EPSILON:
+ v = 1
+ elif v < _MINUS_ONE_EPSILON:
+ v = -1
+ return v
class Transform(NamedTuple):
- """2x2 transformation matrix plus offset, a.k.a. Affine transform.
- Transform instances are immutable: all transforming methods, eg.
- rotate(), return a new Transform instance.
-
- :Example:
-
- >>> t = Transform()
- >>> t
- <Transform [1 0 0 1 0 0]>
- >>> t.scale(2)
- <Transform [2 0 0 2 0 0]>
- >>> t.scale(2.5, 5.5)
- <Transform [2.5 0 0 5.5 0 0]>
- >>>
- >>> t.scale(2, 3).transformPoint((100, 100))
- (200, 300)
-
- Transform's constructor takes six arguments, all of which are
- optional, and can be used as keyword arguments::
-
- >>> Transform(12)
- <Transform [12 0 0 1 0 0]>
- >>> Transform(dx=12)
- <Transform [1 0 0 1 12 0]>
- >>> Transform(yx=12)
- <Transform [1 0 12 1 0 0]>
-
- Transform instances also behave like sequences of length 6::
-
- >>> len(Identity)
- 6
- >>> list(Identity)
- [1, 0, 0, 1, 0, 0]
- >>> tuple(Identity)
- (1, 0, 0, 1, 0, 0)
-
- Transform instances are comparable::
-
- >>> t1 = Identity.scale(2, 3).translate(4, 6)
- >>> t2 = Identity.translate(8, 18).scale(2, 3)
- >>> t1 == t2
- 1
-
- But beware of floating point rounding errors::
-
- >>> t1 = Identity.scale(0.2, 0.3).translate(0.4, 0.6)
- >>> t2 = Identity.translate(0.08, 0.18).scale(0.2, 0.3)
- >>> t1
- <Transform [0.2 0 0 0.3 0.08 0.18]>
- >>> t2
- <Transform [0.2 0 0 0.3 0.08 0.18]>
- >>> t1 == t2
- 0
-
- Transform instances are hashable, meaning you can use them as
- keys in dictionaries::
-
- >>> d = {Scale(12, 13): None}
- >>> d
- {<Transform [12 0 0 13 0 0]>: None}
-
- But again, beware of floating point rounding errors::
-
- >>> t1 = Identity.scale(0.2, 0.3).translate(0.4, 0.6)
- >>> t2 = Identity.translate(0.08, 0.18).scale(0.2, 0.3)
- >>> t1
- <Transform [0.2 0 0 0.3 0.08 0.18]>
- >>> t2
- <Transform [0.2 0 0 0.3 0.08 0.18]>
- >>> d = {t1: None}
- >>> d
- {<Transform [0.2 0 0 0.3 0.08 0.18]>: None}
- >>> d[t2]
- Traceback (most recent call last):
- File "<stdin>", line 1, in ?
- KeyError: <Transform [0.2 0 0 0.3 0.08 0.18]>
- """
-
- xx: float = 1
- xy: float = 0
- yx: float = 0
- yy: float = 1
- dx: float = 0
- dy: float = 0
-
- def transformPoint(self, p):
- """Transform a point.
-
- :Example:
-
- >>> t = Transform()
- >>> t = t.scale(2.5, 5.5)
- >>> t.transformPoint((100, 100))
- (250.0, 550.0)
- """
- (x, y) = p
- xx, xy, yx, yy, dx, dy = self
- return (xx*x + yx*y + dx, xy*x + yy*y + dy)
-
- def transformPoints(self, points):
- """Transform a list of points.
-
- :Example:
-
- >>> t = Scale(2, 3)
- >>> t.transformPoints([(0, 0), (0, 100), (100, 100), (100, 0)])
- [(0, 0), (0, 300), (200, 300), (200, 0)]
- >>>
- """
- xx, xy, yx, yy, dx, dy = self
- return [(xx*x + yx*y + dx, xy*x + yy*y + dy) for x, y in points]
-
- def transformVector(self, v):
- """Transform an (dx, dy) vector, treating translation as zero.
-
- :Example:
-
- >>> t = Transform(2, 0, 0, 2, 10, 20)
- >>> t.transformVector((3, -4))
- (6, -8)
- >>>
- """
- (dx, dy) = v
- xx, xy, yx, yy = self[:4]
- return (xx*dx + yx*dy, xy*dx + yy*dy)
-
- def transformVectors(self, vectors):
- """Transform a list of (dx, dy) vector, treating translation as zero.
-
- :Example:
- >>> t = Transform(2, 0, 0, 2, 10, 20)
- >>> t.transformVectors([(3, -4), (5, -6)])
- [(6, -8), (10, -12)]
- >>>
- """
- xx, xy, yx, yy = self[:4]
- return [(xx*dx + yx*dy, xy*dx + yy*dy) for dx, dy in vectors]
-
- def translate(self, x=0, y=0):
- """Return a new transformation, translated (offset) by x, y.
-
- :Example:
- >>> t = Transform()
- >>> t.translate(20, 30)
- <Transform [1 0 0 1 20 30]>
- >>>
- """
- return self.transform((1, 0, 0, 1, x, y))
-
- def scale(self, x=1, y=None):
- """Return a new transformation, scaled by x, y. The 'y' argument
- may be None, which implies to use the x value for y as well.
-
- :Example:
- >>> t = Transform()
- >>> t.scale(5)
- <Transform [5 0 0 5 0 0]>
- >>> t.scale(5, 6)
- <Transform [5 0 0 6 0 0]>
- >>>
- """
- if y is None:
- y = x
- return self.transform((x, 0, 0, y, 0, 0))
-
- def rotate(self, angle):
- """Return a new transformation, rotated by 'angle' (radians).
-
- :Example:
- >>> import math
- >>> t = Transform()
- >>> t.rotate(math.pi / 2)
- <Transform [0 1 -1 0 0 0]>
- >>>
- """
- import math
- c = _normSinCos(math.cos(angle))
- s = _normSinCos(math.sin(angle))
- return self.transform((c, s, -s, c, 0, 0))
-
- def skew(self, x=0, y=0):
- """Return a new transformation, skewed by x and y.
-
- :Example:
- >>> import math
- >>> t = Transform()
- >>> t.skew(math.pi / 4)
- <Transform [1 0 1 1 0 0]>
- >>>
- """
- import math
- return self.transform((1, math.tan(y), math.tan(x), 1, 0, 0))
-
- def transform(self, other):
- """Return a new transformation, transformed by another
- transformation.
-
- :Example:
- >>> t = Transform(2, 0, 0, 3, 1, 6)
- >>> t.transform((4, 3, 2, 1, 5, 6))
- <Transform [8 9 4 3 11 24]>
- >>>
- """
- xx1, xy1, yx1, yy1, dx1, dy1 = other
- xx2, xy2, yx2, yy2, dx2, dy2 = self
- return self.__class__(
- xx1*xx2 + xy1*yx2,
- xx1*xy2 + xy1*yy2,
- yx1*xx2 + yy1*yx2,
- yx1*xy2 + yy1*yy2,
- xx2*dx1 + yx2*dy1 + dx2,
- xy2*dx1 + yy2*dy1 + dy2)
-
- def reverseTransform(self, other):
- """Return a new transformation, which is the other transformation
- transformed by self. self.reverseTransform(other) is equivalent to
- other.transform(self).
-
- :Example:
- >>> t = Transform(2, 0, 0, 3, 1, 6)
- >>> t.reverseTransform((4, 3, 2, 1, 5, 6))
- <Transform [8 6 6 3 21 15]>
- >>> Transform(4, 3, 2, 1, 5, 6).transform((2, 0, 0, 3, 1, 6))
- <Transform [8 6 6 3 21 15]>
- >>>
- """
- xx1, xy1, yx1, yy1, dx1, dy1 = self
- xx2, xy2, yx2, yy2, dx2, dy2 = other
- return self.__class__(
- xx1*xx2 + xy1*yx2,
- xx1*xy2 + xy1*yy2,
- yx1*xx2 + yy1*yx2,
- yx1*xy2 + yy1*yy2,
- xx2*dx1 + yx2*dy1 + dx2,
- xy2*dx1 + yy2*dy1 + dy2)
-
- def inverse(self):
- """Return the inverse transformation.
-
- :Example:
- >>> t = Identity.translate(2, 3).scale(4, 5)
- >>> t.transformPoint((10, 20))
- (42, 103)
- >>> it = t.inverse()
- >>> it.transformPoint((42, 103))
- (10.0, 20.0)
- >>>
- """
- if self == Identity:
- return self
- xx, xy, yx, yy, dx, dy = self
- det = xx*yy - yx*xy
- xx, xy, yx, yy = yy/det, -xy/det, -yx/det, xx/det
- dx, dy = -xx*dx - yx*dy, -xy*dx - yy*dy
- return self.__class__(xx, xy, yx, yy, dx, dy)
-
- def toPS(self):
- """Return a PostScript representation
-
- :Example:
-
- >>> t = Identity.scale(2, 3).translate(4, 5)
- >>> t.toPS()
- '[2 0 0 3 8 15]'
- >>>
- """
- return "[%s %s %s %s %s %s]" % self
-
- def __bool__(self):
- """Returns True if transform is not identity, False otherwise.
-
- :Example:
-
- >>> bool(Identity)
- False
- >>> bool(Transform())
- False
- >>> bool(Scale(1.))
- False
- >>> bool(Scale(2))
- True
- >>> bool(Offset())
- False
- >>> bool(Offset(0))
- False
- >>> bool(Offset(2))
- True
- """
- return self != Identity
-
- def __repr__(self):
- return "<%s [%g %g %g %g %g %g]>" % ((self.__class__.__name__,) + self)
+ """2x2 transformation matrix plus offset, a.k.a. Affine transform.
+ Transform instances are immutable: all transforming methods, eg.
+ rotate(), return a new Transform instance.
+
+ :Example:
+
+ >>> t = Transform()
+ >>> t
+ <Transform [1 0 0 1 0 0]>
+ >>> t.scale(2)
+ <Transform [2 0 0 2 0 0]>
+ >>> t.scale(2.5, 5.5)
+ <Transform [2.5 0 0 5.5 0 0]>
+ >>>
+ >>> t.scale(2, 3).transformPoint((100, 100))
+ (200, 300)
+
+ Transform's constructor takes six arguments, all of which are
+ optional, and can be used as keyword arguments::
+
+ >>> Transform(12)
+ <Transform [12 0 0 1 0 0]>
+ >>> Transform(dx=12)
+ <Transform [1 0 0 1 12 0]>
+ >>> Transform(yx=12)
+ <Transform [1 0 12 1 0 0]>
+
+ Transform instances also behave like sequences of length 6::
+
+ >>> len(Identity)
+ 6
+ >>> list(Identity)
+ [1, 0, 0, 1, 0, 0]
+ >>> tuple(Identity)
+ (1, 0, 0, 1, 0, 0)
+
+ Transform instances are comparable::
+
+ >>> t1 = Identity.scale(2, 3).translate(4, 6)
+ >>> t2 = Identity.translate(8, 18).scale(2, 3)
+ >>> t1 == t2
+ 1
+
+ But beware of floating point rounding errors::
+
+ >>> t1 = Identity.scale(0.2, 0.3).translate(0.4, 0.6)
+ >>> t2 = Identity.translate(0.08, 0.18).scale(0.2, 0.3)
+ >>> t1
+ <Transform [0.2 0 0 0.3 0.08 0.18]>
+ >>> t2
+ <Transform [0.2 0 0 0.3 0.08 0.18]>
+ >>> t1 == t2
+ 0
+
+ Transform instances are hashable, meaning you can use them as
+ keys in dictionaries::
+
+ >>> d = {Scale(12, 13): None}
+ >>> d
+ {<Transform [12 0 0 13 0 0]>: None}
+
+ But again, beware of floating point rounding errors::
+
+ >>> t1 = Identity.scale(0.2, 0.3).translate(0.4, 0.6)
+ >>> t2 = Identity.translate(0.08, 0.18).scale(0.2, 0.3)
+ >>> t1
+ <Transform [0.2 0 0 0.3 0.08 0.18]>
+ >>> t2
+ <Transform [0.2 0 0 0.3 0.08 0.18]>
+ >>> d = {t1: None}
+ >>> d
+ {<Transform [0.2 0 0 0.3 0.08 0.18]>: None}
+ >>> d[t2]
+ Traceback (most recent call last):
+ File "<stdin>", line 1, in ?
+ KeyError: <Transform [0.2 0 0 0.3 0.08 0.18]>
+ """
+
+ xx: float = 1
+ xy: float = 0
+ yx: float = 0
+ yy: float = 1
+ dx: float = 0
+ dy: float = 0
+
+ def transformPoint(self, p):
+ """Transform a point.
+
+ :Example:
+
+ >>> t = Transform()
+ >>> t = t.scale(2.5, 5.5)
+ >>> t.transformPoint((100, 100))
+ (250.0, 550.0)
+ """
+ (x, y) = p
+ xx, xy, yx, yy, dx, dy = self
+ return (xx * x + yx * y + dx, xy * x + yy * y + dy)
+
+ def transformPoints(self, points):
+ """Transform a list of points.
+
+ :Example:
+
+ >>> t = Scale(2, 3)
+ >>> t.transformPoints([(0, 0), (0, 100), (100, 100), (100, 0)])
+ [(0, 0), (0, 300), (200, 300), (200, 0)]
+ >>>
+ """
+ xx, xy, yx, yy, dx, dy = self
+ return [(xx * x + yx * y + dx, xy * x + yy * y + dy) for x, y in points]
+
+ def transformVector(self, v):
+ """Transform an (dx, dy) vector, treating translation as zero.
+
+ :Example:
+
+ >>> t = Transform(2, 0, 0, 2, 10, 20)
+ >>> t.transformVector((3, -4))
+ (6, -8)
+ >>>
+ """
+ (dx, dy) = v
+ xx, xy, yx, yy = self[:4]
+ return (xx * dx + yx * dy, xy * dx + yy * dy)
+
+ def transformVectors(self, vectors):
+ """Transform a list of (dx, dy) vector, treating translation as zero.
+
+ :Example:
+ >>> t = Transform(2, 0, 0, 2, 10, 20)
+ >>> t.transformVectors([(3, -4), (5, -6)])
+ [(6, -8), (10, -12)]
+ >>>
+ """
+ xx, xy, yx, yy = self[:4]
+ return [(xx * dx + yx * dy, xy * dx + yy * dy) for dx, dy in vectors]
+
+ def translate(self, x=0, y=0):
+ """Return a new transformation, translated (offset) by x, y.
+
+ :Example:
+ >>> t = Transform()
+ >>> t.translate(20, 30)
+ <Transform [1 0 0 1 20 30]>
+ >>>
+ """
+ return self.transform((1, 0, 0, 1, x, y))
+
+ def scale(self, x=1, y=None):
+ """Return a new transformation, scaled by x, y. The 'y' argument
+ may be None, which implies to use the x value for y as well.
+
+ :Example:
+ >>> t = Transform()
+ >>> t.scale(5)
+ <Transform [5 0 0 5 0 0]>
+ >>> t.scale(5, 6)
+ <Transform [5 0 0 6 0 0]>
+ >>>
+ """
+ if y is None:
+ y = x
+ return self.transform((x, 0, 0, y, 0, 0))
+
+ def rotate(self, angle):
+ """Return a new transformation, rotated by 'angle' (radians).
+
+ :Example:
+ >>> import math
+ >>> t = Transform()
+ >>> t.rotate(math.pi / 2)
+ <Transform [0 1 -1 0 0 0]>
+ >>>
+ """
+ import math
+
+ c = _normSinCos(math.cos(angle))
+ s = _normSinCos(math.sin(angle))
+ return self.transform((c, s, -s, c, 0, 0))
+
+ def skew(self, x=0, y=0):
+ """Return a new transformation, skewed by x and y.
+
+ :Example:
+ >>> import math
+ >>> t = Transform()
+ >>> t.skew(math.pi / 4)
+ <Transform [1 0 1 1 0 0]>
+ >>>
+ """
+ import math
+
+ return self.transform((1, math.tan(y), math.tan(x), 1, 0, 0))
+
+ def transform(self, other):
+ """Return a new transformation, transformed by another
+ transformation.
+
+ :Example:
+ >>> t = Transform(2, 0, 0, 3, 1, 6)
+ >>> t.transform((4, 3, 2, 1, 5, 6))
+ <Transform [8 9 4 3 11 24]>
+ >>>
+ """
+ xx1, xy1, yx1, yy1, dx1, dy1 = other
+ xx2, xy2, yx2, yy2, dx2, dy2 = self
+ return self.__class__(
+ xx1 * xx2 + xy1 * yx2,
+ xx1 * xy2 + xy1 * yy2,
+ yx1 * xx2 + yy1 * yx2,
+ yx1 * xy2 + yy1 * yy2,
+ xx2 * dx1 + yx2 * dy1 + dx2,
+ xy2 * dx1 + yy2 * dy1 + dy2,
+ )
+
+ def reverseTransform(self, other):
+ """Return a new transformation, which is the other transformation
+ transformed by self. self.reverseTransform(other) is equivalent to
+ other.transform(self).
+
+ :Example:
+ >>> t = Transform(2, 0, 0, 3, 1, 6)
+ >>> t.reverseTransform((4, 3, 2, 1, 5, 6))
+ <Transform [8 6 6 3 21 15]>
+ >>> Transform(4, 3, 2, 1, 5, 6).transform((2, 0, 0, 3, 1, 6))
+ <Transform [8 6 6 3 21 15]>
+ >>>
+ """
+ xx1, xy1, yx1, yy1, dx1, dy1 = self
+ xx2, xy2, yx2, yy2, dx2, dy2 = other
+ return self.__class__(
+ xx1 * xx2 + xy1 * yx2,
+ xx1 * xy2 + xy1 * yy2,
+ yx1 * xx2 + yy1 * yx2,
+ yx1 * xy2 + yy1 * yy2,
+ xx2 * dx1 + yx2 * dy1 + dx2,
+ xy2 * dx1 + yy2 * dy1 + dy2,
+ )
+
+ def inverse(self):
+ """Return the inverse transformation.
+
+ :Example:
+ >>> t = Identity.translate(2, 3).scale(4, 5)
+ >>> t.transformPoint((10, 20))
+ (42, 103)
+ >>> it = t.inverse()
+ >>> it.transformPoint((42, 103))
+ (10.0, 20.0)
+ >>>
+ """
+ if self == Identity:
+ return self
+ xx, xy, yx, yy, dx, dy = self
+ det = xx * yy - yx * xy
+ xx, xy, yx, yy = yy / det, -xy / det, -yx / det, xx / det
+ dx, dy = -xx * dx - yx * dy, -xy * dx - yy * dy
+ return self.__class__(xx, xy, yx, yy, dx, dy)
+
+ def toPS(self):
+ """Return a PostScript representation
+
+ :Example:
+
+ >>> t = Identity.scale(2, 3).translate(4, 5)
+ >>> t.toPS()
+ '[2 0 0 3 8 15]'
+ >>>
+ """
+ return "[%s %s %s %s %s %s]" % self
+
+ def toDecomposed(self) -> "DecomposedTransform":
+ """Decompose into a DecomposedTransform."""
+ return DecomposedTransform.fromTransform(self)
+
+ def __bool__(self):
+ """Returns True if transform is not identity, False otherwise.
+
+ :Example:
+
+ >>> bool(Identity)
+ False
+ >>> bool(Transform())
+ False
+ >>> bool(Scale(1.))
+ False
+ >>> bool(Scale(2))
+ True
+ >>> bool(Offset())
+ False
+ >>> bool(Offset(0))
+ False
+ >>> bool(Offset(2))
+ True
+ """
+ return self != Identity
+
+ def __repr__(self):
+ return "<%s [%g %g %g %g %g %g]>" % ((self.__class__.__name__,) + self)
Identity = Transform()
+
def Offset(x=0, y=0):
- """Return the identity transformation offset by x, y.
+ """Return the identity transformation offset by x, y.
- :Example:
- >>> Offset(2, 3)
- <Transform [1 0 0 1 2 3]>
- >>>
- """
- return Transform(1, 0, 0, 1, x, y)
+ :Example:
+ >>> Offset(2, 3)
+ <Transform [1 0 0 1 2 3]>
+ >>>
+ """
+ return Transform(1, 0, 0, 1, x, y)
-def Scale(x, y=None):
- """Return the identity transformation scaled by x, y. The 'y' argument
- may be None, which implies to use the x value for y as well.
- :Example:
- >>> Scale(2, 3)
- <Transform [2 0 0 3 0 0]>
- >>>
- """
- if y is None:
- y = x
- return Transform(x, 0, 0, y, 0, 0)
+def Scale(x, y=None):
+ """Return the identity transformation scaled by x, y. The 'y' argument
+ may be None, which implies to use the x value for y as well.
+
+ :Example:
+ >>> Scale(2, 3)
+ <Transform [2 0 0 3 0 0]>
+ >>>
+ """
+ if y is None:
+ y = x
+ return Transform(x, 0, 0, y, 0, 0)
+
+
+@dataclass
+class DecomposedTransform:
+ """The DecomposedTransform class implements a transformation with separate
+ translate, rotation, scale, skew, and transformation-center components.
+ """
+
+ translateX: float = 0
+ translateY: float = 0
+ rotation: float = 0 # in degrees, counter-clockwise
+ scaleX: float = 1
+ scaleY: float = 1
+ skewX: float = 0 # in degrees, clockwise
+ skewY: float = 0 # in degrees, counter-clockwise
+ tCenterX: float = 0
+ tCenterY: float = 0
+
+ @classmethod
+ def fromTransform(self, transform):
+ # Adapted from an answer on
+ # https://math.stackexchange.com/questions/13150/extracting-rotation-scale-values-from-2d-transformation-matrix
+ a, b, c, d, x, y = transform
+
+ sx = math.copysign(1, a)
+ if sx < 0:
+ a *= sx
+ b *= sx
+
+ delta = a * d - b * c
+
+ rotation = 0
+ scaleX = scaleY = 0
+ skewX = skewY = 0
+
+ # Apply the QR-like decomposition.
+ if a != 0 or b != 0:
+ r = math.sqrt(a * a + b * b)
+ rotation = math.acos(a / r) if b >= 0 else -math.acos(a / r)
+ scaleX, scaleY = (r, delta / r)
+ skewX, skewY = (math.atan((a * c + b * d) / (r * r)), 0)
+ elif c != 0 or d != 0:
+ s = math.sqrt(c * c + d * d)
+ rotation = math.pi / 2 - (
+ math.acos(-c / s) if d >= 0 else -math.acos(c / s)
+ )
+ scaleX, scaleY = (delta / s, s)
+ skewX, skewY = (0, math.atan((a * c + b * d) / (s * s)))
+ else:
+ # a = b = c = d = 0
+ pass
+
+ return DecomposedTransform(
+ x,
+ y,
+ math.degrees(rotation),
+ scaleX * sx,
+ scaleY,
+ math.degrees(skewX) * sx,
+ math.degrees(skewY),
+ 0,
+ 0,
+ )
+
+ def toTransform(self):
+ """Return the Transform() equivalent of this transformation.
+
+ :Example:
+ >>> DecomposedTransform(scaleX=2, scaleY=2).toTransform()
+ <Transform [2 0 0 2 0 0]>
+ >>>
+ """
+ t = Transform()
+ t = t.translate(
+ self.translateX + self.tCenterX, self.translateY + self.tCenterY
+ )
+ t = t.rotate(math.radians(self.rotation))
+ t = t.scale(self.scaleX, self.scaleY)
+ t = t.skew(math.radians(self.skewX), math.radians(self.skewY))
+ t = t.translate(-self.tCenterX, -self.tCenterY)
+ return t
if __name__ == "__main__":
- import sys
- import doctest
- sys.exit(doctest.testmod().failed)
+ import sys
+ import doctest
+
+ sys.exit(doctest.testmod().failed)