Skip to content

Commit

Permalink
Add switch to DXT1 encoder to select block mode (With alpha/Without a…
Browse files Browse the repository at this point in the history
…lpha)
  • Loading branch information
REDxEYE committed Dec 26, 2022
1 parent a634a44 commit c3457f1
Show file tree
Hide file tree
Showing 13 changed files with 94 additions and 93 deletions.
Binary file modified Tests/images/vtf_a8.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified Tests/images/vtf_a8.vtf
Binary file not shown.
Binary file modified Tests/images/vtf_bgr888.vtf
Binary file not shown.
Binary file modified Tests/images/vtf_dxt1.vtf
Binary file not shown.
Binary file modified Tests/images/vtf_dxt1A.vtf
Binary file not shown.
Binary file modified Tests/images/vtf_i8.vtf
Binary file not shown.
Binary file modified Tests/images/vtf_ia88.vtf
Binary file not shown.
Binary file modified Tests/images/vtf_rgb888.vtf
Binary file not shown.
Binary file modified Tests/images/vtf_rgba8888.vtf
Binary file not shown.
Binary file modified Tests/images/vtf_uv88.vtf
Binary file not shown.
38 changes: 34 additions & 4 deletions Tests/test_file_vtf.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ def test_get_mipmap_count(size: Tuple[int, int], expected_count: int):
],
)
def test_get_texture_size(
pixel_format: VtfPF, size: Tuple[int, int], expected_size: int
pixel_format: VtfPF, size: Tuple[int, int], expected_size: int
):
assert _get_texture_size(pixel_format, *size) == expected_size

Expand All @@ -72,17 +72,18 @@ def test_get_texture_size(
("etalon_path", "file_path", "expected_mode", "epsilon"),
[
("Tests/images/vtf_i8.png", "Tests/images/vtf_i8.vtf", "L", 0.0),
("Tests/images/vtf_a8.png", "Tests/images/vtf_a8.vtf", "L", 0.0),
("Tests/images/vtf_a8.png", "Tests/images/vtf_a8.vtf", "RGBA", 0.0),
("Tests/images/vtf_ia88.png", "Tests/images/vtf_ia88.vtf", "LA", 0.0),
("Tests/images/vtf_uv88.png", "Tests/images/vtf_uv88.vtf", "RGB", 0.0),
("Tests/images/vtf_rgb888.png", "Tests/images/vtf_rgb888.vtf", "RGB", 0.0),
("Tests/images/vtf_bgr888.png", "Tests/images/vtf_bgr888.vtf", "RGB", 0.0),
("Tests/images/vtf_dxt1.png", "Tests/images/vtf_dxt1.vtf", "RGBA", 3.0),
("Tests/images/vtf_dxt1A.png", "Tests/images/vtf_dxt1A.vtf", "RGBA", 8.0),
("Tests/images/vtf_rgba8888.png", "Tests/images/vtf_rgba8888.vtf", "RGBA", 0),
],
)
def test_vtf_loading(
etalon_path: str, file_path: str, expected_mode: str, epsilon: float
def test_vtf_read(
etalon_path: str, file_path: str, expected_mode: str, epsilon: float
):
e = Image.open(etalon_path)
f = Image.open(file_path)
Expand All @@ -92,3 +93,32 @@ def test_vtf_loading(
assert_image_equal(e, f)
else:
assert_image_similar(e, f, epsilon)


@pytest.mark.parametrize(
("pixel_format", "file_path", "expected_mode", "epsilon"),
[
(VtfPF.I8, "Tests/images/vtf_i8.png", "L", 0.0),
(VtfPF.A8, "Tests/images/vtf_a8.png", "RGBA", 0.0),
(VtfPF.IA88, "Tests/images/vtf_ia88.png", "LA", 0.0),
(VtfPF.UV88, "Tests/images/vtf_uv88.png", "RGB", 0.0),
(VtfPF.RGB888, "Tests/images/vtf_rgb888.png", "RGB", 0.0),
(VtfPF.BGR888, "Tests/images/vtf_bgr888.png", "RGB", 0.0),
(VtfPF.DXT1, "Tests/images/vtf_dxt1.png", "RGBA", 3.0),
(VtfPF.DXT1_ONEBITALPHA, "Tests/images/vtf_dxt1A.png", "RGBA", 8.0),
(VtfPF.RGBA8888, "Tests/images/vtf_rgba8888.png", "RGBA", 0),
],
)
def test_vtf_save(pixel_format: VtfPF, file_path: str,
expected_mode: str, epsilon: float, tmp_path):
f: Image.Image = Image.open(file_path)
out = (tmp_path / "tmp.vtf").as_posix()
f.save(out, pixel_format=pixel_format)
if pixel_format == VtfPF.DXT1:
f = f.convert("RGBA")
e = Image.open(out)
assert e.mode == expected_mode
if epsilon == 0:
assert_image_equal(e, f)
else:
assert_image_similar(e, f, epsilon)
113 changes: 38 additions & 75 deletions src/PIL/VtfImagePlugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,29 +76,29 @@ class VtfPF(IntEnum):
ABGR8888 = 1
RGB888 = 2
BGR888 = 3
RGB565 = 4
# RGB565 = 4
I8 = 5
IA88 = 6
P8 = 7
# P8 = 7
A8 = 8
RGB888_BLUESCREEN = 9
BGR888_BLUESCREEN = 10
# RGB888_BLUESCREEN = 9
# BGR888_BLUESCREEN = 10
ARGB8888 = 11
BGRA8888 = 12
DXT1 = 13
DXT3 = 14
DXT5 = 15
BGRX8888 = 16
BGR565 = 17
BGRX5551 = 18
BGRA4444 = 19
# BGR565 = 17
# BGRX5551 = 18
# BGRA4444 = 19
DXT1_ONEBITALPHA = 20
BGRA5551 = 21
# BGRA5551 = 21
UV88 = 22
UVWQ8888 = 23
RGBA16161616F = 24
RGBA16161616 = 25
UVLX8888 = 26
# UVWQ8888 = 23
# RGBA16161616F = 24
# RGBA16161616 = 25
# UVLX8888 = 26


VTFHeader = NamedTuple(
Expand All @@ -125,26 +125,8 @@ class VtfPF(IntEnum):
("resource_count", int),
],
)
RGB_FORMATS = (
VtfPF.DXT1,
VtfPF.RGB888,
VtfPF.BGR888,
VtfPF.UV88,
)
RGBA_FORMATS = (
VtfPF.DXT1_ONEBITALPHA,
VtfPF.DXT3,
VtfPF.DXT5,
VtfPF.RGBA8888,
)
L_FORMATS = (
VtfPF.A8,
VtfPF.I8,
)
LA_FORMATS = (VtfPF.IA88,)

BLOCK_COMPRESSED = (VtfPF.DXT1, VtfPF.DXT1_ONEBITALPHA, VtfPF.DXT3, VtfPF.DXT5)
SUPPORTED_FORMATS = RGBA_FORMATS + RGB_FORMATS + LA_FORMATS + L_FORMATS
HEADER_V70 = "<I2HI2H4x3f4xfIbI2b"
HEADER_V72 = "<I2HI2H4x3f4xfIbI2bH"
HEADER_V73 = "<I2HI2H4x3f4xfIbI2bH3xI8x"
Expand All @@ -153,22 +135,13 @@ class VtfPF(IntEnum):
def _get_texture_size(pixel_format: VtfPF, width, height):
if pixel_format in (VtfPF.DXT1, VtfPF.DXT1_ONEBITALPHA):
return width * height // 2
elif (
pixel_format
in (
VtfPF.DXT3,
VtfPF.DXT5,
)
+ L_FORMATS
):
elif pixel_format in (VtfPF.DXT3, VtfPF.DXT5):
return width * height
elif pixel_format == VtfPF.UV88:
return width * height * 2
elif pixel_format in LA_FORMATS:
elif pixel_format in (VtfPF.A8, VtfPF.I8,):
return width * height
elif pixel_format in (VtfPF.UV88, VtfPF.IA88):
return width * height * 2
elif pixel_format == VtfPF.RGB888:
return width * height * 3
elif pixel_format == VtfPF.BGR888:
elif pixel_format in (VtfPF.RGB888, VtfPF.BGR888):
return width * height * 3
elif pixel_format == VtfPF.RGBA8888:
return width * height * 4
Expand All @@ -190,36 +163,28 @@ def _write_image(fp: BufferedIOBase, im: Image.Image, pixel_format: VtfPF):
if pixel_format == VtfPF.DXT1:
encoder = "bcn"
encoder_args = (1, "DXT1")
im = im.convert("RGB")
im = im.convert("RGBA")
elif pixel_format == VtfPF.DXT1_ONEBITALPHA:
encoder = "bcn"
encoder_args = (1, "DXT1A")
im = im.convert("RGBA")
elif pixel_format == VtfPF.DXT3:
encoder = "bcn"
encoder_args = (3, "DXT3")
im = im.convert("RGBA")
elif pixel_format == VtfPF.DXT5:
encoder = "bcn"
encoder_args = (5, "DXT5")
im = im.convert("RGBA")
elif pixel_format == VtfPF.RGB888:
encoder = "raw"
encoder_args = ("RGB", 0, 0)
im = im.convert("RGB")
elif pixel_format == VtfPF.BGR888:
encoder = "raw"
encoder_args = ("BGR", 0, 0)
im = im.convert("RGB")
elif pixel_format == VtfPF.RGBA8888:
encoder = "raw"
encoder_args = ("RGBA", 0, 0)
im = im.convert("RGBA")
elif pixel_format == VtfPF.A8:
encoder = "raw"
encoder_args = ("L", 0, 0)
*_, a = im.split()
im = Image.merge("L", (a,))
encoder_args = ("A", 0, 0)
elif pixel_format == VtfPF.I8:
encoder = "raw"
encoder_args = ("L", 0, 0)
Expand All @@ -240,7 +205,7 @@ def _write_image(fp: BufferedIOBase, im: Image.Image, pixel_format: VtfPF):

def _closest_power(x):
possible_results = round(log(x, 2)), ceil(log(x, 2))
return 2 ** min(possible_results, key=lambda z: abs(x - 2**z))
return 2 ** min(possible_results, key=lambda z: abs(x - 2 ** z))


class VtfImageFile(ImageFile.ImageFile):
Expand Down Expand Up @@ -281,15 +246,14 @@ def _open(self):
# flags = CompiledVtfFlags(header.flags)
pixel_format = VtfPF(header.pixel_format)
low_format = VtfPF(header.low_pixel_format)
if pixel_format == VtfPF.DXT1: # Special case for DXT1
if pixel_format in (VtfPF.DXT1_ONEBITALPHA, VtfPF.DXT1, VtfPF.DXT3, VtfPF.DXT5,
VtfPF.RGBA8888, VtfPF.BGRA8888,VtfPF.A8):
self.mode = "RGBA"
elif pixel_format in RGB_FORMATS:
elif pixel_format in (VtfPF.RGB888, VtfPF.BGR888, VtfPF.UV88):
self.mode = "RGB"
elif pixel_format in RGBA_FORMATS:
self.mode = "RGBA"
elif pixel_format in L_FORMATS:
elif pixel_format == VtfPF.I8:
self.mode = "L"
elif pixel_format in LA_FORMATS:
elif pixel_format == VtfPF.IA88:
self.mode = "LA"
else:
raise VTFException(f"Unsupported VTF pixel format: {pixel_format}")
Expand All @@ -311,19 +275,21 @@ def _open(self):
tile = ("bcn", (0, 0) + self.size, data_start, (2, "DXT3"))
elif pixel_format == VtfPF.DXT5:
tile = ("bcn", (0, 0) + self.size, data_start, (3, "DXT5"))
elif pixel_format in (VtfPF.RGBA8888,):
elif pixel_format == VtfPF.RGBA8888:
tile = ("raw", (0, 0) + self.size, data_start, ("RGBA", 0, 1))
elif pixel_format in (VtfPF.RGB888,):
elif pixel_format == VtfPF.RGB888:
tile = ("raw", (0, 0) + self.size, data_start, ("RGB", 0, 1))
elif pixel_format in (VtfPF.BGR888,):
elif pixel_format == VtfPF.BGR888:
tile = ("raw", (0, 0) + self.size, data_start, ("BGR", 0, 1))
elif pixel_format in (VtfPF.BGRA8888,):
elif pixel_format == VtfPF.BGRA8888:
tile = ("raw", (0, 0) + self.size, data_start, ("BGRA", 0, 1))
elif pixel_format in (VtfPF.UV88,):
elif pixel_format == VtfPF.UV88:
tile = ("raw", (0, 0) + self.size, data_start, ("RG", 0, 1))
elif pixel_format in L_FORMATS:
elif pixel_format == VtfPF.I8:
tile = ("raw", (0, 0) + self.size, data_start, ("L", 0, 1))
elif pixel_format in LA_FORMATS:
elif pixel_format == VtfPF.A8:
tile = ("raw", (0, 0) + self.size, data_start, ("A", 0, 1))
elif pixel_format == VtfPF.IA88:
tile = ("raw", (0, 0) + self.size, data_start, ("LA", 0, 1))
else:
raise VTFException(f"Unsupported VTF pixel format: {pixel_format}")
Expand All @@ -343,15 +309,12 @@ def _save(im, fp, filename):

if pixel_format == VtfPF.DXT1_ONEBITALPHA:
flags |= CompiledVtfFlags.ONEBITALPHA
elif pixel_format == VtfPF.A8:
flags |= CompiledVtfFlags.EIGHTBITALPHA
elif pixel_format in RGBA_FORMATS + LA_FORMATS:
elif pixel_format in (VtfPF.DXT3, VtfPF.DXT5,
VtfPF.RGBA8888, VtfPF.BGRA8888,
VtfPF.A8, VtfPF.IA88):
flags |= CompiledVtfFlags.EIGHTBITALPHA
elif pixel_format in RGB_FORMATS + L_FORMATS:
pass
else:
raise VTFException("Unhandled case")

pass
im = im.resize((_closest_power(im.width), _closest_power(im.height)))
width, height = im.size

Expand Down
36 changes: 22 additions & 14 deletions src/libImaging/BcnEncode.c
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ get_closest_color_index(const UINT16 *colors, UINT16 color) {
if (error == 0) {
return color_id;
}
if (error < color_error) {
if (error <= color_error) {
color_error = error;
lowest_id = color_id;
}
Expand All @@ -122,10 +122,10 @@ get_closest_color_index(const UINT16 *colors, UINT16 color) {
int
encode_bc1(Imaging im, ImagingCodecState state, UINT8 *buf, int bytes) {
UINT8 *dst = buf;
UINT8 no_alpha = 0;
UINT8 alpha = 0;
INT32 block_index;
if (strcmp(((BCNSTATE *)state->context)->pixel_format, "DXT1A") != 0) {
no_alpha = 1;
if (strcmp(((BCNSTATE *)state->context)->pixel_format, "DXT1A") == 0) {
alpha = 1;
}
INT32 block_count = (im->xsize * im->ysize) / 16;
if (block_count * sizeof(bc1_color) > bytes) {
Expand All @@ -143,6 +143,7 @@ encode_bc1(Imaging im, ImagingCodecState state, UINT8 *buf, int bytes) {
UINT16 unique_colors[16];
UINT8 color_frequency[16];
UINT8 opaque[16];
UINT8 local_alpha = 0;
memset(all_colors, 0, sizeof(all_colors));
memset(unique_colors, 0, sizeof(unique_colors));
memset(color_frequency, 0, sizeof(color_frequency));
Expand All @@ -157,7 +158,8 @@ encode_bc1(Imaging im, ImagingCodecState state, UINT8 *buf, int bytes) {
UINT8 b = im->image[y][x * im->pixelsize + 0];
UINT8 a = im->image[y][x * im->pixelsize + 3];
UINT16 color = PACK_SHORT_565(r, g, b);
opaque[bx + by * 4] = a >= 127;
opaque[bx + by * 4] = a >= 128;
local_alpha |= a <= 128;
all_colors[bx + by * 4] = color;

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

UINT16 c0 = 0, c1 = 0;
pick_2_major_colors(unique_colors, color_frequency, unique_count, &c0, &c1);
if (c0 < c1 && no_alpha) {
SWAP(UINT16, c0, c1);
if (alpha && local_alpha) {
if (c0 > c1) {
SWAP(UINT16, c0, c1);
}
} else {
if (c0 < c1) {
SWAP(UINT16, c0, c1);
}
}

UINT16 palette[4] = {c0, c1, 0, 0};
if (no_alpha) {
palette[2] = rgb565_lerp(c0, c1, 2, 1);
palette[3] = rgb565_lerp(c0, c1, 1, 2);
} else {
if (alpha && local_alpha) {
palette[2] = rgb565_lerp(c0, c1, 1, 1);
palette[3] = 0;
} else {
palette[2] = rgb565_lerp(c0, c1, 2, 1);
palette[3] = rgb565_lerp(c0, c1, 1, 2);
}
bc1_color block = {0};
block.c0 = c0;
block.c1 = c1;
UINT32 color_id;
for (color_id = 0; color_id < 16; ++color_id) {
UINT8 bc_color_id;
if (opaque[color_id] || no_alpha) {
bc_color_id = get_closest_color_index(palette, all_colors[color_id]);
} else {
if ((alpha && local_alpha) && !opaque[color_id]) {
bc_color_id = 3;
} else {
bc_color_id = get_closest_color_index(palette, all_colors[color_id]);
}
SET_BITS(block.lut, color_id * 2, 2, bc_color_id);
}
Expand Down

0 comments on commit c3457f1

Please sign in to comment.