# # File:: dds_raw.rb # Description:: DirectDraw Surface (DDS) RAW file reader. Nothing is done to # decompress the image data or separate mipmap data. It was # primarily written to diff DDS files. # # Author:: David Muir # Date:: 9 October 2008 # #----------------------------------------------------------------------------- # Uses #----------------------------------------------------------------------------- require 'pipeline/fileformats/magic' require 'pipeline/os/binary_file_reader' include Pipeline::OS #----------------------------------------------------------------------------- # Implementation #----------------------------------------------------------------------------- module Pipeline module FileFormats # # == Description # Raw DDS file representation. Header information is available but in # a fairly unprocessed manner so additional interpretation may be required. # class DDSFileRaw attr_reader :filename # Filename of DDS file attr_reader :filesize # Filesize on disk attr_reader :dwVersion # Version identifier attr_reader :header # DDSurfaceDesc2 object attr_reader :imagedata # Image data read straight from file (no-decompression) attr_reader :mipimagedata # Mipmap image data read straight from file (no-decompression) HEADER_LOC = 4 DDS_9 = 0x44445320.to_s # 'D', 'D', 'S', 0x20 FORMAT_DXT1 = 2 FORMAT_DXT3 = 3 FORMAT_DXT5 = 4 TYPE_STANDARD = 0 TYPE_CUBE = 1 TYPE_DEPTH = 2 TYPE_VOLUME = 3 TYPE_STRINGS = { TYPE_STANDARD => 'TEXTURE', TYPE_CUBE => 'CUBEMAP', TYPE_DEPTH => 'DEPTHMAP', TYPE_VOLUME => 'VOLUMEMAP' } # # == Description # DDS file pixel format structure. # class DDPixelFormat attr_reader :dwSize, :dwFlags, :dwFourCC, :dwRGBBitCount attr_reader :dwRBitMask, :dwGBitMask, :dwBBitMask, :dwRGBAAlphaBitMask SIZE = 32 DDPF_ALPHAPIXELS= 0x00000001 DDPF_ALPHA = 0x00000002 DDPF_FOURCC = 0x00000004 DDPF_RGB = 0x00000040 DDPF_LUMINANCE = 0x00020000 DDPF_FLAGS = { DDPF_ALPHAPIXELS => 'DDPF_ALPHAPIXELS', DDPF_ALPHA => 'DDPF_ALPHA', DDPF_FOURCC => 'DDPF_FOURCC', DDPF_RGB => 'DDPF_RGB', DDPF_LUMINANCE => 'DDPF_LUMINANCE' } def initialize( size, flags, fourcc, bc, rmask, gmask, bmask, amask ) throw RuntimeError.new( "Invalid DDPixelFormat size, #{size} expecting #{DDPixelFormat::SIZE}." ) \ unless ( size.to_i == SIZE ) @dwSize = size.to_i @dwFlags = flags.to_i @dwFourCC = fourcc @dwRGBBitCount = bc.to_i @dwRBitMask = rmask.to_i @dwGBitMask = gmask.to_i @dwBBitMask = bmask.to_i @dwRGBAAlphaBitMask = amask.to_i end def pretty_print( indent = 0, indent_char = "\t" ) flags_str = "" DDPF_FLAGS.each_pair do |k,v| flags_str += "#{v} " if ( ( @dwFlags & k ) == k ) end indent.times do print( indent_char ); end puts "Size: #{@dwSize}" indent.times do print( indent_char ); end puts "Flags: #{flags_str} (#{@dwFlags})" indent.times do print( indent_char ); end if ( @dwFlags & DDPF_FOURCC ) then puts "FourCC: #{@dwFourCC}" indent.times do print( indent_char ); end end puts "RGBBitCount: #{@dwRGBBitCount}" indent.times do print( indent_char ); end puts "RMask: #{@dwRBitMask}" indent.times do print( indent_char ); end puts "GMask: #{@dwGBitMask}" indent.times do print( indent_char ); end puts "BMask: #{@dwBBitMask}" indent.times do print( indent_char ); end puts "RGBAAlphaMask: #{@dwRGBAAlphaBitMask}" end def DDPixelFormat::from_io( io ) throw RuntimeError.new( "Invalid file location for reading DDPixelFormat, #{io.tell} expecting #{DDSurfaceDesc2::LOC_PF}." ) \ unless ( io.tell == ( DDSurfaceDesc2::LOC_PF ) ) size = BinaryFileReader::read_io_ledw( io, 4 ) flags = BinaryFileReader::read_io_ledw( io, 4 ) fourcc = io.read( 4 ) bc = BinaryFileReader::read_io_ledw( io, 4 ) rmask = BinaryFileReader::read_io_ledw( io, 4 ) gmask = BinaryFileReader::read_io_ledw( io, 4 ) bmask = BinaryFileReader::read_io_ledw( io, 4 ) amask = BinaryFileReader::read_io_ledw( io, 4 ) DDPixelFormat.new( size, flags, fourcc, bc, rmask, gmask, bmask, amask ) end end # # == Description # DDS file header capability structure. # class DDSCaps2 attr_reader :dwCaps1, :dwCaps2 SIZE = 16 CAP_COMPLEX = 0x00000008 CAP_TEXTURE = 0x00001000 CAP_MIPMAP = 0x00400000 CAP2_CUBEMAP = 0x00000200 CAP2_CUBEMAP_POSITIVEX = 0x00000400 CAP2_CUBEMAP_NEGATIVEX = 0x00000800 CAP2_CUBEMAP_POSITIVEY = 0x00001000 CAP2_CUBEMAP_NEGATIVEY = 0x00002000 CAP2_CUBEMAP_POSITIVEZ = 0x00004000 CAP2_CUBEMAP_NEGATIVEZ = 0x00008000 CAP2_CUBEMAP_VOLUME = 0x00200000 def initialize( caps1, caps2 ) @dwCaps1 = caps1.to_i @dwCaps2 = caps2.to_i end def pretty_print( indent = 0, indent_char = "\t" ) indent.times do print( indent_char ); end puts "Caps1: #{@dwCaps1}" indent.times do print( indent_char ); end puts "Caps2: #{@dwCaps2}" end def DDSCaps2::from_io( io ) throw RuntimeError.new( "Invalid file location for reading DDSCaps2, #{io.tell} expecting #{DDSurfaceDesc2::LOC_CAPS}." ) \ unless ( io.tell == ( DDSurfaceDesc2::LOC_CAPS ) ) caps1 = BinaryFileReader::read_io_ledw( io, 4 ) caps2 = BinaryFileReader::read_io_ledw( io, 4 ) # Skip DWORD * 2 bytes (dwReserved[2]) io.seek( 2 * 4, IO::SEEK_CUR ) DDSCaps2.new( caps1, caps2 ) end end # # == Description # DDS file main header structure. # class DDSurfaceDesc2 SIZE = 124 LOC_PF = DDSFileRaw::HEADER_LOC + 72 LOC_CAPS = LOC_PF + DDPixelFormat::SIZE DDSD_CAPS = 0x00000001 DDSD_HEIGHT = 0x00000002 DDSD_WIDTH = 0x00000004 DDSD_PITCH = 0x00000008 DDSD_PIXELFORMAT = 0x00001000 DDSD_MIPMAPCOUNT = 0x00020000 DDSD_LINEARSIZE = 0x00080000 DDSD_DEPTH = 0x00800000 DDSD_FLAGS = { DDSD_CAPS => 'DDSD_CAPS', DDSD_HEIGHT => 'DDSD_HEIGHT', DDSD_WIDTH => 'DDSD_WIDTH', DDSD_PITCH => 'DDSD_PITCH', DDSD_PIXELFORMAT => 'DDSD_PIXELFORMAT', DDSD_MIPMAPCOUNT => 'DDSD_MIPMAPCOUNT', DDSD_LINEARSIZE => 'DDSD_LINEARSIZE', DDSD_DEPTH => 'DDSD_DEPTH', } attr_reader :dwSize attr_reader :dwFlags attr_reader :dwHeight, :dwWidth, :dwPitchOrLinearSize, :dwDepth attr_reader :dwMipMapCount attr_reader :fGamma, :fColorExp, :fColorOfs attr_reader :ddpfPixelFormat, :ddsCaps def initialize( size, flags, h, w, pitch, d, mipcount, gamma, colorexp, colorofs, pf, caps ) throw RuntimeError.new( "Invalid DDSurfaceDesc2 size, #{size} expecting #{DDSurfaceDesc2::SIZE}." ) \ unless ( size.to_i == SIZE ) throw RuntimeError.new( "Invalid DDSurfaceDesc2 colorexp, expecting Array of 3 floats." ) \ unless ( colorexp.is_a?( Array ) and 3 == colorexp.size ) throw RuntimeError.new( "Invalid DDSurfaceDesc2 colorofs, expecting Array of 3 floats." ) \ unless ( colorofs.is_a?( Array ) and 3 == colorofs.size ) @dwSize = size.to_i @dwFlags = flags.to_i @dwHeight = h.to_i @dwWidth = w.to_i @dwPitchOrLinearSize = pitch.to_i @dwDepth = d.to_i @dwMipMapCount = mipcount.to_i @fGamma = gamma @fColorExp = colorexp @fColorOfs = colorofs @ddpfPixelFormat = pf @ddsCaps = caps end def pretty_print( indent = 0, indent_char = "\t" ) flags_str = "" DDSD_FLAGS.each_pair do |k,v| flags_str += "#{v} " if ( ( @dwFlags & k ) == k ) end indent.times do print( indent_char ); end puts "Size: #{@dwSize}" indent.times do print( indent_char ); end puts "Flags: #{flags_str} (#{@dwFlags})" indent.times do print( indent_char ); end puts "Height: #{@dwHeight}" indent.times do print( indent_char ); end puts "Width: #{@dwWidth}" indent.times do print( indent_char ); end puts "PitchOrLinearSize: #{@dwPitchOrLinearSize}" indent.times do print( indent_char ); end puts "Depth: #{@dwDepth}" if ( DDSD_MIPMAPCOUNT == ( @dwFlags & DDSD_MIPMAPCOUNT ) ) then indent.times do print( indent_char ); end puts "Mipmap Count: #{@dwMipMapCount}" end @ddpfPixelFormat.pretty_print( indent+1, indent_char ) indent.times do print( indent_char ); end puts "Gamma: #{@fGamma}" indent.times do print( indent_char ); end puts "ColorExp: [ #{@fColorExp.join(', ')} ]" indent.times do print( indent_char ); end puts "ColorOfs: [ #{@fColorOfs.join(', ')} ]" end def DDSurfaceDesc2::from_io( io ) throw RuntimeError.new( "Invalid file location for reading DDSurfaceDesc2, #{io.tell} expecting #{HEADER_LOC}." ) \ unless ( io.tell == 4 ) size = BinaryFileReader::read_io_ledw( io, 4 ) flags = BinaryFileReader::read_io_ledw( io, 4 ) h = BinaryFileReader::read_io_ledw( io, 4 ) w = BinaryFileReader::read_io_ledw( io, 4 ) p = BinaryFileReader::read_io_ledw( io, 4 ) d = BinaryFileReader::read_io_ledw( io, 4 ) mipcount = 1 if ( DDSD_MIPMAPCOUNT == ( flags & DDSD_MIPMAPCOUNT ) ) then mipcount = BinaryFileReader::read_io_ledw( io, 4 ) else # Skip io.seek( 1 * 4, IO::SEEK_CUR ) end # Skip DWORD * 4 bytes (dwReserved1[11]) io.seek( 4 * 4, IO::SEEK_CUR ) # Non-standard DDS format? Normally 11 unused dwords, but # we only have 4 unused. 7 used for other stuff. gamma = BinaryFileReader::read_io_lefloat( io ) colorexp = [] colorexp << BinaryFileReader::read_io_lefloat( io ) colorexp << BinaryFileReader::read_io_lefloat( io ) colorexp << BinaryFileReader::read_io_lefloat( io ) colorofs = [] colorofs << BinaryFileReader::read_io_lefloat( io ) colorofs << BinaryFileReader::read_io_lefloat( io ) colorofs << BinaryFileReader::read_io_lefloat( io ) pf = DDPixelFormat::from_io( io ) caps = DDSCaps2::from_io( io ) # Skip DWORD * 1 bytes (dwReserved2) io.seek( 1 * 4, IO::SEEK_CUR ) # Return DDSurfaceDesc2.new( size, flags, h, w, p, d, mipcount, gamma, colorexp, colorofs, pf, caps ) end end def pretty_print( indent = 0, indent_char = "\t" ) indent.times do print( indent_char ); end puts "Layers: #{@layers}" indent.times do print( indent_char ); end puts "Type: #{TYPE_STRINGS[@type]}" @header.pretty_print( indent+1, indent_char ) end #--------------------------------------------------------------------- # Class Functions #--------------------------------------------------------------------- # # Class function to create a DDSFileRaw object from a DDS filename. # def DDSFileRaw::from_filename( filename ) DDSFileRaw.new( filename ) end #--------------------------------------------------------------------- # Private Functions #--------------------------------------------------------------------- private def initialize( filename ) throw IOError.new( "File #{filename} does not exist or is not readable." ) \ unless ( ::File::exists?( filename ) and ::File::readable?( filename ) ) @filename = filename @imagedata = nil @mipimagedata = [] parse( ) end # # Parse DDS header info. # def parse( ) begin @filesize = ::File::size( @filename ) return if ( 0 == @filesize ) File::open( @filename, 'rb' ) do |fp| version = BinaryFileReader::read_io_bedw( fp, 4 ) case version.to_s when DDS_9 @header = DDSurfaceDesc2::from_io( fp ) else throw RuntimeError.new( "Invalid DDS version: #{version}, expecting: #{DDS_9}." ) end parse_image_data( fp ) end rescue Exception => ex raise end end # # Parse image data and mipmap data. See RAGE's image.cpp, dds.h from the # RageGraphics library for additional info (certainly more than MSDN). # def parse_image_data( io ) ddpf = @header.ddpfPixelFormat ddcaps = @header.ddsCaps @layers = 1 @type = TYPE_STANDARD if ( DDSCaps2::CAP2_CUBEMAP == ( ddcaps.dwCaps2 & DDSCaps2::CAP2_CUBEMAP ) ) then @layers = 1 if ( ddpf.dwFlags & DDSCaps2::CAP2_CUBEMAP_POSITIVEX ) @layers += 1 if ( ddpf.dwFlags & DDSCaps2::CAP2_CUBEMAP_NEGATIVEX ) @layers += 1 if ( ddpf.dwFlags & DDSCaps2::CAP2_CUBEMAP_POSITIVEY ) @layers += 1 if ( ddpf.dwFlags & DDSCaps2::CAP2_CUBEMAP_NEGATIVEY ) @layers += 1 if ( ddpf.dwFlags & DDSCaps2::CAP2_CUBEMAP_POSITIVEZ ) @layers += 1 if ( ddpf.dwFlags & DDSCaps2::CAP2_CUBEMAP_NEGATIVEZ ) @type = TYPE_CUBE elsif ( DDSCaps2::CAP2_CUBEMAP_VOLUME == ( ddcaps.dwCaps2 & DDSCaps2::CAP2_CUBEMAP_VOLUME ) ) then @type = TYPE_VOLUME end if ( DDPixelFormat::DDPF_FOURCC == ( ddpf.dwFlags & DDPixelFormat::DDPF_FOURCC ) ) then # Determine compression algorithm. case ddpf.dwFourCC when 'DXT1': @format = FORMAT_DXT1 when 'DXT3': @format = FORMAT_DXT3 when 'DXT5': @format = FORMAT_DXT5 else throw RuntimeError.new( "Unsupported compression algorithm #{ddpf.dwFourCC}." ) end # Main image data @imagedata = io.read( @header.dwPitchOrLinearSize ) # Read mipmap image data mipwidth = @header.dwWidth / 2 mipheight = @header.dwHeight / 2 mipsize = @header.dwPitchOrLinearSize / 4 # number of bytes ( @header.dwMipMapCount - 1 ).times do |index| # ... but we are compressed as shorts per RGBA... so half size. @mipimagedata << io.read( mipsize / 2 ) mipsize /= 2 end #elsif ( DDPixelFormat::DDPF_RGB == ( ddpf.dwFlags & DDPixelFormat::DDPF_RGB ) ) then #elsif ( DDPixelFormat::DDPF_ALPHA == ( ddpf.dwFlags & DDPixelFormat::DDPF_ALPHA ) ) then #elsif ( DDPixelFormat::DDPF_LUMINANCE == ( ddpf.dwFlags & DDPixelFormat::DDPF_LUMINANCE ) ) then else throw RuntimeError.new( "Unsupported type." ) end end end end # FileFormats module end # Pipeline module if ( __FILE__ == $0 ) then dds = Pipeline::FileFormats::DDSFileRaw::from_filename( 'x:/gta4_map_textures/textures/ab_brick03.dds' ) dds.pretty_print() end # dds_raw.rb