diff --git a/Tests/test_file_dds.py b/Tests/test_file_dds.py index 7a6099ce7..17d88451f 100644 --- a/Tests/test_file_dds.py +++ b/Tests/test_file_dds.py @@ -398,21 +398,48 @@ def test_save(mode: str, test_file: str, tmp_path: Path) -> None: def test_save_dxt1(tmp_path: Path) -> None: + # RGB out = str(tmp_path / "temp.dds") with Image.open(TEST_FILE_DXT1) as im: im.convert("RGB").save(out, pixel_format="DXT1") assert_image_similar_tofile(im, out, 1.84) + # RGBA im_alpha = im.copy() im_alpha.putpixel((0, 0), (0, 0, 0, 0)) im_alpha.save(out, pixel_format="DXT1") with Image.open(out) as reloaded: assert reloaded.getpixel((0, 0)) == (0, 0, 0, 0) + # L im_l = im.convert("L") im_l.save(out, pixel_format="DXT1") - assert_image_similar_tofile(im_l.convert("RGBA"), out, 9.25) + assert_image_similar_tofile(im_l.convert("RGBA"), out, 6.07) + # LA im_alpha.convert("LA").save(out, pixel_format="DXT1") with Image.open(out) as reloaded: assert reloaded.getpixel((0, 0)) == (0, 0, 0, 0) + + +def test_save_dxt5(tmp_path: Path) -> None: + # RGB + out = str(tmp_path / "temp.dds") + with Image.open(TEST_FILE_DXT1) as im: + im.convert("RGB").save(out, pixel_format="DXT5") + assert_image_similar_tofile(im, out, 1.84) + + # RGBA + with Image.open(TEST_FILE_DXT5) as im_rgba: + im_rgba.save(out, pixel_format="DXT5") + assert_image_similar_tofile(im_rgba, out, 3.69) + + # L + im_l = im.convert("L") + im_l.save(out, pixel_format="DXT5") + assert_image_similar_tofile(im_l.convert("RGBA"), out, 6.07) + + # LA + im_la = im_rgba.convert("LA") + im_la.save(out, pixel_format="DXT5") + assert_image_similar_tofile(im_la.convert("RGBA"), out, 8.32) diff --git a/src/PIL/DdsImagePlugin.py b/src/PIL/DdsImagePlugin.py index 718f376e8..2d097fd16 100644 --- a/src/PIL/DdsImagePlugin.py +++ b/src/PIL/DdsImagePlugin.py @@ -520,8 +520,16 @@ 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 - raw = im.encoderinfo.get("pixel_format") != "DXT1" - if raw: + pixel_format = im.encoderinfo.get("pixel_format") + if pixel_format in ("DXT1", "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 = D3DFMT.DXT1 if pixel_format == "DXT1" else D3DFMT.DXT5 + else: codec_name = "raw" flags |= DDSD.PITCH pitch = (im.width * bitcount + 7) // 8 @@ -529,14 +537,14 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: alpha = im.mode[-1] == "A" if im.mode[0] == "L": pixel_flags = DDPF.LUMINANCE - rawmode = im.mode + args = im.mode if alpha: rgba_mask = [0x000000FF, 0x000000FF, 0x000000FF] else: rgba_mask = [0xFF000000, 0xFF000000, 0xFF000000] else: pixel_flags = DDPF.RGB - rawmode = im.mode[::-1] + args = im.mode[::-1] rgba_mask = [0x00FF0000, 0x0000FF00, 0x000000FF] if alpha: @@ -546,15 +554,7 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: pixel_flags |= DDPF.ALPHAPIXELS rgba_mask.append(0xFF000000 if alpha else 0) - fourcc = 0 - else: - codec_name = "bcn" - flags |= DDSD.LINEARSIZE - pitch = (im.width + 3) * 4 - rawmode = None - rgba_mask = [0, 0, 0, 0] - pixel_flags = DDPF.FOURCC - fourcc = D3DFMT.DXT1 + fourcc = D3DFMT.UNKNOWN fp.write( o32(DDS_MAGIC) + struct.pack( @@ -573,7 +573,7 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: + struct.pack("<4I", *rgba_mask) # dwRGBABitMask + struct.pack("<5I", DDSCAPS.TEXTURE, 0, 0, 0, 0) ) - ImageFile._save(im, fp, [ImageFile._Tile(codec_name, (0, 0) + im.size, 0, rawmode)]) + ImageFile._save(im, fp, [ImageFile._Tile(codec_name, (0, 0) + im.size, 0, args)]) def _accept(prefix: bytes) -> bool: diff --git a/src/encode.c b/src/encode.c index d27783f0c..e228237f2 100644 --- a/src/encode.c +++ b/src/encode.c @@ -27,6 +27,7 @@ #include "thirdparty/pythoncapi_compat.h" #include "libImaging/Imaging.h" +#include "libImaging/Bcn.h" #include "libImaging/Gif.h" #ifdef HAVE_UNISTD_H @@ -358,13 +359,21 @@ PyObject * PyImaging_BcnEncoderNew(PyObject *self, PyObject *args) { ImagingEncoderObject *encoder; - encoder = PyImaging_EncoderNew(0); + char *mode; + char *pixel_format; + if (!PyArg_ParseTuple(args, "ss", &mode, &pixel_format)) { + return NULL; + } + + encoder = PyImaging_EncoderNew(sizeof(BCNSTATE)); if (encoder == NULL) { return NULL; } encoder->encode = ImagingBcnEncode; + ((BCNSTATE *)encoder->state.context)->pixel_format = pixel_format; + return (PyObject *)encoder; } diff --git a/src/libImaging/BcnEncode.c b/src/libImaging/BcnEncode.c index 42a46358c..57353246a 100644 --- a/src/libImaging/BcnEncode.c +++ b/src/libImaging/BcnEncode.c @@ -17,8 +17,7 @@ typedef struct { } rgb; typedef struct { - UINT8 color[3]; - int alpha; + UINT8 color[4]; } rgba; static rgb @@ -46,116 +45,198 @@ encode_565(rgba item) { return (r << (5 + 6)) | (g << 5) | b; } +static void +encode_bc1_color(Imaging im, ImagingCodecState state, UINT8 *dst, int separate_alpha) { + int i, j, k; + UINT16 color_min = 0, color_max = 0; + rgb color_min_rgb, color_max_rgb; + rgba block[16], *current_rgba; + + // Determine the min and max colors in this 4x4 block + int first = 1; + int transparency = 0; + 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 + 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)]; + } + if (separate_alpha) { + if ((UINT8)im->image[y][x + 3] == 0) { + current_rgba->color[3] = 0; + transparency = 1; + continue; + } else { + current_rgba->color[3] = 1; + } + } + + UINT16 color = encode_565(*current_rgba); + if (first || color < color_min) { + color_min = color; + } + if (first || color > color_max) { + color_max = color; + } + first = 0; + } + } + + if (transparency) { + *dst++ = color_min; + *dst++ = color_min >> 8; + } + *dst++ = color_max; + *dst++ = color_max >> 8; + if (!transparency) { + *dst++ = color_min; + *dst++ = color_min >> 8; + } + + color_min_rgb = decode_565(color_min); + color_max_rgb = decode_565(color_max); + for (i = 0; i < 4; i++) { + UINT8 l = 0; + for (j = 3; j > -1; j--) { + current_rgba = &block[i * 4 + j]; + if (transparency && !current_rgba->color[3]) { + l |= 3 << (j * 2); + continue; + } + + float distance = 0; + int total = 0; + for (k = 0; k < 3; k++) { + float denom = + (float)abs(color_max_rgb.color[k] - color_min_rgb.color[k]); + if (denom != 0) { + distance += + abs(current_rgba->color[k] - color_min_rgb.color[k]) / denom; + total += 1; + } + } + if (total == 0) { + continue; + } + if (transparency) { + distance *= 4 / total; + if (distance < 1) { + // color_max + } else if (distance < 3) { + l |= 2 << (j * 2); // 1/2 * color_min + 1/2 * color_max + } else { + l |= 1 << (j * 2); // color_min + } + } else { + distance *= 6 / total; + if (distance < 1) { + l |= 1 << (j * 2); // color_min + } else if (distance < 3) { + l |= 3 << (j * 2); // 1/3 * color_min + 2/3 * color_max + } else if (distance < 5) { + l |= 2 << (j * 2); // 2/3 * color_min + 1/3 * color_max + } else { + // color_max + } + } + } + *dst++ = l; + } +} + +static void +encode_bc3_alpha(Imaging im, ImagingCodecState state, UINT8 *dst) { + int i, j; + UINT8 alpha_min = 0, alpha_max = 0; + UINT8 block[16], current_alpha; + + // Determine the min and max colors in this 4x4 block + int first = 1; + 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 + continue; + } + + current_alpha = (UINT8)im->image[y][x + 3]; + block[i + j * 4] = current_alpha; + + if (first || current_alpha < alpha_min) { + alpha_min = current_alpha; + } + if (first || current_alpha > alpha_max) { + alpha_max = current_alpha; + } + first = 0; + } + } + + *dst++ = alpha_min; + *dst++ = alpha_max; + + float denom = (float)abs(alpha_max - alpha_min); + for (i = 0; i < 2; i++) { + UINT32 l = 0; + for (j = 7; j > -1; j--) { + current_alpha = block[i * 8 + j]; + if (!current_alpha) { + l |= 6 << (j * 3); + continue; + } else if (current_alpha == 255 || denom == 0) { + l |= 7 << (j * 3); + continue; + } + + float distance = 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) { + l |= 3 << (j * 3); // 3/5 * alpha_min + 2/5 * alpha_max + } else if (distance < 7) { + l |= 4 << (j * 3); // 2/5 * alpha_min + 3/5 * alpha_max + } else { + l |= 5 << (j * 3); // 1/5 * alpha_min + 4/5 * alpha_max + } + } + *dst++ = l; + *dst++ = l >> 8; + *dst++ = l >> 16; + } +} + int ImagingBcnEncode(Imaging im, ImagingCodecState state, UINT8 *buf, int bytes) { + char *pixel_format = ((BCNSTATE *)state->context)->pixel_format; + int n = strcmp(pixel_format, "DXT5") == 0 ? 3 : 1; + int has_alpha_channel = + strcmp(im->mode, "RGBA") == 0 || strcmp(im->mode, "LA") == 0; + UINT8 *dst = buf; for (;;) { - int i, j, k; - UINT16 color_min = 0, color_max = 0; - rgb color_min_rgb, color_max_rgb; - rgba block[16], *current_rgba; - - // Determine the min and max colors in this 4x4 block - int has_alpha_channel = - strcmp(im->mode, "RGBA") == 0 || strcmp(im->mode, "LA") == 0; - int first = 1; - int transparency = 0; - 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 - 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)]; - } - if (has_alpha_channel) { - if ((UINT8)im->image[y][x + 3] == 0) { - current_rgba->alpha = 0; - transparency = 1; - continue; - } else { - current_rgba->alpha = 1; - } - } - - UINT16 color = encode_565(*current_rgba); - if (first || color < color_min) { - color_min = color; - } - if (first || color > color_max) { - color_max = color; - } - first = 0; - } - } - - if (transparency) { - *dst++ = color_min; - *dst++ = color_min >> 8; - } - *dst++ = color_max; - *dst++ = color_max >> 8; - if (!transparency) { - *dst++ = color_min; - *dst++ = color_min >> 8; - } - - color_min_rgb = decode_565(color_min); - color_max_rgb = decode_565(color_max); - for (i = 0; i < 4; i++) { - UINT8 l = 0; - for (j = 3; j > -1; j--) { - current_rgba = &block[i * 4 + j]; - if (transparency && !current_rgba->alpha) { - l |= 3 << (j * 2); - continue; - } - - float distance = 0; - int total = 0; - for (k = 0; k < 3; k++) { - float denom = - (float)abs(color_max_rgb.color[k] - color_min_rgb.color[k]); - if (denom != 0) { - distance += - abs(current_rgba->color[k] - color_min_rgb.color[k]) / - denom; - total += 1; - } - } - if (total == 0) { - continue; - } - distance *= 6 / total; - if (transparency) { - if (distance < 1.5) { - // color_max - } else if (distance < 4.5) { - l |= 2 << (j * 2); // 1/2 * color_min + 1/2 * color_max - } else { - l |= 1 << (j * 2); // color_min - } - } else { - if (distance < 1) { - l |= 1 << (j * 2); // color_min - } else if (distance < 3) { - l |= 3 << (j * 2); // 1/3 * color_min + 2/3 * color_max - } else if (distance < 5) { - l |= 2 << (j * 2); // 2/3 * color_min + 1/3 * color_max - } else { - // color_max - } + if (n == 3) { + if (has_alpha_channel) { + encode_bc3_alpha(im, state, dst); + dst += 8; + } else { + for (int i = 0; i < 8; i++) { + *dst++ = 0xff; } } - *dst++ = l; } + encode_bc1_color(im, state, dst, n == 1 && has_alpha_channel); + dst += 8; state->x += im->pixelsize * 4;