ElderImageryBsi.pas 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457
  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 for textures and images
  12. from Redguard and BattleSpire.}
  13. unit ElderImageryBsi;
  14. {$I ImagingOptions.inc}
  15. interface
  16. uses
  17. ImagingTypes, Imaging, ElderImagery, ImagingUtility;
  18. type
  19. { Class for loading of BSI format textures and images found
  20. in Redguard and BattleSpire (maybe in other games too, Skynet?). This format
  21. uses chunk structure similar to PNG (HDR/DAT/END).
  22. BattleSpire BSI use *.bsi file extension whilst Redguard uses
  23. texbsi.* mask with number extension (just like Daggerfall).
  24. Only loading is supported for this format.
  25. Redguard stores multiple images in one file (usually related like textures for various
  26. parts of single 3d object). Image data is stored as 8bit. Each image
  27. can have its own embedded palette or it can use external palette.
  28. Default palette (fxart\Redguard.col) is applied to textures without the embedded one
  29. although some texture sets look like their external pal is different (see more *.col
  30. files in fxart).
  31. BattleSpire uses 15bit palette and some lighting data, also some of the images
  32. are RLE compressed. Multiple frames can also be stored in BSI
  33. (multiple frames vs images in Redguard). BSI in BattleSpire are stored in BSA archive -> this
  34. version of BSA support compression of files so you need BSA extractor
  35. which takes BSpire version in account.
  36. Working BSA extractor and BSI description: https://github.com/ariscop/battlespire-tools.}
  37. TBSIFileFormat = class(TElderFileFormat)
  38. private
  39. function IsMultiBSI(Handle: TImagingHandle): Boolean;
  40. procedure ConvertHICLToPalette(HICL: PWordArray; Pal: PPalette32);
  41. protected
  42. procedure Define; override;
  43. function LoadData(Handle: TImagingHandle; var Images: TDynImageDataArray;
  44. OnlyFirstLevel: Boolean): Boolean; override;
  45. public
  46. function TestFormat(Handle: TImagingHandle): Boolean; override;
  47. end;
  48. implementation
  49. const
  50. SBSIFormatName = 'Bethesda Image';
  51. SBSIMasks = '*.bsi,texbsi.*';
  52. resourcestring
  53. SErrorLoadingChunk = 'Error when reading %s chunk data.';
  54. type
  55. { BSI chunk header.}
  56. TChunk = packed record
  57. ChunkID: TChar4;
  58. DataSize: UInt32; // In Big Endian!
  59. end;
  60. { Additional header of BSI textures.}
  61. TTextureBSIHeader = packed record
  62. Name: array[0..8] of AnsiChar;
  63. ImageSize: UInt32;
  64. end;
  65. { Main image info header located in BHDR chunk's data.}
  66. TBHDRChunk = packed record
  67. OffsetX: Int16;
  68. OffsetY: Int16;
  69. Width: Int16;
  70. Height: Int16;
  71. Unk1, Unk2: Byte;
  72. Unk3, Unk4: Word;
  73. Frames: Word;
  74. Unk6, Unk7, Unk8: Word;
  75. Unk9, Unk10: Byte;
  76. Compression: Word;
  77. end;
  78. const
  79. IFHDSignature: TChar4 = 'IFHD';
  80. BSIFSignature: TChar4 = 'BSIF';
  81. BHDRSignature: TChar4 = 'BHDR';
  82. CMAPSignature: TChar4 = 'CMAP';
  83. HICLSignature: TChar4 = 'HICL';
  84. HTBLSignature: TChar4 = 'HTBL';
  85. DATASignature: TChar4 = 'DATA';
  86. ENDSignature: TChar4 = 'END ';
  87. { TBSIFileFormat class implementation }
  88. procedure TBSIFileFormat.Define;
  89. begin
  90. inherited;
  91. FName := SBSIFormatName;
  92. FFeatures := [ffLoad, ffMultiImage];
  93. AddMasks(SBSIMasks);
  94. SetPalette(RedguardPalette);
  95. end;
  96. function TBSIFileFormat.IsMultiBSI(Handle: TImagingHandle): Boolean;
  97. var
  98. ReadCount, StartPos: LongInt;
  99. Sig: TChar4;
  100. begin
  101. Result := False;
  102. if Handle <> nil then
  103. with GetIO do
  104. begin
  105. StartPos := Tell(Handle);
  106. // Redguard textures have 13 byte tex header and then IFHD or BSIF
  107. Seek(Handle, SizeOf(TTextureBSIHeader), smFromCurrent);
  108. ReadCount := Read(Handle, @Sig, SizeOf(Sig));
  109. Seek(Handle, StartPos, smFromBeginning);
  110. Result := Result or ((ReadCount = SizeOf(Sig)) and
  111. ((Sig = IFHDSignature) or (Sig = BSIFSignature)));
  112. end;
  113. end;
  114. procedure TBSIFileFormat.ConvertHICLToPalette(HICL: PWordArray; Pal: PPalette32);
  115. var
  116. I, Idx: Integer;
  117. Col: Word;
  118. begin
  119. for I := 0 to 127 do
  120. begin
  121. // HICL is 256B in size with 128 word colors (R5G5B5).
  122. // Indices in DATA chunk are scaled to full 8 bits.
  123. // So either multiply pal entries by 2 or divide every index in DATA by 2 =>
  124. // since 128 <<< size(DATA) we modify the palette.
  125. Col := HICL[I];
  126. Idx := I * 2;
  127. Pal[Idx].A := 255;
  128. Pal[Idx].R := MulDiv(((Col shr 11) and 31), 255, 31);
  129. Pal[Idx].G := MulDiv(((Col shr 6) and 31), 255, 31);
  130. Pal[Idx].B := MulDiv(((Col shr 1) and 31), 255, 31);
  131. end;
  132. end;
  133. function TBSIFileFormat.LoadData(Handle: TImagingHandle;
  134. var Images: TDynImageDataArray; OnlyFirstLevel: Boolean): Boolean;
  135. var
  136. Chunk: TChunk;
  137. ChunkData: Pointer;
  138. BHDR: TBHDRChunk;
  139. PalLoaded: TPalette24Size256;
  140. HICL: PWordArray;
  141. HTBL: PByteArray;
  142. IsMulti: Boolean;
  143. TextureHdr: TTextureBSIHeader;
  144. PaletteFound: Boolean;
  145. procedure ReadChunk;
  146. begin
  147. GetIO.Read(Handle, @Chunk, SizeOf(Chunk));
  148. Chunk.DataSize := SwapEndianUInt32(Chunk.DataSize);
  149. end;
  150. procedure ReadChunkData;
  151. var
  152. ReadBytes: NativeInt;
  153. begin
  154. FreeMemNil(ChunkData);
  155. GetMem(ChunkData, Chunk.DataSize);
  156. ReadBytes := GetIO.Read(Handle, ChunkData, Chunk.DataSize);
  157. if ReadBytes <> Chunk.DataSize then
  158. RaiseImaging(SErrorLoadingChunk, [Chunk.ChunkID]);
  159. end;
  160. procedure SkipChunkData;
  161. begin
  162. GetIO.Seek(Handle, Chunk.DataSize, smFromCurrent);
  163. end;
  164. procedure GetBHDR;
  165. begin
  166. ReadChunkData;
  167. BHDR := TBHDRChunk(ChunkData^);
  168. end;
  169. procedure GetHICL;
  170. begin
  171. ReadChunkData;
  172. GetMem(HICL, Chunk.DataSize);
  173. Move(ChunkData^, HICL[0], Chunk.DataSize);
  174. end;
  175. procedure GetHTBL;
  176. begin
  177. ReadChunkData;
  178. GetMem(HTBL, Chunk.DataSize);
  179. Move(ChunkData^, HTBL[0], Chunk.DataSize);
  180. end;
  181. procedure GetCMAP;
  182. begin
  183. ReadChunkData;
  184. Move(ChunkData^, PalLoaded, Chunk.DataSize);
  185. PaletteFound := True;
  186. end;
  187. procedure GetDATA;
  188. begin
  189. ReadChunkData;
  190. end;
  191. function AddImage(Width, Height: LongInt): Integer;
  192. begin
  193. Result := Length(Images);
  194. SetLength(Images, Length(Images) + 1);
  195. NewImage(Width, Height, ifIndex8, Images[Result]);
  196. if not PaletteFound then
  197. Move(FARGBPalette[0], Images[Result].Palette[0], Length(FPalette) * SizeOf(TColor32Rec))
  198. else
  199. ConvertPalette(PalLoaded, Images[Result].Palette);
  200. end;
  201. function AddImageHiColor(Width, Height: LongInt; HICL: PWordArray): Integer;
  202. begin
  203. Result := Length(Images);
  204. SetLength(Images, Length(Images) + 1);
  205. NewImage(Width, Height, ifIndex8, Images[Result]);
  206. ConvertHICLToPalette(HICL, Images[Result].Palette);
  207. end;
  208. procedure Reconstruct;
  209. var
  210. ImgIndex, I, J, K: Integer;
  211. RowOffsets: PUInt32Array;
  212. RowPtr: PByte;
  213. Offset: UInt32;
  214. Idx, C: Byte;
  215. IsRleLine, IsRleRun: Boolean;
  216. DestLine: PByte;
  217. Ix, Ir: Integer;
  218. begin
  219. if HICL = nil then
  220. begin
  221. // No HICL data => mostly Redguard images
  222. if BHDR.Frames = 1 then
  223. begin
  224. // Load single image
  225. ImgIndex := AddImage(BHDR.Width, BHDR.Height);
  226. Move(ChunkData^, Images[ImgIndex].Bits^, Images[ImgIndex].Size);
  227. end
  228. else
  229. begin
  230. // Load animated image:
  231. // At the beginning of the chunk data there is BHDR.Height * BHDR.Frames
  232. // 32bit offsets. Each BHDR.Height offsets point to rows of the current frame
  233. RowOffsets := PUInt32Array(ChunkData);
  234. for I := 0 to BHDR.Frames - 1 do
  235. begin
  236. ImgIndex := AddImage(BHDR.Width, BHDR.Height);
  237. with Images[ImgIndex] do
  238. for J := 0 to BHDR.Height - 1 do
  239. Move(PByteArray(ChunkData)[RowOffsets[I * BHDR.Height + J]],
  240. PByteArray(Bits)[J * Width], Width);
  241. end;
  242. end;
  243. end
  244. else
  245. begin
  246. if BHDR.Frames = 1 then
  247. begin
  248. // Load single image
  249. ImgIndex := AddImageHiColor(BHDR.Width, BHDR.Height, HICL);
  250. Move(ChunkData^, Images[ImgIndex].Bits^, Images[ImgIndex].Size);
  251. end
  252. else
  253. begin
  254. // Load animated BattleSpire image, uses offset list just like Redguard
  255. // animated textures (but high word must be zeroed first to get valid offset).
  256. // Frames can also be RLE compressed.
  257. RowOffsets := PUInt32Array(ChunkData);
  258. for I := 0 to BHDR.Frames - 1 do
  259. begin
  260. ImgIndex := AddImageHiColor(BHDR.Width, BHDR.Height, HICL);
  261. if BHDR.Compression = 0 then
  262. begin
  263. with Images[ImgIndex] do
  264. for J := 0 to BHDR.Height - 1 do
  265. for K := 0 to BHDR.Width - 1 do
  266. begin
  267. Idx := PByteArray(ChunkData)[RowOffsets[I * BHDR.Height + J] and $FFFF + K];
  268. PByteArray(Bits)[J * Width + K] := Idx;
  269. end;
  270. end
  271. else
  272. begin
  273. with Images[ImgIndex] do
  274. for J := 0 to BHDR.Height - 1 do
  275. begin
  276. Offset := RowOffsets[I * BHDR.Height + J];
  277. IsRleLine := (Offset and $80000000) = $80000000;
  278. Offset := Offset and $FFFFFF;
  279. RowPtr := @PByteArray(ChunkData)[Offset];
  280. DestLine := @PByteArray(Bits)[J * BHDR.Width];
  281. if not IsRleLine then
  282. begin
  283. Move(PByteArray(ChunkData)[Offset], PByteArray(Bits)[J * Width], Width);
  284. end
  285. else
  286. begin
  287. Ix := 0;
  288. while Ix < Width do
  289. begin
  290. C := RowPtr^;
  291. Inc(RowPtr);
  292. IsRleRun := C >= 128;
  293. C := C and 127;
  294. if IsRleRun then
  295. begin
  296. Idx := RowPtr^;
  297. Inc(RowPtr);
  298. for Ir := 1 to C do
  299. begin
  300. DestLine^ := Idx;
  301. Inc(DestLine);
  302. end;
  303. end
  304. else
  305. begin
  306. for Ir := 1 to C do
  307. begin
  308. Idx := RowPtr^;
  309. Inc(RowPtr);
  310. DestLine^ := Idx;
  311. Inc(DestLine);
  312. end;
  313. end;
  314. Inc(Ix, C);
  315. end;
  316. end;
  317. end;
  318. end;
  319. end;
  320. end;
  321. end;
  322. end;
  323. procedure ReadTextureHeader;
  324. begin
  325. FillChar(TextureHdr, SizeOf(TextureHdr), 0);
  326. if IsMulti then
  327. GetIO.Read(Handle, @TextureHdr, SizeOf(TextureHdr))
  328. else if Length(Images) = 0 then
  329. // Ensure that while loop that reads chunks is executed for
  330. // single-image files
  331. TextureHdr.ImageSize := 1;
  332. end;
  333. begin
  334. ChunkData := nil;
  335. HICL := nil;
  336. HTBL := nil;
  337. SetLength(Images, 0);
  338. IsMulti := IsMultiBSI(Handle);
  339. with GetIO do
  340. begin
  341. // Redguard textures can contain more than one image. Try to read texture
  342. // header and if ImageSize is >0 there is another image.
  343. ReadTextureHeader;
  344. while TextureHdr.ImageSize > 0 do
  345. try
  346. PaletteFound := False;
  347. ReadChunk;
  348. SkipChunkData;
  349. // Read data chunks. If they are recognized their data is stored for
  350. // later image reconstruction
  351. repeat
  352. ReadChunk;
  353. if Chunk.ChunkID = BHDRSignature then
  354. GetBHDR
  355. else if Chunk.ChunkID = HICLSignature then
  356. GetHICL
  357. else if Chunk.ChunkID = HTBLSignature then
  358. GetHTBL
  359. else if Chunk.ChunkID = CMAPSignature then
  360. GetCMAP
  361. else if Chunk.ChunkID = DATASignature then
  362. GetDATA
  363. else
  364. SkipChunkData;
  365. until Eof(Handle) or (Chunk.ChunkID = ENDSignature);
  366. // Reconstruct current image according to data read from chunks
  367. Reconstruct;
  368. // Read header for next image
  369. ReadTextureHeader;
  370. finally
  371. FreeMemNil(ChunkData);
  372. FreeMemNil(HICL);
  373. FreeMemNil(HTBL);
  374. end;
  375. Result := True;
  376. end;
  377. end;
  378. function TBSIFileFormat.TestFormat(Handle: TImagingHandle): Boolean;
  379. var
  380. ReadCount: LongInt;
  381. Sig: TChar4;
  382. begin
  383. // First check if have multi-image BSI file (Redguard textures)
  384. Result := IsMultiBSI(Handle);
  385. if not Result and (Handle <> nil) then
  386. with GetIO do
  387. begin
  388. // Check standard Bettlespire images with IFHD chunk at
  389. // the beginning of the file
  390. ReadCount := Read(Handle, @Sig, SizeOf(Sig));
  391. Seek(Handle, -ReadCount, smFromCurrent);
  392. Result := (ReadCount = SizeOf(Sig)) and (Sig = IFHDSignature);
  393. end;
  394. end;
  395. {
  396. Changes/Bug Fixes:
  397. -- 0.80 -----------------------------------------------------
  398. - BattleSpire images now have correct colors.
  399. -- 0.21 -----------------------------------------------------
  400. - Blue channel of BattleSpire images cracked but others are still unknown.
  401. - Added support for animated BattleSpire images.
  402. - Added support for animated Redguard textures.
  403. - Added support for Redguard textures (Battlespire images still don't figured out).
  404. - Updated to current Imaging version.
  405. -- 0.13 -----------------------------------------------------
  406. - TBSIFileFormat class added
  407. }
  408. end.