Skip to content

Commit b60e447

Browse files
committed
Added DXT1 encoding
1 parent e946c7b commit b60e447

File tree

7 files changed

+205
-23
lines changed

7 files changed

+205
-23
lines changed

Tests/test_file_dds.py

+15-1
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,12 @@
99

1010
from PIL import DdsImagePlugin, Image
1111

12-
from .helper import assert_image_equal, assert_image_equal_tofile, hopper
12+
from .helper import (
13+
assert_image_equal,
14+
assert_image_equal_tofile,
15+
assert_image_similar,
16+
hopper,
17+
)
1318

1419
TEST_FILE_DXT1 = "Tests/images/dxt1-rgb-4bbp-noalpha_MipMaps-1.dds"
1520
TEST_FILE_DXT3 = "Tests/images/dxt3-argb-8bbp-explicitalpha_MipMaps-1.dds"
@@ -391,3 +396,12 @@ def test_save(mode: str, test_file: str, tmp_path: Path) -> None:
391396

392397
with Image.open(out) as reloaded:
393398
assert_image_equal(im, reloaded)
399+
400+
401+
def test_save_dxt1(tmp_path: Path) -> None:
402+
out = str(tmp_path / "temp.dds")
403+
with Image.open("Tests/images/dxt1-rgb-4bbp-noalpha_MipMaps-1.dds") as im:
404+
im.save(out, pixel_format="DXT1")
405+
406+
with Image.open(out) as reloaded:
407+
assert_image_similar(im, reloaded, 1.84)

setup.py

+1
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ def get_version() -> str:
6868
"Reduce",
6969
"Bands",
7070
"BcnDecode",
71+
"BcnEncode",
7172
"BitDecode",
7273
"Blend",
7374
"Chops",

src/PIL/DdsImagePlugin.py

+35-22
Original file line numberDiff line numberDiff line change
@@ -518,30 +518,43 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
518518
msg = f"cannot write mode {im.mode} as DDS"
519519
raise OSError(msg)
520520

521-
alpha = im.mode[-1] == "A"
522-
if im.mode[0] == "L":
523-
pixel_flags = DDPF.LUMINANCE
524-
rawmode = im.mode
525-
if alpha:
526-
rgba_mask = [0x000000FF, 0x000000FF, 0x000000FF]
521+
flags = DDSD.CAPS | DDSD.HEIGHT | DDSD.WIDTH | DDSD.PIXELFORMAT
522+
bitcount = len(im.getbands()) * 8
523+
raw = im.encoderinfo.get("pixel_format") != "DXT1"
524+
if raw:
525+
codec_name = "raw"
526+
flags |= DDSD.PITCH
527+
pitch = (im.width * bitcount + 7) // 8
528+
529+
alpha = im.mode[-1] == "A"
530+
if im.mode[0] == "L":
531+
pixel_flags = DDPF.LUMINANCE
532+
rawmode = im.mode
533+
if alpha:
534+
rgba_mask = [0x000000FF, 0x000000FF, 0x000000FF]
535+
else:
536+
rgba_mask = [0xFF000000, 0xFF000000, 0xFF000000]
527537
else:
528-
rgba_mask = [0xFF000000, 0xFF000000, 0xFF000000]
529-
else:
530-
pixel_flags = DDPF.RGB
531-
rawmode = im.mode[::-1]
532-
rgba_mask = [0x00FF0000, 0x0000FF00, 0x000000FF]
538+
pixel_flags = DDPF.RGB
539+
rawmode = im.mode[::-1]
540+
rgba_mask = [0x00FF0000, 0x0000FF00, 0x000000FF]
533541

542+
if alpha:
543+
r, g, b, a = im.split()
544+
im = Image.merge("RGBA", (a, r, g, b))
534545
if alpha:
535-
r, g, b, a = im.split()
536-
im = Image.merge("RGBA", (a, r, g, b))
537-
if alpha:
538-
pixel_flags |= DDPF.ALPHAPIXELS
539-
rgba_mask.append(0xFF000000 if alpha else 0)
540-
541-
flags = DDSD.CAPS | DDSD.HEIGHT | DDSD.WIDTH | DDSD.PITCH | DDSD.PIXELFORMAT
542-
bitcount = len(im.getbands()) * 8
543-
pitch = (im.width * bitcount + 7) // 8
546+
pixel_flags |= DDPF.ALPHAPIXELS
547+
rgba_mask.append(0xFF000000 if alpha else 0)
544548

549+
fourcc = 0
550+
else:
551+
codec_name = "bcn"
552+
flags |= DDSD.LINEARSIZE
553+
pitch = (im.width + 3) * 4
554+
rawmode = None
555+
rgba_mask = [0, 0, 0, 0]
556+
pixel_flags = DDPF.FOURCC
557+
fourcc = D3DFMT.DXT1
545558
fp.write(
546559
o32(DDS_MAGIC)
547560
+ struct.pack(
@@ -556,11 +569,11 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
556569
)
557570
+ struct.pack("11I", *((0,) * 11)) # reserved
558571
# pfsize, pfflags, fourcc, bitcount
559-
+ struct.pack("<4I", 32, pixel_flags, 0, bitcount)
572+
+ struct.pack("<4I", 32, pixel_flags, fourcc, bitcount)
560573
+ struct.pack("<4I", *rgba_mask) # dwRGBABitMask
561574
+ struct.pack("<5I", DDSCAPS.TEXTURE, 0, 0, 0, 0)
562575
)
563-
ImageFile._save(im, fp, [ImageFile._Tile("raw", (0, 0) + im.size, 0, rawmode)])
576+
ImageFile._save(im, fp, [ImageFile._Tile(codec_name, (0, 0) + im.size, 0, rawmode)])
564577

565578

566579
def _accept(prefix: bytes) -> bool:

src/_imaging.c

+3
Original file line numberDiff line numberDiff line change
@@ -4041,6 +4041,8 @@ PyImaging_ZipDecoderNew(PyObject *self, PyObject *args);
40414041

40424042
/* Encoders (in encode.c) */
40434043
extern PyObject *
4044+
PyImaging_BcnEncoderNew(PyObject *self, PyObject *args);
4045+
extern PyObject *
40444046
PyImaging_EpsEncoderNew(PyObject *self, PyObject *args);
40454047
extern PyObject *
40464048
PyImaging_GifEncoderNew(PyObject *self, PyObject *args);
@@ -4109,6 +4111,7 @@ static PyMethodDef functions[] = {
41094111

41104112
/* Codecs */
41114113
{"bcn_decoder", (PyCFunction)PyImaging_BcnDecoderNew, METH_VARARGS},
4114+
{"bcn_encoder", (PyCFunction)PyImaging_BcnEncoderNew, METH_VARARGS},
41124115
{"bit_decoder", (PyCFunction)PyImaging_BitDecoderNew, METH_VARARGS},
41134116
{"eps_encoder", (PyCFunction)PyImaging_EpsEncoderNew, METH_VARARGS},
41144117
{"fli_decoder", (PyCFunction)PyImaging_FliDecoderNew, METH_VARARGS},

src/encode.c

+18
Original file line numberDiff line numberDiff line change
@@ -350,6 +350,24 @@ get_packer(ImagingEncoderObject *encoder, const char *mode, const char *rawmode)
350350
return 0;
351351
}
352352

353+
/* -------------------------------------------------------------------- */
354+
/* BCN */
355+
/* -------------------------------------------------------------------- */
356+
357+
PyObject *
358+
PyImaging_BcnEncoderNew(PyObject *self, PyObject *args) {
359+
ImagingEncoderObject *encoder;
360+
361+
encoder = PyImaging_EncoderNew(0);
362+
if (encoder == NULL) {
363+
return NULL;
364+
}
365+
366+
encoder->encode = ImagingBcnEncode;
367+
368+
return (PyObject *)encoder;
369+
}
370+
353371
/* -------------------------------------------------------------------- */
354372
/* EPS */
355373
/* -------------------------------------------------------------------- */

src/libImaging/BcnEncode.c

+131
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
/*
2+
* The Python Imaging Library
3+
*
4+
* encoder for DXT1-compressed data
5+
*
6+
* Format documentation:
7+
* https://web.archive.org/web/20170802060935/http://oss.sgi.com/projects/ogl-sample/registry/EXT/texture_compression_s3tc.txt
8+
*
9+
*/
10+
11+
#include "Imaging.h"
12+
13+
#include "Bcn.h"
14+
15+
typedef struct {
16+
UINT8 color[3];
17+
} rgb;
18+
19+
static rgb
20+
decode_565(UINT16 x) {
21+
rgb item;
22+
int r, g, b;
23+
r = (x & 0xf800) >> 8;
24+
r |= r >> 5;
25+
item.color[0] = r;
26+
g = (x & 0x7e0) >> 3;
27+
g |= g >> 6;
28+
item.color[1] = g;
29+
b = (x & 0x1f) << 3;
30+
b |= b >> 5;
31+
item.color[2] = b;
32+
return item;
33+
}
34+
35+
static UINT16
36+
encode_565(rgb item) {
37+
UINT8 r, g, b;
38+
r = item.color[0] >> (8 - 5);
39+
g = item.color[1] >> (8 - 6);
40+
b = item.color[2] >> (8 - 5);
41+
return (r << (5 + 6)) | (g << 5) | b;
42+
}
43+
44+
int
45+
ImagingBcnEncode(Imaging im, ImagingCodecState state, UINT8 *buf, int bytes) {
46+
UINT8 *dst = buf;
47+
48+
for (;;) {
49+
int i, j, k;
50+
UINT16 color_min, color_max;
51+
rgb block[16], color_min_rgb, color_max_rgb, *current_rgb;
52+
53+
// Determine the min and max colors in this 4x4 block
54+
for (i = 0; i < 4; i++) {
55+
for (j = 0; j < 4; j++) {
56+
int x = state->x + i * im->pixelsize;
57+
int y = state->y + j;
58+
if (x >= state->xsize * im->pixelsize || y >= state->ysize) {
59+
// The 4x4 block extends past the edge of the image
60+
continue;
61+
}
62+
63+
current_rgb = &block[i + j * 4];
64+
for (k = 0; k < 3; k++) {
65+
current_rgb->color[k] = (UINT8)im->image[y][x + k];
66+
}
67+
68+
UINT16 color = encode_565(*current_rgb);
69+
if ((i == 0 && j == 0) || color < color_min) {
70+
color_min = color;
71+
}
72+
if ((i == 0 && j == 0) || color > color_max) {
73+
color_max = color;
74+
}
75+
}
76+
}
77+
78+
*dst++ = color_max;
79+
*dst++ = color_max >> 8;
80+
*dst++ = color_min;
81+
*dst++ = color_min >> 8;
82+
83+
color_min_rgb = decode_565(color_min);
84+
color_max_rgb = decode_565(color_max);
85+
for (i = 0; i < 4; i++) {
86+
UINT8 l = 0;
87+
for (j = 3; j > -1; j--) {
88+
current_rgb = &block[i * 4 + j];
89+
90+
float distance = 0;
91+
int total = 0;
92+
for (k = 0; k < 3; k++) {
93+
float denom =
94+
(float)abs(color_max_rgb.color[k] - color_min_rgb.color[k]);
95+
if (denom != 0) {
96+
distance +=
97+
abs(current_rgb->color[k] - color_min_rgb.color[k]) / denom;
98+
total += 1;
99+
}
100+
}
101+
if (total == 0) {
102+
continue;
103+
}
104+
distance *= 6 / total;
105+
if (distance < 1) {
106+
l |= 1 << (j * 2); // color_min
107+
} else if (distance < 3) {
108+
l |= 3 << (j * 2); // 1/3 * color_min + 2/3 * color_max
109+
} else if (distance < 5) {
110+
l |= 2 << (j * 2); // 2/3 * color_min + 1/3 * color_max
111+
} else {
112+
// color_max
113+
}
114+
}
115+
*dst++ = l;
116+
}
117+
118+
state->x += im->pixelsize * 4;
119+
120+
if (state->x >= state->xsize * im->pixelsize) {
121+
state->x = 0;
122+
state->y += 4;
123+
if (state->y >= state->ysize) {
124+
state->errcode = IMAGING_CODEC_END;
125+
break;
126+
}
127+
}
128+
}
129+
130+
return dst - buf;
131+
}

src/libImaging/Imaging.h

+2
Original file line numberDiff line numberDiff line change
@@ -567,6 +567,8 @@ typedef int (*ImagingCodec)(
567567
extern int
568568
ImagingBcnDecode(Imaging im, ImagingCodecState state, UINT8 *buffer, Py_ssize_t bytes);
569569
extern int
570+
ImagingBcnEncode(Imaging im, ImagingCodecState state, UINT8 *buffer, int bytes);
571+
extern int
570572
ImagingBitDecode(Imaging im, ImagingCodecState state, UINT8 *buffer, Py_ssize_t bytes);
571573
extern int
572574
ImagingEpsEncode(Imaging im, ImagingCodecState state, UINT8 *buffer, int bytes);

0 commit comments

Comments
 (0)