From 5150c59bba342c6d40100e7daf1070fb53c6d937 Mon Sep 17 00:00:00 2001 From: Aarni Koskela Date: Wed, 2 Sep 2026 13:19:59 +0300 Subject: [PATCH 1/3] Benchmarks: split PATHS to LOAD_PATHS/SAVE_PATHS; benchmark uncompressed DDS --- Tests/benchmarks.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/Tests/benchmarks.py b/Tests/benchmarks.py index 11ed3cf743b..f00f2a895c6 100644 --- a/Tests/benchmarks.py +++ b/Tests/benchmarks.py @@ -45,9 +45,13 @@ # For benchmarks that act on test fixture files, these are the paths loaded. IMAGES_PATH = pathlib.Path(__file__).parent / "images" -PATHS = [ +SAVE_PATHS = [ IMAGES_PATH / "flower2.jpg", ] +LOAD_PATHS = [ + *SAVE_PATHS, + IMAGES_PATH / "uncompressed_rgb.dds", +] # These are derived from the other configuration, above. RGB_MODES = [mode for mode in MODES if mode.startswith("RGB")] @@ -567,7 +571,7 @@ def test_draw_lines_blend( @pytest.mark.benchmark(group="load") -@pytest.mark.parametrize("path", PATHS, ids=_format_path) +@pytest.mark.parametrize("path", LOAD_PATHS, ids=_format_path) def test_load(bench: BenchmarkFixture, path: pathlib.Path) -> None: def run() -> None: with Image.open(path) as im: @@ -577,7 +581,7 @@ def run() -> None: @pytest.mark.benchmark(group="save") -@pytest.mark.parametrize("path", PATHS, ids=_format_path) +@pytest.mark.parametrize("path", SAVE_PATHS, ids=_format_path) def test_save_jpeg(bench: BenchmarkFixture, path: pathlib.Path) -> None: with Image.open(path) as im: im.load() @@ -853,7 +857,7 @@ def test_quantize_grayscale_to_palette( "source_type", [ "synthetic", - *(pytest.param(image, id=f"{image.stem}") for image in PATHS), + *(pytest.param(image, id=f"{image.stem}") for image in LOAD_PATHS), ], ) @pytest.mark.parametrize("palette_type", ["exact", "grayscale", "web"]) From dadbf4f24c89e7c59c01a75cd193cb1f2d301cb3 Mon Sep 17 00:00:00 2001 From: Aarni Koskela Date: Wed, 2 Sep 2026 14:00:15 +0300 Subject: [PATCH 2/3] Speed up uncompressed DDS reading --- src/PIL/DdsImagePlugin.py | 43 +++++++++++++++++++++++++++------------ 1 file changed, 30 insertions(+), 13 deletions(-) diff --git a/src/PIL/DdsImagePlugin.py b/src/PIL/DdsImagePlugin.py index 40012bc27ad..2de82a9aaae 100644 --- a/src/PIL/DdsImagePlugin.py +++ b/src/PIL/DdsImagePlugin.py @@ -20,7 +20,6 @@ from . import Image, ImageFile, ImagePalette from ._binary import i32le as i32 -from ._binary import o8 from ._binary import o32le as o32 TYPE_CHECKING = False @@ -516,20 +515,38 @@ def decode(self, buffer: Image.DecoderInput) -> tuple[int, int]: mask_totals.append(mask >> offset) assert self.fd is not None - dest_length = self.state.xsize * self.state.ysize * len(masks) - while len(data) < dest_length: - bytes_read = self.fd.read(bytecount) - if len(bytes_read) < bytecount: + pixel_count = self.state.xsize * self.state.ysize + + src = bytearray() + needed = pixel_count * bytecount + while len(src) < needed: + chunk = self.fd.read(min(needed - len(src), ImageFile.SAFEBLOCK)) + if not chunk: break - value = int.from_bytes(bytes_read, "little") - for i, mask in enumerate(masks): - masked_value = value & mask + src += chunk + pixel_count = len(src) // bytecount + src_length = pixel_count * bytecount + del src[src_length:] + + nmasks = len(masks) + data = bytearray(pixel_count * nmasks) + for i, (mask, offset, total) in enumerate( + zip(masks, mask_offsets, mask_totals) + ): + if not total: # Nothing to do here + continue + if total == 0xFF and offset % 8 == 0: # Whole byte, fast path + data[i::nmasks] = src[offset // 8 :: bytecount] + continue + values = ( + int.from_bytes(src[p : p + bytecount], "little") + for p in range(0, src_length, bytecount) + ) + data[i::nmasks] = bytes( # Remove the zero padding, and scale it to 8 bits - data += o8( - int(((masked_value >> mask_offsets[i]) / mask_totals[i]) * 255) - if mask_totals[i] - else 0 - ) + int((((value & mask) >> offset) / total) * 255) + for value in values + ) self.set_as_raw(data) return -1, 0 From dc4eeba6f6dc503bd18aa16d79da5ce3bf042d37 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 15 Sep 2026 08:30:22 +0300 Subject: [PATCH 3/3] DDS: Directly use BGR rawmode when possible --- src/PIL/DdsImagePlugin.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/PIL/DdsImagePlugin.py b/src/PIL/DdsImagePlugin.py index 2de82a9aaae..4e71113950e 100644 --- a/src/PIL/DdsImagePlugin.py +++ b/src/PIL/DdsImagePlugin.py @@ -370,8 +370,11 @@ def _open(self) -> None: mask_count = 3 masks = struct.unpack(f"<{mask_count}I", header[84 : 84 + mask_count * 4]) - self.tile = [ImageFile._Tile("dds_rgb", extents, 0, (bitcount, masks))] - return + if masks == (0xFF0000, 0x00FF00, 0x0000FF): + rawmode = "BGR" + else: + self.tile = [ImageFile._Tile("dds_rgb", extents, 0, (bitcount, masks))] + return elif pfflags & DDPF.LUMINANCE: if bitcount == 8: self._mode = "L"