// SPDX-License-Identifier: LGPL-3.0-linking-exception
{ Classes to read and write animated GIF and animated PNG files. }
unit BGRAAnimatedGif;

{$mode objfpc}{$H+}
{$i bgrabitmap.inc}

interface

uses
  BGRAClasses, SysUtils, BGRAGraphics, FPImage, BGRABitmap, BGRABitmapTypes,
  BGRAPalette, BGRAGifFormat{$IFDEF BGRABITMAP_USE_LCL}, ExtCtrls{$ENDIF};

type
  TDisposeMode = BGRAGifFormat.TDisposeMode;
  TGifSubImage = BGRAGifFormat.TGifSubImage;
  TGifSubImageArray = BGRAGifFormat.TGifSubImageArray;

  {* How to deal with the background under the GIF animation }
  TGifBackgroundMode = (
    gbmSimplePaint,                   // frames are rendered without clearing the backgroud
    gbmEraseBackground,               // pixels in the GIF that become transparent are filled with EraseColor
    gbmSaveBackgroundOnce,            // background is saved once before drawing the first frame
    gbmUpdateBackgroundContinuously); // background is updated continuously to handle overlapping animations

  {** String constants for TGifBackgroundMode }
  const GifBackgroundModeStr: array[TGifBackgroundMode] of string =
    ('gbmSimplePaint', 'gbmEraseBackground', 'gbmSaveBackgroundOnce',
    'gbmUpdateBackgroundContinuously');

type
  {* Class to read/write animated GIF, supports animated PNG as well when specified }
  TBGRAAnimatedGif = class(TGraphic)
  private
    FAspectRatio: single;
    FWidth, FHeight:  integer;
    FBackgroundColor: TColor;

    FPrevDate: TDateTime;
    FPaused:   boolean;
    FTimeAccumulator: double;
    FCurrentImage, FWantedImage: integer;
    FTotalAnimationTime: int64;
    FPreviousDisposeMode: TDisposeMode;

    FBackgroundImage, FPreviousVirtualScreen, FStretchedVirtualScreen,
    FInternalVirtualScreen, FRestoreImage: TBGRABitmap;
    FImageChanged: boolean;

    {$IFDEF BGRABITMAP_USE_LCL}
    FTimer: TTimer;
    {$ENDIF}

    procedure CheckFrameIndex(AIndex: integer);
    function GetAverageDelayMs: integer;
    function GetCount: integer;
    function GetFrameDelayMs(AIndex: integer): integer;
    function GetFrameDisposeMode(AIndex: integer): TDisposeMode;
    function GetFrameDrawMode(AIndex: integer): TDrawMode;
    function GetFrameHasLocalPalette(AIndex: integer): boolean;
    function GetFrameImage(AIndex: integer): TBGRABitmap;
    function GetFrameImagePos(AIndex: integer): TPoint;
    function GetTimeUntilNextImage: integer;
    {$IFDEF BGRABITMAP_USE_LCL}
    procedure OnTimer(Sender: TObject);
    {$ENDIF}
    procedure Render(StretchWidth, StretchHeight: integer);
    procedure SetAspectRatio(AValue: single);
    procedure SetBackgroundColor(AValue: TColor);
    procedure SetFrameDelayMs(AIndex: integer; AValue: integer);
    procedure SetFrameDisposeMode(AIndex: integer; AValue: TDisposeMode);
    procedure SetFrameDrawMode(AIndex: integer; AValue: TDrawMode);
    procedure SetFrameHasLocalPalette(AIndex: integer; AValue: boolean);
    procedure SetFrameImage(AIndex: integer; AValue: TBGRABitmap);
    procedure SetFrameImagePos(AIndex: integer; AValue: TPoint);
    procedure UpdateSimple(Canvas: TCanvas; ARect: TRect;
      DrawOnlyIfChanged: boolean = True);
    procedure UpdateEraseBackground(Canvas: TCanvas; ARect: TRect;
      DrawOnlyIfChanged: boolean = True);
    procedure Init;
    function GetBitmap: TBitmap;
    function GetMemBitmap: TBGRABitmap;
    procedure SaveBackgroundOnce(Canvas: TCanvas; ARect: TRect);
    procedure SetCurrentImage(Index: integer);

  protected
    FImages: TGifSubImageArray;
    FDestroying: boolean;

    {TGraphic}
    procedure Draw(ACanvas: TCanvas; const Rect: TRect); override;
    function GetEmpty: boolean; override;
    function GetHeight: integer; override;
    function GetTransparent: boolean; override;
    function GetWidth: integer; override;
    procedure SetHeight({%H-}Value: integer); override;
    procedure SetTransparent({%H-}Value: boolean); override;
    procedure SetWidth({%H-}Value: integer); override;
    procedure ClearViewer; virtual;
    procedure Changed(Sender: TObject); override;
    procedure EnsureNextFrameRec(AIndex: integer);
    procedure AssignTo(Dest: TPersistent); override;
    procedure AssignImage(AImage: TFPCustomImage; AOwned: boolean);
    procedure LoadFromStreamAsGif(Stream: TStream; AMaxImageCount: integer);
    procedure LoadFromStreamAsPng(Stream: TStream; AMaxImageCount: integer);
    procedure LoadFromStreamAsStatic(Stream: TStream);

    procedure CheckSavable(AFormat: TBGRAImageFormat);
    procedure CheckAnyFrame;
    procedure SaveToStreamAsPng(Stream: TStream);
    procedure SaveToStreamAsPng(Stream: TStream;
      AQuantizer: TBGRAColorQuantizerAny;
      ADitheringAlgorithm: TDitheringAlgorithm); overload; virtual;
    procedure SaveToStreamAsGif(Stream: TStream;
      AQuantizer: TBGRAColorQuantizerAny;
      ADitheringAlgorithm: TDitheringAlgorithm); overload; virtual;

  public
    EraseColor:     TColor;
    BackgroundMode: TGifBackgroundMode;
    LoopCount:      Word;
    LoopDone:       Integer;

    constructor Create(filenameUTF8: string); overload;
    constructor Create(stream: TStream); overload;
    constructor Create(stream: TStream; AMaxImageCount: integer); overload;
    constructor Create; overload; override;
    function Duplicate: TBGRAAnimatedGif;
    procedure Assign(ASource: TPersistent); override;
    function AddFrame(AImage: TFPCustomImage; X,Y: integer; ADelayMs: integer;
      ADisposeMode: TDisposeMode = dmErase; AHasLocalPalette: boolean = false;
      ADrawMode: TDrawMode = dmSetExceptTransparent; AOwned: boolean = false) : integer;
    procedure InsertFrame(AIndex: integer; AImage: TFPCustomImage; X,Y: integer; ADelayMs: integer;
      ADisposeMode: TDisposeMode = dmErase; AHasLocalPalette: boolean = false;
      ADrawMode: TDrawMode = dmSetExceptTransparent; AOwned: boolean = false);
    procedure DeleteFrame(AIndex: integer; AEnsureNextFrameDoesNotChange: boolean);

    {** Add a frame that replaces completely the previous one }
    function AddFullFrame(AImage: TFPCustomImage; ADelayMs: integer;
                          AHasLocalPalette: boolean = true;
                          ADrawMode: TDrawMode = dmSetExceptTransparent; AOwned: boolean = false): integer;
    {** Insert at the specified _AIndex_ a frame that replaces completely the previous one }
    procedure InsertFullFrame(AIndex: integer;
                              AImage: TFPCustomImage; ADelayMs: integer;
                              AHasLocalPalette: boolean = true;
                              ADrawMode: TDrawMode = dmSetExceptTransparent; AOwned: boolean = false);
    procedure ReplaceFullFrame(AIndex: integer;
                              AImage: TFPCustomImage; ADelayMs: integer;
                              AHasLocalPalette: boolean = true;
                              ADrawMode: TDrawMode = dmSetExceptTransparent; AOwned: boolean = false);
    procedure OptimizeFrames;

    {TGraphic}
    procedure LoadFromStream(Stream: TStream); overload; override;
    procedure LoadFromStream(Stream: TStream; AMaxImageCount: integer); overload;
    procedure LoadFromResource(AFilename: string);
    {** Save to a stream using GIF format }
    procedure SaveToStream(Stream: TStream); override; overload;
    {** There are some differences in the dispose modes and draw modes so some files
        cannot be directly saved from one format to the other:
        - dispose mode dmErase is only in GIF and dispose mode dmEraseArea is only in PNG,
        - draw mode in GIF is only dmSetExceptTransparent and draw mode in PNG is dmSet or dmDrawWithTransparency.

        PNG format is not limited to 256 colors, so there is no need for quantization even if it possible.
        When PNG has a palette, it applies to all frames, whereas for GIF, there can be a palette for each frame. }
    procedure SaveToStream(Stream: TStream; AFormat: TBGRAImageFormat); overload;
    procedure LoadFromFile(const AFilenameUTF8: string); override;
    procedure SaveToFile(const AFilenameUTF8: string); override;
    class function GetFileExtensions: string; override;

    procedure SetSize(AWidth,AHeight: integer); virtual;
    procedure SaveToStream(Stream: TStream; AQuantizer: TBGRAColorQuantizerAny;
      ADitheringAlgorithm: TDitheringAlgorithm; AFormat: TBGRAImageFormat = ifGif); overload; virtual;
    procedure Clear; override;
    destructor Destroy; override;
    procedure Pause;
    procedure Resume;

    procedure Show(Canvas: TCanvas; ARect: TRect); overload;
    procedure Update(Canvas: TCanvas; ARect: TRect); overload;
    procedure Hide(Canvas: TCanvas; ARect: TRect); overload;
    function MakeBitmapCopy(ABackground: TColor = clNone): TBitmap;

    property BackgroundColor: TColor Read FBackgroundColor write SetBackgroundColor;
    property Count: integer Read GetCount;
    property Width: integer Read FWidth;
    property Height: integer Read FHeight;
    property Paused: boolean Read FPaused;
    property Bitmap: TBitmap Read GetBitmap;
    property MemBitmap: TBGRABitmap Read GetMemBitmap;
    property CurrentImage: integer Read FCurrentImage Write SetCurrentImage;
    property TimeUntilNextImageMs: integer read GetTimeUntilNextImage;
    property FrameImage[AIndex: integer]: TBGRABitmap read GetFrameImage write SetFrameImage;
    property FrameHasLocalPalette[AIndex: integer]: boolean read GetFrameHasLocalPalette write SetFrameHasLocalPalette;
    property FrameImagePos[AIndex: integer]: TPoint read GetFrameImagePos write SetFrameImagePos;
    property FrameDelayMs[AIndex: integer]: integer read GetFrameDelayMs write SetFrameDelayMs;
    property FrameDisposeMode[AIndex: integer]: TDisposeMode read GetFrameDisposeMode write SetFrameDisposeMode;
    property FrameDrawMode[AIndex: integer]: TDrawMode read GetFrameDrawMode write SetFrameDrawMode; // linear blend only in PNG
    property AspectRatio: single read FAspectRatio write SetAspectRatio;
    property TotalAnimationTimeMs: Int64 read FTotalAnimationTime;
    property AverageDelayMs: integer read GetAverageDelayMs;
  end;

  {* @abstract(Class to read/write animated PNG, supports animated GIF as well when specified.)

     This class only changes default format used, everything is implemented in TBGRAAnimatedGif }
  TBGRAAnimatedPng = class(TBGRAAnimatedGif)
    {** Save to a stream using PNG format }
    procedure SaveToStream(Stream: TStream); override; overload;
    class function GetFileExtensions: string; override;
  end;

  {* Static GIF reader }
  TBGRAReaderGIF = class(TFPCustomImageReader)
  protected
    procedure InternalRead(Str: TStream; Img: TFPCustomImage); override;
    function InternalCheck(Str: TStream): boolean; override;
  end;

  {* Static GIF writer }
  TBGRAWriterGIF = class(TFPCustomImageWriter)
  protected
    procedure InternalWrite(Str: TStream; Img: TFPCustomImage); override;
  end;

implementation

uses BGRABlend, BGRAUTF8,
  BGRAReadPng, BGRAWritePng, BGRAPNGComn
  {$IFDEF BGRABITMAP_USE_LCL}, Graphics{$ENDIF};

const
  {$IFDEF ENDIAN_LITTLE}
  AlphaMask = $FF000000;
  {$ELSE}
  AlphaMask = $000000FF;
  {$ENDIF}


{ TBGRAAnimatedGif }

class function TBGRAAnimatedGif.GetFileExtensions: string;
begin
  Result := 'gif';
end;

procedure TBGRAAnimatedGif.SetSize(AWidth, AHeight: integer);
begin
  ClearViewer;
  FWidth := AWidth;
  FHeight := AHeight;
end;

procedure TBGRAAnimatedGif.SaveToStream(Stream: TStream;
      AQuantizer: TBGRAColorQuantizerAny;
      ADitheringAlgorithm: TDitheringAlgorithm;
      AFormat: TBGRAImageFormat);
begin
  case AFormat of
    ifGif: SaveToStreamAsGif(Stream, AQuantizer, ADitheringAlgorithm);
    ifPng: SaveToStreamAsPng(Stream, AQuantizer, ADitheringAlgorithm);
    else
      raise Exception.Create('Unhandled image format (' + SuggestImageExtension(AFormat) + ')');
  end;
end;

procedure TBGRAAnimatedGif.Render(StretchWidth, StretchHeight: integer);
var
  curDate: TDateTime;
  previousImage, nextImage: integer;

begin
  if FInternalVirtualScreen = nil then
  begin
    FInternalVirtualScreen := TBGRABitmap.Create(FWidth, FHeight);
    if (Count = 0) and (BackgroundColor <> clNone) then
      FInternalVirtualScreen.Fill(BackgroundColor)
    else
      FInternalVirtualScreen.Fill(BGRAPixelTransparent);
    FImageChanged := True;
  end;

  if Count = 0 then
    exit;

  previousImage := FCurrentImage;

  curDate := Now;
  if FWantedImage <> -1 then
  begin
    nextImage    := FWantedImage;
    FTimeAccumulator := 0;
    FWantedImage := -1;
  end
  else
  if FCurrentImage = -1 then
  begin
    nextImage := 0;
    FTimeAccumulator := 0;
    FPreviousDisposeMode := dmNone;
  end
  else
  begin
    if not FPaused then
      IncF(FTimeAccumulator, (curDate - FPrevDate) * 24 * 60 * 60 * 1000);
    if FTotalAnimationTime > 0 then FTimeAccumulator:= frac(FTimeAccumulator/FTotalAnimationTime)*FTotalAnimationTime;
    nextImage := FCurrentImage;
    while FTimeAccumulator > FImages[nextImage].DelayMs do
    begin
      DecF(FTimeAccumulator, FImages[nextImage].DelayMs);
      Inc(nextImage);
      if nextImage >= Count then
      begin
        if (LoopCount > 0) and (LoopDone >= LoopCount-1) then
        begin
          LoopDone := LoopCount;
          dec(nextImage);
          break;
        end else
        begin
          nextImage := 0;
          inc(LoopDone);
        end;
      end;

      if nextImage = previousImage then
      begin
        if not ((LoopCount > 0) and (LoopDone >= LoopCount-1)) then
        begin
          Inc(nextImage);
          if nextImage >= Count then
            nextImage := 0;
        end;
        break;
      end;
    end;
  end;
  FPrevDate := curDate;

  while FCurrentImage <> nextImage do
  begin
    case FPreviousDisposeMode of
      dmEraseArea:
        with FImages[FCurrentImage] do
          FInternalVirtualScreen.EraseRect(
            RectWithSize(Position.X, Position.Y, Image.Width, Image.Height), 255);
    end;

    Inc(FCurrentImage);
    if FCurrentImage >= Count then
    begin
      FCurrentImage := 0;
      FPreviousDisposeMode := dmErase;
    end;

    case FPreviousDisposeMode of
      dmErase: FInternalVirtualScreen.Fill(BGRAPixelTransparent);
      dmRestore: if FRestoreImage <> nil then
          FInternalVirtualScreen.PutImage(0, 0, FRestoreImage, dmSet);
    end;

    with FImages[FCurrentImage] do
    begin
      if disposeMode = dmRestore then
      begin
        if FRestoreImage = nil then
          FRestoreImage := TBGRABitmap.Create(FWidth, FHeight);
        FRestoreImage.PutImage(0, 0, FInternalVirtualScreen, dmSet);
      end;

      if Image <> nil then
        FInternalVirtualScreen.PutImage(Position.X, Position.Y, Image,
          DrawMode);
      FPreviousDisposeMode := DisposeMode;
    end;

    FImageChanged := True;
    previousImage := FCurrentImage;
    FInternalVirtualScreen.InvalidateBitmap;
  end;

  if FStretchedVirtualScreen <> nil then
    FStretchedVirtualScreen.FreeReference;
  if (FInternalVirtualScreen.Width = StretchWidth) and
    (FInternalVirtualScreen.Height = StretchHeight) then
    FStretchedVirtualScreen := TBGRABitmap(FInternalVirtualScreen.NewReference)
  else
    FStretchedVirtualScreen :=
      TBGRABitmap(FInternalVirtualScreen.Resample(StretchWidth, StretchHeight));
end;

procedure TBGRAAnimatedGif.SetAspectRatio(AValue: single);
begin
  if AValue < 0.25 then AValue := 0.25;
  if AValue > 4 then AValue := 4;
  if FAspectRatio=AValue then Exit;
  FAspectRatio:=AValue;
end;

procedure TBGRAAnimatedGif.SetBackgroundColor(AValue: TColor);
begin
  if FBackgroundColor=AValue then Exit;
  FBackgroundColor:=AValue;
end;

procedure TBGRAAnimatedGif.SetFrameDelayMs(AIndex: integer; AValue: integer);
begin
  CheckFrameIndex(AIndex);
  if AValue < 0 then AValue := 0;
  FTotalAnimationTime := FTotalAnimationTime + AValue - FImages[AIndex].DelayMs;
  FImages[AIndex].DelayMs := AValue;
end;

procedure TBGRAAnimatedGif.SetFrameDisposeMode(AIndex: integer;
  AValue: TDisposeMode);
begin
  CheckFrameIndex(AIndex);
  FImages[AIndex].DisposeMode := AValue;
end;

procedure TBGRAAnimatedGif.SetFrameDrawMode(AIndex: integer; AValue: TDrawMode);
begin
  CheckFrameIndex(AIndex);
  if not (AValue in[dmSet, dmSetExceptTransparent, dmLinearBlend]) then
    raise Exception.Create('Unhandled draw mode');
  FImages[AIndex].DrawMode := AValue;
end;

procedure TBGRAAnimatedGif.SetFrameHasLocalPalette(AIndex: integer;
  AValue: boolean);
begin
  CheckFrameIndex(AIndex);
  FImages[AIndex].HasLocalPalette := AValue;
end;

procedure TBGRAAnimatedGif.SetFrameImage(AIndex: integer; AValue: TBGRABitmap);
var ACopy: TBGRABitmap;
begin
  CheckFrameIndex(AIndex);
  ACopy := AValue.Duplicate;
  FImages[AIndex].Image.FreeReference;
  FImages[AIndex].Image := ACopy;
end;

procedure TBGRAAnimatedGif.SetFrameImagePos(AIndex: integer; AValue: TPoint);
begin
  CheckFrameIndex(AIndex);
  FImages[AIndex].Position := AValue;
end;

procedure TBGRAAnimatedGif.UpdateSimple(Canvas: TCanvas; ARect: TRect;
  DrawOnlyIfChanged: boolean = True);
begin
  if FPreviousVirtualScreen <> nil then
  begin
    FPreviousVirtualScreen.FreeReference;
    FPreviousVirtualScreen := nil;
  end;

  Render(ARect.Right - ARect.Left, ARect.Bottom - ARect.Top);
  if FImageChanged then
  begin
    FStretchedVirtualScreen.Draw(Canvas, ARect.Left, ARect.Top, False);
    FImageChanged := False;
  end
  else
  if not DrawOnlyIfChanged then
    FStretchedVirtualScreen.Draw(Canvas, ARect.Left, ARect.Top, False);

  FPreviousVirtualScreen := TBGRABitmap(FStretchedVirtualScreen.NewReference);
end;

procedure TBGRAAnimatedGif.CheckFrameIndex(AIndex: integer);
begin
  if (AIndex < 0) or (AIndex >= Count) then Raise ERangeError.Create('Index out of bounds');
end;

function TBGRAAnimatedGif.GetAverageDelayMs: integer;
var sum: int64;
  i: Integer;
begin
  if Count > 0 then
  begin
    sum := 0;
    for i := 0 to Count-1 do
      inc(sum, FrameDelayMs[i]);
    result := sum div Count;
  end else
    result := 100; //default
end;

function TBGRAAnimatedGif.GetCount: integer;
begin
  Result := length(FImages);
end;

function TBGRAAnimatedGif.GetFrameDelayMs(AIndex: integer): integer;
begin
  CheckFrameIndex(AIndex);
  result := FImages[AIndex].DelayMs;
end;

function TBGRAAnimatedGif.GetFrameDisposeMode(AIndex: integer): TDisposeMode;
begin
  CheckFrameIndex(AIndex);
  result := FImages[AIndex].DisposeMode;
end;

function TBGRAAnimatedGif.GetFrameDrawMode(AIndex: integer): TDrawMode;
begin
  CheckFrameIndex(AIndex);
  result := FImages[AIndex].DrawMode;
end;

function TBGRAAnimatedGif.GetFrameHasLocalPalette(AIndex: integer): boolean;
begin
  CheckFrameIndex(AIndex);
  result := FImages[AIndex].HasLocalPalette;
end;

function TBGRAAnimatedGif.GetFrameImage(AIndex: integer): TBGRABitmap;
begin
  CheckFrameIndex(AIndex);
  result := FImages[AIndex].Image;
end;

function TBGRAAnimatedGif.GetFrameImagePos(AIndex: integer): TPoint;
begin
  CheckFrameIndex(AIndex);
  result := FImages[AIndex].Position;
end;

function TBGRAAnimatedGif.GetTimeUntilNextImage: integer;
var
  acc: double;
begin
  if Count <= 1 then result := 60*1000 else
  if (FWantedImage <> -1) or (FCurrentImage = -1) then
    result := 0
  else
  begin
    acc := FTimeAccumulator;
    if not FPaused then IncF(acc, (Now- FPrevDate) * 24 * 60 * 60 * 1000);
    if acc >= FImages[FCurrentImage].DelayMs then
      result := 0
    else
      result := round(FImages[FCurrentImage].DelayMs-FTimeAccumulator);
  end;
end;

{$IFDEF BGRABITMAP_USE_LCL}
procedure TBGRAAnimatedGif.OnTimer(Sender: TObject);
var
  waitMs: Integer;
begin
  waitMs := TimeUntilNextImageMs;
  if waitMs <= 0 then
  begin
    Changed(self);
  end else
  begin
    FTimer.Enabled := false;
    FTimer.Interval:= waitMs+5;
    FTimer.Enabled := true;
  end;
end;
{$ENDIF}

constructor TBGRAAnimatedGif.Create(filenameUTF8: string);
begin
  inherited Create;
  Init;
  LoadFromFile(filenameUTF8);
end;

constructor TBGRAAnimatedGif.Create(stream: TStream);
begin
  inherited Create;
  Init;
  LoadFromStream(stream);
end;

constructor TBGRAAnimatedGif.Create(stream: TStream; AMaxImageCount: integer);
begin
  inherited Create;
  Init;
  LoadFromStream(stream, AMaxImageCount);
end;

constructor TBGRAAnimatedGif.Create;
begin
  inherited Create;
  Init;
  LoadFromStream(nil);
end;

function TBGRAAnimatedGif.Duplicate: TBGRAAnimatedGif;
var
  i: integer;
begin
  Result := TBGRAAnimatedGif.Create;
  setlength(Result.FImages, length(FImages));
  for i := 0 to high(FImages) do
  begin
    Result.FImages[i] := FImages[i];
    FImages[i].Image.NewReference;
  end;
  Result.FWidth  := FWidth;
  Result.FHeight := FHeight;
  Result.FBackgroundColor := FBackgroundColor;
end;

procedure TBGRAAnimatedGif.Assign(ASource: TPersistent);
var
  i: integer;
  src: TBGRAAnimatedGif;
begin
  if ASource is TBGRAAnimatedGif then
  begin
    src := TBGRAAnimatedGif(ASource);
    Clear;
    FWidth  := src.Width;
    FHeight := src.Height;
    FBackgroundColor := src.BackgroundColor;
    FAspectRatio:= src.AspectRatio;
    LoopDone := 0;
    LoopCount := src.LoopCount;

    SetLength(FImages, src.Count);
    FTotalAnimationTime:= 0;
    for i := 0 to src.Count-1 do
    begin
      FImages[i] := src.FImages[i];
      FImages[i].Image := FImages[i].Image.Duplicate;
      inc(FTotalAnimationTime, FImages[i].DelayMs);
    end;
    Changed(self);
  end else
  if ASource is TFPCustomImage then
    AssignImage(TFPCustomImage(ASource), false)
  else
    inherited Assign(ASource);
end;

function TBGRAAnimatedGif.AddFrame(AImage: TFPCustomImage; X, Y: integer;
  ADelayMs: integer; ADisposeMode: TDisposeMode; AHasLocalPalette: boolean;
  ADrawMode: TDrawMode; AOwned: boolean): integer;
begin
  result := length(FImages);
  InsertFrame(result, AImage, X, Y, ADelayMs, ADisposeMode, AHasLocalPalette,
    ADrawMode, AOwned);
end;

procedure TBGRAAnimatedGif.InsertFrame(AIndex: integer; AImage: TFPCustomImage; X,
  Y: integer; ADelayMs: integer; ADisposeMode: TDisposeMode;
  AHasLocalPalette: boolean; ADrawMode: TDrawMode; AOwned: boolean);
var i: integer;
begin
  if (AIndex < 0) or (AIndex > Count) then
    raise ERangeError.Create('Index out of bounds');
  setlength(FImages, length(FImages)+1);
  if ADelayMs < 0 then ADelayMs:= 0;
  for i := high(FImages) downto AIndex+1 do
    FImages[i] := FImages[i-1];
  with FImages[AIndex] do
  begin
    if AOwned then
    begin
      if AImage is TBGRABitmap then
        Image := TBGRABitmap(AImage)
      else
      begin
        Image := TBGRABitmap.Create(AImage);
        AImage.Free;
      end;
    end else
      Image := TBGRABitmap.Create(AImage);
    Position := Point(x,y);
    DelayMs := ADelayMs;
    HasLocalPalette := AHasLocalPalette;
    DisposeMode := ADisposeMode;
    DrawMode := ADrawMode;
  end;
  inc(FTotalAnimationTime, ADelayMs);
  if AIndex <= FCurrentImage then inc(FCurrentImage);
end;

function TBGRAAnimatedGif.AddFullFrame(AImage: TFPCustomImage;
  ADelayMs: integer; AHasLocalPalette: boolean;
  ADrawMode: TDrawMode; AOwned: boolean): integer;
begin
  if (AImage.Width <> Width) or (AImage.Height <> Height) then
    raise exception.Create('Size mismatch');
  if Count > 0 then
    FrameDisposeMode[Count-1] := dmErase;
  result := AddFrame(AImage, 0,0, ADelayMs, dmErase, AHasLocalPalette,
    ADrawMode, AOwned);
end;

procedure TBGRAAnimatedGif.InsertFullFrame(AIndex: integer;
  AImage: TFPCustomImage; ADelayMs: integer; AHasLocalPalette: boolean;
  ADrawMode: TDrawMode; AOwned: boolean);
begin
  if (AIndex < 0) or (AIndex > Count) then
    raise ERangeError.Create('Index out of bounds');

  if AIndex = Count then
    AddFullFrame(AImage, ADelayMs, AHasLocalPalette, ADrawMode, AOwned)
  else
  begin
    //if previous image did not clear up, ensure that
    //next image will stay the same
    if AIndex > 0 then
      EnsureNextFrameRec(AIndex-1);

    InsertFrame(AIndex, AImage, 0,0, ADelayMs, dmErase, AHasLocalPalette,
      ADrawMode, AOwned);
  end;
end;

procedure TBGRAAnimatedGif.ReplaceFullFrame(AIndex: integer;
  AImage: TFPCustomImage; ADelayMs: integer; AHasLocalPalette: boolean;
  ADrawMode: TDrawMode; AOwned: boolean);
begin
  DeleteFrame(AIndex, True);
  if AIndex > 0 then FrameDisposeMode[AIndex-1] := dmErase;
  InsertFrame(AIndex, AImage, 0,0, ADelayMs, dmErase, AHasLocalPalette,
    ADrawMode, AOwned);
end;

procedure TBGRAAnimatedGif.OptimizeFrames;
var
  prevCurImage, i, y, x: Integer;
  prevFrame, curFrame, changeFrame: TBGRABitmap;
  scanPrev, scanNext: PBGRAPixel;
  transparentAppear: Boolean;
  rChange: TRect;
begin
  if Count <= 1 then exit;
  prevCurImage := CurrentImage;
  CurrentImage := 0;
  prevFrame := MemBitmap.Duplicate;
  for i := 1 to Count-1 do
  begin
    CurrentImage := i;
    curFrame := MemBitmap.Duplicate;
    //necessary only if transparent pixels appear
    if FrameDisposeMode[i-1] = dmErase then
    begin
      transparentAppear := false;
      for y := 0 to Height-1 do
      begin
        scanPrev := prevFrame.ScanLine[y];
        scanNext := curFrame.ScanLine[y];
        for x := 0 to Width-1 do
        begin
          if (scanNext^.alpha < 255) and (scanPrev^ <> scanNext^) then
          begin
            transparentAppear:= true;
            break;
          end;
          inc(scanPrev);
          inc(scanNext);
        end;
      end;
      if not transparentAppear then
        FrameDisposeMode[i-1] := dmKeep;
    end;

    if FrameDisposeMode[i-1] = dmKeep then
    begin
      changeFrame := curFrame.Duplicate;
      for y := 0 to Height-1 do
      begin
        scanPrev := prevFrame.ScanLine[y];
        scanNext := changeFrame.ScanLine[y];
        for x := 0 to Width-1 do
        begin
          if scanPrev^ = scanNext^ then
            scanNext^ := BGRAPixelTransparent;
          inc(scanPrev);
          inc(scanNext);
        end;
      end;
      rChange := changeFrame.GetImageBounds;
      FImages[i].Image.FreeReference;
      if rChange.IsEmpty then
        FImages[i].Image := TBGRABitmap.Create
      else
        FImages[i].Image := changeFrame.GetPart(rChange);
      FImages[i].Position := rChange.TopLeft;
      changeFrame.Free;
    end else
    if FrameDisposeMode[i-1] = dmErase then
    begin
      rChange := curFrame.GetImageBounds;
      if rChange <> RectWithSize(FImages[i].Position.x, FImages[i].Position.y,
         FImages[i].Image.Width, FImages[i].Image.Height) then
      begin
        FImages[i].Image.FreeReference;
        if rChange.IsEmpty then
          FImages[i].Image := TBGRABitmap.Create
        else
          FImages[i].Image := curFrame.GetPart(rChange);
        FImages[i].Position := rChange.TopLeft;
      end;
    end;

    prevFrame.Free;
    prevFrame := curFrame;
    curFrame := nil;
  end;
  prevFrame.Free;
  CurrentImage := prevCurImage;
end;

procedure TBGRAAnimatedGif.DeleteFrame(AIndex: integer;
  AEnsureNextFrameDoesNotChange: boolean);
var
  i: Integer;
begin
  CheckFrameIndex(AIndex);

  //if this frame did not clear up, ensure that
  //next image will stay the same
  if AEnsureNextFrameDoesNotChange then
    EnsureNextFrameRec(AIndex);

  dec(FTotalAnimationTime, FImages[AIndex].DelayMs);

  FImages[AIndex].Image.FreeReference;
  for i := AIndex to Count-2 do
    FImages[i] := FImages[i+1];
  SetLength(FImages, Count-1);

  if AIndex < CurrentImage then
    CurrentImage := CurrentImage-1
  else
  if (CurrentImage >= Count) then
  begin
    CurrentImage := 0;
    Changed(self);
  end;
end;

procedure TBGRAAnimatedGif.LoadFromStream(Stream: TStream);
begin
  LoadFromStream(Stream, maxLongint);
end;

procedure TBGRAAnimatedGif.LoadFromStream(Stream: TStream;
  AMaxImageCount: integer);
begin
  if Stream = nil then
  begin
    Clear;
    FWidth  := 0;
    FHeight := 0;
    exit;
  end;
  case DetectFileFormat(Stream) of
    ifGif: LoadFromStreamAsGif(Stream, AMaxImageCount);
    ifPng: LoadFromStreamAsPng(Stream, AMaxImageCount);
    ifUnknown: raise Exception.Create('Unknown image format');
  else
    LoadFromStreamAsStatic(Stream);
  end;
end;

procedure TBGRAAnimatedGif.LoadFromResource(AFilename: string);
var
  stream: TStream;
begin
  stream := BGRAResource.GetResourceStream(AFilename);
  try
    LoadFromStream(stream);
  finally
    stream.Free;
  end;
end;

procedure TBGRAAnimatedGif.SaveToStream(Stream: TStream);
begin
  SaveToStream(Stream, ifGif);
end;

procedure TBGRAAnimatedGif.SaveToStream(Stream: TStream; AFormat: TBGRAImageFormat);
var temp: TMemoryStream; // needed because stream position is set to zero
begin
  case AFormat of
    ifGif: SaveToStream(Stream, BGRAColorQuantizerFactory, daFloydSteinberg, AFormat);
    ifPng: SaveToStreamAsPng(Stream);
  else
    begin
      temp := TMemoryStream.Create;
      try
        MemBitmap.SaveToStreamAs(temp, AFormat);
        temp.Position := 0;
        Stream.CopyFrom(temp, temp.Size);
      finally
        temp.Free;
      end;
    end;
  end;
end;

procedure TBGRAAnimatedGif.LoadFromFile(const AFilenameUTF8: string);
var stream: TFileStreamUTF8;
begin
  stream := TFileStreamUTF8.Create(AFilenameUTF8,fmOpenRead or fmShareDenyWrite);
  try
    LoadFromStream(stream);
  finally
    Stream.Free;
  end;
end;

procedure TBGRAAnimatedGif.SaveToFile(const AFilenameUTF8: string);
var
  Stream: TFileStreamUTF8;
  imageFormat: TBGRAImageFormat;
begin
  imageFormat := SuggestImageFormat(AFilenameUTF8);
  if imageFormat = ifUnknown then imageFormat := ifGif;
  CheckSavable(imageFormat);
  Stream := TFileStreamUTF8.Create(AFilenameUTF8, fmCreate);
  try
    SaveToStream(Stream, imageFormat);
  finally
    Stream.Free;
  end;
end;

procedure TBGRAAnimatedGif.Draw(ACanvas: TCanvas; const Rect: TRect);
begin
  if FBackgroundImage <> nil then
    FreeAndNil(FBackgroundImage);
  SaveBackgroundOnce(ACanvas, Rect);

  if FPreviousVirtualScreen <> nil then
  begin
    FPreviousVirtualScreen.FreeReference;
    FPreviousVirtualScreen := nil;
  end;

  Render(Rect.Right - Rect.Left, Rect.Bottom - Rect.Top);
  FStretchedVirtualScreen.Draw(ACanvas, Rect.Left, Rect.Top, false);
  FImageChanged := False;

  FPreviousVirtualScreen := TBGRABitmap(FStretchedVirtualScreen.Duplicate);

  {$IFDEF BGRABITMAP_USE_LCL}
  FTimer.Enabled := false;
  if Count > 1 then
  begin
    FTimer.Interval := TimeUntilNextImageMs + 5;
    FTimer.Enabled := true;
  end;
  {$ENDIF}
end;

function TBGRAAnimatedGif.GetEmpty: boolean;
begin
  Result := (length(FImages) = 0);
end;

function TBGRAAnimatedGif.GetHeight: integer;
begin
  Result := FHeight;
end;

function TBGRAAnimatedGif.GetTransparent: boolean;
begin
  Result := True;
end;

function TBGRAAnimatedGif.GetWidth: integer;
begin
  Result := FWidth;
end;

procedure TBGRAAnimatedGif.SetHeight(Value: integer);
begin
  //not implemented
end;

procedure TBGRAAnimatedGif.SetTransparent(Value: boolean);
begin
  //not implemented
end;

procedure TBGRAAnimatedGif.SetWidth(Value: integer);
begin
  //not implemented
end;

procedure TBGRAAnimatedGif.ClearViewer;
begin
  FCurrentImage    := -1;
  FWantedImage     := -1;
  FTimeAccumulator := 0;

  if FStretchedVirtualScreen <> nil then
    FStretchedVirtualScreen.FreeReference;
  if FPreviousVirtualScreen <> nil then
    FPreviousVirtualScreen.FreeReference;
  FInternalVirtualScreen.Free;
  FRestoreImage.Free;
  FBackgroundImage.Free;

  FInternalVirtualScreen := nil;
  FStretchedVirtualScreen := nil;
  FRestoreImage    := nil;
  FBackgroundImage := nil;
  FPreviousVirtualScreen := nil;

  FPreviousDisposeMode := dmNone;
end;

procedure TBGRAAnimatedGif.Changed(Sender: TObject);
begin
  {$IFDEF BGRABITMAP_USE_LCL}
  if Assigned(FTimer) then FTimer.Enabled := false;
  {$ENDIF}
  inherited Changed(Sender);
end;

procedure TBGRAAnimatedGif.EnsureNextFrameRec(AIndex: integer);
var
  nextImage: TBGRABitmap;
  prevCurrentImage: integer;
begin
  if (AIndex < Count-1) and (FrameDisposeMode[AIndex] <> dmErase) then
  begin
    prevCurrentImage := CurrentImage;
    CurrentImage := AIndex+1;
    nextImage := MemBitmap.Duplicate;
    FrameImagePos[AIndex+1] := Point(0,0);
    FrameImage[AIndex+1] := nextImage;
    FrameHasLocalPalette[AIndex+1] := true;
    FreeAndNil(nextImage);
    EnsureNextFrameRec(AIndex+1);
    FrameDisposeMode[AIndex] := dmErase;
    CurrentImage := prevCurrentImage;
  end;
end;

procedure TBGRAAnimatedGif.AssignTo(Dest: TPersistent);

  procedure AssignToBitmap;
  {$IFDEF WINDOWS}
  begin
    MemBitmap.AssignToBitmap(TBitmap(Dest));
  end;
  {$ELSE}
  var
    copy: TBitmap;
  begin
    copy := MemBitmap.MakeBitmapCopy(CSSSilver, true);
    try
      TBitmap(Dest).Assign(copy);
    finally
      copy.Free;
    end;
  end;
  {$ENDIF}

  procedure AssignToFPImage;
  var
    img: TFPCustomImage;
    p: PBGRAPixel;
    yb, xb: Integer;
    bgra: TBGRABitmap;
  begin
    bgra := MemBitmap;
    img := TFPCustomImage(Dest);
    img.SetSize(bgra.Width, bgra.Height);
    for yb := 0 to bgra.Height-1 do
    begin
      p := bgra.ScanLine[yb];
      for xb := 0 to bgra.Width-1 do
      begin
        img.Colors[xb,yb] := p^.ToFPColor;
        inc(p);
      end;
    end;
  end;

begin
  if Dest is TBitmap then
    AssignToBitmap
  else if Dest is TBGRACustomBitmap then
    Dest.Assign(MemBitmap)
  else if Dest is TFPCustomImage then
    AssignToFPImage
  else
    inherited AssignTo(Dest);
end;

procedure TBGRAAnimatedGif.AssignImage(AImage: TFPCustomImage; AOwned: boolean);
begin
  Clear;
  SetSize(AImage.Width, AImage.Height);
  AddFrame(AImage, 0, 0, 100, dmKeep, False, dmSet, AOwned);
  Changed(self);
end;

procedure TBGRAAnimatedGif.LoadFromStreamAsGif(Stream: TStream;
  AMaxImageCount: integer);
var data: TGIFData;
  i: integer;
begin
  data := GIFLoadFromStream(Stream, AMaxImageCount);

  Clear;
  FWidth  := data.Width;
  FHeight := data.Height;
  FBackgroundColor := data.BackgroundColor;
  FAspectRatio:= data.AspectRatio;
  LoopCount := data.LoopCount;

  SetLength(FImages, length(data.Images));
  FTotalAnimationTime:= 0;
  for i := 0 to high(FImages) do
  begin
    FImages[i] := data.Images[i];
    inc(FTotalAnimationTime, FImages[i].DelayMs);
  end;

  Changed(self);
end;

procedure TBGRAAnimatedGif.LoadFromStreamAsPng(Stream: TStream;
  AMaxImageCount: integer);
var
  reader: TBGRAReaderPNG;
  mainBitmap, frameBitmap: TBGRABitmap;
  frameControl: TFrameControlChunk;
  i: Integer;
  disposeMode: TDisposeMode;
  drawMode: TDrawMode;
begin
  reader := TBGRAReaderPNG.Create;
  mainBitmap := nil;
  try
    mainBitmap := TBGRABitmap.Create;
    mainBitmap.CanvasDrawModeFP := dmSet;
    reader.ImageRead(Stream, mainBitmap);
    // if it is actually an animation
    if reader.FrameCount > 0 then
    begin
      Clear;
      LoopCount := reader.LoopCount;
      if mainBitmap.ResolutionY <> 0 then
        AspectRatio := mainBitmap.ResolutionX / mainBitmap.ResolutionY;
      SetSize(mainBitmap.Width, mainBitmap.Height);
      for i := 0 to reader.FrameCount-1 do
      begin
        frameControl := reader.FrameControl[i];
        case frameControl.DisposeOp of
          APNG_DISPOSE_OP_NONE: disposeMode := dmKeep;
          APNG_DISPOSE_OP_PREVIOUS:
          begin
            if i = 0 then
              disposeMode := dmErase
              else disposeMode:= dmRestore;
          end
          else {APNG_DISPOSE_OP_BACKGROUND}
          begin
            if (frameControl.OffsetX = 0) and (frameControl.OffsetY = 0)
              and (frameControl.Width = Width) and (frameControl.Height = Height) then
              disposeMode := dmErase
            else
              disposeMode:= dmEraseArea;
          end;
        end;
        if frameControl.BlendOp = APNG_BLEND_OP_OVER then
          drawMode := dmLinearBlend
        else
          drawMode := dmSet;
        if (i = reader.MainImageFrameIndex) and Assigned(mainBitmap) then
        begin
          frameBitmap := mainBitmap;
          mainBitmap := nil;
        end else
        begin
          frameBitmap := TBGRABitmap.Create;
          frameBitmap.CanvasDrawModeFP := dmSet;
          reader.LoadFrame(i, frameBitmap);
        end;
        AddFrame(frameBitmap, frameControl.OffsetX, frameControl.OffsetY,
          round(frameControl.DelayNum / frameControl.DelayDenom * 1000),
          disposeMode, false, drawMode, true);
      end;
    end else
    begin
      AssignImage(mainBitmap, true);
      mainBitmap := nil;
    end;
  finally
    mainBitmap.Free;
    reader.Free;
  end;
end;

procedure TBGRAAnimatedGif.LoadFromStreamAsStatic(Stream: TStream);
var
  image: TBGRABitmap;
begin
  image := TBGRABitmap.Create(Stream);
  AssignImage(image, true);
end;

procedure TBGRAAnimatedGif.CheckSavable(AFormat: TBGRAImageFormat);
var
  drawMode: TDrawMode;
  disposeMode: TDisposeMode;
  framePos: TPoint;
  i: integer;
begin
  CheckAnyFrame;
  case AFormat of
    ifGif: begin
      for i := 0 to Count-1 do
      begin
        drawMode := FrameDrawMode[i];
        if (drawMode <> dmSetExceptTransparent) and
         not ((drawMode in[dmSet, dmLinearBlend]) and (i = 0)) and
         not ((drawMode = dmLinearBlend) and FrameImage[i].HasSemiTransparentPixels) then
        begin
          raise Exception.Create('Draw mode not supported by GIF');
        end;
        disposeMode := FrameDisposeMode[i];
        framePos := FrameImagePos[i];
        if (disposeMode = dmEraseArea) and
          ((framePos.X <> 0) or (framePos.Y <> 0) or
           (FrameImage[i].Width <> Width) or
           (FrameImage[i].Height <> Height)) then
          raise Exception.Create('Dispose mode not supported by GIF');
      end;
    end;
  end;
end;

procedure TBGRAAnimatedGif.CheckAnyFrame;
begin
  if Count = 0 then
    raise Exception.Create('No frame defined');
  if (Width = 0) or (Height = 0) then
    raise Exception.Create('Image of zero size');
end;

procedure TBGRAAnimatedGif.SaveToStreamAsPng(Stream: TStream);
var
  writer: TBGRAWriterPNG;
  curImage, mainImageWithMargin: TBGRABitmap;
  framesToWrite: TPNGArrayOfFrameToWrite;
  fc: TFrameControlChunk;
  i: integer;
  temp: TMemoryStream; // needed because stream position is set to zero
begin
  CheckSavable(ifPng);

  writer := TBGRAWriterPNG.Create;
  mainImageWithMargin := nil;
  temp := TMemoryStream.Create;
  try
    // check if transparency will be used
    writer.UseAlpha:= false;
    for i := 0 to Count-1 do
      if FrameImage[i].HasTransparentPixels then
        writer.UseAlpha:= true;

    // define frame array to write
    SetLength(framesToWrite, Count);
    for i := 0 to Count-1 do
    begin
      curImage := FrameImage[i];
      if i = 0 then
      begin
        fc.Width := Width;
        fc.Height := Height;
        fc.OffsetX := 0;
        fc.OffsetY := 0;
        if (curImage.Width <> Width) or
          (curImage.Height <> Height) or
          (FrameImagePos[i].X <> 0) or
          (FrameImagePos[i].Y <> 0) then
        begin
          // add margin to main image
          mainImageWithMargin := TBGRABitmap.Create(Width, Height);
          mainImageWithMargin.PutImage(FrameImagePos[i].X,
            FrameImagePos[i].Y, curImage, dmSet);
          curImage.CopyPropertiesTo(mainImageWithMargin);
          curImage := mainImageWithMargin;
          mainImageWithMargin := nil;
        end;
      end else
      begin
        fc.Width := curImage.Width;
        fc.Height := curImage.Height;
        fc.OffsetX:= FrameImagePos[i].X;
        fc.OffsetY:= FrameImagePos[i].Y;
      end;
      fc.DelayNum := FrameDelayMs[i];
      fc.DelayDenom := 1000;
      case FrameDisposeMode[i] of
        dmErase, dmEraseArea: fc.DisposeOp := APNG_DISPOSE_OP_BACKGROUND;
        dmRestore: fc.DisposeOp:= APNG_DISPOSE_OP_PREVIOUS;
        else fc.DisposeOp := APNG_DISPOSE_OP_NONE;
      end;
      case FrameDrawMode[i] of
        dmLinearBlend, dmDrawWithTransparency: fc.BlendOp:= APNG_BLEND_OP_OVER;
        else fc.BlendOp:= APNG_BLEND_OP_SOURCE;
      end;
      framesToWrite[i].FrameControl := fc;
      framesToWrite[i].Image := curImage;
    end;

    writer.AnimationWrite(temp, framesToWrite[0].Image, framesToWrite);
    temp.Position := 0;
    Stream.CopyFrom(temp, temp.Size);
  finally
    temp.Free;
    mainImageWithMargin.Free;
    writer.Free;
  end;
end;

procedure TBGRAAnimatedGif.SaveToStreamAsPng(Stream: TStream;
  AQuantizer: TBGRAColorQuantizerAny; ADitheringAlgorithm: TDitheringAlgorithm);
var
  weightedPalette: TBGRAWeightedPalette;
  reducedPalette: TFPPalette;
  bmp: TBGRABitmap;
  i: Integer;
  pData: PBGRAPixel;
  quantizer: TBGRACustomColorQuantizer;
  writer: TBGRAWriterPNG;
  framesToWrite: TPNGArrayOfFrameToWrite;
  fc: TFrameControlChunk;
  curImage: TBGRABitmap;
  mainImageWithMargin: TBGRABitmap;
begin
  CheckSavable(ifPng);
  weightedPalette := nil;
  reducedPalette := nil;
  quantizer := nil;
  mainImageWithMargin := nil;
  writer := TBGRAWriterPNG.Create;
  try
    // check if transparency will be used
    writer.UseAlpha:= false;
    for i := 0 to Count-1 do
      if FrameImage[i].HasTransparentPixels then
        writer.UseAlpha:= true;

    // make global palette for all frames
    weightedPalette := TBGRAWeightedPalette.Create;
    for i := 0 to Count-1 do
      weightedPalette.IncColors(FrameImage[i]);
    quantizer := AQuantizer.Create(weightedPalette, false, 256);
    FreeAndNil(weightedPalette);
    reducedPalette := TFPPalette.Create(0);
    quantizer.ReducedPalette.AssignTo(reducedPalette);
    writer.CustomPalette := reducedPalette;

    // define frame array to write
    SetLength(framesToWrite, Count);
    for i := 0 to Count-1 do
    begin
      curImage := FrameImage[i];
      if i = 0 then
      begin
        fc.Width := Width;
        fc.Height := Height;
        fc.OffsetX := 0;
        fc.OffsetY := 0;
        if (curImage.Width <> Width) or
          (curImage.Height <> Height) or
          (FrameImagePos[i].X <> 0) or
          (FrameImagePos[i].Y <> 0) then
        begin
          // add margin to main image
          mainImageWithMargin := TBGRABitmap.Create(Width, Height);
          mainImageWithMargin.PutImage(FrameImagePos[i].X,
            FrameImagePos[i].Y, curImage, dmSet);
          curImage.CopyPropertiesTo(mainImageWithMargin);
          quantizer.ApplyDitheringInplace(ADitheringAlgorithm, mainImageWithMargin);
          curImage := mainImageWithMargin;
          mainImageWithMargin := nil;
        end;
      end else
      begin
        fc.Width := curImage.Width;
        fc.Height := curImage.Height;
        fc.OffsetX:= FrameImagePos[i].X;
        fc.OffsetY:= FrameImagePos[i].Y;
        curImage := curImage.Duplicate(true);
        quantizer.ApplyDitheringInplace(ADitheringAlgorithm, curImage);
      end;
      fc.DelayNum := FrameDelayMs[i];
      fc.DelayDenom := 1000;
      case FrameDisposeMode[i] of
        dmErase, dmEraseArea: fc.DisposeOp := APNG_DISPOSE_OP_BACKGROUND;
        dmRestore: fc.DisposeOp:= APNG_DISPOSE_OP_PREVIOUS;
        else fc.DisposeOp := APNG_DISPOSE_OP_NONE;
      end;
      case FrameDrawMode[i] of
        dmLinearBlend, dmDrawWithTransparency: fc.BlendOp:= APNG_BLEND_OP_OVER;
        else fc.BlendOp:= APNG_BLEND_OP_SOURCE;
      end;
      framesToWrite[i].FrameControl := fc;
      framesToWrite[i].Image := curImage;
    end;

    writer.AnimationWrite(Stream, framesToWrite[0].Image, framesToWrite);
  finally
    for i := 0 to high(framesToWrite) do
      framesToWrite[i].Image.Free;
    mainImageWithMargin.Free;
    quantizer.Free;
    weightedPalette.Free;
    reducedPalette.Free;
    writer.Free;
  end;
end;

procedure TBGRAAnimatedGif.SaveToStreamAsGif(Stream: TStream;
  AQuantizer: TBGRAColorQuantizerAny; ADitheringAlgorithm: TDitheringAlgorithm);
var data: TGIFData;
begin
  CheckSavable(ifGif);
  data.Height:= Height;
  data.Width := Width;
  data.AspectRatio := AspectRatio;
  data.BackgroundColor := BackgroundColor;
  data.Images := FImages;
  data.LoopCount := LoopCount;
  GIFSaveToStream(data, Stream, AQuantizer, ADitheringAlgorithm);
end;

procedure TBGRAAnimatedGif.SaveBackgroundOnce(Canvas: TCanvas; ARect: TRect);
begin
  if (FBackgroundImage <> nil) and
    ((FBackgroundImage.Width <> ARect.Right - ARect.Left) or
    (FBackgroundImage.Height <> ARect.Bottom - ARect.Top)) then
    FreeAndNil(FBackgroundImage);

  if (BackgroundMode in [gbmSaveBackgroundOnce, gbmUpdateBackgroundContinuously]) and
    (FBackgroundImage = nil) then
  begin
    FBackgroundImage := TBGRABitmap.Create(ARect.Right - ARect.Left,
      ARect.Bottom - ARect.Top);
    FBackgroundImage.GetImageFromCanvas(Canvas, ARect.Left, ARect.Top);
  end;
end;

procedure TBGRAAnimatedGif.SetCurrentImage(Index: integer);
begin
  if (Index >= 0) and (Index < Length(FImages)) then
    FWantedImage := Index;
end;

procedure TBGRAAnimatedGif.Clear;
var
  i, prevCount: integer;
begin
  inherited Clear;

  prevCount := Count;

  for i := 0 to Count - 1 do
    FImages[i].Image.FreeReference;
  FImages := nil;
  LoopDone := 0;
  LoopCount := 0;
  AspectRatio := 1;
  BackgroundColor:= clNone;
  ClearViewer;

  if not FDestroying and (prevCount <> 0) then
    Changed(self);
end;

destructor TBGRAAnimatedGif.Destroy;
begin
  FDestroying := true;
  {$IFDEF BGRABITMAP_USE_LCL}
  FTimer.Enabled := false;
  FreeAndNil(FTimer);
  {$ENDIF}
  Clear;

  if FStretchedVirtualScreen <> nil then
    FStretchedVirtualScreen.FreeReference;
  if FPreviousVirtualScreen <> nil then
    FPreviousVirtualScreen.FreeReference;
  FInternalVirtualScreen.Free;
  FRestoreImage.Free;
  FBackgroundImage.Free;
  inherited Destroy;
end;

procedure TBGRAAnimatedGif.Pause;
begin
  FPaused := True;
end;

procedure TBGRAAnimatedGif.Resume;
begin
  FPaused := False;
end;

procedure TBGRAAnimatedGif.Show(Canvas: TCanvas; ARect: TRect);
begin
  Canvas.StretchDraw(ARect, self);
end;

procedure TBGRAAnimatedGif.Update(Canvas: TCanvas; ARect: TRect);
var
  n: integer;
  PChangePix, PNewPix, PBackground, PNewBackground: PLongWord;
  oldpix, newpix, newbackpix: LongWord;
  NewBackgroundImage: TBGRABitmap;
begin
  if (BackgroundMode = gbmUpdateBackgroundContinuously) and
    (FBackgroundImage = nil) then
    BackgroundMode := gbmSaveBackgroundOnce;

  SaveBackgroundOnce(Canvas, ARect);

  case BackgroundMode of
    gbmSimplePaint:
    begin
      UpdateSimple(Canvas, ARect);
      exit;
    end;
    gbmEraseBackground:
    begin
      UpdateEraseBackground(Canvas, ARect);
      exit;
    end;
    gbmSaveBackgroundOnce, gbmUpdateBackgroundContinuously:
    begin
      if FPreviousVirtualScreen <> nil then
      begin
        if (FPreviousVirtualScreen.Width <> ARect.Right - ARect.Left) or
          (FPreviousVirtualScreen.Height <> ARect.Bottom - ARect.Top) then
        begin
          FPreviousVirtualScreen.FreeReference;
          FPreviousVirtualScreen := nil;
        end
        else
          FPreviousVirtualScreen := TBGRABitmap(FPreviousVirtualScreen.GetUnique);
      end;

      Render(ARect.Right - ARect.Left, ARect.Bottom - ARect.Top);

      if FImageChanged then
      begin
        if BackgroundMode = gbmUpdateBackgroundContinuously then
        begin
          NewBackgroundImage :=
            TBGRABitmap.Create(FStretchedVirtualScreen.Width,
            FStretchedVirtualScreen.Height);
          NewBackgroundImage.GetImageFromCanvas(Canvas, ARect.Left, ARect.Top);

          if FPreviousVirtualScreen = nil then
          begin
            FPreviousVirtualScreen := TBGRABitmap.Create(FWidth, FHeight);
            FPreviousVirtualScreen.Fill(BGRAPixelTransparent);
          end;

          PChangePix  := PLongWord(FPreviousVirtualScreen.Data);
          PNewPix     := PLongWord(FStretchedVirtualScreen.Data);
          PBackground := PLongWord(FBackgroundImage.Data);
          PNewBackground := PLongWord(NewBackgroundImage.Data);
          for n := FStretchedVirtualScreen.NbPixels - 1 downto 0 do
          begin
            oldpix := PChangePix^;

            if (oldpix and AlphaMask = AlphaMask) then //pixel opaque précédent
            begin
              newbackpix := PNewBackground^;
              if (newbackpix <> oldpix) then //stocke nouveau fond
                PBackground^ := newbackpix;
            end;

            newpix := PNewPix^;

            if newpix and AlphaMask = AlphaMask then
              PChangePix^ := newpix //pixel opaque
            else if newpix and AlphaMask > 0 then
            begin
              PChangePix^ := PBackground^;
              DrawPixelInlineNoAlphaCheck(PBGRAPixel(PChangePix), PBGRAPixel(@newpix)^);
            end
            else if PChangePix^ and AlphaMask <> 0 then
              PChangePix^ := PBackground^; //efface précédent

{               if newpix and AlphaMask > AlphaLimit then PChangePix^ := newpix or AlphaMask //pixel opaque
               else if PChangePix^ and AlphaMask <> 0 then PChangePix^ := PBackground^; //efface précédent}

            Inc(PNewPix);
            Inc(PChangePix);
            Inc(PBackground);
            Inc(PNewBackground);
          end;
          NewBackgroundImage.Free;
          FPreviousVirtualScreen.InvalidateBitmap;
          FPreviousVirtualScreen.Draw(Canvas, ARect.Left, ARect.Top, false);
          FPreviousVirtualScreen.PutImage(0, 0, FStretchedVirtualScreen, dmSet);
        end
        else
        begin
          if FPreviousVirtualScreen = nil then
          begin
            FStretchedVirtualScreen.Draw(Canvas, ARect.Left, ARect.Top, false);
            FPreviousVirtualScreen :=
              TBGRABitmap(FStretchedVirtualScreen.NewReference);
          end
          else
          begin
            PChangePix  := PLongWord(FPreviousVirtualScreen.Data);
            PNewPix     := PLongWord(FStretchedVirtualScreen.Data);
            PBackground := PLongWord(FBackgroundImage.Data);
            for n := FStretchedVirtualScreen.NbPixels - 1 downto 0 do
            begin
              newpix := PNewPix^;

              if newpix and AlphaMask = AlphaMask then
                PChangePix^ := newpix //pixel opaque
              else if newpix and AlphaMask > 0 then
              begin
                PChangePix^ := PBackground^;
                DrawPixelInlineNoAlphaCheck(PBGRAPixel(PChangePix), PBGRAPixel(@newpix)^);
              end
              else if PChangePix^ and AlphaMask <> 0 then
                PChangePix^ := PBackground^; //efface précédent

{                 if newpix and AlphaMask > AlphaLimit then PChangePix^ := newpix or AlphaMask //pixel opaque
                 else if PChangePix^ and AlphaMask <> 0 then PChangePix^ := PBackground^; //efface précédent}

              Inc(PNewPix);
              Inc(PChangePix);
              Inc(PBackground);
            end;
            FPreviousVirtualScreen.InvalidateBitmap;
            FPreviousVirtualScreen.Draw(Canvas, ARect.Left, ARect.Top, false);
            FPreviousVirtualScreen.PutImage(0, 0, FStretchedVirtualScreen, dmSet);
          end;
        end;
        FImageChanged := False;
      end;
    end;
  end;
end;

procedure TBGRAAnimatedGif.Hide(Canvas: TCanvas; ARect: TRect);
var
  shape: TBGRABitmap;
  p, pback: PBGRAPixel;
  MemEraseColor: TBGRAPixel;
  n: integer;
begin
  MemEraseColor := ColorToBGRA(EraseColor);
  if FPreviousVirtualScreen <> nil then
  begin
    if (FPreviousVirtualScreen.Width <> ARect.Right - ARect.Left) or
      (FPreviousVirtualScreen.Height <> ARect.Bottom - ARect.Top) then
    begin
      FPreviousVirtualScreen.FreeReference;
      FPreviousVirtualScreen := nil;
    end;
  end;

  case BackgroundMode of
    gbmEraseBackground, gbmSimplePaint:
    begin
      if FPreviousVirtualScreen <> nil then
      begin
        shape := TBGRABitmap(FPreviousVirtualScreen.Duplicate);
        p     := shape.Data;
        for n := shape.NbPixels - 1 downto 0 do
        begin
          if p^.alpha <> 0 then
            p^ := MemEraseColor
          else
            p^ := BGRAPixelTransparent;
          Inc(p);
        end;
        shape.Draw(Canvas, ARect.Left, ARect.Top, false);
        shape.FreeReference;
      end;
    end;
    gbmSaveBackgroundOnce, gbmUpdateBackgroundContinuously:
    begin
      if (FPreviousVirtualScreen <> nil) and (FBackgroundImage <> nil) then
      begin
        shape := TBGRABitmap(FPreviousVirtualScreen.Duplicate);
        p     := shape.Data;
        pback := FBackgroundImage.Data;
        for n := shape.NbPixels - 1 downto 0 do
        begin
          if p^.alpha <> 0 then
            p^ := pback^
          else
            p^ := BGRAPixelTransparent;
          Inc(p);
          Inc(pback);
        end;
        shape.Draw(Canvas, ARect.Left, ARect.Top, false);
        shape.FreeReference;
      end;
    end;
  end;
end;

function TBGRAAnimatedGif.MakeBitmapCopy(ABackground: TColor): TBitmap;
begin
  result := MemBitmap.MakeBitmapCopy(ABackground);
end;

procedure TBGRAAnimatedGif.UpdateEraseBackground(Canvas: TCanvas;
  ARect: TRect; DrawOnlyIfChanged: boolean);
var
  n:      integer;
  PChangePix, PNewPix: PLongWord;
  newpix: LongWord;
  MemPixEraseColor: LongWord;
begin
  if EraseColor = clNone then
  begin
    UpdateSimple(Canvas, ARect, DrawOnlyIfChanged);
    exit;
  end;

  if FPreviousVirtualScreen <> nil then
  begin
    if (FPreviousVirtualScreen.Width <> ARect.Right - ARect.Left) or
      (FPreviousVirtualScreen.Height <> ARect.Bottom - ARect.Top) then
    begin
      FPreviousVirtualScreen.FreeReference;
      FPreviousVirtualScreen := nil;
    end
    else
      FPreviousVirtualScreen := TBGRABitmap(FPreviousVirtualScreen.GetUnique);
  end;

  Render(ARect.Right - ARect.Left, ARect.Bottom - ARect.Top);
  if FImageChanged then
  begin
    PBGRAPixel(@MemPixEraseColor)^ := ColorToBGRA(EraseColor);
    if FPreviousVirtualScreen = nil then
    begin
      FStretchedVirtualScreen.Draw(Canvas, ARect.Left, ARect.Top, false);
      FPreviousVirtualScreen := TBGRABitmap(FStretchedVirtualScreen.NewReference);
    end
    else
    begin
      PChangePix := PLongWord(FPreviousVirtualScreen.Data);
      PNewPix    := PLongWord(FStretchedVirtualScreen.Data);
      for n := FStretchedVirtualScreen.NbPixels - 1 downto 0 do
      begin
        newpix := PNewPix^;

        if newpix and AlphaMask = AlphaMask then
          PChangePix^ := newpix //pixel opaque
        else if newpix and AlphaMask > 0 then
        begin
          PChangePix^ := MemPixEraseColor;
          DrawPixelInlineNoAlphaCheck(PBGRAPixel(PChangePix), PBGRAPixel(@newpix)^);
        end
        else if PChangePix^ and AlphaMask <> 0 then
          PChangePix^ := MemPixEraseColor; //efface précédent
{           if newpix and AlphaMask > AlphaLimit then PChangePix^ := newpix or AlphaMask //pixel opaque
           else if PChangePix^ and AlphaMask <> 0 then PChangePix^ := MemPixEraseColor; //efface précédent}

        Inc(PNewPix);
        Inc(PChangePix);
      end;
      FPreviousVirtualScreen.InvalidateBitmap;
      FPreviousVirtualScreen.Draw(Canvas, ARect.Left, ARect.Top, false);
      FPreviousVirtualScreen.PutImage(0, 0, FStretchedVirtualScreen, dmSet);
    end;

    FImageChanged := False;
  end;
end;

procedure TBGRAAnimatedGif.Init;
begin
  FDestroying := false;
  BackgroundMode := gbmSaveBackgroundOnce;
  BackgroundColor:= clNone;
  LoopCount := 0;
  LoopDone := 0;
  AspectRatio:= 1;
  {$IFDEF BGRABITMAP_USE_LCL}
  FTimer := TTimer.Create(nil);
  FTimer.Enabled := false;
  FTimer.OnTimer:=@OnTimer;
  {$ENDIF}
end;

function TBGRAAnimatedGif.GetBitmap: TBitmap;
begin
  Render(FWidth, FHeight);
  Result := FStretchedVirtualScreen.Bitmap;
end;

function TBGRAAnimatedGif.GetMemBitmap: TBGRABitmap;
begin
  Render(FWidth, FHeight);
  Result := FStretchedVirtualScreen;
end;

{ TBGRAAnimatedPng }

procedure TBGRAAnimatedPng.SaveToStream(Stream: TStream);
begin
  SaveToStream(Stream, ifPng);
end;

class function TBGRAAnimatedPng.GetFileExtensions: string;
begin
  Result:= 'apng';
end;

{ TBGRAReaderGIF }

procedure TBGRAReaderGIF.InternalRead(Str: TStream; Img: TFPCustomImage);
var
  gif:  TBGRAAnimatedGif;
  x, y: integer;
  Mem:  TBGRABitmap;
begin
  gif := TBGRAAnimatedGif.Create(Str, 1);
  Mem := gif.MemBitmap;
  if Img is TBGRABitmap then
  begin
    TBGRABitmap(Img).Assign(Mem);
  end
  else
  begin
    Img.SetSize(gif.Width, gif.Height);
    for y := 0 to gif.Height - 1 do
      for x := 0 to gif.Width - 1 do
        with Mem.GetPixel(x, y) do
          Img.Colors[x, y] := FPColor(red * $101, green * $101, blue *
            $101, alpha * $101);
  end;
  gif.Free;
end;

function TBGRAReaderGIF.InternalCheck(Str: TStream): boolean;
var
  GIFSignature: TGIFSignature;
  savepos:      int64;
begin
  savepos := str.Position;
  try
    fillchar({%H-}GIFSignature, sizeof(GIFSignature), 0);
    str.Read(GIFSignature, sizeof(GIFSignature));
    if (GIFSignature[1] = 'G') and (GIFSignature[2] = 'I') and
      (GIFSignature[3] = 'F') then
    begin
      Result := True;
    end
    else
      Result := False;
  except
    on ex: Exception do
      Result := False;
  end;
  str.Position := savepos;
end;

{ TBGRAWriterGIF }

procedure TBGRAWriterGIF.InternalWrite(Str: TStream; Img: TFPCustomImage);
var
  gif: TBGRAAnimatedGif;
begin
  gif := TBGRAAnimatedGif.Create;
  try
    gif.SetSize(Img.Width,Img.Height);
    gif.AddFrame(Img, 0,0,0);
    gif.SaveToStream(Str, BGRAColorQuantizerFactory, daFloydSteinberg);
  except
    on ex: EColorQuantizerMissing do
    begin
      FreeAndNil(gif);
      raise EColorQuantizerMissing.Create('Please define the color quantizer factory. You can do that with the following statements: Uses BGRAPalette, BGRAColorQuantization; BGRAColorQuantizerFactory:= TBGRAColorQuantizer;');
    end;
    on ex: Exception do
    begin
      FreeAndNil(gif);
      raise ex;
    end;
  end;
  FreeAndNil(gif);
end;

initialization
  BGRARegisterImageHandlers(ifGif, TBGRAReaderGIF, TBGRAWriterGIF,
    True, 'Animated GIF', 'gif');

  {$IFDEF BGRABITMAP_USE_LCL}
  //Lazarus Picture
  TPicture.RegisterFileFormat('gif', 'Animated GIF', TBGRAAnimatedGif);
  TPicture.RegisterFileFormat('apng', 'Animated PNG', TBGRAAnimatedPng);
  {$ENDIF}
end.

