{ 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.