forked from tannerhelland/PhotoDemon
-
Notifications
You must be signed in to change notification settings - Fork 0
/
pdGIF.cls
2009 lines (1601 loc) · 104 KB
/
pdGIF.cls
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
VERSION 1.0 CLASS
BEGIN
MultiUse = -1 'True
Persistable = 0 'NotPersistable
DataBindingBehavior = 0 'vbNone
DataSourceBehavior = 0 'vbNone
MTSTransactionMode = 0 'NotAnMTSObject
END
Attribute VB_Name = "pdGIF"
Attribute VB_GlobalNameSpace = False
Attribute VB_Creatable = True
Attribute VB_PredeclaredId = False
Attribute VB_Exposed = False
'***************************************************************************
'GIF encoding library
'Copyright 2001-2022 by Tanner Helland
'Created: 4/15/01
'Last updated: 27/March/22
'Last update: add optional palette sorter
'
'Most image exporters exist in the ImageExporter module. GIF is a weird exception
' because animated GIFs require a ton of preprocessing (to optimize animation frames),
' so I've moved them to their own home.
'
'PhotoDemon automatically optimizes saved GIFs to produce the smallest possible files.
' A variety of optimizations are used, and the encoder tests various strategies to try
' and choose the "best" (smallest) solution on each frame. As you can see from the
' size of this module, many many many different optimizations are attempted.
'
'Despite this, the optimization pre-pass is reasonably quick, and the animated GIFs
' produced this way are often many times smaller than GIFs produced by a naive encoder.
'
'Note that the optimization steps are specifically written in an export library
' agnostic way. PD internally stores the results of all optimizations, then just hands
' the optimized frames off to an encoder at the end of the process. Historically PD used
' FreeImage for the actual GIF encoding step, but FreeImage has a number of shortcomings
' (including woeful performance and writing larger GIFs than is necessary), so in 2021
' I moved to an in-house LZW encoder based off the classic UNIX "compress" tool.
' This new LZW encoder has different licensing considerations, so I've kept it in a
' separate file (ImageFormats_GIF_LZW) - look there for additional credits and licensing
' details for that portion of the library.
'
'Unless otherwise noted, all source code in this file is shared under a simplified BSD license.
' Full license details are available in the LICENSE.md file, or at https://photodemon.org/license/
'
'***************************************************************************
Option Explicit
'Compile-time triggers:
'Sort global palette by "importance". If sorted nicely, we can check the "sort flag" in the global palette header.
' This is mostly a historical curiosity, but I did write a sort algorithm that works well, so you can activate
' if curious. Note that this imposes an encoding-time perf penalty, but hypothetically decoding performance will
' improve (due to better color locality).
Private Const SORT_PALETTE_BY_IMPORTANCE As Boolean = False
'Static GIF values:
'Copy of the 8-bpp (palettized) array representing the original image. May be reduced further
' during LZW compression.
Private m_ImgBytes() As Byte
'Final image palette. Alpha element is important, as there may be a 0-alpha entry for GIF transparency.
Private m_ImgPalette() As RGBQuad
'Animated GIF values:
Private Enum PD_GifDisposal
gd_Unknown = 0 'Do not use
gd_Leave = 1 'Do nothing after rendering
gd_Background = 2 'Restore background color
gd_Previous = 3 'Undo current frame's rendering
End Enum
#If False Then
Private Const gd_Unknown = 0, gd_Leave = 1, gd_Background = 2, gd_Previous = 3
#End If
'The animated GIF exporter builds a collection of frame data during export.
Private Type PD_GifFrame
usesGlobalPalette As Boolean 'GIFs allow for both global and local palettes. PD optimizes against both, and will
' automatically use the best palette for each frame.
frameIsDuplicateOrEmpty As Boolean 'PD automatically drops duplicate and/or empty frames
frameNeedsTransparency As Boolean 'PD may require transparency as part of optimizing a given frame (pixel blanking).
' If the final palette ends up without transparency, we will roll back this
' optimization step as necessary.
frameWasBlanked As Boolean 'TRUE if pixel-blanking produced a (likely) smaller frame; may need to be rolled
' back if the final palette doesn't contain (or have room for adding) transparency.
' Note also that the frame may not be *completely* blanked; instead, each scanline is
' conditionally blanked based on whether it reduces overall entropy or not.
frameTime As Long 'GIF frame time is in centiseconds (uuuuuuuugh); we auto-translate from ms
frameDisposal As PD_GifDisposal 'GIF and APNG disposal methods are roughly identical. PD may use any/all of them
' as part of optimizing each frame.
rectOfInterest As RectF 'Frames are auto-cropped to their relevant minimal regions-of-change
backupFrameDIB As pdDIB 'If a frame gets pixel-blanked, we'll save its original version here. If the
' final palette can't fit transparency (global palettes are infamous for this),
' we'll revert to this original, non-blanked version of the frame.
frameDIB As pdDIB 'Only used temporarily, during optimization; ultimately palettized to produce...
pixelData() As Byte '...this bytestream (and associated palette) instead.
palNumColors As Long 'Stores the local palette count and color table, if one exists (it may not -
framePalette() As RGBQuad ' check the usesGlobalPalette bool before accessing)
End Type
'Optimized GIF frames will be stored here. This array is auto-cleared after a successful dump to file.
Private m_allFrames() As PD_GifFrame
'PD always writes a global palette, and it attempts to use it on as many frames as possible.
' (Local palettes will automatically be generated too, as necessary.)
Private m_globalPalette() As RGBQuad, m_numColorsInGP As Long, m_GlobalTrnsIndex As Long
'PD performs compression tests on potential frames to try and see which ones produce the smallest
' compressed size. LZW is fast-ish but lz4 is much faster, so we use it as a rough predictor of
' compressibility. (Compression tests use a shared buffer for improved performance.)
Private m_cmpTestBuffer() As Byte, m_cmpTestBufferSize As Long
'Given a param string generated by the Export GIF dialog, apply any GIF pre-processing steps.
' Works for both export preview and actual export prep steps (depending on the value of usePreviewMode).
' - In preview mode, the palette will be applied to the source DIB so you can see it.
' - In non-preview mode, the palette (and palettized image bytes) will be cached in module-level
' variables so the encoder can use them as-is.
Friend Function GetGifReadyImage(ByRef srcDIB As pdDIB, Optional ByVal formatParams As String = vbNullString, Optional ByVal usePreviewMode As Boolean = False) As Boolean
'Parse all relevant GIF parameters.
' (The GIF export dialog provides more details on how these parameters are generated.)
Dim cParams As pdSerialize
Set cParams = New pdSerialize
cParams.SetParamString formatParams
'Only two parameters are mandatory; the others are used on an as-needed basis
Dim gifColorMode As String, gifAlphaMode As String
gifColorMode = cParams.GetString("gif-color-mode", "auto")
gifAlphaMode = cParams.GetString("gif-alpha-mode", "auto")
Dim gifAlphaCutoff As Long, gifColorCount As Long, gifBackgroundColor As Long, gifAlphaColor As Long
gifAlphaCutoff = cParams.GetLong("gif-alpha-cutoff", 64)
gifColorCount = cParams.GetLong("gif-color-count", 256)
gifBackgroundColor = cParams.GetLong("gif-backcolor", vbWhite)
gifAlphaColor = cParams.GetLong("gif-alpha-color", RGB(255, 0, 255))
'Some combinations of parameters invalidate other parameters. Calculate any overrides now.
Dim gifForceGrayscale As Boolean
gifForceGrayscale = Strings.StringsEqual(gifColorMode, "gray", True)
If Strings.StringsEqual(gifColorMode, "auto", True) Then gifColorCount = 256
Dim desiredAlphaStatus As PD_ALPHA_STATUS
desiredAlphaStatus = PDAS_BinaryAlpha
If Strings.StringsEqual(gifAlphaMode, "none", True) Then desiredAlphaStatus = PDAS_NoAlpha
If Strings.StringsEqual(gifAlphaMode, "by-color", True) Then
desiredAlphaStatus = PDAS_NewAlphaFromColor
gifAlphaCutoff = gifAlphaColor
End If
'We now need to produce an image that meets GIF "criteria" - e.g. 8-bit colors with binary transparency.
' Start by matting the GIF against the supplied background color, using a strategy appropriate to
' whatever transparency method they requested.
Dim trnsValues() As Byte
'No alpha in the final image
If (desiredAlphaStatus = PDAS_NoAlpha) Then
srcDIB.CompositeBackgroundColor Colors.ExtractRed(gifBackgroundColor), Colors.ExtractGreen(gifBackgroundColor), Colors.ExtractBlue(gifBackgroundColor)
'Make the chosen color transparent
ElseIf (desiredAlphaStatus = PDAS_NewAlphaFromColor) Then
DIBs.MakeColorTransparent_Ex srcDIB, trnsValues, gifAlphaCutoff
DIBs.ApplyAlphaCutoff_Gif srcDIB, 127, gifBackgroundColor
'Normal GIF behavior (threshold alpha into "fully transparent" or "fully opaque")
Else
DIBs.ApplyAlphaCutoff_Gif srcDIB, gifAlphaCutoff, gifBackgroundColor
End If
'Alpha is now guaranteed to be only values of 0 or 255.
If (Not usePreviewMode) Then ProgressBars.SetProgBarVal 2
'If the caller requested grayscale, apply that now.
If gifForceGrayscale Then DIBs.MakeDIBGrayscale srcDIB, gifColorCount, False
'All that's left to do is palettize the image! For full-color images, let's use a fast algorithm.
' For smaller color counts, a neural network will produce a much better selection of colors
' (at a potentially significant cost to performance).
Dim curColorCount As Long
curColorCount = Palettes.GetDIBColorCount_FastAbort(srcDIB, m_ImgPalette)
'In preview mode, always use the fast algorithm
If usePreviewMode Then
Palettes.GetOptimizedPaletteIncAlpha srcDIB, m_ImgPalette, gifColorCount, pdqs_Variance, True
Else
'In regular mode, we have a different choice to make. First, see if the palette is already
' a useable size. (This is likely for e.g. a loaded GIF being saved back out to GIF.)
If (curColorCount > gifColorCount) Then
'This image has too many colors and needs to be palettized. For 256-colors, use the
' fastest available algorithm (modified median cut).
If (gifColorCount = 256) Then
Palettes.GetOptimizedPaletteIncAlpha srcDIB, m_ImgPalette, gifColorCount, pdqs_Variance, True
'For lower color counts, use our modified Neuquant for much better quality.
Else
Palettes.GetNeuquantPalette_RGBA srcDIB, m_ImgPalette, gifColorCount, True
End If
'/no Else required; the palette returned by the color count function is useable as-is!
End If
End If
If (Not usePreviewMode) Then ProgressBars.SetProgBarVal 3
'We now have an optimized palette for this image. If this is for export purposes,
' produce an 8-bpp array for export to file. If this is a preview, apply the palette
' to the source DIB so the user can review it.
If usePreviewMode Then
Palettes.ApplyPaletteToImage_KDTree srcDIB, m_ImgPalette, True
'During preview, palette order doesn't matter, but at export-time we want to sort the palette so
' that the transparent index appears in slot 0.
Else
Palettes.SortPaletteForCompression_IncAlpha srcDIB, m_ImgPalette, True, True
DIBs.GetDIBAs8bpp_RGBA_SrcPalette srcDIB, m_ImgPalette, m_ImgBytes
ProgressBars.SetProgBarVal 4
End If
GetGifReadyImage = True
End Function
'Save a static (non-animated) pdImage object to a pdStream. This allows for saving to file, memory, etc.
Friend Function SaveGIF_ToStream_Static(ByRef srcPDImage As pdImage, ByRef dstStream As pdStream, Optional ByVal formatParams As String = vbNullString, Optional ByVal metadataParams As String = vbNullString) As Boolean
Const FUNC_NAME As String = "SaveGIF_ToStream_Static"
SaveGIF_ToStream_Static = False
'Failsafe checks for input params
If (srcPDImage Is Nothing) Or (dstStream Is Nothing) Then
InternalError FUNC_NAME, "null inputs"
Exit Function
End If
'Parameters are available for parsing, although it's expected that most parameters will
' only be useful to the pre-processor.
' (The GIF export dialog provides more details on how these parameters are generated.)
Dim cParams As pdSerialize
Set cParams = New pdSerialize
cParams.SetParamString formatParams
'Raise a progress bar
ProgressBars.SetProgBarMax 6
ProgressBars.SetProgBarVal 0
'Generate a composited image copy, with alpha automatically un-premultiplied
Dim tmpImageCopy As pdDIB
Set tmpImageCopy = New pdDIB
srcPDImage.GetCompositedImage tmpImageCopy, False
ProgressBars.SetProgBarVal 1
'Hand the image off to GetGifReadyImage(), which will pre-process the image according to
' whatever settings the user supplied in the export dialog.
If (Not GetGifReadyImage(tmpImageCopy, formatParams, False)) Then
InternalError FUNC_NAME, "pre-processing failed"
End If
'We no longer need the 32-bpp copy of the image; free it to conserve memory
Set tmpImageCopy = Nothing
'Cache the palette size so we don't have to keep querying UBound of the palette array
Dim numColorsInPalette As Long
numColorsInPalette = UBound(m_ImgPalette) + 1
'Filsafe check only
If (numColorsInPalette < 1) Then numColorsInPalette = 1
If (numColorsInPalette > 256) Then numColorsInPalette = 256
'Background color index; if the first entry in the palette is transparency, we can override its
' color with the background color (because that color will basically be ignored as it's transparent).
' If the image does *not* contain transparency, however, we'll attempt to find the nearest match to
' the matte color in the existing palette. We do this because there's no guarantee the matte color
' will be used in the image - it may have just been used to composite semi-transparent pixels -
' but this at least gives us something "close".
Dim bkgdIndex As Long, bkgdColor As Long
If (m_ImgPalette(0).Alpha = 0) Then
'Overwrite the first palette entry's color with the background color
bkgdColor = cParams.GetLong("gif-backcolor", vbWhite)
With m_ImgPalette(0)
.Red = Colors.ExtractRed(bkgdColor)
.Green = Colors.ExtractGreen(bkgdColor)
.Blue = Colors.ExtractBlue(bkgdColor)
End With
Else
'Stuff the color at the end of the palette if there's room
If (numColorsInPalette < (2 ^ Pow2FromColorCount(numColorsInPalette))) Then
'See if the color already exists in the palette
bkgdColor = cParams.GetLong("gif-backcolor", vbWhite)
bkgdIndex = Palettes.GetNearestIndexRGB(m_ImgPalette, bkgdColor)
If (RGB(m_ImgPalette(bkgdIndex).Red, m_ImgPalette(bkgdIndex).Green, m_ImgPalette(bkgdIndex).Blue) <> bkgdColor) Then
'Background color doesn't exist in the palette. Add it.
If (numColorsInPalette > UBound(m_ImgPalette)) Then ReDim Preserve m_ImgPalette(0 To numColorsInPalette) As RGBQuad
With m_ImgPalette(numColorsInPalette)
.Red = Colors.ExtractRed(bkgdColor)
.Green = Colors.ExtractGreen(bkgdColor)
.Blue = Colors.ExtractBlue(bkgdColor)
End With
'Set the background index to the new entry, and increment the total color count
bkgdIndex = numColorsInPalette
numColorsInPalette = numColorsInPalette + 1
'/no Else required - the background color already exists in the palette
End If
'There's no room; find the nearest color instead and use *that* index
Else
bkgdIndex = Palettes.GetNearestIndexRGB(m_ImgPalette, cParams.GetLong("gif-backcolor", vbWhite))
End If
End If
'For detailed GIF format info, see https://giflib.sourceforge.net/whatsinagif/bits_and_bytes.html
'Start with the file header
SaveGIF_ToStream_Static = WriteGIF_FileHeader(dstStream, srcPDImage.Width, srcPDImage.Height, numColorsInPalette, m_ImgPalette, bkgdIndex)
'The image header is now complete.
'If the image contains transparency, we now need to write an optional block to flag the
' transparent palette index.
If (m_ImgPalette(0).Alpha = 0) Then
'The transparent color may have been overridden in a previous step. Replace it with transparent black now,
' to ensure future color-matching works correctly.
m_ImgPalette(0).Red = 0
m_ImgPalette(0).Green = 0
m_ImgPalette(0).Blue = 0
'A separate "Graphics Control Extension" writer handles this step for us.
SaveGIF_ToStream_Static = WriteGIF_GCE(dstStream, True, trnsIndex:=0)
End If
'Next up is an "image descriptor", basically a frame header.
'1-byte image separator (always 2C)
dstStream.WriteByte &H2C
'Frame dimensions as unsigned shorts, in left/top/width/height order
dstStream.WriteIntU 0
dstStream.WriteIntU 0
dstStream.WriteIntU srcPDImage.Width
dstStream.WriteIntU srcPDImage.Height
'And my favorite, another packed bit-field! (uuuuugh)
' - 1 bit local palette used (always 0 for static images)
' - 1 bit interlaced (always 0, PD never interlaces frames)
' - 1 bit sort flag (same as global table, PD can - and may - do this, but always writes 0 per giflib convention)
' - 2 bits reserved
' - 3 bits size of local color table N (describing 2 ^ n-1 colors in the palette, always 0 for PD)
dstStream.WriteByte 0
'All that's left are the pixel bits. These are prefaced by a byte describing the
' minimum LZW code size. This is a minimum of 2, a maximum of the power-of-2 size
' of the frame's palette (global or local).
Dim lzwCodeSize As Long
lzwCodeSize = Pow2FromColorCount(numColorsInPalette)
If (lzwCodeSize < 2) Then lzwCodeSize = 2
dstStream.WriteByte lzwCodeSize
'Next is the image bitstream! Encoding happens elsewhere; we just pass the stream to them
' and let them encode away.
ProgressBars.SetProgBarVal 5
ImageFormats_GIF_LZW.CompressLZW dstStream, VarPtr(m_ImgBytes(0, 0)), srcPDImage.Width * srcPDImage.Height, lzwCodeSize + 1, True
ProgressBars.SetProgBarVal 6
'All that's left for this frame is to explicitly terminate the block
dstStream.WriteByte 0
'With all frames written, we can write the trailer and exit!
' (This is a magic number from the spec: https://www.w3.org/Graphics/GIF/spec-gif89a.txt)
dstStream.WriteByte &H3B
'Normally we would stop the stream here, but we leave it to the caller instead as they may
' also want to free memory (if this write went out to disk).
SaveGIF_ToStream_Static = True
End Function
'Save an animated (multi-layer) pdImage object to a pdStream. This allows for saving to file, memory, etc.
Friend Function SaveGIF_ToStream_Animated(ByRef srcPDImage As pdImage, ByRef dstStream As pdStream, Optional ByVal formatParams As String = vbNullString, Optional ByVal metadataParams As String = vbNullString) As Boolean
Const FUNC_NAME As String = "SaveGIF_ToStream_Animated"
SaveGIF_ToStream_Animated = False
'Failsafe checks for input params
If (srcPDImage Is Nothing) Or (dstStream Is Nothing) Then
InternalError FUNC_NAME, "null inputs"
Exit Function
End If
'Parameters are available for parsing, although it's expected that most parameters will
' only be useful to the pre-processor.
'
'(The GIF export dialog provides more details on how these parameters are generated.)
Dim cParams As pdSerialize
Set cParams = New pdSerialize
cParams.SetParamString formatParams
Dim useFixedFrameDelay As Boolean, frameDelayDefault As Long
useFixedFrameDelay = cParams.GetBool("use-fixed-frame-delay", False)
frameDelayDefault = cParams.GetLong("frame-delay-default", 100)
Dim gifAlphaCutoff As Long, gifMatteColor As Long
gifAlphaCutoff = cParams.GetLong("alpha-cutoff", 64)
gifMatteColor = cParams.GetLong("matte-color", vbWhite)
Dim autoDither As Boolean, useDithering As Boolean, ditherText As String
ditherText = cParams.GetString("dither", "auto")
autoDither = Strings.StringsEqual(ditherText, "auto", True)
If (Not autoDither) Then useDithering = Strings.StringsEqual(ditherText, "on", True)
'We now begin a long phase of "optimizing" the exported animation. This involves comparing
' neighboring frames against each other, cropping out identical regions (and possibly
' blanking out shared overlapping pixels), figuring out optimal frame disposal strategies,
' and possibly generating unique palettes for each frame and/or using a mix of local and
' global palettes.
'
'At the end of this phase, we'll have an array of optimized GIF frames (and all associated
' parameters) which we can then hand off to any capable GIF encoder.
Dim imgPalette() As RGBQuad
Dim tmpLayer As pdLayer
'GIF files support a "global palette". This is a shared palette that any frame can choose to
' use (in place of a "local palette").
'PhotoDemon always writes a global palette, because even if just the first frame uses it,
' there is no increase in file size (as the first frame will simply not provide a local palette).
' If, however, the first frame does *not* require a full 256-color palette, we will merge colors
' from subsequent frames into the global palette, until we arrive at 256 colors (or until all
' colors in all frames have been assembled).
'This global palette starts out on the range [0, 255] but may be shrunk if fewer colors are used.
ReDim m_globalPalette(0 To 255) As RGBQuad
'Color trackers exist for both global and local palettes (PD may use one or both)
m_numColorsInGP = 0
Dim numColorsInLP As Long
Dim idxPalette As Long
'Frames that only use the global palette are tagged accordingly; they will skip embedding
' a local palette if they can.
Dim frameUsesGP As Boolean: frameUsesGP = False
'Because of the way GIFs are encoded (reliance on a global palette, as just mentioned),
' we can produce smaller files if we perform all optimizations "up front" instead of
' writing the GIF as-we-go. (Writing as-we-go would prevent things like global palette
' optimization, because it has to be provided at the front of the file). As such, we
' build an optimized frame collection in advance, before writing anything to disk -
' which comes with the nice perk of making the actual GIF encoding step "encoder
' agnostic", e.g. any GIF encoder can be dropped-in at the end, because we perform
' all optimizations internally.
ReDim m_allFrames(0 To srcPDImage.GetNumOfLayers - 1) As PD_GifFrame
'GIFs are obnoxious because each frame specifies a "frame disposal" requirement; this is
' what to do with the screen buffer *after* the current frame is displayed. We calculate
' this using data from the next frame in line (because its transparency requirements
' are ultimately what determine the frame disposal requirements of the *previous* frame).
'
'Frames are generally cleared by default; subsequent analyses will set this value on a
' per-frame basis, after testing different optimization strategies.
Dim i As Long
For i = 0 To srcPDImage.GetNumOfLayers - 1
m_allFrames(i).frameDisposal = gd_Background
Next i
'As part of optimizing frames, we need to keep a running copy several different frames:
' 1) what the current frame looks like right now
' 2) what the previous frame looked like
' 3) what our current frame looked like before we started fucking with it
'
'These are used as part of exploring different optimization strategies.
Dim prevFrame As pdDIB, bufferFrame As pdDIB, curFrameBackup As pdDIB
Set prevFrame = New pdDIB
Set bufferFrame = New pdDIB
Set curFrameBackup = New pdDIB
'We also use a soft reference that we can point at whatever frame DIB we want
' (its contents are not maintained between frames).
Dim refFrame As pdDIB
'Some parts of the optimization process are not guaranteed to improve file size
' (e.g. blanking out duplicate pixels between frames can actually hurt compression
' if the current frame is very noisy). To ensure we only apply beneficial optimizations,
' we test-compress the current frame after potentially problematic optimizations to
' double-check that our compression ratio improved. (If it didn't, we'll roll back the
' changes we made - see the multiple DIB copies above!)
'
'Note that for performance reasons, we use lz4 instead of our native GIF encoder.
' lz4 is a hell of a lot faster, and I assume that the best-case result with lz4
' correlates reasonably well with the best-case result for a GIF-style LZW
' compressor, since LZ77 and LZ78 compression share most critical aspects on
' image-style data.
'
'To reduce memory churn, we initialize a single worst-case-size buffer in advance,
' then reuse it for all compression test runs.
m_cmpTestBufferSize = Compression.GetWorstCaseSize(srcPDImage.Width * srcPDImage.Height * 4, cf_Lz4)
ReDim m_cmpTestBuffer(0 To m_cmpTestBufferSize - 1) As Byte
'We also want to know if the source image is non-paletted (e.g. "full color").
' If it isn't (meaning if it's already <= 256 colors per frame), the source pixel data probably
' came from an existing animated GIF file, and we'll want to optimize the data differently.
'
'Also, if auto-dithering is enabled, we dither frames *only* when the source data is full-color.
Dim sourceIsFullColor As Boolean
Set tmpLayer = New pdLayer
tmpLayer.CopyExistingLayer srcPDImage.GetLayerByIndex(0)
tmpLayer.ConvertToNullPaddedLayer srcPDImage.Width, srcPDImage.Height, True
sourceIsFullColor = (Palettes.GetDIBColorCount_FastAbort(tmpLayer.GetLayerDIB, imgPalette) > 256)
If autoDither Then useDithering = sourceIsFullColor
'If we detect two identical back-to-back frames (surprisingly common in GIFs "in the wild"),
' we will simply merge their frame times into a single value and remove the duplicate frame.
' This reduces file size "for free", but it requires more complicated tracking as the number
' of frames may decrease as optimization proceeds.
Dim numGoodFrames As Long, lastGoodFrame As Long
numGoodFrames = 0
lastGoodFrame = 0
'We are now going to iterate through all layers in the image TWICE.
'On this first pass, we will analyze each layer, produce optimized global and
' local palettes, extract frame times from layer names, and determine regions
' of interest in each frame. Then we will palettize each layer and cache the
' palettized pixels in a simple 1D array.
For i = 0 To srcPDImage.GetNumOfLayers - 1
'Optimizing frames can take some time. Keep the user apprised of our progress.
ProgressBars.SetProgBarVal i
Message "Optimizing animation frame %1 of %2...", i + 1, srcPDImage.GetNumOfLayers + 1, "DONOTLOG"
'Initialize default frame settings
SaveGIF_ToStream_Animated = AnimatedGIF_InitFrame(i, useFixedFrameDelay, frameDelayDefault, srcPDImage)
'Make sure this layer is the same size as the parent image, and apply any
' non-destructive transforms. (Note that we *don't* do this for the first frame,
' because we already performed that step above as part of whole-image heuristics)
If (i > 0) Then
Set tmpLayer = New pdLayer
tmpLayer.CopyExistingLayer srcPDImage.GetLayerByIndex(i)
tmpLayer.ConvertToNullPaddedLayer srcPDImage.Width, srcPDImage.Height, True
End If
'Ensure we have a target DIB to operate on; the final, optimized frame will be stored here.
If (curFrameBackup Is Nothing) Then Set curFrameBackup = New pdDIB
curFrameBackup.CreateFromExistingDIB tmpLayer.GetLayerDIB
Set m_allFrames(i).frameDIB = New pdDIB
'Force alpha to 0 or 255 only (this is a GIF requirement). It simplifies
' subsequent steps to handle this up-front.
DIBs.ApplyAlphaCutoff_Gif tmpLayer.GetLayerDIB, gifAlphaCutoff, gifMatteColor
'The first frame in the file should always be full-size, which limits what we can do
' to optimize it. (Technically, you *could* write GIFs whose first frame is smaller
' than the image as a whole. PD does not do this because it's terrible practice,
' and the file size savings are not meaningfully better.)
If (i = 0) Then
'Cache the temporary layer DIB as-is; it serves as both the first frame in
' the animation, and the fallback "static" image for decoders that don't
' understand animated GIFs. (Also, after creating it, we immediately suspend
' the DIB to a compressed array to reduce memory constraints.)
m_allFrames(i).frameDIB.CreateFromExistingDIB tmpLayer.GetLayerDIB
m_allFrames(i).frameDIB.SuspendDIB cf_Lz4, False
'Initialize the frame buffer to be the same as the first frame...
bufferFrame.CreateFromExistingDIB tmpLayer.GetLayerDIB
'...and initialize the "previous" frame buffer to pure transparency (this is complicated
' for GIFs because the recommendation is to avoid this disposal method entirely - due to
' the complications it requires in the decoder - but they also suggest that the decoder
' can ignore these instructions and use the background color of the GIF "if they have to".
' In PNGs, the spec says to use transparent black but GIFs may not support transparency
' at all... so I'm not sure what to do. For now, I'm using the same strategy as PNGs
' and will revisit if problems arise.
prevFrame.CreateBlank tmpLayer.GetLayerDIB.GetDIBWidth, tmpLayer.GetLayerDIB.GetDIBHeight, 32, 0, 0
'Finally, mark this as the "last good frame" (in case subsequent frames are duplicates
' of this one, we'll need to refer back to the "last good frame" index)
lastGoodFrame = i
numGoodFrames = i + 1
'If this is *not* the first frame, there are many ways we can optimize frame contents.
Else
'First, we want to figure out how much of this frame needs to be used at all.
' If this frame reuses regions from the previous frame (an extremely common
' occurrence in animation), we can simply crop out any frame borders that are
' identical to the previous frame. (Said another way: only the first frame really
' needs to be the full size of the image - subsequent frames can be *any* size we
' want, so long as they remain within the image's boundaries.)
'(Note also that if this check fails, it means that this frame is 100% identical
' to the frame that came before it.)
Dim dupArea As RectF
If DIBs.GetRectOfInterest_Overlay(tmpLayer.GetLayerDIB, bufferFrame, dupArea) Then
'This frame contains at least one unique pixel, so it needs to be added to the file.
'Before proceeding further, let's compare this frame to one other buffer -
' specifically, the frame buffer as it appeared *before* the previous frame
' was painted. GIFs define a "previous" disposal mode, which tells the previous frame
' to "undo" its rendering when it's done. On certain frames and/or animation styles,
' this may allow for better compression if this frame is more identical to a frame
' further back in the animation sequence.
Dim prevFrameArea As RectF
'As before, ensure that the previous check succeeded. If it fails, it means this frame
' is 100% identical the the frame that preceded the previous frame. Rather than encode
' this frame at all, we can simply store a lone transparent pixel and paint it "over"
' the corresponding frame buffer - maximum file size savings! (This works very well on
' blinking-style animations, for example.)
If DIBs.GetRectOfInterest_Overlay(tmpLayer.GetLayerDIB, prevFrame, prevFrameArea) Then
'With an overlap rectangle calculated for both cases, determine a "winner"
' (where "winner" equals "least number of pixels"), store its frame rectangle,
' and then mark the *previous* frame's disposal op accordingly. (That's important -
' disposal ops describe what you do *after* the current frame is painted, so if
' we want a certain frame buffer state *before* rendering this frame, we set it
' via the disposal op of the *previous* frame).
'If the frame *before* the frame *before* this one is smallest...
If Int(prevFrameArea.Width * prevFrameArea.Height) < Int(dupArea.Width * dupArea.Height) Then
Set refFrame = prevFrame
m_allFrames(i).rectOfInterest = prevFrameArea
m_allFrames(lastGoodFrame).frameDisposal = gd_Previous
'or if the frame immediately preceding this one is smallest...
Else
Set refFrame = bufferFrame
m_allFrames(i).rectOfInterest = dupArea
m_allFrames(lastGoodFrame).frameDisposal = gd_Leave
End If
'We now have the smallest possible rectangle that defines this frame,
' while accounting for both DISPOSAL_LEAVE and DISPOSAL_PREVIOUS.
'We have one more potential crop operation we can do, and it involves the third
' disposal op (DISPOSAL_BACKGROUND). This disposal op asks the previous frame
' to erase itself completely after rendering. For animations with large
' transparent borders, it may actually be best to crop the current frame
' according to its transparent borders, then use the "erase" disposal op before
' displaying it, thus forgoing any connection whatsoever to preceding frames.
'
'(This is handled by a separate function; all frame flags will be set accordingly
' if the function succeeds.)
AnimatedGIF_CheckDisposalBkgd tmpLayer, i, lastGoodFrame
'Because the current frame came from a premultiplied source, we can safely
' mark it as premultiplied as well.
If (Not m_allFrames(i).frameDIB Is Nothing) Then m_allFrames(i).frameDIB.SetInitialAlphaPremultiplicationState True
'If the previous frame is not being blanked, we have additional optimization
' strategies to attempt. (If, however, the previous frame *is* being blanked,
' we are done with preprocessing because we have no "previous" data to work with.)
If (m_allFrames(lastGoodFrame).frameDisposal <> gd_Background) Then
'Pixel-blanking is handled by a dedicated function
AnimatedGIF_CheckPixelBlanking tmpLayer, refFrame, srcPDImage, i, lastGoodFrame
'/end "previous frame is NOT being blanked, so we can attempt to optimize frame diffs"
End If
'This (rare) branch means that the current frame is identical to the animation as it appeared
' *before* the previous frame was rendered. (This is not unheard of, especially for blinking-
' or spinning-style animations.) A great, cheap optimization is to just ask the previous
' frame to dispose of itself using the DISPOSE_OP_PREVIOUS method (which restores the frame
' buffer to whatever it was *before* the previous frame was rendered), then store this frame
' as a 1-px GIF that matches the top-left pixel color of the previous frame. This effectively
' gives us a copy of the frame two-frames-previous "for free". (Note that we can't just merge
' this frame with the previous one, because this frame doesn't *look* like frame n-1 - it looks
' like frame n-2, so we *must* still provide a frame here... but a 1-px one works fine!)
Else
'Create a 1-px DIB and set a corresponding frame rect
m_allFrames(i).frameNeedsTransparency = False
m_allFrames(i).frameDIB.CreateBlank 1, 1, 32, 0, 0
With m_allFrames(i).rectOfInterest
.Left = 0
.Top = 0
.Width = 1
.Height = 1
End With
m_allFrames(lastGoodFrame).frameDisposal = gd_Previous
'Set the pixel color to match the original frame.
Dim tmpQuad As RGBQuad
If prevFrame.GetPixelRGBQuad(0, 0, tmpQuad) Then m_allFrames(i).frameDIB.SetPixelRGBQuad 0, 0, tmpQuad
End If
'If the GetRectOfInterest() check failed, it means this frame is 100% identical to the
' frame that preceded it. Rather than optimize this frame, let's just delete it from
' the animation and merge its frame time into the previous frame.
Else
m_allFrames(i).frameIsDuplicateOrEmpty = True
m_allFrames(lastGoodFrame).frameTime = m_allFrames(lastGoodFrame).frameTime + m_allFrames(i).frameTime
Set m_allFrames(i).frameDIB = Nothing
End If
'This frame is now optimized as well as we can possibly optimize it.
'Before moving to the next frame, create backup copies of the buffer frames
' *we* were handed. The next frame can request that we reset our state to this
' frame, which may be closer to their frame's contents (and thus compress better).
If (m_allFrames(lastGoodFrame).frameDisposal = gd_Leave) Then
prevFrame.CreateFromExistingDIB bufferFrame
ElseIf (m_allFrames(lastGoodFrame).frameDisposal = gd_Background) Then
prevFrame.ResetDIB 0
'We don't have to cover the case of DISPOSE_OP_PREVIOUS, as that's the state the prevFrame
' DIB is already in!
'Else
End If
'Overwrite the *current* frame buffer with an (untouched) copy of this frame, as it appeared
' before we applied optimizations to it.
bufferFrame.CreateFromExistingDIB curFrameBackup
'If this frame is valid (e.g. not a duplicate of the previous frame), increment our current
' "good frame" count, and mark this frame as the "last good" index.
If (Not m_allFrames(i).frameIsDuplicateOrEmpty) Then
numGoodFrames = numGoodFrames + 1
lastGoodFrame = i
End If
'i !/= 0 branch
End If
'With optimizations accounted for, it is now time to palettize this layer.
' (If this frame is a duplicate of the previous frame, we don't need to perform any more
' optimizations on its pixel data, because we will simply reuse the previous frame in
' its place.)
If (Not m_allFrames(i).frameIsDuplicateOrEmpty) Then
'Generate an optimal 256-color palette for the image. (TODO: move this to our neural-network quantizer.)
' Note that this function will return an exact palette for the frame if the frame contains < 256 colors.
Palettes.GetOptimizedPaletteIncAlpha m_allFrames(i).frameDIB, imgPalette, 256, pdqs_Variance, True
numColorsInLP = UBound(imgPalette) + 1
'Ensure that in the course of producing an optimal palette, the optimizer didn't change
' any transparent values to number other than 0 or 255. (Neural network quantization can be fuzzy
' this way - sometimes values shift minute amounts due to the way neighboring colors affect
' each other.)
Palettes.EnsureBinaryAlphaPalette imgPalette
'Frames that need transparency are now guaranteed to have it in their *local* palette.
'If this is the *first* frame, we will use it as the basis of our *global* palette.
If (i = 0) Then
'Simply copy over the palette as-is into our running global palette tracker
m_numColorsInGP = numColorsInLP
ReDim m_globalPalette(0 To m_numColorsInGP - 1) As RGBQuad
For idxPalette = 0 To m_numColorsInGP - 1
m_globalPalette(idxPalette) = imgPalette(idxPalette)
Next idxPalette
'Sort the palette by popularity (with a few tweaks), which can eke out slightly
' better compression ratios. (Obviously we only have popularity data for the first
' frame, but in real-world usage this is a useful analog for the "average" frame
' that encodes using the global palette.)
Palettes.SortPaletteForCompression_IncAlpha m_allFrames(i).frameDIB, m_globalPalette, True, False
'The first frame always uses the global palette
frameUsesGP = True
'If this is *not* the first frame, and we have yet to write a global palette, append as many
' unique colors from this palette as we can into the global palette.
Else
'If there's still room in the global palette, append this palette to it.
Dim gpTooBig As Boolean: gpTooBig = False
If (m_numColorsInGP < 256) Then
m_numColorsInGP = Palettes.MergePalettes(m_globalPalette, m_numColorsInGP, imgPalette, numColorsInLP)
'Enforce a strict 256-color limit; colors past the end will simply be discarded, and this frame
' will use a local palette instead.
If (m_numColorsInGP > 256) Then
gpTooBig = True
m_numColorsInGP = 256
If (UBound(m_globalPalette) > 255) Then ReDim Preserve m_globalPalette(0 To 255) As RGBQuad
End If
End If
'Next, we need to see if all colors in this frame appear in the global palette.
' If they do, we can simply use the global palette to write this frame.
' (Note that we can automatically skip this step if the previous merge produced
' a too-big palette.)
If (Not gpTooBig) Then
frameUsesGP = Palettes.DoesPaletteContainPalette(m_globalPalette, m_numColorsInGP, imgPalette, numColorsInLP)
Else
frameUsesGP = False
End If
End If
m_allFrames(i).usesGlobalPalette = frameUsesGP
'Frames that use the global palette will be handled later, in a separate pass.
' Local palettes can be processed immediately, however, and we can free some
' of their larger structs (like their 32-bpp frame copy) immediately after.
If m_allFrames(i).usesGlobalPalette Then
'Suspend the DIB (e.g. compress it to a smaller memory stream) to reduce memory constraints
m_allFrames(i).frameDIB.SuspendDIB cf_Lz4, False
Else
'If the current frame requires transparency, and it's using a local palette,
' ensure transparency exists in the palette. (Global palettes will be handled
' in a separate loop, later. We do this because the global palette may not
' have a transparent entry *now*, but because GIF color tables have to be
' padded to the nearest power-of-two, we may get transparency "for free" when
' we finalize the table.)
Dim pEntry As Long
If m_allFrames(i).frameNeedsTransparency Then
Dim trnsFound As Boolean: trnsFound = False
For pEntry = 0 To UBound(imgPalette)
If (imgPalette(pEntry).Alpha = 0) Then
trnsFound = True
Exit For
End If
Next pEntry
'If transparency *wasn't* found, add it manually (if there's room).
If (Not trnsFound) Then
'There's room for another color in the palette! Create a transparent
' color and expand the palette accordingly. (TODO: technically we may
' not want to do this if there's already a power-of-two number of colors
' in the palette. The reason for this is that we'd have to bump-up
' the entire color count to the *next* power-of-two, which may negate
' any size gains we get from having access to a transparent pixel, argh.)
If (numColorsInLP < 256) Then
ReDim Preserve imgPalette(0 To numColorsInLP) As RGBQuad
imgPalette(numColorsInLP).Blue = 0
imgPalette(numColorsInLP).Green = 0
imgPalette(numColorsInLP).Red = 0
imgPalette(numColorsInLP).Alpha = 0
numColorsInLP = numColorsInLP + 1
'Damn, the local palette is full, meaning we can't add a transparent color
' without erasing an existing color. Because this affects our ability to
' losslessly save existing GIFs, dump our pixel-blank-optimized copy of
' the frame and revert to the original, untouched version.
Else
Set m_allFrames(i).frameDIB = m_allFrames(i).backupFrameDIB
Set m_allFrames(i).backupFrameDIB = Nothing
m_allFrames(i).frameNeedsTransparency = False
m_allFrames(i).frameWasBlanked = False
End If
End If
'/end frame needs transparency
End If
'With all optimizations applied, we are finally ready to palettize this layer
' - again *IF* it uses a local palette.
'In LZ77 encoding (e.g. DEFLATE), reordering palette can improve encoding efficiency
' because the sliding-window approach favors recent matches over past ones. LZ78 is
' different this way because it's deterministic and code-agnostic, due to the way it
' precisely matches data against a fixed table (which is fully discarded and rebuilt
' when the table fills). As such, we won't do a full sort - instead, we'll just do
' an "alpha" sort, which moves the transparent pixel (if any) to the front of the
' color table.
Palettes.SortPaletteForCompression_IncAlpha m_allFrames(i).frameDIB, imgPalette, True, True
'Transfer the final palette into the frame collection
m_allFrames(i).palNumColors = numColorsInLP
ReDim m_allFrames(i).framePalette(0 To numColorsInLP - 1)
CopyMemoryStrict VarPtr(m_allFrames(i).framePalette(0)), VarPtr(imgPalette(0)), numColorsInLP * 4
'One last optimization: if this frame received a pixel-blanking optimization pass,
' we want to compare compressibility of the blanked frame against the original frame.
' We can't be quite as aggressive with this pass (e.g. merging results from the two
' possible frames) because the two images may use wildly different palettes, unlike
' a global-palette frame where we are guaranteed that this frame consists only of
' shared colors.
If m_allFrames(i).frameWasBlanked And (Not m_allFrames(i).backupFrameDIB Is Nothing) Then
AnimatedGIF_CheckLocalPaletteCompressibility i, imgPalette
'Pixel blanking wasn't available for this frame (likely due to this frame containing transparency
' where the prior frame did not). Palettize it as-is.
Else
'Palettize the image and cache the result in the frame collection.
' (TODO: switch to neural-network quantizer.)
If useDithering Then
Palettes.GetPalettizedImage_Dithered_IncAlpha m_allFrames(i).frameDIB, imgPalette, m_allFrames(i).pixelData, PDDM_SierraLite, 0.67, True
Else
DIBs.GetDIBAs8bpp_RGBA_SrcPalette m_allFrames(i).frameDIB, imgPalette, m_allFrames(i).pixelData
End If
End If
'We can now free the (temporary) frame DIB copy, because it is either unnecessary
' (because this frame isn't being encoded) or because it has been palettized and
' stored inside m_allFrames(i).pixelData.
Set m_allFrames(i).frameDIB = Nothing
'/end frame uses local palette
End If
'/end frame is duplicate or empty
End If
'Next frame
Next i
'Clear all optimization-related objects, as they are no longer needed
Set prevFrame = Nothing
Set bufferFrame = Nothing
Set curFrameBackup = Nothing
Set refFrame = Nothing
'Note: at this point, frames that rely on the global palette have *not* been optimized yet.
' This is because they may be able to use transparency, which wasn't guaranteed present
' at the time of their original processing (because the global palette hadn't filled up yet.)
'So our next job is to get the global palette in order.
'The GIF spec requires all palette color counts to be a power of 2. (It does this because
' palette color count is stored in 3-bits, ugh.) Any unused entries are ignored, but by
' convention are usually left as black; we do the same here.
m_numColorsInGP = 2 ^ Pow2FromColorCount(m_numColorsInGP)
If (UBound(m_globalPalette) <> m_numColorsInGP - 1) Then ReDim Preserve m_globalPalette(0 To m_numColorsInGP - 1) As RGBQuad
'Move the transparent index, if any, to the front of the global palette
AnimatedGIF_SortGlobalPaletteAlpha
'With the global palette finalized, we can now do a final loop through all frames to palettize
' any frames that rely on the global palette.
For i = 0 To srcPDImage.GetNumOfLayers - 1
'Optimizing frames can take some time. Keep the user apprised of our progress.
ProgressBars.SetProgBarVal srcPDImage.GetNumOfLayers + i
Message "Saving animation frame %1 of %2...", i + 1, srcPDImage.GetNumOfLayers + 1, "DONOTLOG"
'The only frames we care about in this pass are non-empty, non-duplicate frames that rely
' on the global color table.
If (Not m_allFrames(i).frameIsDuplicateOrEmpty) And m_allFrames(i).usesGlobalPalette Then
'Basically, repeat the same steps we did with local palette frames, above.
'If this frame requires transparency, see if the global palette provides such a thing.
If m_allFrames(i).frameNeedsTransparency Then
'Damn, global palette does *not* have transparency support. Roll back to the non-blanked
' version of this frame.
If (m_GlobalTrnsIndex < 0) Then
Set m_allFrames(i).frameDIB = m_allFrames(i).backupFrameDIB
Set m_allFrames(i).backupFrameDIB = Nothing
m_allFrames(i).frameNeedsTransparency = False
m_allFrames(i).frameWasBlanked = False
End If
End If
'One last optimization: if this frame received a pixel-blanking optimization pass,
' we now want to generate a "merged" frame that combines the most-compressible
' scanlines from both the blanked frame and the original frame. This produces a
' "best of both worlds" result that compresses better than either frame alone.
If m_allFrames(i).frameWasBlanked And (Not m_allFrames(i).backupFrameDIB Is Nothing) Then
AnimatedGIF_CheckGlobalPaletteCompressibility i
Else
'Transparency has been dealt with (and rolled back, as necessary). All that's left to do
' is palettize this frame against the finished global palette! TODO: switch to neural network quantizer.
If useDithering Then
Palettes.GetPalettizedImage_Dithered_IncAlpha m_allFrames(i).frameDIB, m_globalPalette, m_allFrames(i).pixelData, PDDM_SierraLite, 0.67, True
Else
DIBs.GetDIBAs8bpp_RGBA_SrcPalette m_allFrames(i).frameDIB, m_globalPalette, m_allFrames(i).pixelData
End If