Merge 487c7b2be1
into 7e4d8e2f55
BIN
Tests/images/vtf_a8.png
Normal file
After Width: | Height: | Size: 4.1 KiB |
BIN
Tests/images/vtf_a8.vtf
Normal file
BIN
Tests/images/vtf_bgr888.png
Normal file
After Width: | Height: | Size: 6.2 KiB |
BIN
Tests/images/vtf_bgr888.vtf
Normal file
BIN
Tests/images/vtf_dxt1.png
Normal file
After Width: | Height: | Size: 6.2 KiB |
BIN
Tests/images/vtf_dxt1.vtf
Normal file
BIN
Tests/images/vtf_dxt1A.png
Normal file
After Width: | Height: | Size: 9.5 KiB |
BIN
Tests/images/vtf_dxt1A.vtf
Normal file
BIN
Tests/images/vtf_i8.png
Normal file
After Width: | Height: | Size: 4.5 KiB |
BIN
Tests/images/vtf_i8.vtf
Normal file
BIN
Tests/images/vtf_ia88.png
Normal file
After Width: | Height: | Size: 7.4 KiB |
BIN
Tests/images/vtf_ia88.vtf
Normal file
BIN
Tests/images/vtf_rgb888.png
Normal file
After Width: | Height: | Size: 6.2 KiB |
BIN
Tests/images/vtf_rgb888.vtf
Normal file
BIN
Tests/images/vtf_rgba8888.png
Normal file
After Width: | Height: | Size: 9.5 KiB |
BIN
Tests/images/vtf_rgba8888.vtf
Normal file
BIN
Tests/images/vtf_uv88.png
Normal file
After Width: | Height: | Size: 4.7 KiB |
BIN
Tests/images/vtf_uv88.vtf
Normal file
124
Tests/test_file_vtf.py
Normal file
|
@ -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)
|
401
src/PIL/VtfImagePlugin.py
Normal file
|
@ -0,0 +1,401 @@
|
||||||
|
"""
|
||||||
|
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/
|
||||||
|
"""
|
||||||
|
|
||||||
|
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 = "<I2HI2H4x3f4xfIbI2b"
|
||||||
|
HEADER_V72 = "<I2HI2H4x3f4xfIbI2bH"
|
||||||
|
HEADER_V73 = "<I2HI2H4x3f4xfIbI2bH3xI8x"
|
||||||
|
|
||||||
|
|
||||||
|
def _get_texture_size(pixel_format: VtfPF, width: int, height: int) -> 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("<I", header.header_size))
|
||||||
|
fp.write(b"\x30\x00\x00\x00")
|
||||||
|
fp.write(struct.pack("<I", header.header_size + len(thumb_buffer.getbuffer())))
|
||||||
|
else:
|
||||||
|
fp.write(b"\x00" * (16 - fp.tell() % 16))
|
||||||
|
fp.write(thumb_buffer.getbuffer())
|
||||||
|
|
||||||
|
if pixel_format in BLOCK_COMPRESSED:
|
||||||
|
min_size = 4
|
||||||
|
else:
|
||||||
|
min_size = 1
|
||||||
|
|
||||||
|
for mip_id in range(mipmap_count - 1, 0, -1):
|
||||||
|
mip_width = max(min_size, width >> 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")
|
|
@ -72,6 +72,7 @@ _plugins = [
|
||||||
"XbmImagePlugin",
|
"XbmImagePlugin",
|
||||||
"XpmImagePlugin",
|
"XpmImagePlugin",
|
||||||
"XVThumbImagePlugin",
|
"XVThumbImagePlugin",
|
||||||
|
"VtfImagePlugin",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -242,6 +242,17 @@ packLA(UINT8 *out, const UINT8 *in, int pixels) {
|
||||||
in += 4;
|
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
|
static void
|
||||||
packLAL(UINT8 *out, const UINT8 *in, int pixels) {
|
packLAL(UINT8 *out, const UINT8 *in, int pixels) {
|
||||||
|
@ -480,7 +491,7 @@ copy3(UINT8 *out, const UINT8 *in, int pixels) {
|
||||||
static void
|
static void
|
||||||
copy4(UINT8 *out, const UINT8 *in, int pixels) {
|
copy4(UINT8 *out, const UINT8 *in, int pixels) {
|
||||||
/* RGBA, CMYK quadruples */
|
/* RGBA, CMYK quadruples */
|
||||||
memcpy(out, in, 4 * pixels);
|
memcpy(out, in, pixels * 4);
|
||||||
}
|
}
|
||||||
|
|
||||||
static void
|
static void
|
||||||
|
@ -569,6 +580,7 @@ static struct {
|
||||||
{"RGB", "BGRX", 32, ImagingPackBGRX},
|
{"RGB", "BGRX", 32, ImagingPackBGRX},
|
||||||
{"RGB", "XBGR", 32, ImagingPackXBGR},
|
{"RGB", "XBGR", 32, ImagingPackXBGR},
|
||||||
{"RGB", "RGB;L", 24, packRGBL},
|
{"RGB", "RGB;L", 24, packRGBL},
|
||||||
|
{"RGB", "RG", 16, packRG},
|
||||||
{"RGB", "R", 8, band0},
|
{"RGB", "R", 8, band0},
|
||||||
{"RGB", "G", 8, band1},
|
{"RGB", "G", 8, band1},
|
||||||
{"RGB", "B", 8, band2},
|
{"RGB", "B", 8, band2},
|
||||||
|
@ -581,6 +593,7 @@ static struct {
|
||||||
{"RGBA", "BGRA", 32, ImagingPackBGRA},
|
{"RGBA", "BGRA", 32, ImagingPackBGRA},
|
||||||
{"RGBA", "ABGR", 32, ImagingPackABGR},
|
{"RGBA", "ABGR", 32, ImagingPackABGR},
|
||||||
{"RGBA", "BGRa", 32, ImagingPackBGRa},
|
{"RGBA", "BGRa", 32, ImagingPackBGRa},
|
||||||
|
{"RGBA", "RG", 16, packRG},
|
||||||
{"RGBA", "R", 8, band0},
|
{"RGBA", "R", 8, band0},
|
||||||
{"RGBA", "G", 8, band1},
|
{"RGBA", "G", 8, band1},
|
||||||
{"RGBA", "B", 8, band2},
|
{"RGBA", "B", 8, band2},
|
||||||
|
|
|
@ -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
|
static void
|
||||||
unpackLAL(UINT8 *_out, const UINT8 *in, int pixels) {
|
unpackLAL(UINT8 *_out, const UINT8 *in, int pixels) {
|
||||||
int i;
|
int i;
|
||||||
|
@ -1612,6 +1624,9 @@ static struct {
|
||||||
{"PA", "PA;L", 16, unpackLAL},
|
{"PA", "PA;L", 16, unpackLAL},
|
||||||
{"PA", "LA", 16, unpackLA},
|
{"PA", "LA", 16, unpackLA},
|
||||||
|
|
||||||
|
/*2 channel to RGB/RGBA*/
|
||||||
|
{"RGB", "RG", 16, unpackRG},
|
||||||
|
{"RGBA", "RG", 16, unpackRG},
|
||||||
/* true colour */
|
/* true colour */
|
||||||
{"RGB", "RGB", 24, ImagingUnpackRGB},
|
{"RGB", "RGB", 24, ImagingUnpackRGB},
|
||||||
{"RGB", "RGB;L", 24, unpackRGBL},
|
{"RGB", "RGB;L", 24, unpackRGBL},
|
||||||
|
|