diff --git a/src/PIL/VtfImagePlugin.py b/src/PIL/VtfImagePlugin.py new file mode 100644 index 000000000..fe1a90762 --- /dev/null +++ b/src/PIL/VtfImagePlugin.py @@ -0,0 +1,233 @@ +""" +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/ +""" + +import struct +from enum import IntFlag, IntEnum +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 + + +VTFHeader = NamedTuple("VTFHeader", [ + ("header_size", int), + ("width", int), + ("height", int), + ("flags", int), + ("frames", int), + ("first_frames", int), + ("padding0", int), + ("reflectivity_r", float), + ("reflectivity_g", float), + ("reflectivity_b", float), + ("padding1", int), + ("bumpmap_scale", float), + ("pixel_format", int), + ("mipmap_count", int), + ("lowres_format", int), + ("lowres_width", int), + ("lowres_height", int), + + # V 7.2+ + ('depth', int), + + # V 7.3+ + ('padding2', int), + ('padding2_', int), + ('resource_count', int), + ('padding3', int), + +]) +RGB_FORMATS = (VtfPF.RGB888,) +RGBA_FORMATS = (VtfPF.DXT1, VtfPF.DXT1_ONEBITALPHA, VtfPF.DXT3, VtfPF.DXT5, VtfPF.RGBA8888) +L_FORMATS = (VtfPF.A8, VtfPF.I8,) +LA_FORMATS = (VtfPF.IA88, VtfPF.UV88,) + +SUPPORTED_FORMATS = RGBA_FORMATS + RGB_FORMATS + LA_FORMATS + L_FORMATS + + +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: + return width * height + elif pixel_format in LA_FORMATS: + return width * height * 2 + elif pixel_format in (VtfPF.RGB888,): + return width * height * 3 + elif pixel_format in (VtfPF.RGBA8888,): + return width * height * 4 + raise VTFException(f'Unsupported VTF pixel format: {pixel_format}') + + +class VtfImageFile(ImageFile.ImageFile): + format = "VTF" + format_description = "Valve Texture Format" + + def _open(self): + if not _accept(self.fp.read(12)): + raise SyntaxError("not a VTF file") + self.fp.seek(4) + version = struct.unpack('<2I', self.fp.read(8)) + if version <= (7, 2): + header = VTFHeader(*struct.unpack('> 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 in (VtfPF.RGBA8888,): + tile = ("raw", (0, 0) + self.size, data_start, ("RGBA", 0, 1)) + elif pixel_format in (VtfPF.RGB888,): + tile = ("raw", (0, 0) + self.size, data_start, ("RGB", 0, 1)) + elif pixel_format in L_FORMATS: + tile = ("raw", (0, 0) + self.size, data_start, ("L", 0, 1)) + elif pixel_format in LA_FORMATS: + tile = ("raw", (0, 0) + self.size, data_start, ("LA", 0, 1)) + else: + raise VTFException(f'Unsupported VTF pixel format: {pixel_format}') + self.tile = [tile] + + +def _save(im, fp, filename): + if im.mode not in ("RGB", "RGBA"): + raise OSError(f"cannot write mode {im.mode} as VTF") + + +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 e65b155b2..4908a3fc3 100644 --- a/src/PIL/__init__.py +++ b/src/PIL/__init__.py @@ -69,6 +69,7 @@ _plugins = [ "XbmImagePlugin", "XpmImagePlugin", "XVThumbImagePlugin", + "VtfImagePlugin", ]