From f1a61a1e76cfbc21cd5345f4b87067ace7347ddf Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 15 Mar 2025 12:50:30 +1100 Subject: [PATCH] Added DXT3 saving --- Tests/test_file_dds.py | 23 ++++++++++++++++++ src/PIL/DdsImagePlugin.py | 20 +++++++++------ src/encode.c | 9 +++---- src/libImaging/BcnEncode.c | 50 ++++++++++++++++++++++++++++++++------ 4 files changed, 83 insertions(+), 19 deletions(-) diff --git a/Tests/test_file_dds.py b/Tests/test_file_dds.py index 3c7c8e604..5ef9fbf05 100644 --- a/Tests/test_file_dds.py +++ b/Tests/test_file_dds.py @@ -436,6 +436,29 @@ def test_save_dxt1(tmp_path: Path) -> None: assert reloaded.getpixel((0, 0)) == (0, 0, 0, 0) +def test_save_dxt3(tmp_path: Path) -> None: + # RGB + out = str(tmp_path / "temp.dds") + with Image.open(TEST_FILE_DXT3) as im: + im_rgb = im.convert("RGB") + im_rgb.save(out, pixel_format="DXT3") + assert_image_similar_tofile(im_rgb.convert("RGBA"), out, 1.26) + + # RGBA + im.save(out, pixel_format="DXT3") + assert_image_similar_tofile(im, out, 3.81) + + # L + im_l = im.convert("L") + im_l.save(out, pixel_format="DXT3") + assert_image_similar_tofile(im_l.convert("RGBA"), out, 5.89) + + # LA + im_la = im.convert("LA") + im_la.save(out, pixel_format="DXT3") + assert_image_similar_tofile(im_la.convert("RGBA"), out, 8.44) + + def test_save_dxt5(tmp_path: Path) -> None: # RGB out = str(tmp_path / "temp.dds") diff --git a/src/PIL/DdsImagePlugin.py b/src/PIL/DdsImagePlugin.py index a5e0a712b..c30672c86 100644 --- a/src/PIL/DdsImagePlugin.py +++ b/src/PIL/DdsImagePlugin.py @@ -525,18 +525,24 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: flags = DDSD.CAPS | DDSD.HEIGHT | DDSD.WIDTH | DDSD.PIXELFORMAT bitcount = len(im.getbands()) * 8 pixel_format = im.encoderinfo.get("pixel_format") - if pixel_format in ("DXT1", "BC3", "DXT5"): + args: tuple[int] | str + if pixel_format in ("DXT1", "DXT3", "BC3", "DXT5"): codec_name = "bcn" flags |= DDSD.LINEARSIZE pitch = (im.width + 3) * 4 - args = pixel_format rgba_mask = [0, 0, 0, 0] pixel_flags = DDPF.FOURCC - fourcc = {"DXT1": D3DFMT.DXT1, "BC3": D3DFMT.DX10, "DXT5": D3DFMT.DXT5}[ - pixel_format - ] - if fourcc == D3DFMT.DX10: - dxgi_format = DXGI_FORMAT.BC3_TYPELESS + if pixel_format == "DXT1": + fourcc = D3DFMT.DXT1 + args = (1,) + elif pixel_format == "DXT3": + fourcc = D3DFMT.DXT3 + args = (2,) + else: + fourcc = D3DFMT.DXT5 if pixel_format == "DXT5" else D3DFMT.DX10 + args = (3,) + if fourcc == D3DFMT.DX10: + dxgi_format = DXGI_FORMAT.BC3_TYPELESS else: codec_name = "raw" flags |= DDSD.PITCH diff --git a/src/encode.c b/src/encode.c index e228237f2..7c365a74f 100644 --- a/src/encode.c +++ b/src/encode.c @@ -360,19 +360,18 @@ PyImaging_BcnEncoderNew(PyObject *self, PyObject *args) { ImagingEncoderObject *encoder; char *mode; - char *pixel_format; - if (!PyArg_ParseTuple(args, "ss", &mode, &pixel_format)) { + int n; + if (!PyArg_ParseTuple(args, "si", &mode, &n)) { return NULL; } - encoder = PyImaging_EncoderNew(sizeof(BCNSTATE)); + encoder = PyImaging_EncoderNew(0); if (encoder == NULL) { return NULL; } encoder->encode = ImagingBcnEncode; - - ((BCNSTATE *)encoder->state.context)->pixel_format = pixel_format; + encoder->state.state = n; return (PyObject *)encoder; } diff --git a/src/libImaging/BcnEncode.c b/src/libImaging/BcnEncode.c index 66f9f39b1..4e0da322e 100644 --- a/src/libImaging/BcnEncode.c +++ b/src/libImaging/BcnEncode.c @@ -10,8 +10,6 @@ #include "Imaging.h" -#include "Bcn.h" - typedef struct { UINT8 color[3]; } rgb; @@ -57,14 +55,18 @@ encode_bc1_color(Imaging im, ImagingCodecState state, UINT8 *dst, int separate_a int transparency = 0; for (i = 0; i < 4; i++) { for (j = 0; j < 4; j++) { + current_rgba = &block[i + j * 4]; + int x = state->x + i * im->pixelsize; int y = state->y + j; if (x >= state->xsize * im->pixelsize || y >= state->ysize) { // The 4x4 block extends past the edge of the image + for (k = 0; k < 3; k++) { + current_rgba->color[k] = 0; + } continue; } - current_rgba = &block[i + j * 4]; for (k = 0; k < 3; k++) { current_rgba->color[k] = (UINT8)im->image[y][x + (im->pixelsize == 1 ? 0 : k)]; @@ -152,6 +154,36 @@ encode_bc1_color(Imaging im, ImagingCodecState state, UINT8 *dst, int separate_a } } +static void +encode_bc2_block(Imaging im, ImagingCodecState state, UINT8 *dst) { + int i, j; + UINT8 block[16], current_alpha; + for (i = 0; i < 4; i++) { + for (j = 0; j < 4; j++) { + int x = state->x + i * im->pixelsize; + int y = state->y + j; + if (x >= state->xsize * im->pixelsize || y >= state->ysize) { + // The 4x4 block extends past the edge of the image + block[i + j * 4] = 0; + continue; + } + + current_alpha = (UINT8)im->image[y][x + 3]; + block[i + j * 4] = current_alpha; + } + } + + for (i = 0; i < 4; i++) { + UINT16 l = 0; + for (j = 3; j > -1; j--) { + current_alpha = block[i * 4 + j]; + l |= current_alpha << (j * 4); + } + *dst++ = l; + *dst++ = l >> 8; + } +} + static void encode_bc3_alpha(Imaging im, ImagingCodecState state, UINT8 *dst) { int i, j; @@ -166,6 +198,7 @@ encode_bc3_alpha(Imaging im, ImagingCodecState state, UINT8 *dst) { int y = state->y + j; if (x >= state->xsize * im->pixelsize || y >= state->ysize) { // The 4x4 block extends past the edge of the image + block[i + j * 4] = 0; continue; } @@ -217,17 +250,20 @@ encode_bc3_alpha(Imaging im, ImagingCodecState state, UINT8 *dst) { int ImagingBcnEncode(Imaging im, ImagingCodecState state, UINT8 *buf, int bytes) { - char *pixel_format = ((BCNSTATE *)state->context)->pixel_format; - int n = strcmp(pixel_format, "DXT1") == 0 ? 1 : 3; + int n = state->state; int has_alpha_channel = strcmp(im->mode, "RGBA") == 0 || strcmp(im->mode, "LA") == 0; UINT8 *dst = buf; for (;;) { - if (n == 3) { + if (n == 2 || n == 3) { if (has_alpha_channel) { - encode_bc3_alpha(im, state, dst); + if (n == 2) { + encode_bc2_block(im, state, dst); + } else { + encode_bc3_alpha(im, state, dst); + } dst += 8; } else { for (int i = 0; i < 8; i++) {