1918 lines
57 KiB
ObjectPascal

// 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
DefaultBGRAImageReader[ifGif] := TBGRAReaderGIF;
DefaultBGRAImageWriter[ifGif] := TBGRAWriterGIF;
{$IFDEF BGRABITMAP_USE_LCL}
//Lazarus Picture
TPicture.RegisterFileFormat('gif', 'Animated GIF', TBGRAAnimatedGif);
TPicture.RegisterFileFormat('apng', 'Animated PNG', TBGRAAnimatedPng);
{$ENDIF}
end.