import subprocess import os import typing import struct import re import io import enum if os.name == 'nt': convertcmd = "convert.exe" compresscmd = "nvcompress.exe" decompresscmd = "nvdecompress.exe" infocmd = "nvddsinfo.exe" else: convertcmd = "convert" compresscmd = "nvcompress" decompresscmd = "nvdecompress" infocmd = "nvddsinfo" def alpha(file): result = subprocess.run([convertcmd, file, "-resize", "1x1", "-format", "%[fx:int(255*a+.5)]", "info:-"], capture_output=True) result.check_returncode() return int(result.stdout) def flip(file, output): result = subprocess.run( [convertcmd, '-flip', file, output], capture_output=True) result.check_returncode() def nvcompress(format, file, output, mips=True): args = [format] if not mips: args.append('-nomips') result = subprocess.run([compresscmd] + args + [file, output], capture_output=True) result.check_returncode() def nvdecompress(file, output): result = subprocess.run([decompresscmd, file, output], capture_output=True) result.check_returncode() def nvinfo(file): result = subprocess.run([infocmd, file], capture_output=True) result.check_returncode() info = { "format": re.search(r"FourCC: '(.{4})'", str(result.stdout)).group(1) } return info class DDSFlags(enum.IntFlag): CAPS = 0x1 HEIGHT = 0x2 WIDTH = 0x4 PITCH = 0x8 PIXEL_FORMAT = 0x1000 MIPMAPCOUNT = 0x20000 LINEAR_SIZE = 0x80000 DEPTH = 0x800000 class DDSPixelFlags(enum.IntFlag): ALPHAPIXELS = 0x1 ALPHA = 0x2 FOURCC = 0x4 RGB = 0x40 YUV = 0x200 LUMINANCE = 0x20000 class DDSHeader: def __init__(self, f): self.read(f) def read(self, f): if f.read(4).decode() != "DDS ": raise ValueError( "File is not a valid DDS f: Incorrect magic bytes") dwords = struct.unpack('20I4s10I', f.read(124)) # check constants if dwords[0] != 124: raise ValueError( "File is not a valid DDS file: Incorrect header size") if dwords[18] != 32: raise ValueError( "File is not a valid DDS file: Incorrect pixelformat size") # read basic info self.flags = DDSFlags(dwords[1]) self.height, self.width = dwords[2:4] if DDSFlags.PITCH in self.flags: self.pitch = dwords[4] else: self.linear_size = dwords[4] if DDSFlags.DEPTH in self.flags: self.depth = dwords[5] if DDSFlags.MIPMAPCOUNT in self.flags: self.mipmapcount = dwords[6] # read pixelformat self.pixel_flags = DDSPixelFlags(dwords[19]) if DDSPixelFlags.ALPHAPIXELS in self.pixel_flags: # texture has alpha data self.a_bitmask = dwords[25] if DDSPixelFlags.FOURCC in self.pixel_flags: # texture is compressed, four_cc contains the format self.format = dwords[20].decode() else: self.format = 'uncompressed' if (DDSPixelFlags.RGB | DDSPixelFlags.YUV) & self.pixel_flags: # uncompressed RGB texture self.rgb_bit_count, self.r_bitmask, self.g_bitmask, self.b_bitmask = dwords[21:25] elif DDSPixelFlags.ALPHA in self.pixel_flags: # uncompressed alpha-only texture self.rgb_bit_count = dwords[21] self.a_bitmask = dwords[25] elif DDSPixelFlags.LUMINANCE in self.pixel_flags: # uncompressed luminance-only texture self.rgb_bit_count = dwords[21] self.r_bitmask = dwords[22] # read caps self.dw_caps, self.dw_caps2, self.dw_caps3, self.dw_caps4 = dwords[26:30] # read dwCaps flags if self.format == 'DX10': dwords = f.read(20) #TODO: actually read the DX10 data