diff --git a/Tests/test_file_jpeg.py b/Tests/test_file_jpeg.py index 1110262ed2d..deed84495c0 100644 --- a/Tests/test_file_jpeg.py +++ b/Tests/test_file_jpeg.py @@ -15,6 +15,7 @@ ImageFile, ImageOps, JpegImagePlugin, + TiffImagePlugin, UnidentifiedImageError, features, ) @@ -879,6 +880,40 @@ def test_dpi_from_dpcm_exif(self) -> None: # Act / Assert assert im.info.get("dpi") == (508, 508) + def test_dpi_from_exif_different_axes(self) -> None: + exif = Image.Exif() + exif[0x011A] = 300.0 # XResolution + exif[0x011B] = 150.0 # YResolution + exif[0x0128] = 2 # ResolutionUnit: inches + buf = BytesIO() + Image.new("RGB", (16, 16)).save(buf, "JPEG", exif=exif) + + # Remove the JFIF APP0 segment, so that DPI must be read from EXIF + data = buf.getvalue() + assert data[2:4] == b"\xff\xe0" + app0_length = int.from_bytes(data[4:6], "big") + data = data[:2] + data[4 + app0_length :] + + with Image.open(BytesIO(data)) as im: + assert im.info.get("dpi") == (300, 150) + + def test_dpi_from_exif_invalid_y_resolution(self) -> None: + exif = Image.Exif() + exif[0x011A] = 300.0 # XResolution + exif[0x011B] = TiffImagePlugin.IFDRational(0, 0) # invalid YResolution + exif[0x0128] = 2 # ResolutionUnit: inches + buf = BytesIO() + Image.new("RGB", (16, 16)).save(buf, "JPEG", exif=exif) + + # Remove the JFIF APP0 segment, so that DPI must be read from EXIF + data = buf.getvalue() + assert data[2:4] == b"\xff\xe0" + app0_length = int.from_bytes(data[4:6], "big") + data = data[:2] + data[4 + app0_length :] + + with Image.open(BytesIO(data)) as im: + assert im.info.get("dpi") == (300, 300) + def test_dpi_exif_zero_division(self) -> None: # Arrange # This is photoshop-200dpi.jpg with EXIF resolution set to 0/0: diff --git a/src/PIL/JpegImagePlugin.py b/src/PIL/JpegImagePlugin.py index dd0a470a736..410afa90ac5 100644 --- a/src/PIL/JpegImagePlugin.py +++ b/src/PIL/JpegImagePlugin.py @@ -521,18 +521,30 @@ def _read_dpi_from_exif(self) -> None: try: exif = self.getexif() resolution_unit = exif[0x0128] - x_resolution = exif[0x011A] - try: - dpi = float(x_resolution[0]) / x_resolution[1] - except TypeError: - dpi = x_resolution - if math.isnan(dpi): - msg = "DPI is not a number" - raise ValueError(msg) - if resolution_unit == 3: # cm - # 1 dpcm = 2.54 dpi - dpi *= 2.54 - self.info["dpi"] = dpi, dpi + + def to_dpi(resolution: Any) -> float: + try: + dpi = float(resolution[0]) / resolution[1] + except TypeError: + dpi = resolution + if math.isnan(dpi): + msg = "DPI is not a number" + raise ValueError(msg) + if resolution_unit == 3: # cm + # 1 dpcm = 2.54 dpi + dpi *= 2.54 + return dpi + + x_dpi = to_dpi(exif[0x011A]) + y_dpi = x_dpi + y_resolution = exif.get(0x011B) + if y_resolution is not None: + try: + y_dpi = to_dpi(y_resolution) + except (TypeError, ValueError, ZeroDivisionError): + # Keep the X resolution if the Y resolution is invalid + pass + self.info["dpi"] = x_dpi, y_dpi except ( struct.error, # truncated EXIF KeyError, # dpi not included