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 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 328Identity = Transform() 329 330def 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 340def 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 354if __name__ == "__main__": 355 import doctest 356 doctest.testmod() 357