xref: /aosp_15_r20/external/fonttools/Lib/fontTools/misc/transform.py (revision e1fe3e4ad2793916b15cccdc4a7da52a7e1dd0e9)
1*e1fe3e4aSElliott Hughes"""Affine 2D transformation matrix class.
2*e1fe3e4aSElliott Hughes
3*e1fe3e4aSElliott HughesThe Transform class implements various transformation matrix operations,
4*e1fe3e4aSElliott Hughesboth on the matrix itself, as well as on 2D coordinates.
5*e1fe3e4aSElliott Hughes
6*e1fe3e4aSElliott HughesTransform instances are effectively immutable: all methods that operate on the
7*e1fe3e4aSElliott Hughestransformation itself always return a new instance. This has as the
8*e1fe3e4aSElliott Hughesinteresting side effect that Transform instances are hashable, ie. they can be
9*e1fe3e4aSElliott Hughesused as dictionary keys.
10*e1fe3e4aSElliott Hughes
11*e1fe3e4aSElliott HughesThis module exports the following symbols:
12*e1fe3e4aSElliott Hughes
13*e1fe3e4aSElliott HughesTransform
14*e1fe3e4aSElliott Hughes	this is the main class
15*e1fe3e4aSElliott HughesIdentity
16*e1fe3e4aSElliott Hughes	Transform instance set to the identity transformation
17*e1fe3e4aSElliott HughesOffset
18*e1fe3e4aSElliott Hughes	Convenience function that returns a translating transformation
19*e1fe3e4aSElliott HughesScale
20*e1fe3e4aSElliott Hughes	Convenience function that returns a scaling transformation
21*e1fe3e4aSElliott Hughes
22*e1fe3e4aSElliott HughesThe DecomposedTransform class implements a transformation with separate
23*e1fe3e4aSElliott Hughestranslate, rotation, scale, skew, and transformation-center components.
24*e1fe3e4aSElliott Hughes
25*e1fe3e4aSElliott Hughes:Example:
26*e1fe3e4aSElliott Hughes
27*e1fe3e4aSElliott Hughes	>>> t = Transform(2, 0, 0, 3, 0, 0)
28*e1fe3e4aSElliott Hughes	>>> t.transformPoint((100, 100))
29*e1fe3e4aSElliott Hughes	(200, 300)
30*e1fe3e4aSElliott Hughes	>>> t = Scale(2, 3)
31*e1fe3e4aSElliott Hughes	>>> t.transformPoint((100, 100))
32*e1fe3e4aSElliott Hughes	(200, 300)
33*e1fe3e4aSElliott Hughes	>>> t.transformPoint((0, 0))
34*e1fe3e4aSElliott Hughes	(0, 0)
35*e1fe3e4aSElliott Hughes	>>> t = Offset(2, 3)
36*e1fe3e4aSElliott Hughes	>>> t.transformPoint((100, 100))
37*e1fe3e4aSElliott Hughes	(102, 103)
38*e1fe3e4aSElliott Hughes	>>> t.transformPoint((0, 0))
39*e1fe3e4aSElliott Hughes	(2, 3)
40*e1fe3e4aSElliott Hughes	>>> t2 = t.scale(0.5)
41*e1fe3e4aSElliott Hughes	>>> t2.transformPoint((100, 100))
42*e1fe3e4aSElliott Hughes	(52.0, 53.0)
43*e1fe3e4aSElliott Hughes	>>> import math
44*e1fe3e4aSElliott Hughes	>>> t3 = t2.rotate(math.pi / 2)
45*e1fe3e4aSElliott Hughes	>>> t3.transformPoint((0, 0))
46*e1fe3e4aSElliott Hughes	(2.0, 3.0)
47*e1fe3e4aSElliott Hughes	>>> t3.transformPoint((100, 100))
48*e1fe3e4aSElliott Hughes	(-48.0, 53.0)
49*e1fe3e4aSElliott Hughes	>>> t = Identity.scale(0.5).translate(100, 200).skew(0.1, 0.2)
50*e1fe3e4aSElliott Hughes	>>> t.transformPoints([(0, 0), (1, 1), (100, 100)])
51*e1fe3e4aSElliott Hughes	[(50.0, 100.0), (50.550167336042726, 100.60135501775433), (105.01673360427253, 160.13550177543362)]
52*e1fe3e4aSElliott Hughes	>>>
53*e1fe3e4aSElliott Hughes"""
54*e1fe3e4aSElliott Hughes
55*e1fe3e4aSElliott Hughesimport math
56*e1fe3e4aSElliott Hughesfrom typing import NamedTuple
57*e1fe3e4aSElliott Hughesfrom dataclasses import dataclass
58*e1fe3e4aSElliott Hughes
59*e1fe3e4aSElliott Hughes
60*e1fe3e4aSElliott Hughes__all__ = ["Transform", "Identity", "Offset", "Scale", "DecomposedTransform"]
61*e1fe3e4aSElliott Hughes
62*e1fe3e4aSElliott Hughes
63*e1fe3e4aSElliott Hughes_EPSILON = 1e-15
64*e1fe3e4aSElliott Hughes_ONE_EPSILON = 1 - _EPSILON
65*e1fe3e4aSElliott Hughes_MINUS_ONE_EPSILON = -1 + _EPSILON
66*e1fe3e4aSElliott Hughes
67*e1fe3e4aSElliott Hughes
68*e1fe3e4aSElliott Hughesdef _normSinCos(v):
69*e1fe3e4aSElliott Hughes    if abs(v) < _EPSILON:
70*e1fe3e4aSElliott Hughes        v = 0
71*e1fe3e4aSElliott Hughes    elif v > _ONE_EPSILON:
72*e1fe3e4aSElliott Hughes        v = 1
73*e1fe3e4aSElliott Hughes    elif v < _MINUS_ONE_EPSILON:
74*e1fe3e4aSElliott Hughes        v = -1
75*e1fe3e4aSElliott Hughes    return v
76*e1fe3e4aSElliott Hughes
77*e1fe3e4aSElliott Hughes
78*e1fe3e4aSElliott Hughesclass Transform(NamedTuple):
79*e1fe3e4aSElliott Hughes    """2x2 transformation matrix plus offset, a.k.a. Affine transform.
80*e1fe3e4aSElliott Hughes    Transform instances are immutable: all transforming methods, eg.
81*e1fe3e4aSElliott Hughes    rotate(), return a new Transform instance.
82*e1fe3e4aSElliott Hughes
83*e1fe3e4aSElliott Hughes    :Example:
84*e1fe3e4aSElliott Hughes
85*e1fe3e4aSElliott Hughes            >>> t = Transform()
86*e1fe3e4aSElliott Hughes            >>> t
87*e1fe3e4aSElliott Hughes            <Transform [1 0 0 1 0 0]>
88*e1fe3e4aSElliott Hughes            >>> t.scale(2)
89*e1fe3e4aSElliott Hughes            <Transform [2 0 0 2 0 0]>
90*e1fe3e4aSElliott Hughes            >>> t.scale(2.5, 5.5)
91*e1fe3e4aSElliott Hughes            <Transform [2.5 0 0 5.5 0 0]>
92*e1fe3e4aSElliott Hughes            >>>
93*e1fe3e4aSElliott Hughes            >>> t.scale(2, 3).transformPoint((100, 100))
94*e1fe3e4aSElliott Hughes            (200, 300)
95*e1fe3e4aSElliott Hughes
96*e1fe3e4aSElliott Hughes    Transform's constructor takes six arguments, all of which are
97*e1fe3e4aSElliott Hughes    optional, and can be used as keyword arguments::
98*e1fe3e4aSElliott Hughes
99*e1fe3e4aSElliott Hughes            >>> Transform(12)
100*e1fe3e4aSElliott Hughes            <Transform [12 0 0 1 0 0]>
101*e1fe3e4aSElliott Hughes            >>> Transform(dx=12)
102*e1fe3e4aSElliott Hughes            <Transform [1 0 0 1 12 0]>
103*e1fe3e4aSElliott Hughes            >>> Transform(yx=12)
104*e1fe3e4aSElliott Hughes            <Transform [1 0 12 1 0 0]>
105*e1fe3e4aSElliott Hughes
106*e1fe3e4aSElliott Hughes    Transform instances also behave like sequences of length 6::
107*e1fe3e4aSElliott Hughes
108*e1fe3e4aSElliott Hughes            >>> len(Identity)
109*e1fe3e4aSElliott Hughes            6
110*e1fe3e4aSElliott Hughes            >>> list(Identity)
111*e1fe3e4aSElliott Hughes            [1, 0, 0, 1, 0, 0]
112*e1fe3e4aSElliott Hughes            >>> tuple(Identity)
113*e1fe3e4aSElliott Hughes            (1, 0, 0, 1, 0, 0)
114*e1fe3e4aSElliott Hughes
115*e1fe3e4aSElliott Hughes    Transform instances are comparable::
116*e1fe3e4aSElliott Hughes
117*e1fe3e4aSElliott Hughes            >>> t1 = Identity.scale(2, 3).translate(4, 6)
118*e1fe3e4aSElliott Hughes            >>> t2 = Identity.translate(8, 18).scale(2, 3)
119*e1fe3e4aSElliott Hughes            >>> t1 == t2
120*e1fe3e4aSElliott Hughes            1
121*e1fe3e4aSElliott Hughes
122*e1fe3e4aSElliott Hughes    But beware of floating point rounding errors::
123*e1fe3e4aSElliott Hughes
124*e1fe3e4aSElliott Hughes            >>> t1 = Identity.scale(0.2, 0.3).translate(0.4, 0.6)
125*e1fe3e4aSElliott Hughes            >>> t2 = Identity.translate(0.08, 0.18).scale(0.2, 0.3)
126*e1fe3e4aSElliott Hughes            >>> t1
127*e1fe3e4aSElliott Hughes            <Transform [0.2 0 0 0.3 0.08 0.18]>
128*e1fe3e4aSElliott Hughes            >>> t2
129*e1fe3e4aSElliott Hughes            <Transform [0.2 0 0 0.3 0.08 0.18]>
130*e1fe3e4aSElliott Hughes            >>> t1 == t2
131*e1fe3e4aSElliott Hughes            0
132*e1fe3e4aSElliott Hughes
133*e1fe3e4aSElliott Hughes    Transform instances are hashable, meaning you can use them as
134*e1fe3e4aSElliott Hughes    keys in dictionaries::
135*e1fe3e4aSElliott Hughes
136*e1fe3e4aSElliott Hughes            >>> d = {Scale(12, 13): None}
137*e1fe3e4aSElliott Hughes            >>> d
138*e1fe3e4aSElliott Hughes            {<Transform [12 0 0 13 0 0]>: None}
139*e1fe3e4aSElliott Hughes
140*e1fe3e4aSElliott Hughes    But again, beware of floating point rounding errors::
141*e1fe3e4aSElliott Hughes
142*e1fe3e4aSElliott Hughes            >>> t1 = Identity.scale(0.2, 0.3).translate(0.4, 0.6)
143*e1fe3e4aSElliott Hughes            >>> t2 = Identity.translate(0.08, 0.18).scale(0.2, 0.3)
144*e1fe3e4aSElliott Hughes            >>> t1
145*e1fe3e4aSElliott Hughes            <Transform [0.2 0 0 0.3 0.08 0.18]>
146*e1fe3e4aSElliott Hughes            >>> t2
147*e1fe3e4aSElliott Hughes            <Transform [0.2 0 0 0.3 0.08 0.18]>
148*e1fe3e4aSElliott Hughes            >>> d = {t1: None}
149*e1fe3e4aSElliott Hughes            >>> d
150*e1fe3e4aSElliott Hughes            {<Transform [0.2 0 0 0.3 0.08 0.18]>: None}
151*e1fe3e4aSElliott Hughes            >>> d[t2]
152*e1fe3e4aSElliott Hughes            Traceback (most recent call last):
153*e1fe3e4aSElliott Hughes              File "<stdin>", line 1, in ?
154*e1fe3e4aSElliott Hughes            KeyError: <Transform [0.2 0 0 0.3 0.08 0.18]>
155*e1fe3e4aSElliott Hughes    """
156*e1fe3e4aSElliott Hughes
157*e1fe3e4aSElliott Hughes    xx: float = 1
158*e1fe3e4aSElliott Hughes    xy: float = 0
159*e1fe3e4aSElliott Hughes    yx: float = 0
160*e1fe3e4aSElliott Hughes    yy: float = 1
161*e1fe3e4aSElliott Hughes    dx: float = 0
162*e1fe3e4aSElliott Hughes    dy: float = 0
163*e1fe3e4aSElliott Hughes
164*e1fe3e4aSElliott Hughes    def transformPoint(self, p):
165*e1fe3e4aSElliott Hughes        """Transform a point.
166*e1fe3e4aSElliott Hughes
167*e1fe3e4aSElliott Hughes        :Example:
168*e1fe3e4aSElliott Hughes
169*e1fe3e4aSElliott Hughes                >>> t = Transform()
170*e1fe3e4aSElliott Hughes                >>> t = t.scale(2.5, 5.5)
171*e1fe3e4aSElliott Hughes                >>> t.transformPoint((100, 100))
172*e1fe3e4aSElliott Hughes                (250.0, 550.0)
173*e1fe3e4aSElliott Hughes        """
174*e1fe3e4aSElliott Hughes        (x, y) = p
175*e1fe3e4aSElliott Hughes        xx, xy, yx, yy, dx, dy = self
176*e1fe3e4aSElliott Hughes        return (xx * x + yx * y + dx, xy * x + yy * y + dy)
177*e1fe3e4aSElliott Hughes
178*e1fe3e4aSElliott Hughes    def transformPoints(self, points):
179*e1fe3e4aSElliott Hughes        """Transform a list of points.
180*e1fe3e4aSElliott Hughes
181*e1fe3e4aSElliott Hughes        :Example:
182*e1fe3e4aSElliott Hughes
183*e1fe3e4aSElliott Hughes                >>> t = Scale(2, 3)
184*e1fe3e4aSElliott Hughes                >>> t.transformPoints([(0, 0), (0, 100), (100, 100), (100, 0)])
185*e1fe3e4aSElliott Hughes                [(0, 0), (0, 300), (200, 300), (200, 0)]
186*e1fe3e4aSElliott Hughes                >>>
187*e1fe3e4aSElliott Hughes        """
188*e1fe3e4aSElliott Hughes        xx, xy, yx, yy, dx, dy = self
189*e1fe3e4aSElliott Hughes        return [(xx * x + yx * y + dx, xy * x + yy * y + dy) for x, y in points]
190*e1fe3e4aSElliott Hughes
191*e1fe3e4aSElliott Hughes    def transformVector(self, v):
192*e1fe3e4aSElliott Hughes        """Transform an (dx, dy) vector, treating translation as zero.
193*e1fe3e4aSElliott Hughes
194*e1fe3e4aSElliott Hughes        :Example:
195*e1fe3e4aSElliott Hughes
196*e1fe3e4aSElliott Hughes                >>> t = Transform(2, 0, 0, 2, 10, 20)
197*e1fe3e4aSElliott Hughes                >>> t.transformVector((3, -4))
198*e1fe3e4aSElliott Hughes                (6, -8)
199*e1fe3e4aSElliott Hughes                >>>
200*e1fe3e4aSElliott Hughes        """
201*e1fe3e4aSElliott Hughes        (dx, dy) = v
202*e1fe3e4aSElliott Hughes        xx, xy, yx, yy = self[:4]
203*e1fe3e4aSElliott Hughes        return (xx * dx + yx * dy, xy * dx + yy * dy)
204*e1fe3e4aSElliott Hughes
205*e1fe3e4aSElliott Hughes    def transformVectors(self, vectors):
206*e1fe3e4aSElliott Hughes        """Transform a list of (dx, dy) vector, treating translation as zero.
207*e1fe3e4aSElliott Hughes
208*e1fe3e4aSElliott Hughes        :Example:
209*e1fe3e4aSElliott Hughes                >>> t = Transform(2, 0, 0, 2, 10, 20)
210*e1fe3e4aSElliott Hughes                >>> t.transformVectors([(3, -4), (5, -6)])
211*e1fe3e4aSElliott Hughes                [(6, -8), (10, -12)]
212*e1fe3e4aSElliott Hughes                >>>
213*e1fe3e4aSElliott Hughes        """
214*e1fe3e4aSElliott Hughes        xx, xy, yx, yy = self[:4]
215*e1fe3e4aSElliott Hughes        return [(xx * dx + yx * dy, xy * dx + yy * dy) for dx, dy in vectors]
216*e1fe3e4aSElliott Hughes
217*e1fe3e4aSElliott Hughes    def translate(self, x=0, y=0):
218*e1fe3e4aSElliott Hughes        """Return a new transformation, translated (offset) by x, y.
219*e1fe3e4aSElliott Hughes
220*e1fe3e4aSElliott Hughes        :Example:
221*e1fe3e4aSElliott Hughes                >>> t = Transform()
222*e1fe3e4aSElliott Hughes                >>> t.translate(20, 30)
223*e1fe3e4aSElliott Hughes                <Transform [1 0 0 1 20 30]>
224*e1fe3e4aSElliott Hughes                >>>
225*e1fe3e4aSElliott Hughes        """
226*e1fe3e4aSElliott Hughes        return self.transform((1, 0, 0, 1, x, y))
227*e1fe3e4aSElliott Hughes
228*e1fe3e4aSElliott Hughes    def scale(self, x=1, y=None):
229*e1fe3e4aSElliott Hughes        """Return a new transformation, scaled by x, y. The 'y' argument
230*e1fe3e4aSElliott Hughes        may be None, which implies to use the x value for y as well.
231*e1fe3e4aSElliott Hughes
232*e1fe3e4aSElliott Hughes        :Example:
233*e1fe3e4aSElliott Hughes                >>> t = Transform()
234*e1fe3e4aSElliott Hughes                >>> t.scale(5)
235*e1fe3e4aSElliott Hughes                <Transform [5 0 0 5 0 0]>
236*e1fe3e4aSElliott Hughes                >>> t.scale(5, 6)
237*e1fe3e4aSElliott Hughes                <Transform [5 0 0 6 0 0]>
238*e1fe3e4aSElliott Hughes                >>>
239*e1fe3e4aSElliott Hughes        """
240*e1fe3e4aSElliott Hughes        if y is None:
241*e1fe3e4aSElliott Hughes            y = x
242*e1fe3e4aSElliott Hughes        return self.transform((x, 0, 0, y, 0, 0))
243*e1fe3e4aSElliott Hughes
244*e1fe3e4aSElliott Hughes    def rotate(self, angle):
245*e1fe3e4aSElliott Hughes        """Return a new transformation, rotated by 'angle' (radians).
246*e1fe3e4aSElliott Hughes
247*e1fe3e4aSElliott Hughes        :Example:
248*e1fe3e4aSElliott Hughes                >>> import math
249*e1fe3e4aSElliott Hughes                >>> t = Transform()
250*e1fe3e4aSElliott Hughes                >>> t.rotate(math.pi / 2)
251*e1fe3e4aSElliott Hughes                <Transform [0 1 -1 0 0 0]>
252*e1fe3e4aSElliott Hughes                >>>
253*e1fe3e4aSElliott Hughes        """
254*e1fe3e4aSElliott Hughes        import math
255*e1fe3e4aSElliott Hughes
256*e1fe3e4aSElliott Hughes        c = _normSinCos(math.cos(angle))
257*e1fe3e4aSElliott Hughes        s = _normSinCos(math.sin(angle))
258*e1fe3e4aSElliott Hughes        return self.transform((c, s, -s, c, 0, 0))
259*e1fe3e4aSElliott Hughes
260*e1fe3e4aSElliott Hughes    def skew(self, x=0, y=0):
261*e1fe3e4aSElliott Hughes        """Return a new transformation, skewed by x and y.
262*e1fe3e4aSElliott Hughes
263*e1fe3e4aSElliott Hughes        :Example:
264*e1fe3e4aSElliott Hughes                >>> import math
265*e1fe3e4aSElliott Hughes                >>> t = Transform()
266*e1fe3e4aSElliott Hughes                >>> t.skew(math.pi / 4)
267*e1fe3e4aSElliott Hughes                <Transform [1 0 1 1 0 0]>
268*e1fe3e4aSElliott Hughes                >>>
269*e1fe3e4aSElliott Hughes        """
270*e1fe3e4aSElliott Hughes        import math
271*e1fe3e4aSElliott Hughes
272*e1fe3e4aSElliott Hughes        return self.transform((1, math.tan(y), math.tan(x), 1, 0, 0))
273*e1fe3e4aSElliott Hughes
274*e1fe3e4aSElliott Hughes    def transform(self, other):
275*e1fe3e4aSElliott Hughes        """Return a new transformation, transformed by another
276*e1fe3e4aSElliott Hughes        transformation.
277*e1fe3e4aSElliott Hughes
278*e1fe3e4aSElliott Hughes        :Example:
279*e1fe3e4aSElliott Hughes                >>> t = Transform(2, 0, 0, 3, 1, 6)
280*e1fe3e4aSElliott Hughes                >>> t.transform((4, 3, 2, 1, 5, 6))
281*e1fe3e4aSElliott Hughes                <Transform [8 9 4 3 11 24]>
282*e1fe3e4aSElliott Hughes                >>>
283*e1fe3e4aSElliott Hughes        """
284*e1fe3e4aSElliott Hughes        xx1, xy1, yx1, yy1, dx1, dy1 = other
285*e1fe3e4aSElliott Hughes        xx2, xy2, yx2, yy2, dx2, dy2 = self
286*e1fe3e4aSElliott Hughes        return self.__class__(
287*e1fe3e4aSElliott Hughes            xx1 * xx2 + xy1 * yx2,
288*e1fe3e4aSElliott Hughes            xx1 * xy2 + xy1 * yy2,
289*e1fe3e4aSElliott Hughes            yx1 * xx2 + yy1 * yx2,
290*e1fe3e4aSElliott Hughes            yx1 * xy2 + yy1 * yy2,
291*e1fe3e4aSElliott Hughes            xx2 * dx1 + yx2 * dy1 + dx2,
292*e1fe3e4aSElliott Hughes            xy2 * dx1 + yy2 * dy1 + dy2,
293*e1fe3e4aSElliott Hughes        )
294*e1fe3e4aSElliott Hughes
295*e1fe3e4aSElliott Hughes    def reverseTransform(self, other):
296*e1fe3e4aSElliott Hughes        """Return a new transformation, which is the other transformation
297*e1fe3e4aSElliott Hughes        transformed by self. self.reverseTransform(other) is equivalent to
298*e1fe3e4aSElliott Hughes        other.transform(self).
299*e1fe3e4aSElliott Hughes
300*e1fe3e4aSElliott Hughes        :Example:
301*e1fe3e4aSElliott Hughes                >>> t = Transform(2, 0, 0, 3, 1, 6)
302*e1fe3e4aSElliott Hughes                >>> t.reverseTransform((4, 3, 2, 1, 5, 6))
303*e1fe3e4aSElliott Hughes                <Transform [8 6 6 3 21 15]>
304*e1fe3e4aSElliott Hughes                >>> Transform(4, 3, 2, 1, 5, 6).transform((2, 0, 0, 3, 1, 6))
305*e1fe3e4aSElliott Hughes                <Transform [8 6 6 3 21 15]>
306*e1fe3e4aSElliott Hughes                >>>
307*e1fe3e4aSElliott Hughes        """
308*e1fe3e4aSElliott Hughes        xx1, xy1, yx1, yy1, dx1, dy1 = self
309*e1fe3e4aSElliott Hughes        xx2, xy2, yx2, yy2, dx2, dy2 = other
310*e1fe3e4aSElliott Hughes        return self.__class__(
311*e1fe3e4aSElliott Hughes            xx1 * xx2 + xy1 * yx2,
312*e1fe3e4aSElliott Hughes            xx1 * xy2 + xy1 * yy2,
313*e1fe3e4aSElliott Hughes            yx1 * xx2 + yy1 * yx2,
314*e1fe3e4aSElliott Hughes            yx1 * xy2 + yy1 * yy2,
315*e1fe3e4aSElliott Hughes            xx2 * dx1 + yx2 * dy1 + dx2,
316*e1fe3e4aSElliott Hughes            xy2 * dx1 + yy2 * dy1 + dy2,
317*e1fe3e4aSElliott Hughes        )
318*e1fe3e4aSElliott Hughes
319*e1fe3e4aSElliott Hughes    def inverse(self):
320*e1fe3e4aSElliott Hughes        """Return the inverse transformation.
321*e1fe3e4aSElliott Hughes
322*e1fe3e4aSElliott Hughes        :Example:
323*e1fe3e4aSElliott Hughes                >>> t = Identity.translate(2, 3).scale(4, 5)
324*e1fe3e4aSElliott Hughes                >>> t.transformPoint((10, 20))
325*e1fe3e4aSElliott Hughes                (42, 103)
326*e1fe3e4aSElliott Hughes                >>> it = t.inverse()
327*e1fe3e4aSElliott Hughes                >>> it.transformPoint((42, 103))
328*e1fe3e4aSElliott Hughes                (10.0, 20.0)
329*e1fe3e4aSElliott Hughes                >>>
330*e1fe3e4aSElliott Hughes        """
331*e1fe3e4aSElliott Hughes        if self == Identity:
332*e1fe3e4aSElliott Hughes            return self
333*e1fe3e4aSElliott Hughes        xx, xy, yx, yy, dx, dy = self
334*e1fe3e4aSElliott Hughes        det = xx * yy - yx * xy
335*e1fe3e4aSElliott Hughes        xx, xy, yx, yy = yy / det, -xy / det, -yx / det, xx / det
336*e1fe3e4aSElliott Hughes        dx, dy = -xx * dx - yx * dy, -xy * dx - yy * dy
337*e1fe3e4aSElliott Hughes        return self.__class__(xx, xy, yx, yy, dx, dy)
338*e1fe3e4aSElliott Hughes
339*e1fe3e4aSElliott Hughes    def toPS(self):
340*e1fe3e4aSElliott Hughes        """Return a PostScript representation
341*e1fe3e4aSElliott Hughes
342*e1fe3e4aSElliott Hughes        :Example:
343*e1fe3e4aSElliott Hughes
344*e1fe3e4aSElliott Hughes                >>> t = Identity.scale(2, 3).translate(4, 5)
345*e1fe3e4aSElliott Hughes                >>> t.toPS()
346*e1fe3e4aSElliott Hughes                '[2 0 0 3 8 15]'
347*e1fe3e4aSElliott Hughes                >>>
348*e1fe3e4aSElliott Hughes        """
349*e1fe3e4aSElliott Hughes        return "[%s %s %s %s %s %s]" % self
350*e1fe3e4aSElliott Hughes
351*e1fe3e4aSElliott Hughes    def toDecomposed(self) -> "DecomposedTransform":
352*e1fe3e4aSElliott Hughes        """Decompose into a DecomposedTransform."""
353*e1fe3e4aSElliott Hughes        return DecomposedTransform.fromTransform(self)
354*e1fe3e4aSElliott Hughes
355*e1fe3e4aSElliott Hughes    def __bool__(self):
356*e1fe3e4aSElliott Hughes        """Returns True if transform is not identity, False otherwise.
357*e1fe3e4aSElliott Hughes
358*e1fe3e4aSElliott Hughes        :Example:
359*e1fe3e4aSElliott Hughes
360*e1fe3e4aSElliott Hughes                >>> bool(Identity)
361*e1fe3e4aSElliott Hughes                False
362*e1fe3e4aSElliott Hughes                >>> bool(Transform())
363*e1fe3e4aSElliott Hughes                False
364*e1fe3e4aSElliott Hughes                >>> bool(Scale(1.))
365*e1fe3e4aSElliott Hughes                False
366*e1fe3e4aSElliott Hughes                >>> bool(Scale(2))
367*e1fe3e4aSElliott Hughes                True
368*e1fe3e4aSElliott Hughes                >>> bool(Offset())
369*e1fe3e4aSElliott Hughes                False
370*e1fe3e4aSElliott Hughes                >>> bool(Offset(0))
371*e1fe3e4aSElliott Hughes                False
372*e1fe3e4aSElliott Hughes                >>> bool(Offset(2))
373*e1fe3e4aSElliott Hughes                True
374*e1fe3e4aSElliott Hughes        """
375*e1fe3e4aSElliott Hughes        return self != Identity
376*e1fe3e4aSElliott Hughes
377*e1fe3e4aSElliott Hughes    def __repr__(self):
378*e1fe3e4aSElliott Hughes        return "<%s [%g %g %g %g %g %g]>" % ((self.__class__.__name__,) + self)
379*e1fe3e4aSElliott Hughes
380*e1fe3e4aSElliott Hughes
381*e1fe3e4aSElliott HughesIdentity = Transform()
382*e1fe3e4aSElliott Hughes
383*e1fe3e4aSElliott Hughes
384*e1fe3e4aSElliott Hughesdef Offset(x=0, y=0):
385*e1fe3e4aSElliott Hughes    """Return the identity transformation offset by x, y.
386*e1fe3e4aSElliott Hughes
387*e1fe3e4aSElliott Hughes    :Example:
388*e1fe3e4aSElliott Hughes            >>> Offset(2, 3)
389*e1fe3e4aSElliott Hughes            <Transform [1 0 0 1 2 3]>
390*e1fe3e4aSElliott Hughes            >>>
391*e1fe3e4aSElliott Hughes    """
392*e1fe3e4aSElliott Hughes    return Transform(1, 0, 0, 1, x, y)
393*e1fe3e4aSElliott Hughes
394*e1fe3e4aSElliott Hughes
395*e1fe3e4aSElliott Hughesdef Scale(x, y=None):
396*e1fe3e4aSElliott Hughes    """Return the identity transformation scaled by x, y. The 'y' argument
397*e1fe3e4aSElliott Hughes    may be None, which implies to use the x value for y as well.
398*e1fe3e4aSElliott Hughes
399*e1fe3e4aSElliott Hughes    :Example:
400*e1fe3e4aSElliott Hughes            >>> Scale(2, 3)
401*e1fe3e4aSElliott Hughes            <Transform [2 0 0 3 0 0]>
402*e1fe3e4aSElliott Hughes            >>>
403*e1fe3e4aSElliott Hughes    """
404*e1fe3e4aSElliott Hughes    if y is None:
405*e1fe3e4aSElliott Hughes        y = x
406*e1fe3e4aSElliott Hughes    return Transform(x, 0, 0, y, 0, 0)
407*e1fe3e4aSElliott Hughes
408*e1fe3e4aSElliott Hughes
409*e1fe3e4aSElliott Hughes@dataclass
410*e1fe3e4aSElliott Hughesclass DecomposedTransform:
411*e1fe3e4aSElliott Hughes    """The DecomposedTransform class implements a transformation with separate
412*e1fe3e4aSElliott Hughes    translate, rotation, scale, skew, and transformation-center components.
413*e1fe3e4aSElliott Hughes    """
414*e1fe3e4aSElliott Hughes
415*e1fe3e4aSElliott Hughes    translateX: float = 0
416*e1fe3e4aSElliott Hughes    translateY: float = 0
417*e1fe3e4aSElliott Hughes    rotation: float = 0  # in degrees, counter-clockwise
418*e1fe3e4aSElliott Hughes    scaleX: float = 1
419*e1fe3e4aSElliott Hughes    scaleY: float = 1
420*e1fe3e4aSElliott Hughes    skewX: float = 0  # in degrees, clockwise
421*e1fe3e4aSElliott Hughes    skewY: float = 0  # in degrees, counter-clockwise
422*e1fe3e4aSElliott Hughes    tCenterX: float = 0
423*e1fe3e4aSElliott Hughes    tCenterY: float = 0
424*e1fe3e4aSElliott Hughes
425*e1fe3e4aSElliott Hughes    @classmethod
426*e1fe3e4aSElliott Hughes    def fromTransform(self, transform):
427*e1fe3e4aSElliott Hughes        # Adapted from an answer on
428*e1fe3e4aSElliott Hughes        # https://math.stackexchange.com/questions/13150/extracting-rotation-scale-values-from-2d-transformation-matrix
429*e1fe3e4aSElliott Hughes        a, b, c, d, x, y = transform
430*e1fe3e4aSElliott Hughes
431*e1fe3e4aSElliott Hughes        sx = math.copysign(1, a)
432*e1fe3e4aSElliott Hughes        if sx < 0:
433*e1fe3e4aSElliott Hughes            a *= sx
434*e1fe3e4aSElliott Hughes            b *= sx
435*e1fe3e4aSElliott Hughes
436*e1fe3e4aSElliott Hughes        delta = a * d - b * c
437*e1fe3e4aSElliott Hughes
438*e1fe3e4aSElliott Hughes        rotation = 0
439*e1fe3e4aSElliott Hughes        scaleX = scaleY = 0
440*e1fe3e4aSElliott Hughes        skewX = skewY = 0
441*e1fe3e4aSElliott Hughes
442*e1fe3e4aSElliott Hughes        # Apply the QR-like decomposition.
443*e1fe3e4aSElliott Hughes        if a != 0 or b != 0:
444*e1fe3e4aSElliott Hughes            r = math.sqrt(a * a + b * b)
445*e1fe3e4aSElliott Hughes            rotation = math.acos(a / r) if b >= 0 else -math.acos(a / r)
446*e1fe3e4aSElliott Hughes            scaleX, scaleY = (r, delta / r)
447*e1fe3e4aSElliott Hughes            skewX, skewY = (math.atan((a * c + b * d) / (r * r)), 0)
448*e1fe3e4aSElliott Hughes        elif c != 0 or d != 0:
449*e1fe3e4aSElliott Hughes            s = math.sqrt(c * c + d * d)
450*e1fe3e4aSElliott Hughes            rotation = math.pi / 2 - (
451*e1fe3e4aSElliott Hughes                math.acos(-c / s) if d >= 0 else -math.acos(c / s)
452*e1fe3e4aSElliott Hughes            )
453*e1fe3e4aSElliott Hughes            scaleX, scaleY = (delta / s, s)
454*e1fe3e4aSElliott Hughes            skewX, skewY = (0, math.atan((a * c + b * d) / (s * s)))
455*e1fe3e4aSElliott Hughes        else:
456*e1fe3e4aSElliott Hughes            # a = b = c = d = 0
457*e1fe3e4aSElliott Hughes            pass
458*e1fe3e4aSElliott Hughes
459*e1fe3e4aSElliott Hughes        return DecomposedTransform(
460*e1fe3e4aSElliott Hughes            x,
461*e1fe3e4aSElliott Hughes            y,
462*e1fe3e4aSElliott Hughes            math.degrees(rotation),
463*e1fe3e4aSElliott Hughes            scaleX * sx,
464*e1fe3e4aSElliott Hughes            scaleY,
465*e1fe3e4aSElliott Hughes            math.degrees(skewX) * sx,
466*e1fe3e4aSElliott Hughes            math.degrees(skewY),
467*e1fe3e4aSElliott Hughes            0,
468*e1fe3e4aSElliott Hughes            0,
469*e1fe3e4aSElliott Hughes        )
470*e1fe3e4aSElliott Hughes
471*e1fe3e4aSElliott Hughes    def toTransform(self):
472*e1fe3e4aSElliott Hughes        """Return the Transform() equivalent of this transformation.
473*e1fe3e4aSElliott Hughes
474*e1fe3e4aSElliott Hughes        :Example:
475*e1fe3e4aSElliott Hughes                >>> DecomposedTransform(scaleX=2, scaleY=2).toTransform()
476*e1fe3e4aSElliott Hughes                <Transform [2 0 0 2 0 0]>
477*e1fe3e4aSElliott Hughes                >>>
478*e1fe3e4aSElliott Hughes        """
479*e1fe3e4aSElliott Hughes        t = Transform()
480*e1fe3e4aSElliott Hughes        t = t.translate(
481*e1fe3e4aSElliott Hughes            self.translateX + self.tCenterX, self.translateY + self.tCenterY
482*e1fe3e4aSElliott Hughes        )
483*e1fe3e4aSElliott Hughes        t = t.rotate(math.radians(self.rotation))
484*e1fe3e4aSElliott Hughes        t = t.scale(self.scaleX, self.scaleY)
485*e1fe3e4aSElliott Hughes        t = t.skew(math.radians(self.skewX), math.radians(self.skewY))
486*e1fe3e4aSElliott Hughes        t = t.translate(-self.tCenterX, -self.tCenterY)
487*e1fe3e4aSElliott Hughes        return t
488*e1fe3e4aSElliott Hughes
489*e1fe3e4aSElliott Hughes
490*e1fe3e4aSElliott Hughesif __name__ == "__main__":
491*e1fe3e4aSElliott Hughes    import sys
492*e1fe3e4aSElliott Hughes    import doctest
493*e1fe3e4aSElliott Hughes
494*e1fe3e4aSElliott Hughes    sys.exit(doctest.testmod().failed)
495