From 3dbd0e57bae0dbe2a3f2c006fb767417a2c5419e Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 10 Mar 2025 10:14:38 +1100 Subject: [PATCH 1/6] Added DXT1 encoding --- Tests/test_file_dds.py | 31 ++++++- setup.py | 1 + src/PIL/DdsImagePlugin.py | 61 ++++++++----- src/_imaging.c | 3 + src/encode.c | 18 ++++ src/libImaging/BcnEncode.c | 173 +++++++++++++++++++++++++++++++++++++ src/libImaging/Imaging.h | 2 + 7 files changed, 262 insertions(+), 27 deletions(-) create mode 100644 src/libImaging/BcnEncode.c diff --git a/Tests/test_file_dds.py b/Tests/test_file_dds.py index 7cc4d79d4..7a6099ce7 100644 --- a/Tests/test_file_dds.py +++ b/Tests/test_file_dds.py @@ -9,7 +9,12 @@ import pytest from PIL import DdsImagePlugin, Image -from .helper import assert_image_equal, assert_image_equal_tofile, hopper +from .helper import ( + assert_image_equal, + assert_image_equal_tofile, + assert_image_similar_tofile, + hopper, +) TEST_FILE_DXT1 = "Tests/images/dxt1-rgb-4bbp-noalpha_MipMaps-1.dds" TEST_FILE_DXT3 = "Tests/images/dxt3-argb-8bbp-explicitalpha_MipMaps-1.dds" @@ -389,5 +394,25 @@ def test_save(mode: str, test_file: str, tmp_path: Path) -> None: assert im.mode == mode im.save(out) - with Image.open(out) as reloaded: - assert_image_equal(im, reloaded) + assert_image_equal_tofile(im, out) + + +def test_save_dxt1(tmp_path: Path) -> None: + 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) + + 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) + + im_l = im.convert("L") + im_l.save(out, pixel_format="DXT1") + assert_image_similar_tofile(im_l.convert("RGBA"), out, 9.25) + + im_alpha.convert("LA").save(out, pixel_format="DXT1") + with Image.open(out) as reloaded: + assert reloaded.getpixel((0, 0)) == (0, 0, 0, 0) diff --git a/setup.py b/setup.py index a85731db9..9fac993b1 100644 --- a/setup.py +++ b/setup.py @@ -68,6 +68,7 @@ _LIB_IMAGING = ( "Reduce", "Bands", "BcnDecode", + "BcnEncode", "BitDecode", "Blend", "Chops", diff --git a/src/PIL/DdsImagePlugin.py b/src/PIL/DdsImagePlugin.py index cdae8dfee..718f376e8 100644 --- a/src/PIL/DdsImagePlugin.py +++ b/src/PIL/DdsImagePlugin.py @@ -518,30 +518,43 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: msg = f"cannot write mode {im.mode} as DDS" raise OSError(msg) - alpha = im.mode[-1] == "A" - if im.mode[0] == "L": - pixel_flags = DDPF.LUMINANCE - rawmode = im.mode - if alpha: - rgba_mask = [0x000000FF, 0x000000FF, 0x000000FF] - else: - rgba_mask = [0xFF000000, 0xFF000000, 0xFF000000] - else: - pixel_flags = DDPF.RGB - rawmode = im.mode[::-1] - rgba_mask = [0x00FF0000, 0x0000FF00, 0x000000FF] - - if alpha: - r, g, b, a = im.split() - im = Image.merge("RGBA", (a, r, g, b)) - if alpha: - pixel_flags |= DDPF.ALPHAPIXELS - rgba_mask.append(0xFF000000 if alpha else 0) - - flags = DDSD.CAPS | DDSD.HEIGHT | DDSD.WIDTH | DDSD.PITCH | DDSD.PIXELFORMAT + flags = DDSD.CAPS | DDSD.HEIGHT | DDSD.WIDTH | DDSD.PIXELFORMAT bitcount = len(im.getbands()) * 8 - pitch = (im.width * bitcount + 7) // 8 + raw = im.encoderinfo.get("pixel_format") != "DXT1" + if raw: + codec_name = "raw" + flags |= DDSD.PITCH + pitch = (im.width * bitcount + 7) // 8 + alpha = im.mode[-1] == "A" + if im.mode[0] == "L": + pixel_flags = DDPF.LUMINANCE + rawmode = im.mode + if alpha: + rgba_mask = [0x000000FF, 0x000000FF, 0x000000FF] + else: + rgba_mask = [0xFF000000, 0xFF000000, 0xFF000000] + else: + pixel_flags = DDPF.RGB + rawmode = im.mode[::-1] + rgba_mask = [0x00FF0000, 0x0000FF00, 0x000000FF] + + if alpha: + r, g, b, a = im.split() + im = Image.merge("RGBA", (a, r, g, b)) + if alpha: + 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 fp.write( o32(DDS_MAGIC) + struct.pack( @@ -556,11 +569,11 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: ) + struct.pack("11I", *((0,) * 11)) # reserved # pfsize, pfflags, fourcc, bitcount - + struct.pack("<4I", 32, pixel_flags, 0, bitcount) + + struct.pack("<4I", 32, pixel_flags, fourcc, bitcount) + struct.pack("<4I", *rgba_mask) # dwRGBABitMask + struct.pack("<5I", DDSCAPS.TEXTURE, 0, 0, 0, 0) ) - ImageFile._save(im, fp, [ImageFile._Tile("raw", (0, 0) + im.size, 0, rawmode)]) + ImageFile._save(im, fp, [ImageFile._Tile(codec_name, (0, 0) + im.size, 0, rawmode)]) def _accept(prefix: bytes) -> bool: diff --git a/src/_imaging.c b/src/_imaging.c index fa38dcc05..330a7eef4 100644 --- a/src/_imaging.c +++ b/src/_imaging.c @@ -4041,6 +4041,8 @@ PyImaging_ZipDecoderNew(PyObject *self, PyObject *args); /* Encoders (in encode.c) */ extern PyObject * +PyImaging_BcnEncoderNew(PyObject *self, PyObject *args); +extern PyObject * PyImaging_EpsEncoderNew(PyObject *self, PyObject *args); extern PyObject * PyImaging_GifEncoderNew(PyObject *self, PyObject *args); @@ -4109,6 +4111,7 @@ static PyMethodDef functions[] = { /* Codecs */ {"bcn_decoder", (PyCFunction)PyImaging_BcnDecoderNew, METH_VARARGS}, + {"bcn_encoder", (PyCFunction)PyImaging_BcnEncoderNew, METH_VARARGS}, {"bit_decoder", (PyCFunction)PyImaging_BitDecoderNew, METH_VARARGS}, {"eps_encoder", (PyCFunction)PyImaging_EpsEncoderNew, METH_VARARGS}, {"fli_decoder", (PyCFunction)PyImaging_FliDecoderNew, METH_VARARGS}, diff --git a/src/encode.c b/src/encode.c index 13d5cdaf7..d27783f0c 100644 --- a/src/encode.c +++ b/src/encode.c @@ -350,6 +350,24 @@ get_packer(ImagingEncoderObject *encoder, const char *mode, const char *rawmode) return 0; } +/* -------------------------------------------------------------------- */ +/* BCN */ +/* -------------------------------------------------------------------- */ + +PyObject * +PyImaging_BcnEncoderNew(PyObject *self, PyObject *args) { + ImagingEncoderObject *encoder; + + encoder = PyImaging_EncoderNew(0); + if (encoder == NULL) { + return NULL; + } + + encoder->encode = ImagingBcnEncode; + + return (PyObject *)encoder; +} + /* -------------------------------------------------------------------- */ /* EPS */ /* -------------------------------------------------------------------- */ diff --git a/src/libImaging/BcnEncode.c b/src/libImaging/BcnEncode.c new file mode 100644 index 000000000..42a46358c --- /dev/null +++ b/src/libImaging/BcnEncode.c @@ -0,0 +1,173 @@ +/* + * The Python Imaging Library + * + * encoder for DXT1-compressed data + * + * Format documentation: + * https://web.archive.org/web/20170802060935/http://oss.sgi.com/projects/ogl-sample/registry/EXT/texture_compression_s3tc.txt + * + */ + +#include "Imaging.h" + +#include "Bcn.h" + +typedef struct { + UINT8 color[3]; +} rgb; + +typedef struct { + UINT8 color[3]; + int alpha; +} rgba; + +static rgb +decode_565(UINT16 x) { + rgb item; + int r, g, b; + r = (x & 0xf800) >> 8; + r |= r >> 5; + item.color[0] = r; + g = (x & 0x7e0) >> 3; + g |= g >> 6; + item.color[1] = g; + b = (x & 0x1f) << 3; + b |= b >> 5; + item.color[2] = b; + return item; +} + +static UINT16 +encode_565(rgba item) { + UINT8 r, g, b; + r = item.color[0] >> (8 - 5); + g = item.color[1] >> (8 - 6); + b = item.color[2] >> (8 - 5); + return (r << (5 + 6)) | (g << 5) | b; +} + +int +ImagingBcnEncode(Imaging im, ImagingCodecState state, UINT8 *buf, int bytes) { + 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 + } + } + } + *dst++ = l; + } + + state->x += im->pixelsize * 4; + + if (state->x >= state->xsize * im->pixelsize) { + state->x = 0; + state->y += 4; + if (state->y >= state->ysize) { + state->errcode = IMAGING_CODEC_END; + break; + } + } + } + + return dst - buf; +} diff --git a/src/libImaging/Imaging.h b/src/libImaging/Imaging.h index 0c2d3fc2e..0fc191d15 100644 --- a/src/libImaging/Imaging.h +++ b/src/libImaging/Imaging.h @@ -567,6 +567,8 @@ typedef int (*ImagingCodec)( extern int ImagingBcnDecode(Imaging im, ImagingCodecState state, UINT8 *buffer, Py_ssize_t bytes); extern int +ImagingBcnEncode(Imaging im, ImagingCodecState state, UINT8 *buffer, int bytes); +extern int ImagingBitDecode(Imaging im, ImagingCodecState state, UINT8 *buffer, Py_ssize_t bytes); extern int ImagingEpsEncode(Imaging im, ImagingCodecState state, UINT8 *buffer, int bytes); From 9430bbe5a133c6e5ccb2ce23e0ea1f7f6adca0fe Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 13 Mar 2025 23:53:08 +1100 Subject: [PATCH 2/6] Added DXT5 saving --- Tests/test_file_dds.py | 29 +++- src/PIL/DdsImagePlugin.py | 28 ++-- src/encode.c | 11 +- src/libImaging/BcnEncode.c | 289 ++++++++++++++++++++++++------------- 4 files changed, 237 insertions(+), 120 deletions(-) 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; From 9f619b814f65a99c2107852027ecf7db4ddec7ec Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 15 Mar 2025 00:21:34 +1100 Subject: [PATCH 3/6] Added BC3 loading and saving --- Tests/test_file_dds.py | 14 ++++++++++++++ src/PIL/DdsImagePlugin.py | 17 +++++++++++++++-- src/libImaging/BcnEncode.c | 2 +- 3 files changed, 30 insertions(+), 3 deletions(-) diff --git a/Tests/test_file_dds.py b/Tests/test_file_dds.py index 17d88451f..3c7c8e604 100644 --- a/Tests/test_file_dds.py +++ b/Tests/test_file_dds.py @@ -12,6 +12,7 @@ from PIL import DdsImagePlugin, Image from .helper import ( assert_image_equal, assert_image_equal_tofile, + assert_image_similar, assert_image_similar_tofile, hopper, ) @@ -114,6 +115,19 @@ def test_sanity_ati1_bc4u(image_path: str) -> None: assert_image_equal_tofile(im, TEST_FILE_ATI1.replace(".dds", ".png")) +def test_dx10_bc3(tmp_path: Path) -> None: + out = str(tmp_path / "temp.dds") + with Image.open(TEST_FILE_DXT5) as im: + im.save(out, pixel_format="BC3") + + with Image.open(out) as reloaded: + assert reloaded.format == "DDS" + assert reloaded.mode == "RGBA" + assert reloaded.size == (256, 256) + + assert_image_similar(im, reloaded, 3.69) + + @pytest.mark.parametrize( "image_path", ( diff --git a/src/PIL/DdsImagePlugin.py b/src/PIL/DdsImagePlugin.py index 2d097fd16..a5e0a712b 100644 --- a/src/PIL/DdsImagePlugin.py +++ b/src/PIL/DdsImagePlugin.py @@ -419,6 +419,10 @@ class DdsImageFile(ImageFile.ImageFile): self._mode = "RGBA" self.pixel_format = "BC1" n = 1 + elif dxgi_format in (DXGI_FORMAT.BC3_TYPELESS, DXGI_FORMAT.BC3_UNORM): + self._mode = "RGBA" + self.pixel_format = "BC3" + n = 3 elif dxgi_format in (DXGI_FORMAT.BC4_TYPELESS, DXGI_FORMAT.BC4_UNORM): self._mode = "L" self.pixel_format = "BC4" @@ -521,14 +525,18 @@ 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", "DXT5"): + if pixel_format in ("DXT1", "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 = D3DFMT.DXT1 if pixel_format == "DXT1" else D3DFMT.DXT5 + fourcc = {"DXT1": D3DFMT.DXT1, "BC3": D3DFMT.DX10, "DXT5": D3DFMT.DXT5}[ + pixel_format + ] + if fourcc == D3DFMT.DX10: + dxgi_format = DXGI_FORMAT.BC3_TYPELESS else: codec_name = "raw" flags |= DDSD.PITCH @@ -573,6 +581,11 @@ 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) ) + if fourcc == D3DFMT.DX10: + fp.write( + # dxgi_format, 2D resource, misc, array size, straight alpha + struct.pack("<5I", dxgi_format, 3, 0, 0, 1) + ) ImageFile._save(im, fp, [ImageFile._Tile(codec_name, (0, 0) + im.size, 0, args)]) diff --git a/src/libImaging/BcnEncode.c b/src/libImaging/BcnEncode.c index 57353246a..66f9f39b1 100644 --- a/src/libImaging/BcnEncode.c +++ b/src/libImaging/BcnEncode.c @@ -218,7 +218,7 @@ 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, "DXT5") == 0 ? 3 : 1; + int n = strcmp(pixel_format, "DXT1") == 0 ? 1 : 3; int has_alpha_channel = strcmp(im->mode, "RGBA") == 0 || strcmp(im->mode, "LA") == 0; From f1a61a1e76cfbc21cd5345f4b87067ace7347ddf Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 15 Mar 2025 12:50:30 +1100 Subject: [PATCH 4/6] 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++) { From b0315cc6039e1eabacc36b7af0677f69378f26bd Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 15 Mar 2025 12:56:32 +1100 Subject: [PATCH 5/6] Added BC2 loading and saving --- Tests/test_file_dds.py | 13 +++++++++++++ src/PIL/DdsImagePlugin.py | 18 ++++++++++++++---- 2 files changed, 27 insertions(+), 4 deletions(-) diff --git a/Tests/test_file_dds.py b/Tests/test_file_dds.py index 5ef9fbf05..3239065d7 100644 --- a/Tests/test_file_dds.py +++ b/Tests/test_file_dds.py @@ -115,6 +115,19 @@ def test_sanity_ati1_bc4u(image_path: str) -> None: assert_image_equal_tofile(im, TEST_FILE_ATI1.replace(".dds", ".png")) +def test_dx10_bc2(tmp_path: Path) -> None: + out = str(tmp_path / "temp.dds") + with Image.open(TEST_FILE_DXT3) as im: + im.save(out, pixel_format="BC2") + + with Image.open(out) as reloaded: + assert reloaded.format == "DDS" + assert reloaded.mode == "RGBA" + assert reloaded.size == (256, 256) + + assert_image_similar(im, reloaded, 3.81) + + def test_dx10_bc3(tmp_path: Path) -> None: out = str(tmp_path / "temp.dds") with Image.open(TEST_FILE_DXT5) as im: diff --git a/src/PIL/DdsImagePlugin.py b/src/PIL/DdsImagePlugin.py index c30672c86..d65e3fc65 100644 --- a/src/PIL/DdsImagePlugin.py +++ b/src/PIL/DdsImagePlugin.py @@ -419,6 +419,10 @@ class DdsImageFile(ImageFile.ImageFile): self._mode = "RGBA" self.pixel_format = "BC1" n = 1 + elif dxgi_format in (DXGI_FORMAT.BC2_TYPELESS, DXGI_FORMAT.BC2_UNORM): + self._mode = "RGBA" + self.pixel_format = "BC2" + n = 2 elif dxgi_format in (DXGI_FORMAT.BC3_TYPELESS, DXGI_FORMAT.BC3_UNORM): self._mode = "RGBA" self.pixel_format = "BC3" @@ -526,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", "DXT3", "BC3", "DXT5"): + if pixel_format in ("DXT1", "BC2", "DXT3", "BC3", "DXT5"): codec_name = "bcn" flags |= DDSD.LINEARSIZE pitch = (im.width + 3) * 4 @@ -538,10 +542,16 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: elif pixel_format == "DXT3": fourcc = D3DFMT.DXT3 args = (2,) - else: - fourcc = D3DFMT.DXT5 if pixel_format == "DXT5" else D3DFMT.DX10 + elif pixel_format == "DXT5": + fourcc = D3DFMT.DXT5 args = (3,) - if fourcc == D3DFMT.DX10: + else: + fourcc = D3DFMT.DX10 + if pixel_format == "BC2": + args = (2,) + dxgi_format = DXGI_FORMAT.BC2_TYPELESS + else: + args = (3,) dxgi_format = DXGI_FORMAT.BC3_TYPELESS else: codec_name = "raw" From cd11792c15c60db34b1d506c816d753174422f86 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 15 Mar 2025 20:16:33 +1100 Subject: [PATCH 6/6] 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;