From 862c80382ccd9e81d0d586f6f65d9931797f3f6c Mon Sep 17 00:00:00 2001 From: Aarni Koskela Date: Wed, 2 Sep 2026 13:19:59 +0300 Subject: [PATCH 1/2] 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 57f358fee152931838cf41bd170ba2491563d4a3 Mon Sep 17 00:00:00 2001 From: Aarni Koskela Date: Wed, 2 Sep 2026 14:00:15 +0300 Subject: [PATCH 2/2] 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 e09a2b867a1..e2e2effabbb 100644 --- a/src/PIL/DdsImagePlugin.py +++ b/src/PIL/DdsImagePlugin.py @@ -18,7 +18,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 @@ -514,20 +513,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 = min(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