Files
2025-09-29 00:52:08 +02:00

439 lines
14 KiB
Ruby
Executable File

#
# 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 <david.muir@rockstarnorth.com>
# 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