diff --git a/Lib/fontTools/misc/transform.py b/Lib/fontTools/misc/transform.py index e0e5111b7..3e6f028f6 100644 --- a/Lib/fontTools/misc/transform.py +++ b/Lib/fontTools/misc/transform.py @@ -45,7 +45,8 @@ Examples: >>> """ -from fontTools.misc.py23 import * +from typing import NamedTuple + __all__ = ["Transform", "Identity", "Offset", "Scale"] @@ -65,7 +66,7 @@ def _normSinCos(v): return v -class Transform(object): +class Transform(NamedTuple): """2x2 transformation matrix plus offset, a.k.a. Affine transform. Transform instances are immutable: all transforming methods, eg. @@ -82,20 +83,68 @@ class Transform(object): >>> >>> t.scale(2, 3).transformPoint((100, 100)) (200, 300) + + Transform's constructor takes six arguments, all of which are + optional, and can be used as keyword arguments: + >>> Transform(12) + + >>> Transform(dx=12) + + >>> Transform(yx=12) + + + Transform instances also behave like sequences of length 6: + >>> len(Identity) + 6 + >>> list(Identity) + [1, 0, 0, 1, 0, 0] + >>> tuple(Identity) + (1, 0, 0, 1, 0, 0) + + Transform instances are comparable: + >>> t1 = Identity.scale(2, 3).translate(4, 6) + >>> t2 = Identity.translate(8, 18).scale(2, 3) + >>> t1 == t2 + 1 + + But beware of floating point rounding errors: + >>> t1 = Identity.scale(0.2, 0.3).translate(0.4, 0.6) + >>> t2 = Identity.translate(0.08, 0.18).scale(0.2, 0.3) + >>> t1 + + >>> t2 + + >>> t1 == t2 + 0 + + Transform instances are hashable, meaning you can use them as + keys in dictionaries: + >>> d = {Scale(12, 13): None} + >>> d + {: None} + + But again, beware of floating point rounding errors: + >>> t1 = Identity.scale(0.2, 0.3).translate(0.4, 0.6) + >>> t2 = Identity.translate(0.08, 0.18).scale(0.2, 0.3) + >>> t1 + + >>> t2 + + >>> d = {t1: None} + >>> d + {: None} + >>> d[t2] + Traceback (most recent call last): + File "", line 1, in ? + KeyError: """ - def __init__(self, xx=1, xy=0, yx=0, yy=1, dx=0, dy=0): - """Transform's constructor takes six arguments, all of which are - optional, and can be used as keyword arguments: - >>> Transform(12) - - >>> Transform(dx=12) - - >>> Transform(yx=12) - - >>> - """ - self.__affine = xx, xy, yx, yy, dx, dy + xx: float = 1 + xy: float = 0 + yx: float = 0 + yy: float = 1 + dx: float = 0 + dy: float = 0 def transformPoint(self, p): """Transform a point. @@ -107,7 +156,7 @@ class Transform(object): (250.0, 550.0) """ (x, y) = p - xx, xy, yx, yy, dx, dy = self.__affine + xx, xy, yx, yy, dx, dy = self return (xx*x + yx*y + dx, xy*x + yy*y + dy) def transformPoints(self, points): @@ -119,7 +168,7 @@ class Transform(object): [(0, 0), (0, 300), (200, 300), (200, 0)] >>> """ - xx, xy, yx, yy, dx, dy = self.__affine + xx, xy, yx, yy, dx, dy = self return [(xx*x + yx*y + dx, xy*x + yy*y + dy) for x, y in points] def translate(self, x=0, y=0): @@ -188,7 +237,7 @@ class Transform(object): >>> """ xx1, xy1, yx1, yy1, dx1, dy1 = other - xx2, xy2, yx2, yy2, dx2, dy2 = self.__affine + xx2, xy2, yx2, yy2, dx2, dy2 = self return self.__class__( xx1*xx2 + xy1*yx2, xx1*xy2 + xy1*yy2, @@ -210,7 +259,7 @@ class Transform(object): >>> """ - xx1, xy1, yx1, yy1, dx1, dy1 = self.__affine + xx1, xy1, yx1, yy1, dx1, dy1 = self xx2, xy2, yx2, yy2, dx2, dy2 = other return self.__class__( xx1*xx2 + xy1*yx2, @@ -232,9 +281,9 @@ class Transform(object): (10.0, 20.0) >>> """ - if self.__affine == (1, 0, 0, 1, 0, 0): + if self == Identity: return self - xx, xy, yx, yy, dx, dy = self.__affine + xx, xy, yx, yy, dx, dy = self det = xx*yy - yx*xy xx, xy, yx, yy = yy/det, -xy/det, -yx/det, xx/det dx, dy = -xx*dx - yx*dy, -xy*dx - yy*dy @@ -247,77 +296,7 @@ class Transform(object): '[2 0 0 3 8 15]' >>> """ - return "[%s %s %s %s %s %s]" % self.__affine - - def __len__(self): - """Transform instances also behave like sequences of length 6: - >>> len(Identity) - 6 - >>> - """ - return 6 - - def __getitem__(self, index): - """Transform instances also behave like sequences of length 6: - >>> list(Identity) - [1, 0, 0, 1, 0, 0] - >>> tuple(Identity) - (1, 0, 0, 1, 0, 0) - >>> - """ - return self.__affine[index] - - def __ne__(self, other): - return not self.__eq__(other) - def __eq__(self, other): - """Transform instances are comparable: - >>> t1 = Identity.scale(2, 3).translate(4, 6) - >>> t2 = Identity.translate(8, 18).scale(2, 3) - >>> t1 == t2 - 1 - >>> - - But beware of floating point rounding errors: - >>> t1 = Identity.scale(0.2, 0.3).translate(0.4, 0.6) - >>> t2 = Identity.translate(0.08, 0.18).scale(0.2, 0.3) - >>> t1 - - >>> t2 - - >>> t1 == t2 - 0 - >>> - """ - xx1, xy1, yx1, yy1, dx1, dy1 = self.__affine - xx2, xy2, yx2, yy2, dx2, dy2 = other - return (xx1, xy1, yx1, yy1, dx1, dy1) == \ - (xx2, xy2, yx2, yy2, dx2, dy2) - - def __hash__(self): - """Transform instances are hashable, meaning you can use them as - keys in dictionaries: - >>> d = {Scale(12, 13): None} - >>> d - {: None} - >>> - - But again, beware of floating point rounding errors: - >>> t1 = Identity.scale(0.2, 0.3).translate(0.4, 0.6) - >>> t2 = Identity.translate(0.08, 0.18).scale(0.2, 0.3) - >>> t1 - - >>> t2 - - >>> d = {t1: None} - >>> d - {: None} - >>> d[t2] - Traceback (most recent call last): - File "", line 1, in ? - KeyError: - >>> - """ - return hash(self.__affine) + return "[%s %s %s %s %s %s]" % self def __bool__(self): """Returns True if transform is not identity, False otherwise. @@ -336,13 +315,12 @@ class Transform(object): >>> bool(Offset(2)) True """ - return self.__affine != Identity.__affine + return self != Identity __nonzero__ = __bool__ def __repr__(self): - return "<%s [%g %g %g %g %g %g]>" % ((self.__class__.__name__,) \ - + self.__affine) + return "<%s [%g %g %g %g %g %g]>" % ((self.__class__.__name__,) + self) Identity = Transform()