diff --git a/Tests/images/vtf_a8.png b/Tests/images/vtf_a8.png new file mode 100644 index 00000000000..1e95446f151 Binary files /dev/null and b/Tests/images/vtf_a8.png differ diff --git a/Tests/images/vtf_a8.vtf b/Tests/images/vtf_a8.vtf new file mode 100644 index 00000000000..4705e9e0915 Binary files /dev/null and b/Tests/images/vtf_a8.vtf differ diff --git a/Tests/images/vtf_bgr888.png b/Tests/images/vtf_bgr888.png new file mode 100644 index 00000000000..7e8acdaf889 Binary files /dev/null and b/Tests/images/vtf_bgr888.png differ diff --git a/Tests/images/vtf_bgr888.vtf b/Tests/images/vtf_bgr888.vtf new file mode 100644 index 00000000000..081220d8056 Binary files /dev/null and b/Tests/images/vtf_bgr888.vtf differ diff --git a/Tests/images/vtf_dxt1.png b/Tests/images/vtf_dxt1.png new file mode 100644 index 00000000000..7e8acdaf889 Binary files /dev/null and b/Tests/images/vtf_dxt1.png differ diff --git a/Tests/images/vtf_dxt1.vtf b/Tests/images/vtf_dxt1.vtf new file mode 100644 index 00000000000..f74c3756b0a Binary files /dev/null and b/Tests/images/vtf_dxt1.vtf differ diff --git a/Tests/images/vtf_dxt1A.png b/Tests/images/vtf_dxt1A.png new file mode 100644 index 00000000000..95f6f79411c Binary files /dev/null and b/Tests/images/vtf_dxt1A.png differ diff --git a/Tests/images/vtf_dxt1A.vtf b/Tests/images/vtf_dxt1A.vtf new file mode 100644 index 00000000000..2ea82c302a2 Binary files /dev/null and b/Tests/images/vtf_dxt1A.vtf differ diff --git a/Tests/images/vtf_i8.png b/Tests/images/vtf_i8.png new file mode 100644 index 00000000000..790e5e7e8ea Binary files /dev/null and b/Tests/images/vtf_i8.png differ diff --git a/Tests/images/vtf_i8.vtf b/Tests/images/vtf_i8.vtf new file mode 100644 index 00000000000..d3e5e94f5ba Binary files /dev/null and b/Tests/images/vtf_i8.vtf differ diff --git a/Tests/images/vtf_ia88.png b/Tests/images/vtf_ia88.png new file mode 100644 index 00000000000..a6e96a5510a Binary files /dev/null and b/Tests/images/vtf_ia88.png differ diff --git a/Tests/images/vtf_ia88.vtf b/Tests/images/vtf_ia88.vtf new file mode 100644 index 00000000000..30df1e0570f Binary files /dev/null and b/Tests/images/vtf_ia88.vtf differ diff --git a/Tests/images/vtf_rgb888.png b/Tests/images/vtf_rgb888.png new file mode 100644 index 00000000000..7e8acdaf889 Binary files /dev/null and b/Tests/images/vtf_rgb888.png differ diff --git a/Tests/images/vtf_rgb888.vtf b/Tests/images/vtf_rgb888.vtf new file mode 100644 index 00000000000..b8eb744dbf5 Binary files /dev/null and b/Tests/images/vtf_rgb888.vtf differ diff --git a/Tests/images/vtf_rgba8888.png b/Tests/images/vtf_rgba8888.png new file mode 100644 index 00000000000..95f6f79411c Binary files /dev/null and b/Tests/images/vtf_rgba8888.png differ diff --git a/Tests/images/vtf_rgba8888.vtf b/Tests/images/vtf_rgba8888.vtf new file mode 100644 index 00000000000..6d8248bc632 Binary files /dev/null and b/Tests/images/vtf_rgba8888.vtf differ diff --git a/Tests/images/vtf_uv88.png b/Tests/images/vtf_uv88.png new file mode 100644 index 00000000000..8689f14ca8c Binary files /dev/null and b/Tests/images/vtf_uv88.png differ diff --git a/Tests/images/vtf_uv88.vtf b/Tests/images/vtf_uv88.vtf new file mode 100644 index 00000000000..8acc013e01a Binary files /dev/null and b/Tests/images/vtf_uv88.vtf differ diff --git a/Tests/test_file_vtf.py b/Tests/test_file_vtf.py new file mode 100644 index 00000000000..62a6ad47d87 --- /dev/null +++ b/Tests/test_file_vtf.py @@ -0,0 +1,123 @@ +from __future__ import annotations + +import pytest + +from PIL import Image +from PIL.VtfImagePlugin import ( + VtfPF, + _closest_power, + _get_mipmap_count, + _get_texture_size, +) + +from .helper import assert_image_equal, assert_image_similar + + +@pytest.mark.parametrize( + ("size", "expected_size"), + [ + (8, 8), + (7, 8), + (9, 8), + (192, 256), + (1, 1), + (2000, 2048), + ], +) +def test_closest_power(size: int, expected_size: int): + assert _closest_power(size) == expected_size + + +@pytest.mark.parametrize( + ("size", "expected_count"), + [ + ((1, 1), 1), + ((2, 2), 2), + ((4, 4), 3), + ((8, 8), 4), + ((128, 128), 8), + ((256, 256), 9), + ((512, 512), 10), + ((1024, 1024), 11), + ((1024, 1), 11), + ], +) +def test_get_mipmap_count(size: tuple[int, int], expected_count: int): + assert _get_mipmap_count(*size) == expected_count + + +@pytest.mark.parametrize( + ("pixel_format", "size", "expected_size"), + [ + (VtfPF.DXT1, (16, 16), (16 * 16) // 2), + (VtfPF.DXT1_ONEBITALPHA, (16, 16), (16 * 16) // 2), + (VtfPF.DXT3, (16, 16), 16 * 16), + (VtfPF.DXT5, (16, 16), 16 * 16), + (VtfPF.BGR888, (16, 16), 16 * 16 * 3), + (VtfPF.RGB888, (16, 16), 16 * 16 * 3), + (VtfPF.RGBA8888, (16, 16), 16 * 16 * 4), + (VtfPF.UV88, (16, 16), 16 * 16 * 2), + (VtfPF.A8, (16, 16), 16 * 16), + (VtfPF.I8, (16, 16), 16 * 16), + (VtfPF.IA88, (16, 16), 16 * 16 * 2), + ], +) +def test_get_texture_size( + pixel_format: VtfPF, size: tuple[int, int], expected_size: int +): + assert _get_texture_size(pixel_format, *size) == expected_size + + +@pytest.mark.parametrize( + ("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", "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_read(etalon_path: str, file_path: str, expected_mode: str, epsilon: float): + e = Image.open(etalon_path) + f = Image.open(file_path) + assert f.mode == expected_mode + e = e.convert(expected_mode) + if epsilon == 0: + 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) diff --git a/setup.py b/setup.py index b26852b0b07..230437c60c8 100644 --- a/setup.py +++ b/setup.py @@ -66,6 +66,7 @@ def get_version(): "Reduce", "Bands", "BcnDecode", + "BcnEncode", "BitDecode", "Blend", "Chops", diff --git a/src/PIL/VtfImagePlugin.py b/src/PIL/VtfImagePlugin.py new file mode 100644 index 00000000000..8aa9c43243b --- /dev/null +++ b/src/PIL/VtfImagePlugin.py @@ -0,0 +1,417 @@ +""" +A Pillow loader for .vtf files (aka Valve Texture Format) +REDxEYE + +Documentation: + https://developer.valvesoftware.com/wiki/Valve_Texture_Format + +The contents of this file are hereby released in the public domain (CC0) +Full text of the CC0 license: + https://creativecommons.org/publicdomain/zero/1.0/ +""" + +from __future__ import annotations + +import struct +from enum import IntEnum, IntFlag +from io import BufferedIOBase, BytesIO +from math import ceil, log +from typing import NamedTuple + +from . import Image, ImageFile + + +class VTFException(Exception): + pass + + +class CompiledVtfFlags(IntFlag): + # Flags from the *.txt config file + POINTSAMPLE = 0x00000001 + TRILINEAR = 0x00000002 + CLAMPS = 0x00000004 + CLAMPT = 0x00000008 + ANISOTROPIC = 0x00000010 + HINT_DXT5 = 0x00000020 + PWL_CORRECTED = 0x00000040 + NORMAL = 0x00000080 + NOMIP = 0x00000100 + NOLOD = 0x00000200 + ALL_MIPS = 0x00000400 + PROCEDURAL = 0x00000800 + + # These are automatically generated by vtex from the texture data. + ONEBITALPHA = 0x00001000 + EIGHTBITALPHA = 0x00002000 + + # Newer flags from the *.txt config file + ENVMAP = 0x00004000 + RENDERTARGET = 0x00008000 + DEPTHRENDERTARGET = 0x00010000 + NODEBUGOVERRIDE = 0x00020000 + SINGLECOPY = 0x00040000 + PRE_SRGB = 0x00080000 + + UNUSED_00100000 = 0x00100000 + UNUSED_00200000 = 0x00200000 + UNUSED_00400000 = 0x00400000 + + NODEPTHBUFFER = 0x00800000 + + UNUSED_01000000 = 0x01000000 + + CLAMPU = 0x02000000 + VERTEXTEXTURE = 0x04000000 + SSBUMP = 0x08000000 + + UNUSED_10000000 = 0x10000000 + + BORDER = 0x20000000 + + UNUSED_40000000 = 0x40000000 + UNUSED_80000000 = 0x80000000 + + +class VtfPF(IntEnum): + NONE = -1 + RGBA8888 = 0 + ABGR8888 = 1 + RGB888 = 2 + BGR888 = 3 + # RGB565 = 4 + I8 = 5 + IA88 = 6 + # P8 = 7 + A8 = 8 + # RGB888_BLUESCREEN = 9 + # BGR888_BLUESCREEN = 10 + ARGB8888 = 11 + BGRA8888 = 12 + DXT1 = 13 + DXT3 = 14 + DXT5 = 15 + BGRX8888 = 16 + # BGR565 = 17 + # BGRX5551 = 18 + # BGRA4444 = 19 + DXT1_ONEBITALPHA = 20 + # BGRA5551 = 21 + UV88 = 22 + # UVWQ8888 = 23 + # RGBA16161616F = 24 + # RGBA16161616 = 25 + # UVLX8888 = 26 + + +class VTFHeader(NamedTuple): + header_size: int + width: int + height: int + flags: int + frames: int + first_frames: int + reflectivity_r: float + reflectivity_g: float + reflectivity_b: float + bumpmap_scale: float + pixel_format: int + mipmap_count: int + low_pixel_format: int + low_width: int + low_height: int + depth: int + resource_count: int + + +BLOCK_COMPRESSED = (VtfPF.DXT1, VtfPF.DXT1_ONEBITALPHA, VtfPF.DXT3, VtfPF.DXT5) +HEADER_V70 = "> mip_count + mip_height = height >> mip_count + if mip_width == 0 and mip_height == 0: + return mip_count + mip_count += 1 + + +def _write_image(fp: BufferedIOBase, im: Image.Image, pixel_format: VtfPF): + extents = (0, 0) + im.size + if pixel_format == VtfPF.DXT1: + encoder = "bcn" + encoder_args = (1, "DXT1") + im = im.convert("RGBA") + elif pixel_format == VtfPF.DXT1_ONEBITALPHA: + encoder = "bcn" + encoder_args = (1, "DXT1A") + elif pixel_format == VtfPF.DXT3: + encoder = "bcn" + encoder_args = (3, "DXT3") + elif pixel_format == VtfPF.DXT5: + encoder = "bcn" + encoder_args = (5, "DXT5") + elif pixel_format == VtfPF.RGB888: + encoder = "raw" + encoder_args = ("RGB", 0, 0) + elif pixel_format == VtfPF.BGR888: + encoder = "raw" + encoder_args = ("BGR", 0, 0) + elif pixel_format == VtfPF.RGBA8888: + encoder = "raw" + encoder_args = ("RGBA", 0, 0) + elif pixel_format == VtfPF.A8: + encoder = "raw" + encoder_args = ("A", 0, 0) + elif pixel_format == VtfPF.I8: + encoder = "raw" + encoder_args = ("L", 0, 0) + im = im.convert("L") + elif pixel_format == VtfPF.IA88: + encoder = "raw" + encoder_args = ("LA", 0, 0) + im = im.convert("LA") + elif pixel_format == VtfPF.UV88: + encoder = "raw" + encoder_args = ("RG", 0, 0) + else: + msg = f"Unsupported pixel format: {pixel_format!r}" + raise VTFException(msg) + + tile = [(encoder, extents, fp.tell(), encoder_args)] + ImageFile._save(im, fp, tile, _get_texture_size(pixel_format, *im.size)) + + +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)) + + +class VtfImageFile(ImageFile.ImageFile): + format = "VTF" + format_description = "Valve Texture Format" + + def _open(self): + if not _accept(self.fp.read(12)): + msg = "not a VTF file" + raise SyntaxError(msg) + self.fp.seek(4) + version = struct.unpack("<2I", self.fp.read(8)) + if version <= (7, 2): + header = VTFHeader( + *struct.unpack(HEADER_V70, self.fp.read(struct.calcsize(HEADER_V70))), + 0, + 0, + 0, + 0, + 0, + ) + self.fp.seek(header.header_size) + elif version < (7, 3): + header = VTFHeader( + *struct.unpack(HEADER_V72, self.fp.read(struct.calcsize(HEADER_V72))), + 0, + 0, + 0, + 0, + ) + self.fp.seek(header.header_size) + elif version < (7, 5): + header = VTFHeader( + *struct.unpack(HEADER_V73, self.fp.read(struct.calcsize(HEADER_V73))) + ) + self.fp.seek(header.header_size) + else: + msg = f"Unsupported VTF version: {version}" + raise VTFException(msg) + # flags = CompiledVtfFlags(header.flags) + pixel_format = VtfPF(header.pixel_format) + low_format = VtfPF(header.low_pixel_format) + 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 (VtfPF.RGB888, VtfPF.BGR888, VtfPF.UV88): + self._mode = "RGB" + elif pixel_format == VtfPF.I8: + self._mode = "L" + elif pixel_format == VtfPF.IA88: + self._mode = "LA" + else: + msg = f"Unsupported VTF pixel format: {pixel_format}" + raise VTFException(msg) + + self._size = (header.width, header.height) + + data_start = self.fp.tell() + data_start += _get_texture_size(low_format, header.low_width, header.low_height) + min_res = 4 if pixel_format in BLOCK_COMPRESSED else 1 + for mip_id in range(header.mipmap_count - 1, 0, -1): + mip_width = max(header.width >> mip_id, min_res) + mip_height = max(header.height >> mip_id, min_res) + + data_start += _get_texture_size(pixel_format, mip_width, mip_height) + + if pixel_format in (VtfPF.DXT1, VtfPF.DXT1_ONEBITALPHA): + tile = ("bcn", (0, 0) + self.size, data_start, (1, "DXT1")) + elif pixel_format == VtfPF.DXT3: + 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 == VtfPF.RGBA8888: + tile = ("raw", (0, 0) + self.size, data_start, ("RGBA", 0, 1)) + elif pixel_format == VtfPF.RGB888: + tile = ("raw", (0, 0) + self.size, data_start, ("RGB", 0, 1)) + elif pixel_format == VtfPF.BGR888: + tile = ("raw", (0, 0) + self.size, data_start, ("BGR", 0, 1)) + elif pixel_format == VtfPF.BGRA8888: + tile = ("raw", (0, 0) + self.size, data_start, ("BGRA", 0, 1)) + elif pixel_format == VtfPF.UV88: + tile = ("raw", (0, 0) + self.size, data_start, ("RG", 0, 1)) + elif pixel_format == VtfPF.I8: + tile = ("raw", (0, 0) + self.size, data_start, ("L", 0, 1)) + 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: + msg = f"Unsupported VTF pixel format: {pixel_format}" + raise VTFException(msg) + self.tile = [tile] + + +def _save(im, fp, filename): + im: Image.Image + if im.mode not in ("RGB", "RGBA", "L", "LA"): + msg = f"cannot write mode {im.mode} as VTF" + raise OSError(msg) + encoderinfo = im.encoderinfo + pixel_format = VtfPF(encoderinfo.get("pixel_format", VtfPF.RGBA8888)) + version = encoderinfo.get("version", (7, 4)) + generate_mips = encoderinfo.get("generate_mips", True) + + flags = CompiledVtfFlags(0) + + if pixel_format == VtfPF.DXT1_ONEBITALPHA: + flags |= CompiledVtfFlags.ONEBITALPHA + elif pixel_format in ( + VtfPF.DXT3, + VtfPF.DXT5, + VtfPF.RGBA8888, + VtfPF.BGRA8888, + VtfPF.A8, + VtfPF.IA88, + ): + flags |= CompiledVtfFlags.EIGHTBITALPHA + else: + pass + im = im.resize((_closest_power(im.width), _closest_power(im.height))) + width, height = im.size + + mipmap_count = 0 + if generate_mips: + mipmap_count = _get_mipmap_count(width, height) + + thumb_buffer = BytesIO() + thumb = im.convert("RGB") + thumb.thumbnail(((min(16, width)), (min(16, height)))) + thumb = thumb.resize((_closest_power(thumb.width), _closest_power(thumb.height))) + _write_image(thumb_buffer, thumb, VtfPF.DXT1) + + header = VTFHeader( + 0, + width, + height, + flags, + 1, + 0, + 1.0, + 1.0, + 1.0, + 1.0, + pixel_format, + mipmap_count, + VtfPF.DXT1, + thumb.width, + thumb.height, + 1, + 2, + ) + + fp.write(b"VTF\x00" + struct.pack("<2I", *version)) + if version < (7, 2): + size = struct.calcsize(HEADER_V70) + 12 + header = header._replace(header_size=size + (16 - size % 16)) + fp.write(struct.pack(HEADER_V70, *header[:15])) + elif version == (7, 2): + size = struct.calcsize(HEADER_V72) + 12 + header = header._replace(header_size=size + (16 - size % 16)) + fp.write(struct.pack(HEADER_V72, *header[:16])) + elif version > (7, 2): + size = struct.calcsize(HEADER_V73) + 12 + header = header._replace(header_size=size + (16 - size % 16)) + fp.write(struct.pack(HEADER_V73, *header)) + else: + msg = f"Unsupported version {version}" + raise VTFException(msg) + + if version > (7, 2): + fp.write(b"\x01\x00\x00\x00") + fp.write(struct.pack("> mip_id) + mip_height = max(min_size, height >> mip_id) + mip = im.resize((mip_width, mip_height)) + _write_image(fp, mip, pixel_format) + _write_image(fp, im, pixel_format) + + +def _accept(prefix): + valid_header = prefix[:4] == b"VTF\x00" + valid_version = struct.unpack_from("<2I", prefix, 4) >= (7, 0) + return valid_header and valid_version + + +Image.register_open(VtfImageFile.format, VtfImageFile, _accept) +Image.register_save(VtfImageFile.format, _save) +Image.register_extension(VtfImageFile.format, ".vtf") diff --git a/src/PIL/__init__.py b/src/PIL/__init__.py index 09546fe6333..a2c3028829a 100644 --- a/src/PIL/__init__.py +++ b/src/PIL/__init__.py @@ -71,6 +71,7 @@ "XbmImagePlugin", "XpmImagePlugin", "XVThumbImagePlugin", + "VtfImagePlugin", ] diff --git a/src/_imaging.c b/src/_imaging.c index 5c2f7b4b623..082a38624ed 100644 --- a/src/_imaging.c +++ b/src/_imaging.c @@ -4179,6 +4179,8 @@ PyImaging_ZipDecoderNew(PyObject *self, PyObject *args); /* Encoders (in encode.c) */ extern PyObject * +PyImaging_BcnEncoderNew(PyObject *self, PyObject *args); +extern PyObject * PyImaging_EpsEncoderNew(PyObject *self, PyObject *args); extern PyObject * PyImaging_GifEncoderNew(PyObject *self, PyObject *args); @@ -4246,6 +4248,7 @@ static PyMethodDef functions[] = { /* Codecs */ {"bcn_decoder", (PyCFunction)PyImaging_BcnDecoderNew, METH_VARARGS}, + {"bcn_encoder", (PyCFunction)PyImaging_BcnEncoderNew, METH_VARARGS}, {"bit_decoder", (PyCFunction)PyImaging_BitDecoderNew, METH_VARARGS}, {"eps_encoder", (PyCFunction)PyImaging_EpsEncoderNew, METH_VARARGS}, {"fli_decoder", (PyCFunction)PyImaging_FliDecoderNew, METH_VARARGS}, diff --git a/src/encode.c b/src/encode.c index f711865d5a0..0c349456a42 100644 --- a/src/encode.c +++ b/src/encode.c @@ -28,6 +28,7 @@ #include "thirdparty/pythoncapi_compat.h" #include "libImaging/Imaging.h" #include "libImaging/Gif.h" +#include "libImaging/Bcn.h" #ifdef HAVE_UNISTD_H #include /* write */ @@ -377,6 +378,33 @@ get_packer(ImagingEncoderObject *encoder, const char *mode, const char *rawmode) return 0; } +/* -------------------------------------------------------------------- */ +/* BCN */ +/* -------------------------------------------------------------------- */ + +PyObject * +PyImaging_BcnEncoderNew(PyObject *self, PyObject *args) { + ImagingEncoderObject *encoder; + + char *mode; + int n = 0; + char *pixel_format = ""; + if (!PyArg_ParseTuple(args, "si|s", &mode, &n, &pixel_format)) { + return NULL; + } + + encoder = PyImaging_EncoderNew(sizeof(char *)); + if (encoder == NULL) { + return NULL; + } + + encoder->encode = ImagingBcnEncode; + encoder->state.state = n; + ((BCNSTATE *)encoder->state.context)->pixel_format = pixel_format; + + return (PyObject *)encoder; +} + /* -------------------------------------------------------------------- */ /* EPS */ /* -------------------------------------------------------------------- */ diff --git a/src/libImaging/Bcn.h b/src/libImaging/Bcn.h index 1a6fbee4576..5ab67a1925c 100644 --- a/src/libImaging/Bcn.h +++ b/src/libImaging/Bcn.h @@ -1,3 +1,42 @@ typedef struct { char *pixel_format; } BCNSTATE; + +typedef struct { + UINT8 r, g, b, a; +} rgba; + +typedef struct { + UINT8 l; +} lum; + +typedef struct { + FLOAT32 r, g, b; +} rgb32f; + +typedef struct { + UINT16 c0, c1; + UINT32 lut; +} bc1_color; + +typedef struct { + UINT8 a0, a1; + UINT8 lut[6]; +} bc3_alpha; + +typedef struct { + INT8 a0, a1; + UINT8 lut[6]; +} bc5s_alpha; + +#define BIT_MASK(bit_count) ((1 << (bit_count)) - 1) +#define SET_BITS(target, bit_offset, bit_count, value) \ + target |= (((value) & BIT_MASK(bit_count)) << (bit_offset)) +#define GET_BITS(source, bit_offset, bit_count) \ + ((source) & (BIT_MASK(bit_count) << (bit_offset))) >> (bit_offset) +#define SWAP(TYPE, A, B) \ + do { \ + TYPE TMP = A; \ + (A) = B; \ + (B) = TMP; \ + } while (0) diff --git a/src/libImaging/BcnDecode.c b/src/libImaging/BcnDecode.c index 9a41febc764..592ba4c01d0 100644 --- a/src/libImaging/BcnDecode.c +++ b/src/libImaging/BcnDecode.c @@ -15,29 +15,6 @@ #include "Bcn.h" -typedef struct { - UINT8 r, g, b, a; -} rgba; - -typedef struct { - UINT8 l; -} lum; - -typedef struct { - UINT16 c0, c1; - UINT32 lut; -} bc1_color; - -typedef struct { - UINT8 a0, a1; - UINT8 lut[6]; -} bc3_alpha; - -typedef struct { - INT8 a0, a1; - UINT8 lut[6]; -} bc5s_alpha; - #define LOAD16(p) (p)[0] | ((p)[1] << 8) #define LOAD32(p) (p)[0] | ((p)[1] << 8) | ((p)[2] << 16) | ((p)[3] << 24) diff --git a/src/libImaging/BcnEncode.c b/src/libImaging/BcnEncode.c new file mode 100644 index 00000000000..807f5cafae3 --- /dev/null +++ b/src/libImaging/BcnEncode.c @@ -0,0 +1,234 @@ +/* + * The Python Imaging Library. + * $Id$ + * + * encoder for packed bitfields (ST3C/DXT) + * + * history: + * 22-08-11 Initial implementation + * + * Copyright (c) REDxEYE 2022. + * + * See the README file for information on usage and redistribution. + */ + +#include "Imaging.h" +#include "Bcn.h" +#include "math.h" + +#define PACK_SHORT_565(r, g, b) \ + ((((b) << 8) & 0xF800) | (((g) << 3) & 0x7E0) | ((r) >> 3)) + +#define UNPACK_SHORT_565(source, r, g, b) \ + (r) = GET_BITS((source), 0, 5); \ + (g) = GET_BITS((source), 5, 6); \ + (b) = GET_BITS((source), 11, 5); + +#define WRITE_SHORT(buf, value) \ + *(buf++) = value & 0xFF; \ + *(buf++) = value >> 8; + +#define WRITE_INT(buf, value) \ + WRITE_SHORT(buf, value & 0xFFFF) \ + WRITE_SHORT(buf, value >> 16) + +#define WRITE_BC1_BLOCK(buf, block) \ + WRITE_SHORT(buf, block.c0) \ + WRITE_SHORT(buf, block.c1) \ + WRITE_INT(buf, block.lut) + +static inline UINT16 +rgb565_diff(UINT16 c0, UINT16 c1) { + UINT8 r0, g0, b0, r1, g1, b1; + UNPACK_SHORT_565(c0, r0, g0, b0) + UNPACK_SHORT_565(c1, r1, g1, b1) + return ((UINT16)abs(r0 - r1)) + abs(g0 - g1) + abs(b0 - b1); +} + +static inline UINT16 +rgb565_lerp(UINT16 c0, UINT16 c1, UINT8 a_fac, UINT8 b_fac) { + UINT8 r0, g0, b0, r1, g1, b1; + UNPACK_SHORT_565(c0, r0, g0, b0) + UNPACK_SHORT_565(c1, r1, g1, b1) + return PACK_SHORT_565( + (r0 * a_fac + r1 * b_fac) / (a_fac + b_fac), + (g0 * a_fac + g1 * b_fac) / (a_fac + b_fac), + (b0 * a_fac + b1 * b_fac) / (a_fac + b_fac) + ); +} + +typedef struct { + UINT16 value; + UINT8 frequency; +} Color; + +static void +selection_sort(Color arr[], UINT32 n) { + UINT32 min_idx, i, j; + + for (i = 0; i < n - 1; i++) { + min_idx = i; + for (j = i + 1; j < n; j++) { + if (arr[j].frequency < arr[min_idx].frequency) { + min_idx = j; + } + } + SWAP(Color, arr[min_idx], arr[i]); + } +} + +static void +pick_2_major_colors( + const UINT16 *unique_colors, + const UINT8 *color_freq, + UINT16 color_count, + UINT16 *color0, + UINT16 *color1 +) { + UINT32 i; + Color colors[16]; + memset(colors, 0, sizeof(colors)); + for (i = 0; i < color_count; ++i) { + colors[i].value = unique_colors[i]; + colors[i].frequency = color_freq[i]; + } + selection_sort(colors, color_count); + *color0 = colors[color_count - 1].value; + + if (color_count == 1) { + *color1 = colors[color_count - 1].value; + } else { + *color1 = colors[color_count - 2].value; + } +} + +static UINT8 +get_closest_color_index(const UINT16 *colors, UINT16 color) { + UINT16 color_error = 0xFFF8; + UINT16 lowest_id = 0; + UINT32 color_id; + + for (color_id = 0; color_id < 4; color_id++) { + UINT8 error = rgb565_diff(colors[color_id], color); + if (error == 0) { + return color_id; + } + if (error <= color_error) { + color_error = error; + lowest_id = color_id; + } + } + return lowest_id; +} + +int +encode_bc1(Imaging im, ImagingCodecState state, UINT8 *buf, int bytes) { + UINT8 *dst = buf; + UINT8 alpha = 0; + INT32 block_index; + 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) { + state->errcode = IMAGING_CODEC_MEMORY; + return 0; + } + + memset(buf, 0, block_count * sizeof(bc1_color)); + for (block_index = 0; block_index < block_count; block_index++) { + state->x = (block_index % (im->xsize / 4)); + state->y = (block_index / (im->xsize / 4)); + UINT16 unique_count = 0; + + UINT16 all_colors[16]; + 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)); + memset(opaque, 0, sizeof(opaque)); + UINT32 by, bx, x, y; + for (by = 0; by < 4; ++by) { + for (bx = 0; bx < 4; ++bx) { + x = (state->x * 4) + bx; + y = (state->y * 4) + by; + UINT8 r = im->image[y][x * im->pixelsize + 2]; + UINT8 g = im->image[y][x * im->pixelsize + 1]; + 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 >= 128; + local_alpha |= a <= 128; + all_colors[bx + by * 4] = color; + + UINT8 new_color = 1; + UINT16 color_id = 1; + for (color_id = 0; color_id < unique_count; color_id++) { + if (unique_colors[color_id] == color) { + color_frequency[color_id]++; + new_color = 0; + break; + } + } + if (new_color) { + unique_colors[unique_count] = color; + color_frequency[unique_count]++; + unique_count++; + } + } + } + + UINT16 c0 = 0, c1 = 0; + pick_2_major_colors(unique_colors, color_frequency, unique_count, &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 (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 ((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); + } + WRITE_BC1_BLOCK(dst, block) + } + state->errcode = IMAGING_CODEC_END; + return dst - buf; +} + +int +ImagingBcnEncode(Imaging im, ImagingCodecState state, UINT8 *buf, int bytes) { + switch (state->state) { + case 1: { + return encode_bc1(im, state, buf, bytes); + } + default: { + state->errcode = IMAGING_CODEC_CONFIG; + return 0; + } + } +} diff --git a/src/libImaging/Imaging.h b/src/libImaging/Imaging.h index b1c3aed4175..6b3ee4df4eb 100644 --- a/src/libImaging/Imaging.h +++ b/src/libImaging/Imaging.h @@ -564,6 +564,8 @@ typedef int (*ImagingCodec)( extern int ImagingBcnDecode(Imaging im, ImagingCodecState state, UINT8 *buffer, Py_ssize_t bytes); extern int +ImagingBcnEncode(Imaging im, ImagingCodecState state, UINT8 *buffer, int bytes); +extern int ImagingBitDecode(Imaging im, ImagingCodecState state, UINT8 *buffer, Py_ssize_t bytes); extern int ImagingEpsEncode(Imaging im, ImagingCodecState state, UINT8 *buffer, int bytes); diff --git a/src/libImaging/Pack.c b/src/libImaging/Pack.c index f3b71421595..43c2e8f9bdb 100644 --- a/src/libImaging/Pack.c +++ b/src/libImaging/Pack.c @@ -242,6 +242,17 @@ packLA(UINT8 *out, const UINT8 *in, int pixels) { in += 4; } } +static void +packRG(UINT8 *out, const UINT8 *in, int pixels) { + int i; + /* LA, pixel interleaved */ + for (i = 0; i < pixels; i++) { + out[0] = in[R]; + out[1] = in[G]; + out += 2; + in += 4; + } +} static void packLAL(UINT8 *out, const UINT8 *in, int pixels) { @@ -491,7 +502,7 @@ copy3(UINT8 *out, const UINT8 *in, int pixels) { static void copy4(UINT8 *out, const UINT8 *in, int pixels) { /* RGBA, CMYK quadruples */ - memcpy(out, in, 4 * pixels); + memcpy(out, in, pixels * 4); } static void @@ -580,6 +591,7 @@ static struct { {"RGB", "BGRX", 32, ImagingPackBGRX}, {"RGB", "XBGR", 32, ImagingPackXBGR}, {"RGB", "RGB;L", 24, packRGBL}, + {"RGB", "RG", 16, packRG}, {"RGB", "R", 8, band0}, {"RGB", "G", 8, band1}, {"RGB", "B", 8, band2}, @@ -592,6 +604,7 @@ static struct { {"RGBA", "BGRA", 32, ImagingPackBGRA}, {"RGBA", "ABGR", 32, ImagingPackABGR}, {"RGBA", "BGRa", 32, ImagingPackBGRa}, + {"RGBA", "RG", 16, packRG}, {"RGBA", "R", 8, band0}, {"RGBA", "G", 8, band1}, {"RGBA", "B", 8, band2}, diff --git a/src/libImaging/Unpack.c b/src/libImaging/Unpack.c index c23d5d889f6..b8b2cc92f06 100644 --- a/src/libImaging/Unpack.c +++ b/src/libImaging/Unpack.c @@ -426,6 +426,18 @@ unpackLA(UINT8 *_out, const UINT8 *in, int pixels) { } } +static void +unpackRG(UINT8 *_out, const UINT8 *in, int pixels) { + int i; + /* LA, pixel interleaved */ + for (i = 0; i < pixels; i++) { + _out[R] = in[0]; + _out[G] = in[1]; + in += 2; + _out += 4; + } +} + static void unpackLAL(UINT8 *_out, const UINT8 *in, int pixels) { int i; @@ -1612,6 +1624,9 @@ static struct { {"PA", "PA;L", 16, unpackLAL}, {"PA", "LA", 16, unpackLA}, + /*2 channel to RGB/RGBA*/ + {"RGB", "RG", 16, unpackRG}, + {"RGBA", "RG", 16, unpackRG}, /* true colour */ {"RGB", "RGB", 24, ImagingUnpackRGB}, {"RGB", "RGB;L", 24, unpackRGBL},