439 lines
14 KiB
Ruby
Executable File
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
|