space928 / Blender-O3D-IO-Public

A plugin supporting blender 2.79.x-3.x.x for importing and exporting OMSI .sco, .cfg, and .o3d files
GNU General Public License v3.0
39 stars 5 forks source link

When working with non-squared non-power of two textures check this divisions #31

Open github-actions[bot] opened 1 year ago

github-actions[bot] commented 1 year ago

https://github.com/space928/Blender-O3D-IO-Public/blob/dd30231969ecf0acde33fa9f8598e22e364e5daa/o3d_io/dds_loader/dds_loader.py#L355


"""
Copyright (c) 2015 Edoardo "sparkon" Dominici

Permission is hereby granted, free of charge, to any person
obtaining a copy of this software and associated documentation
files (the "Software"), to deal in the Software without
restriction, including without limitation the rights to use,
copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following
conditions:

The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE.
"""

import ctypes
import os

from . import dxgi_values

def log(*args):
    print("[DDS_Loader] ", *args)

# Win32 following : https://msdn.microsoft.com/en-us/library/windows/desktop/aa383751(v=vs.85).aspx
class Win32Types:
    DWORD = ctypes.c_ulong
    UINT = ctypes.c_uint

# DDS types
DDSEnumType = ctypes.c_ulong
DDSMagicNumber = Win32Types.DWORD
DDSFormatCC = Win32Types.DWORD

# Contains all default values used for validation
class DDSValues:
    MIN_FILE_SIZE = 128
    MAGIC_NUMBER = 0x20534444

    HEADER_SIZE = 124
    PIXELFORMAT_SIZE = 32

# Values used for flagging
class DDSEnums:
    # Complexity of stores resources DDS_HEADER::dwCaps
    DDSCAPS_COMPLEX = 0x8
    DDSCAPS_MIPMAP = 0x400000
    DDSCAPS_TEXTURE = 0x1000

    # Additional detail on surface DDS_HEADER::dwCaps2
    DDSCAPS2_CUBEMAP = 0x200
    DDSCAPS2_VOLUME = 0x200000

    # Flags to indicate what is stored inside the texture, they
    # are ignored for the most part since pitch and size are
    # calculated manually, they are used for structure validation
    DDSD_CAPS = 0x1
    DDSD_HEIGHT = 0x2
    DDSD_WIDTH = 0x4
    DDSD_PITCH = 0x8
    DDSD_PIXELFORMAT = 0x1000
    DDSD_MIPMAPCOUNT = 0x20000
    DDSD_LINEARSIZE = 0x80000
    DDSD_DEPTH = 0x800000

    # Only flag we are interested in DDS_PIXELFORMAT::dwFlags, is needed
    # to make sure the extended header is included
    DDPF_FOURCC = 0x4

    # Only value we are interested in DDS_PIXELFORMAT::dwFourCC, is needed
    # to make sure the extended header is included
    DX10_CC = int.from_bytes(bytearray(b"DX10"), byteorder="little", signed="false")

    # Misc flag in the DDS_HEADER_DXT10, flags the resource as cubemap
    DDS_RESOURCE_MISC_TEXTURECUBE = 0x4

# 1-to-1 mapping of DDS_PIXELFORMAT https://msdn.microsoft.com/en-us/library/windows/desktop/bb943984(v=vs.85).aspx
class DDSPixelFormat(ctypes.Structure):
    _fields_ = [
        ("dwSize", Win32Types.DWORD),
        ("dwFlags", Win32Types.DWORD),
        ("dwFourCC", DDSFormatCC),
        ("dwRGBBitCount", Win32Types.DWORD),
        ("dwRBitMask", Win32Types.DWORD),
        ("dwGBitMask", Win32Types.DWORD),
        ("dwBBitMask", Win32Types.DWORD),
        ("dwABitMask", Win32Types.DWORD)
    ]

# 1-to-1 mapping of DDS_HEADER https://msdn.microsoft.com/en-us/library/windows/desktop/bb943982(v=vs.85).aspx
class DDSHeader(ctypes.Structure):
    _fields_ = [
        ("dwSize", Win32Types.DWORD),
        ("dwFlags", Win32Types.DWORD),
        ("dwHeight", Win32Types.DWORD),
        ("dwWidth", Win32Types.DWORD),
        ("dwPitchOrLinearSize", Win32Types.DWORD),
        ("dwDepth", Win32Types.DWORD),
        ("dwMipMapCount", Win32Types.DWORD),
        ("dwReserved", Win32Types.DWORD * 11),
        ("ddspf", DDSPixelFormat),
        ("dwCaps", Win32Types.DWORD),
        ("dwCaps2", Win32Types.DWORD),
        ("dwCaps3", Win32Types.DWORD),
        ("dwCaps4", Win32Types.DWORD),
        ("dwReserved2", Win32Types.DWORD)
    ]

# 1-to-1 mapping of DDS_HEADER_DXT10 https://msdn.microsoft.com/en-us/library/windows/desktop/bb943983(v=vs.85).aspx
class DDSExtHeader(ctypes.Structure):
    _fields_ = [
        ("dxgiFormat", DDSEnumType),
        ("resourceDimension", DDSEnumType),
        ("miscFlag", Win32Types.UINT),
        ("arraySize", Win32Types.UINT),
        ("miscFlags2", Win32Types.UINT)
    ]

# FormatNotValid is raised when the file provided is not valid, more information in the message
class FormatNotValid(Exception):
    pass

# FormatNotSupported is raised when the file has some feature that is not supported by this implementation
class FormatNotSupported(Exception):
    pass

# Represents a single surface of any kind, depending on its position and the DDSTexture info the
# miplevel or array index can be deduced
class DDSSurface:
    def __init__(self, width, height, pitch, size, data):
        self.width = width
        self.height = height
        self.pitch = pitch
        self.size = size
        self.data = data

    def __str__(self):
        return "Width: {0} Height: {1} Pitch: {2} Size: {3}".format(self.width, self.height, self.pitch, self.size)

# Represents a loaded DDSFile, the name might be misleading since multiple textures or texturecubes can be
# contained inside here
class DDSTexture:
    # Type of the DDSTexture, volume texture is not supported, and 1D textures should be mapped
    # to 2D textures with height == 1, but this hasn't been tested yet ( TODO )
    class Type:
        Texture2D = 0
        TextureCube = 1
        Texture2DArray = 2
        TextureCubeArray = 3

        names = [
            "Texture2D",
            "TextureCube",
            "Texture2DArray",
            "TextureCubeArray"
        ]

    def __init__(self):
        self.real_size = 0
        self.calculated_size = 0

        # File format
        self.name = None
        self.magic_number = DDSMagicNumber()
        self.header = DDSHeader()
        self.ext_header = DDSExtHeader()

        # Type information
        self.type = None
        self.array_size = None

        # List of surfaces that contain info on how to read raw data
        self.surfaces = []

        # Raw data
        self.data = None

        # Format information
        self.dxgi_format = None
        self.is_compressed = None
        self.bpp_or_block_size = None
        self.mipmap_count = None
        self.format_code = self.header.ddspf.dwFourCC

    def __str__(self):
        return "{0} | Type={1} ArraySize={2} MipMapCount={3} Format={4} BPP={5}".format(self.name,
                                                                                        self.Type.names[self.type],
                                                                                        self.array_size,
                                                                                        self.mipmap_count,
                                                                                        self.dxgi_format,
                                                                                        self.bpp_or_block_size)

    # Used internally to validate information contained inside the headers to make sure reading
    # or writing was successful. This test could be removed, but files that do not pass this *NOT COMPLETE*
    # validation phase are not following the specification
    def _validate_structures(self):
        if self.header.dwSize != DDSValues.HEADER_SIZE:
            raise FormatNotValid("File not formatted correctly, header size not matching")

        if self.header.ddspf.dwSize != DDSValues.PIXELFORMAT_SIZE:
            raise FormatNotValid("File not formatted correctly, extended header size not matching")

        if not (self.header.dwFlags & DDSEnums.DDSD_CAPS) or \
                not (self.header.dwFlags & DDSEnums.DDSD_WIDTH) or \
                not (self.header.dwFlags & DDSEnums.DDSD_HEIGHT) or \
                not (self.header.dwFlags & DDSEnums.DDSD_PIXELFORMAT) or \
                not (self.header.dwFlags & DDSEnums.DDSD_CAPS) or \
                not (self.header.dwFlags & DDSEnums.DDSD_CAPS):
            raise FormatNotValid("File not formatted correctly, required flags not present")

        if not (self.header.dwCaps & DDSEnums.DDSCAPS_TEXTURE):
            raise FormatNotValid("File not formatted correctly, required flags not present")

    # Fills in information regarding the pixel format
    def _compute_format(self):
        if self.format_code != DDSEnums.DX10_CC:
            # <DX10 DDS file, try to infer the format
            pf = self.header.ddspf
            try:
                if pf.dwFlags & DDSEnums.DDPF_FOURCC:
                    self.dxgi_format = dxgi_values.four_cc_to_dxgi[pf.dwFourCC]
                else:
                    self.dxgi_format = dxgi_values.format_to_dxgi[(pf.dwFlags, pf.dwRGBBitCount, pf.dwRBitMask,
                                                                   pf.dwGBitMask, pf.dwBBitMask, pf.dwABitMask)]
            except KeyError:
                # For very wierd dds formats which we don't support
                self.dxgi_format = -1
        else:
            self.dxgi_format = self.ext_header.dxgiFormat

        # If the texture is compressed
        self.bpp_or_block_size = dxgi_values.dxgi_pixel_or_block_size[self.dxgi_format]

        # Checking if the texture is compressed or not ( we need it to calculate pitch )
        self.is_compressed = self.dxgi_format in dxgi_values.dxgi_compressed_formats

        # Checking if there are mipmaps
        self.mipmap_count = max(self.header.dwMipMapCount, 1)

    # Computes the type of the DDSTexture
    def _compute_type(self):
        if not (self.header.dwCaps & DDSEnums.DDSCAPS_TEXTURE):
            raise FormatNotValid("Invalid format file not tagged as texture")

        if self.header.dwCaps2 & DDSEnums.DDSCAPS2_CUBEMAP:
            if self.ext_header.arraySize > 1:
                self.type = DDSTexture.Type.TextureCubeArray
                self.array_size = self.ext_header.arraySize
            else:
                self.type = DDSTexture.Type.TextureCube
                self.array_size = 1
        # We either have a single texture or a texture array ( 2D )
        else:
            if self.format_code == DDSEnums.DX10_CC and \
                    self.ext_header.arraySize > 1:
                self.type = DDSTexture.Type.Texture2DArray
                self.array_size = self.ext_header.arraySize
            else:
                self.type = DDSTexture.Type.Texture2D
                self.array_size = 1

    # Loads the texture from the filename, obtaining data - array of c_byte containing untouched texture data
    # formatted matching the surfaces data surfaces - metadata for the raw texture data that describes how it can be
    # read, its valued are ready for DirectX11 creation ( Pitch, width, height, size ) format - DXGI compatible format
    # the integer self.format can be safely static_cast<DXGI_FORMAT> to obtain the C++ enumerator counterpart
    def load(self, filename):
        self.name = filename

        # Making sure the file is big enough to contain the magic number + default header
        self.real_size = os.path.getsize(filename)
        if self.real_size < DDSValues.MIN_FILE_SIZE:
            raise FormatNotValid("File is too small")

        with open(filename, "rb") as file_stream:
            # Reading magic number and making sure is valid
            bytes_read = file_stream.readinto(self.magic_number)
            if bytes_read < ctypes.sizeof(self.magic_number):
                raise FormatNotValid("Failed to read magic number")

            if self.magic_number.value != DDSValues.MAGIC_NUMBER:
                raise FormatNotValid("Invalid magic number")

            # Reading header
            bytes_read = file_stream.readinto(self.header)
            if bytes_read < ctypes.sizeof(self.header):
                raise FormatNotValid("Failed to read header")

            # Reading extended header
            if self.header.ddspf.dwFlags & DDSEnums.DDPF_FOURCC and self.header.ddspf.dwFourCC == DDSEnums.DX10_CC:
                bytes_read = file_stream.readinto(self.ext_header)
                if bytes_read < ctypes.sizeof(self.ext_header):
                    raise FormatNotValid("Failed to read extended header")
            # else:
            #     raise FormatNotSupported(
            #         "Sorry we only support DDS texture with DX10 extended header, if you are using texconv tools use "
            #         "the -dx10 flag")

            # Validating the DDS_HEADER and DX10_DDS_HEADER to make sure they comply to specification
            # or supported features
            self._validate_structures()

            # Calculates the compression / format and bpp
            self._compute_format()
            # Computes the type of this DDSTexture instance
            self._compute_type()

            total_data_size = 0
            total_bytes_read = 0

            # Looping through all the textures and saving surface data
            elements = self.array_size
            if self.type == self.Type.TextureCube or self.type == self.Type.TextureCubeArray:
                elements *= 6

            for i in range(elements):
                next_width = self.header.dwWidth
                next_height = self.header.dwHeight
                if self.is_compressed:
                    next_size = self.header.dwPitchOrLinearSize
                else:
                    next_size = next_height * next_width * self.header.ddspf.dwRGBBitCount // 8

                for mipmap in range(self.mipmap_count):
                    total_data_size += next_size
                    if self.is_compressed:
                        pitch = max(1, int(((next_width + 3) / 4))) * self.bpp_or_block_size
                    else:
                        pitch = (next_width * self.bpp_or_block_size + 7) / 8

                    # Read surface data
                    data = (ctypes.c_byte * next_size)()
                    total_bytes_read += file_stream.readinto(data)

                    self.surfaces.append(DDSSurface(next_width, next_height, pitch, next_size, data))
                    # TODO When working with non-squared non-power of two textures check this divisions
                    next_width = int(next_width / 2)
                    next_height = int(next_height / 2)
                    next_size = max(8, int(next_size / 4))

            if total_bytes_read < total_data_size:
                log("WARNING: Invalid DDS file loaded! Not enough data in file for specified format! Read {0} bytes, "
                    "expected {1}. Path: {2} format: {3}".format(total_bytes_read, total_data_size, filename,
                                                                 self.dxgi_format))
                # raise FormatNotValid("Metadata doesn't match actual data")