| # 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 |