Skip to content

Commit 363fab4

Browse files
committed
Added DXT1 encoding
1 parent a3d1e2f commit 363fab4

File tree

9 files changed

+301
-21
lines changed

9 files changed

+301
-21
lines changed

Tests/test_file_dds.py

+9-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
from PIL import DdsImagePlugin, Image
77

8-
from .helper import assert_image_equal, assert_image_equal_tofile, hopper
8+
from .helper import assert_image_equal, assert_image_similar, assert_image_equal_tofile, hopper
99

1010
TEST_FILE_DXT1 = "Tests/images/dxt1-rgb-4bbp-noalpha_MipMaps-1.dds"
1111
TEST_FILE_DXT3 = "Tests/images/dxt3-argb-8bbp-explicitalpha_MipMaps-1.dds"
@@ -266,3 +266,11 @@ def test_save(mode, test_file, tmp_path):
266266

267267
with Image.open(out) as reloaded:
268268
assert_image_equal(im, reloaded)
269+
270+
def test_bcn(tmp_path):
271+
im = Image.open("Tests/images/dxt1-rgb-4bbp-noalpha_MipMaps-1.dds")
272+
out = str(tmp_path / "temp.dds")
273+
im.save(out, mode="bcn")
274+
275+
reloaded = Image.open(out)
276+
assert_image_similar(im, reloaded, 1)

setup.py

+1
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ def get_version():
6161
"Reduce",
6262
"Bands",
6363
"BcnDecode",
64+
"BcnEncode",
6465
"BitDecode",
6566
"Blend",
6667
"Chops",

src/PIL/DdsImagePlugin.py

+38-15
Original file line numberDiff line numberDiff line change
@@ -206,36 +206,59 @@ def _save(im, fp, filename):
206206
if im.mode not in ("RGB", "RGBA"):
207207
raise OSError(f"cannot write mode {im.mode} as DDS")
208208

209+
raw = im.encoderinfo.get("mode") != "bcn"
210+
flags = DDSD_CAPS | DDSD_HEIGHT | DDSD_WIDTH | DDSD_PIXELFORMAT
211+
if raw:
212+
flags |= DDSD_PITCH
213+
pitchorlinearsize = (im.width * (32 if im.mode == "RGBA" else 24) + 7) // 8
214+
mipmaps = 0
215+
pfflags = DDS_RGBA if im.mode == "RGBA" else DDPF_RGB
216+
fourcc = o32(0)
217+
rbitmask = 0xFF0000
218+
gbitmask = 0xFF00
219+
bbitmask = 0xFF
220+
abitmask = 0xFF000000 if im.mode == "RGBA" else 0
221+
else:
222+
flags |= DDSD_MIPMAPCOUNT | DDSD_LINEARSIZE
223+
pitchorlinearsize = (im.width + 3) * 4
224+
mipmaps = 1
225+
pfflags = DDPF_FOURCC
226+
fourcc = b"DXT1"
227+
rbitmask = 0
228+
gbitmask = 0
229+
bbitmask = 0
230+
abitmask = 0
209231
fp.write(
210232
o32(DDS_MAGIC)
211233
+ o32(124) # header size
212-
+ o32(
213-
DDSD_CAPS | DDSD_HEIGHT | DDSD_WIDTH | DDSD_PITCH | DDSD_PIXELFORMAT
214-
) # flags
234+
+ o32(flags)
215235
+ o32(im.height)
216236
+ o32(im.width)
217-
+ o32((im.width * (32 if im.mode == "RGBA" else 24) + 7) // 8) # pitch
237+
+ o32(pitchorlinearsize)
218238
+ o32(0) # depth
219-
+ o32(0) # mipmaps
239+
+ o32(mipmaps)
220240
+ o32(0) * 11 # reserved
221241
+ o32(32) # pfsize
222-
+ o32(DDS_RGBA if im.mode == "RGBA" else DDPF_RGB) # pfflags
223-
+ o32(0) # fourcc
242+
+ o32(pfflags) # pfflags
243+
+ fourcc
224244
+ o32(32 if im.mode == "RGBA" else 24) # bitcount
225-
+ o32(0xFF0000) # rbitmask
226-
+ o32(0xFF00) # gbitmask
227-
+ o32(0xFF) # bbitmask
228-
+ o32(0xFF000000 if im.mode == "RGBA" else 0) # abitmask
245+
+ o32(rbitmask)
246+
+ o32(gbitmask)
247+
+ o32(bbitmask)
248+
+ o32(abitmask)
229249
+ o32(DDSCAPS_TEXTURE) # dwCaps
230250
+ o32(0) # dwCaps2
231251
+ o32(0) # dwCaps3
232252
+ o32(0) # dwCaps4
233253
+ o32(0) # dwReserved2
234254
)
235-
if im.mode == "RGBA":
236-
r, g, b, a = im.split()
237-
im = Image.merge("RGBA", (a, r, g, b))
238-
ImageFile._save(im, fp, [("raw", (0, 0) + im.size, 0, (im.mode[::-1], 0, 1))])
255+
if raw:
256+
if im.mode == "RGBA":
257+
r, g, b, a = im.split()
258+
im = Image.merge("RGBA", (a, r, g, b))
259+
ImageFile._save(im, fp, [("raw", (0, 0) + im.size, 0, (im.mode[::-1], 0, 1))])
260+
else:
261+
ImageFile._save(im, fp, [("bcn", (0, 0) + im.size, 0, None)])
239262

240263

241264
def _accept(prefix):

src/_imaging.c

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

39413941
/* Encoders (in encode.c) */
39423942
extern PyObject *
3943+
PyImaging_BcnEncoderNew(PyObject *self, PyObject *args);
3944+
extern PyObject *
39433945
PyImaging_EpsEncoderNew(PyObject *self, PyObject *args);
39443946
extern PyObject *
39453947
PyImaging_GifEncoderNew(PyObject *self, PyObject *args);
@@ -4009,6 +4011,7 @@ static PyMethodDef functions[] = {
40094011

40104012
/* Codecs */
40114013
{"bcn_decoder", (PyCFunction)PyImaging_BcnDecoderNew, METH_VARARGS},
4014+
{"bcn_encoder", (PyCFunction)PyImaging_BcnEncoderNew, METH_VARARGS},
40124015
{"bit_decoder", (PyCFunction)PyImaging_BitDecoderNew, METH_VARARGS},
40134016
{"eps_encoder", (PyCFunction)PyImaging_EpsEncoderNew, METH_VARARGS},
40144017
{"fli_decoder", (PyCFunction)PyImaging_FliDecoderNew, METH_VARARGS},

src/encode.c

+18
Original file line numberDiff line numberDiff line change
@@ -375,6 +375,24 @@ get_packer(ImagingEncoderObject *encoder, const char *mode, const char *rawmode)
375375
return 0;
376376
}
377377

378+
/* -------------------------------------------------------------------- */
379+
/* BCN */
380+
/* -------------------------------------------------------------------- */
381+
382+
PyObject *
383+
PyImaging_BcnEncoderNew(PyObject *self, PyObject *args) {
384+
ImagingEncoderObject *encoder;
385+
386+
encoder = PyImaging_EncoderNew(0);
387+
if (encoder == NULL) {
388+
return NULL;
389+
}
390+
391+
encoder->encode = ImagingBcnEncode;
392+
393+
return (PyObject *)encoder;
394+
}
395+
378396
/* -------------------------------------------------------------------- */
379397
/* EPS */
380398
/* -------------------------------------------------------------------- */

src/libImaging/Bcn.h

+4
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
11
typedef struct {
22
char *pixel_format;
33
} BCNSTATE;
4+
5+
typedef struct {
6+
UINT8 r, g, b, a;
7+
} rgba;

src/libImaging/BcnDecode.c

-5
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,6 @@
1515

1616
#include "Bcn.h"
1717

18-
typedef struct {
19-
UINT8 r, g, b, a;
20-
} rgba;
21-
2218
typedef struct {
2319
UINT8 l;
2420
} lum;
@@ -87,7 +83,6 @@ decode_bc1_color(rgba *dst, const UINT8 *src, int separate_alpha) {
8783
g1 = p[1].g;
8884
b1 = p[1].b;
8985

90-
9186
/* NOTE: BC2 and BC3 reuse BC1 color blocks but always act like c0 > c1 */
9287
if (col.c0 > col.c1 || separate_alpha) {
9388
p[2].r = (2 * r0 + 1 * r1) / 3;

src/libImaging/BcnEncode.c

+226
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,226 @@
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+
static rgba
16+
decode_565(UINT16 x) {
17+
rgba c;
18+
int r, g, b;
19+
r = (x & 0xf800) >> 8;
20+
r |= r >> 5;
21+
c.r = r;
22+
g = (x & 0x7e0) >> 3;
23+
g |= g >> 6;
24+
c.g = g;
25+
b = (x & 0x1f) << 3;
26+
b |= b >> 5;
27+
c.b = b;
28+
c.a = 0xff;
29+
return c;
30+
}
31+
32+
typedef struct {
33+
UINT8 color[3];
34+
} rgb;
35+
36+
static UINT16
37+
encode_565(rgb item) {
38+
UINT8 r, g, b;
39+
r = item.color[0] >> (8 - 5);
40+
g = item.color[1] >> (8 - 6);
41+
b = item.color[2] >> (8 - 5);
42+
return (r << (5 + 6)) | (g << 5) | b;
43+
}
44+
45+
int
46+
ImagingBcnEncode(Imaging im, ImagingCodecState state, UINT8 *buf, int bytes) {
47+
UINT8 *dst = buf;
48+
49+
for (;;) {
50+
int i, j, k, three_color_block = 0;
51+
UINT16 cmin, cmax;
52+
rgb block[16], cmin_rgb, cmax_rgb, *current_rgb;
53+
54+
for (i = 0; i < 4; i++) {
55+
for (j = 0; j < 4; j++) {
56+
UINT16 c;
57+
int x = state->x + i * im->pixelsize;
58+
int y = state->y + j;
59+
if (x >= state->xsize * im->pixelsize || y >= state->ysize) {
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+
c = encode_565(*current_rgb);
69+
if ((i == 0 && j == 0) || c < cmin) {
70+
cmin = c;
71+
}
72+
if ((i == 0 && j == 0) || c > cmax) {
73+
cmax = c;
74+
}
75+
}
76+
}
77+
rgba back = decode_565(cmin);
78+
cmin_rgb.color[0] = back.r;
79+
cmin_rgb.color[1] = back.g;
80+
cmin_rgb.color[2] = back.b;
81+
back = decode_565(cmax);
82+
cmax_rgb.color[0] = back.r;
83+
cmax_rgb.color[1] = back.g;
84+
cmax_rgb.color[2] = back.b;
85+
if (cmin == cmax) {
86+
UINT8 r = cmax_rgb.color[0];
87+
UINT8 g = cmax_rgb.color[1];
88+
UINT8 b = cmax_rgb.color[2];
89+
int halves = 0;
90+
int thirds = 0;
91+
int posr = 0;
92+
int negr = 0;
93+
int posg = 0;
94+
int negg = 0;
95+
int posb = 0;
96+
int negb = 0;
97+
for (i = 0; i < 16; i++) {
98+
for (j = 0; j < 3; j++) {
99+
int diff = block[i].color[j] - cmin_rgb.color[j];
100+
if (diff == (j == 1 ? 2 : 4)) {
101+
halves += 1;
102+
} else if (diff == (j == 1 ? 1 : 3) || diff == (j == 1 ? 3 : 5)) {
103+
thirds += 1;
104+
}
105+
if (j == 0) {
106+
if (diff > 0) {
107+
posr += 1;
108+
} else if (diff < 0) {
109+
negr += 1;
110+
}
111+
} else if (j == 1) {
112+
if (diff > 0) {
113+
posg += 1;
114+
} else if (diff < 0) {
115+
negg += 1;
116+
}
117+
} else {
118+
if (diff > 0) {
119+
posb += 1;
120+
} else if (diff < 0) {
121+
negb += 1;
122+
}
123+
}
124+
}
125+
}
126+
three_color_block = halves > thirds ? 1 : 0;
127+
if (posr > negr) {
128+
cmax_rgb.color[0] += 8;
129+
} else if (posr < negr) {
130+
cmin_rgb.color[0] -= 8;
131+
}
132+
if (posg > negg) {
133+
cmax_rgb.color[1] += 4;
134+
} else if (posg < negg) {
135+
cmin_rgb.color[1] -= 4;
136+
}
137+
if (posb > negb) {
138+
cmax_rgb.color[2] += 8;
139+
} else if (posb < negb) {
140+
cmin_rgb.color[2] -= 8;
141+
}
142+
cmin = encode_565(cmin_rgb);
143+
cmax = encode_565(cmax_rgb);
144+
}
145+
if (three_color_block == 1) {
146+
*dst++ = cmin;
147+
*dst++ = cmin >> 8;
148+
*dst++ = cmax;
149+
*dst++ = cmax >> 8;
150+
for (i = 0; i < 4; i++) {
151+
UINT8 m = 0;
152+
for (j = 3; j > -1; j--) {
153+
current_rgb = &block[i * 4 + j];
154+
155+
float distance = 0;
156+
int total = 0;
157+
for (k = 0; k < 3; k++) {
158+
float denom = (float)abs(cmax_rgb.color[k] - cmin_rgb.color[k]);
159+
if (denom != 0) {
160+
distance += abs(current_rgb->color[k] - cmin_rgb.color[k]) / denom;
161+
total += 1;
162+
}
163+
}
164+
if (total != 0) {
165+
distance *= (6 / total);
166+
}
167+
if (distance < 1.5) {
168+
m |= 1 << (j * 2); // 01 cmin
169+
} else if (distance < 4.5) {
170+
m |= 2 << (j * 2); // 10 2 cmax 1 cmin
171+
} else {
172+
// 00 cmax
173+
}
174+
}
175+
*dst++ = m;
176+
}
177+
} else {
178+
*dst++ = cmax;
179+
*dst++ = cmax >> 8;
180+
*dst++ = cmin;
181+
*dst++ = cmin >> 8;
182+
for (i = 0; i < 4; i++) {
183+
UINT8 m = 0;
184+
for (j = 3; j > -1; j--) {
185+
current_rgb = &block[i * 4 + j];
186+
187+
float distance = 0;
188+
int total = 0;
189+
for (k = 0; k < 3; k++) {
190+
float denom = (float)abs(cmax_rgb.color[k] - cmin_rgb.color[k]);
191+
if (denom != 0) {
192+
distance += abs(current_rgb->color[k] - cmin_rgb.color[k]) / denom;
193+
total += 1;
194+
}
195+
}
196+
if (total != 0) {
197+
distance *= (6 / total);
198+
}
199+
if (distance < 1) {
200+
m |= 1 << (j * 2); // 01 cmin
201+
} else if (distance < 3) {
202+
m |= 3 << (j * 2); // 11 1 cmax 2 cmin
203+
} else if (distance < 5) {
204+
m |= 2 << (j * 2); // 10 2 cmax 1 cmin
205+
} else {
206+
// 00 cmax
207+
}
208+
}
209+
*dst++ = m;
210+
}
211+
}
212+
213+
state->x += im->pixelsize * 4;
214+
215+
if (state->x >= state->xsize * im->pixelsize) {
216+
state->x = 0;
217+
state->y += 4;
218+
if (state->y >= state->ysize) {
219+
state->errcode = IMAGING_CODEC_END;
220+
break;
221+
}
222+
}
223+
}
224+
225+
return dst - buf;
226+
}

0 commit comments

Comments
 (0)