# dds_writer.py # # Minimal DDS writer that mirrors the C/C++ save_dds() implementation you provided. # It writes a DX9-style DDS header, and optionally a DX10 extension header, # followed by the raw compressed blocks. # # No mipmaps, no cubes, no 3D volumes – exactly like the original C code. import struct import sys from typing import Union # --------------------------------------------------------------------------- # FourCC helper (same as PIXEL_FMT_FOURCC macro) # --------------------------------------------------------------------------- def make_fourcc(a: str, b: str, c: str, d: str) -> int: return (ord(a) | (ord(b) << 8) | (ord(c) << 16) | (ord(d) << 24)) # --------------------------------------------------------------------------- # DDS-related constants (only the ones we actually use) # --------------------------------------------------------------------------- # DDSD flags DDSD_CAPS = 0x00000001 DDSD_HEIGHT = 0x00000002 DDSD_WIDTH = 0x00000004 DDSD_PIXELFORMAT= 0x00001000 DDSD_LINEARSIZE = 0x00080000 # DDPF flags DDPF_FOURCC = 0x00000004 # DDSCAPS flags DDSCAPS_TEXTURE = 0x00001000 # DXGI_FORMAT subset (values must match the C enum) class DXGI_FORMAT: UNKNOWN = 0 BC1_UNORM = 71 BC3_UNORM = 77 BC4_UNORM = 80 BC5_UNORM = 83 # You can add more as needed; for DX10 header we just write the integer value. # DX10 resource dimension class D3D10_RESOURCE_DIMENSION: UNKNOWN = 0 BUFFER = 1 TEXTURE1D = 2 TEXTURE2D = 3 TEXTURE3D = 4 # --------------------------------------------------------------------------- # DDS writer class # --------------------------------------------------------------------------- class DDSWriter: """ Python port of the C save_dds() function. Usage: writer = DDSWriter() ok = writer.save_dds( filename="out.dds", width=width, height=height, blocks=bc_data, # bytes or bytearray pixel_format_bpp=4, # e.g. 4 for BC1, 8 for BC3/4/5/etc. dxgi_format=DXGI_FORMAT.BC1_UNORM, srgb=False, force_dx10_header=False, ) """ DDS_MAGIC = b"DDS " # same as fwrite("DDS ", 4, 1, pFile); def save_dds( self, filename: str, width: int, height: int, blocks: Union[bytes, bytearray, memoryview], pixel_format_bpp: int, dxgi_format: int, srgb: bool = False, force_dx10_header: bool = False, ) -> bool: """ Port of: bool save_dds(const char* pFilename, uint32_t width, uint32_t height, const void* pBlocks, uint32_t pixel_format_bpp, DXGI_FORMAT dxgi_format, bool srgb, bool force_dx10_header); The 'blocks' buffer is written as-is (up to computed linear size). """ # srgb is intentionally unused in the original C code (commented logic). _ = srgb # Open file like the C code try: f = open(filename, "wb") except OSError: print(f"Failed creating file {filename}!", file=sys.stderr) return False try: # Write the "DDS " magic f.write(self.DDS_MAGIC) # ----------------------------------------------------------------- # Build DDSURFACEDESC2 equivalent # ----------------------------------------------------------------- # We'll pack DDSURFACEDESC2 as 31 uint32's (124 bytes) in little-endian: # struct DDSURFACEDESC2 { # uint32 dwSize; # uint32 dwFlags; # uint32 dwHeight; # uint32 dwWidth; # uint32 lPitch_or_dwLinearSize; # uint32 dwBackBufferCount; # uint32 dwMipMapCount; # uint32 dwAlphaBitDepth; # uint32 dwUnused0; # uint32 lpSurface; # DDCOLORKEY unused0; (2 * uint32) # DDCOLORKEY unused1; (2 * uint32) # DDCOLORKEY unused2; (2 * uint32) # DDCOLORKEY unused3; (2 * uint32) # DDPIXELFORMAT ddpfPixelFormat; (8 * uint32) # DDSCAPS2 ddsCaps; (4 * uint32) # uint32 dwUnused1; # }; dwSize = 124 # sizeof(DDSURFACEDESC2) dwFlags = ( DDSD_WIDTH | DDSD_HEIGHT | DDSD_PIXELFORMAT | DDSD_CAPS ) dwWidth = int(width) dwHeight = int(height) # lPitch (actually LinearSize for compressed formats), same as: # (((dwWidth + 3) & ~3) * ((dwHeight + 3) & ~3) * pixel_format_bpp) >> 3; lPitch = ( ((dwWidth + 3) & ~3) * ((dwHeight + 3) & ~3) * int(pixel_format_bpp) ) >> 3 dwFlags |= DDSD_LINEARSIZE dwBackBufferCount = 0 dwMipMapCount = 0 dwAlphaBitDepth = 0 dwUnused0 = 0 lpSurface = 0 # DDCOLORKEY unused0..3, all zero ddcolorkey_zero = [0, 0] * 4 # 4 DDCOLORKEY structs # DDPIXELFORMAT # struct DDPIXELFORMAT { # uint32 dwSize; # uint32 dwFlags; # uint32 dwFourCC; # uint32 dwRGBBitCount; # uint32 dwRBitMask; # uint32 dwGBitMask; # uint32 dwBBitMask; # uint32 dwRGBAlphaBitMask; # }; ddpf_dwSize = 32 ddpf_dwFlags = DDPF_FOURCC ddpf_dwFourCC = 0 ddpf_dwRGBBitCount = 0 ddpf_dwRBitMask = 0 ddpf_dwGBitMask = 0 ddpf_dwBBitMask = 0 ddpf_dwRGBAlphaBitMask = 0 # DDSCAPS2 # struct DDSCAPS2 { # uint32 dwCaps; # uint32 dwCaps2; # uint32 dwCaps3; # uint32 dwCaps4; # }; ddsCaps_dwCaps = DDSCAPS_TEXTURE ddsCaps_dwCaps2 = 0 ddsCaps_dwCaps3 = 0 ddsCaps_dwCaps4 = 0 dwUnused1 = 0 # Decide whether to use legacy FourCC (DXT1/DXT5/ATI1/ATI2) or DX10 header use_legacy = ( not force_dx10_header and dxgi_format in ( DXGI_FORMAT.BC1_UNORM, DXGI_FORMAT.BC3_UNORM, DXGI_FORMAT.BC4_UNORM, DXGI_FORMAT.BC5_UNORM, ) ) if use_legacy: if dxgi_format == DXGI_FORMAT.BC1_UNORM: ddpf_dwFourCC = make_fourcc('D', 'X', 'T', '1') elif dxgi_format == DXGI_FORMAT.BC3_UNORM: ddpf_dwFourCC = make_fourcc('D', 'X', 'T', '5') elif dxgi_format == DXGI_FORMAT.BC4_UNORM: ddpf_dwFourCC = make_fourcc('A', 'T', 'I', '1') elif dxgi_format == DXGI_FORMAT.BC5_UNORM: ddpf_dwFourCC = make_fourcc('A', 'T', 'I', '2') else: # Write DX10 header, FourCC = "DX10" ddpf_dwFourCC = make_fourcc('D', 'X', '1', '0') # Build the 31 uint32's for DDSURFACEDESC2 header_values = [ dwSize, dwFlags, dwHeight, dwWidth, lPitch, dwBackBufferCount, dwMipMapCount, dwAlphaBitDepth, dwUnused0, lpSurface, ] header_values.extend(ddcolorkey_zero) # 8 uint32's ddpf_values = [ ddpf_dwSize, ddpf_dwFlags, ddpf_dwFourCC, ddpf_dwRGBBitCount, ddpf_dwRBitMask, ddpf_dwGBitMask, ddpf_dwBBitMask, ddpf_dwRGBAlphaBitMask, ] header_values.extend(ddpf_values) # 8 uint32's ddsCaps_values = [ ddsCaps_dwCaps, ddsCaps_dwCaps2, ddsCaps_dwCaps3, ddsCaps_dwCaps4, ] header_values.extend(ddsCaps_values) # 4 uint32's header_values.append(dwUnused1) # final uint32 if len(header_values) != 31: raise RuntimeError("Internal error: DDSURFACEDESC2 must contain 31 uint32's") # Pack and write DDSURFACEDESC2 dds_header = struct.pack("<31I", *header_values) f.write(dds_header) # If needed, write the DX10 header (DDS_HEADER_DXT10) if not use_legacy: # struct DDS_HEADER_DXT10 { # DXGI_FORMAT dxgiFormat; # D3D10_RESOURCE_DIMENSION resourceDimension; # uint32 miscFlag; # uint32 arraySize; # uint32 miscFlags2; # }; dxgiFormat = int(dxgi_format) resourceDimension = D3D10_RESOURCE_DIMENSION.TEXTURE2D miscFlag = 0 arraySize = 1 miscFlags2 = 0 dxt10_header = struct.pack( "<5I", dxgiFormat, resourceDimension, miscFlag, arraySize, miscFlags2, ) f.write(dxt10_header) # ----------------------------------------------------------------- # Write the actual texture data blocks (pBlocks) # ----------------------------------------------------------------- # C code: fwrite(pBlocks, desc.lPitch, 1, pFile); # i.e. write exactly lPitch bytes. data = memoryview(blocks) if len(data) < lPitch: raise ValueError( f"blocks buffer too small: need at least {lPitch} bytes, got {len(data)}" ) f.write(data[:lPitch]) except Exception as e: # Mimic the C-style error reporting as much as practical print(f"Failed writing to DDS file {filename}: {e}", file=sys.stderr) try: f.close() except Exception: pass return False # Close file try: f.close() except OSError: print(f"Failed closing DDS file {filename}!", file=sys.stderr) return False return True