From cd11792c15c60db34b1d506c816d753174422f86 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 15 Mar 2025 20:16:33 +1100 Subject: [PATCH] Added BC5 saving --- Tests/test_file_dds.py | 20 +++++++++++++++++++- src/PIL/DdsImagePlugin.py | 13 +++++++++++-- src/libImaging/BcnEncode.c | 38 +++++++++++++++++++++++--------------- 3 files changed, 53 insertions(+), 18 deletions(-) diff --git a/Tests/test_file_dds.py b/Tests/test_file_dds.py index 3239065d7..9a6042660 100644 --- a/Tests/test_file_dds.py +++ b/Tests/test_file_dds.py @@ -402,7 +402,7 @@ def test_not_implemented(test_file: str) -> None: def test_save_unsupported_mode(tmp_path: Path) -> None: out = str(tmp_path / "temp.dds") im = hopper("HSV") - with pytest.raises(OSError): + with pytest.raises(OSError, match="cannot write mode HSV as DDS"): im.save(out) @@ -424,6 +424,13 @@ def test_save(mode: str, test_file: str, tmp_path: Path) -> None: assert_image_equal_tofile(im, out) +def test_save_unsupported_pixel_format(tmp_path: Path) -> None: + out = str(tmp_path / "temp.dds") + im = hopper() + with pytest.raises(OSError, match="cannot write pixel format UNKNOWN"): + im.save(out, pixel_format="UNKNOWN") + + def test_save_dxt1(tmp_path: Path) -> None: # RGB out = str(tmp_path / "temp.dds") @@ -493,3 +500,14 @@ def test_save_dxt5(tmp_path: Path) -> None: im_la = im_rgba.convert("LA") im_la.save(out, pixel_format="DXT5") assert_image_similar_tofile(im_la.convert("RGBA"), out, 8.32) + + +def test_save_dx10_bc5(tmp_path: Path) -> None: + out = str(tmp_path / "temp.dds") + with Image.open(TEST_FILE_DX10_BC5_TYPELESS) as im: + im.save(out, pixel_format="BC5") + assert_image_similar_tofile(im, out, 9.56) + + im = hopper("L") + with pytest.raises(OSError, match="only RGB mode can be written as BC5"): + im.save(out, pixel_format="BC5") diff --git a/src/PIL/DdsImagePlugin.py b/src/PIL/DdsImagePlugin.py index d65e3fc65..26307817c 100644 --- a/src/PIL/DdsImagePlugin.py +++ b/src/PIL/DdsImagePlugin.py @@ -530,7 +530,7 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: bitcount = len(im.getbands()) * 8 pixel_format = im.encoderinfo.get("pixel_format") args: tuple[int] | str - if pixel_format in ("DXT1", "BC2", "DXT3", "BC3", "DXT5"): + if pixel_format: codec_name = "bcn" flags |= DDSD.LINEARSIZE pitch = (im.width + 3) * 4 @@ -550,9 +550,18 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: if pixel_format == "BC2": args = (2,) dxgi_format = DXGI_FORMAT.BC2_TYPELESS - else: + elif pixel_format == "BC3": args = (3,) dxgi_format = DXGI_FORMAT.BC3_TYPELESS + elif pixel_format == "BC5": + args = (5,) + dxgi_format = DXGI_FORMAT.BC5_TYPELESS + if im.mode != "RGB": + msg = "only RGB mode can be written as BC5" + raise OSError(msg) + else: + msg = f"cannot write pixel format {pixel_format}" + raise OSError(msg) else: codec_name = "raw" flags |= DDSD.PITCH diff --git a/src/libImaging/BcnEncode.c b/src/libImaging/BcnEncode.c index 4e0da322e..2bad73b92 100644 --- a/src/libImaging/BcnEncode.c +++ b/src/libImaging/BcnEncode.c @@ -185,7 +185,7 @@ encode_bc2_block(Imaging im, ImagingCodecState state, UINT8 *dst) { } static void -encode_bc3_alpha(Imaging im, ImagingCodecState state, UINT8 *dst) { +encode_bc3_alpha(Imaging im, ImagingCodecState state, UINT8 *dst, int o) { int i, j; UINT8 alpha_min = 0, alpha_max = 0; UINT8 block[16], current_alpha; @@ -202,7 +202,7 @@ encode_bc3_alpha(Imaging im, ImagingCodecState state, UINT8 *dst) { continue; } - current_alpha = (UINT8)im->image[y][x + 3]; + current_alpha = (UINT8)im->image[y][x + o]; block[i + j * 4] = current_alpha; if (first || current_alpha < alpha_min) { @@ -226,12 +226,13 @@ encode_bc3_alpha(Imaging im, ImagingCodecState state, UINT8 *dst) { if (!current_alpha) { l |= 6 << (j * 3); continue; - } else if (current_alpha == 255 || denom == 0) { + } else if (current_alpha == 255) { l |= 7 << (j * 3); continue; } - float distance = abs(current_alpha - alpha_min) / denom * 10; + float distance = + denom == 0 ? 0 : abs(current_alpha - alpha_min) / denom * 10; if (distance < 3) { l |= 2 << (j * 3); // 4/5 * alpha_min + 1/5 * alpha_max } else if (distance < 5) { @@ -257,21 +258,28 @@ ImagingBcnEncode(Imaging im, ImagingCodecState state, UINT8 *buf, int bytes) { UINT8 *dst = buf; for (;;) { - if (n == 2 || n == 3) { - if (has_alpha_channel) { - if (n == 2) { - encode_bc2_block(im, state, dst); + if (n == 5) { + encode_bc3_alpha(im, state, dst, 0); + dst += 8; + + encode_bc3_alpha(im, state, dst, 1); + } else { + if (n == 2 || n == 3) { + if (has_alpha_channel) { + if (n == 2) { + encode_bc2_block(im, state, dst); + } else { + encode_bc3_alpha(im, state, dst, 3); + } + dst += 8; } else { - encode_bc3_alpha(im, state, dst); - } - dst += 8; - } else { - for (int i = 0; i < 8; i++) { - *dst++ = 0xff; + for (int i = 0; i < 8; i++) { + *dst++ = 0xff; + } } } + encode_bc1_color(im, state, dst, n == 1 && has_alpha_channel); } - encode_bc1_color(im, state, dst, n == 1 && has_alpha_channel); dst += 8; state->x += im->pixelsize * 4;