unit FPWriteGIF; {$mode objfpc}{$H+} interface uses Classes, SysUtils, FPImage, FPReadGif; type TColor = -$7FFFFFFF - 1..$7FFFFFFF; const // GIF record separators kGifImageSeparator: byte = $2c; kGifExtensionSeparator: byte = $21; kGifTerminator: byte = $3b; kGifLabelGraphic: byte = $f9; kGifBlockTerminator: byte = $00; // LZW encode table sizes kGifCodeTableSize = 4096; // Raw rgb value clNone = TColor($1FFFFFFF); AlphaOpaque = $FF; AlphaTransparent = 0; MaxArr = (MaxLongint div Sizeof(integer)) - 1; type APixel8 = array[0..MaxArr] of Byte; PAPixel8 = ^APixel8; TRGBQuadArray256 = array[0..256] of TFPCompactImgRGBA8BitValue; TOpenColorTableArray = array of TColor; TColorTableArray = array[0..$FF] of TColor; TOctreeNode = class; // Forward definition so TReducibleNodes can be declared TReducibleNodes = array[0..7] of TOctreeNode; TOctreeNode = class(TObject) IsLeaf: Boolean; PixelCount: Integer; RedSum, GreenSum, BlueSum: Integer; Next: TOctreeNode; Child: TReducibleNodes; constructor Create(const Level: Integer; var LeafCount: Integer; var ReducibleNodes: TReducibleNodes); destructor Destroy; override; end; TFPWriterGIF = class(TFPCustomImageWriter) private fHeader: TGifHeader; fDescriptor: TGifImageDescriptor; // only one image supported fGraphicsCtrlExt: TGifGraphicsControlExtension; fTransparent: Boolean; fBackground: TColor; fPixels: PAPixel8; fPixelList: PChar; // decoded pixel indices fPixelCount: longint; // number of pixels fColorTable: TColorTableArray; fColorTableSize: integer; procedure SaveToStream(Destination: TStream); protected procedure InternalWrite(Stream: TStream; Img: TFPCustomImage); override; public constructor Create; override; destructor Destroy; override; end; implementation {$REGION ' - TOctreeNode - '} constructor TOctreeNode.Create(const Level: Integer; var LeafCount: Integer; var ReducibleNodes: TReducibleNodes); var i: Integer; begin PixelCount := 0; RedSum := 0; GreenSum := 0; BlueSum := 0; for i := Low(Child) to High(Child) do Child[i] := nil; IsLeaf := (Level = 8); if IsLeaf then begin Next := nil; Inc(LeafCount); end else begin Next := ReducibleNodes[Level]; ReducibleNodes[Level] := Self; end end; destructor TOctreeNode.Destroy; var i: Integer; begin for i := Low(Child) to High(Child) do Child[i].Free end; {$ENDREGION} {$REGION ' - TFPWriterGIF. - '} constructor TFPWriterGIF.Create; begin inherited Create; end; destructor TFPWriterGIF.Destroy; begin inherited Destroy; end; // save the current GIF definition to a stream object // at first, just write it to our memory stream fSOURCE procedure TFPWriterGIF.SaveToStream(Destination: TStream); var LZWStream: TMemoryStream; // temp storage for LZW LZWSize: integer; // LZW minimum code size // these LZW encode routines sqrunch a bitmap into a memory stream procedure LZWEncode(); var rPrefix: array[0..kGifCodeTableSize-1] of integer; // string prefixes rSuffix: array[0..kGifCodeTableSize-1] of integer; // string suffixes rCodeStack: array[0..kGifCodeTableSize-1] of byte; // encoded pixels rSP: integer; // pointer into CodeStack rClearCode: integer; // reset decode params rEndCode: integer; // last code in input stream rCurSize: integer; // current code size rBitString: integer; // steady stream of bits to be decoded rBits: integer; // number of valid bits in BitString rMaxVal: boolean; // max code value found? rCurX: integer; // position of next pixel rCurY: integer; // position of next pixel rCurPass: integer; // pixel line pass 1..4 rFirstSlot: integer; // for encoding an image rNextSlot: integer; // for encoding rCount: integer; // number of bytes read/written rLast: integer; // last byte read in rUnget: boolean; // read a new byte, or use zLast? procedure LZWReset; var i: integer; begin for i := 0 to (kGifCodeTableSize - 1) do begin rPrefix[i] := 0; rSuffix[i] := 0; end; rCurSize := LZWSize + 1; rClearCode := (1 shl LZWSize); rEndCode := rClearCode + 1; rFirstSlot := (1 shl (rCurSize - 1)) + 2; rNextSlot := rFirstSlot; rMaxVal := false; end; // save a code value on the code stack procedure LZWSaveCode(Code: integer); begin rCodeStack[rSP] := Code; inc(rSP); end; // save the code in the output data stream procedure LZWPutCode(code: integer); var n: integer; b: byte; begin // write out finished bytes // a literal "8" for 8 bits per byte while (rBits >= 8) do begin b := (rBitString and $ff); rBitString := (rBitString shr 8); rBits := rBits - 8; LZWStream.Write(b, 1); end; // make sure no junk bits left above the first byte rBitString := (rBitString and $ff); // and save out-going code n := (code shl rBits); rBitString := (rBitString or n); rBits := rBits + rCurSize; end; // get the next pixel from the bitmap, and return it as an index into the colormap function LZWReadBitmap: integer; var n: integer; j: longint; p: PChar; begin if (rUnget) then begin n := rLast; rUnget := false; end else begin inc(rCount); j := (rCurY * fDescriptor.Width) + rCurX; if ((0 <= j) and (j < fPixelCount)) then begin p := fPixelList + j; n := ord(p^); end else n := 0; // if first pass, make sure CurPass was initialized if (rCurPass = 0) then rCurPass := 1; inc(rCurX); // inc X position if (rCurX >= fDescriptor.Width) then // bumping Y ? begin rCurX := 0; inc(rCurY); end; end; rLast := n; result := n; end; var i,n, cc: integer; // current code to translate oc: integer; // last code encoded found: boolean; // decoded string in prefix table? pixel: byte; // lowest code to search for ldx: integer; // last index found fdx: integer; // current index found b: byte; begin // init data block fillchar(rCodeStack, sizeof(rCodeStack), 0); rBitString := 0; rBits := 0; rCurX := 0; rCurY := 0; rCurPass := 0; rLast := 0; rUnget:= false; LZWReset; // all within the data record // always save the clear code first ... LZWPutCode(rClearCode); // and first pixel oc := LZWReadBitmap; LZWPutCode(oc); // nothing found yet (but then, we haven't searched) ldx := 0; fdx := 0; // and the rest of the pixels rCount := 1; while (rCount <= fPixelCount) do begin rSP := 0; // empty the stack of old data n := LZWReadBitmap; // next pixel from the bitmap LZWSaveCode(n); cc := rCodeStack[0]; // beginning of the string // add new encode table entry rPrefix[rNextSlot] := oc; rSuffix[rNextSlot] := cc; inc(rNextSlot); if (rNextSlot >= kGifCodeTableSize) then rMaxVal := true else if (rNextSlot > (1 shl rCurSize)) then inc(rCurSize); // find the running string of matching codes ldx := cc; found := true; while (found and (rCount <= fPixelCount)) do begin n := LZWReadBitmap; LZWSaveCode(n); cc := rCodeStack[0]; if (ldx < rFirstSlot) then i := rFirstSlot else i := ldx + 1; pixel := rCodeStack[rSP - 1]; found := false; while ((not found) and (i < rNextSlot)) do begin found := ((rPrefix[i] = ldx) and (rSuffix[i] = pixel)); inc(i); end; if (found) then begin ldx := i - 1; fdx := i - 1; end; end; // if not found, save this index, and get the same code again if (not found) then begin rUnget := true; rLast := rCodeStack[rSP-1]; dec(rSP); cc := ldx; end else cc := fdx; // whatever we got, write it out as current table entry LZWPutCode(cc); if ((rMaxVal) and (rCount <= fPixelCount)) then begin LZWPutCode(rClearCode); LZWReset; cc := LZWReadBitmap; LZWPutCode(cc); end; oc := cc; end; LZWPutCode(rEndCode); // write out the rest of the bit string while (rBits > 0) do begin b := (rBitString and $ff); rBitString := (rBitString shr 8); rBits := rBits - 8; LZWStream.Write(b, 1); end; end; var i: integer; begin Destination.Position := 0; with fHeader do begin // write the GIF signature // if only one image, and no image extensions, then GIF is GIF87a, // else use the updated version GIF98a // we just added an extension block; the signature must be version 89a Destination.Write(Signature, 3); Destination.Write(Version, 3); // write the overall GIF screen description to the source stream Destination.Write(ScreenWidth, 2); // logical screen width Destination.Write(ScreenHeight, 2); // logical screen height Destination.Write(Packedbit, 1); // packed bit fields (Global Color valid, Global Color size, Sorted, Color Resolution) Destination.Write(BackgroundColor, 1); // background color Destination.Write(AspectRatio, 1); // pixel aspect ratio if (Packedbit and $80)>0 then //Global Color valid // write out color gobal table with RGB values for i := 0 to fColorTableSize-1 do Destination.Write(fColorTable[i], 3); end; // write out graphic extension for this image Destination.Write(kGifExtensionSeparator, 1); // write the extension separator Destination.Write(kGifLabelGraphic, 1); // write the extension label Destination.Write(fGraphicsCtrlExt.BlockSize, 1); // block size (always 4) Destination.Write(fGraphicsCtrlExt.Packedbit, 1); // packed bit field Destination.Write(fGraphicsCtrlExt.DelayTime, 2); // delay time Destination.Write(fGraphicsCtrlExt.ColorIndex, 1); // transparent color Destination.Write(fGraphicsCtrlExt.Terminator, 1); // block terminator // write actual image data Destination.Write(kGifImageSeparator, 1); // write the next image descriptor shortcut to the record fields with fDescriptor do begin // write the basic descriptor record Destination.Write(Left, 2); // left position Destination.Write(Top, 2); // top position Destination.Write(Width, 2); // size of image Destination.Write(Height, 2); // size of image Destination.Write(Packedbit, 1); // packed bit field // there is no local color table defined we use global LZWSize := 8; // the LZW minimum code size Destination.Write(LZWSize, 1); LZWStream := TMemoryStream.Create; // init the storage for compressed data try LZWEncode(); // encode the image and save it in LZWStream // write out the data stream as a series of data blocks LZWStream.Position := 0; while (LZWStream.Position < LZWStream.Size) do begin i := LZWStream.Size - LZWStream.Position; if (i > 255) then i := 255; Destination.Write(i, 1); Destination.CopyFrom(LZWStream, i); end; finally FreeAndNil(LZWStream); end; Destination.Write(kGifBlockTerminator, 1); // block terminator end; Destination.Write(kGifTerminator, 1); // done with writing end; procedure TFPWriterGIF.InternalWrite(Stream: TStream; Img: TFPCustomImage); var CT: TOpenColorTableArray; Palette: TList; PaletteHasAllColours: Boolean; Mappings: array[BYTE, BYTE] of TList; Tree: TOctreeNode; LeafCount: Integer; ReducibleNodes: TReducibleNodes; LastColor: TColor; LastColorIndex: Byte; // convert TFPCustomImage TFPColor to TColor function FPColorToTColor(const FPColor: TFPColor): TColor; begin result := TColor(((FPColor.Red shr 8) and $ff) or (FPColor.Green and $ff00) or ((FPColor.Blue shl 8) and $ff0000)); end; // try to make color table of all colors function MakeColorTableOfAllColors(): Boolean; var Flags: array[Byte, Byte] of TBits; x, y, ci: Cardinal; Red, Green, Blue: Byte; Cnt: word; begin result := false; // init Flags for y := 0 to $FF do for x := 0 to $FF do Flags[x, y] := nil; try for ci := 0 to $ff do CT[ci] := 0; Cnt := 0; for y := 0 to Img.Height - 1 do for x := 0 to Img.Width - 1 do begin Red := Byte(Img.Colors[x, y].red shr 8); Green := Byte(Img.Colors[x, y].green shr 8); Blue := Byte(Img.Colors[x, y].blue shr 8); if (Flags[Red, Green]) = nil then begin Flags[Red, Green] := Classes.TBits.Create; Flags[Red, Green].Size := 256; end; if not Flags[Red, Green].Bits[Blue] then begin CT[Cnt] := FPColorToTColor(Img.Colors[x, y]); if Cnt = $ff then exit; inc(Cnt); Flags[Red, Green].Bits[Blue] := true; end; end; result := true; PaletteHasAllColours := true; finally // free Flags for y := 0 to $FF do for x := 0 to $FF do if Flags[x, y] <> nil then FreeAndNil(Flags[x, y]); end; fColorTableSize := High(CT) + 1; for x := 0 to fColorTableSize - 1 do fColorTable[x] := CT[x]; LastColor := clNone; end; procedure MakeColorTableofReducedColors(); procedure AddColor(var Node: TOctreeNode; const r, g, b: Byte; const Level: Integer; var ReducibleNodes: TReducibleNodes); const mask: array[0..7] of Byte = ($80, $40, $20, $10, $08, $04, $02, $01); var Index, Shift: Integer; begin if Node = nil then Node := TOctreeNode.Create(Level, LeafCount, ReducibleNodes); if Node.IsLeaf then begin Inc(Node.PixelCount); Inc(Node.RedSum, r); Inc(Node.GreenSum, g); Inc(Node.BlueSum, b) end else begin Shift := 7 - Level; Index := (((r and mask[Level]) shr Shift) shl 2) or (((g and mask[Level]) shr Shift) shl 1) or ((b and mask[Level]) shr Shift); AddColor(Node.Child[Index], r, g, b, Level + 1, ReducibleNodes) end end; procedure ReduceTree(var LeafCount: Integer; var ReducibleNodes: TReducibleNodes); var RedSum, BlueSum, GreenSum, Children, i: Integer; Node: TOctreeNode; begin i := 7; while (i > 0) and (ReducibleNodes[i] = nil) do dec(i); Node := ReducibleNodes[i]; ReducibleNodes[i] := Node.Next; RedSum := 0; GreenSum := 0; BlueSum := 0; Children := 0; for i := Low(ReducibleNodes) to High(ReducibleNodes) do if Node.Child[i] <> nil then begin Inc(RedSum, Node.Child[i].RedSum); Inc(GreenSum, Node.Child[i].GreenSum); Inc(BlueSum, Node.Child[i].BlueSum); Inc(Node.PixelCount, Node.Child[i].PixelCount); Node.Child[i].Free; Node.Child[i] := nil; inc(Children) end; Node.IsLeaf := true; Node.RedSum := RedSum; Node.GreenSum := GreenSum; Node.BlueSum := BlueSum; Dec(LeafCount, Children - 1) end; procedure GetPaletteColors(const Node: TOctreeNode; var RGBQuadArray: TRGBQuadArray256; var Index: integer); var i: integer; begin if Node.IsLeaf then begin with RGBQuadArray[Index] do begin try r := Byte(Node.RedSum div Node.PixelCount); g := Byte(Node.GreenSum div Node.PixelCount); b := Byte(Node.BlueSum div Node.PixelCount); a := 0; except r := 0; g := 0; b := 0; a := 0; end; a := 0 end; inc(Index); end else for i := Low(Node.Child) to High(Node.Child) do if Node.Child[i] <> nil then GetPaletteColors(Node.Child[i], RGBQuadArray, Index) end; procedure SetPalette(Pal: array of TColor; Size: integer); var PalSize, i: integer; Col: PFPCompactImgRGB8BitValue; x, y: Cardinal; Red, Green, Blue: Byte; Pcol: PInteger; DistanceSquared, SmallestDistanceSquared: integer; R1, G1, B1: Byte; begin if Size <> -1 then PalSize := Size else PalSize := High(Pal) + 1; for i := 0 to PalSize - 1 do begin GetMem(Col, SizeOf(TFPCompactImgRGB8BitValue)); Col^.r := Byte(Pal[i]); Col^.g := Byte(Pal[i] shr 8); Col^.b := Byte(Pal[i] shr 16); Palette.Add(Col); end; for y := 0 to $ff do for x := 0 to $ff do Mappings[y,x] := nil; for y := 0 to Img.Height - 1 do for x := 0 to Img.Width - 1 do begin Red := Byte(Img.Colors[x, y].red shr 8); Green := Byte(Img.Colors[x, y].green shr 8); Blue := Byte(Img.Colors[x, y].blue shr 8); //Small reduction of color space dec(Red, Red mod 3); dec(Green, Green mod 3); dec(Blue, Blue mod 3); if (Mappings[Red, Green]) = nil then begin Mappings[Red, Green] := TList.Create; Mappings[Red, Green].Count := 256; end; if (Mappings[Red, Green].Items[Blue] = nil) then begin GetMem(Pcol, SizeOf(integer)); PCol^ := 0; SmallestDistanceSquared := $1000000; for i := 0 to Palette.Count - 1 do begin R1 := PFPCompactImgRGB8BitValue(Palette[i])^.r; G1 := PFPCompactImgRGB8BitValue(Palette[i])^.g; B1 := PFPCompactImgRGB8BitValue(Palette[i])^.b; DistanceSquared := (Red - R1) * (Red - R1) + (Green - G1) * (Green - G1) + (Blue - B1) * (Blue - B1); if DistanceSquared < SmallestDistanceSquared then begin PCol^ := i; if (Red = R1) and (Green = G1) and (Blue = B1) then break; SmallestDistanceSquared := DistanceSquared; end end; Mappings[Red, Green].Items[Blue] := PCol; end; end; end; procedure DeleteTree(var Node: TOctreeNode); var i: integer; begin for i := Low(TReducibleNodes) to High(TReducibleNodes) do if Node.Child[i] <> nil then DeleteTree(Node.Child[i]); FreeAndNil(Node); end; var i, j, Index: integer; QArr: TRGBQuadArray256; begin PaletteHasAllColours := false; Tree := nil; LeafCount := 0; for i := Low(ReducibleNodes) to High(ReducibleNodes) do ReducibleNodes[i] := nil; if (Img.Height > 0) and (Img.Width > 0) then for j := 0 to Img.Height - 1 do for i := 0 to Img.Width - 1 do begin AddColor(Tree, Byte(Img.Colors[i,j].red shr 8), Byte(Img.Colors[i,j].green shr 8), Byte(Img.Colors[i,j].blue shr 8), 0, ReducibleNodes); while LeafCount > 256 do ReduceTree(LeafCount, ReducibleNodes) end; Index := 0; GetPaletteColors(Tree, QArr, Index); for i := 0 to LeafCount - 1 do CT[i] := (QArr[i].b shl 16) + (QArr[i].g shl 8) + QArr[i].r; fColorTableSize := LeafCount; for i := 0 to fColorTableSize - 1 do fColorTable[i] := CT[i]; LastColor := clNone; SetPalette(fColorTable, LeafCount); if Tree <> nil then DeleteTree(Tree); end; procedure ClearMappings; var i, j, k: integer; begin for j := 0 to $FF do for i := 0 to $FF do begin if Assigned(Mappings[i, j]) then begin for k := 0 to $FF do FreeMem(Mappings[i, j].Items[k], SizeOf(TColor)); Mappings[i, j].Free; end; Mappings[i, j] := nil; end; end; procedure SetPixel(X, Y: Integer; Value: TColor); var Val: integer; PCol: PInteger; R, G, B: byte; begin if not ((Img.Width >= X) and (Img.Height >= Y) and (X > -1) and (Y > -1)) then exit; Val := -1; if LastColor = Value then Val := LastColorIndex else begin if PaletteHasAllColours then begin TFPCompactImgRGBA8BitValue(Value).a := 0; for Val := 0 to fColorTableSize - 1 do if fColorTable[Val] = Value then break; end else begin B := Byte(Value shr 16); B := B - (B mod 3); G := Byte(Value shr 8); G := G - (G mod 3); R := Byte(Value); R := R - (R mod 3); Val := -1; if Mappings[R, G] <> nil then begin PCol := Mappings[R, G].Items[B]; if PCol <> nil then Val := PCol^; end; end; LastColor := Value; LastColorIndex := Val; end; fPixels^[Y * Img.Width + X] := Val; end; // find the color within the color table; returns 0..255, -1 if color not found function FindColorIndex(c: TColor): integer; var i: integer; begin i := 0; result := -1; while (i -1) then begin Packedbit := Packedbit or $01; // transparent color given (Packedbit or $01) ColorIndex := n; //transparent color index end; end; DelayTime := 0; Terminator := 0; // allways 0 end; SaveToStream(Stream); if (fPixelList <> nil) then FreeMem(fPixelList); FreeMem(fPixels); fPixels := nil; end; {$ENDREGION} initialization ImageHandlers.RegisterImageWriter ('GIF Graphics', 'gif', TFPWriterGif); end.