From 3ac0420d6cfdfc72c784ee9ee24a1c2dfb3b7757 Mon Sep 17 00:00:00 2001 From: Enric Pou Date: Wed, 21 May 2025 13:48:46 +0200 Subject: [PATCH 1/7] Fix: Set tag type accordingly if IFDRational with 0 denominator --- Tests/test_file_tiff_metadata.py | 25 +++++++++++++++++++++++++ Tests/test_tiff_ifdrational.py | 11 +++++++++++ src/PIL/TiffImagePlugin.py | 5 ++++- 3 files changed, 40 insertions(+), 1 deletion(-) diff --git a/Tests/test_file_tiff_metadata.py b/Tests/test_file_tiff_metadata.py index 884868345..fbaec7835 100644 --- a/Tests/test_file_tiff_metadata.py +++ b/Tests/test_file_tiff_metadata.py @@ -1,6 +1,7 @@ from __future__ import annotations import io +import math import struct from pathlib import Path @@ -280,6 +281,30 @@ def test_writing_other_types_to_undefined( assert reloaded.tag_v2[33723] == b"1" +@pytest.mark.parametrize( + "value, expected", + ( + (IFDRational(1, 0), TiffTags.RATIONAL), + (IFDRational(-1, 0), TiffTags.SIGNED_RATIONAL), + ), +) +def test_tagtype_on_zero_denominator( + value: IFDRational, expected: int, tmp_path: Path +) -> None: + info = TiffImagePlugin.ImageFileDirectory_v2() + + info[37380] = value + assert info.tagtype[37380] == expected + + im = hopper() + out = tmp_path / "temp.tiff" + im.save(out, tiffinfo=info) + + with Image.open(out) as reloaded: + assert isinstance(reloaded, TiffImagePlugin.TiffImageFile) + assert math.isnan(reloaded.tag_v2[37380]) + + def test_undefined_zero(tmp_path: Path) -> None: # Check that the tag has not been changed since this test was created tag = TiffTags.TAGS_V2[45059] diff --git a/Tests/test_tiff_ifdrational.py b/Tests/test_tiff_ifdrational.py index 42d06b896..5233cb2b9 100644 --- a/Tests/test_tiff_ifdrational.py +++ b/Tests/test_tiff_ifdrational.py @@ -74,3 +74,14 @@ def test_ifd_rational_save( with Image.open(out) as reloaded: assert isinstance(reloaded, TiffImagePlugin.TiffImageFile) assert float(IFDRational(301, 1)) == float(reloaded.tag_v2[282]) + + +@pytest.mark.parametrize( + "numerator, denominator, expected_result", + [ + (1, 1, 1.0), + (1, 0, float("nan")), + ], +) +def test_float_cast(numerator, denominator, expected_result): + float(IFDRational(numerator, denominator)) == expected_result diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index 88af9162e..0147f79f2 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -402,6 +402,9 @@ class IFDRational(Rational): f = self._val.limit_denominator(max_denominator) return f.numerator, f.denominator + def __float__(self): + return float(self._val) + def __repr__(self) -> str: return str(float(self._val)) @@ -688,7 +691,7 @@ class ImageFileDirectory_v2(_IFDv2Base): if all(isinstance(v, IFDRational) for v in values): for v in values: assert isinstance(v, IFDRational) - if v < 0: + if v < 0 or (math.isnan(v) and v.numerator < 0): self.tagtype[tag] = TiffTags.SIGNED_RATIONAL break else: From c1f0c23b47970c28ed286c104dad51d40db0a9bf Mon Sep 17 00:00:00 2001 From: Enric Pou Date: Thu, 22 May 2025 08:57:41 +0200 Subject: [PATCH 2/7] Mypy fix: Cast v.numerator to float --- src/PIL/TiffImagePlugin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index 0147f79f2..9bdf7c015 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -691,7 +691,7 @@ class ImageFileDirectory_v2(_IFDv2Base): if all(isinstance(v, IFDRational) for v in values): for v in values: assert isinstance(v, IFDRational) - if v < 0 or (math.isnan(v) and v.numerator < 0): + if v < 0 or (math.isnan(v) and float(v.numerator) < 0): self.tagtype[tag] = TiffTags.SIGNED_RATIONAL break else: From 025f3ba23a06845e49ea6a23da27bc591d893369 Mon Sep 17 00:00:00 2001 From: Enric Pou Date: Mon, 26 May 2025 09:12:54 +0200 Subject: [PATCH 3/7] Using math.info to treat 0 denominators for IFDRational --- Tests/test_file_tiff_metadata.py | 5 ++++- Tests/test_tiff_ifdrational.py | 11 +++++++++-- src/PIL/TiffImagePlugin.py | 9 +++++++-- 3 files changed, 20 insertions(+), 5 deletions(-) diff --git a/Tests/test_file_tiff_metadata.py b/Tests/test_file_tiff_metadata.py index fbaec7835..399736750 100644 --- a/Tests/test_file_tiff_metadata.py +++ b/Tests/test_file_tiff_metadata.py @@ -302,7 +302,10 @@ def test_tagtype_on_zero_denominator( with Image.open(out) as reloaded: assert isinstance(reloaded, TiffImagePlugin.TiffImageFile) - assert math.isnan(reloaded.tag_v2[37380]) + if expected == TiffTags.RATIONAL: + assert reloaded.tag_v2[37380] == math.inf + elif TiffTags.SIGNED_RATIONAL: + assert reloaded.tag_v2[37380] == -math.inf def test_undefined_zero(tmp_path: Path) -> None: diff --git a/Tests/test_tiff_ifdrational.py b/Tests/test_tiff_ifdrational.py index 5233cb2b9..efb51cf9c 100644 --- a/Tests/test_tiff_ifdrational.py +++ b/Tests/test_tiff_ifdrational.py @@ -1,6 +1,7 @@ from __future__ import annotations from fractions import Fraction +import math from pathlib import Path import pytest @@ -80,8 +81,14 @@ def test_ifd_rational_save( "numerator, denominator, expected_result", [ (1, 1, 1.0), - (1, 0, float("nan")), + (1, 0, math.inf), + (-1, 0, -math.inf), + (0, 0, float("nan")), ], ) def test_float_cast(numerator, denominator, expected_result): - float(IFDRational(numerator, denominator)) == expected_result + value = float(IFDRational(numerator, denominator)) + if math.isnan(expected_result): + assert value + else: + assert value == expected_result diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index 9bdf7c015..272adee99 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -372,7 +372,12 @@ class IFDRational(Rational): self._denominator = denominator if denominator == 0: - self._val = float("nan") + if value == 0: + self._val = float("nan") + elif value > 0: + self._val = math.inf + else: + self._val = -math.inf elif denominator == 1: self._val = Fraction(value) elif int(value) == value: @@ -691,7 +696,7 @@ class ImageFileDirectory_v2(_IFDv2Base): if all(isinstance(v, IFDRational) for v in values): for v in values: assert isinstance(v, IFDRational) - if v < 0 or (math.isnan(v) and float(v.numerator) < 0): + if v < 0: self.tagtype[tag] = TiffTags.SIGNED_RATIONAL break else: From 4a486e4ec2bec1894125dedf15c285bfff4f719d Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 26 May 2025 07:20:47 +0000 Subject: [PATCH 4/7] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- Tests/test_tiff_ifdrational.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/test_tiff_ifdrational.py b/Tests/test_tiff_ifdrational.py index efb51cf9c..e212ba64a 100644 --- a/Tests/test_tiff_ifdrational.py +++ b/Tests/test_tiff_ifdrational.py @@ -1,7 +1,7 @@ from __future__ import annotations -from fractions import Fraction import math +from fractions import Fraction from pathlib import Path import pytest From e393f01a731ceb08872e05995f376cf9bf3bbb4e Mon Sep 17 00:00:00 2001 From: Enric Pou Date: Wed, 28 May 2025 08:27:31 +0200 Subject: [PATCH 5/7] Add test_float_cast typing Co-authored-by: Andrew Murray <3112309+radarhere@users.noreply.github.com> --- Tests/test_tiff_ifdrational.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/test_tiff_ifdrational.py b/Tests/test_tiff_ifdrational.py index e212ba64a..4fd5b8fbe 100644 --- a/Tests/test_tiff_ifdrational.py +++ b/Tests/test_tiff_ifdrational.py @@ -86,7 +86,7 @@ def test_ifd_rational_save( (0, 0, float("nan")), ], ) -def test_float_cast(numerator, denominator, expected_result): +def test_float_cast(numerator: int, denominator: int, expected_result: float) -> None: value = float(IFDRational(numerator, denominator)) if math.isnan(expected_result): assert value From ee26eb8608a12adfbb2a8c543b5dfd2182132bb9 Mon Sep 17 00:00:00 2001 From: Enric Pou Date: Wed, 28 May 2025 08:27:41 +0200 Subject: [PATCH 6/7] Add __float__ typing Co-authored-by: Andrew Murray <3112309+radarhere@users.noreply.github.com> --- src/PIL/TiffImagePlugin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index 272adee99..bbc9ffce8 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -407,7 +407,7 @@ class IFDRational(Rational): f = self._val.limit_denominator(max_denominator) return f.numerator, f.denominator - def __float__(self): + def __float__(self) -> float: return float(self._val) def __repr__(self) -> str: From f86f11c84381c5ced4e6f59b222f661663e55387 Mon Sep 17 00:00:00 2001 From: Enric Pou Date: Fri, 30 May 2025 17:36:00 +0200 Subject: [PATCH 7/7] Update Tests/test_file_tiff_metadata.py Co-authored-by: Andrew Murray <3112309+radarhere@users.noreply.github.com> --- Tests/test_file_tiff_metadata.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/test_file_tiff_metadata.py b/Tests/test_file_tiff_metadata.py index 399736750..01f63a0a9 100644 --- a/Tests/test_file_tiff_metadata.py +++ b/Tests/test_file_tiff_metadata.py @@ -304,7 +304,7 @@ def test_tagtype_on_zero_denominator( assert isinstance(reloaded, TiffImagePlugin.TiffImageFile) if expected == TiffTags.RATIONAL: assert reloaded.tag_v2[37380] == math.inf - elif TiffTags.SIGNED_RATIONAL: + elif expected == TiffTags.SIGNED_RATIONAL: assert reloaded.tag_v2[37380] == -math.inf