Skip to content

Commit 4cd30a4

Browse files
committed
Added DXT1 encoding
1 parent e946c7b commit 4cd30a4

File tree

7 files changed

+260
-25
lines changed

7 files changed

+260
-25
lines changed

Tests/test_file_dds.py

+28-3
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_tofile,
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"
@@ -389,5 +394,25 @@ def test_save(mode: str, test_file: str, tmp_path: Path) -> None:
389394
assert im.mode == mode
390395
im.save(out)
391396

392-
with Image.open(out) as reloaded:
393-
assert_image_equal(im, reloaded)
397+
assert_image_equal_tofile(im, out)
398+
399+
400+
def test_save_dxt1(tmp_path: Path) -> None:
401+
out = str(tmp_path / "temp.dds")
402+
with Image.open(TEST_FILE_DXT1) as im:
403+
im.convert("RGB").save(out, pixel_format="DXT1")
404+
assert_image_similar_tofile(im, out, 1.84)
405+
406+
im_alpha = im.copy()
407+
im_alpha.putpixel((0, 0), (0, 0, 0, 0))
408+
im_alpha.save(out, pixel_format="DXT1")
409+
with Image.open(out) as reloaded:
410+
assert reloaded.getpixel((0, 0)) == (0, 0, 0, 0)
411+
412+
im_l = im.convert("L")
413+
im_l.save(out, pixel_format="DXT1")
414+
assert_image_similar_tofile(im_l.convert("RGBA"), out, 9.25)
415+
416+
im_alpha.convert("LA").save(out, pixel_format="DXT1")
417+
with Image.open(out) as reloaded:
418+
assert reloaded.getpixel((0, 0)) == (0, 0, 0, 0)

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

+173
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
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+
typedef struct {
20+
UINT8 color[3];
21+
int alpha;
22+
} rgba;
23+
24+
static rgb
25+
decode_565(UINT16 x) {
26+
rgb item;
27+
int r, g, b;
28+
r = (x & 0xf800) >> 8;
29+
r |= r >> 5;
30+
item.color[0] = r;
31+
g = (x & 0x7e0) >> 3;
32+
g |= g >> 6;
33+
item.color[1] = g;
34+
b = (x & 0x1f) << 3;
35+
b |= b >> 5;
36+
item.color[2] = b;
37+
return item;
38+
}
39+
40+
static UINT16
41+
encode_565(rgba item) {
42+
UINT8 r, g, b;
43+
r = item.color[0] >> (8 - 5);
44+
g = item.color[1] >> (8 - 6);
45+
b = item.color[2] >> (8 - 5);
46+
return (r << (5 + 6)) | (g << 5) | b;
47+
}
48+
49+
int
50+
ImagingBcnEncode(Imaging im, ImagingCodecState state, UINT8 *buf, int bytes) {
51+
UINT8 *dst = buf;
52+
53+
for (;;) {
54+
int i, j, k;
55+
UINT16 color_min = 0, color_max = 0;
56+
rgb color_min_rgb, color_max_rgb;
57+
rgba block[16], *current_rgba;
58+
59+
// Determine the min and max colors in this 4x4 block
60+
int has_alpha_channel =
61+
strcmp(im->mode, "RGBA") == 0 || strcmp(im->mode, "LA") == 0;
62+
int first = 1;
63+
int transparency = 0;
64+
for (i = 0; i < 4; i++) {
65+
for (j = 0; j < 4; j++) {
66+
int x = state->x + i * im->pixelsize;
67+
int y = state->y + j;
68+
if (x >= state->xsize * im->pixelsize || y >= state->ysize) {
69+
// The 4x4 block extends past the edge of the image
70+
continue;
71+
}
72+
73+
current_rgba = &block[i + j * 4];
74+
for (k = 0; k < 3; k++) {
75+
current_rgba->color[k] =
76+
(UINT8)im->image[y][x + (im->pixelsize == 1 ? 1 : k)];
77+
}
78+
if (has_alpha_channel) {
79+
if ((UINT8)im->image[y][x + 3] == 0) {
80+
current_rgba->alpha = 0;
81+
transparency = 1;
82+
continue;
83+
} else {
84+
current_rgba->alpha = 1;
85+
}
86+
}
87+
88+
UINT16 color = encode_565(*current_rgba);
89+
if (first || color < color_min) {
90+
color_min = color;
91+
}
92+
if (first || color > color_max) {
93+
color_max = color;
94+
}
95+
first = 0;
96+
}
97+
}
98+
99+
if (transparency) {
100+
*dst++ = color_min;
101+
*dst++ = color_min >> 8;
102+
}
103+
*dst++ = color_max;
104+
*dst++ = color_max >> 8;
105+
if (!transparency) {
106+
*dst++ = color_min;
107+
*dst++ = color_min >> 8;
108+
}
109+
110+
color_min_rgb = decode_565(color_min);
111+
color_max_rgb = decode_565(color_max);
112+
for (i = 0; i < 4; i++) {
113+
UINT8 l = 0;
114+
for (j = 3; j > -1; j--) {
115+
current_rgba = &block[i * 4 + j];
116+
if (transparency && !current_rgba->alpha) {
117+
l |= 3 << (j * 2);
118+
continue;
119+
}
120+
121+
float distance = 0;
122+
int total = 0;
123+
for (k = 0; k < 3; k++) {
124+
float denom =
125+
(float)abs(color_max_rgb.color[k] - color_min_rgb.color[k]);
126+
if (denom != 0) {
127+
distance +=
128+
abs(current_rgba->color[k] - color_min_rgb.color[k]) /
129+
denom;
130+
total += 1;
131+
}
132+
}
133+
if (total == 0) {
134+
continue;
135+
}
136+
distance *= 6 / total;
137+
if (transparency) {
138+
if (distance < 1.5) {
139+
// color_max
140+
} else if (distance < 4.5) {
141+
l |= 2 << (j * 2); // 1/2 * color_min + 1/2 * color_max
142+
} else {
143+
l |= 1 << (j * 2); // color_min
144+
}
145+
} else {
146+
if (distance < 1) {
147+
l |= 1 << (j * 2); // color_min
148+
} else if (distance < 3) {
149+
l |= 3 << (j * 2); // 1/3 * color_min + 2/3 * color_max
150+
} else if (distance < 5) {
151+
l |= 2 << (j * 2); // 2/3 * color_min + 1/3 * color_max
152+
} else {
153+
// color_max
154+
}
155+
}
156+
}
157+
*dst++ = l;
158+
}
159+
160+
state->x += im->pixelsize * 4;
161+
162+
if (state->x >= state->xsize * im->pixelsize) {
163+
state->x = 0;
164+
state->y += 4;
165+
if (state->y >= state->ysize) {
166+
state->errcode = IMAGING_CODEC_END;
167+
break;
168+
}
169+
}
170+
}
171+
172+
return dst - buf;
173+
}

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)