diff --git a/Tests/images/vtf_a8.png b/Tests/images/vtf_a8.png new file mode 100644 index 000000000..1e95446f1 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 000000000..4705e9e09 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 000000000..7e8acdaf8 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 000000000..081220d80 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 000000000..7e8acdaf8 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 000000000..f74c3756b 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 000000000..95f6f7941 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 000000000..2ea82c302 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 000000000..790e5e7e8 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 000000000..d3e5e94f5 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 000000000..a6e96a551 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 000000000..30df1e057 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 000000000..7e8acdaf8 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 000000000..b8eb744db 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 000000000..95f6f7941 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 000000000..6d8248bc6 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 000000000..8689f14ca 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 000000000..8acc013e0 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 000000000..3c15954dd --- /dev/null +++ b/Tests/test_file_vtf.py @@ -0,0 +1,124 @@ +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) -> None: + 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) -> None: + 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 +) -> None: + 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 +) -> None: + with Image.open(file_path) as f: + assert f.mode == expected_mode + with Image.open(etalon_path) as e: + converted_e = e.convert(expected_mode) + if epsilon == 0: + assert_image_equal(converted_e, f) + else: + assert_image_similar(converted_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.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 +) -> None: + 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/src/PIL/VtfImagePlugin.py b/src/PIL/VtfImagePlugin.py new file mode 100644 index 000000000..3aa1444f0 --- /dev/null +++ b/src/PIL/VtfImagePlugin.py @@ -0,0 +1,401 @@ +""" +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 BytesIO +from math import ceil, log +from typing import IO, 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 = " int: + if pixel_format in (VtfPF.DXT1, VtfPF.DXT1_ONEBITALPHA): + return width * height // 2 + elif pixel_format in ( + VtfPF.DXT3, + VtfPF.DXT5, + VtfPF.A8, + VtfPF.I8, + ): + return width * height + elif pixel_format in (VtfPF.UV88, VtfPF.IA88): + return width * height * 2 + elif pixel_format in (VtfPF.RGB888, VtfPF.BGR888): + return width * height * 3 + elif pixel_format == VtfPF.RGBA8888: + return width * height * 4 + msg = f"Unsupported VTF pixel format: {pixel_format}" + raise VTFException(msg) + + +def _get_mipmap_count(width: int, height: int) -> int: + mip_count = 1 + while True: + mip_width = width >> 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: IO[bytes], im: Image.Image, pixel_format: VtfPF) -> None: + encoder_args: tuple[int,] | tuple[str, int, int] + if pixel_format == VtfPF.DXT1: + encoder_args = (1,) + elif pixel_format == VtfPF.DXT3: + encoder_args = (3,) + elif pixel_format == VtfPF.DXT5: + encoder_args = (5,) + elif pixel_format == VtfPF.RGB888: + encoder_args = ("RGB", 0, 0) + elif pixel_format == VtfPF.BGR888: + encoder_args = ("BGR", 0, 0) + elif pixel_format == VtfPF.RGBA8888: + encoder_args = ("RGBA", 0, 0) + elif pixel_format == VtfPF.A8: + encoder_args = ("A", 0, 0) + elif pixel_format == VtfPF.I8: + encoder_args = ("L", 0, 0) + elif pixel_format == VtfPF.IA88: + encoder_args = ("LA", 0, 0) + elif pixel_format == VtfPF.UV88: + encoder_args = ("RG", 0, 0) + else: + msg = f"Unsupported pixel format: {pixel_format!r}" + raise VTFException(msg) + + codec_name = "bcn" if pixel_format in BLOCK_COMPRESSED else "raw" + tile = [ImageFile._Tile(codec_name, (0, 0) + im.size, fp.tell(), encoder_args)] + ImageFile._save(im, fp, tile, _get_texture_size(pixel_format, *im.size)) + + +def _closest_power(x: int) -> int: + 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) + + pixel_format = VtfPF(header.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) + + if pixel_format in (VtfPF.DXT1, VtfPF.DXT1_ONEBITALPHA): + args = (1, "DXT1") + elif pixel_format == VtfPF.DXT3: + args = (2, "DXT3") + elif pixel_format == VtfPF.DXT5: + args = (3, "DXT5") + elif pixel_format == VtfPF.RGBA8888: + args = ("RGBA", 0, 1) + elif pixel_format == VtfPF.RGB888: + args = ("RGB", 0, 1) + elif pixel_format == VtfPF.BGR888: + args = ("BGR", 0, 1) + elif pixel_format == VtfPF.BGRA8888: + args = ("BGRA", 0, 1) + elif pixel_format == VtfPF.UV88: + args = ("RG", 0, 1) + elif pixel_format == VtfPF.I8: + args = ("L", 0, 1) + elif pixel_format == VtfPF.A8: + args = ("A", 0, 1) + elif pixel_format == VtfPF.IA88: + args = ("LA", 0, 1) + else: + msg = f"Unsupported VTF pixel format: {pixel_format}" + raise VTFException(msg) + + self._size = (header.width, header.height) + + low_format = VtfPF(header.low_pixel_format) + + 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) + + codec_name = "bcn" if pixel_format in BLOCK_COMPRESSED else "raw" + self.tile = [ImageFile._Tile(codec_name, (0, 0) + self.size, data_start, args)] + + +def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: + 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 in ( + VtfPF.DXT3, + VtfPF.DXT5, + VtfPF.RGBA8888, + VtfPF.BGRA8888, + VtfPF.A8, + VtfPF.IA88, + ): + flags |= CompiledVtfFlags.EIGHTBITALPHA + 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: bytes) -> bool: + valid_header = prefix.startswith(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 6e4c23f89..fabc61139 100644 --- a/src/PIL/__init__.py +++ b/src/PIL/__init__.py @@ -72,6 +72,7 @@ _plugins = [ "XbmImagePlugin", "XpmImagePlugin", "XVThumbImagePlugin", + "VtfImagePlugin", ] diff --git a/src/libImaging/Pack.c b/src/libImaging/Pack.c index c29473d90..138d50284 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; + /* RG, 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) { @@ -480,7 +491,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 @@ -569,6 +580,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}, @@ -581,6 +593,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 9c3ee2665..142af9fb2 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; + /* RG, 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},