ElderImageryTexture.pas 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374
  1. {
  2. Vampyre Imaging Library
  3. by Marek Mauder
  4. https://github.com/galfar/imaginglib
  5. https://imaginglib.sourceforge.io
  6. - - - - -
  7. This Source Code Form is subject to the terms of the Mozilla Public
  8. License, v. 2.0. If a copy of the MPL was not distributed with this
  9. file, You can obtain one at https://mozilla.org/MPL/2.0.
  10. }
  11. { This unit contains image format loader of Daggerfall texture file format.}
  12. unit ElderImageryTexture;
  13. {$I ImagingOptions.inc}
  14. interface
  15. uses
  16. ImagingTypes, Imaging, ElderImagery, ImagingIO, ImagingUtility;
  17. type
  18. { Class that provides loading of textures from TES2: Daggerfall
  19. (works for Terminator: FS and maybe other games too).
  20. Textures are stored in 8bit indexed format with external palette.
  21. This format is very complicated (more images with subimages,
  22. non-standard RLE, many unknowns) so module supports only loading.
  23. These texture files cannot be recognized by filename extension because
  24. their filenames are in form texture.### where # is number. Use filename
  25. masks instead. Also note that after loading the input position is not set
  26. at the exact end of the data so it's not "stream-safe".}
  27. TTextureFileFormat = class(TElderFileFormat)
  28. private
  29. FLastTextureName: string;
  30. { Deletes non-valid chars from texture name.}
  31. function RepairName(const S: array of AnsiChar): string;
  32. protected
  33. procedure Define; override;
  34. function LoadData(Handle: TImagingHandle; var Images: TDynImageDataArray;
  35. OnlyFirstLevel: Boolean): Boolean; override;
  36. public
  37. function TestFormat(Handle: TImagingHandle): Boolean; override;
  38. { Internal name of the last texture loaded.}
  39. property LastTextureName: string read FLastTextureName;
  40. end;
  41. const
  42. { Metadata item id for accessing name of last loaded Daggerfall texture.
  43. Value type is string.}
  44. SMetaDagTextureName = 'DagTexture.Name';
  45. implementation
  46. const
  47. STextureFormatName = 'Daggerfall Texture';
  48. STextureMasks = '*.dagtexture,texture.*'; // fake ext first, it's used as format id
  49. type
  50. { Main texture header.}
  51. TTexHeader = packed record
  52. ImgCount: Word; // Number of images in texture
  53. TexName: array[0..23] of AnsiChar; // Name of texture
  54. end;
  55. { Offset list for texture.}
  56. TOffset = packed record
  57. Type1: Word; // ??
  58. HdrOffset: Int32; // Contains offset of Img header from the origin
  59. // of the file
  60. Type2: Word; // ??
  61. Unk: UInt32; // Ranges from 0 to 4 (0 in 90%)
  62. Null1: UInt32; // Always 0
  63. Null2: UInt32; // Always 0
  64. end;
  65. TOffsetList = array[Word] of TOffset;
  66. POffsetList = ^TOffsetList;
  67. { Image header for texture.}
  68. TTexImgHeader = packed record
  69. XOff: Word;
  70. YOff: Word;
  71. Width: Word;
  72. Height: Word;
  73. Unk1: Word; // $0108 = Image has subimages which are RLE
  74. // compressed data.
  75. // $1108 = Image has RLE type compressed data with
  76. // a row offset section before the single image data.
  77. ImageSize: UInt32; // Image size (including header)
  78. ImageOff: UInt32; // Pointer to start of image data from this header
  79. Unk2: Word; // $0000 = Image has subimages in special
  80. // compressed format.
  81. // $00C0 = Usual value, regular single image.
  82. // NonZero = Regular single image.Unknown what the
  83. // differences indicate
  84. SubImages: Word; // Number of subimages (1 = single image)
  85. Unk3: UInt32;
  86. Unk4: Word;
  87. end;
  88. { TTextureFileFormat }
  89. procedure TTextureFileFormat.Define;
  90. begin
  91. inherited;
  92. FFeatures := [ffLoad, ffMultiImage];
  93. FName := STextureFormatName;
  94. AddMasks(STextureMasks);
  95. end;
  96. function TTextureFileFormat.RepairName(const S: array of AnsiChar): string;
  97. var
  98. I: LongInt;
  99. First: Boolean;
  100. begin
  101. I := 1;
  102. Result := string(S);
  103. First := False;
  104. while I <= Length(Result) do
  105. begin
  106. if (Ord(Result[I]) < 32) or ((Ord(Result[I]) = 32) and (not First)) then
  107. begin
  108. Delete(Result, I, 1);
  109. end
  110. else
  111. begin
  112. Inc(I);
  113. First := True;
  114. end;
  115. end;
  116. end;
  117. function TTextureFileFormat.LoadData(Handle: TImagingHandle;
  118. var Images: TDynImageDataArray; OnlyFirstLevel: Boolean): Boolean;
  119. var
  120. Hdr: TTexHeader;
  121. InputSize, BasePos, HdrPos, Index, I, Bias: LongInt;
  122. List: POffsetList;
  123. ImageHdr: TTexImgHeader;
  124. function AddImage(Width, Height: LongInt): LongInt;
  125. begin
  126. Result := Length(Images);
  127. SetLength(Images, Length(Images) + 1);
  128. NewImage(Width, Height, ifIndex8, Images[Result]);
  129. Move(FARGBPalette[0], Images[Result].Palette[0], Length(FPalette) * SizeOf(TColor32Rec));
  130. end;
  131. procedure LoadUncompressed;
  132. var
  133. I: LongInt;
  134. begin
  135. // Add image and read its pixels row by row
  136. Index := AddImage(ImageHdr.Width, ImageHdr.Height);
  137. with GetIO, Images[Index] do
  138. for I := 0 to ImageHdr.Height - 1 do
  139. begin
  140. Read(Handle, @PByteArray(Bits)[I * Width], Width);
  141. Seek(Handle, 256 - Width, smFromCurrent);
  142. end;
  143. end;
  144. procedure LoadUncompressedSubImages;
  145. var
  146. SubOffs: packed array[0..63] of Int32;
  147. I, StartPos, J, WritePos: LongInt;
  148. NumZeroes, NumImageBytes: Byte;
  149. SubWidth, SubHeight: Word;
  150. begin
  151. // Read subimages offset list
  152. StartPos := GetIO.Tell(Handle);
  153. FillChar(SubOffs, SizeOf(SubOffs), 0);
  154. GetIO.Read(Handle, @SubOffs, ImageHdr.SubImages * 4);
  155. for I := 0 to ImageHdr.SubImages - 1 do
  156. begin
  157. // Add new subimage and load its pixels
  158. Index := AddImage(ImageHdr.Width, ImageHdr.Height);
  159. with GetIO, Images[Index] do
  160. begin
  161. Seek(Handle, StartPos + SubOffs[I], smFromBeginning);
  162. Read(Handle, @SubWidth, 2);
  163. Read(Handle, @SubHeight, 2);
  164. // Read rows
  165. for J := 0 to SubHeight - 1 do
  166. begin
  167. WritePos := 0;
  168. while WritePos < SubWidth do
  169. begin
  170. // First there is a number of zero pixels that should be written
  171. // to this row (slight compression as many images/sprites have
  172. // many zero pixels)
  173. Read(Handle, @NumZeroes, 1);
  174. FillChar(PByteArray(Bits)[J * SubWidth + WritePos], NumZeroes, 0);
  175. WritePos := WritePos + NumZeroes;
  176. // Now there is a number of bytes that contain image data and should
  177. // be copied to this row
  178. Read(Handle, @NumImageBytes, 1);
  179. Read(Handle, @PByteArray(Bits)[J * SubWidth + WritePos], NumImageBytes);
  180. WritePos := WritePos + NumImageBytes;
  181. end;
  182. end;
  183. end;
  184. end;
  185. end;
  186. procedure LoadRLESubImages;
  187. type
  188. TRowOff = packed record
  189. Off: Word;
  190. RLEStatus: Word;
  191. end;
  192. var
  193. RowOffs: packed array[0..255] of TRowOff;
  194. I, J, WritePos, NextOffsetPos: LongInt;
  195. RLEData: Byte;
  196. ByteCount, RowWidth: SmallInt;
  197. begin
  198. NextOffsetPos := GetIO.Tell(Handle);
  199. for I := 0 to ImageHdr.SubImages - 1 do
  200. begin
  201. // Read row offsets for RLE subimage
  202. FillChar(RowOffs, SizeOf(RowOffs), 0);
  203. GetIO.Seek(Handle, NextOffsetPos, smFromBeginning);
  204. GetIO.Read(Handle, @RowOffs, ImageHdr.Height * SizeOf(TRowOff));
  205. NextOffsetPos := GetIO.Tell(Handle);
  206. // Add new image
  207. Index := AddImage(ImageHdr.Width, ImageHdr.Height);
  208. with GetIO, Images[Index] do
  209. begin
  210. for J := 0 to Height - 1 do
  211. begin
  212. // Seek to the beginning of the current row in the source
  213. Seek(Handle, HdrPos + RowOffs[J].Off, smFromBeginning);
  214. if RowOffs[J].RLEStatus = $8000 then
  215. begin
  216. // This row is compressed so it must be decoded (it is different
  217. // from RLE in IMG/CIF files)
  218. Read(Handle, @RowWidth, 2);
  219. WritePos := 0;
  220. while WritePos < RowWidth do
  221. begin
  222. Read(Handle, @ByteCount, 2);
  223. if ByteCount > 0 then
  224. begin
  225. Read(Handle, @PByteArray(Bits)[J * Width + WritePos], ByteCount);
  226. WritePos := WritePos + ByteCount;
  227. end
  228. else
  229. begin
  230. Read(Handle, @RLEData, 1);
  231. FillChar(PByteArray(Bits)[J * Width + WritePos], -ByteCount, RLEData);
  232. WritePos := WritePos - ByteCount;
  233. end;
  234. end;
  235. end
  236. else
  237. // Read uncompressed row
  238. Read(Handle, @PByteArray(Bits)[J * Width], Width);
  239. end;
  240. end;
  241. end;
  242. end;
  243. begin
  244. Result := False;
  245. SetLength(Images, 0);
  246. with GetIO do
  247. begin
  248. InputSize := GetInputSize(GetIO, Handle);
  249. BasePos := Tell(Handle);
  250. Read(Handle, @Hdr, SizeOf(Hdr));
  251. FLastTextureName := RepairName(Hdr.TexName);
  252. FMetadata.SetMetaItem(SMetaDagTextureName, FLastTextureName);
  253. if InputSize = 2586 then
  254. begin
  255. // Handle texture.001 and texture.000 files
  256. // They contain only indices to palette so we create small
  257. // images with colors defined by these indices
  258. Bias := 0;
  259. if Pos('B', FLastTextureName) > 0 then
  260. Bias := 128;
  261. for I := 0 to Hdr.ImgCount - 1 do
  262. begin
  263. Index := AddImage(16, 16);
  264. FillMemoryByte(Images[Index].Bits, Images[Index].Size, I + Bias);
  265. end;
  266. end
  267. else if (InputSize = 46) or (InputSize = 126) or (InputSize = 266) then
  268. begin
  269. // These textures don't contain any image data
  270. Exit;
  271. end
  272. else
  273. begin
  274. GetMem(List, Hdr.ImgCount * SizeOf(TOffset));
  275. try
  276. // Load offsets
  277. for I := 0 to Hdr.ImgCount - 1 do
  278. Read(Handle, @List[I], SizeOf(TOffset));
  279. // Load subimages one by one
  280. for I := 0 to Hdr.ImgCount - 1 do
  281. begin
  282. // Jump at position of image header
  283. Seek(Handle, BasePos + List[I].HdrOffset, smFromBeginning);
  284. HdrPos := Tell(Handle);
  285. Read(Handle, @ImageHdr, SizeOf(ImageHdr));
  286. Seek(Handle, HdrPos + ImageHdr.ImageOff, smFromBeginning);
  287. // According to number of subimages and RLE settings appropriate
  288. // procedure is called to load subimages
  289. if ImageHdr.SubImages = 1 then
  290. begin
  291. if (ImageHdr.Unk1 <> $1108) and (ImageHdr.Unk1 <> $0108) then
  292. LoadUncompressed
  293. else
  294. LoadRLESubImages;
  295. end
  296. else
  297. begin
  298. if (ImageHdr.Unk1 <> $0108) then
  299. LoadUncompressedSubImages
  300. else
  301. LoadRLESubImages;
  302. end;
  303. end;
  304. finally
  305. FreeMem(List);
  306. end;
  307. end;
  308. Result := True;
  309. end;
  310. end;
  311. function TTextureFileFormat.TestFormat(Handle: TImagingHandle): Boolean;
  312. var
  313. Hdr: TTexHeader;
  314. ReadCount, I: LongInt;
  315. begin
  316. Result := False;
  317. if Handle <> nil then
  318. begin
  319. ReadCount := GetIO.Read(Handle, @Hdr, SizeOf(Hdr));
  320. GetIO.Seek(Handle, -ReadCount, smFromCurrent);
  321. Result := (ReadCount = SizeOf(Hdr)) and (Hdr.ImgCount > 0) and
  322. (Hdr.ImgCount <= 2048);
  323. if Result then
  324. begin
  325. for I := 0 to High(Hdr.TexName) do
  326. begin
  327. if not (Hdr.TexName[I] in [#0, #32, 'a'..'z', 'A'..'Z', '0'..'9', '.',
  328. '(', ')', '_', ',', '-', '''', '"', '/', '\', #9, '+']) then
  329. begin
  330. Result := False;
  331. Exit;
  332. end;
  333. end;
  334. end;
  335. end;
  336. end;
  337. {
  338. File Notes:
  339. -- TODOS ----------------------------------------------------
  340. - nothing now
  341. -- 0.26.5 Changes/Bug Fixes ---------------------------------
  342. - Last texture name now accessible trough metadata interface.
  343. -- 0.21 Changes/Bug Fixes -----------------------------------
  344. - Initial version created based on my older code (fixed few things).
  345. }
  346. end.