• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1"""Affine 2D transformation matrix class.
2
3The Transform class implements various transformation matrix operations,
4both on the matrix itself, as well as on 2D coordinates.
5
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.
10
11This module exports the following symbols:
12
13	Transform -- this is the main class
14	Identity  -- Transform instance set to the identity transformation
15	Offset    -- Convenience function that returns a translating transformation
16	Scale     -- Convenience function that returns a scaling transformation
17
18Examples:
19
20	>>> t = Transform(2, 0, 0, 3, 0, 0)
21	>>> t.transformPoint((100, 100))
22	(200, 300)
23	>>> t = Scale(2, 3)
24	>>> t.transformPoint((100, 100))
25	(200, 300)
26	>>> t.transformPoint((0, 0))
27	(0, 0)
28	>>> t = Offset(2, 3)
29	>>> t.transformPoint((100, 100))
30	(102, 103)
31	>>> t.transformPoint((0, 0))
32	(2, 3)
33	>>> t2 = t.scale(0.5)
34	>>> t2.transformPoint((100, 100))
35	(52.0, 53.0)
36	>>> import math
37	>>> t3 = t2.rotate(math.pi / 2)
38	>>> t3.transformPoint((0, 0))
39	(2.0, 3.0)
40	>>> t3.transformPoint((100, 100))
41	(-48.0, 53.0)
42	>>> t = Identity.scale(0.5).translate(100, 200).skew(0.1, 0.2)
43	>>> t.transformPoints([(0, 0), (1, 1), (100, 100)])
44	[(50.0, 100.0), (50.550167336042726, 100.60135501775433), (105.01673360427253, 160.13550177543362)]
45	>>>
46"""
47
48from __future__ import print_function, division, absolute_import
49from fontTools.misc.py23 import *
50
51__all__ = ["Transform", "Identity", "Offset", "Scale"]
52
53
54_EPSILON = 1e-15
55_ONE_EPSILON = 1 - _EPSILON
56_MINUS_ONE_EPSILON = -1 + _EPSILON
57
58
59def _normSinCos(v):
60	if abs(v) < _EPSILON:
61		v = 0
62	elif v > _ONE_EPSILON:
63		v = 1
64	elif v < _MINUS_ONE_EPSILON:
65		v = -1
66	return v
67
68
69class Transform(object):
70
71	"""2x2 transformation matrix plus offset, a.k.a. Affine transform.
72	Transform instances are immutable: all transforming methods, eg.
73	rotate(), return a new Transform instance.
74
75	Examples:
76		>>> t = Transform()
77		>>> t
78		<Transform [1 0 0 1 0 0]>
79		>>> t.scale(2)
80		<Transform [2 0 0 2 0 0]>
81		>>> t.scale(2.5, 5.5)
82		<Transform [2.5 0 0 5.5 0 0]>
83		>>>
84		>>> t.scale(2, 3).transformPoint((100, 100))
85		(200, 300)
86	"""
87
88	def __init__(self, xx=1, xy=0, yx=0, yy=1, dx=0, dy=0):
89		"""Transform's constructor takes six arguments, all of which are
90		optional, and can be used as keyword arguments:
91			>>> Transform(12)
92			<Transform [12 0 0 1 0 0]>
93			>>> Transform(dx=12)
94			<Transform [1 0 0 1 12 0]>
95			>>> Transform(yx=12)
96			<Transform [1 0 12 1 0 0]>
97			>>>
98		"""
99		self.__affine = xx, xy, yx, yy, dx, dy
100
101	def transformPoint(self, p):
102		"""Transform a point.
103
104		Example:
105			>>> t = Transform()
106			>>> t = t.scale(2.5, 5.5)
107			>>> t.transformPoint((100, 100))
108			(250.0, 550.0)
109		"""
110		(x, y) = p
111		xx, xy, yx, yy, dx, dy = self.__affine
112		return (xx*x + yx*y + dx, xy*x + yy*y + dy)
113
114	def transformPoints(self, points):
115		"""Transform a list of points.
116
117		Example:
118			>>> t = Scale(2, 3)
119			>>> t.transformPoints([(0, 0), (0, 100), (100, 100), (100, 0)])
120			[(0, 0), (0, 300), (200, 300), (200, 0)]
121			>>>
122		"""
123		xx, xy, yx, yy, dx, dy = self.__affine
124		return [(xx*x + yx*y + dx, xy*x + yy*y + dy) for x, y in points]
125
126	def translate(self, x=0, y=0):
127		"""Return a new transformation, translated (offset) by x, y.
128
129		Example:
130			>>> t = Transform()
131			>>> t.translate(20, 30)
132			<Transform [1 0 0 1 20 30]>
133			>>>
134		"""
135		return self.transform((1, 0, 0, 1, x, y))
136
137	def scale(self, x=1, y=None):
138		"""Return a new transformation, scaled by x, y. The 'y' argument
139		may be None, which implies to use the x value for y as well.
140
141		Example:
142			>>> t = Transform()
143			>>> t.scale(5)
144			<Transform [5 0 0 5 0 0]>
145			>>> t.scale(5, 6)
146			<Transform [5 0 0 6 0 0]>
147			>>>
148		"""
149		if y is None:
150			y = x
151		return self.transform((x, 0, 0, y, 0, 0))
152
153	def rotate(self, angle):
154		"""Return a new transformation, rotated by 'angle' (radians).
155
156		Example:
157			>>> import math
158			>>> t = Transform()
159			>>> t.rotate(math.pi / 2)
160			<Transform [0 1 -1 0 0 0]>
161			>>>
162		"""
163		import math
164		c = _normSinCos(math.cos(angle))
165		s = _normSinCos(math.sin(angle))
166		return self.transform((c, s, -s, c, 0, 0))
167
168	def skew(self, x=0, y=0):
169		"""Return a new transformation, skewed by x and y.
170
171		Example:
172			>>> import math
173			>>> t = Transform()
174			>>> t.skew(math.pi / 4)
175			<Transform [1 0 1 1 0 0]>
176			>>>
177		"""
178		import math
179		return self.transform((1, math.tan(y), math.tan(x), 1, 0, 0))
180
181	def transform(self, other):
182		"""Return a new transformation, transformed by another
183		transformation.
184
185		Example:
186			>>> t = Transform(2, 0, 0, 3, 1, 6)
187			>>> t.transform((4, 3, 2, 1, 5, 6))
188			<Transform [8 9 4 3 11 24]>
189			>>>
190		"""
191		xx1, xy1, yx1, yy1, dx1, dy1 = other
192		xx2, xy2, yx2, yy2, dx2, dy2 = self.__affine
193		return self.__class__(
194				xx1*xx2 + xy1*yx2,
195				xx1*xy2 + xy1*yy2,
196				yx1*xx2 + yy1*yx2,
197				yx1*xy2 + yy1*yy2,
198				xx2*dx1 + yx2*dy1 + dx2,
199				xy2*dx1 + yy2*dy1 + dy2)
200
201	def reverseTransform(self, other):
202		"""Return a new transformation, which is the other transformation
203		transformed by self. self.reverseTransform(other) is equivalent to
204		other.transform(self).
205
206		Example:
207			>>> t = Transform(2, 0, 0, 3, 1, 6)
208			>>> t.reverseTransform((4, 3, 2, 1, 5, 6))
209			<Transform [8 6 6 3 21 15]>
210			>>> Transform(4, 3, 2, 1, 5, 6).transform((2, 0, 0, 3, 1, 6))
211			<Transform [8 6 6 3 21 15]>
212			>>>
213		"""
214		xx1, xy1, yx1, yy1, dx1, dy1 = self.__affine
215		xx2, xy2, yx2, yy2, dx2, dy2 = other
216		return self.__class__(
217				xx1*xx2 + xy1*yx2,
218				xx1*xy2 + xy1*yy2,
219				yx1*xx2 + yy1*yx2,
220				yx1*xy2 + yy1*yy2,
221				xx2*dx1 + yx2*dy1 + dx2,
222				xy2*dx1 + yy2*dy1 + dy2)
223
224	def inverse(self):
225		"""Return the inverse transformation.
226
227		Example:
228			>>> t = Identity.translate(2, 3).scale(4, 5)
229			>>> t.transformPoint((10, 20))
230			(42, 103)
231			>>> it = t.inverse()
232			>>> it.transformPoint((42, 103))
233			(10.0, 20.0)
234			>>>
235		"""
236		if self.__affine == (1, 0, 0, 1, 0, 0):
237			return self
238		xx, xy, yx, yy, dx, dy = self.__affine
239		det = xx*yy - yx*xy
240		xx, xy, yx, yy = yy/det, -xy/det, -yx/det, xx/det
241		dx, dy = -xx*dx - yx*dy, -xy*dx - yy*dy
242		return self.__class__(xx, xy, yx, yy, dx, dy)
243
244	def toPS(self):
245		"""Return a PostScript representation:
246			>>> t = Identity.scale(2, 3).translate(4, 5)
247			>>> t.toPS()
248			'[2 0 0 3 8 15]'
249			>>>
250		"""
251		return "[%s %s %s %s %s %s]" % self.__affine
252
253	def __len__(self):
254		"""Transform instances also behave like sequences of length 6:
255			>>> len(Identity)
256			6
257			>>>
258		"""
259		return 6
260
261	def __getitem__(self, index):
262		"""Transform instances also behave like sequences of length 6:
263			>>> list(Identity)
264			[1, 0, 0, 1, 0, 0]
265			>>> tuple(Identity)
266			(1, 0, 0, 1, 0, 0)
267			>>>
268		"""
269		return self.__affine[index]
270
271	def __ne__(self, other):
272		return not self.__eq__(other)
273	def __eq__(self, other):
274		"""Transform instances are comparable:
275			>>> t1 = Identity.scale(2, 3).translate(4, 6)
276			>>> t2 = Identity.translate(8, 18).scale(2, 3)
277			>>> t1 == t2
278			1
279			>>>
280
281		But beware of floating point rounding errors:
282			>>> t1 = Identity.scale(0.2, 0.3).translate(0.4, 0.6)
283			>>> t2 = Identity.translate(0.08, 0.18).scale(0.2, 0.3)
284			>>> t1
285			<Transform [0.2 0 0 0.3 0.08 0.18]>
286			>>> t2
287			<Transform [0.2 0 0 0.3 0.08 0.18]>
288			>>> t1 == t2
289			0
290			>>>
291		"""
292		xx1, xy1, yx1, yy1, dx1, dy1 = self.__affine
293		xx2, xy2, yx2, yy2, dx2, dy2 = other
294		return (xx1, xy1, yx1, yy1, dx1, dy1) == \
295				(xx2, xy2, yx2, yy2, dx2, dy2)
296
297	def __hash__(self):
298		"""Transform instances are hashable, meaning you can use them as
299		keys in dictionaries:
300			>>> d = {Scale(12, 13): None}
301			>>> d
302			{<Transform [12 0 0 13 0 0]>: None}
303			>>>
304
305		But again, beware of floating point rounding errors:
306			>>> t1 = Identity.scale(0.2, 0.3).translate(0.4, 0.6)
307			>>> t2 = Identity.translate(0.08, 0.18).scale(0.2, 0.3)
308			>>> t1
309			<Transform [0.2 0 0 0.3 0.08 0.18]>
310			>>> t2
311			<Transform [0.2 0 0 0.3 0.08 0.18]>
312			>>> d = {t1: None}
313			>>> d
314			{<Transform [0.2 0 0 0.3 0.08 0.18]>: None}
315			>>> d[t2]
316			Traceback (most recent call last):
317			  File "<stdin>", line 1, in ?
318			KeyError: <Transform [0.2 0 0 0.3 0.08 0.18]>
319			>>>
320		"""
321		return hash(self.__affine)
322
323	def __bool__(self):
324		"""Returns True if transform is not identity, False otherwise.
325			>>> bool(Identity)
326			False
327			>>> bool(Transform())
328			False
329			>>> bool(Scale(1.))
330			False
331			>>> bool(Scale(2))
332			True
333			>>> bool(Offset())
334			False
335			>>> bool(Offset(0))
336			False
337			>>> bool(Offset(2))
338			True
339		"""
340		return self.__affine != Identity.__affine
341
342	__nonzero__ = __bool__
343
344	def __repr__(self):
345		return "<%s [%g %g %g %g %g %g]>" % ((self.__class__.__name__,) \
346				+ self.__affine)
347
348
349Identity = Transform()
350
351def Offset(x=0, y=0):
352	"""Return the identity transformation offset by x, y.
353
354	Example:
355		>>> Offset(2, 3)
356		<Transform [1 0 0 1 2 3]>
357		>>>
358	"""
359	return Transform(1, 0, 0, 1, x, y)
360
361def Scale(x, y=None):
362	"""Return the identity transformation scaled by x, y. The 'y' argument
363	may be None, which implies to use the x value for y as well.
364
365	Example:
366		>>> Scale(2, 3)
367		<Transform [2 0 0 3 0 0]>
368		>>>
369	"""
370	if y is None:
371		y = x
372	return Transform(x, 0, 0, y, 0, 0)
373
374
375if __name__ == "__main__":
376	import sys
377	import doctest
378	sys.exit(doctest.testmod().failed)
379