Initial VTF read support

This commit is contained in:
REDxEYE 2022-08-07 04:09:49 +03:00
parent 2755e0ffaa
commit df3c025cbc
2 changed files with 234 additions and 0 deletions

233
src/PIL/VtfImagePlugin.py Normal file
View File

@ -0,0 +1,233 @@
"""
A Pillow loader for .vtf files (aka Valve Texture Format)
REDxEYE <med45c@gmail.com>
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('<I2HI2HI3fIfIbI2b', self.fp.read(51)), 0, 0, 0, 0, 0)
elif (7, 2) <= version < (7, 3):
header = VTFHeader(*struct.unpack('<I2HI2HI3fIfIbI2bH', self.fp.read(53)), 0, 0, 0, 0)
elif (7, 3) <= version < (7, 5):
header = VTFHeader(*struct.unpack('<I2HI2HI3fIfIbI2bHHBIQ', self.fp.read(68)))
self.fp.seek(header.resource_count * 8, 1)
else:
raise VTFException(f"Unsupported VTF version: {version}")
flags = CompiledVtfFlags(header.flags)
pixel_format = VtfPF(header.pixel_format)
if pixel_format in RGB_FORMATS:
self.mode = 'RGB'
elif pixel_format in RGBA_FORMATS:
self.mode = 'RGBA'
elif pixel_format in L_FORMATS:
self.mode = 'L'
elif pixel_format in LA_FORMATS:
self.mode = 'LA'
else:
raise VTFException(f'Unsupported VTF pixel format: {pixel_format}')
# if flags & CompiledVtfFlags.EIGHTBITALPHA or flags & CompiledVtfFlags.ONEBITALPHA:
# if 'A' not in self.mode:
# raise SyntaxError('Expected an "RGBA" mode when EIGHTBITALPHA or ONEBITALPHA flags are set ')
# else:
# if 'A' in self.mode:
# raise SyntaxError('Expected an "RGB" mode when EIGHTBITALPHA or ONEBITALPHA flags are not set ')
self._size = (header.width, header.height)
data_start = self.fp.tell()
data_start += _get_texture_size(header.lowres_format, header.lowres_width, header.lowres_height)
min_res = 4 if pixel_format in (VtfPF.DXT1, VtfPF.DXT1_ONEBITALPHA, VtfPF.DXT3, VtfPF.DXT5) 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 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")

View File

@ -69,6 +69,7 @@ _plugins = [
"XbmImagePlugin",
"XpmImagePlugin",
"XVThumbImagePlugin",
"VtfImagePlugin",
]