Skip to content

Commit c3457f1

Browse files
committed
Add switch to DXT1 encoder to select block mode (With alpha/Without alpha)
1 parent a634a44 commit c3457f1

13 files changed

+94
-93
lines changed

Tests/images/vtf_a8.png

998 Bytes
Loading

Tests/images/vtf_a8.vtf

0 Bytes
Binary file not shown.

Tests/images/vtf_bgr888.vtf

0 Bytes
Binary file not shown.

Tests/images/vtf_dxt1.vtf

0 Bytes
Binary file not shown.

Tests/images/vtf_dxt1A.vtf

0 Bytes
Binary file not shown.

Tests/images/vtf_i8.vtf

0 Bytes
Binary file not shown.

Tests/images/vtf_ia88.vtf

0 Bytes
Binary file not shown.

Tests/images/vtf_rgb888.vtf

0 Bytes
Binary file not shown.

Tests/images/vtf_rgba8888.vtf

0 Bytes
Binary file not shown.

Tests/images/vtf_uv88.vtf

0 Bytes
Binary file not shown.

Tests/test_file_vtf.py

+34-4
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ def test_get_mipmap_count(size: Tuple[int, int], expected_count: int):
6363
],
6464
)
6565
def test_get_texture_size(
66-
pixel_format: VtfPF, size: Tuple[int, int], expected_size: int
66+
pixel_format: VtfPF, size: Tuple[int, int], expected_size: int
6767
):
6868
assert _get_texture_size(pixel_format, *size) == expected_size
6969

@@ -72,17 +72,18 @@ def test_get_texture_size(
7272
("etalon_path", "file_path", "expected_mode", "epsilon"),
7373
[
7474
("Tests/images/vtf_i8.png", "Tests/images/vtf_i8.vtf", "L", 0.0),
75-
("Tests/images/vtf_a8.png", "Tests/images/vtf_a8.vtf", "L", 0.0),
75+
("Tests/images/vtf_a8.png", "Tests/images/vtf_a8.vtf", "RGBA", 0.0),
7676
("Tests/images/vtf_ia88.png", "Tests/images/vtf_ia88.vtf", "LA", 0.0),
7777
("Tests/images/vtf_uv88.png", "Tests/images/vtf_uv88.vtf", "RGB", 0.0),
7878
("Tests/images/vtf_rgb888.png", "Tests/images/vtf_rgb888.vtf", "RGB", 0.0),
7979
("Tests/images/vtf_bgr888.png", "Tests/images/vtf_bgr888.vtf", "RGB", 0.0),
8080
("Tests/images/vtf_dxt1.png", "Tests/images/vtf_dxt1.vtf", "RGBA", 3.0),
81+
("Tests/images/vtf_dxt1A.png", "Tests/images/vtf_dxt1A.vtf", "RGBA", 8.0),
8182
("Tests/images/vtf_rgba8888.png", "Tests/images/vtf_rgba8888.vtf", "RGBA", 0),
8283
],
8384
)
84-
def test_vtf_loading(
85-
etalon_path: str, file_path: str, expected_mode: str, epsilon: float
85+
def test_vtf_read(
86+
etalon_path: str, file_path: str, expected_mode: str, epsilon: float
8687
):
8788
e = Image.open(etalon_path)
8889
f = Image.open(file_path)
@@ -92,3 +93,32 @@ def test_vtf_loading(
9293
assert_image_equal(e, f)
9394
else:
9495
assert_image_similar(e, f, epsilon)
96+
97+
98+
@pytest.mark.parametrize(
99+
("pixel_format", "file_path", "expected_mode", "epsilon"),
100+
[
101+
(VtfPF.I8, "Tests/images/vtf_i8.png", "L", 0.0),
102+
(VtfPF.A8, "Tests/images/vtf_a8.png", "RGBA", 0.0),
103+
(VtfPF.IA88, "Tests/images/vtf_ia88.png", "LA", 0.0),
104+
(VtfPF.UV88, "Tests/images/vtf_uv88.png", "RGB", 0.0),
105+
(VtfPF.RGB888, "Tests/images/vtf_rgb888.png", "RGB", 0.0),
106+
(VtfPF.BGR888, "Tests/images/vtf_bgr888.png", "RGB", 0.0),
107+
(VtfPF.DXT1, "Tests/images/vtf_dxt1.png", "RGBA", 3.0),
108+
(VtfPF.DXT1_ONEBITALPHA, "Tests/images/vtf_dxt1A.png", "RGBA", 8.0),
109+
(VtfPF.RGBA8888, "Tests/images/vtf_rgba8888.png", "RGBA", 0),
110+
],
111+
)
112+
def test_vtf_save(pixel_format: VtfPF, file_path: str,
113+
expected_mode: str, epsilon: float, tmp_path):
114+
f: Image.Image = Image.open(file_path)
115+
out = (tmp_path / "tmp.vtf").as_posix()
116+
f.save(out, pixel_format=pixel_format)
117+
if pixel_format == VtfPF.DXT1:
118+
f = f.convert("RGBA")
119+
e = Image.open(out)
120+
assert e.mode == expected_mode
121+
if epsilon == 0:
122+
assert_image_equal(e, f)
123+
else:
124+
assert_image_similar(e, f, epsilon)

src/PIL/VtfImagePlugin.py

+38-75
Original file line numberDiff line numberDiff line change
@@ -76,29 +76,29 @@ class VtfPF(IntEnum):
7676
ABGR8888 = 1
7777
RGB888 = 2
7878
BGR888 = 3
79-
RGB565 = 4
79+
# RGB565 = 4
8080
I8 = 5
8181
IA88 = 6
82-
P8 = 7
82+
# P8 = 7
8383
A8 = 8
84-
RGB888_BLUESCREEN = 9
85-
BGR888_BLUESCREEN = 10
84+
# RGB888_BLUESCREEN = 9
85+
# BGR888_BLUESCREEN = 10
8686
ARGB8888 = 11
8787
BGRA8888 = 12
8888
DXT1 = 13
8989
DXT3 = 14
9090
DXT5 = 15
9191
BGRX8888 = 16
92-
BGR565 = 17
93-
BGRX5551 = 18
94-
BGRA4444 = 19
92+
# BGR565 = 17
93+
# BGRX5551 = 18
94+
# BGRA4444 = 19
9595
DXT1_ONEBITALPHA = 20
96-
BGRA5551 = 21
96+
# BGRA5551 = 21
9797
UV88 = 22
98-
UVWQ8888 = 23
99-
RGBA16161616F = 24
100-
RGBA16161616 = 25
101-
UVLX8888 = 26
98+
# UVWQ8888 = 23
99+
# RGBA16161616F = 24
100+
# RGBA16161616 = 25
101+
# UVLX8888 = 26
102102

103103

104104
VTFHeader = NamedTuple(
@@ -125,26 +125,8 @@ class VtfPF(IntEnum):
125125
("resource_count", int),
126126
],
127127
)
128-
RGB_FORMATS = (
129-
VtfPF.DXT1,
130-
VtfPF.RGB888,
131-
VtfPF.BGR888,
132-
VtfPF.UV88,
133-
)
134-
RGBA_FORMATS = (
135-
VtfPF.DXT1_ONEBITALPHA,
136-
VtfPF.DXT3,
137-
VtfPF.DXT5,
138-
VtfPF.RGBA8888,
139-
)
140-
L_FORMATS = (
141-
VtfPF.A8,
142-
VtfPF.I8,
143-
)
144-
LA_FORMATS = (VtfPF.IA88,)
145128

146129
BLOCK_COMPRESSED = (VtfPF.DXT1, VtfPF.DXT1_ONEBITALPHA, VtfPF.DXT3, VtfPF.DXT5)
147-
SUPPORTED_FORMATS = RGBA_FORMATS + RGB_FORMATS + LA_FORMATS + L_FORMATS
148130
HEADER_V70 = "<I2HI2H4x3f4xfIbI2b"
149131
HEADER_V72 = "<I2HI2H4x3f4xfIbI2bH"
150132
HEADER_V73 = "<I2HI2H4x3f4xfIbI2bH3xI8x"
@@ -153,22 +135,13 @@ class VtfPF(IntEnum):
153135
def _get_texture_size(pixel_format: VtfPF, width, height):
154136
if pixel_format in (VtfPF.DXT1, VtfPF.DXT1_ONEBITALPHA):
155137
return width * height // 2
156-
elif (
157-
pixel_format
158-
in (
159-
VtfPF.DXT3,
160-
VtfPF.DXT5,
161-
)
162-
+ L_FORMATS
163-
):
138+
elif pixel_format in (VtfPF.DXT3, VtfPF.DXT5):
164139
return width * height
165-
elif pixel_format == VtfPF.UV88:
166-
return width * height * 2
167-
elif pixel_format in LA_FORMATS:
140+
elif pixel_format in (VtfPF.A8, VtfPF.I8,):
141+
return width * height
142+
elif pixel_format in (VtfPF.UV88, VtfPF.IA88):
168143
return width * height * 2
169-
elif pixel_format == VtfPF.RGB888:
170-
return width * height * 3
171-
elif pixel_format == VtfPF.BGR888:
144+
elif pixel_format in (VtfPF.RGB888, VtfPF.BGR888):
172145
return width * height * 3
173146
elif pixel_format == VtfPF.RGBA8888:
174147
return width * height * 4
@@ -190,36 +163,28 @@ def _write_image(fp: BufferedIOBase, im: Image.Image, pixel_format: VtfPF):
190163
if pixel_format == VtfPF.DXT1:
191164
encoder = "bcn"
192165
encoder_args = (1, "DXT1")
193-
im = im.convert("RGB")
166+
im = im.convert("RGBA")
194167
elif pixel_format == VtfPF.DXT1_ONEBITALPHA:
195168
encoder = "bcn"
196169
encoder_args = (1, "DXT1A")
197-
im = im.convert("RGBA")
198170
elif pixel_format == VtfPF.DXT3:
199171
encoder = "bcn"
200172
encoder_args = (3, "DXT3")
201-
im = im.convert("RGBA")
202173
elif pixel_format == VtfPF.DXT5:
203174
encoder = "bcn"
204175
encoder_args = (5, "DXT5")
205-
im = im.convert("RGBA")
206176
elif pixel_format == VtfPF.RGB888:
207177
encoder = "raw"
208178
encoder_args = ("RGB", 0, 0)
209-
im = im.convert("RGB")
210179
elif pixel_format == VtfPF.BGR888:
211180
encoder = "raw"
212181
encoder_args = ("BGR", 0, 0)
213-
im = im.convert("RGB")
214182
elif pixel_format == VtfPF.RGBA8888:
215183
encoder = "raw"
216184
encoder_args = ("RGBA", 0, 0)
217-
im = im.convert("RGBA")
218185
elif pixel_format == VtfPF.A8:
219186
encoder = "raw"
220-
encoder_args = ("L", 0, 0)
221-
*_, a = im.split()
222-
im = Image.merge("L", (a,))
187+
encoder_args = ("A", 0, 0)
223188
elif pixel_format == VtfPF.I8:
224189
encoder = "raw"
225190
encoder_args = ("L", 0, 0)
@@ -240,7 +205,7 @@ def _write_image(fp: BufferedIOBase, im: Image.Image, pixel_format: VtfPF):
240205

241206
def _closest_power(x):
242207
possible_results = round(log(x, 2)), ceil(log(x, 2))
243-
return 2 ** min(possible_results, key=lambda z: abs(x - 2**z))
208+
return 2 ** min(possible_results, key=lambda z: abs(x - 2 ** z))
244209

245210

246211
class VtfImageFile(ImageFile.ImageFile):
@@ -281,15 +246,14 @@ def _open(self):
281246
# flags = CompiledVtfFlags(header.flags)
282247
pixel_format = VtfPF(header.pixel_format)
283248
low_format = VtfPF(header.low_pixel_format)
284-
if pixel_format == VtfPF.DXT1: # Special case for DXT1
249+
if pixel_format in (VtfPF.DXT1_ONEBITALPHA, VtfPF.DXT1, VtfPF.DXT3, VtfPF.DXT5,
250+
VtfPF.RGBA8888, VtfPF.BGRA8888,VtfPF.A8):
285251
self.mode = "RGBA"
286-
elif pixel_format in RGB_FORMATS:
252+
elif pixel_format in (VtfPF.RGB888, VtfPF.BGR888, VtfPF.UV88):
287253
self.mode = "RGB"
288-
elif pixel_format in RGBA_FORMATS:
289-
self.mode = "RGBA"
290-
elif pixel_format in L_FORMATS:
254+
elif pixel_format == VtfPF.I8:
291255
self.mode = "L"
292-
elif pixel_format in LA_FORMATS:
256+
elif pixel_format == VtfPF.IA88:
293257
self.mode = "LA"
294258
else:
295259
raise VTFException(f"Unsupported VTF pixel format: {pixel_format}")
@@ -311,19 +275,21 @@ def _open(self):
311275
tile = ("bcn", (0, 0) + self.size, data_start, (2, "DXT3"))
312276
elif pixel_format == VtfPF.DXT5:
313277
tile = ("bcn", (0, 0) + self.size, data_start, (3, "DXT5"))
314-
elif pixel_format in (VtfPF.RGBA8888,):
278+
elif pixel_format == VtfPF.RGBA8888:
315279
tile = ("raw", (0, 0) + self.size, data_start, ("RGBA", 0, 1))
316-
elif pixel_format in (VtfPF.RGB888,):
280+
elif pixel_format == VtfPF.RGB888:
317281
tile = ("raw", (0, 0) + self.size, data_start, ("RGB", 0, 1))
318-
elif pixel_format in (VtfPF.BGR888,):
282+
elif pixel_format == VtfPF.BGR888:
319283
tile = ("raw", (0, 0) + self.size, data_start, ("BGR", 0, 1))
320-
elif pixel_format in (VtfPF.BGRA8888,):
284+
elif pixel_format == VtfPF.BGRA8888:
321285
tile = ("raw", (0, 0) + self.size, data_start, ("BGRA", 0, 1))
322-
elif pixel_format in (VtfPF.UV88,):
286+
elif pixel_format == VtfPF.UV88:
323287
tile = ("raw", (0, 0) + self.size, data_start, ("RG", 0, 1))
324-
elif pixel_format in L_FORMATS:
288+
elif pixel_format == VtfPF.I8:
325289
tile = ("raw", (0, 0) + self.size, data_start, ("L", 0, 1))
326-
elif pixel_format in LA_FORMATS:
290+
elif pixel_format == VtfPF.A8:
291+
tile = ("raw", (0, 0) + self.size, data_start, ("A", 0, 1))
292+
elif pixel_format == VtfPF.IA88:
327293
tile = ("raw", (0, 0) + self.size, data_start, ("LA", 0, 1))
328294
else:
329295
raise VTFException(f"Unsupported VTF pixel format: {pixel_format}")
@@ -343,15 +309,12 @@ def _save(im, fp, filename):
343309

344310
if pixel_format == VtfPF.DXT1_ONEBITALPHA:
345311
flags |= CompiledVtfFlags.ONEBITALPHA
346-
elif pixel_format == VtfPF.A8:
347-
flags |= CompiledVtfFlags.EIGHTBITALPHA
348-
elif pixel_format in RGBA_FORMATS + LA_FORMATS:
312+
elif pixel_format in (VtfPF.DXT3, VtfPF.DXT5,
313+
VtfPF.RGBA8888, VtfPF.BGRA8888,
314+
VtfPF.A8, VtfPF.IA88):
349315
flags |= CompiledVtfFlags.EIGHTBITALPHA
350-
elif pixel_format in RGB_FORMATS + L_FORMATS:
351-
pass
352316
else:
353-
raise VTFException("Unhandled case")
354-
317+
pass
355318
im = im.resize((_closest_power(im.width), _closest_power(im.height)))
356319
width, height = im.size
357320

src/libImaging/BcnEncode.c

+22-14
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,7 @@ get_closest_color_index(const UINT16 *colors, UINT16 color) {
111111
if (error == 0) {
112112
return color_id;
113113
}
114-
if (error < color_error) {
114+
if (error <= color_error) {
115115
color_error = error;
116116
lowest_id = color_id;
117117
}
@@ -122,10 +122,10 @@ get_closest_color_index(const UINT16 *colors, UINT16 color) {
122122
int
123123
encode_bc1(Imaging im, ImagingCodecState state, UINT8 *buf, int bytes) {
124124
UINT8 *dst = buf;
125-
UINT8 no_alpha = 0;
125+
UINT8 alpha = 0;
126126
INT32 block_index;
127-
if (strcmp(((BCNSTATE *)state->context)->pixel_format, "DXT1A") != 0) {
128-
no_alpha = 1;
127+
if (strcmp(((BCNSTATE *)state->context)->pixel_format, "DXT1A") == 0) {
128+
alpha = 1;
129129
}
130130
INT32 block_count = (im->xsize * im->ysize) / 16;
131131
if (block_count * sizeof(bc1_color) > bytes) {
@@ -143,6 +143,7 @@ encode_bc1(Imaging im, ImagingCodecState state, UINT8 *buf, int bytes) {
143143
UINT16 unique_colors[16];
144144
UINT8 color_frequency[16];
145145
UINT8 opaque[16];
146+
UINT8 local_alpha = 0;
146147
memset(all_colors, 0, sizeof(all_colors));
147148
memset(unique_colors, 0, sizeof(unique_colors));
148149
memset(color_frequency, 0, sizeof(color_frequency));
@@ -157,7 +158,8 @@ encode_bc1(Imaging im, ImagingCodecState state, UINT8 *buf, int bytes) {
157158
UINT8 b = im->image[y][x * im->pixelsize + 0];
158159
UINT8 a = im->image[y][x * im->pixelsize + 3];
159160
UINT16 color = PACK_SHORT_565(r, g, b);
160-
opaque[bx + by * 4] = a >= 127;
161+
opaque[bx + by * 4] = a >= 128;
162+
local_alpha |= a <= 128;
161163
all_colors[bx + by * 4] = color;
162164

163165
UINT8 new_color = 1;
@@ -179,28 +181,34 @@ encode_bc1(Imaging im, ImagingCodecState state, UINT8 *buf, int bytes) {
179181

180182
UINT16 c0 = 0, c1 = 0;
181183
pick_2_major_colors(unique_colors, color_frequency, unique_count, &c0, &c1);
182-
if (c0 < c1 && no_alpha) {
183-
SWAP(UINT16, c0, c1);
184+
if (alpha && local_alpha) {
185+
if (c0 > c1) {
186+
SWAP(UINT16, c0, c1);
187+
}
188+
} else {
189+
if (c0 < c1) {
190+
SWAP(UINT16, c0, c1);
191+
}
184192
}
185193

186194
UINT16 palette[4] = {c0, c1, 0, 0};
187-
if (no_alpha) {
188-
palette[2] = rgb565_lerp(c0, c1, 2, 1);
189-
palette[3] = rgb565_lerp(c0, c1, 1, 2);
190-
} else {
195+
if (alpha && local_alpha) {
191196
palette[2] = rgb565_lerp(c0, c1, 1, 1);
192197
palette[3] = 0;
198+
} else {
199+
palette[2] = rgb565_lerp(c0, c1, 2, 1);
200+
palette[3] = rgb565_lerp(c0, c1, 1, 2);
193201
}
194202
bc1_color block = {0};
195203
block.c0 = c0;
196204
block.c1 = c1;
197205
UINT32 color_id;
198206
for (color_id = 0; color_id < 16; ++color_id) {
199207
UINT8 bc_color_id;
200-
if (opaque[color_id] || no_alpha) {
201-
bc_color_id = get_closest_color_index(palette, all_colors[color_id]);
202-
} else {
208+
if ((alpha && local_alpha) && !opaque[color_id]) {
203209
bc_color_id = 3;
210+
} else {
211+
bc_color_id = get_closest_color_index(palette, all_colors[color_id]);
204212
}
205213
SET_BITS(block.lut, color_id * 2, 2, bc_color_id);
206214
}

0 commit comments

Comments
 (0)