Merge pull request #6486 from REDxEYE/improved_dds

This commit is contained in:
Hugo van Kemenade 2023-12-06 22:51:05 +02:00 committed by GitHub
commit 7cc0482804
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 541 additions and 198 deletions

BIN
Tests/images/bc1.dds Executable file

Binary file not shown.

BIN
Tests/images/bc1_typeless.dds Executable file

Binary file not shown.

BIN
Tests/images/bc4u.dds Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -17,6 +17,9 @@ TEST_FILE_DX10_BC4_UNORM = "Tests/images/bc4_unorm.dds"
TEST_FILE_DX10_BC5_TYPELESS = "Tests/images/bc5_typeless.dds" TEST_FILE_DX10_BC5_TYPELESS = "Tests/images/bc5_typeless.dds"
TEST_FILE_DX10_BC5_UNORM = "Tests/images/bc5_unorm.dds" TEST_FILE_DX10_BC5_UNORM = "Tests/images/bc5_unorm.dds"
TEST_FILE_DX10_BC5_SNORM = "Tests/images/bc5_snorm.dds" TEST_FILE_DX10_BC5_SNORM = "Tests/images/bc5_snorm.dds"
TEST_FILE_DX10_BC1 = "Tests/images/bc1.dds"
TEST_FILE_DX10_BC1_TYPELESS = "Tests/images/bc1_typeless.dds"
TEST_FILE_BC4U = "Tests/images/bc4u.dds"
TEST_FILE_BC5S = "Tests/images/bc5s.dds" TEST_FILE_BC5S = "Tests/images/bc5s.dds"
TEST_FILE_BC5U = "Tests/images/bc5u.dds" TEST_FILE_BC5U = "Tests/images/bc5u.dds"
TEST_FILE_BC6H = "Tests/images/bc6h.dds" TEST_FILE_BC6H = "Tests/images/bc6h.dds"
@ -31,11 +34,20 @@ TEST_FILE_UNCOMPRESSED_RGB = "Tests/images/hopper.dds"
TEST_FILE_UNCOMPRESSED_RGB_WITH_ALPHA = "Tests/images/uncompressed_rgb.dds" TEST_FILE_UNCOMPRESSED_RGB_WITH_ALPHA = "Tests/images/uncompressed_rgb.dds"
def test_sanity_dxt1(): @pytest.mark.parametrize(
"""Check DXT1 images can be opened""" "image_path",
(
TEST_FILE_DXT1,
# hexeditted to use DX10 FourCC
TEST_FILE_DX10_BC1,
TEST_FILE_DX10_BC1_TYPELESS,
),
)
def test_sanity_dxt1_bc1(image_path):
"""Check DXT1 and BC1 images can be opened"""
with Image.open(TEST_FILE_DXT1.replace(".dds", ".png")) as target: with Image.open(TEST_FILE_DXT1.replace(".dds", ".png")) as target:
target = target.convert("RGBA") target = target.convert("RGBA")
with Image.open(TEST_FILE_DXT1) as im: with Image.open(image_path) as im:
im.load() im.load()
assert im.format == "DDS" assert im.format == "DDS"
@ -71,10 +83,18 @@ def test_sanity_dxt5():
assert_image_equal_tofile(im, TEST_FILE_DXT5.replace(".dds", ".png")) assert_image_equal_tofile(im, TEST_FILE_DXT5.replace(".dds", ".png"))
def test_sanity_ati1(): @pytest.mark.parametrize(
"""Check ATI1 images can be opened""" "image_path",
(
TEST_FILE_ATI1,
# hexeditted to use BC4U FourCC
TEST_FILE_BC4U,
),
)
def test_sanity_ati1_bc4u(image_path):
"""Check ATI1 and BC4U images can be opened"""
with Image.open(TEST_FILE_ATI1) as im: with Image.open(image_path) as im:
im.load() im.load()
assert im.format == "DDS" assert im.format == "DDS"
@ -222,12 +242,6 @@ def test_dx10_r8g8b8a8_unorm_srgb():
) )
def test_unimplemented_dxgi_format():
with pytest.raises(NotImplementedError):
with Image.open("Tests/images/unimplemented_dxgi_format.dds"):
pass
@pytest.mark.parametrize( @pytest.mark.parametrize(
("mode", "size", "test_file"), ("mode", "size", "test_file"),
[ [
@ -326,9 +340,29 @@ def test_palette():
assert_image_equal_tofile(im, "Tests/images/transparent.gif") assert_image_equal_tofile(im, "Tests/images/transparent.gif")
def test_unimplemented_pixel_format(): @pytest.mark.parametrize(
"test_file",
(
"Tests/images/unsupported_bitcount_rgb.dds",
"Tests/images/unsupported_bitcount_luminance.dds",
),
)
def test_unsupported_bitcount(test_file):
with pytest.raises(OSError):
with Image.open(test_file):
pass
@pytest.mark.parametrize(
"test_file",
(
"Tests/images/unimplemented_dxgi_format.dds",
"Tests/images/unimplemented_pfflags.dds",
),
)
def test_not_implemented(test_file):
with pytest.raises(NotImplementedError): with pytest.raises(NotImplementedError):
with Image.open("Tests/images/unimplemented_pixel_format.dds"): with Image.open(test_file):
pass pass

View File

@ -33,6 +33,14 @@ Plugin reference
:undoc-members: :undoc-members:
:show-inheritance: :show-inheritance:
:mod:`~PIL.DdsImagePlugin` Module
---------------------------------
.. automodule:: PIL.DdsImagePlugin
:members:
:undoc-members:
:show-inheritance:
:mod:`~PIL.EpsImagePlugin` Module :mod:`~PIL.EpsImagePlugin` Module
--------------------------------- ---------------------------------

View File

@ -0,0 +1,56 @@
10.2.0
------
Backwards Incompatible Changes
==============================
TODO
^^^^
TODO
Deprecations
============
TODO
^^^^
TODO
API Changes
===========
TODO
^^^^
TODO
API Additions
=============
Added DdsImagePlugin enums
^^^^^^^^^^^^^^^^^^^^^^^^^^
:py:class:`~PIL.DdsImagePlugin.DDSD`, :py:class:`~PIL.DdsImagePlugin.DDSCAPS`,
:py:class:`~PIL.DdsImagePlugin.DDSCAPS2`, :py:class:`~PIL.DdsImagePlugin.DDPF`,
:py:class:`~PIL.DdsImagePlugin.DXGI_FORMAT` and :py:class:`~PIL.DdsImagePlugin.D3DFMT`
enums have been added to :py:class:`PIL.DdsImagePlugin`.
Security
========
TODO
^^^^
TODO
Other Changes
=============
Added DDS BC4U and DX10 BC1 and BC4 reading
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Support has been added to read the BC4U format of DDS images.
Support has also been added to read DX10 BC1 and BC4, whether UNORM or
TYPELESS.

View File

@ -14,6 +14,7 @@ expected to be backported to earlier versions.
.. toctree:: .. toctree::
:maxdepth: 2 :maxdepth: 2
10.2.0
10.1.0 10.1.0
10.0.1 10.0.1
10.0.0 10.0.0

View File

@ -3,111 +3,321 @@ A Pillow loader for .dds files (S3TC-compressed aka DXTC)
Jerome Leclanche <jerome@leclan.ch> Jerome Leclanche <jerome@leclan.ch>
Documentation: Documentation:
https://web.archive.org/web/20170802060935/http://oss.sgi.com/projects/ogl-sample/registry/EXT/texture_compression_s3tc.txt https://web.archive.org/web/20170802060935/http://oss.sgi.com/projects/ogl-sample/registry/EXT/texture_compression_s3tc.txt
The contents of this file are hereby released in the public domain (CC0) The contents of this file are hereby released in the public domain (CC0)
Full text of the CC0 license: Full text of the CC0 license:
https://creativecommons.org/publicdomain/zero/1.0/ https://creativecommons.org/publicdomain/zero/1.0/
""" """
import io
import struct import struct
from io import BytesIO import sys
from enum import IntEnum, IntFlag
from . import Image, ImageFile, ImagePalette from . import Image, ImageFile, ImagePalette
from ._binary import i32le as i32
from ._binary import o32le as o32 from ._binary import o32le as o32
# Magic ("DDS ") # Magic ("DDS ")
DDS_MAGIC = 0x20534444 DDS_MAGIC = 0x20534444
# DDS flags # DDS flags
DDSD_CAPS = 0x1 class DDSD(IntFlag):
DDSD_HEIGHT = 0x2 CAPS = 0x1
DDSD_WIDTH = 0x4 HEIGHT = 0x2
DDSD_PITCH = 0x8 WIDTH = 0x4
DDSD_PIXELFORMAT = 0x1000 PITCH = 0x8
DDSD_MIPMAPCOUNT = 0x20000 PIXELFORMAT = 0x1000
DDSD_LINEARSIZE = 0x80000 MIPMAPCOUNT = 0x20000
DDSD_DEPTH = 0x800000 LINEARSIZE = 0x80000
DEPTH = 0x800000
# DDS caps # DDS caps
DDSCAPS_COMPLEX = 0x8 class DDSCAPS(IntFlag):
DDSCAPS_TEXTURE = 0x1000 COMPLEX = 0x8
DDSCAPS_MIPMAP = 0x400000 TEXTURE = 0x1000
MIPMAP = 0x400000
class DDSCAPS2(IntFlag):
CUBEMAP = 0x200
CUBEMAP_POSITIVEX = 0x400
CUBEMAP_NEGATIVEX = 0x800
CUBEMAP_POSITIVEY = 0x1000
CUBEMAP_NEGATIVEY = 0x2000
CUBEMAP_POSITIVEZ = 0x4000
CUBEMAP_NEGATIVEZ = 0x8000
VOLUME = 0x200000
DDSCAPS2_CUBEMAP = 0x200
DDSCAPS2_CUBEMAP_POSITIVEX = 0x400
DDSCAPS2_CUBEMAP_NEGATIVEX = 0x800
DDSCAPS2_CUBEMAP_POSITIVEY = 0x1000
DDSCAPS2_CUBEMAP_NEGATIVEY = 0x2000
DDSCAPS2_CUBEMAP_POSITIVEZ = 0x4000
DDSCAPS2_CUBEMAP_NEGATIVEZ = 0x8000
DDSCAPS2_VOLUME = 0x200000
# Pixel Format # Pixel Format
DDPF_ALPHAPIXELS = 0x1 class DDPF(IntFlag):
DDPF_ALPHA = 0x2 ALPHAPIXELS = 0x1
DDPF_FOURCC = 0x4 ALPHA = 0x2
DDPF_PALETTEINDEXED8 = 0x20 FOURCC = 0x4
DDPF_RGB = 0x40 PALETTEINDEXED8 = 0x20
DDPF_LUMINANCE = 0x20000 RGB = 0x40
LUMINANCE = 0x20000
# dds.h
DDS_FOURCC = DDPF_FOURCC
DDS_RGB = DDPF_RGB
DDS_RGBA = DDPF_RGB | DDPF_ALPHAPIXELS
DDS_LUMINANCE = DDPF_LUMINANCE
DDS_LUMINANCEA = DDPF_LUMINANCE | DDPF_ALPHAPIXELS
DDS_ALPHA = DDPF_ALPHA
DDS_PAL8 = DDPF_PALETTEINDEXED8
DDS_HEADER_FLAGS_TEXTURE = DDSD_CAPS | DDSD_HEIGHT | DDSD_WIDTH | DDSD_PIXELFORMAT
DDS_HEADER_FLAGS_MIPMAP = DDSD_MIPMAPCOUNT
DDS_HEADER_FLAGS_VOLUME = DDSD_DEPTH
DDS_HEADER_FLAGS_PITCH = DDSD_PITCH
DDS_HEADER_FLAGS_LINEARSIZE = DDSD_LINEARSIZE
DDS_HEIGHT = DDSD_HEIGHT
DDS_WIDTH = DDSD_WIDTH
DDS_SURFACE_FLAGS_TEXTURE = DDSCAPS_TEXTURE
DDS_SURFACE_FLAGS_MIPMAP = DDSCAPS_COMPLEX | DDSCAPS_MIPMAP
DDS_SURFACE_FLAGS_CUBEMAP = DDSCAPS_COMPLEX
DDS_CUBEMAP_POSITIVEX = DDSCAPS2_CUBEMAP | DDSCAPS2_CUBEMAP_POSITIVEX
DDS_CUBEMAP_NEGATIVEX = DDSCAPS2_CUBEMAP | DDSCAPS2_CUBEMAP_NEGATIVEX
DDS_CUBEMAP_POSITIVEY = DDSCAPS2_CUBEMAP | DDSCAPS2_CUBEMAP_POSITIVEY
DDS_CUBEMAP_NEGATIVEY = DDSCAPS2_CUBEMAP | DDSCAPS2_CUBEMAP_NEGATIVEY
DDS_CUBEMAP_POSITIVEZ = DDSCAPS2_CUBEMAP | DDSCAPS2_CUBEMAP_POSITIVEZ
DDS_CUBEMAP_NEGATIVEZ = DDSCAPS2_CUBEMAP | DDSCAPS2_CUBEMAP_NEGATIVEZ
# DXT1
DXT1_FOURCC = 0x31545844
# DXT3
DXT3_FOURCC = 0x33545844
# DXT5
DXT5_FOURCC = 0x35545844
# dxgiformat.h # dxgiformat.h
class DXGI_FORMAT(IntEnum):
UNKNOWN = 0
R32G32B32A32_TYPELESS = 1
R32G32B32A32_FLOAT = 2
R32G32B32A32_UINT = 3
R32G32B32A32_SINT = 4
R32G32B32_TYPELESS = 5
R32G32B32_FLOAT = 6
R32G32B32_UINT = 7
R32G32B32_SINT = 8
R16G16B16A16_TYPELESS = 9
R16G16B16A16_FLOAT = 10
R16G16B16A16_UNORM = 11
R16G16B16A16_UINT = 12
R16G16B16A16_SNORM = 13
R16G16B16A16_SINT = 14
R32G32_TYPELESS = 15
R32G32_FLOAT = 16
R32G32_UINT = 17
R32G32_SINT = 18
R32G8X24_TYPELESS = 19
D32_FLOAT_S8X24_UINT = 20
R32_FLOAT_X8X24_TYPELESS = 21
X32_TYPELESS_G8X24_UINT = 22
R10G10B10A2_TYPELESS = 23
R10G10B10A2_UNORM = 24
R10G10B10A2_UINT = 25
R11G11B10_FLOAT = 26
R8G8B8A8_TYPELESS = 27
R8G8B8A8_UNORM = 28
R8G8B8A8_UNORM_SRGB = 29
R8G8B8A8_UINT = 30
R8G8B8A8_SNORM = 31
R8G8B8A8_SINT = 32
R16G16_TYPELESS = 33
R16G16_FLOAT = 34
R16G16_UNORM = 35
R16G16_UINT = 36
R16G16_SNORM = 37
R16G16_SINT = 38
R32_TYPELESS = 39
D32_FLOAT = 40
R32_FLOAT = 41
R32_UINT = 42
R32_SINT = 43
R24G8_TYPELESS = 44
D24_UNORM_S8_UINT = 45
R24_UNORM_X8_TYPELESS = 46
X24_TYPELESS_G8_UINT = 47
R8G8_TYPELESS = 48
R8G8_UNORM = 49
R8G8_UINT = 50
R8G8_SNORM = 51
R8G8_SINT = 52
R16_TYPELESS = 53
R16_FLOAT = 54
D16_UNORM = 55
R16_UNORM = 56
R16_UINT = 57
R16_SNORM = 58
R16_SINT = 59
R8_TYPELESS = 60
R8_UNORM = 61
R8_UINT = 62
R8_SNORM = 63
R8_SINT = 64
A8_UNORM = 65
R1_UNORM = 66
R9G9B9E5_SHAREDEXP = 67
R8G8_B8G8_UNORM = 68
G8R8_G8B8_UNORM = 69
BC1_TYPELESS = 70
BC1_UNORM = 71
BC1_UNORM_SRGB = 72
BC2_TYPELESS = 73
BC2_UNORM = 74
BC2_UNORM_SRGB = 75
BC3_TYPELESS = 76
BC3_UNORM = 77
BC3_UNORM_SRGB = 78
BC4_TYPELESS = 79
BC4_UNORM = 80
BC4_SNORM = 81
BC5_TYPELESS = 82
BC5_UNORM = 83
BC5_SNORM = 84
B5G6R5_UNORM = 85
B5G5R5A1_UNORM = 86
B8G8R8A8_UNORM = 87
B8G8R8X8_UNORM = 88
R10G10B10_XR_BIAS_A2_UNORM = 89
B8G8R8A8_TYPELESS = 90
B8G8R8A8_UNORM_SRGB = 91
B8G8R8X8_TYPELESS = 92
B8G8R8X8_UNORM_SRGB = 93
BC6H_TYPELESS = 94
BC6H_UF16 = 95
BC6H_SF16 = 96
BC7_TYPELESS = 97
BC7_UNORM = 98
BC7_UNORM_SRGB = 99
AYUV = 100
Y410 = 101
Y416 = 102
NV12 = 103
P010 = 104
P016 = 105
OPAQUE_420 = 106
YUY2 = 107
Y210 = 108
Y216 = 109
NV11 = 110
AI44 = 111
IA44 = 112
P8 = 113
A8P8 = 114
B4G4R4A4_UNORM = 115
P208 = 130
V208 = 131
V408 = 132
SAMPLER_FEEDBACK_MIN_MIP_OPAQUE = 189
SAMPLER_FEEDBACK_MIP_REGION_USED_OPAQUE = 190
DXGI_FORMAT_R8G8B8A8_TYPELESS = 27
DXGI_FORMAT_R8G8B8A8_UNORM = 28 class D3DFMT(IntEnum):
DXGI_FORMAT_R8G8B8A8_UNORM_SRGB = 29 UNKNOWN = 0
DXGI_FORMAT_BC4_TYPELESS = 79 R8G8B8 = 20
DXGI_FORMAT_BC4_UNORM = 80 A8R8G8B8 = 21
DXGI_FORMAT_BC5_TYPELESS = 82 X8R8G8B8 = 22
DXGI_FORMAT_BC5_UNORM = 83 R5G6B5 = 23
DXGI_FORMAT_BC5_SNORM = 84 X1R5G5B5 = 24
DXGI_FORMAT_BC6H_UF16 = 95 A1R5G5B5 = 25
DXGI_FORMAT_BC6H_SF16 = 96 A4R4G4B4 = 26
DXGI_FORMAT_BC7_TYPELESS = 97 R3G3B2 = 27
DXGI_FORMAT_BC7_UNORM = 98 A8 = 28
DXGI_FORMAT_BC7_UNORM_SRGB = 99 A8R3G3B2 = 29
X4R4G4B4 = 30
A2B10G10R10 = 31
A8B8G8R8 = 32
X8B8G8R8 = 33
G16R16 = 34
A2R10G10B10 = 35
A16B16G16R16 = 36
A8P8 = 40
P8 = 41
L8 = 50
A8L8 = 51
A4L4 = 52
V8U8 = 60
L6V5U5 = 61
X8L8V8U8 = 62
Q8W8V8U8 = 63
V16U16 = 64
A2W10V10U10 = 67
D16_LOCKABLE = 70
D32 = 71
D15S1 = 73
D24S8 = 75
D24X8 = 77
D24X4S4 = 79
D16 = 80
D32F_LOCKABLE = 82
D24FS8 = 83
D32_LOCKABLE = 84
S8_LOCKABLE = 85
L16 = 81
VERTEXDATA = 100
INDEX16 = 101
INDEX32 = 102
Q16W16V16U16 = 110
R16F = 111
G16R16F = 112
A16B16G16R16F = 113
R32F = 114
G32R32F = 115
A32B32G32R32F = 116
CxV8U8 = 117
A1 = 118
A2B10G10R10_XR_BIAS = 119
BINARYBUFFER = 199
UYVY = i32(b"UYVY")
R8G8_B8G8 = i32(b"RGBG")
YUY2 = i32(b"YUY2")
G8R8_G8B8 = i32(b"GRGB")
DXT1 = i32(b"DXT1")
DXT2 = i32(b"DXT2")
DXT3 = i32(b"DXT3")
DXT4 = i32(b"DXT4")
DXT5 = i32(b"DXT5")
DX10 = i32(b"DX10")
BC4S = i32(b"BC4S")
BC4U = i32(b"BC4U")
BC5S = i32(b"BC5S")
BC5U = i32(b"BC5U")
ATI1 = i32(b"ATI1")
ATI2 = i32(b"ATI2")
MULTI2_ARGB8 = i32(b"MET1")
# Backward compatibility layer
module = sys.modules[__name__]
for item in DDSD:
setattr(module, "DDSD_" + item.name, item.value)
for item in DDSCAPS:
setattr(module, "DDSCAPS_" + item.name, item.value)
for item in DDSCAPS2:
setattr(module, "DDSCAPS2_" + item.name, item.value)
for item in DDPF:
setattr(module, "DDPF_" + item.name, item.value)
DDS_FOURCC = DDPF.FOURCC
DDS_RGB = DDPF.RGB
DDS_RGBA = DDPF.RGB | DDPF.ALPHAPIXELS
DDS_LUMINANCE = DDPF.LUMINANCE
DDS_LUMINANCEA = DDPF.LUMINANCE | DDPF.ALPHAPIXELS
DDS_ALPHA = DDPF.ALPHA
DDS_PAL8 = DDPF.PALETTEINDEXED8
DDS_HEADER_FLAGS_TEXTURE = DDSD.CAPS | DDSD.HEIGHT | DDSD.WIDTH | DDSD.PIXELFORMAT
DDS_HEADER_FLAGS_MIPMAP = DDSD.MIPMAPCOUNT
DDS_HEADER_FLAGS_VOLUME = DDSD.DEPTH
DDS_HEADER_FLAGS_PITCH = DDSD.PITCH
DDS_HEADER_FLAGS_LINEARSIZE = DDSD.LINEARSIZE
DDS_HEIGHT = DDSD.HEIGHT
DDS_WIDTH = DDSD.WIDTH
DDS_SURFACE_FLAGS_TEXTURE = DDSCAPS.TEXTURE
DDS_SURFACE_FLAGS_MIPMAP = DDSCAPS.COMPLEX | DDSCAPS.MIPMAP
DDS_SURFACE_FLAGS_CUBEMAP = DDSCAPS.COMPLEX
DDS_CUBEMAP_POSITIVEX = DDSCAPS2.CUBEMAP | DDSCAPS2.CUBEMAP_POSITIVEX
DDS_CUBEMAP_NEGATIVEX = DDSCAPS2.CUBEMAP | DDSCAPS2.CUBEMAP_NEGATIVEX
DDS_CUBEMAP_POSITIVEY = DDSCAPS2.CUBEMAP | DDSCAPS2.CUBEMAP_POSITIVEY
DDS_CUBEMAP_NEGATIVEY = DDSCAPS2.CUBEMAP | DDSCAPS2.CUBEMAP_NEGATIVEY
DDS_CUBEMAP_POSITIVEZ = DDSCAPS2.CUBEMAP | DDSCAPS2.CUBEMAP_POSITIVEZ
DDS_CUBEMAP_NEGATIVEZ = DDSCAPS2.CUBEMAP | DDSCAPS2.CUBEMAP_NEGATIVEZ
DXT1_FOURCC = D3DFMT.DXT1
DXT3_FOURCC = D3DFMT.DXT3
DXT5_FOURCC = D3DFMT.DXT5
DXGI_FORMAT_R8G8B8A8_TYPELESS = DXGI_FORMAT.R8G8B8A8_TYPELESS
DXGI_FORMAT_R8G8B8A8_UNORM = DXGI_FORMAT.R8G8B8A8_UNORM
DXGI_FORMAT_R8G8B8A8_UNORM_SRGB = DXGI_FORMAT.R8G8B8A8_UNORM_SRGB
DXGI_FORMAT_BC5_TYPELESS = DXGI_FORMAT.BC5_TYPELESS
DXGI_FORMAT_BC5_UNORM = DXGI_FORMAT.BC5_UNORM
DXGI_FORMAT_BC5_SNORM = DXGI_FORMAT.BC5_SNORM
DXGI_FORMAT_BC6H_UF16 = DXGI_FORMAT.BC6H_UF16
DXGI_FORMAT_BC6H_SF16 = DXGI_FORMAT.BC6H_SF16
DXGI_FORMAT_BC7_TYPELESS = DXGI_FORMAT.BC7_TYPELESS
DXGI_FORMAT_BC7_UNORM = DXGI_FORMAT.BC7_UNORM
DXGI_FORMAT_BC7_UNORM_SRGB = DXGI_FORMAT.BC7_UNORM_SRGB
class DdsImageFile(ImageFile.ImageFile): class DdsImageFile(ImageFile.ImageFile):
@ -126,118 +336,140 @@ class DdsImageFile(ImageFile.ImageFile):
if len(header_bytes) != 120: if len(header_bytes) != 120:
msg = f"Incomplete header: {len(header_bytes)} bytes" msg = f"Incomplete header: {len(header_bytes)} bytes"
raise OSError(msg) raise OSError(msg)
header = BytesIO(header_bytes) header = io.BytesIO(header_bytes)
flags, height, width = struct.unpack("<3I", header.read(12)) flags, height, width = struct.unpack("<3I", header.read(12))
self._size = (width, height) self._size = (width, height)
self._mode = "RGBA"
pitch, depth, mipmaps = struct.unpack("<3I", header.read(12)) pitch, depth, mipmaps = struct.unpack("<3I", header.read(12))
struct.unpack("<11I", header.read(44)) # reserved struct.unpack("<11I", header.read(44)) # reserved
# pixel format # pixel format
pfsize, pfflags = struct.unpack("<2I", header.read(8)) pfsize, pfflags, fourcc, bitcount = struct.unpack("<4I", header.read(16))
fourcc = header.read(4) n = 0
(bitcount,) = struct.unpack("<I", header.read(4)) rawmode = None
masks = struct.unpack("<4I", header.read(16)) if pfflags & DDPF.RGB:
if pfflags & DDPF_LUMINANCE: # Texture contains uncompressed RGB data
# Texture contains uncompressed L or LA data masks = struct.unpack("<4I", header.read(16))
if pfflags & DDPF_ALPHAPIXELS: masks = {mask: ["R", "G", "B", "A"][i] for i, mask in enumerate(masks)}
if bitcount == 24:
self._mode = "RGB"
rawmode = masks[0x000000FF] + masks[0x0000FF00] + masks[0x00FF0000]
elif bitcount == 32 and pfflags & DDPF.ALPHAPIXELS:
self._mode = "RGBA"
rawmode = (
masks[0x000000FF]
+ masks[0x0000FF00]
+ masks[0x00FF0000]
+ masks[0xFF000000]
)
else:
msg = f"Unsupported bitcount {bitcount} for {pfflags}"
raise OSError(msg)
elif pfflags & DDPF.LUMINANCE:
if bitcount == 8:
self._mode = "L"
elif bitcount == 16 and pfflags & DDPF.ALPHAPIXELS:
self._mode = "LA" self._mode = "LA"
else: else:
self._mode = "L" msg = f"Unsupported bitcount {bitcount} for {pfflags}"
raise OSError(msg)
self.tile = [("raw", (0, 0) + self.size, 0, (self.mode, 0, 1))] elif pfflags & DDPF.PALETTEINDEXED8:
elif pfflags & DDPF_RGB:
# Texture contains uncompressed RGB data
masks = {mask: ["R", "G", "B", "A"][i] for i, mask in enumerate(masks)}
rawmode = ""
if pfflags & DDPF_ALPHAPIXELS:
rawmode += masks[0xFF000000]
else:
self._mode = "RGB"
rawmode += masks[0xFF0000] + masks[0xFF00] + masks[0xFF]
self.tile = [("raw", (0, 0) + self.size, 0, (rawmode[::-1], 0, 1))]
elif pfflags & DDPF_PALETTEINDEXED8:
self._mode = "P" self._mode = "P"
self.palette = ImagePalette.raw("RGBA", self.fp.read(1024)) self.palette = ImagePalette.raw("RGBA", self.fp.read(1024))
self.tile = [("raw", (0, 0) + self.size, 0, "L")] elif pfflags & DDPF.FOURCC:
else: offset = header_size + 4
data_start = header_size + 4 if fourcc == D3DFMT.DXT1:
n = 0 self._mode = "RGBA"
if fourcc == b"DXT1":
self.pixel_format = "DXT1" self.pixel_format = "DXT1"
n = 1 n = 1
elif fourcc == b"DXT3": elif fourcc == D3DFMT.DXT3:
self._mode = "RGBA"
self.pixel_format = "DXT3" self.pixel_format = "DXT3"
n = 2 n = 2
elif fourcc == b"DXT5": elif fourcc == D3DFMT.DXT5:
self._mode = "RGBA"
self.pixel_format = "DXT5" self.pixel_format = "DXT5"
n = 3 n = 3
elif fourcc == b"ATI1": elif fourcc in (D3DFMT.BC4U, D3DFMT.ATI1):
self._mode = "L"
self.pixel_format = "BC4" self.pixel_format = "BC4"
n = 4 n = 4
self._mode = "L" elif fourcc == D3DFMT.BC5S:
elif fourcc in (b"ATI2", b"BC5U"):
self.pixel_format = "BC5"
n = 5
self._mode = "RGB" self._mode = "RGB"
elif fourcc == b"BC5S":
self.pixel_format = "BC5S" self.pixel_format = "BC5S"
n = 5 n = 5
elif fourcc in (D3DFMT.BC5U, D3DFMT.ATI2):
self._mode = "RGB" self._mode = "RGB"
elif fourcc == b"DX10": self.pixel_format = "BC5"
data_start += 20 n = 5
elif fourcc == D3DFMT.DX10:
offset += 20
# ignoring flags which pertain to volume textures and cubemaps # ignoring flags which pertain to volume textures and cubemaps
(dxgi_format,) = struct.unpack("<I", self.fp.read(4)) (dxgi_format,) = struct.unpack("<I", self.fp.read(4))
self.fp.read(16) self.fp.read(16)
if dxgi_format in (DXGI_FORMAT_BC4_TYPELESS, DXGI_FORMAT_BC4_UNORM): if dxgi_format in (
DXGI_FORMAT.BC1_UNORM,
DXGI_FORMAT.BC1_TYPELESS,
):
self._mode = "RGBA"
self.pixel_format = "BC1"
n = 1
elif dxgi_format in (DXGI_FORMAT.BC4_TYPELESS, DXGI_FORMAT.BC4_UNORM):
self._mode = "L"
self.pixel_format = "BC4" self.pixel_format = "BC4"
n = 4 n = 4
self._mode = "L" elif dxgi_format in (DXGI_FORMAT.BC5_TYPELESS, DXGI_FORMAT.BC5_UNORM):
elif dxgi_format in (DXGI_FORMAT_BC5_TYPELESS, DXGI_FORMAT_BC5_UNORM): self._mode = "RGB"
self.pixel_format = "BC5" self.pixel_format = "BC5"
n = 5 n = 5
elif dxgi_format == DXGI_FORMAT.BC5_SNORM:
self._mode = "RGB" self._mode = "RGB"
elif dxgi_format == DXGI_FORMAT_BC5_SNORM:
self.pixel_format = "BC5S" self.pixel_format = "BC5S"
n = 5 n = 5
elif dxgi_format == DXGI_FORMAT.BC6H_UF16:
self._mode = "RGB" self._mode = "RGB"
elif dxgi_format == DXGI_FORMAT_BC6H_UF16:
self.pixel_format = "BC6H" self.pixel_format = "BC6H"
n = 6 n = 6
elif dxgi_format == DXGI_FORMAT.BC6H_SF16:
self._mode = "RGB" self._mode = "RGB"
elif dxgi_format == DXGI_FORMAT_BC6H_SF16:
self.pixel_format = "BC6HS" self.pixel_format = "BC6HS"
n = 6 n = 6
self._mode = "RGB"
elif dxgi_format in (DXGI_FORMAT_BC7_TYPELESS, DXGI_FORMAT_BC7_UNORM):
self.pixel_format = "BC7"
n = 7
elif dxgi_format == DXGI_FORMAT_BC7_UNORM_SRGB:
self.pixel_format = "BC7"
self.info["gamma"] = 1 / 2.2
n = 7
elif dxgi_format in ( elif dxgi_format in (
DXGI_FORMAT_R8G8B8A8_TYPELESS, DXGI_FORMAT.BC7_TYPELESS,
DXGI_FORMAT_R8G8B8A8_UNORM, DXGI_FORMAT.BC7_UNORM,
DXGI_FORMAT_R8G8B8A8_UNORM_SRGB, DXGI_FORMAT.BC7_UNORM_SRGB,
): ):
self.tile = [("raw", (0, 0) + self.size, 0, ("RGBA", 0, 1))] self._mode = "RGBA"
if dxgi_format == DXGI_FORMAT_R8G8B8A8_UNORM_SRGB: self.pixel_format = "BC7"
n = 7
if dxgi_format == DXGI_FORMAT.BC7_UNORM_SRGB:
self.info["gamma"] = 1 / 2.2
elif dxgi_format in (
DXGI_FORMAT.R8G8B8A8_TYPELESS,
DXGI_FORMAT.R8G8B8A8_UNORM,
DXGI_FORMAT.R8G8B8A8_UNORM_SRGB,
):
self._mode = "RGBA"
if dxgi_format == DXGI_FORMAT.R8G8B8A8_UNORM_SRGB:
self.info["gamma"] = 1 / 2.2 self.info["gamma"] = 1 / 2.2
return
else: else:
msg = f"Unimplemented DXGI format {dxgi_format}" msg = f"Unimplemented DXGI format {dxgi_format}"
raise NotImplementedError(msg) raise NotImplementedError(msg)
else: else:
msg = f"Unimplemented pixel format {repr(fourcc)}" msg = f"Unimplemented pixel format {repr(fourcc)}"
raise NotImplementedError(msg) raise NotImplementedError(msg)
else:
msg = f"Unknown pixel format flags {pfflags}"
raise NotImplementedError(msg)
extents = (0, 0) + self.size
if n:
self.tile = [ self.tile = [
("bcn", (0, 0) + self.size, data_start, (n, self.pixel_format)) ImageFile._Tile("bcn", extents, offset, (n, self.pixel_format))
] ]
else:
self.tile = [ImageFile._Tile("raw", extents, 0, rawmode or self.mode)]
def load_seek(self, pos): def load_seek(self, pos):
pass pass
@ -248,48 +480,51 @@ def _save(im, fp, filename):
msg = f"cannot write mode {im.mode} as DDS" msg = f"cannot write mode {im.mode} as DDS"
raise OSError(msg) raise OSError(msg)
rawmode = im.mode alpha = im.mode[-1] == "A"
masks = [0xFF0000, 0xFF00, 0xFF] if im.mode[0] == "L":
if im.mode in ("L", "LA"): pixel_flags = DDPF.LUMINANCE
pixel_flags = DDPF_LUMINANCE rawmode = im.mode
if alpha:
rgba_mask = [0x000000FF, 0x000000FF, 0x000000FF]
else:
rgba_mask = [0xFF000000, 0xFF000000, 0xFF000000]
else: else:
pixel_flags = DDPF_RGB pixel_flags = DDPF.RGB
rawmode = rawmode[::-1] rawmode = im.mode[::-1]
if im.mode in ("LA", "RGBA"): rgba_mask = [0x00FF0000, 0x0000FF00, 0x000000FF]
pixel_flags |= DDPF_ALPHAPIXELS
masks.append(0xFF000000)
bitcount = len(masks) * 8 if alpha:
while len(masks) < 4: r, g, b, a = im.split()
masks.append(0) im = Image.merge("RGBA", (a, r, g, b))
if alpha:
pixel_flags |= DDPF.ALPHAPIXELS
rgba_mask.append(0xFF000000 if alpha else 0)
flags = DDSD.CAPS | DDSD.HEIGHT | DDSD.WIDTH | DDSD.PITCH | DDSD.PIXELFORMAT
bitcount = len(im.getbands()) * 8
pitch = (im.width * bitcount + 7) // 8
fp.write( fp.write(
o32(DDS_MAGIC) o32(DDS_MAGIC)
+ o32(124) # header size + struct.pack(
+ o32( "<7I",
DDSD_CAPS | DDSD_HEIGHT | DDSD_WIDTH | DDSD_PITCH | DDSD_PIXELFORMAT 124, # header size
) # flags flags, # flags
+ o32(im.height) im.height,
+ o32(im.width) im.width,
+ o32((im.width * bitcount + 7) // 8) # pitch pitch,
+ o32(0) # depth 0, # depth
+ o32(0) # mipmaps 0, # mipmaps
+ o32(0) * 11 # reserved )
+ o32(32) # pfsize + struct.pack("11I", *((0,) * 11)) # reserved
+ o32(pixel_flags) # pfflags # pfsize, pfflags, fourcc, bitcount
+ o32(0) # fourcc + struct.pack("<4I", 32, pixel_flags, 0, bitcount)
+ o32(bitcount) # bitcount + struct.pack("<4I", *rgba_mask) # dwRGBABitMask
+ b"".join(o32(mask) for mask in masks) # rgbabitmask + struct.pack("<5I", DDSCAPS.TEXTURE, 0, 0, 0, 0)
+ o32(DDSCAPS_TEXTURE) # dwCaps )
+ o32(0) # dwCaps2 ImageFile._save(
+ o32(0) # dwCaps3 im, fp, [ImageFile._Tile("raw", (0, 0) + im.size, 0, (rawmode, 0, 1))]
+ o32(0) # dwCaps4
+ o32(0) # dwReserved2
) )
if im.mode == "RGBA":
r, g, b, a = im.split()
im = Image.merge("RGBA", (a, r, g, b))
ImageFile._save(im, fp, [("raw", (0, 0) + im.size, 0, (rawmode, 0, 1))])
def _accept(prefix): def _accept(prefix):

View File

@ -26,11 +26,13 @@
# #
# See the README file for information on usage and redistribution. # See the README file for information on usage and redistribution.
# #
from __future__ import annotations
import io import io
import itertools import itertools
import struct import struct
import sys import sys
from typing import NamedTuple
from . import Image from . import Image
from ._util import is_path from ._util import is_path
@ -77,6 +79,13 @@ def _tilesort(t):
return t[2] return t[2]
class _Tile(NamedTuple):
encoder_name: str
extents: tuple[int, int, int, int]
offset: int
args: tuple | str | None
# #
# -------------------------------------------------------------------- # --------------------------------------------------------------------
# ImageFile base class # ImageFile base class
@ -520,13 +529,13 @@ def _save(im, fp, tile, bufsize=0):
fp.flush() fp.flush()
def _encode_tile(im, fp, tile, bufsize, fh, exc=None): def _encode_tile(im, fp, tile: list[_Tile], bufsize, fh, exc=None):
for e, b, o, a in tile: for encoder_name, extents, offset, args in tile:
if o > 0: if offset > 0:
fp.seek(o) fp.seek(offset)
encoder = Image._getencoder(im.mode, e, a, im.encoderconfig) encoder = Image._getencoder(im.mode, encoder_name, args, im.encoderconfig)
try: try:
encoder.setimage(im.im, b) encoder.setimage(im.im, extents)
if encoder.pushes_fd: if encoder.pushes_fd:
encoder.setfd(fp) encoder.setfd(fp)
errcode = encoder.encode_to_pyfd()[1] errcode = encoder.encode_to_pyfd()[1]