| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440 |
- {
- 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.
- }
- { Adds support for the Quite OK Image Format (QOI).
- Based on the QOI specification by Dominic Szablewski https://qoiformat.org. }
- unit ImagingQoi;
- {$I ImagingOptions.inc}
- interface
- uses
- SysUtils, Classes, ImagingTypes, Imaging, ImagingFormats, ImagingIO, ImagingUtility;
- type
- { Class for loading and saving Quite OK Image (QOI) files.
- Supports 3-channel (RGB) and 4-channel (RGBA) images.
- Uses a simple, fast, lossless compression scheme. }
- TQoiFileFormat = class(TImageFileFormat)
- protected
- procedure Define; override;
- function LoadData(Handle: TImagingHandle; var Images: TDynImageDataArray;
- OnlyFirstLevel: Boolean): Boolean; override;
- function SaveData(Handle: TImagingHandle; const Images: TDynImageDataArray;
- Index: LongInt): Boolean; override;
- procedure ConvertToSupported(var Image: TImageData;
- const Info: TImageFormatInfo); override;
- public
- function TestFormat(Handle: TImagingHandle): Boolean; override;
- end;
- implementation
- uses
- ImagingColors;
- const
- SQOIFormatName = 'Quite OK Image';
- SQOIMasks = '*.qoi';
- QOISupportedFormats: TImageFormats = [ifR8G8B8, ifA8R8G8B8];
- const
- // QOI constants from the specification
- QoiMagic = UInt32(Byte('q') or (Byte('o') shl 8) or (Byte('i') shl 16) or (Byte('f') shl 24)); // 'qoif' magic bytes
- QoiHeaderSize = 14;
- QoiPaddingSize = 8; // 7 bytes 0x00, 1 byte 0x01
- QoiMask2Tag = $C0; // 11000000 - Mask for 2-bit tags
- QoiMask2Data = not QoiMask2Tag; // $3F = 00111111 - Mask for "data" of 2-bit tags
- // QOI Opcodes
- // 2-bit tags
- QOI_OP_INDEX = $00; // 00xxxxxx
- QOI_OP_DIFF = $40; // 01xxxxxx
- QOI_OP_LUMA = $80; // 10xxxxxx
- QOI_OP_RUN = $C0; // 11xxxxxx
- // 8-bit tags
- QOI_OP_RGB = $FE; // 11111110
- QOI_OP_RGBA = $FF; // 11111111
- var
- // Padding written at the end of the QOI file
- QoiPadding: array[0..QoiPaddingSize - 1] of Byte = (0, 0, 0, 0, 0, 0, 0, 1);
- type
- TQoiHeader = packed record
- Magic: UInt32; // Magic identifier 'qoif'
- Width: UInt32; // Image width in pixels (Big Endian)
- Height: UInt32; // Image height in pixels (Big Endian)
- Channels: Byte; // 3 = RGB, 4 = RGBA
- Colorspace: Byte; // 0 = sRGB with linear alpha, 1 = all channels linear
- end;
- // Calculates the QOI hash index for a color
- function QoiColorHash(const C: TColor32Rec): Byte; {$IFDEF USE_INLINE}inline;{$ENDIF}
- begin
- Result := (C.R * 3 + C.G * 5 + C.B * 7 + C.A * 11) mod 64;
- end;
- // Swaps header fields between Big Endian (QOI file) and Little Endian (System)
- procedure SwapQoiHeader(var Header: TQoiHeader);
- begin
- // Magic is already correct if read as UInt32 on Little Endian
- Header.Width := SwapEndianUInt32(Header.Width);
- Header.Height := SwapEndianUInt32(Header.Height);
- // Channels and Colorspace are single bytes, no swap needed
- end;
- { TQoiFileFormat implementation }
- procedure TQoiFileFormat.Define;
- begin
- inherited Define;
- FName := SQOIFormatName;
- FFeatures := [ffLoad, ffSave];
- FSupportedFormats := QOISupportedFormats;
- AddMasks(SQOIMasks);
- end;
- function TQoiFileFormat.TestFormat(Handle: TImagingHandle): Boolean;
- var
- Magic: UInt32;
- ReadCount: LongInt;
- begin
- Result := False;
- if Handle <> nil then
- begin
- ReadCount := GetIO.Read(Handle, @Magic, SizeOf(Magic));
- GetIO.Seek(Handle, -ReadCount, smFromCurrent);
- Result := (ReadCount = SizeOf(Magic)) and (Magic = QoiMagic);
- end;
- end;
- function TQoiFileFormat.LoadData(Handle: TImagingHandle;
- var Images: TDynImageDataArray; OnlyFirstLevel: Boolean): Boolean;
- var
- Header: TQoiHeader;
- Stream: TImagingIOStream;
- NumPixels, PixelIndex: NativeInt;
- RunLength: Integer;
- Index: array[0..63] of TColor32Rec; // Running index of recently seen colors
- Pixel, PrevPixel: TColor32Rec;
- DestPtr: PByte;
- B1, B2: Byte;
- DR, DG, DB: Integer; // Differences
- begin
- Result := False;
- SetLength(Images, 1);
- Stream := TImagingIOStream.Create(GetIO, Handle);
- with Images[0] do
- try
- // Read and validate header
- Stream.ReadBuffer(Header, SizeOf(Header));
- if Header.Magic <> QoiMagic then
- Exit;
- SwapQoiHeader(Header); // Convert from Big Endian
- if (Header.Width = 0) or (Header.Height = 0) or
- (Header.Channels < 3) or (Header.Channels > 4) or
- (Header.Colorspace > 1) then
- begin
- Exit; // Invalid header data
- end;
- if Header.Channels = 3 then
- Format := ifR8G8B8
- else
- Format := ifA8R8G8B8;
- if not NewImage(Header.Width, Header.Height, Format, Images[0]) then
- Exit;
- // Initialize decoder state
- FillChar(Index, SizeOf(Index), 0);
- PrevPixel.Color := pcBlack; // Start with opaque black
- Pixel := PrevPixel;
- DestPtr := Bits;
- NumPixels := NativeInt(Header.Width) * Header.Height;
- PixelIndex := 0;
- RunLength := 0;
- while PixelIndex < NumPixels do
- begin
- if RunLength > 0 then // Handle pending run
- begin
- Dec(RunLength);
- end
- else // Read next tag/opcode
- begin
- B1 := Stream.ReadByte;
- if B1 = QOI_OP_RGB then
- begin
- Pixel.R := Stream.ReadByte;
- Pixel.G := Stream.ReadByte;
- Pixel.B := Stream.ReadByte;
- // Alpha remains the same as previous pixel
- end
- else if B1 = QOI_OP_RGBA then
- begin
- Pixel.R := Stream.ReadByte;
- Pixel.G := Stream.ReadByte;
- Pixel.B := Stream.ReadByte;
- Pixel.A := Stream.ReadByte;
- end
- else if (B1 and QoiMask2Tag) = QOI_OP_INDEX then
- begin
- Pixel := Index[B1 and QoiMask2Data];
- end
- else if (B1 and QoiMask2Tag) = QOI_OP_DIFF then
- begin
- DR := ((B1 shr 4) and $03) - 2; // dr = ((B1 >> 4) & 0x03) - 2;
- DG := ((B1 shr 2) and $03) - 2; // dg = ((B1 >> 2) & 0x03) - 2;
- DB := ( B1 and $03) - 2; // db = ( B1 & 0x03) - 2;
- // QOI_OP_DIFF can be the first OP in the file and then
- // DR/DG/DB will be -1 => valid case producing white pixel.
- // So we cannot clamp to byte range here when assigning channels of Pixel
- // or just do nothing like C implementation does (Pascal's range check error).
- // We take lowest 8 bits of the -1 value, producing a 255 byte,
- // alternative to e.g. B:=Byte(Cardinal(-1)).
- Pixel.R := (PrevPixel.R + DR) and $FF;
- Pixel.G := (PrevPixel.G + DG) and $FF;
- Pixel.B := (PrevPixel.B + DB) and $FF;
- // Alpha remains the same
- end
- else if (B1 and QoiMask2Tag) = QOI_OP_LUMA then
- begin
- B2 := Stream.ReadByte;
- DG := (B1 and QoiMask2Data) - 32; // dg = (B1 & 0x3f) - 32;
- DR := DG + ((B2 shr 4) and $0F) - 8; // dr_dg = (B2 >> 4) & 0x0f; dr = dr_dg - 8 + dg;
- DB := DG + ( B2 and $0F) - 8; // db_dg = (B2 ) & 0x0f; db = db_dg - 8 + dg;
- Pixel.R := (PrevPixel.R + DR) and $FF;
- Pixel.G := (PrevPixel.G + DG) and $FF;
- Pixel.B := (PrevPixel.B + DB) and $FF;
- // Alpha remains the same
- end
- else if (B1 and QoiMask2Tag) = QOI_OP_RUN then
- begin
- RunLength := (B1 and QoiMask2Data); // run = (B1 & 0x3f)
- // Pixel value remains the same as previous
- end;
- end; // end read next tag
- Index[QoiColorHash(Pixel)] := Pixel; // Update running index
- if Format = ifA8R8G8B8 then
- begin
- PColor32Rec(DestPtr)^ := Pixel;
- Inc(DestPtr, 4);
- end
- else
- begin
- PColor24Rec(DestPtr)^ := Pixel.Color24Rec;
- Inc(DestPtr, 3);
- end;
- PrevPixel := Pixel;
- Inc(PixelIndex);
- end; // while PixelIndex < PixelsTotal
- Stream.Seek(QoiPaddingSize, soFromCurrent);
- Result := (PixelIndex = NumPixels); // Check if all pixels were decoded
- finally
- Stream.Free;
- end;
- end;
- function TQoiFileFormat.SaveData(Handle: TImagingHandle;
- const Images: TDynImageDataArray; Index: LongInt): Boolean;
- var
- ImageToSave: TImageData;
- Stream: TImagingIOStream;
- MustBeFreed: Boolean;
- Header: TQoiHeader;
- RunLength: Integer;
- NumPixels, PixelIndex: NativeInt;
- ColorsIndex: array[0..63] of TColor32Rec; // Running index of colors
- Pixel, PrevPixel: TColor32Rec;
- SrcPtr: PByte;
- HashIndex: Byte;
- DR, DG, DB: Integer;
- DR_DG, DB_DG : Integer;
- B1, B2: Byte;
- begin
- Result := False;
- MustBeFreed := False;
- // Make image compatible (ifR8G8B8 or ifA8R8G8B8)
- if not MakeCompatible(Images[Index], ImageToSave, MustBeFreed) then
- Exit;
- Stream := TImagingIOStream.Create(GetIO, Handle);
- try
- // Prepare Header
- FillChar(Header, SizeOf(Header), 0);
- Header.Magic := QoiMagic;
- Header.Width := ImageToSave.Width;
- Header.Height := ImageToSave.Height;
- if ImageToSave.Format = ifA8R8G8B8 then
- Header.Channels := 4
- else // ifR8G8B8
- Header.Channels := 3;
- Header.Colorspace := 0; // sRGB (linear alpha assumed by spec)
- SwapQoiHeader(Header); // Convert to Big Endian for file
- Stream.WriteBuffer(Header, SizeOf(Header));
- // Initialize encoder state
- FillChar(ColorsIndex, SizeOf(ColorsIndex), 0);
- PrevPixel.Color := pcBlack;
- Pixel := PrevPixel;
- RunLength := 0;
- SrcPtr := ImageToSave.Bits;
- NumPixels := NativeInt(ImageToSave.Width) * ImageToSave.Height;
- for PixelIndex := 0 to NumPixels - 1 do
- begin
- // Read pixel from source TImageData
- if Header.Channels = 4 then
- begin
- Pixel := PColor32Rec(SrcPtr)^;
- Inc(SrcPtr, 4);
- end
- else // Channels = 3
- begin
- Pixel.Color24Rec := PColor24Rec(SrcPtr)^;
- Pixel.A := PrevPixel.A; // Keep previous alpha for RGB format
- Inc(SrcPtr, 3);
- end;
- // Check for run
- if (Pixel.R = PrevPixel.R) and (Pixel.G = PrevPixel.G) and
- (Pixel.B = PrevPixel.B) and (Pixel.A = PrevPixel.A) then
- begin
- Inc(RunLength);
- if (RunLength = 62) or (PixelIndex = (NumPixels - 1)) then
- begin
- // Max run length or EOF, write QOI_OP_RUN
- B1 := QOI_OP_RUN or (RunLength - 1);
- Stream.WriteByte(B1);
- RunLength := 0;
- end;
- end
- else // Not a run, or run ended
- begin
- // Write any pending run first
- if RunLength > 0 then
- begin
- B1 := QOI_OP_RUN or (RunLength - 1);
- Stream.WriteByte(B1);
- RunLength := 0;
- end;
- // Try other encodings
- HashIndex := QoiColorHash(Pixel);
- if (ColorsIndex[HashIndex].R = Pixel.R) and (ColorsIndex[HashIndex].G = Pixel.G) and
- (ColorsIndex[HashIndex].B = Pixel.B) and (ColorsIndex[HashIndex].A = Pixel.A) then
- begin
- // QOI_OP_INDEX
- B1 := QOI_OP_INDEX or HashIndex;
- Stream.WriteByte(B1);
- end
- else // Not in index, try diff/luma/rgb(a)
- begin
- ColorsIndex[HashIndex] := Pixel; // Update index for next time
- if Pixel.A = PrevPixel.A then // Alpha hasn't changed, try diff/luma
- begin
- DR := Pixel.R - PrevPixel.R;
- DG := Pixel.G - PrevPixel.G;
- DB := Pixel.B - PrevPixel.B;
- DR_DG := DR - DG;
- DB_DG := DB - DG;
- if (DR >= -2) and (DR <= 1) and (DG >= -2) and (DG <= 1) and (DB >= -2) and (DB <= 1) then
- begin
- // QOI_OP_DIFF
- B1 := QOI_OP_DIFF or ((DR + 2) shl 4) or ((DG + 2) shl 2) or (DB + 2);
- Stream.WriteByte(B1);
- end
- else if (DR_DG >= -8) and (DR_DG <= 7) and (DG >= -32) and (DG <= 31) and (DB_DG >= -8) and (DB_DG <= 7) then
- begin
- // QOI_OP_LUMA
- B1 := QOI_OP_LUMA or (DG + 32);
- B2 := ((DR_DG + 8) shl 4) or (DB_DG + 8);
- Stream.WriteByte(B1);
- Stream.WriteByte(B2);
- end
- else // Can't use diff or luma, use RGB
- begin
- B1 := QOI_OP_RGB;
- Stream.WriteByte(B1);
- Stream.WriteByte(Pixel.R);
- Stream.WriteByte(Pixel.G);
- Stream.WriteByte(Pixel.B);
- end;
- end
- else // Alpha changed, use RGBA
- begin
- B1 := QOI_OP_RGBA;
- Stream.WriteByte(B1);
- Stream.WriteByte(Pixel.R);
- Stream.WriteByte(Pixel.G);
- Stream.WriteByte(Pixel.B);
- Stream.WriteByte(Pixel.A);
- end;
- end; // end not in index
- end; // end not a run
- PrevPixel := Pixel;
- end; // for PixelIndex
- // Write the final padding
- Stream.WriteBuffer(QoiPadding, SizeOf(QoiPadding));
- Result := True;
- finally
- Stream.Free;
- if MustBeFreed then
- FreeImage(ImageToSave);
- end;
- end;
- procedure TQoiFileFormat.ConvertToSupported(var Image: TImageData;
- const Info: TImageFormatInfo);
- var
- ConvFormat: TImageFormat;
- begin
- // QOI supports RGB and RGBA. Convert other formats appropriately.
- if Info.HasAlphaChannel or Info.IsIndexed then // Indexed might have transparency
- ConvFormat := ifA8R8G8B8
- else
- ConvFormat := ifR8G8B8;
- ConvertImage(Image, ConvFormat);
- end;
- initialization
- RegisterImageFileFormat(TQoiFileFormat);
- end.
|