Home | History | Annotate | Download | only in misc
      1 """Affine 2D transformation matrix class.
      2 
      3 The Transform class implements various transformation matrix operations,
      4 both on the matrix itself, as well as on 2D coordinates.
      5 
      6 Transform instances are effectively immutable: all methods that operate on the
      7 transformation itself always return a new instance. This has as the
      8 interesting side effect that Transform instances are hashable, ie. they can be
      9 used as dictionary keys.
     10 
     11 This 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 
     18 Examples:
     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 
     48 from __future__ import print_function, division, absolute_import
     49 from 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 
     59 def _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 
     69 class 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 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 0.0 1.0 1.0 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.0 0.3 0.08 0.18]>
    286 			>>> t2
    287 			<Transform [0.2 0.0 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.0 0.3 0.08 0.18]>
    310 			>>> t2
    311 			<Transform [0.2 0.0 0.0 0.3 0.08 0.18]>
    312 			>>> d = {t1: None}
    313 			>>> d
    314 			{<Transform [0.2 0.0 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.0 0.3 0.08 0.18]>
    319 			>>>
    320 		"""
    321 		return hash(self.__affine)
    322 
    323 	def __repr__(self):
    324 		return "<%s [%s %s %s %s %s %s]>" % ((self.__class__.__name__,) \
    325 				 + tuple(map(str, self.__affine)))
    326 
    327 
    328 Identity = Transform()
    329 
    330 def Offset(x=0, y=0):
    331 	"""Return the identity transformation offset by x, y.
    332 
    333 	Example:
    334 		>>> Offset(2, 3)
    335 		<Transform [1 0 0 1 2 3]>
    336 		>>>
    337 	"""
    338 	return Transform(1, 0, 0, 1, x, y)
    339 
    340 def Scale(x, y=None):
    341 	"""Return the identity transformation scaled by x, y. The 'y' argument
    342 	may be None, which implies to use the x value for y as well.
    343 
    344 	Example:
    345 		>>> Scale(2, 3)
    346 		<Transform [2 0 0 3 0 0]>
    347 		>>>
    348 	"""
    349 	if y is None:
    350 		y = x
    351 	return Transform(x, 0, 0, y, 0, 0)
    352 
    353 
    354 if __name__ == "__main__":
    355 	import doctest
    356 	doctest.testmod()
    357