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