ImagingQoi.pas 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440
  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. { Adds support for the Quite OK Image Format (QOI).
  12. Based on the QOI specification by Dominic Szablewski https://qoiformat.org. }
  13. unit ImagingQoi;
  14. {$I ImagingOptions.inc}
  15. interface
  16. uses
  17. SysUtils, Classes, ImagingTypes, Imaging, ImagingFormats, ImagingIO, ImagingUtility;
  18. type
  19. { Class for loading and saving Quite OK Image (QOI) files.
  20. Supports 3-channel (RGB) and 4-channel (RGBA) images.
  21. Uses a simple, fast, lossless compression scheme. }
  22. TQoiFileFormat = class(TImageFileFormat)
  23. protected
  24. procedure Define; override;
  25. function LoadData(Handle: TImagingHandle; var Images: TDynImageDataArray;
  26. OnlyFirstLevel: Boolean): Boolean; override;
  27. function SaveData(Handle: TImagingHandle; const Images: TDynImageDataArray;
  28. Index: LongInt): Boolean; override;
  29. procedure ConvertToSupported(var Image: TImageData;
  30. const Info: TImageFormatInfo); override;
  31. public
  32. function TestFormat(Handle: TImagingHandle): Boolean; override;
  33. end;
  34. implementation
  35. uses
  36. ImagingColors;
  37. const
  38. SQOIFormatName = 'Quite OK Image';
  39. SQOIMasks = '*.qoi';
  40. QOISupportedFormats: TImageFormats = [ifR8G8B8, ifA8R8G8B8];
  41. const
  42. // QOI constants from the specification
  43. QoiMagic = UInt32(Byte('q') or (Byte('o') shl 8) or (Byte('i') shl 16) or (Byte('f') shl 24)); // 'qoif' magic bytes
  44. QoiHeaderSize = 14;
  45. QoiPaddingSize = 8; // 7 bytes 0x00, 1 byte 0x01
  46. QoiMask2Tag = $C0; // 11000000 - Mask for 2-bit tags
  47. QoiMask2Data = not QoiMask2Tag; // $3F = 00111111 - Mask for "data" of 2-bit tags
  48. // QOI Opcodes
  49. // 2-bit tags
  50. QOI_OP_INDEX = $00; // 00xxxxxx
  51. QOI_OP_DIFF = $40; // 01xxxxxx
  52. QOI_OP_LUMA = $80; // 10xxxxxx
  53. QOI_OP_RUN = $C0; // 11xxxxxx
  54. // 8-bit tags
  55. QOI_OP_RGB = $FE; // 11111110
  56. QOI_OP_RGBA = $FF; // 11111111
  57. var
  58. // Padding written at the end of the QOI file
  59. QoiPadding: array[0..QoiPaddingSize - 1] of Byte = (0, 0, 0, 0, 0, 0, 0, 1);
  60. type
  61. TQoiHeader = packed record
  62. Magic: UInt32; // Magic identifier 'qoif'
  63. Width: UInt32; // Image width in pixels (Big Endian)
  64. Height: UInt32; // Image height in pixels (Big Endian)
  65. Channels: Byte; // 3 = RGB, 4 = RGBA
  66. Colorspace: Byte; // 0 = sRGB with linear alpha, 1 = all channels linear
  67. end;
  68. // Calculates the QOI hash index for a color
  69. function QoiColorHash(const C: TColor32Rec): Byte; {$IFDEF USE_INLINE}inline;{$ENDIF}
  70. begin
  71. Result := (C.R * 3 + C.G * 5 + C.B * 7 + C.A * 11) mod 64;
  72. end;
  73. // Swaps header fields between Big Endian (QOI file) and Little Endian (System)
  74. procedure SwapQoiHeader(var Header: TQoiHeader);
  75. begin
  76. // Magic is already correct if read as UInt32 on Little Endian
  77. Header.Width := SwapEndianUInt32(Header.Width);
  78. Header.Height := SwapEndianUInt32(Header.Height);
  79. // Channels and Colorspace are single bytes, no swap needed
  80. end;
  81. { TQoiFileFormat implementation }
  82. procedure TQoiFileFormat.Define;
  83. begin
  84. inherited Define;
  85. FName := SQOIFormatName;
  86. FFeatures := [ffLoad, ffSave];
  87. FSupportedFormats := QOISupportedFormats;
  88. AddMasks(SQOIMasks);
  89. end;
  90. function TQoiFileFormat.TestFormat(Handle: TImagingHandle): Boolean;
  91. var
  92. Magic: UInt32;
  93. ReadCount: LongInt;
  94. begin
  95. Result := False;
  96. if Handle <> nil then
  97. begin
  98. ReadCount := GetIO.Read(Handle, @Magic, SizeOf(Magic));
  99. GetIO.Seek(Handle, -ReadCount, smFromCurrent);
  100. Result := (ReadCount = SizeOf(Magic)) and (Magic = QoiMagic);
  101. end;
  102. end;
  103. function TQoiFileFormat.LoadData(Handle: TImagingHandle;
  104. var Images: TDynImageDataArray; OnlyFirstLevel: Boolean): Boolean;
  105. var
  106. Header: TQoiHeader;
  107. Stream: TImagingIOStream;
  108. NumPixels, PixelIndex: NativeInt;
  109. RunLength: Integer;
  110. Index: array[0..63] of TColor32Rec; // Running index of recently seen colors
  111. Pixel, PrevPixel: TColor32Rec;
  112. DestPtr: PByte;
  113. B1, B2: Byte;
  114. DR, DG, DB: Integer; // Differences
  115. begin
  116. Result := False;
  117. SetLength(Images, 1);
  118. Stream := TImagingIOStream.Create(GetIO, Handle);
  119. with Images[0] do
  120. try
  121. // Read and validate header
  122. Stream.ReadBuffer(Header, SizeOf(Header));
  123. if Header.Magic <> QoiMagic then
  124. Exit;
  125. SwapQoiHeader(Header); // Convert from Big Endian
  126. if (Header.Width = 0) or (Header.Height = 0) or
  127. (Header.Channels < 3) or (Header.Channels > 4) or
  128. (Header.Colorspace > 1) then
  129. begin
  130. Exit; // Invalid header data
  131. end;
  132. if Header.Channels = 3 then
  133. Format := ifR8G8B8
  134. else
  135. Format := ifA8R8G8B8;
  136. if not NewImage(Header.Width, Header.Height, Format, Images[0]) then
  137. Exit;
  138. // Initialize decoder state
  139. FillChar(Index, SizeOf(Index), 0);
  140. PrevPixel.Color := pcBlack; // Start with opaque black
  141. Pixel := PrevPixel;
  142. DestPtr := Bits;
  143. NumPixels := NativeInt(Header.Width) * Header.Height;
  144. PixelIndex := 0;
  145. RunLength := 0;
  146. while PixelIndex < NumPixels do
  147. begin
  148. if RunLength > 0 then // Handle pending run
  149. begin
  150. Dec(RunLength);
  151. end
  152. else // Read next tag/opcode
  153. begin
  154. B1 := Stream.ReadByte;
  155. if B1 = QOI_OP_RGB then
  156. begin
  157. Pixel.R := Stream.ReadByte;
  158. Pixel.G := Stream.ReadByte;
  159. Pixel.B := Stream.ReadByte;
  160. // Alpha remains the same as previous pixel
  161. end
  162. else if B1 = QOI_OP_RGBA then
  163. begin
  164. Pixel.R := Stream.ReadByte;
  165. Pixel.G := Stream.ReadByte;
  166. Pixel.B := Stream.ReadByte;
  167. Pixel.A := Stream.ReadByte;
  168. end
  169. else if (B1 and QoiMask2Tag) = QOI_OP_INDEX then
  170. begin
  171. Pixel := Index[B1 and QoiMask2Data];
  172. end
  173. else if (B1 and QoiMask2Tag) = QOI_OP_DIFF then
  174. begin
  175. DR := ((B1 shr 4) and $03) - 2; // dr = ((B1 >> 4) & 0x03) - 2;
  176. DG := ((B1 shr 2) and $03) - 2; // dg = ((B1 >> 2) & 0x03) - 2;
  177. DB := ( B1 and $03) - 2; // db = ( B1 & 0x03) - 2;
  178. // QOI_OP_DIFF can be the first OP in the file and then
  179. // DR/DG/DB will be -1 => valid case producing white pixel.
  180. // So we cannot clamp to byte range here when assigning channels of Pixel
  181. // or just do nothing like C implementation does (Pascal's range check error).
  182. // We take lowest 8 bits of the -1 value, producing a 255 byte,
  183. // alternative to e.g. B:=Byte(Cardinal(-1)).
  184. Pixel.R := (PrevPixel.R + DR) and $FF;
  185. Pixel.G := (PrevPixel.G + DG) and $FF;
  186. Pixel.B := (PrevPixel.B + DB) and $FF;
  187. // Alpha remains the same
  188. end
  189. else if (B1 and QoiMask2Tag) = QOI_OP_LUMA then
  190. begin
  191. B2 := Stream.ReadByte;
  192. DG := (B1 and QoiMask2Data) - 32; // dg = (B1 & 0x3f) - 32;
  193. DR := DG + ((B2 shr 4) and $0F) - 8; // dr_dg = (B2 >> 4) & 0x0f; dr = dr_dg - 8 + dg;
  194. DB := DG + ( B2 and $0F) - 8; // db_dg = (B2 ) & 0x0f; db = db_dg - 8 + dg;
  195. Pixel.R := (PrevPixel.R + DR) and $FF;
  196. Pixel.G := (PrevPixel.G + DG) and $FF;
  197. Pixel.B := (PrevPixel.B + DB) and $FF;
  198. // Alpha remains the same
  199. end
  200. else if (B1 and QoiMask2Tag) = QOI_OP_RUN then
  201. begin
  202. RunLength := (B1 and QoiMask2Data); // run = (B1 & 0x3f)
  203. // Pixel value remains the same as previous
  204. end;
  205. end; // end read next tag
  206. Index[QoiColorHash(Pixel)] := Pixel; // Update running index
  207. if Format = ifA8R8G8B8 then
  208. begin
  209. PColor32Rec(DestPtr)^ := Pixel;
  210. Inc(DestPtr, 4);
  211. end
  212. else
  213. begin
  214. PColor24Rec(DestPtr)^ := Pixel.Color24Rec;
  215. Inc(DestPtr, 3);
  216. end;
  217. PrevPixel := Pixel;
  218. Inc(PixelIndex);
  219. end; // while PixelIndex < PixelsTotal
  220. Stream.Seek(QoiPaddingSize, soFromCurrent);
  221. Result := (PixelIndex = NumPixels); // Check if all pixels were decoded
  222. finally
  223. Stream.Free;
  224. end;
  225. end;
  226. function TQoiFileFormat.SaveData(Handle: TImagingHandle;
  227. const Images: TDynImageDataArray; Index: LongInt): Boolean;
  228. var
  229. ImageToSave: TImageData;
  230. Stream: TImagingIOStream;
  231. MustBeFreed: Boolean;
  232. Header: TQoiHeader;
  233. RunLength: Integer;
  234. NumPixels, PixelIndex: NativeInt;
  235. ColorsIndex: array[0..63] of TColor32Rec; // Running index of colors
  236. Pixel, PrevPixel: TColor32Rec;
  237. SrcPtr: PByte;
  238. HashIndex: Byte;
  239. DR, DG, DB: Integer;
  240. DR_DG, DB_DG : Integer;
  241. B1, B2: Byte;
  242. begin
  243. Result := False;
  244. MustBeFreed := False;
  245. // Make image compatible (ifR8G8B8 or ifA8R8G8B8)
  246. if not MakeCompatible(Images[Index], ImageToSave, MustBeFreed) then
  247. Exit;
  248. Stream := TImagingIOStream.Create(GetIO, Handle);
  249. try
  250. // Prepare Header
  251. FillChar(Header, SizeOf(Header), 0);
  252. Header.Magic := QoiMagic;
  253. Header.Width := ImageToSave.Width;
  254. Header.Height := ImageToSave.Height;
  255. if ImageToSave.Format = ifA8R8G8B8 then
  256. Header.Channels := 4
  257. else // ifR8G8B8
  258. Header.Channels := 3;
  259. Header.Colorspace := 0; // sRGB (linear alpha assumed by spec)
  260. SwapQoiHeader(Header); // Convert to Big Endian for file
  261. Stream.WriteBuffer(Header, SizeOf(Header));
  262. // Initialize encoder state
  263. FillChar(ColorsIndex, SizeOf(ColorsIndex), 0);
  264. PrevPixel.Color := pcBlack;
  265. Pixel := PrevPixel;
  266. RunLength := 0;
  267. SrcPtr := ImageToSave.Bits;
  268. NumPixels := NativeInt(ImageToSave.Width) * ImageToSave.Height;
  269. for PixelIndex := 0 to NumPixels - 1 do
  270. begin
  271. // Read pixel from source TImageData
  272. if Header.Channels = 4 then
  273. begin
  274. Pixel := PColor32Rec(SrcPtr)^;
  275. Inc(SrcPtr, 4);
  276. end
  277. else // Channels = 3
  278. begin
  279. Pixel.Color24Rec := PColor24Rec(SrcPtr)^;
  280. Pixel.A := PrevPixel.A; // Keep previous alpha for RGB format
  281. Inc(SrcPtr, 3);
  282. end;
  283. // Check for run
  284. if (Pixel.R = PrevPixel.R) and (Pixel.G = PrevPixel.G) and
  285. (Pixel.B = PrevPixel.B) and (Pixel.A = PrevPixel.A) then
  286. begin
  287. Inc(RunLength);
  288. if (RunLength = 62) or (PixelIndex = (NumPixels - 1)) then
  289. begin
  290. // Max run length or EOF, write QOI_OP_RUN
  291. B1 := QOI_OP_RUN or (RunLength - 1);
  292. Stream.WriteByte(B1);
  293. RunLength := 0;
  294. end;
  295. end
  296. else // Not a run, or run ended
  297. begin
  298. // Write any pending run first
  299. if RunLength > 0 then
  300. begin
  301. B1 := QOI_OP_RUN or (RunLength - 1);
  302. Stream.WriteByte(B1);
  303. RunLength := 0;
  304. end;
  305. // Try other encodings
  306. HashIndex := QoiColorHash(Pixel);
  307. if (ColorsIndex[HashIndex].R = Pixel.R) and (ColorsIndex[HashIndex].G = Pixel.G) and
  308. (ColorsIndex[HashIndex].B = Pixel.B) and (ColorsIndex[HashIndex].A = Pixel.A) then
  309. begin
  310. // QOI_OP_INDEX
  311. B1 := QOI_OP_INDEX or HashIndex;
  312. Stream.WriteByte(B1);
  313. end
  314. else // Not in index, try diff/luma/rgb(a)
  315. begin
  316. ColorsIndex[HashIndex] := Pixel; // Update index for next time
  317. if Pixel.A = PrevPixel.A then // Alpha hasn't changed, try diff/luma
  318. begin
  319. DR := Pixel.R - PrevPixel.R;
  320. DG := Pixel.G - PrevPixel.G;
  321. DB := Pixel.B - PrevPixel.B;
  322. DR_DG := DR - DG;
  323. DB_DG := DB - DG;
  324. if (DR >= -2) and (DR <= 1) and (DG >= -2) and (DG <= 1) and (DB >= -2) and (DB <= 1) then
  325. begin
  326. // QOI_OP_DIFF
  327. B1 := QOI_OP_DIFF or ((DR + 2) shl 4) or ((DG + 2) shl 2) or (DB + 2);
  328. Stream.WriteByte(B1);
  329. end
  330. else if (DR_DG >= -8) and (DR_DG <= 7) and (DG >= -32) and (DG <= 31) and (DB_DG >= -8) and (DB_DG <= 7) then
  331. begin
  332. // QOI_OP_LUMA
  333. B1 := QOI_OP_LUMA or (DG + 32);
  334. B2 := ((DR_DG + 8) shl 4) or (DB_DG + 8);
  335. Stream.WriteByte(B1);
  336. Stream.WriteByte(B2);
  337. end
  338. else // Can't use diff or luma, use RGB
  339. begin
  340. B1 := QOI_OP_RGB;
  341. Stream.WriteByte(B1);
  342. Stream.WriteByte(Pixel.R);
  343. Stream.WriteByte(Pixel.G);
  344. Stream.WriteByte(Pixel.B);
  345. end;
  346. end
  347. else // Alpha changed, use RGBA
  348. begin
  349. B1 := QOI_OP_RGBA;
  350. Stream.WriteByte(B1);
  351. Stream.WriteByte(Pixel.R);
  352. Stream.WriteByte(Pixel.G);
  353. Stream.WriteByte(Pixel.B);
  354. Stream.WriteByte(Pixel.A);
  355. end;
  356. end; // end not in index
  357. end; // end not a run
  358. PrevPixel := Pixel;
  359. end; // for PixelIndex
  360. // Write the final padding
  361. Stream.WriteBuffer(QoiPadding, SizeOf(QoiPadding));
  362. Result := True;
  363. finally
  364. Stream.Free;
  365. if MustBeFreed then
  366. FreeImage(ImageToSave);
  367. end;
  368. end;
  369. procedure TQoiFileFormat.ConvertToSupported(var Image: TImageData;
  370. const Info: TImageFormatInfo);
  371. var
  372. ConvFormat: TImageFormat;
  373. begin
  374. // QOI supports RGB and RGBA. Convert other formats appropriately.
  375. if Info.HasAlphaChannel or Info.IsIndexed then // Indexed might have transparency
  376. ConvFormat := ifA8R8G8B8
  377. else
  378. ConvFormat := ifR8G8B8;
  379. ConvertImage(Image, ConvFormat);
  380. end;
  381. initialization
  382. RegisterImageFileFormat(TQoiFileFormat);
  383. end.