From 642ab2eb69ba8638ff7ac5229c6d2c16d9a809fa Mon Sep 17 00:00:00 2001 From: Guillaume Marty Date: Mon, 26 Oct 2026 11:42:02 +0100 Subject: [PATCH 1/2] Allow arithmetic between two IFDRational instances Co-Authored-By: Claude Opus 5.5 (1M context) --- Tests/test_tiff_ifdrational.py | 13 +++++++++++++ src/PIL/TiffImagePlugin.py | 7 ++++++- 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/Tests/test_tiff_ifdrational.py b/Tests/test_tiff_ifdrational.py index c16cd506e65..4f0a2cd4ebc 100644 --- a/Tests/test_tiff_ifdrational.py +++ b/Tests/test_tiff_ifdrational.py @@ -54,6 +54,19 @@ def test_float() -> None: assert math.isnan(float(IFDRational(0, 0))) +def test_arithmetic_between_rationals() -> None: + a = IFDRational(1, 2) + b = IFDRational(1, 3) + + assert a + b == Fraction(5, 6) + assert a - b == Fraction(1, 6) + assert a * b == Fraction(1, 6) + assert a / b == Fraction(3, 2) + assert a // b == 1 + assert a % b == Fraction(1, 6) + assert math.isnan(a + IFDRational(0, 0)) + + def test_ranges() -> None: for num in range(1, 10): for denom in range(1, 10): diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index df0173f2475..9258517e5c2 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -337,7 +337,12 @@ def _delegate(op: str) -> Any: def delegate( self: IFDRational, *args: tuple[float, ...] ) -> bool | float | Fraction: - return getattr(self._val, op)(*args) + result = getattr(self._val, op)(*args) + if result is NotImplemented and args and isinstance(args[0], IFDRational): + # Python does not try the reflected operation + # when both operands are IFDRationals + result = getattr(self._val, op)(args[0]._val) + return result return delegate From c8798b131dd015915e2b2450998c117d80af95e7 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 27 Oct 2026 21:44:09 +1100 Subject: [PATCH 2/2] Simplify code --- Tests/test_tiff_ifdrational.py | 3 --- src/PIL/TiffImagePlugin.py | 17 ++++++++++------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/Tests/test_tiff_ifdrational.py b/Tests/test_tiff_ifdrational.py index 4f0a2cd4ebc..b38c418dd58 100644 --- a/Tests/test_tiff_ifdrational.py +++ b/Tests/test_tiff_ifdrational.py @@ -62,9 +62,6 @@ def test_arithmetic_between_rationals() -> None: assert a - b == Fraction(1, 6) assert a * b == Fraction(1, 6) assert a / b == Fraction(3, 2) - assert a // b == 1 - assert a % b == Fraction(1, 6) - assert math.isnan(a + IFDRational(0, 0)) def test_ranges() -> None: diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index 9258517e5c2..5a9cf6b850d 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -337,12 +337,10 @@ def _delegate(op: str) -> Any: def delegate( self: IFDRational, *args: tuple[float, ...] ) -> bool | float | Fraction: - result = getattr(self._val, op)(*args) - if result is NotImplemented and args and isinstance(args[0], IFDRational): - # Python does not try the reflected operation - # when both operands are IFDRationals - result = getattr(self._val, op)(args[0]._val) - return result + func = getattr(self._val, op) + if len(args) == 1 and isinstance(args[0], IFDRational): + return func(args[0]._val) + return func(*args) return delegate @@ -705,7 +703,12 @@ def _setitem(self, tag: int, value: Any, legacy_api: bool) -> None: self.tagtype[tag] = TiffTags.UNDEFINED if all(isinstance(v, IFDRational) for v in values): for v in values: - if v < IFDRational(0): + value = ( + v.numerator + if isinstance(v, IFDRational) and v.denominator == 0 + else v + ) + if value < IFDRational(0): self.tagtype[tag] = TiffTags.SIGNED_RATIONAL break else: