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

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 

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 

21 

22The DecomposedTransform class implements a transformation with separate 

23translate, rotation, scale, skew, and transformation-center components. 

24 

25:Example: 

26 

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""" 

54 

55import math 

56from typing import NamedTuple 

57from dataclasses import dataclass 

58 

59 

60__all__ = ["Transform", "Identity", "Offset", "Scale", "DecomposedTransform"] 

61 

62 

63_EPSILON = 1e-15 

64_ONE_EPSILON = 1 - _EPSILON 

65_MINUS_ONE_EPSILON = -1 + _EPSILON 

66 

67 

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 

76 

77 

78class Transform(NamedTuple): 

79 

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. 

83 

84 :Example: 

85 

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) 

96 

97 Transform's constructor takes six arguments, all of which are 

98 optional, and can be used as keyword arguments:: 

99 

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]> 

106 

107 Transform instances also behave like sequences of length 6:: 

108 

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) 

115 

116 Transform instances are comparable:: 

117 

118 >>> t1 = Identity.scale(2, 3).translate(4, 6) 

119 >>> t2 = Identity.translate(8, 18).scale(2, 3) 

120 >>> t1 == t2 

121 1 

122 

123 But beware of floating point rounding errors:: 

124 

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 

133 

134 Transform instances are hashable, meaning you can use them as 

135 keys in dictionaries:: 

136 

137 >>> d = {Scale(12, 13): None} 

138 >>> d 

139 {<Transform [12 0 0 13 0 0]>: None} 

140 

141 But again, beware of floating point rounding errors:: 

142 

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 """ 

157 

158 xx: float = 1 

159 xy: float = 0 

160 yx: float = 0 

161 yy: float = 1 

162 dx: float = 0 

163 dy: float = 0 

164 

165 def transformPoint(self, p): 

166 """Transform a point. 

167 

168 :Example: 

169 

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) 

178 

179 def transformPoints(self, points): 

180 """Transform a list of points. 

181 

182 :Example: 

183 

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] 

191 

192 def transformVector(self, v): 

193 """Transform an (dx, dy) vector, treating translation as zero. 

194 

195 :Example: 

196 

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) 

205 

206 def transformVectors(self, vectors): 

207 """Transform a list of (dx, dy) vector, treating translation as zero. 

208 

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] 

217 

218 def translate(self, x=0, y=0): 

219 """Return a new transformation, translated (offset) by x, y. 

220 

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)) 

228 

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. 

232 

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)) 

244 

245 def rotate(self, angle): 

246 """Return a new transformation, rotated by 'angle' (radians). 

247 

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 

256 

257 c = _normSinCos(math.cos(angle)) 

258 s = _normSinCos(math.sin(angle)) 

259 return self.transform((c, s, -s, c, 0, 0)) 

260 

261 def skew(self, x=0, y=0): 

262 """Return a new transformation, skewed by x and y. 

263 

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 

272 

273 return self.transform((1, math.tan(y), math.tan(x), 1, 0, 0)) 

274 

275 def transform(self, other): 

276 """Return a new transformation, transformed by another 

277 transformation. 

278 

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 ) 

295 

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). 

300 

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 ) 

319 

320 def inverse(self): 

321 """Return the inverse transformation. 

322 

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) 

339 

340 def toPS(self): 

341 """Return a PostScript representation 

342 

343 :Example: 

344 

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 

351 

352 def toDecomposed(self) -> "DecomposedTransform": 

353 """Decompose into a DecomposedTransform.""" 

354 return DecomposedTransform.fromTransform(self) 

355 

356 def __bool__(self): 

357 """Returns True if transform is not identity, False otherwise. 

358 

359 :Example: 

360 

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 

377 

378 def __repr__(self): 

379 return "<%s [%g %g %g %g %g %g]>" % ((self.__class__.__name__,) + self) 

380 

381 

382Identity = Transform() 

383 

384 

385def Offset(x=0, y=0): 

386 """Return the identity transformation offset by x, y. 

387 

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) 

394 

395 

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. 

399 

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) 

408 

409 

410@dataclass 

411class DecomposedTransform: 

412 """The DecomposedTransform class implements a transformation with separate 

413 translate, rotation, scale, skew, and transformation-center components. 

414 """ 

415 

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 

425 

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 

431 

432 sx = math.copysign(1, a) 

433 if sx < 0: 

434 a *= sx 

435 b *= sx 

436 

437 delta = a * d - b * c 

438 

439 rotation = 0 

440 scaleX = scaleY = 0 

441 skewX = skewY = 0 

442 

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 

459 

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 ) 

471 

472 def toTransform(self): 

473 """Return the Transform() equivalent of this transformation. 

474 

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 

489 

490 

491if __name__ == "__main__": 

492 import sys 

493 import doctest 

494 

495 sys.exit(doctest.testmod().failed)