From cf9dece8f269384ba06e966348f7d19308ff1ed8 Mon Sep 17 00:00:00 2001 From: Behdad Esfahbod Date: Fri, 3 Feb 2023 09:54:43 -0700 Subject: [PATCH] [transform] Add Transform.toDecomposed() --- Lib/fontTools/misc/transform.py | 38 +++++++++++++++++++++++++++++++++ Tests/misc/transform_test.py | 8 +++++++ 2 files changed, 46 insertions(+) diff --git a/Lib/fontTools/misc/transform.py b/Lib/fontTools/misc/transform.py index 1e5c8693e..101652969 100644 --- a/Lib/fontTools/misc/transform.py +++ b/Lib/fontTools/misc/transform.py @@ -349,6 +349,44 @@ class Transform(NamedTuple): """ return "[%s %s %s %s %s %s]" % self + def toDecomposed(self) -> "DecomposedTransform": + """Decompose into a DecomposedTransform.""" + # Adapted from an answer on + # https://math.stackexchange.com/questions/13150/extracting-rotation-scale-values-from-2d-transformation-matrix + a, b, c, d, x, y = self + delta = a * d - b * c + + rotation = 0 + scaleX = scaleY = 0 + skewX = skewY = 0 + + # Apply the QR-like decomposition. + if a != 0 or b != 0: + r = math.sqrt(a * a + b * b) + rotation = math.acos(a / r) if b > 0 else -math.acos(a / r) + scaleX, scaleY = (r, delta / r) + skewX, skewY = (math.atan((a * c + b * d) / (r * r)), 0) + elif c != 0 or d != 0: + s = math.sqrt(c * c + d * d) + rotation = math.pi / 2 - (math.acos(-c / s) if d > 0 else -math.acos(c / s)) + scaleX, scaleY = (delta / s, s) + skewX, skewY = (0, math.atan((a * c + b * d) / (s * s))) + else: + # a = b = c = d = 0 + pass + + return DecomposedTransform( + x, + y, + math.degrees(rotation), + scaleX, + scaleY, + -math.degrees(skewX), + math.degrees(skewY), + 0, + 0, + ) + def __bool__(self): """Returns True if transform is not identity, False otherwise. diff --git a/Tests/misc/transform_test.py b/Tests/misc/transform_test.py index 0e978cd2b..3c9b57cfd 100644 --- a/Tests/misc/transform_test.py +++ b/Tests/misc/transform_test.py @@ -115,6 +115,14 @@ class TransformTest(object): assert Scale(2) == Transform(2, 0, 0, 2, 0, 0) assert Scale(1, 2) == Transform(1, 0, 0, 2, 0, 0) + def test_decompose(self): + t = Transform(2, 0, 0, 3, 5, 7) + d = t.toDecomposed() + assert d.scaleX == 2 + assert d.scaleY == 3 + assert d.translateX == 5 + assert d.translateY == 7 + class DecomposedTransformTest(object): def test_identity(self):