From 88dc42ff50e849acf4fabf5c0dbeffb72ebdf3c0 Mon Sep 17 00:00:00 2001 From: nyxst4ck <289980115+nyxst4ck@users.noreply.github.com> Date: Tue, 16 Jun 2026 20:00:31 -0300 Subject: [PATCH 1/2] Fix IFDRational.__float__ returning the wrong value IFDRational delegates __eq__, __int__, __round__, __repr__ and arithmetic to self._val (the normalized Fraction), but never defined __float__. For a non-integral numerator (e.g. IFDRational(1.5, 3), whose value is 0.5) float() fell back to numbers.Rational.__float__, which computes int(numerator) / int(denominator) = int(1.5) / int(3) = 0.333..., disagreeing with ==, int() and repr() (all 0.5). Delegate __float__ to self._val so float() is consistent with the rest of the class. --- Tests/test_tiff_ifdrational.py | 15 +++++++++++++++ src/PIL/TiffImagePlugin.py | 1 + 2 files changed, 16 insertions(+) diff --git a/Tests/test_tiff_ifdrational.py b/Tests/test_tiff_ifdrational.py index 42d06b89696..93501351d76 100644 --- a/Tests/test_tiff_ifdrational.py +++ b/Tests/test_tiff_ifdrational.py @@ -1,5 +1,6 @@ from __future__ import annotations +import math from fractions import Fraction from pathlib import Path @@ -38,6 +39,20 @@ def test_sanity() -> None: _test_equal(7, 5, 1.4) +def test_float() -> None: + # float() must agree with ==, int() and repr(), which all use the + # normalized fraction. For a non-integral numerator (1.5 / 3 == 0.5) the + # inherited Rational.__float__ used int(numerator) / int(denominator) and + # returned 1/3 instead. + r = IFDRational(1.5, 3) + assert r == 0.5 + assert float(r) == 0.5 + + # Integral and 0/0 (nan) cases stay correct. + assert float(IFDRational(4, 2)) == 2.0 + assert math.isnan(float(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 5094faa1325..02ce330d034 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -464,6 +464,7 @@ def __setstate__(self, state: list[float | Fraction | IntegralLike]) -> None: __ceil__ = _delegate("__ceil__") __floor__ = _delegate("__floor__") __round__ = _delegate("__round__") + __float__ = _delegate("__float__") # Python >= 3.11 if hasattr(Fraction, "__int__"): __int__ = _delegate("__int__") From 2e093e445b2e6daf27302fb5ec871474a179a271 Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Fri, 19 Jun 2026 19:19:08 +1000 Subject: [PATCH 2/2] Updated comments Co-authored-by: Andrew Murray <3112309+radarhere@users.noreply.github.com> --- Tests/test_tiff_ifdrational.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/Tests/test_tiff_ifdrational.py b/Tests/test_tiff_ifdrational.py index 93501351d76..8cf48ba7dc6 100644 --- a/Tests/test_tiff_ifdrational.py +++ b/Tests/test_tiff_ifdrational.py @@ -41,14 +41,12 @@ def test_sanity() -> None: def test_float() -> None: # float() must agree with ==, int() and repr(), which all use the - # normalized fraction. For a non-integral numerator (1.5 / 3 == 0.5) the - # inherited Rational.__float__ used int(numerator) / int(denominator) and - # returned 1/3 instead. + # normalized fraction r = IFDRational(1.5, 3) assert r == 0.5 assert float(r) == 0.5 - # Integral and 0/0 (nan) cases stay correct. + # Integer numerator and 0/0 (nan) cases assert float(IFDRational(4, 2)) == 2.0 assert math.isnan(float(IFDRational(0, 0)))