[transform] Improve DecomposedTransform

And add tests.

See thread starting at:
https://github.com/fonttools/fonttools/pull/2958#issuecomment-1416859441
This commit is contained in:
Behdad Esfahbod 2023-02-04 15:33:26 -07:00
parent 08d03a82b2
commit 4355d006ad
2 changed files with 71 additions and 4 deletions

View File

@ -407,6 +407,10 @@ def Scale(x, y=None):
return Transform(x, 0, 0, y, 0, 0)
def _sign(v):
return +1 if v >= 0 else -1
@dataclass
class DecomposedTransform:
"""The DecomposedTransform class implements a transformation with separate
@ -428,6 +432,12 @@ class 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 = transform
sx = _sign(a)
if sx < 0:
a *= sx
b *= sx
delta = a * d - b * c
rotation = 0
@ -437,12 +447,14 @@ class DecomposedTransform:
# 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)
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))
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:
@ -453,9 +465,9 @@ class DecomposedTransform:
x,
y,
math.degrees(rotation),
scaleX,
scaleX * sx,
scaleY,
math.degrees(skewX),
math.degrees(skewX) * sx,
math.degrees(skewY),
0,
0,

View File

@ -123,6 +123,19 @@ class TransformTest(object):
assert d.translateX == 5
assert d.translateY == 7
def test_decompose(self):
t = Transform(-1, 0, 0, 1, 0, 0)
d = t.toDecomposed()
assert d.scaleX == -1
assert d.scaleY == 1
assert d.rotation == 0
t = Transform(1, 0, 0, -1, 0, 0)
d = t.toDecomposed()
assert d.scaleX == 1
assert d.scaleY == -1
assert d.rotation == 0
class DecomposedTransformTest(object):
def test_identity(self):
@ -141,3 +154,45 @@ class DecomposedTransformTest(object):
def test_toTransform(self):
t = DecomposedTransform(scaleX=2, scaleY=3)
assert t.toTransform() == (2, 0, 0, 3, 0, 0)
@pytest.mark.parametrize(
"decomposed",
[
DecomposedTransform(scaleX=1, scaleY=0),
DecomposedTransform(scaleX=0, scaleY=1),
DecomposedTransform(scaleX=1, scaleY=0, rotation=30),
DecomposedTransform(scaleX=0, scaleY=1, rotation=30),
DecomposedTransform(scaleX=1, scaleY=1),
DecomposedTransform(scaleX=-1, scaleY=1),
DecomposedTransform(scaleX=1, scaleY=-1),
DecomposedTransform(scaleX=-1, scaleY=-1),
DecomposedTransform(rotation=90),
DecomposedTransform(rotation=-90),
DecomposedTransform(skewX=45),
DecomposedTransform(skewY=45),
DecomposedTransform(scaleX=-1, skewX=45),
DecomposedTransform(scaleX=-1, skewY=45),
DecomposedTransform(scaleY=-1, skewX=45),
DecomposedTransform(scaleY=-1, skewY=45),
DecomposedTransform(scaleX=-1, skewX=45, rotation=30),
DecomposedTransform(scaleX=-1, skewY=45, rotation=30),
DecomposedTransform(scaleY=-1, skewX=45, rotation=30),
DecomposedTransform(scaleY=-1, skewY=45, rotation=30),
DecomposedTransform(scaleX=-1, skewX=45, rotation=-30),
DecomposedTransform(scaleX=-1, skewY=45, rotation=-30),
DecomposedTransform(scaleY=-1, skewX=45, rotation=-30),
DecomposedTransform(scaleY=-1, skewY=45, rotation=-30),
DecomposedTransform(scaleX=-2, skewX=45, rotation=30),
DecomposedTransform(scaleX=-2, skewY=45, rotation=30),
DecomposedTransform(scaleY=-2, skewX=45, rotation=30),
DecomposedTransform(scaleY=-2, skewY=45, rotation=30),
DecomposedTransform(scaleX=-2, skewX=45, rotation=-30),
DecomposedTransform(scaleX=-2, skewY=45, rotation=-30),
DecomposedTransform(scaleY=-2, skewX=45, rotation=-30),
DecomposedTransform(scaleY=-2, skewY=45, rotation=-30),
],
)
def test_roundtrip(lst, decomposed):
assert decomposed.toTransform().toDecomposed().toTransform() == pytest.approx(
tuple(decomposed.toTransform())
), decomposed