989 lines
31 KiB
Plaintext
989 lines
31 KiB
Plaintext
{
|
|
$Id: ImagingGif.pas 111 2007-12-02 23:25:44Z galfar $
|
|
Vampyre Imaging Library
|
|
by Marek Mauder
|
|
http://imaginglib.sourceforge.net
|
|
|
|
The contents of this file are used with permission, subject to the Mozilla
|
|
Public License Version 1.1 (the "License"); you may not use this file except
|
|
in compliance with the License. You may obtain a copy of the License at
|
|
http://www.mozilla.org/MPL/MPL-1.1.html
|
|
|
|
Software distributed under the License is distributed on an "AS IS" basis,
|
|
WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for
|
|
the specific language governing rights and limitations under the License.
|
|
|
|
Alternatively, the contents of this file may be used under the terms of the
|
|
GNU Lesser General Public License (the "LGPL License"), in which case the
|
|
provisions of the LGPL License are applicable instead of those above.
|
|
If you wish to allow use of your version of this file only under the terms
|
|
of the LGPL License and not to allow others to use your version of this file
|
|
under the MPL, indicate your decision by deleting the provisions above and
|
|
replace them with the notice and other provisions required by the LGPL
|
|
License. If you do not delete the provisions above, a recipient may use
|
|
your version of this file under either the MPL or the LGPL License.
|
|
|
|
For more information about the LGPL: http://www.gnu.org/copyleft/lesser.html
|
|
}
|
|
|
|
{ This unit contains image format loader/saver for GIF images.}
|
|
unit ImagingGif;
|
|
|
|
{$I ImagingOptions.inc}
|
|
|
|
interface
|
|
|
|
uses
|
|
SysUtils, Classes, Imaging, ImagingTypes, ImagingUtility;
|
|
|
|
type
|
|
{ GIF (Graphics Interchange Format) loader/saver class. GIF was
|
|
(and is still used) popular format for storing images supporting
|
|
multiple images per file and single color transparency.
|
|
Pixel format is 8 bit indexed where each image frame can have
|
|
its own color palette. GIF uses lossless LZW compression
|
|
(patent expired few years ago).
|
|
Imaging can load and save all GIFs with all frames and supports
|
|
transparency.}
|
|
TGIFFileFormat = class(TImageFileFormat)
|
|
private
|
|
function InterlaceStep(Y, Height: Integer; var Pass: Integer): Integer;
|
|
procedure LZWDecompress(const IO: TIOFunctions; Handle: TImagingHandle;
|
|
Width, Height: Integer; Interlaced: Boolean; Data: Pointer);
|
|
procedure LZWCompress(const IO: TIOFunctions; Handle: TImagingHandle;
|
|
Width, Height, BitCount: Integer; Interlaced: Boolean; Data: Pointer);
|
|
protected
|
|
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
|
|
constructor Create; override;
|
|
function TestFormat(Handle: TImagingHandle): Boolean; override;
|
|
end;
|
|
|
|
implementation
|
|
|
|
const
|
|
SGIFFormatName = 'Graphics Interchange Format';
|
|
SGIFMasks = '*.gif';
|
|
GIFSupportedFormats: TImageFormats = [ifIndex8];
|
|
|
|
type
|
|
TGIFVersion = (gv87, gv89);
|
|
TDisposalMethod = (dmUndefined, dmLeave, dmRestoreBackground,
|
|
dmRestorePrevious, dmReserved4, dmReserved5, dmReserved6, dmReserved7);
|
|
|
|
const
|
|
GIFSignature: TChar3 = 'GIF';
|
|
GIFVersions: array[TGIFVersion] of TChar3 = ('87a', '89a');
|
|
|
|
// Masks for accessing fields in PackedFields of TGIFHeader
|
|
GIFGlobalColorTable = $80;
|
|
GIFColorResolution = $70;
|
|
GIFColorTableSorted = $08;
|
|
GIFColorTableSize = $07;
|
|
|
|
// Masks for accessing fields in PackedFields of TImageDescriptor
|
|
GIFLocalColorTable = $80;
|
|
GIFInterlaced = $40;
|
|
GIFLocalTableSorted = $20;
|
|
|
|
// Block identifiers
|
|
GIFPlainText: Byte = $01;
|
|
GIFGraphicControlExtension: Byte = $F9;
|
|
GIFCommentExtension: Byte = $FE;
|
|
GIFApplicationExtension: Byte = $FF;
|
|
GIFImageDescriptor: Byte = Ord(',');
|
|
GIFExtensionIntroducer: Byte = Ord('!');
|
|
GIFTrailer: Byte = Ord(';');
|
|
GIFBlockTerminator: Byte = $00;
|
|
|
|
// Masks for accessing fields in PackedFields of TGraphicControlExtension
|
|
GIFTransparent = $01;
|
|
GIFUserInput = $02;
|
|
GIFDisposalMethod = $1C;
|
|
|
|
type
|
|
TGIFHeader = packed record
|
|
// File header part
|
|
Signature: TChar3; // Header Signature (always "GIF")
|
|
Version: TChar3; // GIF format version("87a" or "89a")
|
|
// Logical Screen Descriptor part
|
|
ScreenWidth: Word; // Width of Display Screen in Pixels
|
|
ScreenHeight: Word; // Height of Display Screen in Pixels
|
|
PackedFields: Byte; // Screen and color map information
|
|
BackgroundColorIndex: Byte; // Background color index (in global color table)
|
|
AspectRatio: Byte; // Pixel aspect ratio, ratio = (AspectRatio + 15) / 64
|
|
end;
|
|
|
|
TImageDescriptor = packed record
|
|
//Separator: Byte; // leave that out since we always read one bye ahead
|
|
Left: Word; // X position of image with respect to logical screen
|
|
Top: Word; // Y position
|
|
Width: Word;
|
|
Height: Word;
|
|
PackedFields: Byte;
|
|
end;
|
|
|
|
const
|
|
// GIF extension labels
|
|
GIFExtTypeGraphic = $F9;
|
|
GIFExtTypePlainText = $01;
|
|
GIFExtTypeApplication = $FF;
|
|
GIFExtTypeComment = $FE;
|
|
|
|
type
|
|
TGraphicControlExtension = packed record
|
|
BlockSize: Byte;
|
|
PackedFields: Byte;
|
|
DelayTime: Word;
|
|
TransparentColorIndex: Byte;
|
|
Terminator: Byte;
|
|
end;
|
|
|
|
const
|
|
CodeTableSize = 4096;
|
|
HashTableSize = 17777;
|
|
|
|
type
|
|
TReadContext = record
|
|
Inx: Integer;
|
|
Size: Integer;
|
|
Buf: array [0..255 + 4] of Byte;
|
|
CodeSize: Integer;
|
|
ReadMask: Integer;
|
|
end;
|
|
PReadContext = ^TReadContext;
|
|
|
|
TWriteContext = record
|
|
Inx: Integer;
|
|
CodeSize: Integer;
|
|
Buf: array [0..255 + 4] of Byte;
|
|
end;
|
|
PWriteContext = ^TWriteContext;
|
|
|
|
TOutputContext = record
|
|
W: Integer;
|
|
H: Integer;
|
|
X: Integer;
|
|
Y: Integer;
|
|
BitsPerPixel: Integer;
|
|
Pass: Integer;
|
|
Interlace: Boolean;
|
|
LineIdent: Integer;
|
|
Data: Pointer;
|
|
CurrLineData: Pointer;
|
|
end;
|
|
|
|
TImageDict = record
|
|
Tail: Word;
|
|
Index: Word;
|
|
Col: Byte;
|
|
end;
|
|
PImageDict = ^TImageDict;
|
|
|
|
PIntCodeTable = ^TIntCodeTable;
|
|
TIntCodeTable = array [0..CodeTableSize - 1] of Word;
|
|
|
|
TDictTable = array [0..CodeTableSize - 1] of TImageDict;
|
|
PDictTable = ^TDictTable;
|
|
|
|
resourcestring
|
|
SGIFDecodingError = 'Error when decoding GIF LZW data';
|
|
|
|
{
|
|
TGIFFileFormat implementation
|
|
}
|
|
|
|
constructor TGIFFileFormat.Create;
|
|
begin
|
|
inherited Create;
|
|
FName := SGIFFormatName;
|
|
FCanLoad := True;
|
|
FCanSave := True;
|
|
FIsMultiImageFormat := True;
|
|
FSupportedFormats := GIFSupportedFormats;
|
|
|
|
AddMasks(SGIFMasks);
|
|
end;
|
|
|
|
function TGIFFileFormat.InterlaceStep(Y, Height: Integer; var Pass: Integer): Integer;
|
|
begin
|
|
Result := Y;
|
|
case Pass of
|
|
0, 1:
|
|
Inc(Result, 8);
|
|
2:
|
|
Inc(Result, 4);
|
|
3:
|
|
Inc(Result, 2);
|
|
end;
|
|
if Result >= Height then
|
|
begin
|
|
if Pass = 0 then
|
|
begin
|
|
Pass := 1;
|
|
Result := 4;
|
|
if Result < Height then
|
|
Exit;
|
|
end;
|
|
if Pass = 1 then
|
|
begin
|
|
Pass := 2;
|
|
Result := 2;
|
|
if Result < Height then
|
|
Exit;
|
|
end;
|
|
if Pass = 2 then
|
|
begin
|
|
Pass := 3;
|
|
Result := 1;
|
|
end;
|
|
end;
|
|
end;
|
|
|
|
{ GIF LZW decompresion code is from JVCL JvGIF.pas unit.}
|
|
procedure TGIFFileFormat.LZWDecompress(const IO: TIOFunctions; Handle: TImagingHandle; Width, Height: Integer;
|
|
Interlaced: Boolean; Data: Pointer);
|
|
var
|
|
MinCodeSize: Byte;
|
|
MaxCode, BitMask, InitCodeSize: Integer;
|
|
ClearCode, EndingCode, FirstFreeCode, FreeCode: Word;
|
|
I, OutCount, Code: Integer;
|
|
CurCode, OldCode, InCode, FinalChar: Word;
|
|
Prefix, Suffix, OutCode: PIntCodeTable;
|
|
ReadCtxt: TReadContext;
|
|
OutCtxt: TOutputContext;
|
|
TableFull: Boolean;
|
|
|
|
function ReadCode(var Context: TReadContext): Integer;
|
|
var
|
|
RawCode: Integer;
|
|
ByteIndex: Integer;
|
|
Bytes: Byte;
|
|
BytesToLose: Integer;
|
|
begin
|
|
while Context.Inx + Context.CodeSize > Context.Size do
|
|
begin
|
|
// Not enough bits in buffer - refill it - Not very efficient, but infrequently called
|
|
BytesToLose := Context.Inx shr 3;
|
|
// Note biggest Code Size is 12 bits. And this can at worst span 3 Bytes
|
|
Move(Context.Buf[Word(BytesToLose)], Context.Buf[0], 3);
|
|
Context.Inx := Context.Inx and 7;
|
|
Context.Size := Context.Size - (BytesToLose shl 3);
|
|
IO.Read(Handle, @Bytes, 1);
|
|
if Bytes > 0 then
|
|
IO.Read(Handle, @Context.Buf[Word(Context.Size shr 3)], Bytes);
|
|
Context.Size := Context.Size + (Bytes shl 3);
|
|
end;
|
|
ByteIndex := Context.Inx shr 3;
|
|
RawCode := Context.Buf[Word(ByteIndex)] +
|
|
(Word(Context.Buf[Word(ByteIndex + 1)]) shl 8);
|
|
if Context.CodeSize > 8 then
|
|
RawCode := RawCode + (Longint(Context.Buf[ByteIndex + 2]) shl 16);
|
|
RawCode := RawCode shr (Context.Inx and 7);
|
|
Context.Inx := Context.Inx + Byte(Context.CodeSize);
|
|
Result := RawCode and Context.ReadMask;
|
|
end;
|
|
|
|
procedure Output(Value: Byte; var Context: TOutputContext);
|
|
var
|
|
P: PByte;
|
|
begin
|
|
if Context.Y >= Context.H then
|
|
Exit;
|
|
|
|
// Only ifIndex8 supported
|
|
P := @PByteArray(Context.CurrLineData)[Context.X];
|
|
P^ := Value;
|
|
|
|
{case Context.BitsPerPixel of
|
|
1:
|
|
begin
|
|
P := @PByteArray(Context.CurrLineData)[Context.X shr 3];
|
|
if (Context.X and $07) <> 0 then
|
|
P^ := P^ or Word(Value shl (7 - (Word(Context.X and 7))))
|
|
else
|
|
P^ := Byte(Value shl 7);
|
|
end;
|
|
4:
|
|
begin
|
|
P := @PByteArray(Context.CurrLineData)[Context.X shr 1];
|
|
if (Context.X and 1) <> 0 then
|
|
P^ := P^ or Value
|
|
else
|
|
P^ := Byte(Value shl 4);
|
|
end;
|
|
8:
|
|
begin
|
|
P := @PByteArray(Context.CurrLineData)[Context.X];
|
|
P^ := Value;
|
|
end;
|
|
end;}
|
|
Inc(Context.X);
|
|
|
|
if Context.X < Context.W then
|
|
Exit;
|
|
Context.X := 0;
|
|
if Context.Interlace then
|
|
Context.Y := InterlaceStep(Context.Y, Context.H, Context.Pass)
|
|
else
|
|
Inc(Context.Y);
|
|
|
|
Context.CurrLineData := @PByteArray(Context.Data)[Context.Y * Context.LineIdent];
|
|
end;
|
|
|
|
begin
|
|
OutCount := 0;
|
|
OldCode := 0;
|
|
FinalChar := 0;
|
|
TableFull := False;
|
|
GetMem(Prefix, SizeOf(TIntCodeTable));
|
|
GetMem(Suffix, SizeOf(TIntCodeTable));
|
|
GetMem(OutCode, SizeOf(TIntCodeTable) + SizeOf(Word));
|
|
try
|
|
IO.Read(Handle, @MinCodeSize, 1);
|
|
if (MinCodeSize < 2) or (MinCodeSize > 9) then
|
|
RaiseImaging(SGIFDecodingError, []);
|
|
// Initial read context
|
|
ReadCtxt.Inx := 0;
|
|
ReadCtxt.Size := 0;
|
|
ReadCtxt.CodeSize := MinCodeSize + 1;
|
|
ReadCtxt.ReadMask := (1 shl ReadCtxt.CodeSize) - 1;
|
|
// Initialise pixel-output context
|
|
OutCtxt.X := 0;
|
|
OutCtxt.Y := 0;
|
|
OutCtxt.Pass := 0;
|
|
OutCtxt.W := Width;
|
|
OutCtxt.H := Height;
|
|
OutCtxt.BitsPerPixel := MinCodeSize;
|
|
OutCtxt.Interlace := Interlaced;
|
|
OutCtxt.LineIdent := Width;
|
|
OutCtxt.Data := Data;
|
|
OutCtxt.CurrLineData := Data;
|
|
BitMask := (1 shl OutCtxt.BitsPerPixel) - 1;
|
|
// 2 ^ MinCodeSize accounts for all colours in file
|
|
ClearCode := 1 shl MinCodeSize;
|
|
EndingCode := ClearCode + 1;
|
|
FreeCode := ClearCode + 2;
|
|
FirstFreeCode := FreeCode;
|
|
// 2^ (MinCodeSize + 1) includes clear and eoi Code and space too
|
|
InitCodeSize := ReadCtxt.CodeSize;
|
|
MaxCode := 1 shl ReadCtxt.CodeSize;
|
|
Code := ReadCode(ReadCtxt);
|
|
while (Code <> EndingCode) and (Code <> $FFFF) and
|
|
(OutCtxt.Y < OutCtxt.H) do
|
|
begin
|
|
if Code = ClearCode then
|
|
begin
|
|
ReadCtxt.CodeSize := InitCodeSize;
|
|
MaxCode := 1 shl ReadCtxt.CodeSize;
|
|
ReadCtxt.ReadMask := MaxCode - 1;
|
|
FreeCode := FirstFreeCode;
|
|
Code := ReadCode(ReadCtxt);
|
|
CurCode := Code;
|
|
OldCode := Code;
|
|
if Code = $FFFF then
|
|
Break;
|
|
FinalChar := (CurCode and BitMask);
|
|
Output(Byte(FinalChar), OutCtxt);
|
|
TableFull := False;
|
|
end
|
|
else
|
|
begin
|
|
CurCode := Code;
|
|
InCode := Code;
|
|
if CurCode >= FreeCode then
|
|
begin
|
|
CurCode := OldCode;
|
|
OutCode^[OutCount] := FinalChar;
|
|
Inc(OutCount);
|
|
end;
|
|
while CurCode > BitMask do
|
|
begin
|
|
if OutCount > CodeTableSize then
|
|
RaiseImaging(SGIFDecodingError, []);
|
|
OutCode^[OutCount] := Suffix^[CurCode];
|
|
Inc(OutCount);
|
|
CurCode := Prefix^[CurCode];
|
|
end;
|
|
|
|
FinalChar := CurCode and BitMask;
|
|
OutCode^[OutCount] := FinalChar;
|
|
Inc(OutCount);
|
|
for I := OutCount - 1 downto 0 do
|
|
Output(Byte(OutCode^[I]), OutCtxt);
|
|
OutCount := 0;
|
|
// Update dictionary
|
|
if not TableFull then
|
|
begin
|
|
Prefix^[FreeCode] := OldCode;
|
|
Suffix^[FreeCode] := FinalChar;
|
|
// Advance to next free slot
|
|
Inc(FreeCode);
|
|
if FreeCode >= MaxCode then
|
|
begin
|
|
if ReadCtxt.CodeSize < 12 then
|
|
begin
|
|
Inc(ReadCtxt.CodeSize);
|
|
MaxCode := MaxCode shl 1;
|
|
ReadCtxt.ReadMask := (1 shl ReadCtxt.CodeSize) - 1;
|
|
end
|
|
else
|
|
TableFull := True;
|
|
end;
|
|
end;
|
|
OldCode := InCode;
|
|
end;
|
|
Code := ReadCode(ReadCtxt);
|
|
end;
|
|
if Code = $FFFF then
|
|
RaiseImaging(SGIFDecodingError, []);
|
|
finally
|
|
FreeMem(Prefix);
|
|
FreeMem(OutCode);
|
|
FreeMem(Suffix);
|
|
end;
|
|
end;
|
|
|
|
{ GIF LZW compresion code is from JVCL JvGIF.pas unit.}
|
|
procedure TGIFFileFormat.LZWCompress(const IO: TIOFunctions; Handle: TImagingHandle; Width, Height, BitCount: Integer;
|
|
Interlaced: Boolean; Data: Pointer);
|
|
var
|
|
LineIdent: Integer;
|
|
MinCodeSize, Col: Byte;
|
|
InitCodeSize, X, Y: Integer;
|
|
Pass: Integer;
|
|
MaxCode: Integer; { 1 shl CodeSize }
|
|
ClearCode, EndingCode, LastCode, Tail: Integer;
|
|
I, HashValue: Integer;
|
|
LenString: Word;
|
|
Dict: PDictTable;
|
|
HashTable: TList;
|
|
PData: PByte;
|
|
WriteCtxt: TWriteContext;
|
|
|
|
function InitHash(P: Integer): Integer;
|
|
begin
|
|
Result := (P + 3) * 301;
|
|
end;
|
|
|
|
procedure WriteCode(Code: Integer; var Context: TWriteContext);
|
|
var
|
|
BufIndex: Integer;
|
|
Bytes: Byte;
|
|
begin
|
|
BufIndex := Context.Inx shr 3;
|
|
Code := Code shl (Context.Inx and 7);
|
|
Context.Buf[BufIndex] := Context.Buf[BufIndex] or Byte(Code);
|
|
Context.Buf[BufIndex + 1] := Byte(Code shr 8);
|
|
Context.Buf[BufIndex + 2] := Byte(Code shr 16);
|
|
Context.Inx := Context.Inx + Context.CodeSize;
|
|
if Context.Inx >= 255 * 8 then
|
|
begin
|
|
// Flush out full buffer
|
|
Bytes := 255;
|
|
IO.Write(Handle, @Bytes, 1);
|
|
IO.Write(Handle, @Context.Buf, Bytes);
|
|
Move(Context.Buf[255], Context.Buf[0], 2);
|
|
FillChar(Context.Buf[2], 255, 0);
|
|
Context.Inx := Context.Inx - (255 * 8);
|
|
end;
|
|
end;
|
|
|
|
procedure FlushCode(var Context: TWriteContext);
|
|
var
|
|
Bytes: Byte;
|
|
begin
|
|
Bytes := (Context.Inx + 7) shr 3;
|
|
if Bytes > 0 then
|
|
begin
|
|
IO.Write(Handle, @Bytes, 1);
|
|
IO.Write(Handle, @Context.Buf, Bytes);
|
|
end;
|
|
// Data block terminator - a block of zero Size
|
|
Bytes := 0;
|
|
IO.Write(Handle, @Bytes, 1);
|
|
end;
|
|
|
|
begin
|
|
LineIdent := Width;
|
|
Tail := 0;
|
|
HashValue := 0;
|
|
Col := 0;
|
|
HashTable := TList.Create;
|
|
GetMem(Dict, SizeOf(TDictTable));
|
|
try
|
|
for I := 0 to HashTableSize - 1 do
|
|
HashTable.Add(nil);
|
|
|
|
// Initialise encoder variables
|
|
InitCodeSize := BitCount + 1;
|
|
if InitCodeSize = 2 then
|
|
Inc(InitCodeSize);
|
|
MinCodeSize := InitCodeSize - 1;
|
|
IO.Write(Handle, @MinCodeSize, 1);
|
|
ClearCode := 1 shl MinCodeSize;
|
|
EndingCode := ClearCode + 1;
|
|
LastCode := EndingCode;
|
|
MaxCode := 1 shl InitCodeSize;
|
|
LenString := 0;
|
|
// Setup write context
|
|
WriteCtxt.Inx := 0;
|
|
WriteCtxt.CodeSize := InitCodeSize;
|
|
FillChar(WriteCtxt.Buf, SizeOf(WriteCtxt.Buf), 0);
|
|
WriteCode(ClearCode, WriteCtxt);
|
|
Y := 0;
|
|
Pass := 0;
|
|
|
|
while Y < Height do
|
|
begin
|
|
PData := @PByteArray(Data)[Y * LineIdent];
|
|
for X := 0 to Width - 1 do
|
|
begin
|
|
// Only ifIndex8 support
|
|
case BitCount of
|
|
8:
|
|
begin
|
|
Col := PData^;
|
|
PData := @PByteArray(PData)[1];
|
|
end;
|
|
{4:
|
|
begin
|
|
if X and 1 <> 0 then
|
|
begin
|
|
Col := PData^ and $0F;
|
|
PData := @PByteArray(PData)[1];
|
|
end
|
|
else
|
|
Col := PData^ shr 4;
|
|
end;
|
|
1:
|
|
begin
|
|
if X and 7 = 7 then
|
|
begin
|
|
Col := PData^ and 1;
|
|
PData := @PByteArray(PData)[1];
|
|
end
|
|
else
|
|
Col := (PData^ shr (7 - (X and $07))) and $01;
|
|
end;}
|
|
end;
|
|
Inc(LenString);
|
|
if LenString = 1 then
|
|
begin
|
|
Tail := Col;
|
|
HashValue := InitHash(Col);
|
|
end
|
|
else
|
|
begin
|
|
HashValue := HashValue * (Col + LenString + 4);
|
|
I := HashValue mod HashTableSize;
|
|
HashValue := HashValue mod HashTableSize;
|
|
while (HashTable[I] <> nil) and
|
|
((PImageDict(HashTable[I])^.Tail <> Tail) or
|
|
(PImageDict(HashTable[I])^.Col <> Col)) do
|
|
begin
|
|
Inc(I);
|
|
if I >= HashTableSize then
|
|
I := 0;
|
|
end;
|
|
if HashTable[I] <> nil then // Found in the strings table
|
|
Tail := PImageDict(HashTable[I])^.Index
|
|
else
|
|
begin
|
|
// Not found
|
|
WriteCode(Tail, WriteCtxt);
|
|
Inc(LastCode);
|
|
HashTable[I] := @Dict^[LastCode];
|
|
PImageDict(HashTable[I])^.Index := LastCode;
|
|
PImageDict(HashTable[I])^.Tail := Tail;
|
|
PImageDict(HashTable[I])^.Col := Col;
|
|
Tail := Col;
|
|
HashValue := InitHash(Col);
|
|
LenString := 1;
|
|
if LastCode >= MaxCode then
|
|
begin
|
|
// Next Code will be written longer
|
|
MaxCode := MaxCode shl 1;
|
|
Inc(WriteCtxt.CodeSize);
|
|
end
|
|
else
|
|
if LastCode >= CodeTableSize - 2 then
|
|
begin
|
|
// Reset tables
|
|
WriteCode(Tail, WriteCtxt);
|
|
WriteCode(ClearCode, WriteCtxt);
|
|
LenString := 0;
|
|
LastCode := EndingCode;
|
|
WriteCtxt.CodeSize := InitCodeSize;
|
|
MaxCode := 1 shl InitCodeSize;
|
|
for I := 0 to HashTableSize - 1 do
|
|
HashTable[I] := nil;
|
|
end;
|
|
end;
|
|
end;
|
|
end;
|
|
if Interlaced then
|
|
Y := InterlaceStep(Y, Height, Pass)
|
|
else
|
|
Inc(Y);
|
|
end;
|
|
WriteCode(Tail, WriteCtxt);
|
|
WriteCode(EndingCode, WriteCtxt);
|
|
FlushCode(WriteCtxt);
|
|
finally
|
|
HashTable.Free;
|
|
FreeMem(Dict);
|
|
end;
|
|
end;
|
|
|
|
function TGIFFileFormat.LoadData(Handle: TImagingHandle;
|
|
var Images: TDynImageDataArray; OnlyFirstLevel: Boolean): Boolean;
|
|
var
|
|
Header: TGIFHeader;
|
|
HasGlobalPal: Boolean;
|
|
GlobalPalLength: Integer;
|
|
GlobalPal: TPalette32Size256;
|
|
I: Integer;
|
|
BlockID: Byte;
|
|
HasGraphicExt: Boolean;
|
|
GraphicExt: TGraphicControlExtension;
|
|
Disposals: array of TDisposalMethod;
|
|
|
|
function ReadBlockID: Byte;
|
|
begin
|
|
Result := GIFTrailer;
|
|
GetIO.Read(Handle, @Result, SizeOf(Result));
|
|
end;
|
|
|
|
procedure ReadExtensions;
|
|
var
|
|
BlockSize, ExtType: Byte;
|
|
begin
|
|
HasGraphicExt := False;
|
|
|
|
// Read extensions until image descriptor is found. Only graphic extension
|
|
// is stored now (for transparency), others are skipped.
|
|
while BlockID = GIFExtensionIntroducer do
|
|
with GetIO do
|
|
begin
|
|
Read(Handle, @ExtType, SizeOf(ExtType));
|
|
|
|
if ExtType = GIFGraphicControlExtension then
|
|
begin
|
|
HasGraphicExt := True;
|
|
Read(Handle, @GraphicExt, SizeOf(GraphicExt));
|
|
end
|
|
else if ExtType in [GIFCommentExtension, GIFApplicationExtension, GIFPlainText] then
|
|
repeat
|
|
// Read block sizes and skip them
|
|
Read(Handle, @BlockSize, SizeOf(BlockSize));
|
|
Seek(Handle, BlockSize, smFromCurrent);
|
|
until BlockSize = 0;
|
|
|
|
// Read ID of following block
|
|
BlockID := ReadBlockID;
|
|
end;
|
|
end;
|
|
|
|
procedure CopyFrameTransparent(const Image, Frame: TImageData; Left, Top, TransIndex: Integer);
|
|
var
|
|
X, Y: Integer;
|
|
Src, Dst: PByte;
|
|
begin
|
|
Src := Frame.Bits;
|
|
|
|
// Copy all pixels from frame to log screen but ignore the transparent ones
|
|
for Y := 0 to Frame.Height - 1 do
|
|
begin
|
|
Dst := @PByteArray(Image.Bits)[(Top + Y) * Image.Width + Left];
|
|
for X := 0 to Frame.Width - 1 do
|
|
begin
|
|
if Src^ <> TransIndex then
|
|
Dst^ := Src^;
|
|
Inc(Src);
|
|
Inc(Dst);
|
|
end;
|
|
end;
|
|
end;
|
|
|
|
procedure ReadFrame;
|
|
var
|
|
ImageDesc: TImageDescriptor;
|
|
HasLocalPal, Interlaced, HasTransparency: Boolean;
|
|
I, Idx, LocalPalLength, TransIndex: Integer;
|
|
LocalPal: TPalette32Size256;
|
|
BlockTerm: Byte;
|
|
Frame: TImageData;
|
|
begin
|
|
Idx := Length(Images);
|
|
SetLength(Images, Idx + 1);
|
|
FillChar(LocalPal, SizeOf(LocalPal), 0);
|
|
with GetIO do
|
|
begin
|
|
// Read and parse image descriptor
|
|
Read(Handle, @ImageDesc, SizeOf(ImageDesc));
|
|
HasLocalPal := (ImageDesc.PackedFields and GIFLocalColorTable) = GIFLocalColorTable;
|
|
Interlaced := (ImageDesc.PackedFields and GIFInterlaced) = GIFInterlaced;
|
|
LocalPalLength := ImageDesc.PackedFields and GIFColorTableSize;
|
|
LocalPalLength := 1 shl (LocalPalLength + 1); // Total pal length is 2^(n+1)
|
|
|
|
// Create new logical screen
|
|
NewImage(Header.ScreenWidth, Header.ScreenHeight, ifIndex8, Images[Idx]);
|
|
// Create new image for this frame which would be later pasted onto logical screen
|
|
InitImage(Frame);
|
|
NewImage(ImageDesc.Width, ImageDesc.Height, ifIndex8, Frame);
|
|
|
|
// Load local palette if there is any
|
|
if HasLocalPal then
|
|
for I := 0 to LocalPalLength - 1 do
|
|
begin
|
|
LocalPal[I].A := 255;
|
|
Read(Handle, @LocalPal[I].R, SizeOf(LocalPal[I].R));
|
|
Read(Handle, @LocalPal[I].G, SizeOf(LocalPal[I].G));
|
|
Read(Handle, @LocalPal[I].B, SizeOf(LocalPal[I].B));
|
|
end;
|
|
|
|
// Use local pal if present or global pal if present or create
|
|
// default pal if neither of them is present
|
|
if HasLocalPal then
|
|
Move(LocalPal, Images[Idx].Palette^, SizeOf(LocalPal))
|
|
else if HasGlobalPal then
|
|
Move(GlobalPal, Images[Idx].Palette^, SizeOf(GlobalPal))
|
|
else
|
|
FillCustomPalette(Images[Idx].Palette, GlobalPalLength, 3, 3, 2);
|
|
|
|
// Add default disposal method for this frame
|
|
SetLength(Disposals, Length(Disposals) + 1);
|
|
Disposals[High(Disposals)] := dmUndefined;
|
|
|
|
// If Grahic Control Extension is present make use of it
|
|
if HasGraphicExt then
|
|
begin
|
|
HasTransparency := (GraphicExt.PackedFields and GIFTransparent) = GIFTransparent;
|
|
Disposals[High(Disposals)] := TDisposalMethod((GraphicExt.PackedFields and GIFDisposalMethod) shr 2);
|
|
if HasTransparency then
|
|
Images[Idx].Palette[GraphicExt.TransparentColorIndex].A := 0;
|
|
end
|
|
else
|
|
HasTransparency := False;
|
|
|
|
if Idx >= 1 then
|
|
begin
|
|
// If previous frame had some special disposal method we take it into
|
|
// account now
|
|
case Disposals[Idx - 1] of
|
|
dmUndefined: ; // Do nothing
|
|
dmLeave:
|
|
begin
|
|
// Leave previous frame on log screen
|
|
CopyRect(Images[Idx - 1], 0, 0, Images[Idx].Width,
|
|
Images[Idx].Height, Images[Idx], 0, 0);
|
|
end;
|
|
dmRestoreBackground:
|
|
begin
|
|
// Clear log screen with background color
|
|
FillRect(Images[Idx], 0, 0, Images[Idx].Width, Images[Idx].Height,
|
|
@Header.BackgroundColorIndex);
|
|
end;
|
|
dmRestorePrevious:
|
|
if Idx >= 2 then
|
|
begin
|
|
// Set log screen to "previous of previous" frame
|
|
CopyRect(Images[Idx - 2], 0, 0, Images[Idx].Width,
|
|
Images[Idx].Height, Images[Idx], 0, 0);
|
|
end;
|
|
end;
|
|
end
|
|
else
|
|
begin
|
|
// First frame - just fill with background color
|
|
FillRect(Images[Idx], 0, 0, Images[Idx].Width, Images[Idx].Height,
|
|
@Header.BackgroundColorIndex);
|
|
end;
|
|
|
|
try
|
|
// Data decompression finally
|
|
LZWDecompress(GetIO, Handle, ImageDesc.Width, ImageDesc.Height, Interlaced, Frame.Bits);
|
|
Read(Handle, @BlockTerm, SizeOf(BlockTerm));
|
|
// Now copy frame to logical screen with skipping of transparent pixels (if enabled)
|
|
TransIndex := Iff(HasTransparency, GraphicExt.TransparentColorIndex, MaxInt);
|
|
CopyFrameTransparent(Images[Idx], Frame, ImageDesc.Left, ImageDesc.Top, TransIndex);
|
|
finally
|
|
FreeImage(Frame);
|
|
end;
|
|
end;
|
|
end;
|
|
|
|
begin
|
|
SetLength(Images, 0);
|
|
FillChar(GlobalPal, SizeOf(GlobalPal), 0);
|
|
with GetIO do
|
|
begin
|
|
// Read GIF header
|
|
Read(Handle, @Header, SizeOf(Header));
|
|
HasGlobalPal := Header.PackedFields and GIFGlobalColorTable = GIFGlobalColorTable; // Bit 7
|
|
GlobalPalLength := Header.PackedFields and GIFColorTableSize; // Bits 0-2
|
|
GlobalPalLength := 1 shl (GlobalPalLength + 1); // Total pal length is 2^(n+1)
|
|
|
|
// Read global palette from file if present
|
|
if HasGlobalPal then
|
|
begin
|
|
for I := 0 to GlobalPalLength - 1 do
|
|
begin
|
|
GlobalPal[I].A := 255;
|
|
Read(Handle, @GlobalPal[I].R, SizeOf(GlobalPal[I].R));
|
|
Read(Handle, @GlobalPal[I].G, SizeOf(GlobalPal[I].G));
|
|
Read(Handle, @GlobalPal[I].B, SizeOf(GlobalPal[I].B));
|
|
end;
|
|
GlobalPal[Header.BackgroundColorIndex].A := 0;
|
|
end;
|
|
|
|
// Read ID of the first block
|
|
BlockID := ReadBlockID;
|
|
|
|
// Now read all data blocks in the file until file trailer is reached
|
|
while BlockID <> GIFTrailer do
|
|
begin
|
|
// Read supported and skip unsupported extensions
|
|
ReadExtensions;
|
|
// If image frame is found read it
|
|
if BlockID = GIFImageDescriptor then
|
|
ReadFrame;
|
|
// Read next block's ID
|
|
BlockID := ReadBlockID;
|
|
// If block ID is unknown set it to end-of-GIF marker
|
|
if not (BlockID in [GIFExtensionIntroducer, GIFTrailer, GIFImageDescriptor]) then
|
|
BlockID := GIFTrailer;
|
|
end;
|
|
|
|
Result := True;
|
|
end;
|
|
end;
|
|
|
|
function TGIFFileFormat.SaveData(Handle: TImagingHandle;
|
|
const Images: TDynImageDataArray; Index: Integer): Boolean;
|
|
var
|
|
Header: TGIFHeader;
|
|
ImageDesc: TImageDescriptor;
|
|
ImageToSave: TImageData;
|
|
MustBeFreed: Boolean;
|
|
I, J: Integer;
|
|
GraphicExt: TGraphicControlExtension;
|
|
|
|
procedure FindMaxDimensions(var MaxWidth, MaxHeight: Word);
|
|
var
|
|
I: Integer;
|
|
begin
|
|
MaxWidth := Images[FFirstIdx].Width;
|
|
MaxHeight := Images[FFirstIdx].Height;
|
|
|
|
for I := FFirstIdx + 1 to FLastIdx do
|
|
begin
|
|
MaxWidth := Iff(Images[I].Width > MaxWidth, Images[I].Width, MaxWidth);
|
|
MaxHeight := Iff(Images[I].Height > MaxWidth, Images[I].Height, MaxHeight);
|
|
end;
|
|
end;
|
|
|
|
begin
|
|
// Fill header with data, select size of largest image in array as
|
|
// logical screen size
|
|
FillChar(Header, Sizeof(Header), 0);
|
|
Header.Signature := GIFSignature;
|
|
Header.Version := GIFVersions[gv89];
|
|
FindMaxDimensions(Header.ScreenWidth, Header.ScreenHeight);
|
|
Header.PackedFields := GIFColorResolution; // Color resolution is 256
|
|
GetIO.Write(Handle, @Header, SizeOf(Header));
|
|
|
|
// Prepare default GC extension with delay
|
|
FillChar(GraphicExt, Sizeof(GraphicExt), 0);
|
|
GraphicExt.DelayTime := 65;
|
|
GraphicExt.BlockSize := 4;
|
|
|
|
for I := FFirstIdx to FLastIdx do
|
|
begin
|
|
if MakeCompatible(Images[I], ImageToSave, MustBeFreed) then
|
|
with GetIO, ImageToSave do
|
|
try
|
|
// Write Graphic Control Extension with default delay
|
|
Write(Handle, @GIFExtensionIntroducer, SizeOf(GIFExtensionIntroducer));
|
|
Write(Handle, @GIFGraphicControlExtension, SizeOf(GIFGraphicControlExtension));
|
|
Write(Handle, @GraphicExt, SizeOf(GraphicExt));
|
|
// Write frame marker and fill and write image descriptor for this frame
|
|
Write(Handle, @GIFImageDescriptor, SizeOf(GIFImageDescriptor));
|
|
FillChar(ImageDesc, Sizeof(ImageDesc), 0);
|
|
ImageDesc.Width := Width;
|
|
ImageDesc.Height := Height;
|
|
ImageDesc.PackedFields := GIFLocalColorTable or GIFColorTableSize; // Use lccal color table with 256 entries
|
|
Write(Handle, @ImageDesc, SizeOf(ImageDesc));
|
|
|
|
// Write local color table for each frame
|
|
for J := 0 to 255 do
|
|
begin
|
|
Write(Handle, @Palette[J].R, SizeOf(Palette[J].R));
|
|
Write(Handle, @Palette[J].G, SizeOf(Palette[J].G));
|
|
Write(Handle, @Palette[J].B, SizeOf(Palette[J].B));
|
|
end;
|
|
|
|
// Fonally compress image data
|
|
LZWCompress(GetIO, Handle, Width, Height, 8, False, Bits);
|
|
|
|
finally
|
|
if MustBeFreed then
|
|
FreeImage(ImageToSave);
|
|
end;
|
|
end;
|
|
|
|
GetIO.Write(Handle, @GIFTrailer, SizeOf(GIFTrailer));
|
|
Result := True;
|
|
end;
|
|
|
|
procedure TGIFFileFormat.ConvertToSupported(var Image: TImageData;
|
|
const Info: TImageFormatInfo);
|
|
begin
|
|
ConvertImage(Image, ifIndex8);
|
|
end;
|
|
|
|
function TGIFFileFormat.TestFormat(Handle: TImagingHandle): Boolean;
|
|
var
|
|
Header: TGIFHeader;
|
|
ReadCount: LongInt;
|
|
begin
|
|
Result := False;
|
|
if Handle <> nil then
|
|
begin
|
|
ReadCount := GetIO.Read(Handle, @Header, SizeOf(Header));
|
|
GetIO.Seek(Handle, -ReadCount, smFromCurrent);
|
|
Result := (ReadCount >= SizeOf(Header)) and
|
|
(Header.Signature = GIFSignature) and
|
|
((Header.Version = GIFVersions[gv87]) or (Header.Version = GIFVersions[gv89]));
|
|
end;
|
|
end;
|
|
|
|
initialization
|
|
RegisterImageFileFormat(TGIFFileFormat);
|
|
|
|
{
|
|
File Notes:
|
|
|
|
-- TODOS ----------------------------------------------------
|
|
- nothing now
|
|
|
|
-- 0.24.1 Changes/Bug Fixes ---------------------------------
|
|
- Made backround color transparent by default (alpha = 0).
|
|
|
|
-- 0.23 Changes/Bug Fixes -----------------------------------
|
|
- Fixed other loading bugs (local pal size, transparency).
|
|
- Added GIF saving.
|
|
- Fixed bug when loading multiframe GIFs and implemented few animation
|
|
features (disposal methods, ...).
|
|
- Loading of GIFs working.
|
|
- Unit created with initial stuff!
|
|
}
|
|
|
|
end.
|