| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374 |
- {
- Vampyre Imaging Library
- by Marek Mauder
- https://github.com/galfar/imaginglib
- https://imaginglib.sourceforge.io
- - - - - -
- This Source Code Form is subject to the terms of the Mozilla Public
- License, v. 2.0. If a copy of the MPL was not distributed with this
- file, You can obtain one at https://mozilla.org/MPL/2.0.
- }
- { This unit contains image format loader of Daggerfall texture file format.}
- unit ElderImageryTexture;
- {$I ImagingOptions.inc}
- interface
- uses
- ImagingTypes, Imaging, ElderImagery, ImagingIO, ImagingUtility;
- type
- { Class that provides loading of textures from TES2: Daggerfall
- (works for Terminator: FS and maybe other games too).
- Textures are stored in 8bit indexed format with external palette.
- This format is very complicated (more images with subimages,
- non-standard RLE, many unknowns) so module supports only loading.
- These texture files cannot be recognized by filename extension because
- their filenames are in form texture.### where # is number. Use filename
- masks instead. Also note that after loading the input position is not set
- at the exact end of the data so it's not "stream-safe".}
- TTextureFileFormat = class(TElderFileFormat)
- private
- FLastTextureName: string;
- { Deletes non-valid chars from texture name.}
- function RepairName(const S: array of AnsiChar): string;
- protected
- procedure Define; override;
- function LoadData(Handle: TImagingHandle; var Images: TDynImageDataArray;
- OnlyFirstLevel: Boolean): Boolean; override;
- public
- function TestFormat(Handle: TImagingHandle): Boolean; override;
- { Internal name of the last texture loaded.}
- property LastTextureName: string read FLastTextureName;
- end;
- const
- { Metadata item id for accessing name of last loaded Daggerfall texture.
- Value type is string.}
- SMetaDagTextureName = 'DagTexture.Name';
- implementation
- const
- STextureFormatName = 'Daggerfall Texture';
- STextureMasks = '*.dagtexture,texture.*'; // fake ext first, it's used as format id
- type
- { Main texture header.}
- TTexHeader = packed record
- ImgCount: Word; // Number of images in texture
- TexName: array[0..23] of AnsiChar; // Name of texture
- end;
- { Offset list for texture.}
- TOffset = packed record
- Type1: Word; // ??
- HdrOffset: Int32; // Contains offset of Img header from the origin
- // of the file
- Type2: Word; // ??
- Unk: UInt32; // Ranges from 0 to 4 (0 in 90%)
- Null1: UInt32; // Always 0
- Null2: UInt32; // Always 0
- end;
- TOffsetList = array[Word] of TOffset;
- POffsetList = ^TOffsetList;
- { Image header for texture.}
- TTexImgHeader = packed record
- XOff: Word;
- YOff: Word;
- Width: Word;
- Height: Word;
- Unk1: Word; // $0108 = Image has subimages which are RLE
- // compressed data.
- // $1108 = Image has RLE type compressed data with
- // a row offset section before the single image data.
- ImageSize: UInt32; // Image size (including header)
- ImageOff: UInt32; // Pointer to start of image data from this header
- Unk2: Word; // $0000 = Image has subimages in special
- // compressed format.
- // $00C0 = Usual value, regular single image.
- // NonZero = Regular single image.Unknown what the
- // differences indicate
- SubImages: Word; // Number of subimages (1 = single image)
- Unk3: UInt32;
- Unk4: Word;
- end;
- { TTextureFileFormat }
- procedure TTextureFileFormat.Define;
- begin
- inherited;
- FFeatures := [ffLoad, ffMultiImage];
- FName := STextureFormatName;
- AddMasks(STextureMasks);
- end;
- function TTextureFileFormat.RepairName(const S: array of AnsiChar): string;
- var
- I: LongInt;
- First: Boolean;
- begin
- I := 1;
- Result := string(S);
- First := False;
- while I <= Length(Result) do
- begin
- if (Ord(Result[I]) < 32) or ((Ord(Result[I]) = 32) and (not First)) then
- begin
- Delete(Result, I, 1);
- end
- else
- begin
- Inc(I);
- First := True;
- end;
- end;
- end;
- function TTextureFileFormat.LoadData(Handle: TImagingHandle;
- var Images: TDynImageDataArray; OnlyFirstLevel: Boolean): Boolean;
- var
- Hdr: TTexHeader;
- InputSize, BasePos, HdrPos, Index, I, Bias: LongInt;
- List: POffsetList;
- ImageHdr: TTexImgHeader;
- function AddImage(Width, Height: LongInt): LongInt;
- begin
- Result := Length(Images);
- SetLength(Images, Length(Images) + 1);
- NewImage(Width, Height, ifIndex8, Images[Result]);
- Move(FARGBPalette[0], Images[Result].Palette[0], Length(FPalette) * SizeOf(TColor32Rec));
- end;
- procedure LoadUncompressed;
- var
- I: LongInt;
- begin
- // Add image and read its pixels row by row
- Index := AddImage(ImageHdr.Width, ImageHdr.Height);
- with GetIO, Images[Index] do
- for I := 0 to ImageHdr.Height - 1 do
- begin
- Read(Handle, @PByteArray(Bits)[I * Width], Width);
- Seek(Handle, 256 - Width, smFromCurrent);
- end;
- end;
- procedure LoadUncompressedSubImages;
- var
- SubOffs: packed array[0..63] of Int32;
- I, StartPos, J, WritePos: LongInt;
- NumZeroes, NumImageBytes: Byte;
- SubWidth, SubHeight: Word;
- begin
- // Read subimages offset list
- StartPos := GetIO.Tell(Handle);
- FillChar(SubOffs, SizeOf(SubOffs), 0);
- GetIO.Read(Handle, @SubOffs, ImageHdr.SubImages * 4);
- for I := 0 to ImageHdr.SubImages - 1 do
- begin
- // Add new subimage and load its pixels
- Index := AddImage(ImageHdr.Width, ImageHdr.Height);
- with GetIO, Images[Index] do
- begin
- Seek(Handle, StartPos + SubOffs[I], smFromBeginning);
- Read(Handle, @SubWidth, 2);
- Read(Handle, @SubHeight, 2);
- // Read rows
- for J := 0 to SubHeight - 1 do
- begin
- WritePos := 0;
- while WritePos < SubWidth do
- begin
- // First there is a number of zero pixels that should be written
- // to this row (slight compression as many images/sprites have
- // many zero pixels)
- Read(Handle, @NumZeroes, 1);
- FillChar(PByteArray(Bits)[J * SubWidth + WritePos], NumZeroes, 0);
- WritePos := WritePos + NumZeroes;
- // Now there is a number of bytes that contain image data and should
- // be copied to this row
- Read(Handle, @NumImageBytes, 1);
- Read(Handle, @PByteArray(Bits)[J * SubWidth + WritePos], NumImageBytes);
- WritePos := WritePos + NumImageBytes;
- end;
- end;
- end;
- end;
- end;
- procedure LoadRLESubImages;
- type
- TRowOff = packed record
- Off: Word;
- RLEStatus: Word;
- end;
- var
- RowOffs: packed array[0..255] of TRowOff;
- I, J, WritePos, NextOffsetPos: LongInt;
- RLEData: Byte;
- ByteCount, RowWidth: SmallInt;
- begin
- NextOffsetPos := GetIO.Tell(Handle);
- for I := 0 to ImageHdr.SubImages - 1 do
- begin
- // Read row offsets for RLE subimage
- FillChar(RowOffs, SizeOf(RowOffs), 0);
- GetIO.Seek(Handle, NextOffsetPos, smFromBeginning);
- GetIO.Read(Handle, @RowOffs, ImageHdr.Height * SizeOf(TRowOff));
- NextOffsetPos := GetIO.Tell(Handle);
- // Add new image
- Index := AddImage(ImageHdr.Width, ImageHdr.Height);
- with GetIO, Images[Index] do
- begin
- for J := 0 to Height - 1 do
- begin
- // Seek to the beginning of the current row in the source
- Seek(Handle, HdrPos + RowOffs[J].Off, smFromBeginning);
- if RowOffs[J].RLEStatus = $8000 then
- begin
- // This row is compressed so it must be decoded (it is different
- // from RLE in IMG/CIF files)
- Read(Handle, @RowWidth, 2);
- WritePos := 0;
- while WritePos < RowWidth do
- begin
- Read(Handle, @ByteCount, 2);
- if ByteCount > 0 then
- begin
- Read(Handle, @PByteArray(Bits)[J * Width + WritePos], ByteCount);
- WritePos := WritePos + ByteCount;
- end
- else
- begin
- Read(Handle, @RLEData, 1);
- FillChar(PByteArray(Bits)[J * Width + WritePos], -ByteCount, RLEData);
- WritePos := WritePos - ByteCount;
- end;
- end;
- end
- else
- // Read uncompressed row
- Read(Handle, @PByteArray(Bits)[J * Width], Width);
- end;
- end;
- end;
- end;
- begin
- Result := False;
- SetLength(Images, 0);
- with GetIO do
- begin
- InputSize := GetInputSize(GetIO, Handle);
- BasePos := Tell(Handle);
- Read(Handle, @Hdr, SizeOf(Hdr));
- FLastTextureName := RepairName(Hdr.TexName);
- FMetadata.SetMetaItem(SMetaDagTextureName, FLastTextureName);
- if InputSize = 2586 then
- begin
- // Handle texture.001 and texture.000 files
- // They contain only indices to palette so we create small
- // images with colors defined by these indices
- Bias := 0;
- if Pos('B', FLastTextureName) > 0 then
- Bias := 128;
- for I := 0 to Hdr.ImgCount - 1 do
- begin
- Index := AddImage(16, 16);
- FillMemoryByte(Images[Index].Bits, Images[Index].Size, I + Bias);
- end;
- end
- else if (InputSize = 46) or (InputSize = 126) or (InputSize = 266) then
- begin
- // These textures don't contain any image data
- Exit;
- end
- else
- begin
- GetMem(List, Hdr.ImgCount * SizeOf(TOffset));
- try
- // Load offsets
- for I := 0 to Hdr.ImgCount - 1 do
- Read(Handle, @List[I], SizeOf(TOffset));
- // Load subimages one by one
- for I := 0 to Hdr.ImgCount - 1 do
- begin
- // Jump at position of image header
- Seek(Handle, BasePos + List[I].HdrOffset, smFromBeginning);
- HdrPos := Tell(Handle);
- Read(Handle, @ImageHdr, SizeOf(ImageHdr));
- Seek(Handle, HdrPos + ImageHdr.ImageOff, smFromBeginning);
- // According to number of subimages and RLE settings appropriate
- // procedure is called to load subimages
- if ImageHdr.SubImages = 1 then
- begin
- if (ImageHdr.Unk1 <> $1108) and (ImageHdr.Unk1 <> $0108) then
- LoadUncompressed
- else
- LoadRLESubImages;
- end
- else
- begin
- if (ImageHdr.Unk1 <> $0108) then
- LoadUncompressedSubImages
- else
- LoadRLESubImages;
- end;
- end;
- finally
- FreeMem(List);
- end;
- end;
- Result := True;
- end;
- end;
- function TTextureFileFormat.TestFormat(Handle: TImagingHandle): Boolean;
- var
- Hdr: TTexHeader;
- ReadCount, I: LongInt;
- begin
- Result := False;
- if Handle <> nil then
- begin
- ReadCount := GetIO.Read(Handle, @Hdr, SizeOf(Hdr));
- GetIO.Seek(Handle, -ReadCount, smFromCurrent);
- Result := (ReadCount = SizeOf(Hdr)) and (Hdr.ImgCount > 0) and
- (Hdr.ImgCount <= 2048);
- if Result then
- begin
- for I := 0 to High(Hdr.TexName) do
- begin
- if not (Hdr.TexName[I] in [#0, #32, 'a'..'z', 'A'..'Z', '0'..'9', '.',
- '(', ')', '_', ',', '-', '''', '"', '/', '\', #9, '+']) then
- begin
- Result := False;
- Exit;
- end;
- end;
- end;
- end;
- end;
- {
- File Notes:
- -- TODOS ----------------------------------------------------
- - nothing now
- -- 0.26.5 Changes/Bug Fixes ---------------------------------
- - Last texture name now accessible trough metadata interface.
- -- 0.21 Changes/Bug Fixes -----------------------------------
- - Initial version created based on my older code (fixed few things).
- }
- end.
|