Added DXT5 saving

This commit is contained in:
Andrew Murray 2025-03-13 23:53:08 +11:00
parent 3dbd0e57ba
commit 9430bbe5a1
4 changed files with 237 additions and 120 deletions

View File

@ -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)

View File

@ -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:

View File

@ -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;
}

View File

@ -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;