1 // SPDX-License-Identifier: LGPL-3.0-linking-exception
2 unit BGRAAnimatedGif;
3 
4 {$mode objfpc}{$H+}
5 {$i bgrabitmap.inc}
6 
7 interface
8 
9 uses
10   BGRAClasses, SysUtils, BGRAGraphics, FPImage, BGRABitmap, BGRABitmapTypes,
11   BGRAPalette, BGRAGifFormat;
12 
13 type
14   TDisposeMode = BGRAGifFormat.TDisposeMode;
15   TGifSubImage = BGRAGifFormat.TGifSubImage;
16   TGifSubImageArray = BGRAGifFormat.TGifSubImageArray;
17 
18   //how to deal with the background under the GIF animation
19   TGifBackgroundMode = (gbmSimplePaint, gbmEraseBackground,
20     gbmSaveBackgroundOnce, gbmUpdateBackgroundContinuously);
21 
22   { TBGRAAnimatedGif }
23 
24   TBGRAAnimatedGif = class(TGraphic)
25   private
26     FAspectRatio: single;
27     FWidth, FHeight:  integer;
28     FBackgroundColor: TColor;
29 
30     FPrevDate: TDateTime;
31     FPaused:   boolean;
32     FTimeAccumulator: double;
33     FCurrentImage, FWantedImage: integer;
34     FTotalAnimationTime: int64;
35     FPreviousDisposeMode: TDisposeMode;
36 
37     FBackgroundImage, FPreviousVirtualScreen, FStretchedVirtualScreen,
38     FInternalVirtualScreen, FRestoreImage: TBGRABitmap;
39     FImageChanged: boolean;
40 
41     procedure CheckFrameIndex(AIndex: integer);
GetAverageDelayMsnull42     function GetAverageDelayMs: integer;
GetCountnull43     function GetCount: integer;
GetFrameDelayMsnull44     function GetFrameDelayMs(AIndex: integer): integer;
GetFrameDisposeModenull45     function GetFrameDisposeMode(AIndex: integer): TDisposeMode;
GetFrameHasLocalPalettenull46     function GetFrameHasLocalPalette(AIndex: integer): boolean;
GetFrameImagenull47     function GetFrameImage(AIndex: integer): TBGRABitmap;
GetFrameImagePosnull48     function GetFrameImagePos(AIndex: integer): TPoint;
GetTimeUntilNextImagenull49     function GetTimeUntilNextImage: integer;
50     procedure Render(StretchWidth, StretchHeight: integer);
51     procedure SetAspectRatio(AValue: single);
52     procedure SetBackgroundColor(AValue: TColor);
53     procedure SetFrameDelayMs(AIndex: integer; AValue: integer);
54     procedure SetFrameDisposeMode(AIndex: integer; AValue: TDisposeMode);
55     procedure SetFrameHasLocalPalette(AIndex: integer; AValue: boolean);
56     procedure SetFrameImage(AIndex: integer; AValue: TBGRABitmap);
57     procedure SetFrameImagePos(AIndex: integer; AValue: TPoint);
58     procedure UpdateSimple(Canvas: TCanvas; ARect: TRect;
59       DrawOnlyIfChanged: boolean = True);
60     procedure UpdateEraseBackground(Canvas: TCanvas; ARect: TRect;
61       DrawOnlyIfChanged: boolean = True);
62     procedure Init;
GetBitmapnull63     function GetBitmap: TBitmap;
GetMemBitmapnull64     function GetMemBitmap: TBGRABitmap;
65     procedure SaveBackgroundOnce(Canvas: TCanvas; ARect: TRect);
66     procedure SetCurrentImage(Index: integer);
67 
68   protected
69     FImages: TGifSubImageArray;
70 
71     {TGraphic}
72     procedure Draw(ACanvas: TCanvas; const Rect: TRect); override;
GetEmptynull73     function GetEmpty: boolean; override;
GetHeightnull74     function GetHeight: integer; override;
GetTransparentnull75     function GetTransparent: boolean; override;
GetWidthnull76     function GetWidth: integer; override;
77     procedure SetHeight({%H-}Value: integer); override;
78     procedure SetTransparent({%H-}Value: boolean); override;
79     procedure SetWidth({%H-}Value: integer); override;
80     procedure ClearViewer; virtual;
81 
82   public
83     EraseColor:     TColor;
84     BackgroundMode: TGifBackgroundMode;
85     LoopCount:      Word;
86     LoopDone:       Integer;
87 
88     constructor Create(filenameUTF8: string); overload;
89     constructor Create(stream: TStream); overload;
90     constructor Create(stream: TStream; AMaxImageCount: integer); overload;
91     constructor Create; overload; override;
Duplicatenull92     function Duplicate: TBGRAAnimatedGif;
AddFramenull93     function AddFrame(AImage: TFPCustomImage; X,Y: integer; ADelayMs: integer;
94       ADisposeMode: TDisposeMode = dmErase; AHasLocalPalette: boolean = false) : integer;
95     procedure InsertFrame(AIndex: integer; AImage: TFPCustomImage; X,Y: integer; ADelayMs: integer;
96       ADisposeMode: TDisposeMode = dmErase; AHasLocalPalette: boolean = false);
97     procedure DeleteFrame(AIndex: integer; AEnsureNextFrameDoesNotChange: boolean);
98 
99     //add a frame that replaces completely the previous one
AddFullFramenull100     function AddFullFrame(AImage: TFPCustomImage; ADelayMs: integer;
101                           AHasLocalPalette: boolean = true): integer;
102     procedure InsertFullFrame(AIndex: integer;
103                               AImage: TFPCustomImage; ADelayMs: integer;
104                               AHasLocalPalette: boolean = true);
105     procedure ReplaceFullFrame(AIndex: integer;
106                               AImage: TFPCustomImage; ADelayMs: integer;
107                               AHasLocalPalette: boolean = true);
108 
109     {TGraphic}
110     procedure LoadFromStream(Stream: TStream); overload; override;
111     procedure LoadFromStream(Stream: TStream; AMaxImageCount: integer); overload;
112     procedure LoadFromResource(AFilename: string);
113     procedure SaveToStream(Stream: TStream); overload; override;
114     procedure LoadFromFile(const AFilenameUTF8: string); override;
115     procedure SaveToFile(const AFilenameUTF8: string); override;
GetFileExtensionsnull116     class function GetFileExtensions: string; override;
117 
118     procedure SetSize(AWidth,AHeight: integer); virtual;
119     procedure SaveToStream(Stream: TStream; AQuantizer: TBGRAColorQuantizerAny;
120       ADitheringAlgorithm: TDitheringAlgorithm); overload; virtual;
121     procedure Clear; override;
122     destructor Destroy; override;
123     procedure Pause;
124     procedure Resume;
125 
126     procedure Show(Canvas: TCanvas; ARect: TRect); overload;
127     procedure Update(Canvas: TCanvas; ARect: TRect); overload;
128     procedure Hide(Canvas: TCanvas; ARect: TRect); overload;
129 
130     property BackgroundColor: TColor Read FBackgroundColor write SetBackgroundColor;
131     property Count: integer Read GetCount;
132     property Width: integer Read FWidth;
133     property Height: integer Read FHeight;
134     property Paused: boolean Read FPaused;
135     property Bitmap: TBitmap Read GetBitmap;
136     property MemBitmap: TBGRABitmap Read GetMemBitmap;
137     property CurrentImage: integer Read FCurrentImage Write SetCurrentImage;
138     property TimeUntilNextImageMs: integer read GetTimeUntilNextImage;
139     property FrameImage[AIndex: integer]: TBGRABitmap read GetFrameImage write SetFrameImage;
140     property FrameHasLocalPalette[AIndex: integer]: boolean read GetFrameHasLocalPalette write SetFrameHasLocalPalette;
141     property FrameImagePos[AIndex: integer]: TPoint read GetFrameImagePos write SetFrameImagePos;
142     property FrameDelayMs[AIndex: integer]: integer read GetFrameDelayMs write SetFrameDelayMs;
143     property FrameDisposeMode[AIndex: integer]: TDisposeMode read GetFrameDisposeMode write SetFrameDisposeMode;
144     property AspectRatio: single read FAspectRatio write SetAspectRatio;
145     property TotalAnimationTimeMs: Int64 read FTotalAnimationTime;
146     property AverageDelayMs: integer read GetAverageDelayMs;
147   end;
148 
149   { TBGRAReaderGIF }
150 
151   TBGRAReaderGIF = class(TFPCustomImageReader)
152   protected
153     procedure InternalRead(Str: TStream; Img: TFPCustomImage); override;
InternalChecknull154     function InternalCheck(Str: TStream): boolean; override;
155   end;
156 
157   { TBGRAWriterGIF }
158 
159   TBGRAWriterGIF = class(TFPCustomImageWriter)
160   protected
161     procedure InternalWrite(Str: TStream; Img: TFPCustomImage); override;
162   end;
163 
164 const
165   GifBackgroundModeStr: array[TGifBackgroundMode] of string =
166     ('gbmSimplePaint', 'gbmEraseBackground', 'gbmSaveBackgroundOnce',
167     'gbmUpdateBackgroundContinuously');
168 
169 implementation
170 
171 uses BGRABlend, BGRAUTF8{$IFDEF BGRABITMAP_USE_LCL}, Graphics{$ENDIF};
172 
173 const
174   {$IFDEF ENDIAN_LITTLE}
175   AlphaMask = $FF000000;
176   {$ELSE}
177   AlphaMask = $000000FF;
178   {$ENDIF}
179 
180 
181 { TBGRAAnimatedGif }
182 
TBGRAAnimatedGif.GetFileExtensionsnull183 class function TBGRAAnimatedGif.GetFileExtensions: string;
184 begin
185   Result := 'gif';
186 end;
187 
188 procedure TBGRAAnimatedGif.SetSize(AWidth, AHeight: integer);
189 begin
190   ClearViewer;
191   FWidth := AWidth;
192   FHeight := AHeight;
193 end;
194 
195 procedure TBGRAAnimatedGif.SaveToStream(Stream: TStream;
196       AQuantizer: TBGRAColorQuantizerAny;
197       ADitheringAlgorithm: TDitheringAlgorithm);
198 var data: TGIFData;
199 begin
200   data.Height:= Height;
201   data.Width := Width;
202   data.AspectRatio := 1;
203   data.BackgroundColor := BackgroundColor;
204   data.Images := FImages;
205   data.LoopCount := LoopCount;
206   GIFSaveToStream(data, Stream, AQuantizer, ADitheringAlgorithm);
207 end;
208 
209 procedure TBGRAAnimatedGif.Render(StretchWidth, StretchHeight: integer);
210 var
211   curDate: TDateTime;
212   previousImage, nextImage: integer;
213 
214 begin
215   if FInternalVirtualScreen = nil then
216   begin
217     FInternalVirtualScreen := TBGRABitmap.Create(FWidth, FHeight);
218     if (Count = 0) and (BackgroundColor <> clNone) then
219       FInternalVirtualScreen.Fill(BackgroundColor)
220     else
221       FInternalVirtualScreen.Fill(BGRAPixelTransparent);
222     FImageChanged := True;
223   end;
224 
225   if Count = 0 then
226     exit;
227 
228   previousImage := FCurrentImage;
229 
230   curDate := Now;
231   if FWantedImage <> -1 then
232   begin
233     nextImage    := FWantedImage;
234     FTimeAccumulator := 0;
235     FWantedImage := -1;
236   end
237   else
238   if FCurrentImage = -1 then
239   begin
240     nextImage := 0;
241     FTimeAccumulator := 0;
242     FPreviousDisposeMode := dmNone;
243   end
244   else
245   begin
246     if not FPaused then
247       IncF(FTimeAccumulator, (curDate - FPrevDate) * 24 * 60 * 60 * 1000);
248     if FTotalAnimationTime > 0 then FTimeAccumulator:= frac(FTimeAccumulator/FTotalAnimationTime)*FTotalAnimationTime;
249     nextImage := FCurrentImage;
250     while FTimeAccumulator > FImages[nextImage].DelayMs do
251     begin
252       DecF(FTimeAccumulator, FImages[nextImage].DelayMs);
253       Inc(nextImage);
254       if nextImage >= Count then
255       begin
256         if (LoopCount > 0) and (LoopDone >= LoopCount-1) then
257         begin
258           LoopDone := LoopCount;
259           dec(nextImage);
260           break;
261         end else
262         begin
263           nextImage := 0;
264           inc(LoopDone);
265         end;
266       end;
267 
268       if nextImage = previousImage then
269       begin
270         if not ((LoopCount > 0) and (LoopDone >= LoopCount-1)) then
271         begin
272           Inc(nextImage);
273           if nextImage >= Count then
274             nextImage := 0;
275         end;
276         break;
277       end;
278     end;
279   end;
280   FPrevDate := curDate;
281 
282   while FCurrentImage <> nextImage do
283   begin
284     Inc(FCurrentImage);
285     if FCurrentImage >= Count then
286     begin
287       FCurrentImage := 0;
288       FPreviousDisposeMode := dmErase;
289     end;
290 
291     case FPreviousDisposeMode of
292       dmErase: FInternalVirtualScreen.Fill(BGRAPixelTransparent);
293       dmRestore: if FRestoreImage <> nil then
294           FInternalVirtualScreen.PutImage(0, 0, FRestoreImage, dmSet);
295     end;
296 
297     with FImages[FCurrentImage] do
298     begin
299       if disposeMode = dmRestore then
300       begin
301         if FRestoreImage = nil then
302           FRestoreImage := TBGRABitmap.Create(FWidth, FHeight);
303         FRestoreImage.PutImage(0, 0, FInternalVirtualScreen, dmSet);
304       end;
305 
306       if Image <> nil then
307         FInternalVirtualScreen.PutImage(Position.X, Position.Y, Image,
308           dmSetExceptTransparent);
309       FPreviousDisposeMode := DisposeMode;
310     end;
311 
312     FImageChanged := True;
313     previousImage := FCurrentImage;
314     FInternalVirtualScreen.InvalidateBitmap;
315   end;
316 
317   if FStretchedVirtualScreen <> nil then
318     FStretchedVirtualScreen.FreeReference;
319   if (FInternalVirtualScreen.Width = StretchWidth) and
320     (FInternalVirtualScreen.Height = StretchHeight) then
321     FStretchedVirtualScreen := TBGRABitmap(FInternalVirtualScreen.NewReference)
322   else
323     FStretchedVirtualScreen :=
324       TBGRABitmap(FInternalVirtualScreen.Resample(StretchWidth, StretchHeight));
325 end;
326 
327 procedure TBGRAAnimatedGif.SetAspectRatio(AValue: single);
328 begin
329   if AValue < 0.25 then AValue := 0.25;
330   if AValue > 4 then AValue := 4;
331   if FAspectRatio=AValue then Exit;
332   FAspectRatio:=AValue;
333 end;
334 
335 procedure TBGRAAnimatedGif.SetBackgroundColor(AValue: TColor);
336 begin
337   if FBackgroundColor=AValue then Exit;
338   FBackgroundColor:=AValue;
339 end;
340 
341 procedure TBGRAAnimatedGif.SetFrameDelayMs(AIndex: integer; AValue: integer);
342 begin
343   CheckFrameIndex(AIndex);
344   if AValue < 0 then AValue := 0;
345   FTotalAnimationTime := FTotalAnimationTime + AValue - FImages[AIndex].DelayMs;
346   FImages[AIndex].DelayMs := AValue;
347 end;
348 
349 procedure TBGRAAnimatedGif.SetFrameDisposeMode(AIndex: integer;
350   AValue: TDisposeMode);
351 begin
352   CheckFrameIndex(AIndex);
353   FImages[AIndex].DisposeMode := AValue;
354 end;
355 
356 procedure TBGRAAnimatedGif.SetFrameHasLocalPalette(AIndex: integer;
357   AValue: boolean);
358 begin
359   CheckFrameIndex(AIndex);
360   FImages[AIndex].HasLocalPalette := AValue;
361 
362 end;
363 
364 procedure TBGRAAnimatedGif.SetFrameImage(AIndex: integer; AValue: TBGRABitmap);
365 var ACopy: TBGRABitmap;
366 begin
367   CheckFrameIndex(AIndex);
368   ACopy := AValue.Duplicate;
369   FImages[AIndex].Image.FreeReference;
370   FImages[AIndex].Image := ACopy;
371 end;
372 
373 procedure TBGRAAnimatedGif.SetFrameImagePos(AIndex: integer; AValue: TPoint);
374 begin
375   CheckFrameIndex(AIndex);
376   FImages[AIndex].Position := AValue;
377 end;
378 
379 procedure TBGRAAnimatedGif.UpdateSimple(Canvas: TCanvas; ARect: TRect;
380   DrawOnlyIfChanged: boolean = True);
381 begin
382   if FPreviousVirtualScreen <> nil then
383   begin
384     FPreviousVirtualScreen.FreeReference;
385     FPreviousVirtualScreen := nil;
386   end;
387 
388   Render(ARect.Right - ARect.Left, ARect.Bottom - ARect.Top);
389   if FImageChanged then
390   begin
391     FStretchedVirtualScreen.Draw(Canvas, ARect.Left, ARect.Top, False);
392     FImageChanged := False;
393   end
394   else
395   if not DrawOnlyIfChanged then
396     FStretchedVirtualScreen.Draw(Canvas, ARect.Left, ARect.Top, False);
397 
398   FPreviousVirtualScreen := TBGRABitmap(FStretchedVirtualScreen.NewReference);
399 end;
400 
401 procedure TBGRAAnimatedGif.CheckFrameIndex(AIndex: integer);
402 begin
403   if (AIndex < 0) or (AIndex >= Count) then Raise ERangeError.Create('Index out of bounds');
404 end;
405 
TBGRAAnimatedGif.GetAverageDelayMsnull406 function TBGRAAnimatedGif.GetAverageDelayMs: integer;
407 var sum: int64;
408   i: Integer;
409 begin
410   if Count > 0 then
411   begin
412     sum := 0;
413     for i := 0 to Count-1 do
414       inc(sum, FrameDelayMs[i]);
415     result := sum div Count;
416   end else
417     result := 100; //default
418 end;
419 
GetCountnull420 function TBGRAAnimatedGif.GetCount: integer;
421 begin
422   Result := length(FImages);
423 end;
424 
GetFrameDelayMsnull425 function TBGRAAnimatedGif.GetFrameDelayMs(AIndex: integer): integer;
426 begin
427   CheckFrameIndex(AIndex);
428   result := FImages[AIndex].DelayMs;
429 end;
430 
TBGRAAnimatedGif.GetFrameDisposeModenull431 function TBGRAAnimatedGif.GetFrameDisposeMode(AIndex: integer): TDisposeMode;
432 begin
433   CheckFrameIndex(AIndex);
434   result := FImages[AIndex].DisposeMode;
435 end;
436 
TBGRAAnimatedGif.GetFrameHasLocalPalettenull437 function TBGRAAnimatedGif.GetFrameHasLocalPalette(AIndex: integer): boolean;
438 begin
439   CheckFrameIndex(AIndex);
440   result := FImages[AIndex].HasLocalPalette;
441 end;
442 
GetFrameImagenull443 function TBGRAAnimatedGif.GetFrameImage(AIndex: integer): TBGRABitmap;
444 begin
445   CheckFrameIndex(AIndex);
446   result := FImages[AIndex].Image;
447 end;
448 
TBGRAAnimatedGif.GetFrameImagePosnull449 function TBGRAAnimatedGif.GetFrameImagePos(AIndex: integer): TPoint;
450 begin
451   CheckFrameIndex(AIndex);
452   result := FImages[AIndex].Position;
453 end;
454 
TBGRAAnimatedGif.GetTimeUntilNextImagenull455 function TBGRAAnimatedGif.GetTimeUntilNextImage: integer;
456 var
457   acc: double;
458 begin
459   if Count <= 1 then result := 60*1000 else
460   if (FWantedImage <> -1) or (FCurrentImage = -1) then
461     result := 0
462   else
463   begin
464     acc := FTimeAccumulator;
465     if not FPaused then IncF(acc, (Now- FPrevDate) * 24 * 60 * 60 * 1000);
466     if acc >= FImages[FCurrentImage].DelayMs then
467       result := 0
468     else
469       result := round(FImages[FCurrentImage].DelayMs-FTimeAccumulator);
470   end;
471 end;
472 
473 constructor TBGRAAnimatedGif.Create(filenameUTF8: string);
474 begin
475   inherited Create;
476   Init;
477   LoadFromFile(filenameUTF8);
478 end;
479 
480 constructor TBGRAAnimatedGif.Create(stream: TStream);
481 begin
482   inherited Create;
483   Init;
484   LoadFromStream(stream);
485 end;
486 
487 constructor TBGRAAnimatedGif.Create(stream: TStream; AMaxImageCount: integer);
488 begin
489   inherited Create;
490   Init;
491   LoadFromStream(stream, AMaxImageCount);
492 end;
493 
494 constructor TBGRAAnimatedGif.Create;
495 begin
496   inherited Create;
497   Init;
498   LoadFromStream(nil);
499 end;
500 
Duplicatenull501 function TBGRAAnimatedGif.Duplicate: TBGRAAnimatedGif;
502 var
503   i: integer;
504 begin
505   Result := TBGRAAnimatedGif.Create;
506   setlength(Result.FImages, length(FImages));
507   for i := 0 to high(FImages) do
508   begin
509     Result.FImages[i] := FImages[i];
510     FImages[i].Image.NewReference;
511   end;
512   Result.FWidth  := FWidth;
513   Result.FHeight := FHeight;
514   Result.FBackgroundColor := FBackgroundColor;
515 end;
516 
AddFramenull517 function TBGRAAnimatedGif.AddFrame(AImage: TFPCustomImage; X, Y: integer;
518   ADelayMs: integer; ADisposeMode: TDisposeMode; AHasLocalPalette: boolean
519   ): integer;
520 begin
521   result := length(FImages);
522   setlength(FImages, length(FImages)+1);
523   if ADelayMs < 0 then ADelayMs:= 0;
524   with FImages[result] do
525   begin
526     Image := TBGRABitmap.Create(AImage);
527     Position := Point(x,y);
528     DelayMs := ADelayMs;
529     HasLocalPalette := AHasLocalPalette;
530     DisposeMode := ADisposeMode;
531   end;
532   inc(FTotalAnimationTime, ADelayMs);
533 end;
534 
535 procedure TBGRAAnimatedGif.InsertFrame(AIndex: integer; AImage: TFPCustomImage; X,
536   Y: integer; ADelayMs: integer; ADisposeMode: TDisposeMode;
537   AHasLocalPalette: boolean);
538 var i: integer;
539 begin
540   if (AIndex < 0) or (AIndex > Count) then
541     raise ERangeError.Create('Index out of bounds');
542   setlength(FImages, length(FImages)+1);
543   if ADelayMs < 0 then ADelayMs:= 0;
544   for i := high(FImages) downto AIndex+1 do
545     FImages[i] := FImages[i-1];
546   with FImages[AIndex] do
547   begin
548     Image := TBGRABitmap.Create(AImage);
549     Position := Point(x,y);
550     DelayMs := ADelayMs;
551     HasLocalPalette := AHasLocalPalette;
552     DisposeMode := ADisposeMode;
553   end;
554   inc(FTotalAnimationTime, ADelayMs);
555 end;
556 
AddFullFramenull557 function TBGRAAnimatedGif.AddFullFrame(AImage: TFPCustomImage;
558   ADelayMs: integer; AHasLocalPalette: boolean): integer;
559 begin
560   if (AImage.Width <> Width) or (AImage.Height <> Height) then
561     raise exception.Create('Size mismatch');
562   if Count > 0 then
563     FrameDisposeMode[Count-1] := dmErase;
564   result := AddFrame(AImage, 0,0, ADelayMs, dmErase, AHasLocalPalette);
565 end;
566 
567 procedure TBGRAAnimatedGif.InsertFullFrame(AIndex: integer;
568   AImage: TFPCustomImage; ADelayMs: integer; AHasLocalPalette: boolean);
569 var nextImage: TBGRABitmap;
570 begin
571   if (AIndex < 0) or (AIndex > Count) then
572     raise ERangeError.Create('Index out of bounds');
573 
574   if AIndex = Count then
575     AddFullFrame(AImage, ADelayMs, AHasLocalPalette)
576   else
577   begin
578     //if previous image did not clear up, ensure that
579     //next image will stay the same
580     if (AIndex > 0) and (FrameDisposeMode[AIndex-1] <> dmErase) then
581     begin
582       CurrentImage := AIndex;
583       nextImage := MemBitmap.Duplicate;
584       FrameImagePos[AIndex] := Point(0,0);
585       FrameImage[AIndex] := nextImage;
586       FrameHasLocalPalette[AIndex] := true;
587       FreeAndNil(nextImage);
588 
589       FrameDisposeMode[AIndex-1] := dmErase;
590     end;
591 
592     InsertFrame(AIndex, AImage, 0,0, ADelayMs, dmErase, AHasLocalPalette);
593   end;
594 end;
595 
596 procedure TBGRAAnimatedGif.ReplaceFullFrame(AIndex: integer;
597   AImage: TFPCustomImage; ADelayMs: integer; AHasLocalPalette: boolean);
598 begin
599   DeleteFrame(AIndex, True);
600   if AIndex > 0 then FrameDisposeMode[AIndex-1] := dmErase;
601   InsertFrame(AIndex, AImage, 0,0, ADelayMs, dmErase, AHasLocalPalette);
602 end;
603 
604 procedure TBGRAAnimatedGif.DeleteFrame(AIndex: integer;
605   AEnsureNextFrameDoesNotChange: boolean);
606 var
607   nextImage: TBGRABitmap;
608   i: Integer;
609 begin
610   CheckFrameIndex(AIndex);
611 
612   //if this frame did not clear up, ensure that
613   //next image will stay the same
614   if AEnsureNextFrameDoesNotChange and
615     ((AIndex < Count-1) and (FrameDisposeMode[AIndex] <> dmErase)) then
616   begin
617     CurrentImage := AIndex+1;
618     nextImage := MemBitmap.Duplicate;
619     FrameImagePos[AIndex+1] := Point(0,0);
620     FrameImage[AIndex+1] := nextImage;
621     FrameHasLocalPalette[AIndex+1] := true;
622     FreeAndNil(nextImage);
623   end;
624 
625   dec(FTotalAnimationTime, FImages[AIndex].DelayMs);
626 
627   FImages[AIndex].Image.FreeReference;
628   for i := AIndex to Count-2 do
629     FImages[i] := FImages[i+1];
630   SetLength(FImages, Count-1);
631 
632   if (CurrentImage >= Count) then
633     CurrentImage := 0;
634 end;
635 
636 procedure TBGRAAnimatedGif.LoadFromStream(Stream: TStream);
637 begin
638   LoadFromStream(Stream, maxLongint);
639 end;
640 
641 procedure TBGRAAnimatedGif.LoadFromStream(Stream: TStream;
642   AMaxImageCount: integer);
643 var data: TGIFData;
644   i: integer;
645 begin
646   data := GIFLoadFromStream(Stream, AMaxImageCount);
647 
648   ClearViewer;
649   Clear;
650   FWidth  := data.Width;
651   FHeight := data.Height;
652   FBackgroundColor := data.BackgroundColor;
653   FAspectRatio:= data.AspectRatio;
654   LoopDone := 0;
655   LoopCount := data.LoopCount;
656 
657   SetLength(FImages, length(data.Images));
658   FTotalAnimationTime:= 0;
659   for i := 0 to high(FImages) do
660   begin
661     FImages[i] := data.Images[i];
662     inc(FTotalAnimationTime, FImages[i].DelayMs);
663   end;
664 end;
665 
666 procedure TBGRAAnimatedGif.LoadFromResource(AFilename: string);
667 var
668   stream: TStream;
669 begin
670   stream := BGRAResource.GetResourceStream(AFilename);
671   try
672     LoadFromStream(stream);
673   finally
674     stream.Free;
675   end;
676 end;
677 
678 procedure TBGRAAnimatedGif.SaveToStream(Stream: TStream);
679 begin
680   SaveToStream(Stream, BGRAColorQuantizerFactory, daFloydSteinberg);
681 end;
682 
683 procedure TBGRAAnimatedGif.LoadFromFile(const AFilenameUTF8: string);
684 var stream: TFileStreamUTF8;
685 begin
686   stream := TFileStreamUTF8.Create(AFilenameUTF8,fmOpenRead or fmShareDenyWrite);
687   try
688     LoadFromStream(stream);
689   finally
690     Stream.Free;
691   end;
692 end;
693 
694 procedure TBGRAAnimatedGif.SaveToFile(const AFilenameUTF8: string);
695 var
696   Stream: TFileStreamUTF8;
697 begin
698   Stream := TFileStreamUTF8.Create(AFilenameUTF8, fmCreate);
699   try
700     SaveToStream(Stream);
701   finally
702     Stream.Free;
703   end;
704 end;
705 
706 procedure TBGRAAnimatedGif.Draw(ACanvas: TCanvas; const Rect: TRect);
707 begin
708   if FBackgroundImage <> nil then
709     FreeAndNil(FBackgroundImage);
710   SaveBackgroundOnce(ACanvas, Rect);
711 
712   if FPreviousVirtualScreen <> nil then
713   begin
714     FPreviousVirtualScreen.FreeReference;
715     FPreviousVirtualScreen := nil;
716   end;
717 
718   Render(Rect.Right - Rect.Left, Rect.Bottom - Rect.Top);
719   FStretchedVirtualScreen.Draw(ACanvas, Rect.Left, Rect.Top, false);
720   FImageChanged := False;
721 
722   FPreviousVirtualScreen := TBGRABitmap(FStretchedVirtualScreen.Duplicate);
723 end;
724 
TBGRAAnimatedGif.GetEmptynull725 function TBGRAAnimatedGif.GetEmpty: boolean;
726 begin
727   Result := (length(FImages) = 0);
728 end;
729 
TBGRAAnimatedGif.GetHeightnull730 function TBGRAAnimatedGif.GetHeight: integer;
731 begin
732   Result := FHeight;
733 end;
734 
TBGRAAnimatedGif.GetTransparentnull735 function TBGRAAnimatedGif.GetTransparent: boolean;
736 begin
737   Result := True;
738 end;
739 
GetWidthnull740 function TBGRAAnimatedGif.GetWidth: integer;
741 begin
742   Result := FWidth;
743 end;
744 
745 procedure TBGRAAnimatedGif.SetHeight(Value: integer);
746 begin
747   //not implemented
748 end;
749 
750 procedure TBGRAAnimatedGif.SetTransparent(Value: boolean);
751 begin
752   //not implemented
753 end;
754 
755 procedure TBGRAAnimatedGif.SetWidth(Value: integer);
756 begin
757   //not implemented
758 end;
759 
760 procedure TBGRAAnimatedGif.ClearViewer;
761 begin
762   FCurrentImage    := -1;
763   FWantedImage     := -1;
764   FTimeAccumulator := 0;
765 
766   if FStretchedVirtualScreen <> nil then
767     FStretchedVirtualScreen.FreeReference;
768   if FPreviousVirtualScreen <> nil then
769     FPreviousVirtualScreen.FreeReference;
770   FInternalVirtualScreen.Free;
771   FRestoreImage.Free;
772   FBackgroundImage.Free;
773 
774   FInternalVirtualScreen := nil;
775   FStretchedVirtualScreen := nil;
776   FRestoreImage    := nil;
777   FBackgroundImage := nil;
778   FPreviousVirtualScreen := nil;
779 
780   FPreviousDisposeMode := dmNone;
781 end;
782 
783 procedure TBGRAAnimatedGif.SaveBackgroundOnce(Canvas: TCanvas; ARect: TRect);
784 begin
785   if (FBackgroundImage <> nil) and
786     ((FBackgroundImage.Width <> ARect.Right - ARect.Left) or
787     (FBackgroundImage.Height <> ARect.Bottom - ARect.Top)) then
788     FreeAndNil(FBackgroundImage);
789 
790   if (BackgroundMode in [gbmSaveBackgroundOnce, gbmUpdateBackgroundContinuously]) and
791     (FBackgroundImage = nil) then
792   begin
793     FBackgroundImage := TBGRABitmap.Create(ARect.Right - ARect.Left,
794       ARect.Bottom - ARect.Top);
795     FBackgroundImage.GetImageFromCanvas(Canvas, ARect.Left, ARect.Top);
796   end;
797 end;
798 
799 procedure TBGRAAnimatedGif.SetCurrentImage(Index: integer);
800 begin
801   if (Index >= 0) and (Index < Length(FImages)) then
802     FWantedImage := Index;
803 end;
804 
805 procedure TBGRAAnimatedGif.Clear;
806 var
807   i: integer;
808 begin
809   inherited Clear;
810 
811   for i := 0 to Count - 1 do
812     FImages[i].Image.FreeReference;
813   FImages := nil;
814   LoopDone := 0;
815   LoopCount := 0;
816 end;
817 
818 destructor TBGRAAnimatedGif.Destroy;
819 begin
820   Clear;
821 
822   if FStretchedVirtualScreen <> nil then
823     FStretchedVirtualScreen.FreeReference;
824   if FPreviousVirtualScreen <> nil then
825     FPreviousVirtualScreen.FreeReference;
826   FInternalVirtualScreen.Free;
827   FRestoreImage.Free;
828   FBackgroundImage.Free;
829   inherited Destroy;
830 end;
831 
832 procedure TBGRAAnimatedGif.Pause;
833 begin
834   FPaused := True;
835 end;
836 
837 procedure TBGRAAnimatedGif.Resume;
838 begin
839   FPaused := False;
840 end;
841 
842 procedure TBGRAAnimatedGif.Show(Canvas: TCanvas; ARect: TRect);
843 begin
844   Canvas.StretchDraw(ARect, self);
845 end;
846 
847 procedure TBGRAAnimatedGif.Update(Canvas: TCanvas; ARect: TRect);
848 var
849   n: integer;
850   PChangePix, PNewPix, PBackground, PNewBackground: PLongWord;
851   oldpix, newpix, newbackpix: LongWord;
852   NewBackgroundImage: TBGRABitmap;
853 begin
854   if (BackgroundMode = gbmUpdateBackgroundContinuously) and
855     (FBackgroundImage = nil) then
856     BackgroundMode := gbmSaveBackgroundOnce;
857 
858   SaveBackgroundOnce(Canvas, ARect);
859 
860   case BackgroundMode of
861     gbmSimplePaint:
862     begin
863       UpdateSimple(Canvas, ARect);
864       exit;
865     end;
866     gbmEraseBackground:
867     begin
868       UpdateEraseBackground(Canvas, ARect);
869       exit;
870     end;
871     gbmSaveBackgroundOnce, gbmUpdateBackgroundContinuously:
872     begin
873       if FPreviousVirtualScreen <> nil then
874       begin
875         if (FPreviousVirtualScreen.Width <> ARect.Right - ARect.Left) or
876           (FPreviousVirtualScreen.Height <> ARect.Bottom - ARect.Top) then
877         begin
878           FPreviousVirtualScreen.FreeReference;
879           FPreviousVirtualScreen := nil;
880         end
881         else
882           FPreviousVirtualScreen := TBGRABitmap(FPreviousVirtualScreen.GetUnique);
883       end;
884 
885       Render(ARect.Right - ARect.Left, ARect.Bottom - ARect.Top);
886 
887       if FImageChanged then
888       begin
889         if BackgroundMode = gbmUpdateBackgroundContinuously then
890         begin
891           NewBackgroundImage :=
892             TBGRABitmap.Create(FStretchedVirtualScreen.Width,
893             FStretchedVirtualScreen.Height);
894           NewBackgroundImage.GetImageFromCanvas(Canvas, ARect.Left, ARect.Top);
895 
896           if FPreviousVirtualScreen = nil then
897           begin
898             FPreviousVirtualScreen := TBGRABitmap.Create(FWidth, FHeight);
899             FPreviousVirtualScreen.Fill(BGRAPixelTransparent);
900           end;
901 
902           PChangePix  := PLongWord(FPreviousVirtualScreen.Data);
903           PNewPix     := PLongWord(FStretchedVirtualScreen.Data);
904           PBackground := PLongWord(FBackgroundImage.Data);
905           PNewBackground := PLongWord(NewBackgroundImage.Data);
906           for n := FStretchedVirtualScreen.NbPixels - 1 downto 0 do
907           begin
908             oldpix := PChangePix^;
909 
910             if (oldpix and AlphaMask = AlphaMask) then //pixel opaque précédent
911             begin
912               newbackpix := PNewBackground^;
913               if (newbackpix <> oldpix) then //stocke nouveau fond
914                 PBackground^ := newbackpix;
915             end;
916 
917             newpix := PNewPix^;
918 
919             if newpix and AlphaMask = AlphaMask then
920               PChangePix^ := newpix //pixel opaque
921             else if newpix and AlphaMask > 0 then
922             begin
923               PChangePix^ := PBackground^;
924               DrawPixelInlineNoAlphaCheck(PBGRAPixel(PChangePix), PBGRAPixel(@newpix)^);
925             end
926             else if PChangePix^ and AlphaMask <> 0 then
927               PChangePix^ := PBackground^; //efface précédent
928 
929 {               if newpix and AlphaMask > AlphaLimit then PChangePix^ := newpix or AlphaMask //pixel opaque
930                else if PChangePix^ and AlphaMask <> 0 then PChangePix^ := PBackground^; //efface précédent}
931 
932             Inc(PNewPix);
933             Inc(PChangePix);
934             Inc(PBackground);
935             Inc(PNewBackground);
936           end;
937           NewBackgroundImage.Free;
938           FPreviousVirtualScreen.InvalidateBitmap;
939           FPreviousVirtualScreen.Draw(Canvas, ARect.Left, ARect.Top, false);
940           FPreviousVirtualScreen.PutImage(0, 0, FStretchedVirtualScreen, dmSet);
941         end
942         else
943         begin
944           if FPreviousVirtualScreen = nil then
945           begin
946             FStretchedVirtualScreen.Draw(Canvas, ARect.Left, ARect.Top, false);
947             FPreviousVirtualScreen :=
948               TBGRABitmap(FStretchedVirtualScreen.NewReference);
949           end
950           else
951           begin
952             PChangePix  := PLongWord(FPreviousVirtualScreen.Data);
953             PNewPix     := PLongWord(FStretchedVirtualScreen.Data);
954             PBackground := PLongWord(FBackgroundImage.Data);
955             for n := FStretchedVirtualScreen.NbPixels - 1 downto 0 do
956             begin
957               newpix := PNewPix^;
958 
959               if newpix and AlphaMask = AlphaMask then
960                 PChangePix^ := newpix //pixel opaque
961               else if newpix and AlphaMask > 0 then
962               begin
963                 PChangePix^ := PBackground^;
964                 DrawPixelInlineNoAlphaCheck(PBGRAPixel(PChangePix), PBGRAPixel(@newpix)^);
965               end
966               else if PChangePix^ and AlphaMask <> 0 then
967                 PChangePix^ := PBackground^; //efface précédent
968 
969 {                 if newpix and AlphaMask > AlphaLimit then PChangePix^ := newpix or AlphaMask //pixel opaque
970                  else if PChangePix^ and AlphaMask <> 0 then PChangePix^ := PBackground^; //efface précédent}
971 
972               Inc(PNewPix);
973               Inc(PChangePix);
974               Inc(PBackground);
975             end;
976             FPreviousVirtualScreen.InvalidateBitmap;
977             FPreviousVirtualScreen.Draw(Canvas, ARect.Left, ARect.Top, false);
978             FPreviousVirtualScreen.PutImage(0, 0, FStretchedVirtualScreen, dmSet);
979           end;
980         end;
981         FImageChanged := False;
982       end;
983     end;
984   end;
985 end;
986 
987 procedure TBGRAAnimatedGif.Hide(Canvas: TCanvas; ARect: TRect);
988 var
989   shape: TBGRABitmap;
990   p, pback: PBGRAPixel;
991   MemEraseColor: TBGRAPixel;
992   n: integer;
993 begin
994   MemEraseColor := ColorToBGRA(EraseColor);
995   if FPreviousVirtualScreen <> nil then
996   begin
997     if (FPreviousVirtualScreen.Width <> ARect.Right - ARect.Left) or
998       (FPreviousVirtualScreen.Height <> ARect.Bottom - ARect.Top) then
999     begin
1000       FPreviousVirtualScreen.FreeReference;
1001       FPreviousVirtualScreen := nil;
1002     end;
1003   end;
1004 
1005   case BackgroundMode of
1006     gbmEraseBackground, gbmSimplePaint:
1007     begin
1008       if FPreviousVirtualScreen <> nil then
1009       begin
1010         shape := TBGRABitmap(FPreviousVirtualScreen.Duplicate);
1011         p     := shape.Data;
1012         for n := shape.NbPixels - 1 downto 0 do
1013         begin
1014           if p^.alpha <> 0 then
1015             p^ := MemEraseColor
1016           else
1017             p^ := BGRAPixelTransparent;
1018           Inc(p);
1019         end;
1020         shape.Draw(Canvas, ARect.Left, ARect.Top, false);
1021         shape.FreeReference;
1022       end;
1023     end;
1024     gbmSaveBackgroundOnce, gbmUpdateBackgroundContinuously:
1025     begin
1026       if (FPreviousVirtualScreen <> nil) and (FBackgroundImage <> nil) then
1027       begin
1028         shape := TBGRABitmap(FPreviousVirtualScreen.Duplicate);
1029         p     := shape.Data;
1030         pback := FBackgroundImage.Data;
1031         for n := shape.NbPixels - 1 downto 0 do
1032         begin
1033           if p^.alpha <> 0 then
1034             p^ := pback^
1035           else
1036             p^ := BGRAPixelTransparent;
1037           Inc(p);
1038           Inc(pback);
1039         end;
1040         shape.Draw(Canvas, ARect.Left, ARect.Top, false);
1041         shape.FreeReference;
1042       end;
1043     end;
1044   end;
1045 end;
1046 
1047 procedure TBGRAAnimatedGif.UpdateEraseBackground(Canvas: TCanvas;
1048   ARect: TRect; DrawOnlyIfChanged: boolean);
1049 var
1050   n:      integer;
1051   PChangePix, PNewPix: PLongWord;
1052   newpix: LongWord;
1053   MemPixEraseColor: LongWord;
1054 begin
1055   if EraseColor = clNone then
1056   begin
1057     UpdateSimple(Canvas, ARect, DrawOnlyIfChanged);
1058     exit;
1059   end;
1060 
1061   if FPreviousVirtualScreen <> nil then
1062   begin
1063     if (FPreviousVirtualScreen.Width <> ARect.Right - ARect.Left) or
1064       (FPreviousVirtualScreen.Height <> ARect.Bottom - ARect.Top) then
1065     begin
1066       FPreviousVirtualScreen.FreeReference;
1067       FPreviousVirtualScreen := nil;
1068     end
1069     else
1070       FPreviousVirtualScreen := TBGRABitmap(FPreviousVirtualScreen.GetUnique);
1071   end;
1072 
1073   Render(ARect.Right - ARect.Left, ARect.Bottom - ARect.Top);
1074   if FImageChanged then
1075   begin
1076     PBGRAPixel(@MemPixEraseColor)^ := ColorToBGRA(EraseColor);
1077     if FPreviousVirtualScreen = nil then
1078     begin
1079       FStretchedVirtualScreen.Draw(Canvas, ARect.Left, ARect.Top, false);
1080       FPreviousVirtualScreen := TBGRABitmap(FStretchedVirtualScreen.NewReference);
1081     end
1082     else
1083     begin
1084       PChangePix := PLongWord(FPreviousVirtualScreen.Data);
1085       PNewPix    := PLongWord(FStretchedVirtualScreen.Data);
1086       for n := FStretchedVirtualScreen.NbPixels - 1 downto 0 do
1087       begin
1088         newpix := PNewPix^;
1089 
1090         if newpix and AlphaMask = AlphaMask then
1091           PChangePix^ := newpix //pixel opaque
1092         else if newpix and AlphaMask > 0 then
1093         begin
1094           PChangePix^ := MemPixEraseColor;
1095           DrawPixelInlineNoAlphaCheck(PBGRAPixel(PChangePix), PBGRAPixel(@newpix)^);
1096         end
1097         else if PChangePix^ and AlphaMask <> 0 then
1098           PChangePix^ := MemPixEraseColor; //efface précédent
1099 {           if newpix and AlphaMask > AlphaLimit then PChangePix^ := newpix or AlphaMask //pixel opaque
1100            else if PChangePix^ and AlphaMask <> 0 then PChangePix^ := MemPixEraseColor; //efface précédent}
1101 
1102         Inc(PNewPix);
1103         Inc(PChangePix);
1104       end;
1105       FPreviousVirtualScreen.InvalidateBitmap;
1106       FPreviousVirtualScreen.Draw(Canvas, ARect.Left, ARect.Top, false);
1107       FPreviousVirtualScreen.PutImage(0, 0, FStretchedVirtualScreen, dmSet);
1108     end;
1109 
1110     FImageChanged := False;
1111   end;
1112 end;
1113 
1114 procedure TBGRAAnimatedGif.Init;
1115 begin
1116   BackgroundMode := gbmSaveBackgroundOnce;
1117   LoopCount := 0;
1118   LoopDone := 0;
1119 end;
1120 
GetBitmapnull1121 function TBGRAAnimatedGif.GetBitmap: TBitmap;
1122 begin
1123   Render(FWidth, FHeight);
1124   Result := FStretchedVirtualScreen.Bitmap;
1125 end;
1126 
GetMemBitmapnull1127 function TBGRAAnimatedGif.GetMemBitmap: TBGRABitmap;
1128 begin
1129   Render(FWidth, FHeight);
1130   Result := FStretchedVirtualScreen;
1131 end;
1132 
1133 { TBGRAReaderGIF }
1134 
1135 procedure TBGRAReaderGIF.InternalRead(Str: TStream; Img: TFPCustomImage);
1136 var
1137   gif:  TBGRAAnimatedGif;
1138   x, y: integer;
1139   Mem:  TBGRABitmap;
1140 begin
1141   gif := TBGRAAnimatedGif.Create(Str, 1);
1142   Mem := gif.MemBitmap;
1143   if Img is TBGRABitmap then
1144   begin
1145     TBGRABitmap(Img).Assign(Mem);
1146   end
1147   else
1148   begin
1149     Img.SetSize(gif.Width, gif.Height);
1150     for y := 0 to gif.Height - 1 do
1151       for x := 0 to gif.Width - 1 do
1152         with Mem.GetPixel(x, y) do
1153           Img.Colors[x, y] := FPColor(red * $101, green * $101, blue *
1154             $101, alpha * $101);
1155   end;
1156   gif.Free;
1157 end;
1158 
TBGRAReaderGIF.InternalChecknull1159 function TBGRAReaderGIF.InternalCheck(Str: TStream): boolean;
1160 var
1161   GIFSignature: TGIFSignature;
1162   savepos:      int64;
1163 begin
1164   savepos := str.Position;
1165   try
1166     fillchar({%H-}GIFSignature, sizeof(GIFSignature), 0);
1167     str.Read(GIFSignature, sizeof(GIFSignature));
1168     if (GIFSignature[1] = 'G') and (GIFSignature[2] = 'I') and
1169       (GIFSignature[3] = 'F') then
1170     begin
1171       Result := True;
1172     end
1173     else
1174       Result := False;
1175   except
1176     on ex: Exception do
1177       Result := False;
1178   end;
1179   str.Position := savepos;
1180 end;
1181 
1182 { TBGRAWriterGIF }
1183 
1184 procedure TBGRAWriterGIF.InternalWrite(Str: TStream; Img: TFPCustomImage);
1185 var
1186   gif: TBGRAAnimatedGif;
1187 begin
1188   gif := TBGRAAnimatedGif.Create;
1189   try
1190     gif.SetSize(Img.Width,Img.Height);
1191     gif.AddFrame(Img, 0,0,0);
1192     gif.SaveToStream(Str, BGRAColorQuantizerFactory, daFloydSteinberg);
1193   except
1194     on ex: EColorQuantizerMissing do
1195     begin
1196       FreeAndNil(gif);
1197       raise EColorQuantizerMissing.Create('Please define the color quantizer factory. You can do that with the following statements: Uses BGRAPalette, BGRAColorQuantization; BGRAColorQuantizerFactory:= TBGRAColorQuantizer;');
1198     end;
1199     on ex: Exception do
1200     begin
1201       FreeAndNil(gif);
1202       raise ex;
1203     end;
1204   end;
1205   FreeAndNil(gif);
1206 end;
1207 
1208 initialization
1209 
1210   DefaultBGRAImageReader[ifGif] := TBGRAReaderGIF;
1211   DefaultBGRAImageWriter[ifGif] := TBGRAWriterGIF;
1212 
1213   //Free Pascal Image
1214   ImageHandlers.RegisterImageReader('Animated GIF', TBGRAAnimatedGif.GetFileExtensions,
1215     TBGRAReaderGIF);
1216   ImageHandlers.RegisterImageWriter('Animated GIF', TBGRAAnimatedGif.GetFileExtensions,
1217     TBGRAWriterGIF);
1218 
1219   {$IFDEF BGRABITMAP_USE_LCL}
1220   //Lazarus Picture
1221   TPicture.RegisterFileFormat(TBGRAAnimatedGif.GetFileExtensions, 'Animated GIF',
1222     TBGRAAnimatedGif);
1223   {$ENDIF}
1224 end.
1225 
1226