From 638b4c0584cadd11207ae24a1254ed3e7a0adc9b Mon Sep 17 00:00:00 2001 From: RD42 <42702181+dashr9230@users.noreply.github.com> Date: Wed, 6 Dec 2023 23:03:26 +0800 Subject: [PATCH] [exgui] Add GIFImage 2.2 unit --- exgui/GIFImage.dcu | Bin 0 -> 218953 bytes exgui/GIFImage.pas | 12560 +++++++++++++++++++++++++++++++++++++++++++ exgui/Main.pas | 2 +- 3 files changed, 12561 insertions(+), 1 deletion(-) create mode 100644 exgui/GIFImage.dcu create mode 100644 exgui/GIFImage.pas diff --git a/exgui/GIFImage.dcu b/exgui/GIFImage.dcu new file mode 100644 index 0000000000000000000000000000000000000000..e32def88a00bc0609665c5b85b693c7e9bf2e2c5 GIT binary patch literal 218953 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

a&=d%RhmUI;RcS6Z0RA^-*n> z9~^y^cvKAdB;0;?j{~+IQ*$sP=5}G3PlP)fm@2m{@yU(gL7+_BoE;$YVlQ3f%3&&B zSncEcwyJ2AvuLX*dO}}xhF`AZ;)ZINudL!XR*-7pyaZAkg`xPQKitjZ9d$4S7M&$x z(1~?^1X7Gj#o^${21GKywrc0E;%2XxFFl`#rG{a^Q;*xU3vinjKJBRMt)cP~zpu!{ z1P*1Ebi4-)#OE0?a9q0VKtPb^r4qa24M-ehOFNh{m}(dEXFTLr1WU{NDUxT-^VF)H zE5%KhaY_cExH1`6Di`N|@@2Z02z0io^3mmPx+GlEL8vr7X`bR+;m`Ncy>KK%N;K|rqdfD#jaFeL;PlhyQ z!UvM{=Beis=g-A=?1s~tQpO1=o{FooYK|S_L^)#Q0Fb=pk?U8ulG3&a$V1o-w(UaK zhyef?+|$NAXJBHGujYoR9=hT_n7YvqZ6k=XogBO&vcwrF=(^MqS(E{g1Kpt_)Q&&o zjBtRj2qNI67Hu48uAJCe)DYis7dkji&_#|7qz6;W?y`C*ncQ=PDmjZSpLqnk_Roo% z=$T3JO+5RDByZd(b_9FtL&^w4dn4Wh6%4iDWIh3@XM)2|N(;2ZpBC!V^Y*mQBPLSl!ii z>=Zl#E2pw>qkQZ=@}~?8HzX6k#CI!fBBFKcg}*i6DV;cu*zo#^bdP;NT;WZd|4b3M z%^@S;PnI~`L9>UvWujK9$?#;xN!&6cwQ~c;eWa00r=*W5>`|oG()1pJQH8YAo1;7^ zPRFJW!hmmxbs-G-L9-WG0d23>?2#NI-NxNu9@^W{miQ&L5IIiwwki064Aug>6lMUB zOcR5`zf$SojlIM5Ct`n}X2c0}@PbX=b`|AX6LNjA%h51XUZjz446-w3>7U5`XgW6#L>J2K_ufQh)2#q5LD9`2x13RcG~F}mL-dvF}iCpy2s_{03iDXfmPTxtb%Q4yh~=?89(y>w=LqmIqDY*JoS6U>h;9NTv?WdO zaiXZB)I#K#BEaWlP!S*lfMi90&#QDnfX9gdX`nO#>W)>B?=5RUCDFVNqG5YZ=B@Ap zjItPO`oqrD-=Yt=FlY7lAZAMr5qxn5`=J~`$$tqv@qw5eSiW!A9NYy@dO%IAJI(be zkT9RgMdOc+VJuSlTP$LyBOrWdi(F zLQqBRi)jy$5T+mVut?)~bmMs#A+o_KM0{{U@Gr@Q?{G!p1^oF-kkYOAp@&btgr6Jn z^9B6O#n1alzXX5(8-B1H)d$#D_Vcgq^D~~rY(~?Wmh~QvV;_C!g+zHavqpb?MS6m# z>^IrIud{uBUN3{$%xqiyWsoX#Md$IC#^5}>n}Kds`J#1j{#=cpvJVc$APr}ogENr% zVxG9G^2B{QPaN)Ly6eO8K^vWz3!X>74+znx%oU?2yO2bzeLaLA zLHMfc<@m^w-&NEI&S

w^8&q2Mcs~YrGgbtxyE;@v%c@vX*jH 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�|3`hK%Gz8Y3_7UfjBKGew)~v3e`j3%Z&ikCmgAJhCx3@+&V7s^>lSatZ63 zTA0HvG9$Ni5Q-N(q68enKq8Rzm(3o#P-lI=*z2Td$3E}*lTLKRM{mhxMHNFNa|yHk zC?vFB4%WBt-gHCEup`_V;?@%Wjj6xsC}Kd)+X^09)65l)?7F*kN4+*eM=c86f7n{n zy2bIRwAQR!`cc1Bt@-ZKkXj?me%V^PbIW_}rQSO^HuOl8Yj~>KL9z%{dZFPO5j)d} zvIn?}8*szZV)G3L%VV!g+rV2<`FLUp5Cfp(%d;q&W*J3fzzfjG7aK;#?0<+5(J?|F zpZ#@$1b(>e%Kh?%J0DwoDRz0LzbkWSd9J@JC%_{i;5NRizpLxe@)iAED}1H)tx_HP zEp+T5YTJlP?Kuh$@IKVtEC?V>gaTlCuwy;l?ZD#of_2Ac(5Dzd4YWwnpBHS zDpqpIvBW$t@oQ4V)DPL--?jbF@<;o-9`!>W@9%p2(DI%AT_XIX{s#}xV*Ll?_s~Hx zS@a+94HrV4UHx6V4lUo)-?hh2zt^OZ-$MuYn`ikoSP$>}mCwlaO!WU?fqm%GhZ)^N z)^)hT4%}EaDAKZuki-HvHql#MlQKfXydIiLa};^lyxuP0SN3gTVjPIycW5b7@GX>g z)H&uy!@1yR;lh443g!KG4VM~ysH?ww1@#rxD8p?gZbd5soAF+NUS8#0{=@YiLly}1< zzRK3{V_;_Kj)}FjWxhcXLw95w+fKv^<#*i`Q^ikB3X%S02r)BzF*CSkLJgvRC$yiT z1li4{l{32c14S67-wH9;+j)}3TOh}-{ zmCP32@;5SALm(YXDEy@m18K$XBavM(mwwQ(R?ao`j57%azv|Ou`*wX8Wi7FnF*^=2Fo% zf98%hUXn}L0tv_t5sF#6cKefCAKdcAhacPWCApcz=n8LhaqQKB-pl5)C{bG3%_dZ1 z(dMTHX|`Rz?T*`viJPFRrFgLoln;|sKJy?%7@UaGYjOt&1ZT7N{Z!8W`@0t)*WO+A zm|MPNbsc};ly7Nmv_Wj%GLtF!w3gqi0?ob4oylAn>1j8tw@?L#(V?e*b4sIc-ZfIe zP{jOPx?#G7otDkA06KbVWC_}mjq%+sb-G(K{=%mB=ax=xKN3dGm=)|iYwe+qB{GAl zlMw-!@Ty#nWI!|VvmXSqhw74S?Xva5Q2kWDD4lI z=?^~+s`XFy^}cL+lnV;t26Klxe`=E%NEYE;#goB?bQv%M33Kk!&JTIsJ!a=KBehD< z+n=z^;x~*FaaV%)0UIwn0bSfR%aS8^(Xa{9A7pwEZgJVc9_R2=FKa^Vrsf81-r{@E z#v!_~9PXcJnv$97)&9h#h|e%qA?)f&Y&*7@Fu8=Slk42*`yBn1c9hSXqe{qFJJj51 zl}o>FGLRrOmT&Jr@GWUGH0A?xNq`^zc>jU#nDCwb2fiB$-_?KMdnSBO|AFrp!w>G| zx&OfP65>>3IrQ(KIfYxs9$(>>Q?4I&M!lY?k_PF4WOtR{N#ogy56YU^!S3}WG0iMV z&8m&LJj;l|FpWP5s%}h-CX0c2)f_M_f#HTEu@)ItDH%C>K+cyEa>TJ@`DX^IJqDy3 zq@fws<;!l+MS`

mo2}X#E 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 HT#Em{O7~Ld literal 0 HcmV?d00001 diff --git a/exgui/GIFImage.pas b/exgui/GIFImage.pas new file mode 100644 index 0000000..8529e42 --- /dev/null +++ b/exgui/GIFImage.pas @@ -0,0 +1,12560 @@ +unit GIFImage; +//////////////////////////////////////////////////////////////////////////////// +// // +// Project: GIF Graphics Object // +// Module: gifimage // +// Description: TGraphic implementation of the GIF89a graphics format // +// Version: 2.2 // +// Release: 5 // +// Date: 23-MAY-1999 // +// Target: Win32, Delphi 2, 3, 4 & 5, C++ Builder 3 & 4 // +// Author(s): anme: Anders Melander, anders@melander.dk // +// fila: Filip Larsen // +// rps: Reinier Sterkenburg // +// Copyright: (c) 1997-99 Anders Melander. // +// All rights reserved. // +// Formatting: 2 space indent, 8 space tabs, 80 columns. // +// // +//////////////////////////////////////////////////////////////////////////////// +// Changed 2001.07.23 by Finn Tolderlund // +// Changed according to e-mail from "Rolf Frei" // +// on 2001.07.23 so that it works in Delphi 6. // +// // +// Changed 2002.07.07 by Finn Tolderlund // +// Incorporated additional modifications by Alexey Barkovoy (clootie@reactor.ru) +// found in his Delphi 6 GifImage.pas (from 22-Dec-2001). // +// Alexey Barkovoy's Delphi 6 gifimage.pas can be downloaded from // +// http://clootie.narod.ru/delphi/download_vcl.html // +// These changes made showing of animated gif files more stable. The code // +// from 2001.07.23 could crash sometimes with an Execption EAccessViolation. // +// // +//////////////////////////////////////////////////////////////////////////////// +// // +// Please read the "Conditions of use" in the release notes. // +// // +//////////////////////////////////////////////////////////////////////////////// +// Known problems: +// +// * The combination of buffered, tiled and transparent draw will display the +// background incorrectly (scaled). +// If this is a problem for you, use non-buffered (goDirectDraw) drawing +// instead. +// +// * The combination of non-buffered, transparent and stretched draw is +// sometimes distorted with a pattern effect when the image is displayed +// smaller than the real size (shrinked). +// +// * Buffered display flickers when TGIFImage is used by a transparent TImage +// component. +// This is a problem with TImage caused by the fact that TImage was designed +// with static images in mind. Not much I can do about it. +// +//////////////////////////////////////////////////////////////////////////////// +// To do (in rough order of priority): +// { TODO -oanme -cFeature : TImage hook for destroy notification. } +// { TODO -oanme -cFeature : TBitmap pool to limit resource consumption on Win95/98. } +// { TODO -oanme -cImprovement : Make BitsPerPixel property writable. } +// { TODO -oanme -cFeature : Visual GIF component. } +// { TODO -oanme -cImprovement : Easier method to determine DrawPainter status. } +// { TODO -oanme -cFeature : Import to 256+ color GIF. } +// { TODO -oanme -cFeature : Make some of TGIFImage's properties persistent (DrawOptions etc). } +// { TODO -oanme -cFeature : Add TGIFImage.Persistent property. Should save published properties in application extension when this options is set. } +// { TODO -oanme -cBugFix : Solution for background buffering in scrollbox. } +// +////////////////////////////////////////////////////////////////////////////////// +{$ifdef BCB} +{$ObjExportAll On} +{$endif} + +interface +//////////////////////////////////////////////////////////////////////////////// +// +// Conditional Compiler Symbols +// +//////////////////////////////////////////////////////////////////////////////// +(* + DEBUG Must be defined if any of the DEBUG_xxx + symbols are defined. + If the symbol is defined the source will not be + optimized and overflow- and range checks will be + enabled. + + DEBUG_HASHPERFORMANCE Calculates hash table performance data. + DEBUG_HASHFILLFACTOR Calculates fill factor of hash table - + Interferes with DEBUG_HASHPERFORMANCE. + DEBUG_COMPRESSPERFORMANCE Calculates LZW compressor performance data. + DEBUG_DECOMPRESSPERFORMANCE Calculates LZW decompressor performance data. + DEBUG_DITHERPERFORMANCE Calculates color reduction performance data. + DEBUG_DRAWPERFORMANCE Calculates low level drawing performance data. + The performance data for DEBUG_DRAWPERFORMANCE + will be displayed when you press the Ctrl key. + DEBUG_RENDERPERFORMANCE Calculates performance data for the GIF to + bitmap converter. + The performance data for DEBUG_DRAWPERFORMANCE + will be displayed when you press the Ctrl key. + + GIF_NOSAFETY Define this symbol to disable overflow- and + range checks. + Ignored if the DEBUG symbol is defined. + + STRICT_MOZILLA Define to mimic Mozilla as closely as possible. + If not defined, a slightly more "optimal" + implementation is used (IMHO). + + FAST_AS_HELL Define this symbol to use strictly GIF compliant + (but too fast) animation timing. + Since our paint routines are much faster and + more precise timed than Mozilla's, the standard + GIF and Mozilla values causes animations to loop + faster than they would in Mozilla. + If the symbol is _not_ defined, an alternative + set of tweaked timing values will be used. + The tweaked values are not optimal but are based + on tests performed on my reference system: + - Windows 95 + - 133 MHz Pentium + - 64Mb RAM + - Diamond Stealth64/V3000 + - 1600*1200 in 256 colors + The alternate values can be modified if you are + not satisfied with my defaults (they can be + found a few pages down). + + REGISTER_TGIFIMAGE Define this symbol to register TGIFImage with + the TPicture class and integrate with TImage. + This is required to be able to display GIFs in + the TImage component. + The symbol is defined by default. + Undefine if you use another GIF library to + provide GIF support for TImage. + + PIXELFORMAT_TOO_SLOW When this symbol is defined, the internal + PixelFormat routines are used in some places + instead of TBitmap.PixelFormat. + The current implementation (Delphi4, Builder 3) + of TBitmap.PixelFormat can in some situation + degrade performance. + The symbol is defined by default. + + CREATEDIBSECTION_SLOW If this symbol is defined, TDIBWriter will + use global memory as scanline storage, instead + of a DIB section. + Benchmarks have shown that a DIB section is + twice as slow as global memory. + The symbol is defined by default. + The symbol requires that PIXELFORMAT_TOO_SLOW + is defined. + + SERIALIZE_RENDER Define this symbol to serialize threaded + GIF to bitmap rendering. + When a GIF is displayed with the goAsync option + (the default), the GIF to bitmap rendering is + executed in the context of the draw thread. + If more than one thread is drawing the same GIF + or the GIF is being modified while it is + animating, the GIF to bitmap rendering should be + serialized to guarantee that the bitmap isn't + modified by more than one thread at a time. If + SERIALIZE_RENDER is defined, the draw threads + uses TThread.Synchronize to serialize GIF to + bitmap rendering. +*) + +{$DEFINE REGISTER_TGIFIMAGE} +{$DEFINE PIXELFORMAT_TOO_SLOW} +{$DEFINE CREATEDIBSECTION_SLOW} + +//////////////////////////////////////////////////////////////////////////////// +// +// Determine Delphi and C++ Builder version +// +//////////////////////////////////////////////////////////////////////////////// + +// Delphi 1.x +{$IFDEF VER80} + 'Error: TGIFImage does not support Delphi 1.x' +{$ENDIF} + +// Delphi 2.x +{$IFDEF VER90} + {$DEFINE VER9x} +{$ENDIF} + +// C++ Builder 1.x +{$IFDEF VER93} + // Good luck... + {$DEFINE VER9x} +{$ENDIF} + +// Delphi 3.x +{$IFDEF VER100} + {$DEFINE VER10_PLUS} + {$DEFINE D3_BCB3} +{$ENDIF} + +// C++ Builder 3.x +{$IFDEF VER110} + {$DEFINE VER10_PLUS} + {$DEFINE VER11_PLUS} + {$DEFINE D3_BCB3} + {$DEFINE BAD_STACK_ALIGNMENT} +{$ENDIF} + +// Delphi 4.x +{$IFDEF VER120} + {$DEFINE VER10_PLUS} + {$DEFINE VER11_PLUS} + {$DEFINE VER12_PLUS} + {$DEFINE BAD_STACK_ALIGNMENT} +{$ENDIF} + +// C++ Builder 4.x +{$IFDEF VER125} + {$DEFINE VER10_PLUS} + {$DEFINE VER11_PLUS} + {$DEFINE VER12_PLUS} + {$DEFINE VER125_PLUS} + {$DEFINE BAD_STACK_ALIGNMENT} +{$ENDIF} + +// Delphi 5.x +{$IFDEF VER130} + {$DEFINE VER10_PLUS} + {$DEFINE VER11_PLUS} + {$DEFINE VER12_PLUS} + {$DEFINE VER125_PLUS} + {$DEFINE VER13_PLUS} + {$DEFINE BAD_STACK_ALIGNMENT} +{$ENDIF} + +// Delphi 6.x +{$IFDEF VER140} +{$WARN SYMBOL_PLATFORM OFF} + {$DEFINE VER10_PLUS} + {$DEFINE VER11_PLUS} + {$DEFINE VER12_PLUS} + {$DEFINE VER125_PLUS} + {$DEFINE VER13_PLUS} + {$DEFINE VER14_PLUS} + {$DEFINE BAD_STACK_ALIGNMENT} +{$ENDIF} + +// Unknown compiler version - assume D4 compatible +{$IFNDEF VER9x} + {$IFNDEF VER10_PLUS} + {$DEFINE VER10_PLUS} + {$DEFINE VER11_PLUS} + {$DEFINE VER12_PLUS} + {$DEFINE BAD_STACK_ALIGNMENT} + {$ENDIF} +{$ENDIF} + +//////////////////////////////////////////////////////////////////////////////// +// +// Compiler Options required to compile this library +// +//////////////////////////////////////////////////////////////////////////////// +{$A+,B-,H+,J+,K-,M-,T-,X+} + +// Debug control - You can safely change these settings +{$IFDEF DEBUG} + {$C+} // ASSERTIONS + {$O-} // OPTIMIZATION + {$Q+} // OVERFLOWCHECKS + {$R+} // RANGECHECKS +{$ELSE} + {$C-} // ASSERTIONS + {$IFDEF GIF_NOSAFETY} + {$Q-}// OVERFLOWCHECKS + {$R-}// RANGECHECKS + {$ENDIF} +{$ENDIF} + +// Special options for Time2Help parser +{$ifdef TIME2HELP} +{$UNDEF PIXELFORMAT_TOO_SLOW} +{$endif} + +//////////////////////////////////////////////////////////////////////////////// +// +// External dependecies +// +//////////////////////////////////////////////////////////////////////////////// +uses + sysutils, + Windows, + Graphics, + Classes; + +//////////////////////////////////////////////////////////////////////////////// +// +// TGIFImage library version +// +//////////////////////////////////////////////////////////////////////////////// +const + GIFVersion = $0202; + GIFVersionMajor = 2; + GIFVersionMinor = 2; + GIFVersionRelease = 5; + +//////////////////////////////////////////////////////////////////////////////// +// +// Misc constants and support types +// +//////////////////////////////////////////////////////////////////////////////// +const + GIFMaxColors = 256; // Max number of colors supported by GIF + // Don't bother changing this value! + + BitmapAllocationThreshold = 500000; // Bitmap pixel count limit at which + // a newly allocated bitmap will be + // converted to 1 bit format before + // being resized and converted to 8 bit. + +var +{$IFDEF FAST_AS_HELL} + GIFDelayExp: integer = 10; // Delay multiplier in mS. +{$ELSE} + GIFDelayExp: integer = 12; // Delay multiplier in mS. Tweaked. +{$ENDIF} + // * GIFDelayExp: + // The following delay values should all + // be multiplied by this value to + // calculate the effective time (in mS). + // According to the GIF specs, this + // value should be 10. + // Since our paint routines are much + // faster than Mozilla's, you might need + // to increase this value if your + // animations loops too fast. The + // optimal value is impossible to + // determine since it depends on the + // speed of the CPU, the viceo card, + // memory and many other factors. + + GIFDefaultDelay: integer = 10; // * GIFDefaultDelay: + // Default animation delay. + // This value is used if no GCE is + // defined. + // (10 = 100 mS) + +{$IFDEF FAST_AS_HELL} + GIFMinimumDelay: integer = 1; // Minimum delay (from Mozilla source). + // (1 = 10 mS) +{$ELSE} + GIFMinimumDelay: integer = 3; // Minimum delay - Tweaked. +{$ENDIF} + // * GIFMinimumDelay: + // The minumum delay used in the Mozilla + // source is 10mS. This corresponds to a + // value of 1. However, since our paint + // routines are much faster than + // Mozilla's, a value of 3 or 4 gives + // better results. + + GIFMaximumDelay: integer = 1000; // * GIFMaximumDelay: + // Maximum delay when painter is running + // in main thread (goAsync is not set). + // This value guarantees that a very + // long and slow GIF does not hang the + // system. + // (1000 = 10000 mS = 10 Seconds) + +type + TGIFVersion = (gvUnknown, gv87a, gv89a); + TGIFVersionRec = array[0..2] of char; + +const + GIFVersions : array[gv87a..gv89a] of TGIFVersionRec = ('87a', '89a'); + +type + // TGIFImage mostly throws exceptions of type GIFException + GIFException = class(EInvalidGraphic); + + // Severity level as indicated in the Warning methods and the OnWarning event + TGIFSeverity = (gsInfo, gsWarning, gsError); + +//////////////////////////////////////////////////////////////////////////////// +// +// Delphi 2.x support +// +//////////////////////////////////////////////////////////////////////////////// +{$IFDEF VER9x} +// Delphi 2 doesn't support TBitmap.PixelFormat +{$DEFINE PIXELFORMAT_TOO_SLOW} +type + // TThreadList from Delphi 3 classes.pas + TThreadList = class + private + FList: TList; + FLock: TRTLCriticalSection; + public + constructor Create; + destructor Destroy; override; + procedure Add(Item: Pointer); + procedure Clear; + function LockList: TList; + procedure Remove(Item: Pointer); + procedure UnlockList; + end; + + // From Delphi 3 sysutils.pas + EOutOfMemory = class(Exception); + + // From Delphi 3 classes.pas + EOutOfResources = class(EOutOfMemory); + + // From Delphi 3 windows.pas + PMaxLogPalette = ^TMaxLogPalette; + TMaxLogPalette = packed record + palVersion: Word; + palNumEntries: Word; + palPalEntry: array [Byte] of TPaletteEntry; + end; { TMaxLogPalette } + + // From Delphi 3 graphics.pas. Used by the D3 TGraphic class. + TProgressStage = (psStarting, psRunning, psEnding); + TProgressEvent = procedure (Sender: TObject; Stage: TProgressStage; + PercentDone: Byte; RedrawNow: Boolean; const R: TRect; const Msg: string) of object; + + // From Delphi 3 windows.pas + PRGBTriple = ^TRGBTriple; +{$ENDIF} + +//////////////////////////////////////////////////////////////////////////////// +// +// Forward declarations +// +//////////////////////////////////////////////////////////////////////////////// +type + TGIFImage = class; + TGIFSubImage = class; + +//////////////////////////////////////////////////////////////////////////////// +// +// TGIFItem +// +//////////////////////////////////////////////////////////////////////////////// + TGIFItem = class(TPersistent) + private + FGIFImage: TGIFImage; + protected + function GetVersion: TGIFVersion; virtual; + procedure Warning(Severity: TGIFSeverity; Message: string); virtual; + public + constructor Create(GIFImage: TGIFImage); virtual; + + procedure SaveToStream(Stream: TStream); virtual; abstract; + procedure LoadFromStream(Stream: TStream); virtual; abstract; + procedure SaveToFile(const Filename: string); virtual; + procedure LoadFromFile(const Filename: string); virtual; + property Version: TGIFVersion read GetVersion; + property Image: TGIFImage read FGIFImage; + end; + +//////////////////////////////////////////////////////////////////////////////// +// +// TGIFList +// +//////////////////////////////////////////////////////////////////////////////// + TGIFList = class(TPersistent) + private + FItems: TList; + FImage: TGIFImage; + protected + function GetItem(Index: Integer): TGIFItem; + procedure SetItem(Index: Integer; Item: TGIFItem); + function GetCount: Integer; + procedure Warning(Severity: TGIFSeverity; Message: string); virtual; + public + constructor Create(Image: TGIFImage); + destructor Destroy; override; + + function Add(Item: TGIFItem): Integer; + procedure Clear; + procedure Delete(Index: Integer); + procedure Exchange(Index1, Index2: Integer); + function First: TGIFItem; + function IndexOf(Item: TGIFItem): Integer; + procedure Insert(Index: Integer; Item: TGIFItem); + function Last: TGIFItem; + procedure Move(CurIndex, NewIndex: Integer); + function Remove(Item: TGIFItem): Integer; + procedure SaveToStream(Stream: TStream); virtual; + procedure LoadFromStream(Stream: TStream; Parent: TObject); virtual; abstract; + + property Items[Index: Integer]: TGIFItem read GetItem write SetItem; default; + property Count: Integer read GetCount; + property List: TList read FItems; + property Image: TGIFImage read FImage; + end; + +//////////////////////////////////////////////////////////////////////////////// +// +// TGIFColorMap +// +//////////////////////////////////////////////////////////////////////////////// + // One way to do it: + // TBaseColor = (bcRed, bcGreen, bcBlue); + // TGIFColor = array[bcRed..bcBlue] of BYTE; + // Another way: + TGIFColor = packed record + Red: byte; + Green: byte; + Blue: byte; + end; + + TColorMap = packed array[0..GIFMaxColors-1] of TGIFColor; + PColorMap = ^TColorMap; + + TUsageCount = record + Count : integer; // # of pixels using color index + Index : integer; // Color index + end; + TColormapHistogram = array[0..255] of TUsageCount; + TColormapReverse = array[0..255] of byte; + + TGIFColorMap = class(TPersistent) + private + FColorMap : PColorMap; + FCount : integer; + FCapacity : integer; + FOptimized : boolean; + protected + function GetColor(Index: integer): TColor; + procedure SetColor(Index: integer; Value: TColor); + function GetBitsPerPixel: integer; + function DoOptimize: boolean; + procedure SetCapacity(Size: integer); + procedure Warning(Severity: TGIFSeverity; Message: string); virtual; abstract; + procedure BuildHistogram(var Histogram: TColormapHistogram); virtual; abstract; + procedure MapImages(var Map: TColormapReverse); virtual; abstract; + + public + constructor Create; + destructor Destroy; override; + class function Color2RGB(Color: TColor): TGIFColor; + class function RGB2Color(Color: TGIFColor): TColor; + procedure SaveToStream(Stream: TStream); + procedure LoadFromStream(Stream: TStream; Count: integer); + procedure Assign(Source: TPersistent); override; + function IndexOf(Color: TColor): integer; + function Add(Color: TColor): integer; + function AddUnique(Color: TColor): integer; + procedure Delete(Index: integer); + procedure Clear; + function Optimize: boolean; virtual; abstract; + procedure Changed; virtual; abstract; + procedure ImportPalette(Palette: HPalette); + procedure ImportColorTable(Pal: pointer; Count: integer); + procedure ImportDIBColors(Handle: HDC); + procedure ImportColorMap(Map: TColorMap; Count: integer); + function ExportPalette: HPalette; + property Colors[Index: integer]: TColor read GetColor write SetColor; default; + property Data: PColorMap read FColorMap; + property Count: integer read FCount; + property Optimized: boolean read FOptimized write FOptimized; + property BitsPerPixel: integer read GetBitsPerPixel; + end; + +//////////////////////////////////////////////////////////////////////////////// +// +// TGIFHeader +// +//////////////////////////////////////////////////////////////////////////////// + TLogicalScreenDescriptor = packed record + ScreenWidth: word; { logical screen width } + ScreenHeight: word; { logical screen height } + PackedFields: byte; { packed fields } + BackgroundColorIndex: byte; { index to global color table } + AspectRatio: byte; { actual ratio = (AspectRatio + 15) / 64 } + end; + + TGIFHeader = class(TGIFItem) + private + FLogicalScreenDescriptor: TLogicalScreenDescriptor; + FColorMap : TGIFColorMap; + procedure Prepare; + protected + function GetVersion: TGIFVersion; override; + function GetBackgroundColor: TColor; + procedure SetBackgroundColor(Color: TColor); + procedure SetBackgroundColorIndex(Index: BYTE); + function GetBitsPerPixel: integer; + function GetColorResolution: integer; + public + constructor Create(GIFImage: TGIFImage); override; + destructor Destroy; override; + procedure Assign(Source: TPersistent); override; + procedure SaveToStream(Stream: TStream); override; + procedure LoadFromStream(Stream: TStream); override; + procedure Clear; + property Version: TGIFVersion read GetVersion; + property Width: WORD read FLogicalScreenDescriptor.ScreenWidth + write FLogicalScreenDescriptor.ScreenWidth; + property Height: WORD read FLogicalScreenDescriptor.ScreenHeight + write FLogicalScreenDescriptor.Screenheight; + property BackgroundColorIndex: BYTE read FLogicalScreenDescriptor.BackgroundColorIndex + write SetBackgroundColorIndex; + property BackgroundColor: TColor read GetBackgroundColor + write SetBackgroundColor; + property AspectRatio: BYTE read FLogicalScreenDescriptor.AspectRatio + write FLogicalScreenDescriptor.AspectRatio; + property ColorMap: TGIFColorMap read FColorMap; + property BitsPerPixel: integer read GetBitsPerPixel; + property ColorResolution: integer read GetColorResolution; + end; + +//////////////////////////////////////////////////////////////////////////////// +// +// TGIFExtension +// +//////////////////////////////////////////////////////////////////////////////// + TGIFExtensionType = BYTE; + TGIFExtension = class; + TGIFExtensionClass = class of TGIFExtension; + + TGIFGraphicControlExtension = class; + + TGIFExtension = class(TGIFItem) + private + FSubImage: TGIFSubImage; + protected + function GetExtensionType: TGIFExtensionType; virtual; abstract; + function GetVersion: TGIFVersion; override; + function DoReadFromStream(Stream: TStream): TGIFExtensionType; + class procedure RegisterExtension(elabel: BYTE; eClass: TGIFExtensionClass); + class function FindExtension(Stream: TStream): TGIFExtensionClass; + class function FindSubExtension(Stream: TStream): TGIFExtensionClass; virtual; + public + // Ignore compiler warning about hiding base class constructor + constructor Create(ASubImage: TGIFSubImage); {$IFDEF VER12_PLUS} reintroduce; {$ENDIF} virtual; + destructor Destroy; override; + procedure SaveToStream(Stream: TStream); override; + procedure LoadFromStream(Stream: TStream); override; + property ExtensionType: TGIFExtensionType read GetExtensionType; + property SubImage: TGIFSubImage read FSubImage; + end; + +//////////////////////////////////////////////////////////////////////////////// +// +// TGIFSubImage +// +//////////////////////////////////////////////////////////////////////////////// + TGIFExtensionList = class(TGIFList) + protected + function GetExtension(Index: Integer): TGIFExtension; + procedure SetExtension(Index: Integer; Extension: TGIFExtension); + public + procedure LoadFromStream(Stream: TStream; Parent: TObject); override; + property Extensions[Index: Integer]: TGIFExtension read GetExtension write SetExtension; default; + end; + + TImageDescriptor = packed record + Separator: byte; { fixed value of ImageSeparator } + Left: word; { Column in pixels in respect to left edge of logical screen } + Top: word; { row in pixels in respect to top of logical screen } + Width: word; { width of image in pixels } + Height: word; { height of image in pixels } + PackedFields: byte; { Bit fields } + end; + + TGIFSubImage = class(TGIFItem) + private + FBitmap : TBitmap; + FMask : HBitmap; + FNeedMask : boolean; + FLocalPalette : HPalette; + FData : PChar; + FDataSize : integer; + FColorMap : TGIFColorMap; + FImageDescriptor : TImageDescriptor; + FExtensions : TGIFExtensionList; + FTransparent : boolean; + FGCE : TGIFGraphicControlExtension; + procedure Prepare; + procedure Compress(Stream: TStream); + procedure Decompress(Stream: TStream); + protected + function GetVersion: TGIFVersion; override; + function GetInterlaced: boolean; + procedure SetInterlaced(Value: boolean); + function GetColorResolution: integer; + function GetBitsPerPixel: integer; + procedure AssignTo(Dest: TPersistent); override; + function DoGetBitmap: TBitmap; + function DoGetDitherBitmap: TBitmap; + function GetBitmap: TBitmap; + procedure SetBitmap(Value: TBitmap); + procedure FreeMask; + function GetEmpty: Boolean; + function GetPalette: HPALETTE; + procedure SetPalette(Value: HPalette); + function GetActiveColorMap: TGIFColorMap; + function GetBoundsRect: TRect; + procedure SetBoundsRect(const Value: TRect); + procedure DoSetBounds(ALeft, ATop, AWidth, AHeight: integer); + function GetClientRect: TRect; + function GetPixel(x, y: integer): BYTE; + function GetScanline(y: integer): pointer; + procedure NewBitmap; + procedure FreeBitmap; + procedure NewImage; + procedure FreeImage; + procedure NeedImage; + function ScaleRect(DestRect: TRect): TRect; + function HasMask: boolean; + function GetBounds(Index: integer): WORD; + procedure SetBounds(Index: integer; Value: WORD); + function GetHasBitmap: boolean; + procedure SetHasBitmap(Value: boolean); + public + constructor Create(GIFImage: TGIFImage); override; + destructor Destroy; override; + procedure Clear; + procedure SaveToStream(Stream: TStream); override; + procedure LoadFromStream(Stream: TStream); override; + procedure Assign(Source: TPersistent); override; + procedure Draw(ACanvas: TCanvas; const Rect: TRect; + DoTransparent, DoTile: boolean); + procedure StretchDraw(ACanvas: TCanvas; const Rect: TRect; + DoTransparent, DoTile: boolean); + procedure Crop; + procedure Merge(Previous: TGIFSubImage); + property HasBitmap: boolean read GetHasBitmap write SetHasBitmap; + property Left: WORD index 1 read GetBounds write SetBounds; + property Top: WORD index 2 read GetBounds write SetBounds; + property Width: WORD index 3 read GetBounds write SetBounds; + property Height: WORD index 4 read GetBounds write SetBounds; + property BoundsRect: TRect read GetBoundsRect write SetBoundsRect; + property ClientRect: TRect read GetClientRect; + property Interlaced: boolean read GetInterlaced write SetInterlaced; + property ColorMap: TGIFColorMap read FColorMap; + property ActiveColorMap: TGIFColorMap read GetActiveColorMap; + property Data: PChar read FData; + property DataSize: integer read FDataSize; + property Extensions: TGIFExtensionList read FExtensions; + property Version: TGIFVersion read GetVersion; + property ColorResolution: integer read GetColorResolution; + property BitsPerPixel: integer read GetBitsPerPixel; + property Bitmap: TBitmap read GetBitmap write SetBitmap; + property Mask: HBitmap read FMask; + property Palette: HPALETTE read GetPalette write SetPalette; + property Empty: boolean read GetEmpty; + property Transparent: boolean read FTransparent; + property GraphicControlExtension: TGIFGraphicControlExtension read FGCE; + property Pixels[x, y: integer]: BYTE read GetPixel; + property Scanline[y: integer]: pointer read GetScanline; + end; + +//////////////////////////////////////////////////////////////////////////////// +// +// TGIFTrailer +// +//////////////////////////////////////////////////////////////////////////////// + TGIFTrailer = class(TGIFItem) + procedure SaveToStream(Stream: TStream); override; + procedure LoadFromStream(Stream: TStream); override; + end; + +//////////////////////////////////////////////////////////////////////////////// +// +// TGIFGraphicControlExtension +// +//////////////////////////////////////////////////////////////////////////////// + // Graphic Control Extension block a.k.a GCE + TGIFGCERec = packed record + BlockSize: byte; { should be 4 } + PackedFields: Byte; + DelayTime: Word; { in centiseconds } + TransparentColorIndex: Byte; + Terminator: Byte; + end; + + TDisposalMethod = (dmNone, dmNoDisposal, dmBackground, dmPrevious); + + TGIFGraphicControlExtension = class(TGIFExtension) + private + FGCExtension: TGIFGCERec; + protected + function GetExtensionType: TGIFExtensionType; override; + function GetTransparent: boolean; + procedure SetTransparent(Value: boolean); + function GetTransparentColor: TColor; + procedure SetTransparentColor(Color: TColor); + function GetTransparentColorIndex: BYTE; + procedure SetTransparentColorIndex(Value: BYTE); + function GetDelay: WORD; + procedure SetDelay(Value: WORD); + function GetUserInput: boolean; + procedure SetUserInput(Value: boolean); + function GetDisposal: TDisposalMethod; + procedure SetDisposal(Value: TDisposalMethod); + + public + constructor Create(ASubImage: TGIFSubImage); override; + destructor Destroy; override; + procedure SaveToStream(Stream: TStream); override; + procedure LoadFromStream(Stream: TStream); override; + property Delay: WORD read GetDelay write SetDelay; + property Transparent: boolean read GetTransparent write SetTransparent; + property TransparentColorIndex: BYTE read GetTransparentColorIndex + write SetTransparentColorIndex; + property TransparentColor: TColor read GetTransparentColor write SetTransparentColor; + property UserInput: boolean read GetUserInput write SetUserInput; + property Disposal: TDisposalMethod read GetDisposal write SetDisposal; + end; + +//////////////////////////////////////////////////////////////////////////////// +// +// TGIFTextExtension +// +//////////////////////////////////////////////////////////////////////////////// + TGIFPlainTextExtensionRec = packed record + BlockSize: byte; { should be 12 } + Left, Top, Width, Height: Word; + CellWidth, CellHeight: Byte; + TextFGColorIndex, + TextBGColorIndex: Byte; + end; + + TGIFTextExtension = class(TGIFExtension) + private + FText : TStrings; + FPlainTextExtension : TGIFPlainTextExtensionRec; + protected + function GetExtensionType: TGIFExtensionType; override; + function GetForegroundColor: TColor; + procedure SetForegroundColor(Color: TColor); + function GetBackgroundColor: TColor; + procedure SetBackgroundColor(Color: TColor); + function GetBounds(Index: integer): WORD; + procedure SetBounds(Index: integer; Value: WORD); + function GetCharWidthHeight(Index: integer): BYTE; + procedure SetCharWidthHeight(Index: integer; Value: BYTE); + function GetColorIndex(Index: integer): BYTE; + procedure SetColorIndex(Index: integer; Value: BYTE); + public + constructor Create(ASubImage: TGIFSubImage); override; + destructor Destroy; override; + procedure SaveToStream(Stream: TStream); override; + procedure LoadFromStream(Stream: TStream); override; + property Left: WORD index 1 read GetBounds write SetBounds; + property Top: WORD index 2 read GetBounds write SetBounds; + property GridWidth: WORD index 3 read GetBounds write SetBounds; + property GridHeight: WORD index 4 read GetBounds write SetBounds; + property CharWidth: BYTE index 1 read GetCharWidthHeight write SetCharWidthHeight; + property CharHeight: BYTE index 2 read GetCharWidthHeight write SetCharWidthHeight; + property ForegroundColorIndex: BYTE index 1 read GetColorIndex write SetColorIndex; + property ForegroundColor: TColor read GetForegroundColor; + property BackgroundColorIndex: BYTE index 2 read GetColorIndex write SetColorIndex; + property BackgroundColor: TColor read GetBackgroundColor; + property Text: TStrings read FText write FText; + end; + +//////////////////////////////////////////////////////////////////////////////// +// +// TGIFCommentExtension +// +//////////////////////////////////////////////////////////////////////////////// + TGIFCommentExtension = class(TGIFExtension) + private + FText : TStrings; + protected + function GetExtensionType: TGIFExtensionType; override; + public + constructor Create(ASubImage: TGIFSubImage); override; + destructor Destroy; override; + procedure SaveToStream(Stream: TStream); override; + procedure LoadFromStream(Stream: TStream); override; + property Text: TStrings read FText; + end; + +//////////////////////////////////////////////////////////////////////////////// +// +// TGIFApplicationExtension +// +//////////////////////////////////////////////////////////////////////////////// + TGIFIdentifierCode = array[0..7] of char; + TGIFAuthenticationCode = array[0..2] of char; + TGIFApplicationRec = packed record + Identifier : TGIFIdentifierCode; + Authentication : TGIFAuthenticationCode; + end; + + TGIFApplicationExtension = class; + TGIFAppExtensionClass = class of TGIFApplicationExtension; + + TGIFApplicationExtension = class(TGIFExtension) + private + FIdent : TGIFApplicationRec; + function GetAuthentication: string; + function GetIdentifier: string; + protected + function GetExtensionType: TGIFExtensionType; override; + procedure SetAuthentication(const Value: string); + procedure SetIdentifier(const Value: string); + procedure SaveData(Stream: TStream); virtual; abstract; + procedure LoadData(Stream: TStream); virtual; abstract; + public + constructor Create(ASubImage: TGIFSubImage); override; + destructor Destroy; override; + procedure SaveToStream(Stream: TStream); override; + procedure LoadFromStream(Stream: TStream); override; + class procedure RegisterExtension(eIdent: TGIFApplicationRec; eClass: TGIFAppExtensionClass); + class function FindSubExtension(Stream: TStream): TGIFExtensionClass; override; + property Identifier: string read GetIdentifier write SetIdentifier; + property Authentication: string read GetAuthentication write SetAuthentication; + end; + +//////////////////////////////////////////////////////////////////////////////// +// +// TGIFUnknownAppExtension +// +//////////////////////////////////////////////////////////////////////////////// + TGIFBlock = class(TObject) + private + FSize : BYTE; + FData : pointer; + public + constructor Create(ASize: integer); + destructor Destroy; override; + procedure SaveToStream(Stream: TStream); + procedure LoadFromStream(Stream: TStream); + property Size: BYTE read FSize; + property Data: pointer read FData; + end; + + TGIFUnknownAppExtension = class(TGIFApplicationExtension) + private + FBlocks : TList; + protected + procedure SaveData(Stream: TStream); override; + procedure LoadData(Stream: TStream); override; + public + constructor Create(ASubImage: TGIFSubImage); override; + destructor Destroy; override; + property Blocks: TList read FBlocks; + end; + +//////////////////////////////////////////////////////////////////////////////// +// +// TGIFAppExtNSLoop +// +//////////////////////////////////////////////////////////////////////////////// + TGIFAppExtNSLoop = class(TGIFApplicationExtension) + private + FLoops : WORD; + FBufferSize : DWORD; + protected + procedure SaveData(Stream: TStream); override; + procedure LoadData(Stream: TStream); override; + public + constructor Create(ASubImage: TGIFSubImage); override; + property Loops: WORD read FLoops write FLoops; + property BufferSize: DWORD read FBufferSize write FBufferSize; + end; + +//////////////////////////////////////////////////////////////////////////////// +// +// TGIFImage +// +//////////////////////////////////////////////////////////////////////////////// + TGIFImageList = class(TGIFList) + protected + function GetImage(Index: Integer): TGIFSubImage; + procedure SetImage(Index: Integer; SubImage: TGIFSubImage); + public + procedure LoadFromStream(Stream: TStream; Parent: TObject); override; + procedure SaveToStream(Stream: TStream); override; + property SubImages[Index: Integer]: TGIFSubImage read GetImage write SetImage; default; + end; + + // Compression algorithms + TGIFCompression = + (gcLZW, // Normal LZW compression + gcRLE // GIF compatible RLE compression + ); + + // Color reduction methods + TColorReduction = + (rmNone, // Do not perform color reduction + rmWindows20, // Reduce to the Windows 20 color system palette + rmWindows256, // Reduce to the Windows 256 color halftone palette (Only works in 256 color display mode) + rmWindowsGray, // Reduce to the Windows 4 grayscale colors + rmMonochrome, // Reduce to a black/white monochrome palette + rmGrayScale, // Reduce to a uniform 256 shade grayscale palette + rmNetscape, // Reduce to the Netscape 216 color palette + rmQuantize, // Reduce to optimal 2^n color palette + rmQuantizeWindows, // Reduce to optimal 256 color windows palette + rmPalette // Reduce to custom palette + ); + TDitherMode = + (dmNearest, // Nearest color matching w/o error correction + dmFloydSteinberg, // Floyd Steinberg Error Diffusion dithering + dmStucki, // Stucki Error Diffusion dithering + dmSierra, // Sierra Error Diffusion dithering + dmJaJuNI, // Jarvis, Judice & Ninke Error Diffusion dithering + dmSteveArche, // Stevenson & Arche Error Diffusion dithering + dmBurkes // Burkes Error Diffusion dithering + // dmOrdered, // Ordered dither + ); + + // Optimization options + TGIFOptimizeOption = + (ooCrop, // Crop animated GIF frames + ooMerge, // Merge pixels of same color + ooCleanup, // Remove comments and application extensions + ooColorMap, // Sort color map by usage and remove unused entries + ooReduceColors // Reduce color depth ***NOT IMPLEMENTED*** + ); + TGIFOptimizeOptions = set of TGIFOptimizeOption; + + TGIFDrawOption = + (goAsync, // Asyncronous draws (paint in thread) + goTransparent, // Transparent draws + goAnimate, // Animate draws + goLoop, // Loop animations + goLoopContinously, // Ignore loop count and loop forever + goValidateCanvas, // Validate canvas in threaded paint ***NOT IMPLEMENTED*** + goDirectDraw, // Draw() directly on canvas + goClearOnLoop, // Clear animation on loop + goTile, // Tiled display + goDither, // Dither to Netscape palette + goAutoDither // Only dither on 256 color systems + ); + TGIFDrawOptions = set of TGIFDrawOption; + // Note: if goAsync is not set then goDirectDraw should be set. Otherwise + // the image will not be displayed. + + PGIFPainter = ^TGIFPainter; + + TGIFPainter = class(TThread) + private + FImage : TGIFImage; // The TGIFImage that owns this painter + FCanvas : TCanvas; // Destination canvas + FRect : TRect; // Destination rect + FDrawOptions : TGIFDrawOptions;// Paint options + FAnimationSpeed : integer; // Animation speed % + FActiveImage : integer; // Current frame + Disposal , // Used by synchronized paint + OldDisposal : TDisposalMethod;// Used by synchronized paint + BackupBuffer : TBitmap; // Used by synchronized paint + FrameBuffer : TBitmap; // Used by synchronized paint + Background : TBitmap; // Used by synchronized paint + ValidateDC : HDC; + DoRestart : boolean; // Flag used to restart animation + FStarted : boolean; // Flag used to signal start of paint + PainterRef : PGIFPainter; // Pointer to var referencing painter + FEventHandle : THandle; // Animation delay event + ExceptObject : Exception; // Eaten exception + ExceptAddress : pointer; // Eaten exceptions address + FEvent : TNotifyEvent; // Used by synchronized events + FOnStartPaint : TNotifyEvent; + FOnPaint : TNotifyEvent; + FOnAfterPaint : TNotifyEvent; + FOnLoop : TNotifyEvent; + FOnEndPaint : TNotifyEvent; + procedure DoOnTerminate(Sender: TObject);// Sync. shutdown procedure + procedure DoSynchronize(Method: TThreadMethod);// Conditional sync stub +{$ifdef SERIALIZE_RENDER} + procedure PrefetchBitmap; // Sync. bitmap prefetch +{$endif} + procedure DoPaintFrame; // Sync. buffered paint procedure + procedure DoPaint; // Sync. paint procedure + procedure DoEvent; + procedure SetActiveImage(const Value: integer);// Sync. event procedure + protected + procedure Execute; override; + procedure SetAnimationSpeed(Value: integer); + public + constructor Create(AImage: TGIFImage; ACanvas: TCanvas; ARect: TRect; + Options: TGIFDrawOptions); + constructor CreateRef(Painter: PGIFPainter; AImage: TGIFImage; ACanvas: TCanvas; ARect: TRect; + Options: TGIFDrawOptions); + destructor Destroy; override; + procedure Start; + procedure Stop; + procedure Restart; + property Image: TGIFImage read FImage; + property Canvas: TCanvas read FCanvas; + property Rect: TRect read FRect write FRect; + property DrawOptions: TGIFDrawOptions read FDrawOptions write FDrawOptions; + property AnimationSpeed: integer read FAnimationSpeed write SetAnimationSpeed; + property Started: boolean read FStarted; + property ActiveImage: integer read FActiveImage write SetActiveImage; + property OnStartPaint: TNotifyEvent read FOnStartPaint write FOnStartPaint; + property OnPaint: TNotifyEvent read FOnPaint write FOnPaint; + property OnAfterPaint: TNotifyEvent read FOnAfterPaint write FOnAfterPaint; + property OnLoop: TNotifyEvent read FOnLoop write FOnLoop; + property OnEndPaint : TNotifyEvent read FOnEndPaint write FOnEndPaint ; + property EventHandle: THandle read FEventHandle; + end; + + TGIFWarning = procedure(Sender: TObject; Severity: TGIFSeverity; Message: string) of object; + + TGIFImage = class(TGraphic) + private + IsDrawing : Boolean; + IsInsideGetPalette : boolean; + FImages : TGIFImageList; + FHeader : TGIFHeader; + FGlobalPalette : HPalette; + FPainters : TThreadList; + FDrawOptions : TGIFDrawOptions; + FColorReduction : TColorReduction; + FReductionBits : integer; + FDitherMode : TDitherMode; + FCompression : TGIFCompression; + FOnWarning : TGIFWarning; + FBitmap : TBitmap; + FDrawPainter : TGIFPainter; + FThreadPriority : TThreadPriority; + FAnimationSpeed : integer; + FDrawBackgroundColor: TColor; + FOnStartPaint : TNotifyEvent; + FOnPaint : TNotifyEvent; + FOnAfterPaint : TNotifyEvent; + FOnLoop : TNotifyEvent; + FOnEndPaint : TNotifyEvent; +{$IFDEF VER9x} + FPaletteModified : Boolean; + FOnProgress : TProgressEvent; +{$ENDIF} + function GetAnimate: Boolean; // 2002.07.07 + procedure SetAnimate(const Value: Boolean); // 2002.07.07 + protected + // Obsolete: procedure Changed(Sender: TObject); {$IFDEF VER9x} virtual; {$ELSE} override; {$ENDIF} + function GetHeight: Integer; override; + procedure SetHeight(Value: Integer); override; + function GetWidth: Integer; override; + procedure SetWidth(Value: Integer); override; + procedure AssignTo(Dest: TPersistent); override; + function InternalPaint(Painter: PGIFPainter; ACanvas: TCanvas; const Rect: TRect; Options: TGIFDrawOptions): TGIFPainter; + procedure Draw(ACanvas: TCanvas; const Rect: TRect); override; + function Equals(Graphic: TGraphic): Boolean; override; + function GetPalette: HPALETTE; {$IFDEF VER9x} virtual; {$ELSE} override; {$ENDIF} + procedure SetPalette(Value: HPalette); {$IFDEF VER9x} virtual; {$ELSE} override; {$ENDIF} + function GetEmpty: Boolean; override; + procedure WriteData(Stream: TStream); override; + function GetIsTransparent: Boolean; + function GetVersion: TGIFVersion; + function GetColorResolution: integer; + function GetBitsPerPixel: integer; + function GetBackgroundColorIndex: BYTE; + procedure SetBackgroundColorIndex(const Value: BYTE); + function GetBackgroundColor: TColor; + procedure SetBackgroundColor(const Value: TColor); + function GetAspectRatio: BYTE; + procedure SetAspectRatio(const Value: BYTE); + procedure SetDrawOptions(Value: TGIFDrawOptions); + procedure SetAnimationSpeed(Value: integer); + procedure SetReductionBits(Value: integer); + procedure NewImage; + function GetBitmap: TBitmap; + function NewBitmap: TBitmap; + procedure FreeBitmap; + function GetColorMap: TGIFColorMap; + function GetDoDither: boolean; + property DrawPainter: TGIFPainter read FDrawPainter; // Extremely volatile + property DoDither: boolean read GetDoDither; +{$IFDEF VER9x} + procedure Progress(Sender: TObject; Stage: TProgressStage; + PercentDone: Byte; RedrawNow: Boolean; const R: TRect; const Msg: string); dynamic; +{$ENDIF} + public + constructor Create; override; + destructor Destroy; override; + procedure SaveToStream(Stream: TStream); override; + procedure LoadFromStream(Stream: TStream); override; + procedure LoadFromResourceName(Instance: THandle; const ResName: String); // 2002.07.07 + function Add(Source: TPersistent): integer; + procedure Pack; + procedure OptimizeColorMap; + procedure Optimize(Options: TGIFOptimizeOptions; + ColorReduction: TColorReduction; DitherMode: TDitherMode; + ReductionBits: integer); + procedure Clear; + procedure StopDraw; + function Paint(ACanvas: TCanvas; const Rect: TRect; Options: TGIFDrawOptions): TGIFPainter; + procedure PaintStart; + procedure PaintPause; + procedure PaintStop; + procedure PaintResume; + procedure PaintRestart; + procedure Warning(Sender: TObject; Severity: TGIFSeverity; Message: string); virtual; + procedure Assign(Source: TPersistent); override; + procedure LoadFromClipboardFormat(AFormat: Word; AData: THandle; + APalette: HPALETTE); override; + procedure SaveToClipboardFormat(var AFormat: Word; var AData: THandle; + var APalette: HPALETTE); override; + property GlobalColorMap: TGIFColorMap read GetColorMap; + property Version: TGIFVersion read GetVersion; + property Images: TGIFImageList read FImages; + property ColorResolution: integer read GetColorResolution; + property BitsPerPixel: integer read GetBitsPerPixel; + property BackgroundColorIndex: BYTE read GetBackgroundColorIndex write SetBackgroundColorIndex; + property BackgroundColor: TColor read GetBackgroundColor write SetBackgroundColor; + property AspectRatio: BYTE read GetAspectRatio write SetAspectRatio; + property Header: TGIFHeader read FHeader; // ***OBSOLETE*** + property IsTransparent: boolean read GetIsTransparent; + property DrawOptions: TGIFDrawOptions read FDrawOptions write SetDrawOptions; + property DrawBackgroundColor: TColor read FDrawBackgroundColor write FDrawBackgroundColor; + property ColorReduction: TColorReduction read FColorReduction write FColorReduction; + property ReductionBits: integer read FReductionBits write SetReductionBits; + property DitherMode: TDitherMode read FDitherMode write FDitherMode; + property Compression: TGIFCompression read FCompression write FCompression; + property AnimationSpeed: integer read FAnimationSpeed write SetAnimationSpeed; + property Animate: Boolean read GetAnimate write SetAnimate; // 2002.07.07 + property Painters: TThreadList read FPainters; + property ThreadPriority: TThreadPriority read FThreadPriority write FThreadPriority; + property Bitmap: TBitmap read GetBitmap; // Volatile - beware! + property OnWarning: TGIFWarning read FOnWarning write FOnWarning; + property OnStartPaint: TNotifyEvent read FOnStartPaint write FOnStartPaint; + property OnPaint: TNotifyEvent read FOnPaint write FOnPaint; + property OnAfterPaint: TNotifyEvent read FOnAfterPaint write FOnAfterPaint; + property OnLoop: TNotifyEvent read FOnLoop write FOnLoop; + property OnEndPaint : TNotifyEvent read FOnEndPaint write FOnEndPaint ; +{$IFDEF VER9x} + property Palette: HPALETTE read GetPalette write SetPalette; + property PaletteModified: Boolean read FPaletteModified write FPaletteModified; + property OnProgress: TProgressEvent read FOnProgress write FOnProgress; +{$ENDIF} + end; + +//////////////////////////////////////////////////////////////////////////////// +// +// Utility routines +// +//////////////////////////////////////////////////////////////////////////////// + // WebPalette creates a 216 color uniform palette a.k.a. the Netscape Palette + function WebPalette: HPalette; + + // ReduceColors + // Map colors in a bitmap to their nearest representation in a palette using + // the methods specified by the ColorReduction and DitherMode parameters. + // The ReductionBits parameter specifies the desired number of colors (bits + // per pixel) when the reduction method is rmQuantize. The CustomPalette + // specifies the palette when the rmPalette reduction method is used. + function ReduceColors(Bitmap: TBitmap; ColorReduction: TColorReduction; + DitherMode: TDitherMode; ReductionBits: integer; CustomPalette: hPalette): TBitmap; + + // CreateOptimizedPaletteFromManyBitmaps + //: Performs Color Quantization on multiple bitmaps. + // The Bitmaps parameter is a list of bitmaps. Returns an optimized palette. + function CreateOptimizedPaletteFromManyBitmaps(Bitmaps: TList; Colors, ColorBits: integer; + Windows: boolean): hPalette; + +{$IFDEF VER9x} + // From Delphi 3 graphics.pas +type + TPixelFormat = (pfDevice, pf1bit, pf4bit, pf8bit, pf15bit, pf16bit, pf24bit, pf32bit, pfCustom); +{$ENDIF} + + procedure InternalGetDIBSizes(Bitmap: HBITMAP; var InfoHeaderSize: Integer; + var ImageSize: longInt; PixelFormat: TPixelFormat); + function InternalGetDIB(Bitmap: HBITMAP; Palette: HPALETTE; + var BitmapInfo; var Bits; PixelFormat: TPixelFormat): Boolean; + +//////////////////////////////////////////////////////////////////////////////// +// +// Global variables +// +//////////////////////////////////////////////////////////////////////////////// +// GIF Clipboard format identifier for use by LoadFromClipboardFormat and +// SaveToClipboardFormat. +// Set in Initialization section. +var + CF_GIF: WORD; + +//////////////////////////////////////////////////////////////////////////////// +// +// Library defaults +// +//////////////////////////////////////////////////////////////////////////////// +var + //: Default options for TGIFImage.DrawOptions. + GIFImageDefaultDrawOptions : TGIFDrawOptions = + [goAsync, goLoop, goTransparent, goAnimate, goDither, goAutoDither +{$IFDEF STRICT_MOZILLA} + ,goClearOnLoop +{$ENDIF} + ]; + + // WARNING! Do not use goAsync and goDirectDraw unless you have absolute + // control of the destination canvas. + // TGIFPainter will continue to write on the canvas even after the canvas has + // been deleted, unless *you* prevent it. + // The goValidateCanvas option will fix this problem if it is ever implemented. + + //: Default color reduction methods for bitmap import. + // These are the fastest settings, but also the ones that gives the + // worst result (in most cases). + GIFImageDefaultColorReduction: TColorReduction = rmNetscape; + GIFImageDefaultColorReductionBits: integer = 8; // Range 3 - 8 + GIFImageDefaultDitherMode: TDitherMode = dmNearest; + + //: Default encoder compression method. + GIFImageDefaultCompression: TGIFCompression = gcLZW; + + //: Default painter thread priority + GIFImageDefaultThreadPriority: TThreadPriority = tpNormal; + + //: Default animation speed in % of normal speed (range 0 - 1000) + GIFImageDefaultAnimationSpeed: integer = 100; + + // DoAutoDither is set to True in the initializaion section if the desktop DC + // supports 256 colors or less. + // It can be modified in your application to disable/enable Auto Dithering + DoAutoDither: boolean = False; + + // Palette is set to True in the initialization section if the desktop DC + // supports 256 colors or less. + // You should NOT modify it. + PaletteDevice: boolean = False; + + // Set GIFImageRenderOnLoad to True to render (convert to bitmap) the + // GIF frames as they are loaded instead of rendering them on-demand. + // This might increase resource consumption and will increase load time, + // but will cause animated GIFs to display more smoothly. + GIFImageRenderOnLoad: boolean = False; + + // If GIFImageOptimizeOnStream is true, the GIF will be optimized + // before it is streamed to the DFM file. + // This will not affect TGIFImage.SaveToStream or SaveToFile. + GIFImageOptimizeOnStream: boolean = False; + +//////////////////////////////////////////////////////////////////////////////// +// +// Design Time support +// +//////////////////////////////////////////////////////////////////////////////// +// Dummy component registration for design time support of GIFs in TImage +procedure Register; + +//////////////////////////////////////////////////////////////////////////////// +// +// Error messages +// +//////////////////////////////////////////////////////////////////////////////// +{$ifndef VER9x} +resourcestring +{$else} +const +{$endif} + // GIF Error messages + sOutOfData = 'Premature end of data'; + sTooManyColors = 'Color table overflow'; + sBadColorIndex = 'Invalid color index'; + sBadVersion = 'Unsupported GIF version'; + sBadSignature = 'Invalid GIF signature'; + sScreenBadColorSize = 'Invalid number of colors specified in Screen Descriptor'; + sImageBadColorSize = 'Invalid number of colors specified in Image Descriptor'; + sUnknownExtension = 'Unknown extension type'; + sBadExtensionLabel = 'Invalid extension introducer'; + sOutOfMemDIB = 'Failed to allocate memory for GIF DIB'; + sDIBCreate = 'Failed to create DIB from Bitmap'; + sDecodeTooFewBits = 'Decoder bit buffer under-run'; + sDecodeCircular = 'Circular decoder table entry'; + sBadTrailer = 'Invalid Image trailer'; + sBadExtensionInstance = 'Internal error: Extension Instance does not match Extension Label'; + sBadBlockSize = 'Unsupported Application Extension block size'; + sBadBlock = 'Unknown GIF block type'; + sUnsupportedClass = 'Object type not supported for operation'; + sInvalidData = 'Invalid GIF data'; + sBadHeight = 'Image height too small for contained frames'; + sBadWidth = 'Image width too small for contained frames'; +{$IFNDEF REGISTER_TGIFIMAGE} + sGIFToClipboard = 'Clipboard operations not supported for GIF objects'; +{$ELSE} + sFailedPaste = 'Failed to store GIF on clipboard'; +{$IFDEF VER9x} + sUnknownClipboardFormat= 'Unsupported clipboard format'; +{$ENDIF} +{$ENDIF} + sScreenSizeExceeded = 'Image exceeds Logical Screen size'; + sNoColorTable = 'No global or local color table defined'; + sBadPixelCoordinates = 'Invalid pixel coordinates'; + sUnsupportedBitmap = 'Unsupported bitmap format'; + sInvalidPixelFormat = 'Unsupported PixelFormat'; + sBadDimension = 'Invalid image dimensions'; + sNoDIB = 'Image has no DIB'; + sInvalidStream = 'Invalid stream operation'; + sInvalidColor = 'Color not in color table'; + sInvalidBitSize = 'Invalid Bits Per Pixel value'; + sEmptyColorMap = 'Color table is empty'; + sEmptyImage = 'Image is empty'; + sInvalidBitmapList = 'Invalid bitmap list'; + sInvalidReduction = 'Invalid reduction method'; +{$IFDEF VER9x} + // From Delphi 3 consts.pas + SOutOfResources = 'Out of system resources'; + SInvalidBitmap = 'Bitmap image is not valid'; + SScanLine = 'Scan line index out of range'; +{$ENDIF} + +//////////////////////////////////////////////////////////////////////////////// +// +// Misc texts +// +//////////////////////////////////////////////////////////////////////////////// + // File filter name + sGIFImageFile = 'GIF Image'; + + // Progress messages + sProgressLoading = 'Loading...'; + sProgressSaving = 'Saving...'; + sProgressConverting = 'Converting...'; + sProgressRendering = 'Rendering...'; + sProgressCopying = 'Copying...'; + sProgressOptimizing = 'Optimizing...'; + + +//////////////////////////////////////////////////////////////////////////////// +//////////////////////////////////////////////////////////////////////////////// +// +// Implementation +// +//////////////////////////////////////////////////////////////////////////////// +//////////////////////////////////////////////////////////////////////////////// +implementation + +{ This makes me long for the C preprocessor... } +{$ifdef DEBUG} + {$ifdef DEBUG_COMPRESSPERFORMANCE} + {$define DEBUG_PERFORMANCE} + {$else} + {$ifdef DEBUG_DECOMPRESSPERFORMANCE} + {$define DEBUG_PERFORMANCE} + {$else} + {$ifdef DEBUG_DITHERPERFORMANCE} + {$define DEBUG_PERFORMANCE} + {$else} + {$ifdef DEBUG_DITHERPERFORMANCE} + {$define DEBUG_PERFORMANCE} + {$else} + {$ifdef DEBUG_DRAWPERFORMANCE} + {$define DEBUG_PERFORMANCE} + {$else} + {$ifdef DEBUG_RENDERPERFORMANCE} + {$define DEBUG_PERFORMANCE} + {$endif} + {$endif} + {$endif} + {$endif} + {$endif} + {$endif} +{$endif} + +uses +{$ifdef DEBUG} + dialogs, +{$endif} + mmsystem, // timeGetTime() + messages, + Consts; + + +//////////////////////////////////////////////////////////////////////////////// +// +// Misc consts +// +//////////////////////////////////////////////////////////////////////////////// +const + { Extension/block label values } + bsPlainTextExtension = $01; + bsGraphicControlExtension = $F9; + bsCommentExtension = $FE; + bsApplicationExtension = $FF; + + bsImageDescriptor = Ord(','); + bsExtensionIntroducer = Ord('!'); + bsTrailer = ord(';'); + + // Thread messages - Used by TThread.Synchronize() + CM_DESTROYWINDOW = $8FFE; // Defined in classes.pas + CM_EXECPROC = $8FFF; // Defined in classes.pas + + +//////////////////////////////////////////////////////////////////////////////// +// +// Design Time support +// +//////////////////////////////////////////////////////////////////////////////// +//: Dummy component registration to add design-time support of GIFs to TImage. +// Since TGIFImage isn't a component there's nothing to register here, but +// since Register is only called at design time we can set the design time +// GIF paint options here (modify as you please): +procedure Register; +begin + // Don't loop animations at design-time. Animated GIFs will animate once and + // then stop thus not using CPU resources and distracting the developer. + Exclude(GIFImageDefaultDrawOptions, goLoop); +end; + +//////////////////////////////////////////////////////////////////////////////// +// +// Utilities +// +//////////////////////////////////////////////////////////////////////////////// +//: Creates a 216 color uniform non-dithering Netscape palette. +function WebPalette: HPalette; +type + TLogWebPalette = packed record + palVersion : word; + palNumEntries : word; + PalEntries : array[0..5,0..5,0..5] of TPaletteEntry; + end; +var + r, g, b : byte; + LogWebPalette : TLogWebPalette; + LogPalette : TLogpalette absolute LogWebPalette; // Stupid typecast +begin + with LogWebPalette do + begin + palVersion:= $0300; + palNumEntries:= 216; + for r:=0 to 5 do + for g:=0 to 5 do + for b:=0 to 5 do + begin + with PalEntries[r,g,b] do + begin + peRed := 51 * r; + peGreen := 51 * g; + peBlue := 51 * b; + peFlags := 0; + end; + end; + end; + Result := CreatePalette(Logpalette); +end; + +(* +** GDI Error handling +** Adapted from graphics.pas +*) +{$IFOPT R+} + {$DEFINE R_PLUS} + {$RANGECHECKS OFF} +{$ENDIF} +{$ifdef D3_BCB3} +function GDICheck(Value: Integer): Integer; +{$else} +function GDICheck(Value: Cardinal): Cardinal; +{$endif} +var + ErrorCode : integer; + Buf : array [byte] of char; + + function ReturnAddr: Pointer; + // From classes.pas + asm + MOV EAX,[EBP+4] // sysutils.pas says [EBP-4], but this works ! + end; + +begin + if (Value = 0) then + begin + ErrorCode := GetLastError; + if (ErrorCode <> 0) and (FormatMessage(FORMAT_MESSAGE_FROM_SYSTEM, nil, + ErrorCode, LOCALE_USER_DEFAULT, Buf, sizeof(Buf), nil) <> 0) then + raise EOutOfResources.Create(Buf) at ReturnAddr + else + raise EOutOfResources.Create(SOutOfResources) at ReturnAddr; + end; + Result := Value; +end; +{$IFDEF R_PLUS} + {$RANGECHECKS ON} + {$UNDEF R_PLUS} +{$ENDIF} + +(* +** Raise error condition +*) +procedure Error(msg: string); + function ReturnAddr: Pointer; + // From classes.pas + asm + MOV EAX,[EBP+4] // sysutils.pas says [EBP-4] ! + end; +begin + raise GIFException.Create(msg) at ReturnAddr; +end; + +(* +** Return number bytes required to +** hold a given number of bits. +*) +function ByteAlignBit(Bits: Cardinal): Cardinal; +begin + Result := (Bits+7) SHR 3; +end; +// Rounded up to nearest 2 +function WordAlignBit(Bits: Cardinal): Cardinal; +begin + Result := ((Bits+15) SHR 4) SHL 1; +end; +// Rounded up to nearest 4 +function DWordAlignBit(Bits: Cardinal): Cardinal; +begin + Result := ((Bits+31) SHR 5) SHL 2; +end; +// Round to arbitrary number of bits +function AlignBit(Bits, BitsPerPixel, Alignment: Cardinal): Cardinal; +begin + Dec(Alignment); + Result := ((Bits * BitsPerPixel) + Alignment) and not Alignment; + Result := Result SHR 3; +end; + +(* +** Compute Bits per Pixel from Number of Colors +** (Return the ceiling log of n) +*) +function Colors2bpp(Colors: integer): integer; +var + MaxColor : integer; +begin + (* + ** This might be faster computed by multiple if then else statements + *) + + if (Colors = 0) then + Result := 0 + else + begin + Result := 1; + MaxColor := 2; + while (Colors > MaxColor) do + begin + inc(Result); + MaxColor := MaxColor SHL 1; + end; + end; +end; + +(* +** Write an ordinal byte value to a stream +*) +procedure WriteByte(Stream: TStream; b: BYTE); +begin + Stream.Write(b, 1); +end; + +(* +** Read an ordinal byte value from a stream +*) +function ReadByte(Stream: TStream): BYTE; +begin + Stream.Read(Result, 1); +end; + +(* +** Read data from stream and raise exception of EOF +*) +procedure ReadCheck(Stream: TStream; var Buffer; Size: LongInt); +var + ReadSize : integer; +begin + ReadSize := Stream.Read(Buffer, Size); + if (ReadSize <> Size) then + Error(sOutOfData); +end; + +(* +** Write a string list to a stream as multiple blocks +** of max 255 characters in each. +*) +procedure WriteStrings(Stream: TStream; Text: TStrings); +var + i : integer; + b : BYTE; + size : integer; + s : string; +begin + for i := 0 to Text.Count-1 do + begin + s := Text[i]; + size := length(s); + if (size > 255) then + b := 255 + else + b := size; + while (size > 0) do + begin + dec(size, b); + WriteByte(Stream, b); + Stream.Write(PChar(s)^, b); + delete(s, 1, b); + if (b > size) then + b := size; + end; + end; + // Terminating zero (length = 0) + WriteByte(Stream, 0); +end; + + +(* +** Read a string list from a stream as multiple blocks +** of max 255 characters in each. +*) +{ TODO -oanme -cImprovement : Replace ReadStrings with TGIFReader. } +procedure ReadStrings(Stream: TStream; Text: TStrings); +var + size : BYTE; + buf : array[0..255] of char; +begin + Text.Clear; + if (Stream.Read(size, 1) <> 1) then + exit; + while (size > 0) do + begin + ReadCheck(Stream, buf, size); + buf[size] := #0; + Text.Add(Buf); + if (Stream.Read(size, 1) <> 1) then + exit; + end; +end; + + +//////////////////////////////////////////////////////////////////////////////// +// +// Delphi 2.x / C++ Builder 1.x support +// +//////////////////////////////////////////////////////////////////////////////// +{$IFDEF VER9x} +var + // From Delphi 3 graphics.pas + SystemPalette16: HPalette; // 16 color palette that maps to the system palette + +type + TPixelFormats = set of TPixelFormat; + +const + // Only pf1bit, pf4bit and pf8bit is supported since they are the only ones + // with palettes + SupportedPixelformats: TPixelFormats = [pf1bit, pf4bit, pf8bit]; +{$ENDIF} + + +// -------------------------- +// InitializeBitmapInfoHeader +// -------------------------- +// Fills a TBitmapInfoHeader with the values of a bitmap when converted to a +// DIB of a specified PixelFormat. +// +// Parameters: +// Bitmap The handle of the source bitmap. +// Info The TBitmapInfoHeader buffer that will receive the values. +// PixelFormat The pixel format of the destination DIB. +// +{$IFDEF BAD_STACK_ALIGNMENT} + // Disable optimization to circumvent optimizer bug... + {$IFOPT O+} + {$DEFINE O_PLUS} + {$O-} + {$ENDIF} +{$ENDIF} +procedure InitializeBitmapInfoHeader(Bitmap: HBITMAP; var Info: TBitmapInfoHeader; + PixelFormat: TPixelFormat); +// From graphics.pas, "optimized" for our use +var + DIB : TDIBSection; + Bytes : Integer; +begin + DIB.dsbmih.biSize := 0; + Bytes := GetObject(Bitmap, SizeOf(DIB), @DIB); + if (Bytes = 0) then + Error(sInvalidBitmap); + + if (Bytes >= (sizeof(DIB.dsbm) + sizeof(DIB.dsbmih))) and + (DIB.dsbmih.biSize >= sizeof(DIB.dsbmih)) then + Info := DIB.dsbmih + else + begin + FillChar(Info, sizeof(Info), 0); + with Info, DIB.dsbm do + begin + biSize := SizeOf(Info); + biWidth := bmWidth; + biHeight := bmHeight; + end; + end; + case PixelFormat of + pf1bit: Info.biBitCount := 1; + pf4bit: Info.biBitCount := 4; + pf8bit: Info.biBitCount := 8; + pf24bit: Info.biBitCount := 24; + else + Error(sInvalidPixelFormat); + // Info.biBitCount := DIB.dsbm.bmBitsPixel * DIB.dsbm.bmPlanes; + end; + Info.biPlanes := 1; + Info.biCompression := BI_RGB; // Always return data in RGB format + Info.biSizeImage := AlignBit(Info.biWidth, Info.biBitCount, 32) * Cardinal(abs(Info.biHeight)); +end; +{$IFDEF O_PLUS} + {$O+} + {$UNDEF O_PLUS} +{$ENDIF} + +// ------------------- +// InternalGetDIBSizes +// ------------------- +// Calculates the buffer sizes nescessary for convertion of a bitmap to a DIB +// of a specified PixelFormat. +// See the GetDIBSizes API function for more info. +// +// Parameters: +// Bitmap The handle of the source bitmap. +// InfoHeaderSize +// The returned size of a buffer that will receive the DIB's +// TBitmapInfo structure. +// ImageSize The returned size of a buffer that will receive the DIB's +// pixel data. +// PixelFormat The pixel format of the destination DIB. +// +procedure InternalGetDIBSizes(Bitmap: HBITMAP; var InfoHeaderSize: Integer; + var ImageSize: longInt; PixelFormat: TPixelFormat); +// From graphics.pas, "optimized" for our use +var + Info : TBitmapInfoHeader; +begin + InitializeBitmapInfoHeader(Bitmap, Info, PixelFormat); + // Check for palette device format + if (Info.biBitCount > 8) then + begin + // Header but no palette + InfoHeaderSize := SizeOf(TBitmapInfoHeader); + if ((Info.biCompression and BI_BITFIELDS) <> 0) then + Inc(InfoHeaderSize, 12); + end else + // Header and palette + InfoHeaderSize := SizeOf(TBitmapInfoHeader) + SizeOf(TRGBQuad) * (1 shl Info.biBitCount); + ImageSize := Info.biSizeImage; +end; + +// -------------- +// InternalGetDIB +// -------------- +// Converts a bitmap to a DIB of a specified PixelFormat. +// +// Parameters: +// Bitmap The handle of the source bitmap. +// Pal The handle of the source palette. +// BitmapInfo The buffer that will receive the DIB's TBitmapInfo structure. +// A buffer of sufficient size must have been allocated prior to +// calling this function. +// Bits The buffer that will receive the DIB's pixel data. +// A buffer of sufficient size must have been allocated prior to +// calling this function. +// PixelFormat The pixel format of the destination DIB. +// +// Returns: +// True on success, False on failure. +// +// Note: The InternalGetDIBSizes function can be used to calculate the +// nescessary sizes of the BitmapInfo and Bits buffers. +// +function InternalGetDIB(Bitmap: HBITMAP; Palette: HPALETTE; + var BitmapInfo; var Bits; PixelFormat: TPixelFormat): Boolean; +// From graphics.pas, "optimized" for our use +var + OldPal : HPALETTE; + DC : HDC; +begin + InitializeBitmapInfoHeader(Bitmap, TBitmapInfoHeader(BitmapInfo), PixelFormat); + OldPal := 0; + DC := CreateCompatibleDC(0); + try + if (Palette <> 0) then + begin + OldPal := SelectPalette(DC, Palette, False); + RealizePalette(DC); + end; + Result := (GetDIBits(DC, Bitmap, 0, abs(TBitmapInfoHeader(BitmapInfo).biHeight), + @Bits, TBitmapInfo(BitmapInfo), DIB_RGB_COLORS) <> 0); + finally + if (OldPal <> 0) then + SelectPalette(DC, OldPal, False); + DeleteDC(DC); + end; +end; + +// ---------- +// DIBFromBit +// ---------- +// Converts a bitmap to a DIB of a specified PixelFormat. +// The DIB is returned in a TMemoryStream ready for streaming to a BMP file. +// +// Note: As opposed to D2's DIBFromBit function, the returned stream also +// contains a TBitmapFileHeader at offset 0. +// +// Parameters: +// Stream The TMemoryStream used to store the bitmap data. +// The stream must be allocated and freed by the caller prior to +// calling this function. +// Src The handle of the source bitmap. +// Pal The handle of the source palette. +// PixelFormat The pixel format of the destination DIB. +// DIBHeader A pointer to the DIB's TBitmapInfo (or TBitmapInfoHeader) +// structure in the memory stream. +// The size of the structure can either be deduced from the +// pixel format (i.e. number of colors) or calculated by +// subtracting the DIBHeader pointer from the DIBBits pointer. +// DIBBits A pointer to the DIB's pixel data in the memory stream. +// +procedure DIBFromBit(Stream: TMemoryStream; Src: HBITMAP; + Pal: HPALETTE; PixelFormat: TPixelFormat; var DIBHeader, DIBBits: Pointer); +// (From D2 graphics.pas, "optimized" for our use) +var + HeaderSize : integer; + FileSize : longInt; + ImageSize : longInt; + BitmapFileHeader : PBitmapFileHeader; +begin + if (Src = 0) then + Error(sInvalidBitmap); + // Get header- and pixel data size for new pixel format + InternalGetDIBSizes(Src, HeaderSize, ImageSize, PixelFormat); + // Make room in stream for a TBitmapInfo and pixel data + FileSize := sizeof(TBitmapFileHeader) + HeaderSize + ImageSize; + Stream.SetSize(FileSize); + // Get pointer to TBitmapFileHeader + BitmapFileHeader := Stream.Memory; + // Get pointer to TBitmapInfo + DIBHeader := Pointer(Longint(BitmapFileHeader) + sizeof(TBitmapFileHeader)); + // Get pointer to pixel data + DIBBits := Pointer(Longint(DIBHeader) + HeaderSize); + // Initialize file header + FillChar(BitmapFileHeader^, sizeof(TBitmapFileHeader), 0); + with BitmapFileHeader^ do + begin + bfType := $4D42; // 'BM' = Windows BMP signature + bfSize := FileSize; // File size (not needed) + bfOffBits := sizeof(TBitmapFileHeader) + HeaderSize; // Offset of pixel data + end; + // Get pixel data in new pixel format + InternalGetDIB(Src, Pal, DIBHeader^, DIBBits^, PixelFormat); +end; + +// -------------- +// GetPixelFormat +// -------------- +// Returns the current pixel format of a bitmap. +// +// Replacement for delphi 3 TBitmap.PixelFormat getter. +// +// Parameters: +// Bitmap The bitmap which pixel format is returned. +// +// Returns: +// The PixelFormat of the bitmap +// +function GetPixelFormat(Bitmap: TBitmap): TPixelFormat; +{$IFDEF VER9x} +// From graphics.pas, "optimized" for our use +var + DIBSection : TDIBSection; + Bytes : Integer; + Handle : HBitmap; +begin + Result := pfCustom; // This value is never returned + // BAD_STACK_ALIGNMENT + // Note: To work around an optimizer bug, we do not use Bitmap.Handle + // directly. Instead we store the value and use it indirectly. Unless we do + // this, the register containing Bitmap.Handle will be overwritten! + Handle := Bitmap.Handle; + if (Handle <> 0) then + begin + Bytes := GetObject(Handle, SizeOf(DIBSection), @DIBSection); + if (Bytes = 0) then + Error(sInvalidBitmap); + + with (DIBSection) do + begin + // Check for NT bitmap + if (Bytes < (SizeOf(dsbm) + SizeOf(dsbmih))) or (dsbmih.biSize < SizeOf(dsbmih)) then + DIBSection.dsBmih.biBitCount := dsbm.bmBitsPixel * dsbm.bmPlanes; + + case (dsBmih.biBitCount) of + 0: Result := pfDevice; + 1: Result := pf1bit; + 4: Result := pf4bit; + 8: Result := pf8bit; + 16: case (dsBmih.biCompression) of + BI_RGB: + Result := pf15Bit; + BI_BITFIELDS: + if (dsBitFields[1] = $07E0) then + Result := pf16Bit; + end; + 24: Result := pf24Bit; + 32: if (dsBmih.biCompression = BI_RGB) then + Result := pf32Bit; + else + Error(sUnsupportedBitmap); + end; + end; + end else +// Result := pfDevice; + Error(sUnsupportedBitmap); +end; +{$ELSE} +begin + Result := Bitmap.PixelFormat; +end; +{$ENDIF} + +// -------------- +// SetPixelFormat +// -------------- +// Changes the pixel format of a TBitmap. +// +// Replacement for delphi 3 TBitmap.PixelFormat setter. +// The returned TBitmap will always be a DIB. +// +// Note: Under Delphi 3.x this function will leak a palette handle each time it +// converts a TBitmap to pf8bit format! +// If possible, use SafeSetPixelFormat instead to avoid this. +// +// Parameters: +// Bitmap The bitmap to modify. +// PixelFormat The pixel format to convert to. +// +procedure SetPixelFormat(Bitmap: TBitmap; PixelFormat: TPixelFormat); +{$IFDEF VER9x} +var + Stream : TMemoryStream; + Header , + Bits : Pointer; +begin + // Can't change anything without a handle + if (Bitmap.Handle = 0) then + Error(sInvalidBitmap); + + // Only convert to supported formats + if not(PixelFormat in SupportedPixelformats) then + Error(sInvalidPixelFormat); + + // No need to convert to same format + if (GetPixelFormat(Bitmap) = PixelFormat) then + exit; + + Stream := TMemoryStream.Create; + try + // Convert to DIB file in memory stream + DIBFromBit(Stream, Bitmap.Handle, Bitmap.Palette, PixelFormat, Header, Bits); + // Load DIB from stream + Stream.Position := 0; + Bitmap.LoadFromStream(Stream); + finally + Stream.Free; + end; +end; +{$ELSE} +begin + Bitmap.PixelFormat := PixelFormat; +end; +{$ENDIF} + +{$IFDEF VER100} +var + pf8BitBitmap: TBitmap = nil; +{$ENDIF} + +// ------------------ +// SafeSetPixelFormat +// ------------------ +// Changes the pixel format of a TBitmap but doesn't preserve the contents. +// +// Replacement for Delphi 3 TBitmap.PixelFormat setter. +// The returned TBitmap will always be an empty DIB of the same size as the +// original bitmap. +// +// This function is used to avoid the palette handle leak that Delphi 3's +// SetPixelFormat and TBitmap.PixelFormat suffers from. +// +// Parameters: +// Bitmap The bitmap to modify. +// PixelFormat The pixel format to convert to. +// +procedure SafeSetPixelFormat(Bitmap: TBitmap; PixelFormat: TPixelFormat); +{$IFDEF VER9x} +begin + SetPixelFormat(Bitmap, PixelFormat); +end; +{$ELSE} +{$IFNDEF VER100} +var + Palette : hPalette; +begin + Bitmap.PixelFormat := PixelFormat; + + // Work around a bug in TBitmap: + // When converting to pf8bit format, the palette assigned to TBitmap.Palette + // will be a half tone palette (which only contains the 20 system colors). + // Unfortunately this is not the palette used to render the bitmap and it + // is also not the palette saved with the bitmap. + if (PixelFormat = pf8bit) then + begin + // Disassociate the wrong palette from the bitmap (without affecting + // the DIB color table) + Palette := Bitmap.ReleasePalette; + if (Palette <> 0) then + DeleteObject(Palette); + // Recreate the palette from the DIB color table + Bitmap.Palette; + end; +end; +{$ELSE} +var + Width , + Height : integer; +begin + if (PixelFormat = pf8bit) then + begin + // Partial solution to "TBitmap.PixelFormat := pf8bit" leak + // by Greg Chapman + if (pf8BitBitmap = nil) then + begin + // Create a "template" bitmap + // The bitmap is deleted in the finalization section of the unit. + pf8BitBitmap:= TBitmap.Create; + // Convert template to pf8bit format + // This will leak 1 palette handle, but only once + pf8BitBitmap.PixelFormat:= pf8Bit; + end; + // Store the size of the original bitmap + Width := Bitmap.Width; + Height := Bitmap.Height; + // Convert to pf8bit format by copying template + Bitmap.Assign(pf8BitBitmap); + // Restore the original size + Bitmap.Width := Width; + Bitmap.Height := Height; + end else + // This is safe since only pf8bit leaks + Bitmap.PixelFormat := PixelFormat; +end; +{$ENDIF} +{$ENDIF} + + +{$IFDEF VER9x} + +// ----------- +// CopyPalette +// ----------- +// Copies a HPALETTE. +// +// Copied from D3 graphics.pas. +// This is declared private in some old versions of Delphi 2 so we have to +// implement it here to support those old versions. +// +// Parameters: +// Palette The palette to copy. +// +// Returns: +// The handle to a new palette. +// +function CopyPalette(Palette: HPALETTE): HPALETTE; +var + PaletteSize: Integer; + LogPal: TMaxLogPalette; +begin + Result := 0; + if Palette = 0 then Exit; + PaletteSize := 0; + if GetObject(Palette, SizeOf(PaletteSize), @PaletteSize) = 0 then Exit; + if PaletteSize = 0 then Exit; + with LogPal do + begin + palVersion := $0300; + palNumEntries := PaletteSize; + GetPaletteEntries(Palette, 0, PaletteSize, palPalEntry); + end; + Result := CreatePalette(PLogPalette(@LogPal)^); +end; + + +// TThreadList implementation from Delphi 3 classes.pas +constructor TThreadList.Create; +begin + inherited Create; + InitializeCriticalSection(FLock); + FList := TList.Create; +end; + +destructor TThreadList.Destroy; +begin + LockList; // Make sure nobody else is inside the list. + try + FList.Free; + inherited Destroy; + finally + UnlockList; + DeleteCriticalSection(FLock); + end; +end; + +procedure TThreadList.Add(Item: Pointer); +begin + LockList; + try + if FList.IndexOf(Item) = -1 then + FList.Add(Item); + finally + UnlockList; + end; +end; + +procedure TThreadList.Clear; +begin + LockList; + try + FList.Clear; + finally + UnlockList; + end; +end; + +function TThreadList.LockList: TList; +begin + EnterCriticalSection(FLock); + Result := FList; +end; + +procedure TThreadList.Remove(Item: Pointer); +begin + LockList; + try + FList.Remove(Item); + finally + UnlockList; + end; +end; + +procedure TThreadList.UnlockList; +begin + LeaveCriticalSection(FLock); +end; +// End of TThreadList implementation + +// From Delphi 3 sysutils.pas +{ CompareMem performs a binary compare of Length bytes of memory referenced + by P1 to that of P2. CompareMem returns True if the memory referenced by + P1 is identical to that of P2. } +function CompareMem(P1, P2: Pointer; Length: Integer): Boolean; assembler; +asm + PUSH ESI + PUSH EDI + MOV ESI,P1 + MOV EDI,P2 + MOV EDX,ECX + XOR EAX,EAX + AND EDX,3 + SHR ECX,1 + SHR ECX,1 + REPE CMPSD + JNE @@2 + MOV ECX,EDX + REPE CMPSB + JNE @@2 +@@1: INC EAX +@@2: POP EDI + POP ESI +end; + +// Dummy ASSERT procedure since ASSERT does not exist in Delphi 2.x +procedure ASSERT(Condition: boolean; Message: string); +begin +end; + +{$ENDIF} // Delphi 2.x stuff + +//////////////////////////////////////////////////////////////////////////////// +// +// TDIB Classes +// +// These classes gives read and write access to TBitmap's pixel data +// independently of the Delphi version used. +// +//////////////////////////////////////////////////////////////////////////////// +type + TDIB = class(TObject) + private + FBitmap : TBitmap; + FPixelFormat : TPixelFormat; + protected + function GetScanline(Row: integer): pointer; virtual; abstract; + constructor Create(ABitmap: TBitmap; APixelFormat: TPixelFormat); + public + property Scanline[Row: integer]: pointer read GetScanline; + property Bitmap: TBitmap read FBitmap; + property PixelFormat: TPixelFormat read FPixelFormat; + end; + + TDIBReader = class(TDIB) + private +{$ifdef VER9x} + FDIB : TDIBSection; + FDC : HDC; + FScanLine : pointer; + FLastRow : integer; + FInfo : PBitmapInfo; + FBytes : integer; +{$endif} + protected + function GetScanline(Row: integer): pointer; override; + public + constructor Create(ABitmap: TBitmap; APixelFormat: TPixelFormat); + destructor Destroy; override; + end; + + TDIBWriter = class(TDIB) + private +{$ifdef PIXELFORMAT_TOO_SLOW} + FDIBInfo : PBitmapInfo; + FDIBBits : pointer; + FDIBInfoSize : integer; + FDIBBitsSize : longInt; +{$ifndef CREATEDIBSECTION_SLOW} + FDIB : HBITMAP; +{$endif} +{$endif} + FPalette : HPalette; + FHeight : integer; + FWidth : integer; + protected + procedure CreateDIB; + procedure FreeDIB; + procedure NeedDIB; + function GetScanline(Row: integer): pointer; override; + public + constructor Create(ABitmap: TBitmap; APixelFormat: TPixelFormat; + AWidth, AHeight: integer; APalette: HPalette); + destructor Destroy; override; + procedure UpdateBitmap; + property Width: integer read FWidth; + property Height: integer read FHeight; + property Palette: HPalette read FPalette; + end; + +//////////////////////////////////////////////////////////////////////////////// +constructor TDIB.Create(ABitmap: TBitmap; APixelFormat: TPixelFormat); +begin + inherited Create; + FBitmap := ABitmap; + FPixelFormat := APixelFormat; +end; + +//////////////////////////////////////////////////////////////////////////////// +constructor TDIBReader.Create(ABitmap: TBitmap; APixelFormat: TPixelFormat); +{$ifdef VER9x} +var + InfoHeaderSize : integer; + ImageSize : longInt; +{$endif} +begin + inherited Create(ABitmap, APixelFormat); +{$ifndef VER9x} + SetPixelFormat(FBitmap, FPixelFormat); +{$else} + FDC := CreateCompatibleDC(0); + SelectPalette(FDC, FBitmap.Palette, False); + + // Allocate DIB info structure + InternalGetDIBSizes(ABitmap.Handle, InfoHeaderSize, ImageSize, APixelFormat); + GetMem(FInfo, InfoHeaderSize); + // Get DIB info + InitializeBitmapInfoHeader(ABitmap.Handle, FInfo^.bmiHeader, APixelFormat); + + // Allocate scan line buffer + GetMem(FScanLine, ImageSize DIV abs(FInfo^.bmiHeader.biHeight)); + + FLastRow := -1; +{$endif} +end; + +destructor TDIBReader.Destroy; +begin +{$ifdef VER9x} + DeleteDC(FDC); + FreeMem(FScanLine); + FreeMem(FInfo); +{$endif} + inherited Destroy; +end; + +function TDIBReader.GetScanline(Row: integer): pointer; +begin +{$ifdef VER9x} + if (Row < 0) or (Row >= FBitmap.Height) then + raise EInvalidGraphicOperation.Create(SScanLine); + GDIFlush; + + Result := FScanLine; + if (Row = FLastRow) then + exit; + FLastRow := Row; + + if (FInfo^.bmiHeader.biHeight > 0) then // bottom-up DIB + Row := FInfo^.bmiHeader.biHeight - Row - 1; + GetDIBits(FDC, FBitmap.Handle, Row, 1, FScanLine, FInfo^, DIB_RGB_COLORS); + +{$else} + Result := FBitmap.ScanLine[Row]; +{$endif} +end; + +//////////////////////////////////////////////////////////////////////////////// +constructor TDIBWriter.Create(ABitmap: TBitmap; APixelFormat: TPixelFormat; + AWidth, AHeight: integer; APalette: HPalette); +begin + inherited Create(ABitmap, APixelFormat); + + // DIB writer only supports 8 or 24 bit bitmaps + if not(APixelFormat in [pf8bit, pf24bit]) then + Error(sInvalidPixelFormat); + if (AWidth = 0) or (AHeight = 0) then + Error(sBadDimension); + + FHeight := AHeight; + FWidth := AWidth; +{$ifndef PIXELFORMAT_TOO_SLOW} + FBitmap.Palette := 0; + FBitmap.Height := FHeight; + FBitmap.Width := FWidth; + SafeSetPixelFormat(FBitmap, FPixelFormat); + FPalette := CopyPalette(APalette); + FBitmap.Palette := FPalette; +{$else} + FPalette := APalette; + FDIBInfo := nil; + FDIBBits := nil; +{$ifndef CREATEDIBSECTION_SLOW} + FDIB := 0; +{$endif} +{$endif} +end; + +destructor TDIBWriter.Destroy; +begin + UpdateBitmap; + FreeDIB; + inherited Destroy; +end; + +function TDIBWriter.GetScanline(Row: integer): pointer; +begin +{$ifdef PIXELFORMAT_TOO_SLOW} + NeedDIB; + + if (FDIBBits = nil) then + Error(sNoDIB); + with FDIBInfo^.bmiHeader do + begin + if (Row < 0) or (Row >= Height) then + raise EInvalidGraphicOperation.Create(SScanLine); + GDIFlush; + + if biHeight > 0 then // bottom-up DIB + Row := biHeight - Row - 1; + Result := PChar(Cardinal(FDIBBits) + Cardinal(Row) * AlignBit(biWidth, biBitCount, 32)); + end; +{$else} + Result := FBitmap.ScanLine[Row]; +{$endif} +end; + +procedure TDIBWriter.CreateDIB; +{$IFDEF PIXELFORMAT_TOO_SLOW} +var + SrcColors : WORD; +// ScreenDC : HDC; + + // From Delphi 3.02 graphics.pas + // There is a bug in the ByteSwapColors from Delphi 3.0! + procedure ByteSwapColors(var Colors; Count: Integer); + var // convert RGB to BGR and vice-versa. TRGBQuad <-> TPaletteEntry + SysInfo: TSystemInfo; + begin + GetSystemInfo(SysInfo); + asm + MOV EDX, Colors + MOV ECX, Count + DEC ECX + JS @@END + LEA EAX, SysInfo + CMP [EAX].TSystemInfo.wProcessorLevel, 3 + JE @@386 + @@1: MOV EAX, [EDX+ECX*4] + BSWAP EAX + SHR EAX,8 + MOV [EDX+ECX*4],EAX + DEC ECX + JNS @@1 + JMP @@END + @@386: + PUSH EBX + @@2: XOR EBX,EBX + MOV EAX, [EDX+ECX*4] + MOV BH, AL + MOV BL, AH + SHR EAX,16 + SHL EBX,8 + MOV BL, AL + MOV [EDX+ECX*4],EBX + DEC ECX + JNS @@2 + POP EBX + @@END: + end; + end; +{$ENDIF} +begin +{$ifdef PIXELFORMAT_TOO_SLOW} + FreeDIB; + + if (PixelFormat = pf8bit) then + // 8 bit: Header and palette + FDIBInfoSize := SizeOf(TBitmapInfoHeader) + SizeOf(TRGBQuad) * (1 shl 8) + else + // 24 bit: Header but no palette + FDIBInfoSize := SizeOf(TBitmapInfoHeader); + + // Allocate TBitmapInfo structure + GetMem(FDIBInfo, FDIBInfoSize); + try + FDIBInfo^.bmiHeader.biSize := SizeOf(FDIBInfo^.bmiHeader); + FDIBInfo^.bmiHeader.biWidth := Width; + FDIBInfo^.bmiHeader.biHeight := Height; + FDIBInfo^.bmiHeader.biPlanes := 1; + FDIBInfo^.bmiHeader.biSizeImage := 0; + FDIBInfo^.bmiHeader.biCompression := BI_RGB; + + if (PixelFormat = pf8bit) then + begin + FDIBInfo^.bmiHeader.biBitCount := 8; + // Find number of colors defined by palette + if (Palette <> 0) and + (GetObject(Palette, sizeof(SrcColors), @SrcColors) <> 0) and + (SrcColors <> 0) then + begin + // Copy all colors... + GetPaletteEntries(Palette, 0, SrcColors, FDIBInfo^.bmiColors[0]); + // ...and convert BGR to RGB + ByteSwapColors(FDIBInfo^.bmiColors[0], SrcColors); + end else + SrcColors := 0; + + // Finally zero any unused entried + if (SrcColors < 256) then + FillChar(pointer(LongInt(@FDIBInfo^.bmiColors)+SizeOf(TRGBQuad)*SrcColors)^, + 256 - SrcColors, 0); + FDIBInfo^.bmiHeader.biClrUsed := 256; + FDIBInfo^.bmiHeader.biClrImportant := SrcColors; + end else + begin + FDIBInfo^.bmiHeader.biBitCount := 24; + FDIBInfo^.bmiHeader.biClrUsed := 0; + FDIBInfo^.bmiHeader.biClrImportant := 0; + end; + FDIBBitsSize := AlignBit(Width, FDIBInfo^.bmiHeader.biBitCount, 32) * Cardinal(abs(Height)); + +{$ifdef CREATEDIBSECTION_SLOW} + FDIBBits := GlobalAllocPtr(GMEM_MOVEABLE, FDIBBitsSize); + if (FDIBBits = nil) then + raise EOutOfMemory.Create(sOutOfMemDIB); +{$else} +// ScreenDC := GDICheck(GetDC(0)); + try + // Allocate DIB section + // Note: You can ignore warnings about the HDC parameter being 0. The + // parameter is not used for 24 bit bitmaps + FDIB := GDICheck(CreateDIBSection(0 {ScreenDC}, FDIBInfo^, DIB_RGB_COLORS, + FDIBBits, + {$IFDEF VER9x} nil, {$ELSE} 0, {$ENDIF} + 0)); + finally +// ReleaseDC(0, ScreenDC); + end; +{$endif} + + except + FreeDIB; + raise; + end; +{$endif} +end; + +procedure TDIBWriter.FreeDIB; +begin +{$ifdef PIXELFORMAT_TOO_SLOW} + if (FDIBInfo <> nil) then + FreeMem(FDIBInfo); +{$ifdef CREATEDIBSECTION_SLOW} + if (FDIBBits <> nil) then + GlobalFreePtr(FDIBBits); +{$else} + if (FDIB <> 0) then + DeleteObject(FDIB); + FDIB := 0; +{$endif} + FDIBInfo := nil; + FDIBBits := nil; +{$endif} +end; + +procedure TDIBWriter.NeedDIB; +begin +{$ifdef PIXELFORMAT_TOO_SLOW} +{$ifdef CREATEDIBSECTION_SLOW} + if (FDIBBits = nil) then +{$else} + if (FDIB = 0) then +{$endif} + CreateDIB; +{$endif} +end; + +// Convert the DIB created by CreateDIB back to a TBitmap +procedure TDIBWriter.UpdateBitmap; +{$ifdef PIXELFORMAT_TOO_SLOW} +var + Stream : TMemoryStream; + FileSize : longInt; + BitmapFileHeader : TBitmapFileHeader; +{$endif} +begin +{$ifdef PIXELFORMAT_TOO_SLOW} + +{$ifdef CREATEDIBSECTION_SLOW} + if (FDIBBits = nil) then +{$else} + if (FDIB = 0) then +{$endif} + exit; + + // Win95 and NT differs in what solution performs best +{$ifndef CREATEDIBSECTION_SLOW} +{$ifdef VER10_PLUS} + if (Win32Platform = VER_PLATFORM_WIN32_NT) then + begin + // Assign DIB to bitmap + FBitmap.Handle := FDIB; + FDIB := 0; + FBitmap.Palette := CopyPalette(Palette); + end else +{$endif} +{$endif} + begin + // Write DIB to a stream in the BMP file format + Stream := TMemoryStream.Create; + try + // Make room in stream for a TBitmapInfo and pixel data + FileSize := sizeof(TBitmapFileHeader) + FDIBInfoSize + FDIBBitsSize; + Stream.SetSize(FileSize); + // Initialize file header + FillChar(BitmapFileHeader, sizeof(TBitmapFileHeader), 0); + with BitmapFileHeader do + begin + bfType := $4D42; // 'BM' = Windows BMP signature + bfSize := FileSize; // File size (not needed) + bfOffBits := sizeof(TBitmapFileHeader) + FDIBInfoSize; // Offset of pixel data + end; + // Save file header + Stream.Write(BitmapFileHeader, sizeof(TBitmapFileHeader)); + // Save TBitmapInfo structure + Stream.Write(FDIBInfo^, FDIBInfoSize); + // Save pixel data + Stream.Write(FDIBBits^, FDIBBitsSize); + + // Rewind and load bitmap from stream + Stream.Position := 0; + FBitmap.LoadFromStream(Stream); + finally + Stream.Free; + end; + end; +{$endif} +end; + +//////////////////////////////////////////////////////////////////////////////// +// +// Color Mapping +// +//////////////////////////////////////////////////////////////////////////////// +type + TColorLookup = class(TObject) + private + FColors : integer; + public + constructor Create(Palette: hPalette); virtual; + function Lookup(Red, Green, Blue: BYTE; var R, G, B: BYTE): char; virtual; abstract; + property Colors: integer read FColors; + end; + + PRGBQuadArray = ^TRGBQuadArray; // From Delphi 3 graphics.pas + TRGBQuadArray = array[Byte] of TRGBQuad; // From Delphi 3 graphics.pas + + BGRArray = array[0..0] of TRGBTriple; + PBGRArray = ^BGRArray; + + PalArray = array[byte] of TPaletteEntry; + PPalArray = ^PalArray; + + // TFastColorLookup implements a simple but reasonably fast generic color + // mapper. It trades precision for speed by reducing the size of the color + // space. + // Using a class instead of inline code results in a speed penalty of + // approx. 15% but reduces the complexity of the color reduction routines that + // uses it. If bitmap to GIF conversion speed is really important to you, the + // implementation can easily be inlined again. + TInverseLookup = array[0..1 SHL 15-1] of SmallInt; + PInverseLookup = ^TInverseLookup; + + TFastColorLookup = class(TColorLookup) + private + FPaletteEntries : PPalArray; + FInverseLookup : PInverseLookup; + public + constructor Create(Palette: hPalette); override; + destructor Destroy; override; + function Lookup(Red, Green, Blue: BYTE; var R, G, B: BYTE): char; override; + end; + + // TSlowColorLookup implements a precise but very slow generic color mapper. + // It uses the GetNearestPaletteIndex GDI function. + // Note: Tests has shown TFastColorLookup to be more precise than + // TSlowColorLookup in many cases. I can't explain why... + TSlowColorLookup = class(TColorLookup) + private + FPaletteEntries : PPalArray; + FPalette : hPalette; + public + constructor Create(Palette: hPalette); override; + destructor Destroy; override; + function Lookup(Red, Green, Blue: BYTE; var R, G, B: BYTE): char; override; + end; + + // TNetscapeColorLookup maps colors to the netscape 6*6*6 color cube. + TNetscapeColorLookup = class(TColorLookup) + public + constructor Create(Palette: hPalette); override; + function Lookup(Red, Green, Blue: BYTE; var R, G, B: BYTE): char; override; + end; + + // TGrayWindowsLookup maps colors to 4 shade palette. + TGrayWindowsLookup = class(TSlowColorLookup) + public + constructor Create(Palette: hPalette); override; + function Lookup(Red, Green, Blue: BYTE; var R, G, B: BYTE): char; override; + end; + + // TGrayScaleLookup maps colors to a uniform 256 shade palette. + TGrayScaleLookup = class(TColorLookup) + public + constructor Create(Palette: hPalette); override; + function Lookup(Red, Green, Blue: BYTE; var R, G, B: BYTE): char; override; + end; + + // TMonochromeLookup maps colors to a black/white palette. + TMonochromeLookup = class(TColorLookup) + public + constructor Create(Palette: hPalette); override; + function Lookup(Red, Green, Blue: BYTE; var R, G, B: BYTE): char; override; + end; + +constructor TColorLookup.Create(Palette: hPalette); +begin + inherited Create; +end; + +constructor TFastColorLookup.Create(Palette: hPalette); +var + i : integer; + InverseIndex : integer; +begin + inherited Create(Palette); + + GetMem(FPaletteEntries, sizeof(TPaletteEntry) * 256); + FColors := GetPaletteEntries(Palette, 0, 256, FPaletteEntries^); + + New(FInverseLookup); + for i := low(TInverseLookup) to high(TInverseLookup) do + FInverseLookup^[i] := -1; + + // Premap palette colors + if (FColors > 0) then + for i := 0 to FColors-1 do + with FPaletteEntries^[i] do + begin + InverseIndex := (peRed SHR 3) OR ((peGreen AND $F8) SHL 2) OR ((peBlue AND $F8) SHL 7); + if (FInverseLookup^[InverseIndex] = -1) then + FInverseLookup^[InverseIndex] := i; + end; +end; + +destructor TFastColorLookup.Destroy; +begin + if (FPaletteEntries <> nil) then + FreeMem(FPaletteEntries); + if (FInverseLookup <> nil) then + Dispose(FInverseLookup); + + inherited Destroy; +end; + +// Map color to arbitrary palette +function TFastColorLookup.Lookup(Red, Green, Blue: BYTE; var R, G, B: BYTE): char; +var + i : integer; + InverseIndex : integer; + Delta , + MinDelta , + MinColor : integer; +begin + // Reduce color space with 3 bits in each dimension + InverseIndex := (Red SHR 3) OR ((Green AND $F8) SHL 2) OR ((Blue AND $F8) SHL 7); + + if (FInverseLookup^[InverseIndex] <> -1) then + Result := char(FInverseLookup^[InverseIndex]) + else + begin + // Sequential scan for nearest color to minimize euclidian distance + MinDelta := 3 * (256 * 256); + MinColor := 0; + for i := 0 to FColors-1 do + with FPaletteEntries[i] do + begin + Delta := ABS(peRed - Red) + ABS(peGreen - Green) + ABS(peBlue - Blue); + if (Delta < MinDelta) then + begin + MinDelta := Delta; + MinColor := i; + end; + end; + Result := char(MinColor); + FInverseLookup^[InverseIndex] := MinColor; + end; + + with FPaletteEntries^[ord(Result)] do + begin + R := peRed; + G := peGreen; + B := peBlue; + end; +end; + +constructor TSlowColorLookup.Create(Palette: hPalette); +begin + inherited Create(Palette); + FPalette := Palette; + FColors := GetPaletteEntries(Palette, 0, 256, nil^); + if (FColors > 0) then + begin + GetMem(FPaletteEntries, sizeof(TPaletteEntry) * FColors); + FColors := GetPaletteEntries(Palette, 0, 256, FPaletteEntries^); + end; +end; + +destructor TSlowColorLookup.Destroy; +begin + if (FPaletteEntries <> nil) then + FreeMem(FPaletteEntries); + + inherited Destroy; +end; + +// Map color to arbitrary palette +function TSlowColorLookup.Lookup(Red, Green, Blue: BYTE; var R, G, B: BYTE): char; +begin + Result := char(GetNearestPaletteIndex(FPalette, Red OR (Green SHL 8) OR (Blue SHL 16))); + if (FPaletteEntries <> nil) then + with FPaletteEntries^[ord(Result)] do + begin + R := peRed; + G := peGreen; + B := peBlue; + end; +end; + +constructor TNetscapeColorLookup.Create(Palette: hPalette); +begin + inherited Create(Palette); + FColors := 6*6*6; // This better be true or something is wrong +end; + +// Map color to netscape 6*6*6 color cube +function TNetscapeColorLookup.Lookup(Red, Green, Blue: BYTE; var R, G, B: BYTE): char; +begin + R := (Red+3) DIV 51; + G := (Green+3) DIV 51; + B := (Blue+3) DIV 51; + Result := char(B + 6*G + 36*R); + R := R * 51; + G := G * 51; + B := B * 51; +end; + +constructor TGrayWindowsLookup.Create(Palette: hPalette); +begin + inherited Create(Palette); + FColors := 4; +end; + +// Convert color to windows grays +function TGrayWindowsLookup.Lookup(Red, Green, Blue: BYTE; var R, G, B: BYTE): char; +begin + Result := inherited Lookup(MulDiv(Red, 77, 256), + MulDiv(Green, 150, 256), MulDiv(Blue, 29, 256), R, G, B); +end; + +constructor TGrayScaleLookup.Create(Palette: hPalette); +begin + inherited Create(Palette); + FColors := 256; +end; + +// Convert color to grayscale +function TGrayScaleLookup.Lookup(Red, Green, Blue: BYTE; var R, G, B: BYTE): char; +begin + Result := char((Blue*29 + Green*150 + Red*77) DIV 256); + R := ord(Result); + G := ord(Result); + B := ord(Result); +end; + +constructor TMonochromeLookup.Create(Palette: hPalette); +begin + inherited Create(Palette); + FColors := 2; +end; + +// Convert color to black/white +function TMonochromeLookup.Lookup(Red, Green, Blue: BYTE; var R, G, B: BYTE): char; +begin + if ((Blue*29 + Green*150 + Red*77) > 32512) then + begin + Result := #1; + R := 255; + G := 255; + B := 255; + end else + begin + Result := #0; + R := 0; + G := 0; + B := 0; + end; +end; + +//////////////////////////////////////////////////////////////////////////////// +// +// Dithering engine +// +//////////////////////////////////////////////////////////////////////////////// +type + TDitherEngine = class + private + protected + FDirection : integer; + FColumn : integer; + FLookup : TColorLookup; + Width : integer; + public + constructor Create(AWidth: integer; Lookup: TColorLookup); virtual; + function Dither(Red, Green, Blue: BYTE; var R, G, B: BYTE): char; virtual; + procedure NextLine; virtual; + procedure NextColumn; + + property Direction: integer read FDirection; + property Column: integer read FColumn; + end; + + // Note: TErrorTerm does only *need* to be 16 bits wide, but since + // it is *much* faster to use native machine words (32 bit), we sacrifice + // some bytes (a lot actually) to improve performance. + TErrorTerm = Integer; + TErrors = array[0..0] of TErrorTerm; + PErrors = ^TErrors; + + TFloydSteinbergDitherer = class(TDitherEngine) + private + ErrorsR , + ErrorsG , + ErrorsB : PErrors; + ErrorR , + ErrorG , + ErrorB : PErrors; + CurrentErrorR , // Current error or pixel value + CurrentErrorG , + CurrentErrorB , + BelowErrorR , // Error for pixel below current + BelowErrorG , + BelowErrorB , + BelowPrevErrorR , // Error for pixel below previous pixel + BelowPrevErrorG , + BelowPrevErrorB : TErrorTerm; + public + constructor Create(AWidth: integer; Lookup: TColorLookup); override; + destructor Destroy; override; + function Dither(Red, Green, Blue: BYTE; var R, G, B: BYTE): char; override; + procedure NextLine; override; + end; + + T5by3Ditherer = class(TDitherEngine) + private + ErrorsR0 , + ErrorsG0 , + ErrorsB0 , + ErrorsR1 , + ErrorsG1 , + ErrorsB1 , + ErrorsR2 , + ErrorsG2 , + ErrorsB2 : PErrors; + ErrorR0 , + ErrorG0 , + ErrorB0 , + ErrorR1 , + ErrorG1 , + ErrorB1 , + ErrorR2 , + ErrorG2 , + ErrorB2 : PErrors; + FDirection2 : integer; + protected + FDivisor : integer; + procedure Propagate(Errors0, Errors1, Errors2: PErrors; Error: integer); virtual; abstract; + public + constructor Create(AWidth: integer; Lookup: TColorLookup); override; + destructor Destroy; override; + function Dither(Red, Green, Blue: BYTE; var R, G, B: BYTE): char; override; + procedure NextLine; override; + end; + + TStuckiDitherer = class(T5by3Ditherer) + protected + procedure Propagate(Errors0, Errors1, Errors2: PErrors; Error: integer); override; + public + constructor Create(AWidth: integer; Lookup: TColorLookup); override; + end; + + TSierraDitherer = class(T5by3Ditherer) + protected + procedure Propagate(Errors0, Errors1, Errors2: PErrors; Error: integer); override; + public + constructor Create(AWidth: integer; Lookup: TColorLookup); override; + end; + + TJaJuNiDitherer = class(T5by3Ditherer) + protected + procedure Propagate(Errors0, Errors1, Errors2: PErrors; Error: integer); override; + public + constructor Create(AWidth: integer; Lookup: TColorLookup); override; + end; + + TSteveArcheDitherer = class(TDitherEngine) + private + ErrorsR0 , + ErrorsG0 , + ErrorsB0 , + ErrorsR1 , + ErrorsG1 , + ErrorsB1 , + ErrorsR2 , + ErrorsG2 , + ErrorsB2 , + ErrorsR3 , + ErrorsG3 , + ErrorsB3 : PErrors; + ErrorR0 , + ErrorG0 , + ErrorB0 , + ErrorR1 , + ErrorG1 , + ErrorB1 , + ErrorR2 , + ErrorG2 , + ErrorB2 , + ErrorR3 , + ErrorG3 , + ErrorB3 : PErrors; + FDirection2 , + FDirection3 : integer; + public + constructor Create(AWidth: integer; Lookup: TColorLookup); override; + destructor Destroy; override; + function Dither(Red, Green, Blue: BYTE; var R, G, B: BYTE): char; override; + procedure NextLine; override; + end; + + TBurkesDitherer = class(TDitherEngine) + private + ErrorsR0 , + ErrorsG0 , + ErrorsB0 , + ErrorsR1 , + ErrorsG1 , + ErrorsB1 : PErrors; + ErrorR0 , + ErrorG0 , + ErrorB0 , + ErrorR1 , + ErrorG1 , + ErrorB1 : PErrors; + FDirection2 : integer; + public + constructor Create(AWidth: integer; Lookup: TColorLookup); override; + destructor Destroy; override; + function Dither(Red, Green, Blue: BYTE; var R, G, B: BYTE): char; override; + procedure NextLine; override; + end; + +//////////////////////////////////////////////////////////////////////////////// +// TDitherEngine +constructor TDitherEngine.Create(AWidth: integer; Lookup: TColorLookup); +begin + inherited Create; + + FLookup := Lookup; + Width := AWidth; + + FDirection := 1; + FColumn := 0; +end; + +function TDitherEngine.Dither(Red, Green, Blue: BYTE; var R, G, B: BYTE): char; +begin + // Map color to palette + Result := FLookup.Lookup(Red, Green, Blue, R, G, B); + NextColumn; +end; + +procedure TDitherEngine.NextLine; +begin + FDirection := -FDirection; + if (FDirection = 1) then + FColumn := 0 + else + FColumn := Width-1; +end; + +procedure TDitherEngine.NextColumn; +begin + inc(FColumn, FDirection); +end; + +//////////////////////////////////////////////////////////////////////////////// +// TFloydSteinbergDitherer +constructor TFloydSteinbergDitherer.Create(AWidth: integer; Lookup: TColorLookup); +begin + inherited Create(AWidth, Lookup); + + // The Error arrays has (columns + 2) entries; the extra entry at + // each end saves us from special-casing the first and last pixels. + // We can get away with a single array (holding one row's worth of errors) + // by using it to store the current row's errors at pixel columns not yet + // processed, but the next row's errors at columns already processed. We + // need only a few extra variables to hold the errors immediately around the + // current column. (If we are lucky, those variables are in registers, but + // even if not, they're probably cheaper to access than array elements are.) + GetMem(ErrorsR, sizeof(TErrorTerm)*(Width+2)); + GetMem(ErrorsG, sizeof(TErrorTerm)*(Width+2)); + GetMem(ErrorsB, sizeof(TErrorTerm)*(Width+2)); + FillChar(ErrorsR^, sizeof(TErrorTerm)*(Width+2), 0); + FillChar(ErrorsG^, sizeof(TErrorTerm)*(Width+2), 0); + FillChar(ErrorsB^, sizeof(TErrorTerm)*(Width+2), 0); + ErrorR := ErrorsR; + ErrorG := ErrorsG; + ErrorB := ErrorsB; + CurrentErrorR := 0; + CurrentErrorG := CurrentErrorR; + CurrentErrorB := CurrentErrorR; + BelowErrorR := CurrentErrorR; + BelowErrorG := CurrentErrorR; + BelowErrorB := CurrentErrorR; + BelowPrevErrorR := CurrentErrorR; + BelowPrevErrorG := CurrentErrorR; + BelowPrevErrorB := CurrentErrorR; +end; + +destructor TFloydSteinbergDitherer.Destroy; +begin + FreeMem(ErrorsR); + FreeMem(ErrorsG); + FreeMem(ErrorsB); + inherited Destroy; +end; + +{$IFOPT R+} + {$DEFINE R_PLUS} + {$RANGECHECKS OFF} +{$ENDIF} +function TFloydSteinbergDitherer.Dither(Red, Green, Blue: BYTE; var R, G, B: BYTE): char; +var + BelowNextError : TErrorTerm; + Delta : TErrorTerm; +begin + CurrentErrorR := Red + (CurrentErrorR + ErrorR[0] + 8) DIV 16; +// CurrentErrorR := Red + (CurrentErrorR + ErrorR[Direction] + 8) DIV 16; + if (CurrentErrorR < 0) then + CurrentErrorR := 0 + else if (CurrentErrorR > 255) then + CurrentErrorR := 255; + + CurrentErrorG := Green + (CurrentErrorG + ErrorG[0] + 8) DIV 16; +// CurrentErrorG := Green + (CurrentErrorG + ErrorG[Direction] + 8) DIV 16; + if (CurrentErrorG < 0) then + CurrentErrorG := 0 + else if (CurrentErrorG > 255) then + CurrentErrorG := 255; + + CurrentErrorB := Blue + (CurrentErrorB + ErrorB[0] + 8) DIV 16; +// CurrentErrorB := Blue + (CurrentErrorB + ErrorB[Direction] + 8) DIV 16; + if (CurrentErrorB < 0) then + CurrentErrorB := 0 + else if (CurrentErrorB > 255) then + CurrentErrorB := 255; + + // Map color to palette + Result := inherited Dither(CurrentErrorR, CurrentErrorG, CurrentErrorB, R, G, B); + + // Propagate Floyd-Steinberg error terms. + // Errors are accumulated into the error arrays, at a resolution of + // 1/16th of a pixel count. The error at a given pixel is propagated + // to its not-yet-processed neighbors using the standard F-S fractions, + // ... (here) 7/16 + // 3/16 5/16 1/16 + // We work left-to-right on even rows, right-to-left on odd rows. + + // Red component + CurrentErrorR := CurrentErrorR - R; + if (CurrentErrorR <> 0) then + begin + BelowNextError := CurrentErrorR; // Error * 1 + + Delta := CurrentErrorR * 2; + inc(CurrentErrorR, Delta); + ErrorR[0] := BelowPrevErrorR + CurrentErrorR; // Error * 3 + + inc(CurrentErrorR, Delta); + BelowPrevErrorR := BelowErrorR + CurrentErrorR; // Error * 5 + + BelowErrorR := BelowNextError; // Error * 1 + + inc(CurrentErrorR, Delta); // Error * 7 + end; + + // Green component + CurrentErrorG := CurrentErrorG - G; + if (CurrentErrorG <> 0) then + begin + BelowNextError := CurrentErrorG; // Error * 1 + + Delta := CurrentErrorG * 2; + inc(CurrentErrorG, Delta); + ErrorG[0] := BelowPrevErrorG + CurrentErrorG; // Error * 3 + + inc(CurrentErrorG, Delta); + BelowPrevErrorG := BelowErrorG + CurrentErrorG; // Error * 5 + + BelowErrorG := BelowNextError; // Error * 1 + + inc(CurrentErrorG, Delta); // Error * 7 + end; + + // Blue component + CurrentErrorB := CurrentErrorB - B; + if (CurrentErrorB <> 0) then + begin + BelowNextError := CurrentErrorB; // Error * 1 + + Delta := CurrentErrorB * 2; + inc(CurrentErrorB, Delta); + ErrorB[0] := BelowPrevErrorB + CurrentErrorB; // Error * 3 + + inc(CurrentErrorB, Delta); + BelowPrevErrorB := BelowErrorB + CurrentErrorB; // Error * 5 + + BelowErrorB := BelowNextError; // Error * 1 + + inc(CurrentErrorB, Delta); // Error * 7 + end; + + // Move on to next column + if (Direction = 1) then + begin + inc(longInt(ErrorR), sizeof(TErrorTerm)); + inc(longInt(ErrorG), sizeof(TErrorTerm)); + inc(longInt(ErrorB), sizeof(TErrorTerm)); + end else + begin + dec(longInt(ErrorR), sizeof(TErrorTerm)); + dec(longInt(ErrorG), sizeof(TErrorTerm)); + dec(longInt(ErrorB), sizeof(TErrorTerm)); + end; +end; +{$IFDEF R_PLUS} + {$RANGECHECKS ON} + {$UNDEF R_PLUS} +{$ENDIF} + +{$IFOPT R+} + {$DEFINE R_PLUS} + {$RANGECHECKS OFF} +{$ENDIF} +procedure TFloydSteinbergDitherer.NextLine; +begin + ErrorR[0] := BelowPrevErrorR; + ErrorG[0] := BelowPrevErrorG; + ErrorB[0] := BelowPrevErrorB; + + // Note: The optimizer produces better code for this construct: + // a := 0; b := a; c := a; + // compared to this construct: + // a := 0; b := 0; c := 0; + CurrentErrorR := 0; + CurrentErrorG := CurrentErrorR; + CurrentErrorB := CurrentErrorG; + BelowErrorR := CurrentErrorG; + BelowErrorG := CurrentErrorG; + BelowErrorB := CurrentErrorG; + BelowPrevErrorR := CurrentErrorG; + BelowPrevErrorG := CurrentErrorG; + BelowPrevErrorB := CurrentErrorG; + + inherited NextLine; + + if (Direction = 1) then + begin + ErrorR := ErrorsR; + ErrorG := ErrorsG; + ErrorB := ErrorsB; + end else + begin + ErrorR := @ErrorsR[Width+1]; + ErrorG := @ErrorsG[Width+1]; + ErrorB := @ErrorsB[Width+1]; + end; +end; +{$IFDEF R_PLUS} + {$RANGECHECKS ON} + {$UNDEF R_PLUS} +{$ENDIF} + +//////////////////////////////////////////////////////////////////////////////// +// T5by3Ditherer +constructor T5by3Ditherer.Create(AWidth: integer; Lookup: TColorLookup); +begin + inherited Create(AWidth, Lookup); + + GetMem(ErrorsR0, sizeof(TErrorTerm)*(Width+4)); + GetMem(ErrorsG0, sizeof(TErrorTerm)*(Width+4)); + GetMem(ErrorsB0, sizeof(TErrorTerm)*(Width+4)); + GetMem(ErrorsR1, sizeof(TErrorTerm)*(Width+4)); + GetMem(ErrorsG1, sizeof(TErrorTerm)*(Width+4)); + GetMem(ErrorsB1, sizeof(TErrorTerm)*(Width+4)); + GetMem(ErrorsR2, sizeof(TErrorTerm)*(Width+4)); + GetMem(ErrorsG2, sizeof(TErrorTerm)*(Width+4)); + GetMem(ErrorsB2, sizeof(TErrorTerm)*(Width+4)); + FillChar(ErrorsR0^, sizeof(TErrorTerm)*(Width+4), 0); + FillChar(ErrorsG0^, sizeof(TErrorTerm)*(Width+4), 0); + FillChar(ErrorsB0^, sizeof(TErrorTerm)*(Width+4), 0); + FillChar(ErrorsR1^, sizeof(TErrorTerm)*(Width+4), 0); + FillChar(ErrorsG1^, sizeof(TErrorTerm)*(Width+4), 0); + FillChar(ErrorsB1^, sizeof(TErrorTerm)*(Width+4), 0); + FillChar(ErrorsR2^, sizeof(TErrorTerm)*(Width+4), 0); + FillChar(ErrorsG2^, sizeof(TErrorTerm)*(Width+4), 0); + FillChar(ErrorsB2^, sizeof(TErrorTerm)*(Width+4), 0); + + FDivisor := 1; + FDirection2 := 2 * Direction; + ErrorR0 := PErrors(longInt(ErrorsR0)+2*sizeof(TErrorTerm)); + ErrorG0 := PErrors(longInt(ErrorsG0)+2*sizeof(TErrorTerm)); + ErrorB0 := PErrors(longInt(ErrorsB0)+2*sizeof(TErrorTerm)); + ErrorR1 := PErrors(longInt(ErrorsR1)+2*sizeof(TErrorTerm)); + ErrorG1 := PErrors(longInt(ErrorsG1)+2*sizeof(TErrorTerm)); + ErrorB1 := PErrors(longInt(ErrorsB1)+2*sizeof(TErrorTerm)); + ErrorR2 := PErrors(longInt(ErrorsR2)+2*sizeof(TErrorTerm)); + ErrorG2 := PErrors(longInt(ErrorsG2)+2*sizeof(TErrorTerm)); + ErrorB2 := PErrors(longInt(ErrorsB2)+2*sizeof(TErrorTerm)); +end; + +destructor T5by3Ditherer.Destroy; +begin + FreeMem(ErrorsR0); + FreeMem(ErrorsG0); + FreeMem(ErrorsB0); + FreeMem(ErrorsR1); + FreeMem(ErrorsG1); + FreeMem(ErrorsB1); + FreeMem(ErrorsR2); + FreeMem(ErrorsG2); + FreeMem(ErrorsB2); + inherited Destroy; +end; + +{$IFOPT R+} + {$DEFINE R_PLUS} + {$RANGECHECKS OFF} +{$ENDIF} +function T5by3Ditherer.Dither(Red, Green, Blue: BYTE; var R, G, B: BYTE): char; +var + ColorR , + ColorG , + ColorB : integer; // Error for current pixel +begin + // Apply red component error correction + ColorR := Red + (ErrorR0[0] + FDivisor DIV 2) DIV FDivisor; + if (ColorR < 0) then + ColorR := 0 + else if (ColorR > 255) then + ColorR := 255; + + // Apply green component error correction + ColorG := Green + (ErrorG0[0] + FDivisor DIV 2) DIV FDivisor; + if (ColorG < 0) then + ColorG := 0 + else if (ColorG > 255) then + ColorG := 255; + + // Apply blue component error correction + ColorB := Blue + (ErrorB0[0] + FDivisor DIV 2) DIV FDivisor; + if (ColorB < 0) then + ColorB := 0 + else if (ColorB > 255) then + ColorB := 255; + + // Map color to palette + Result := inherited Dither(ColorR, ColorG, ColorB, R, G, B); + + // Propagate red component error + Propagate(ErrorR0, ErrorR1, ErrorR2, ColorR - R); + // Propagate green component error + Propagate(ErrorG0, ErrorG1, ErrorG2, ColorG - G); + // Propagate blue component error + Propagate(ErrorB0, ErrorB1, ErrorB2, ColorB - B); + + // Move on to next column + if (Direction = 1) then + begin + inc(longInt(ErrorR0), sizeof(TErrorTerm)); + inc(longInt(ErrorG0), sizeof(TErrorTerm)); + inc(longInt(ErrorB0), sizeof(TErrorTerm)); + inc(longInt(ErrorR1), sizeof(TErrorTerm)); + inc(longInt(ErrorG1), sizeof(TErrorTerm)); + inc(longInt(ErrorB1), sizeof(TErrorTerm)); + inc(longInt(ErrorR2), sizeof(TErrorTerm)); + inc(longInt(ErrorG2), sizeof(TErrorTerm)); + inc(longInt(ErrorB2), sizeof(TErrorTerm)); + end else + begin + dec(longInt(ErrorR0), sizeof(TErrorTerm)); + dec(longInt(ErrorG0), sizeof(TErrorTerm)); + dec(longInt(ErrorB0), sizeof(TErrorTerm)); + dec(longInt(ErrorR1), sizeof(TErrorTerm)); + dec(longInt(ErrorG1), sizeof(TErrorTerm)); + dec(longInt(ErrorB1), sizeof(TErrorTerm)); + dec(longInt(ErrorR2), sizeof(TErrorTerm)); + dec(longInt(ErrorG2), sizeof(TErrorTerm)); + dec(longInt(ErrorB2), sizeof(TErrorTerm)); + end; +end; +{$IFDEF R_PLUS} + {$RANGECHECKS ON} + {$UNDEF R_PLUS} +{$ENDIF} + +{$IFOPT R+} + {$DEFINE R_PLUS} + {$RANGECHECKS OFF} +{$ENDIF} +procedure T5by3Ditherer.NextLine; +var + TempErrors : PErrors; +begin + FillChar(ErrorsR0^, sizeof(TErrorTerm)*(Width+4), 0); + FillChar(ErrorsG0^, sizeof(TErrorTerm)*(Width+4), 0); + FillChar(ErrorsB0^, sizeof(TErrorTerm)*(Width+4), 0); + + // Swap lines + TempErrors := ErrorsR0; + ErrorsR0 := ErrorsR1; + ErrorsR1 := ErrorsR2; + ErrorsR2 := TempErrors; + + TempErrors := ErrorsG0; + ErrorsG0 := ErrorsG1; + ErrorsG1 := ErrorsG2; + ErrorsG2 := TempErrors; + + TempErrors := ErrorsB0; + ErrorsB0 := ErrorsB1; + ErrorsB1 := ErrorsB2; + ErrorsB2 := TempErrors; + + inherited NextLine; + + FDirection2 := 2 * Direction; + if (Direction = 1) then + begin + // ErrorsR0[1] gives compiler error, so we + // use PErrors(longInt(ErrorsR0)+sizeof(TErrorTerm)) instead... + ErrorR0 := PErrors(longInt(ErrorsR0)+2*sizeof(TErrorTerm)); + ErrorG0 := PErrors(longInt(ErrorsG0)+2*sizeof(TErrorTerm)); + ErrorB0 := PErrors(longInt(ErrorsB0)+2*sizeof(TErrorTerm)); + ErrorR1 := PErrors(longInt(ErrorsR1)+2*sizeof(TErrorTerm)); + ErrorG1 := PErrors(longInt(ErrorsG1)+2*sizeof(TErrorTerm)); + ErrorB1 := PErrors(longInt(ErrorsB1)+2*sizeof(TErrorTerm)); + ErrorR2 := PErrors(longInt(ErrorsR2)+2*sizeof(TErrorTerm)); + ErrorG2 := PErrors(longInt(ErrorsG2)+2*sizeof(TErrorTerm)); + ErrorB2 := PErrors(longInt(ErrorsB2)+2*sizeof(TErrorTerm)); + end else + begin + ErrorR0 := @ErrorsR0[Width+1]; + ErrorG0 := @ErrorsG0[Width+1]; + ErrorB0 := @ErrorsB0[Width+1]; + ErrorR1 := @ErrorsR1[Width+1]; + ErrorG1 := @ErrorsG1[Width+1]; + ErrorB1 := @ErrorsB1[Width+1]; + ErrorR2 := @ErrorsR2[Width+1]; + ErrorG2 := @ErrorsG2[Width+1]; + ErrorB2 := @ErrorsB2[Width+1]; + end; +end; +{$IFDEF R_PLUS} + {$RANGECHECKS ON} + {$UNDEF R_PLUS} +{$ENDIF} + +//////////////////////////////////////////////////////////////////////////////// +// TStuckiDitherer +constructor TStuckiDitherer.Create(AWidth: integer; Lookup: TColorLookup); +begin + inherited Create(AWidth, Lookup); + FDivisor := 42; +end; + +{$IFOPT R+} + {$DEFINE R_PLUS} + {$RANGECHECKS OFF} +{$ENDIF} +procedure TStuckiDitherer.Propagate(Errors0, Errors1, Errors2: PErrors; Error: integer); +begin + if (Error = 0) then + exit; + // Propagate Stucki error terms: + // ... ... (here) 8/42 4/42 + // 2/42 4/42 8/42 4/42 2/42 + // 1/42 2/42 4/42 2/42 1/42 + inc(Errors2[FDirection2], Error); // Error * 1 + inc(Errors2[-FDirection2], Error); // Error * 1 + + Error := Error + Error; + inc(Errors1[FDirection2], Error); // Error * 2 + inc(Errors1[-FDirection2], Error); // Error * 2 + inc(Errors2[Direction], Error); // Error * 2 + inc(Errors2[-Direction], Error); // Error * 2 + + Error := Error + Error; + inc(Errors0[FDirection2], Error); // Error * 4 + inc(Errors1[-Direction], Error); // Error * 4 + inc(Errors1[Direction], Error); // Error * 4 + inc(Errors2[0], Error); // Error * 4 + + Error := Error + Error; + inc(Errors0[Direction], Error); // Error * 8 + inc(Errors1[0], Error); // Error * 8 +end; +{$IFDEF R_PLUS} + {$RANGECHECKS ON} + {$UNDEF R_PLUS} +{$ENDIF} + +//////////////////////////////////////////////////////////////////////////////// +// TSierraDitherer +constructor TSierraDitherer.Create(AWidth: integer; Lookup: TColorLookup); +begin + inherited Create(AWidth, Lookup); + FDivisor := 32; +end; + +{$IFOPT R+} + {$DEFINE R_PLUS} + {$RANGECHECKS OFF} +{$ENDIF} +procedure TSierraDitherer.Propagate(Errors0, Errors1, Errors2: PErrors; Error: integer); +var + TempError : integer; +begin + if (Error = 0) then + exit; + // Propagate Sierra error terms: + // ... ... (here) 5/32 3/32 + // 2/32 4/32 5/32 4/32 2/32 + // ... 2/32 3/32 2/32 ... + TempError := Error + Error; + inc(Errors1[FDirection2], TempError); // Error * 2 + inc(Errors1[-FDirection2], TempError);// Error * 2 + inc(Errors2[Direction], TempError); // Error * 2 + inc(Errors2[-Direction], TempError); // Error * 2 + + inc(TempError, Error); + inc(Errors0[FDirection2], TempError); // Error * 3 + inc(Errors2[0], TempError); // Error * 3 + + inc(TempError, Error); + inc(Errors1[-Direction], TempError); // Error * 4 + inc(Errors1[Direction], TempError); // Error * 4 + + inc(TempError, Error); + inc(Errors0[Direction], TempError); // Error * 5 + inc(Errors1[0], TempError); // Error * 5 +end; +{$IFDEF R_PLUS} + {$RANGECHECKS ON} + {$UNDEF R_PLUS} +{$ENDIF} + +//////////////////////////////////////////////////////////////////////////////// +// TJaJuNiDitherer +constructor TJaJuNiDitherer.Create(AWidth: integer; Lookup: TColorLookup); +begin + inherited Create(AWidth, Lookup); + FDivisor := 38; +end; + +{$IFOPT R+} + {$DEFINE R_PLUS} + {$RANGECHECKS OFF} +{$ENDIF} +procedure TJaJuNiDitherer.Propagate(Errors0, Errors1, Errors2: PErrors; Error: integer); +var + TempError : integer; +begin + if (Error = 0) then + exit; + // Propagate Jarvis, Judice and Ninke error terms: + // ... ... (here) 8/38 4/38 + // 2/38 4/38 8/38 4/38 2/38 + // 1/38 2/38 4/38 2/38 1/38 + inc(Errors2[FDirection2], Error); // Error * 1 + inc(Errors2[-FDirection2], Error); // Error * 1 + + TempError := Error + Error; + inc(Error, TempError); + inc(Errors1[FDirection2], Error); // Error * 3 + inc(Errors1[-FDirection2], Error); // Error * 3 + inc(Errors2[Direction], Error); // Error * 3 + inc(Errors2[-Direction], Error); // Error * 3 + + inc(Error, TempError); + inc(Errors0[FDirection2], Error); // Error * 5 + inc(Errors1[-Direction], Error); // Error * 5 + inc(Errors1[Direction], Error); // Error * 5 + inc(Errors2[0], Error); // Error * 5 + + inc(Error, TempError); + inc(Errors0[Direction], Error); // Error * 7 + inc(Errors1[0], Error); // Error * 7 +end; +{$IFDEF R_PLUS} + {$RANGECHECKS ON} + {$UNDEF R_PLUS} +{$ENDIF} + +//////////////////////////////////////////////////////////////////////////////// +// TSteveArcheDitherer +constructor TSteveArcheDitherer.Create(AWidth: integer; Lookup: TColorLookup); +begin + inherited Create(AWidth, Lookup); + + GetMem(ErrorsR0, sizeof(TErrorTerm)*(Width+6)); + GetMem(ErrorsG0, sizeof(TErrorTerm)*(Width+6)); + GetMem(ErrorsB0, sizeof(TErrorTerm)*(Width+6)); + GetMem(ErrorsR1, sizeof(TErrorTerm)*(Width+6)); + GetMem(ErrorsG1, sizeof(TErrorTerm)*(Width+6)); + GetMem(ErrorsB1, sizeof(TErrorTerm)*(Width+6)); + GetMem(ErrorsR2, sizeof(TErrorTerm)*(Width+6)); + GetMem(ErrorsG2, sizeof(TErrorTerm)*(Width+6)); + GetMem(ErrorsB2, sizeof(TErrorTerm)*(Width+6)); + GetMem(ErrorsR3, sizeof(TErrorTerm)*(Width+6)); + GetMem(ErrorsG3, sizeof(TErrorTerm)*(Width+6)); + GetMem(ErrorsB3, sizeof(TErrorTerm)*(Width+6)); + FillChar(ErrorsR0^, sizeof(TErrorTerm)*(Width+6), 0); + FillChar(ErrorsG0^, sizeof(TErrorTerm)*(Width+6), 0); + FillChar(ErrorsB0^, sizeof(TErrorTerm)*(Width+6), 0); + FillChar(ErrorsR1^, sizeof(TErrorTerm)*(Width+6), 0); + FillChar(ErrorsG1^, sizeof(TErrorTerm)*(Width+6), 0); + FillChar(ErrorsB1^, sizeof(TErrorTerm)*(Width+6), 0); + FillChar(ErrorsR2^, sizeof(TErrorTerm)*(Width+6), 0); + FillChar(ErrorsG2^, sizeof(TErrorTerm)*(Width+6), 0); + FillChar(ErrorsB2^, sizeof(TErrorTerm)*(Width+6), 0); + FillChar(ErrorsR3^, sizeof(TErrorTerm)*(Width+6), 0); + FillChar(ErrorsG3^, sizeof(TErrorTerm)*(Width+6), 0); + FillChar(ErrorsB3^, sizeof(TErrorTerm)*(Width+6), 0); + + FDirection2 := 2 * Direction; + FDirection3 := 3 * Direction; + + ErrorR0 := PErrors(longInt(ErrorsR0)+3*sizeof(TErrorTerm)); + ErrorG0 := PErrors(longInt(ErrorsG0)+3*sizeof(TErrorTerm)); + ErrorB0 := PErrors(longInt(ErrorsB0)+3*sizeof(TErrorTerm)); + ErrorR1 := PErrors(longInt(ErrorsR1)+3*sizeof(TErrorTerm)); + ErrorG1 := PErrors(longInt(ErrorsG1)+3*sizeof(TErrorTerm)); + ErrorB1 := PErrors(longInt(ErrorsB1)+3*sizeof(TErrorTerm)); + ErrorR2 := PErrors(longInt(ErrorsR2)+3*sizeof(TErrorTerm)); + ErrorG2 := PErrors(longInt(ErrorsG2)+3*sizeof(TErrorTerm)); + ErrorB2 := PErrors(longInt(ErrorsB2)+3*sizeof(TErrorTerm)); + ErrorR3 := PErrors(longInt(ErrorsR3)+3*sizeof(TErrorTerm)); + ErrorG3 := PErrors(longInt(ErrorsG3)+3*sizeof(TErrorTerm)); + ErrorB3 := PErrors(longInt(ErrorsB3)+3*sizeof(TErrorTerm)); +end; + +destructor TSteveArcheDitherer.Destroy; +begin + FreeMem(ErrorsR0); + FreeMem(ErrorsG0); + FreeMem(ErrorsB0); + FreeMem(ErrorsR1); + FreeMem(ErrorsG1); + FreeMem(ErrorsB1); + FreeMem(ErrorsR2); + FreeMem(ErrorsG2); + FreeMem(ErrorsB2); + FreeMem(ErrorsR3); + FreeMem(ErrorsG3); + FreeMem(ErrorsB3); + inherited Destroy; +end; + +{$IFOPT R+} + {$DEFINE R_PLUS} + {$RANGECHECKS OFF} +{$ENDIF} +function TSteveArcheDitherer.Dither(Red, Green, Blue: BYTE; var R, G, B: BYTE): char; +var + ColorR , + ColorG , + ColorB : integer; // Error for current pixel + + // Propagate Stevenson & Arche error terms: + // ... ... ... (here) ... 32/200 ... + // 12/200 ... 26/200 ... 30/200 ... 16/200 + // ... 12/200 ... 26/200 ... 12/200 ... + // 5/200 ... 12/200 ... 12/200 ... 5/200 + procedure Propagate(Errors0, Errors1, Errors2, Errors3: PErrors; Error: integer); + var + TempError : integer; + begin + if (Error = 0) then + exit; + TempError := 5 * Error; + inc(Errors3[FDirection3], TempError); // Error * 5 + inc(Errors3[-FDirection3], TempError); // Error * 5 + + TempError := 12 * Error; + inc(Errors1[-FDirection3], TempError); // Error * 12 + inc(Errors2[-FDirection2], TempError); // Error * 12 + inc(Errors2[FDirection2], TempError); // Error * 12 + inc(Errors3[-Direction], TempError); // Error * 12 + inc(Errors3[Direction], TempError); // Error * 12 + + inc(Errors1[FDirection3], 16 * TempError); // Error * 16 + + TempError := 26 * Error; + inc(Errors1[-Direction], TempError); // Error * 26 + inc(Errors2[0], TempError); // Error * 26 + + inc(Errors1[Direction], 30 * Error); // Error * 30 + + inc(Errors0[FDirection2], 32 * Error); // Error * 32 + end; + +begin + // Apply red component error correction + ColorR := Red + (ErrorR0[0] + 100) DIV 200; + if (ColorR < 0) then + ColorR := 0 + else if (ColorR > 255) then + ColorR := 255; + + // Apply green component error correction + ColorG := Green + (ErrorG0[0] + 100) DIV 200; + if (ColorG < 0) then + ColorG := 0 + else if (ColorG > 255) then + ColorG := 255; + + // Apply blue component error correction + ColorB := Blue + (ErrorB0[0] + 100) DIV 200; + if (ColorB < 0) then + ColorB := 0 + else if (ColorB > 255) then + ColorB := 255; + + // Map color to palette + Result := inherited Dither(ColorR, ColorG, ColorB, R, G, B); + + // Propagate red component error + Propagate(ErrorR0, ErrorR1, ErrorR2, ErrorR3, ColorR - R); + // Propagate green component error + Propagate(ErrorG0, ErrorG1, ErrorG2, ErrorG3, ColorG - G); + // Propagate blue component error + Propagate(ErrorB0, ErrorB1, ErrorB2, ErrorB3, ColorB - B); + + // Move on to next column + if (Direction = 1) then + begin + inc(longInt(ErrorR0), sizeof(TErrorTerm)); + inc(longInt(ErrorG0), sizeof(TErrorTerm)); + inc(longInt(ErrorB0), sizeof(TErrorTerm)); + inc(longInt(ErrorR1), sizeof(TErrorTerm)); + inc(longInt(ErrorG1), sizeof(TErrorTerm)); + inc(longInt(ErrorB1), sizeof(TErrorTerm)); + inc(longInt(ErrorR2), sizeof(TErrorTerm)); + inc(longInt(ErrorG2), sizeof(TErrorTerm)); + inc(longInt(ErrorB2), sizeof(TErrorTerm)); + inc(longInt(ErrorR3), sizeof(TErrorTerm)); + inc(longInt(ErrorG3), sizeof(TErrorTerm)); + inc(longInt(ErrorB3), sizeof(TErrorTerm)); + end else + begin + dec(longInt(ErrorR0), sizeof(TErrorTerm)); + dec(longInt(ErrorG0), sizeof(TErrorTerm)); + dec(longInt(ErrorB0), sizeof(TErrorTerm)); + dec(longInt(ErrorR1), sizeof(TErrorTerm)); + dec(longInt(ErrorG1), sizeof(TErrorTerm)); + dec(longInt(ErrorB1), sizeof(TErrorTerm)); + dec(longInt(ErrorR2), sizeof(TErrorTerm)); + dec(longInt(ErrorG2), sizeof(TErrorTerm)); + dec(longInt(ErrorB2), sizeof(TErrorTerm)); + dec(longInt(ErrorR3), sizeof(TErrorTerm)); + dec(longInt(ErrorG3), sizeof(TErrorTerm)); + dec(longInt(ErrorB3), sizeof(TErrorTerm)); + end; +end; +{$IFDEF R_PLUS} + {$RANGECHECKS ON} + {$UNDEF R_PLUS} +{$ENDIF} + +{$IFOPT R+} + {$DEFINE R_PLUS} + {$RANGECHECKS OFF} +{$ENDIF} +procedure TSteveArcheDitherer.NextLine; +var + TempErrors : PErrors; +begin + FillChar(ErrorsR0^, sizeof(TErrorTerm)*(Width+6), 0); + FillChar(ErrorsG0^, sizeof(TErrorTerm)*(Width+6), 0); + FillChar(ErrorsB0^, sizeof(TErrorTerm)*(Width+6), 0); + + // Swap lines + TempErrors := ErrorsR0; + ErrorsR0 := ErrorsR1; + ErrorsR1 := ErrorsR2; + ErrorsR2 := ErrorsR3; + ErrorsR3 := TempErrors; + + TempErrors := ErrorsG0; + ErrorsG0 := ErrorsG1; + ErrorsG1 := ErrorsG2; + ErrorsG2 := ErrorsG3; + ErrorsG3 := TempErrors; + + TempErrors := ErrorsB0; + ErrorsB0 := ErrorsB1; + ErrorsB1 := ErrorsB2; + ErrorsB2 := ErrorsB3; + ErrorsB3 := TempErrors; + + inherited NextLine; + + FDirection2 := 2 * Direction; + FDirection3 := 3 * Direction; + + if (Direction = 1) then + begin + // ErrorsR0[1] gives compiler error, so we + // use PErrors(longInt(ErrorsR0)+sizeof(TErrorTerm)) instead... + ErrorR0 := PErrors(longInt(ErrorsR0)+3*sizeof(TErrorTerm)); + ErrorG0 := PErrors(longInt(ErrorsG0)+3*sizeof(TErrorTerm)); + ErrorB0 := PErrors(longInt(ErrorsB0)+3*sizeof(TErrorTerm)); + ErrorR1 := PErrors(longInt(ErrorsR1)+3*sizeof(TErrorTerm)); + ErrorG1 := PErrors(longInt(ErrorsG1)+3*sizeof(TErrorTerm)); + ErrorB1 := PErrors(longInt(ErrorsB1)+3*sizeof(TErrorTerm)); + ErrorR2 := PErrors(longInt(ErrorsR2)+3*sizeof(TErrorTerm)); + ErrorG2 := PErrors(longInt(ErrorsG2)+3*sizeof(TErrorTerm)); + ErrorB2 := PErrors(longInt(ErrorsB2)+3*sizeof(TErrorTerm)); + ErrorR3 := PErrors(longInt(ErrorsR3)+3*sizeof(TErrorTerm)); + ErrorG3 := PErrors(longInt(ErrorsG3)+3*sizeof(TErrorTerm)); + ErrorB3 := PErrors(longInt(ErrorsB3)+3*sizeof(TErrorTerm)); + end else + begin + ErrorR0 := @ErrorsR0[Width+2]; + ErrorG0 := @ErrorsG0[Width+2]; + ErrorB0 := @ErrorsB0[Width+2]; + ErrorR1 := @ErrorsR1[Width+2]; + ErrorG1 := @ErrorsG1[Width+2]; + ErrorB1 := @ErrorsB1[Width+2]; + ErrorR2 := @ErrorsR2[Width+2]; + ErrorG2 := @ErrorsG2[Width+2]; + ErrorB2 := @ErrorsB2[Width+2]; + ErrorR3 := @ErrorsR2[Width+2]; + ErrorG3 := @ErrorsG2[Width+2]; + ErrorB3 := @ErrorsB2[Width+2]; + end; +end; +{$IFDEF R_PLUS} + {$RANGECHECKS ON} + {$UNDEF R_PLUS} +{$ENDIF} + +//////////////////////////////////////////////////////////////////////////////// +// TBurkesDitherer +constructor TBurkesDitherer.Create(AWidth: integer; Lookup: TColorLookup); +begin + inherited Create(AWidth, Lookup); + + GetMem(ErrorsR0, sizeof(TErrorTerm)*(Width+4)); + GetMem(ErrorsG0, sizeof(TErrorTerm)*(Width+4)); + GetMem(ErrorsB0, sizeof(TErrorTerm)*(Width+4)); + GetMem(ErrorsR1, sizeof(TErrorTerm)*(Width+4)); + GetMem(ErrorsG1, sizeof(TErrorTerm)*(Width+4)); + GetMem(ErrorsB1, sizeof(TErrorTerm)*(Width+4)); + FillChar(ErrorsR0^, sizeof(TErrorTerm)*(Width+4), 0); + FillChar(ErrorsG0^, sizeof(TErrorTerm)*(Width+4), 0); + FillChar(ErrorsB0^, sizeof(TErrorTerm)*(Width+4), 0); + FillChar(ErrorsR1^, sizeof(TErrorTerm)*(Width+4), 0); + FillChar(ErrorsG1^, sizeof(TErrorTerm)*(Width+4), 0); + FillChar(ErrorsB1^, sizeof(TErrorTerm)*(Width+4), 0); + + FDirection2 := 2 * Direction; + ErrorR0 := PErrors(longInt(ErrorsR0)+2*sizeof(TErrorTerm)); + ErrorG0 := PErrors(longInt(ErrorsG0)+2*sizeof(TErrorTerm)); + ErrorB0 := PErrors(longInt(ErrorsB0)+2*sizeof(TErrorTerm)); + ErrorR1 := PErrors(longInt(ErrorsR1)+2*sizeof(TErrorTerm)); + ErrorG1 := PErrors(longInt(ErrorsG1)+2*sizeof(TErrorTerm)); + ErrorB1 := PErrors(longInt(ErrorsB1)+2*sizeof(TErrorTerm)); +end; + +destructor TBurkesDitherer.Destroy; +begin + FreeMem(ErrorsR0); + FreeMem(ErrorsG0); + FreeMem(ErrorsB0); + FreeMem(ErrorsR1); + FreeMem(ErrorsG1); + FreeMem(ErrorsB1); + inherited Destroy; +end; + +{$IFOPT R+} + {$DEFINE R_PLUS} + {$RANGECHECKS OFF} +{$ENDIF} +function TBurkesDitherer.Dither(Red, Green, Blue: BYTE; var R, G, B: BYTE): char; +var + ErrorR , + ErrorG , + ErrorB : integer; // Error for current pixel + + // Propagate Burkes error terms: + // ... ... (here) 8/32 4/32 + // 2/32 4/32 8/32 4/32 2/32 + procedure Propagate(Errors0, Errors1: PErrors; Error: integer); + begin + if (Error = 0) then + exit; + inc(Error, Error); + inc(Errors1[FDirection2], Error); // Error * 2 + inc(Errors1[-FDirection2], Error); // Error * 2 + + inc(Error, Error); + inc(Errors0[FDirection2], Error); // Error * 4 + inc(Errors1[-Direction], Error); // Error * 4 + inc(Errors1[Direction], Error); // Error * 4 + + inc(Error, Error); + inc(Errors0[Direction], Error); // Error * 8 + inc(Errors1[0], Error); // Error * 8 + end; + +begin + // Apply red component error correction + ErrorR := Red + (ErrorR0[0] + 16) DIV 32; + if (ErrorR < 0) then + ErrorR := 0 + else if (ErrorR > 255) then + ErrorR := 255; + + // Apply green component error correction + ErrorG := Green + (ErrorG0[0] + 16) DIV 32; + if (ErrorG < 0) then + ErrorG := 0 + else if (ErrorG > 255) then + ErrorG := 255; + + // Apply blue component error correction + ErrorB := Blue + (ErrorB0[0] + 16) DIV 32; + if (ErrorB < 0) then + ErrorB := 0 + else if (ErrorB > 255) then + ErrorB := 255; + + // Map color to palette + Result := inherited Dither(ErrorR, ErrorG, ErrorB, R, G, B); + + // Propagate red component error + Propagate(ErrorR0, ErrorR1, ErrorR - R); + // Propagate green component error + Propagate(ErrorG0, ErrorG1, ErrorG - G); + // Propagate blue component error + Propagate(ErrorB0, ErrorB1, ErrorB - B); + + // Move on to next column + if (Direction = 1) then + begin + inc(longInt(ErrorR0), sizeof(TErrorTerm)); + inc(longInt(ErrorG0), sizeof(TErrorTerm)); + inc(longInt(ErrorB0), sizeof(TErrorTerm)); + inc(longInt(ErrorR1), sizeof(TErrorTerm)); + inc(longInt(ErrorG1), sizeof(TErrorTerm)); + inc(longInt(ErrorB1), sizeof(TErrorTerm)); + end else + begin + dec(longInt(ErrorR0), sizeof(TErrorTerm)); + dec(longInt(ErrorG0), sizeof(TErrorTerm)); + dec(longInt(ErrorB0), sizeof(TErrorTerm)); + dec(longInt(ErrorR1), sizeof(TErrorTerm)); + dec(longInt(ErrorG1), sizeof(TErrorTerm)); + dec(longInt(ErrorB1), sizeof(TErrorTerm)); + end; +end; +{$IFDEF R_PLUS} + {$RANGECHECKS ON} + {$UNDEF R_PLUS} +{$ENDIF} + +{$IFOPT R+} + {$DEFINE R_PLUS} + {$RANGECHECKS OFF} +{$ENDIF} +procedure TBurkesDitherer.NextLine; +var + TempErrors : PErrors; +begin + FillChar(ErrorsR0^, sizeof(TErrorTerm)*(Width+4), 0); + FillChar(ErrorsG0^, sizeof(TErrorTerm)*(Width+4), 0); + FillChar(ErrorsB0^, sizeof(TErrorTerm)*(Width+4), 0); + + // Swap lines + TempErrors := ErrorsR0; + ErrorsR0 := ErrorsR1; + ErrorsR1 := TempErrors; + + TempErrors := ErrorsG0; + ErrorsG0 := ErrorsG1; + ErrorsG1 := TempErrors; + + TempErrors := ErrorsB0; + ErrorsB0 := ErrorsB1; + ErrorsB1 := TempErrors; + + inherited NextLine; + + FDirection2 := 2 * Direction; + if (Direction = 1) then + begin + // ErrorsR0[1] gives compiler error, so we + // use PErrors(longInt(ErrorsR0)+sizeof(TErrorTerm)) instead... + ErrorR0 := PErrors(longInt(ErrorsR0)+2*sizeof(TErrorTerm)); + ErrorG0 := PErrors(longInt(ErrorsG0)+2*sizeof(TErrorTerm)); + ErrorB0 := PErrors(longInt(ErrorsB0)+2*sizeof(TErrorTerm)); + ErrorR1 := PErrors(longInt(ErrorsR1)+2*sizeof(TErrorTerm)); + ErrorG1 := PErrors(longInt(ErrorsG1)+2*sizeof(TErrorTerm)); + ErrorB1 := PErrors(longInt(ErrorsB1)+2*sizeof(TErrorTerm)); + end else + begin + ErrorR0 := @ErrorsR0[Width+1]; + ErrorG0 := @ErrorsG0[Width+1]; + ErrorB0 := @ErrorsB0[Width+1]; + ErrorR1 := @ErrorsR1[Width+1]; + ErrorG1 := @ErrorsG1[Width+1]; + ErrorB1 := @ErrorsB1[Width+1]; + end; +end; +{$IFDEF R_PLUS} + {$RANGECHECKS ON} + {$UNDEF R_PLUS} +{$ENDIF} + +//////////////////////////////////////////////////////////////////////////////// +// +// Octree Color Quantization Engine +// +//////////////////////////////////////////////////////////////////////////////// +// Adapted from Earl F. Glynn's ColorQuantizationLibrary, March 1998 +//////////////////////////////////////////////////////////////////////////////// +type + TOctreeNode = class; // Forward definition so TReducibleNodes can be declared + + TReducibleNodes = array[0..7] of TOctreeNode; + + TOctreeNode = Class(TObject) + public + IsLeaf : Boolean; + PixelCount : integer; + RedSum : integer; + GreenSum : integer; + BlueSum : integer; + Next : TOctreeNode; + Child : TReducibleNodes; + + constructor Create(Level: integer; ColorBits: integer; var LeafCount: integer; + var ReducibleNodes: TReducibleNodes); + destructor Destroy; override; + end; + + TColorQuantizer = class(TObject) + private + FTree : TOctreeNode; + FLeafCount : integer; + FReducibleNodes : TReducibleNodes; + FMaxColors : integer; + FColorBits : integer; + + protected + procedure AddColor(var Node: TOctreeNode; r, g, b: byte; ColorBits: integer; + Level: integer; var LeafCount: integer; var ReducibleNodes: TReducibleNodes); + procedure DeleteTree(var Node: TOctreeNode); + procedure GetPaletteColors(const Node: TOctreeNode; + var RGBQuadArray: TRGBQuadArray; var Index: integer); + procedure ReduceTree(ColorBits: integer; var LeafCount: integer; + var ReducibleNodes: TReducibleNodes); + + public + constructor Create(MaxColors: integer; ColorBits: integer); + destructor Destroy; override; + + procedure GetColorTable(var RGBQuadArray: TRGBQuadArray); + function ProcessImage(const DIB: TDIBReader): boolean; + + property ColorCount: integer read FLeafCount; + end; + +constructor TOctreeNode.Create(Level: integer; ColorBits: integer; + var LeafCount: integer; var ReducibleNodes: TReducibleNodes); +var + i : integer; +begin + PixelCount := 0; + RedSum := 0; + GreenSum := 0; + BlueSum := 0; + for i := Low(Child) to High(Child) do + Child[i] := nil; + + IsLeaf := (Level = ColorBits); + if (IsLeaf) then + begin + Next := nil; + inc(LeafCount); + end else + begin + Next := ReducibleNodes[Level]; + ReducibleNodes[Level] := self; + end; +end; + +destructor TOctreeNode.Destroy; +var + i : integer; +begin + for i := High(Child) downto Low(Child) do + Child[i].Free; +end; + +constructor TColorQuantizer.Create(MaxColors: integer; ColorBits: integer); +var + i : integer; +begin + ASSERT(ColorBits <= 8, 'ColorBits must be 8 or less'); + + FTree := nil; + FLeafCount := 0; + + // Initialize all nodes even though only ColorBits+1 of them are needed + for i := Low(FReducibleNodes) to High(FReducibleNodes) do + FReducibleNodes[i] := nil; + + FMaxColors := MaxColors; + FColorBits := ColorBits; +end; + +destructor TColorQuantizer.Destroy; +begin + if (FTree <> nil) then + DeleteTree(FTree); +end; + +procedure TColorQuantizer.GetColorTable(var RGBQuadArray: TRGBQuadArray); +var + Index : integer; +begin + Index := 0; + GetPaletteColors(FTree, RGBQuadArray, Index); +end; + +// Handles passed to ProcessImage should refer to DIB sections, not DDBs. +// In certain cases, specifically when it's called upon to process 1, 4, or +// 8-bit per pixel images on systems with palettized display adapters, +// ProcessImage can produce incorrect results if it's passed a handle to a +// DDB. +function TColorQuantizer.ProcessImage(const DIB: TDIBReader): boolean; +var + i , + j : integer; + ScanLine : pointer; + Pixel : PRGBTriple; +begin + Result := True; + + for j := 0 to DIB.Bitmap.Height-1 do + begin + Scanline := DIB.Scanline[j]; + Pixel := ScanLine; + for i := 0 to DIB.Bitmap.Width-1 do + begin + with Pixel^ do + AddColor(FTree, rgbtRed, rgbtGreen, rgbtBlue, + FColorBits, 0, FLeafCount, FReducibleNodes); + + while FLeafCount > FMaxColors do + ReduceTree(FColorbits, FLeafCount, FReducibleNodes); + inc(Pixel); + end; + end; +end; + +procedure TColorQuantizer.AddColor(var Node: TOctreeNode; r,g,b: byte; + ColorBits: integer; Level: integer; var LeafCount: integer; + var ReducibleNodes: TReducibleNodes); +const + Mask: array[0..7] of BYTE = ($80, $40, $20, $10, $08, $04, $02, $01); +var + Index : integer; + Shift : integer; +begin + // If the node doesn't exist, create it. + if (Node = nil) then + Node := TOctreeNode.Create(Level, ColorBits, LeafCount, ReducibleNodes); + + if (Node.IsLeaf) then + begin + inc(Node.PixelCount); + inc(Node.RedSum, r); + inc(Node.GreenSum, g); + inc(Node.BlueSum, b); + end else + begin + // Recurse a level deeper if the node is not a leaf. + Shift := 7 - Level; + + Index := (((r and mask[Level]) SHR Shift) SHL 2) or + (((g and mask[Level]) SHR Shift) SHL 1) or + ((b and mask[Level]) SHR Shift); + AddColor(Node.Child[Index], r, g, b, ColorBits, Level+1, LeafCount, ReducibleNodes); + end; +end; + +procedure TColorQuantizer.DeleteTree(var Node: TOctreeNode); +var + i : integer; +begin + for i := High(TReducibleNodes) downto Low(TReducibleNodes) do + if (Node.Child[i] <> nil) then + DeleteTree(Node.Child[i]); + + Node.Free; + Node := nil; +end; + +procedure TColorQuantizer.GetPaletteColors(const Node: TOctreeNode; + var RGBQuadArray: TRGBQuadArray; var Index: integer); +var + i : integer; +begin + if (Node.IsLeaf) then + begin + with RGBQuadArray[Index] do + begin + if (Node.PixelCount <> 0) then + begin + rgbRed := BYTE(Node.RedSum DIV Node.PixelCount); + rgbGreen := BYTE(Node.GreenSum DIV Node.PixelCount); + rgbBlue := BYTE(Node.BlueSum DIV Node.PixelCount); + end else + begin + rgbRed := 0; + rgbGreen := 0; + rgbBlue := 0; + end; + rgbReserved := 0; + end; + inc(Index); + end else + begin + for i := Low(Node.Child) to High(Node.Child) do + if (Node.Child[i] <> nil) then + GetPaletteColors(Node.Child[i], RGBQuadArray, Index); + end; +end; + +procedure TColorQuantizer.ReduceTree(ColorBits: integer; var LeafCount: integer; + var ReducibleNodes: TReducibleNodes); +var + RedSum , + GreenSum , + BlueSum : integer; + Children : integer; + i : integer; + Node : TOctreeNode; +begin + // Find the deepest level containing at least one reducible node + i := Colorbits - 1; + while (i > 0) and (ReducibleNodes[i] = nil) do + dec(i); + + // Reduce the node most recently added to the list at level i. + Node := ReducibleNodes[i]; + ReducibleNodes[i] := Node.Next; + + RedSum := 0; + GreenSum := 0; + BlueSum := 0; + Children := 0; + + for i := Low(ReducibleNodes) to High(ReducibleNodes) do + if (Node.Child[i] <> nil) then + begin + inc(RedSum, Node.Child[i].RedSum); + inc(GreenSum, Node.Child[i].GreenSum); + inc(BlueSum, Node.Child[i].BlueSum); + inc(Node.PixelCount, Node.Child[i].PixelCount); + Node.Child[i].Free; + Node.Child[i] := nil; + inc(Children); + end; + + Node.IsLeaf := TRUE; + Node.RedSum := RedSum; + Node.GreenSum := GreenSum; + Node.BlueSum := BlueSum; + dec(LeafCount, Children-1); +end; + +//////////////////////////////////////////////////////////////////////////////// +// +// Octree Color Quantization Wrapper +// +//////////////////////////////////////////////////////////////////////////////// +// Adapted from Earl F. Glynn's PaletteLibrary, March 1998 +//////////////////////////////////////////////////////////////////////////////// + +// Wrapper for internal use - uses TDIBReader for bitmap access +function doCreateOptimizedPaletteFromSingleBitmap(const DIB: TDIBReader; + Colors, ColorBits: integer; Windows: boolean): hPalette; +var + SystemPalette : HPalette; + ColorQuantizer : TColorQuantizer; + i : integer; + LogicalPalette : TMaxLogPalette; + RGBQuadArray : TRGBQuadArray; + Offset : integer; +begin + LogicalPalette.palVersion := $0300; + LogicalPalette.palNumEntries := Colors; + + if (Windows) then + begin + // Get the windows 20 color system palette + SystemPalette := GetStockObject(DEFAULT_PALETTE); + GetPaletteEntries(SystemPalette, 0, 10, LogicalPalette.palPalEntry[0]); + GetPaletteEntries(SystemPalette, 10, 10, LogicalPalette.palPalEntry[245]); + Colors := 236; + Offset := 10; + LogicalPalette.palNumEntries := 256; + end else + Offset := 0; + + // Normally for 24-bit images, use ColorBits of 5 or 6. For 8-bit images + // use ColorBits = 8. + ColorQuantizer := TColorQuantizer.Create(Colors, ColorBits); + try + ColorQuantizer.ProcessImage(DIB); + ColorQuantizer.GetColorTable(RGBQuadArray); + finally + ColorQuantizer.Free; + end; + + for i := 0 to Colors-1 do + with LogicalPalette.palPalEntry[i+Offset] do + begin + peRed := RGBQuadArray[i].rgbRed; + peGreen := RGBQuadArray[i].rgbGreen; + peBlue := RGBQuadArray[i].rgbBlue; + peFlags := RGBQuadArray[i].rgbReserved; + end; + Result := CreatePalette(pLogPalette(@LogicalPalette)^); +end; + +function CreateOptimizedPaletteFromSingleBitmap(const Bitmap: TBitmap; + Colors, ColorBits: integer; Windows: boolean): hPalette; +var + DIB : TDIBReader; +begin + DIB := TDIBReader.Create(Bitmap, pf24bit); + try + Result := doCreateOptimizedPaletteFromSingleBitmap(DIB, Colors, ColorBits, Windows); + finally + DIB.Free; + end; +end; + +function CreateOptimizedPaletteFromManyBitmaps(Bitmaps: TList; Colors, ColorBits: integer; + Windows: boolean): hPalette; +var + SystemPalette : HPalette; + ColorQuantizer : TColorQuantizer; + i : integer; + LogicalPalette : TMaxLogPalette; + RGBQuadArray : TRGBQuadArray; + Offset : integer; + DIB : TDIBReader; +begin + if (Bitmaps = nil) or (Bitmaps.Count = 0) then + Error(sInvalidBitmapList); + + LogicalPalette.palVersion := $0300; + LogicalPalette.palNumEntries := Colors; + + if (Windows) then + begin + // Get the windows 20 color system palette + SystemPalette := GetStockObject(DEFAULT_PALETTE); + GetPaletteEntries(SystemPalette, 0, 10, LogicalPalette.palPalEntry[0]); + GetPaletteEntries(SystemPalette, 10, 10, LogicalPalette.palPalEntry[245]); + Colors := 236; + Offset := 10; + LogicalPalette.palNumEntries := 256; + end else + Offset := 0; + + // Normally for 24-bit images, use ColorBits of 5 or 6. For 8-bit images + // use ColorBits = 8. + ColorQuantizer := TColorQuantizer.Create(Colors, ColorBits); + try + for i := 0 to Bitmaps.Count-1 do + begin + DIB := TDIBReader.Create(TBitmap(Bitmaps[i]), pf24bit); + try + ColorQuantizer.ProcessImage(DIB); + finally + DIB.Free; + end; + end; + ColorQuantizer.GetColorTable(RGBQuadArray); + finally + ColorQuantizer.Free; + end; + + for i := 0 to Colors-1 do + with LogicalPalette.palPalEntry[i+Offset] do + begin + peRed := RGBQuadArray[i].rgbRed; + peGreen := RGBQuadArray[i].rgbGreen; + peBlue := RGBQuadArray[i].rgbBlue; + peFlags := RGBQuadArray[i].rgbReserved; + end; + Result := CreatePalette(pLogPalette(@LogicalPalette)^); +end; + +//////////////////////////////////////////////////////////////////////////////// +// +// Color reduction +// +//////////////////////////////////////////////////////////////////////////////// +{$IFOPT R+} + {$DEFINE R_PLUS} + {$RANGECHECKS OFF} +{$ENDIF} +//: Reduces the color depth of a bitmap using color quantization and dithering. +function ReduceColors(Bitmap: TBitmap; ColorReduction: TColorReduction; + DitherMode: TDitherMode; ReductionBits: integer; CustomPalette: hPalette): TBitmap; +var + Palette : hPalette; + ColorLookup : TColorLookup; + Ditherer : TDitherEngine; + Row : Integer; + DIBResult : TDIBWriter; + DIBSource : TDIBReader; + SrcScanLine , + Src : PRGBTriple; + DstScanLine , + Dst : PChar; + BGR : TRGBTriple; +{$ifdef DEBUG_DITHERPERFORMANCE} + TimeStart , + TimeStop : DWORD; +{$endif} + + function GrayScalePalette: hPalette; + var + i : integer; + Pal : TMaxLogPalette; + begin + Pal.palVersion := $0300; + Pal.palNumEntries := 256; + for i := 0 to 255 do + begin + with (Pal.palPalEntry[i]) do + begin + peRed := i; + peGreen := i; + peBlue := i; + peFlags := PC_NOCOLLAPSE; + end; + end; + Result := CreatePalette(pLogPalette(@Pal)^); + end; + + function MonochromePalette: hPalette; + var + i : integer; + Pal : TMaxLogPalette; + const + Values : array[0..1] of byte + = (0, 255); + begin + Pal.palVersion := $0300; + Pal.palNumEntries := 2; + for i := 0 to 1 do + begin + with (Pal.palPalEntry[i]) do + begin + peRed := Values[i]; + peGreen := Values[i]; + peBlue := Values[i]; + peFlags := PC_NOCOLLAPSE; + end; + end; + Result := CreatePalette(pLogPalette(@Pal)^); + end; + + function WindowsGrayScalePalette: hPalette; + var + i : integer; + Pal : TMaxLogPalette; + const + Values : array[0..3] of byte + = (0, 128, 192, 255); + begin + Pal.palVersion := $0300; + Pal.palNumEntries := 4; + for i := 0 to 3 do + begin + with (Pal.palPalEntry[i]) do + begin + peRed := Values[i]; + peGreen := Values[i]; + peBlue := Values[i]; + peFlags := PC_NOCOLLAPSE; + end; + end; + Result := CreatePalette(pLogPalette(@Pal)^); + end; + + function WindowsHalftonePalette: hPalette; + var + DC : HDC; + begin + DC := GDICheck(GetDC(0)); + try + Result := CreateHalfTonePalette(DC); + finally + ReleaseDC(0, DC); + end; + end; + +begin +{$ifdef DEBUG_DITHERPERFORMANCE} + timeBeginPeriod(5); + TimeStart := timeGetTime; +{$endif} + + Result := TBitmap.Create; + try + + if (ColorReduction = rmNone) then + begin + Result.Assign(Bitmap); +{$ifndef VER9x} + SetPixelFormat(Result, pf24bit); +{$endif} + exit; + end; + +{$IFNDEF VER9x} + if (Bitmap.Width*Bitmap.Height > BitmapAllocationThreshold) then + SetPixelFormat(Result, pf1bit); // To reduce resource consumption of resize +{$ENDIF} + + ColorLookup := nil; + Ditherer := nil; + DIBResult := nil; + DIBSource := nil; + Palette := 0; + try // Protect above resources + + // Dithering and color mapper only supports 24 bit bitmaps, + // so we have convert the source bitmap to the appropiate format. + DIBSource := TDIBReader.Create(Bitmap, pf24bit); + + // Create a palette based on current options + case (ColorReduction) of + rmQuantize: + Palette := doCreateOptimizedPaletteFromSingleBitmap(DIBSource, 1 SHL ReductionBits, 8, False); + rmQuantizeWindows: + Palette := CreateOptimizedPaletteFromSingleBitmap(Bitmap, 256, 8, True); + rmNetscape: + Palette := WebPalette; + rmGrayScale: + Palette := GrayScalePalette; + rmMonochrome: + Palette := MonochromePalette; + rmWindowsGray: + Palette := WindowsGrayScalePalette; + rmWindows20: + Palette := GetStockObject(DEFAULT_PALETTE); + rmWindows256: + Palette := WindowsHalftonePalette; + rmPalette: + Palette := CopyPalette(CustomPalette); + else + exit; + end; + + { TODO -oanme -cImprovement : Gray scale conversion should be done prior to dithering/mapping. Otherwise corrected values will be converted multiple times. } + + // Create a color mapper based on current options + case (ColorReduction) of + // For some strange reason my fast and dirty color lookup + // is more precise that Windows GetNearestPaletteIndex... + // rmWindows20: + // ColorLookup := TSlowColorLookup.Create(Palette); + // rmWindowsGray: + // ColorLookup := TGrayWindowsLookup.Create(Palette); + rmQuantize: + ColorLookup := TFastColorLookup.Create(Palette); + rmNetscape: + ColorLookup := TNetscapeColorLookup.Create(Palette); + rmGrayScale: + ColorLookup := TGrayScaleLookup.Create(Palette); + rmMonochrome: + ColorLookup := TMonochromeLookup.Create(Palette); + else + ColorLookup := TFastColorLookup.Create(Palette); + end; + + // Nothing to do if palette doesn't contain any colors + if (ColorLookup.Colors = 0) then + exit; + + // Create a ditherer based on current options + case (DitherMode) of + dmNearest: + Ditherer := TDitherEngine.Create(Bitmap.Width, ColorLookup); + dmFloydSteinberg: + Ditherer := TFloydSteinbergDitherer.Create(Bitmap.Width, ColorLookup); + dmStucki: + Ditherer := TStuckiDitherer.Create(Bitmap.Width, ColorLookup); + dmSierra: + Ditherer := TSierraDitherer.Create(Bitmap.Width, ColorLookup); + dmJaJuNI: + Ditherer := TJaJuNIDitherer.Create(Bitmap.Width, ColorLookup); + dmSteveArche: + Ditherer := TSteveArcheDitherer.Create(Bitmap.Width, ColorLookup); + dmBurkes: + Ditherer := TBurkesDitherer.Create(Bitmap.Width, ColorLookup); + else + exit; + end; + + // The processed bitmap is returned in pf8bit format + DIBResult := TDIBWriter.Create(Result, pf8bit, Bitmap.Width, Bitmap.Height, + Palette); + + // Process the image + Row := 0; + while (Row < Bitmap.Height) do + begin + SrcScanline := DIBSource.ScanLine[Row]; + DstScanline := DIBResult.ScanLine[Row]; + Src := pointer(longInt(SrcScanLine) + Ditherer.Column*sizeof(TRGBTriple)); + Dst := pointer(longInt(DstScanLine) + Ditherer.Column); + + while (Ditherer.Column < Ditherer.Width) and (Ditherer.Column >= 0) do + begin + BGR := Src^; + // Dither and map a single pixel + Dst^ := Ditherer.Dither(BGR.rgbtRed, BGR.rgbtGreen, BGR.rgbtBlue, + BGR.rgbtRed, BGR.rgbtGreen, BGR.rgbtBlue); + + inc(Src, Ditherer.Direction); + inc(Dst, Ditherer.Direction); + end; + + Inc(Row); + Ditherer.NextLine; + end; + finally + if (ColorLookup <> nil) then + ColorLookup.Free; + if (Ditherer <> nil) then + Ditherer.Free; + if (DIBResult <> nil) then + DIBResult.Free; + if (DIBSource <> nil) then + DIBSource.Free; + // Must delete palette after TDIBWriter since TDIBWriter uses palette + if (Palette <> 0) then + DeleteObject(Palette); + end; + except + Result.Free; + raise; + end; + +{$ifdef DEBUG_DITHERPERFORMANCE} + TimeStop := timeGetTime; + ShowMessage(format('Dithered %d pixels in %d mS, Rate %d pixels/mS (%d pixels/S)', + [Bitmap.Height*Bitmap.Width, TimeStop-TimeStart, + MulDiv(Bitmap.Height, Bitmap.Width, TimeStop-TimeStart+1), + MulDiv(Bitmap.Height, Bitmap.Width * 1000, TimeStop-TimeStart+1)])); + timeEndPeriod(5); +{$endif} +end; +{$IFDEF R_PLUS} + {$RANGECHECKS ON} + {$UNDEF R_PLUS} +{$ENDIF} + +//////////////////////////////////////////////////////////////////////////////// +// +// TGIFColorMap +// +//////////////////////////////////////////////////////////////////////////////// +const + InitColorMapSize = 16; + DeltaColorMapSize = 32; + +//: Creates an instance of a TGIFColorMap object. +constructor TGIFColorMap.Create; +begin + inherited Create; + FColorMap := nil; + FCapacity := 0; + FCount := 0; + FOptimized := False; +end; + +//: Destroys an instance of a TGIFColorMap object. +destructor TGIFColorMap.Destroy; +begin + Clear; + Changed; + inherited Destroy; +end; + +//: Empties the color map. +procedure TGIFColorMap.Clear; +begin + if (FColorMap <> nil) then + FreeMem(FColorMap); + FColorMap := nil; + FCapacity := 0; + FCount := 0; + FOptimized := False; +end; + +//: Converts a Windows color value to a RGB value. +class function TGIFColorMap.Color2RGB(Color: TColor): TGIFColor; +begin + Result.Blue := (Color shr 16) and $FF; + Result.Green := (Color shr 8) and $FF; + Result.Red := Color and $FF; +end; + +//: Converts a RGB value to a Windows color value. +class function TGIFColorMap.RGB2Color(Color: TGIFColor): TColor; +begin + Result := (Color.Blue SHL 16) OR (Color.Green SHL 8) OR Color.Red; +end; + +//: Saves the color map to a stream. +procedure TGIFColorMap.SaveToStream(Stream: TStream); +var + Dummies : integer; + Dummy : TGIFColor; +begin + if (FCount = 0) then + exit; + Stream.WriteBuffer(FColorMap^, FCount*sizeof(TGIFColor)); + Dummies := (1 SHL BitsPerPixel)-FCount; + Dummy.Red := 0; + Dummy.Green := 0; + Dummy.Blue := 0; + while (Dummies > 0) do + begin + Stream.WriteBuffer(Dummy, sizeof(TGIFColor)); + dec(Dummies); + end; +end; + +//: Loads the color map from a stream. +procedure TGIFColorMap.LoadFromStream(Stream: TStream; Count: integer); +begin + Clear; + SetCapacity(Count); + ReadCheck(Stream, FColorMap^, Count*sizeof(TGIFColor)); + FCount := Count; +end; + +//: Returns the position of a color in the color map. +function TGIFColorMap.IndexOf(Color: TColor): integer; +var + RGB : TGIFColor; +begin + RGB := Color2RGB(Color); + if (FOptimized) then + begin + // Optimized palette has most frequently occuring entries first + Result := 0; + // Reverse search to (hopefully) check latest colors first + while (Result < FCount) do + with (FColorMap^[Result]) do + begin + if (RGB.Red = Red) and (RGB.Green = Green) and (RGB.Blue = Blue) then + exit; + Inc(Result); + end; + Result := -1; + end else + begin + Result := FCount-1; + // Reverse search to (hopefully) check latest colors first + while (Result >= 0) do + with (FColorMap^[Result]) do + begin + if (RGB.Red = Red) and (RGB.Green = Green) and (RGB.Blue = Blue) then + exit; + Dec(Result); + end; + end; +end; + +procedure TGIFColorMap.SetCapacity(Size: integer); +begin + if (Size >= FCapacity) then + begin + if (Size <= InitColorMapSize) then + FCapacity := InitColorMapSize + else + FCapacity := (Size + DeltaColorMapSize - 1) DIV DeltaColorMapSize * DeltaColorMapSize; + if (FCapacity > GIFMaxColors) then + FCapacity := GIFMaxColors; + ReallocMem(FColorMap, FCapacity * sizeof(TGIFColor)); + end; +end; + +//: Imports a Windows palette into the color map. +procedure TGIFColorMap.ImportPalette(Palette: HPalette); +type + PalArray = array[byte] of TPaletteEntry; +var + Pal : PalArray; + NewCount : integer; + i : integer; +begin + Clear; + NewCount := GetPaletteEntries(Palette, 0, 256, pal); + if (NewCount = 0) then + exit; + SetCapacity(NewCount); + for i := 0 to NewCount-1 do + with FColorMap[i], Pal[i] do + begin + Red := peRed; + Green := peGreen; + Blue := peBlue; + end; + FCount := NewCount; + Changed; +end; + +//: Imports a color map structure into the color map. +procedure TGIFColorMap.ImportColorMap(Map: TColorMap; Count: integer); +begin + Clear; + if (Count = 0) then + exit; + SetCapacity(Count); + FCount := Count; + + System.Move(Map, FColorMap^, FCount * sizeof(TGIFColor)); + + Changed; +end; + +//: Imports a Windows palette structure into the color map. +procedure TGIFColorMap.ImportColorTable(Pal: pointer; Count: integer); +var + i : integer; +begin + Clear; + if (Count = 0) then + exit; + SetCapacity(Count); + for i := 0 to Count-1 do + with FColorMap[i], PRGBQuadArray(Pal)[i] do + begin + Red := rgbRed; + Green := rgbGreen; + Blue := rgbBlue; + end; + FCount := Count; + Changed; +end; + +//: Imports the color table of a DIB into the color map. +procedure TGIFColorMap.ImportDIBColors(Handle: HDC); +var + Pal : Pointer; + NewCount : integer; +begin + Clear; + GetMem(Pal, sizeof(TRGBQuad) * 256); + try + NewCount := GetDIBColorTable(Handle, 0, 256, Pal^); + ImportColorTable(Pal, NewCount); + finally + FreeMem(Pal); + end; + Changed; +end; + +//: Creates a Windows palette from the color map. +function TGIFColorMap.ExportPalette: HPalette; +var + Pal : TMaxLogPalette; + i : Integer; +begin + if (Count = 0) then + begin + Result := 0; + exit; + end; + Pal.palVersion := $300; + Pal.palNumEntries := Count; + for i := 0 to Count-1 do + with FColorMap[i], Pal.palPalEntry[i] do + begin + peRed := Red; + peGreen := Green; + peBlue := Blue; + peFlags := PC_NOCOLLAPSE; { TODO -oanme -cImprovement : Verify that PC_NOCOLLAPSE is the correct value to use. } + end; + Result := CreatePalette(PLogPalette(@Pal)^); +end; + +//: Adds a color to the color map. +function TGIFColorMap.Add(Color: TColor): integer; +begin + if (FCount >= GIFMaxColors) then + // Color map full + Error(sTooManyColors); + + Result := FCount; + if (Result >= FCapacity) then + SetCapacity(FCount+1); + FColorMap^[FCount] := Color2RGB(Color); + inc(FCount); + FOptimized := False; + Changed; +end; + +function TGIFColorMap.AddUnique(Color: TColor): integer; +begin + // Look up color before add (same as IndexOf) + Result := IndexOf(Color); + if (Result >= 0) then + // Color already in map + exit; + + Result := Add(Color); +end; + +//: Removes a color from the color map. +procedure TGIFColorMap.Delete(Index: integer); +begin + if (Index < 0) or (Index >= FCount) then + // Color index out of range + Error(sBadColorIndex); + dec(FCount); + if (Index < FCount) then + System.Move(FColorMap^[Index + 1], FColorMap^[Index], (FCount - Index)* sizeof(TGIFColor)); + FOptimized := False; + Changed; +end; + +function TGIFColorMap.GetColor(Index: integer): TColor; +begin + if (Index < 0) or (Index >= FCount) then + begin + // Color index out of range + Warning(gsWarning, sBadColorIndex); + // Raise an exception if the color map is empty + if (FCount = 0) then + Error(sEmptyColorMap); + // Default to color index 0 + Index := 0; + end; + Result := RGB2Color(FColorMap^[Index]); +end; + +procedure TGIFColorMap.SetColor(Index: integer; Value: TColor); +begin + if (Index < 0) or (Index >= FCount) then + // Color index out of range + Error(sBadColorIndex); + FColorMap^[Index] := Color2RGB(Value); + Changed; +end; + +function TGIFColorMap.DoOptimize: boolean; +var + Usage : TColormapHistogram; + TempMap : array[0..255] of TGIFColor; + ReverseMap : TColormapReverse; + i : integer; + LastFound : boolean; + NewCount : integer; + T : TUsageCount; + Pivot : integer; + + procedure QuickSort(iLo, iHi: Integer); + var + Lo, Hi: Integer; + begin + repeat + Lo := iLo; + Hi := iHi; + Pivot := Usage[(iLo + iHi) SHR 1].Count; + repeat + while (Usage[Lo].Count - Pivot > 0) do inc(Lo); + while (Usage[Hi].Count - Pivot < 0) do dec(Hi); + if (Lo <= Hi) then + begin + T := Usage[Lo]; + Usage[Lo] := Usage[Hi]; + Usage[Hi] := T; + inc(Lo); + dec(Hi); + end; + until (Lo > Hi); + if (iLo < Hi) then + QuickSort(iLo, Hi); + iLo := Lo; + until (Lo >= iHi); + end; + +begin + if (FCount <= 1) then + begin + Result := False; + exit; + end; + + FOptimized := True; + Result := True; + + BuildHistogram(Usage); + + (* + ** Sort according to usage count + *) + QuickSort(0, FCount-1); + + (* + ** Test for table already sorted + *) + for i := 0 to FCount-1 do + if (Usage[i].Index <> i) then + break; + if (i = FCount) then + exit; + + (* + ** Build old to new map + *) + for i := 0 to FCount-1 do + ReverseMap[Usage[i].Index] := i; + + + MapImages(ReverseMap); + + (* + ** Reorder colormap + *) + LastFound := False; + NewCount := FCount; + Move(FColorMap^, TempMap, FCount * sizeof(TGIFColor)); + for i := 0 to FCount-1 do + begin + FColorMap^[ReverseMap[i]] := TempMap[i]; + // Find last used color index + if (Usage[i].Count = 0) and not(LastFound) then + begin + LastFound := True; + NewCount := i; + end; + end; + + FCount := NewCount; + + Changed; +end; + +function TGIFColorMap.GetBitsPerPixel: integer; +begin + Result := Colors2bpp(FCount); +end; + +//: Copies one color map to another. +procedure TGIFColorMap.Assign(Source: TPersistent); +begin + if (Source is TGIFColorMap) then + begin + Clear; + FCapacity := TGIFColorMap(Source).FCapacity; + FCount := TGIFColorMap(Source).FCount; + FOptimized := TGIFColorMap(Source).FOptimized; + FColorMap := AllocMem(FCapacity * sizeof(TGIFColor)); + System.Move(TGIFColorMap(Source).FColorMap^, FColorMap^, FCount * sizeof(TGIFColor)); + Changed; + end else + inherited Assign(Source); +end; + +//////////////////////////////////////////////////////////////////////////////// +// +// TGIFItem +// +//////////////////////////////////////////////////////////////////////////////// +constructor TGIFItem.Create(GIFImage: TGIFImage); +begin + inherited Create; + + FGIFImage := GIFImage; +end; + +procedure TGIFItem.Warning(Severity: TGIFSeverity; Message: string); +begin + FGIFImage.Warning(self, Severity, Message); +end; + +function TGIFItem.GetVersion: TGIFVersion; +begin + Result := gv87a; +end; + +procedure TGIFItem.LoadFromFile(const Filename: string); +var + Stream: TStream; +begin + Stream := TFileStream.Create(Filename, fmOpenRead OR fmShareDenyWrite); + try + LoadFromStream(Stream); + finally + Stream.Free; + end; +end; + +procedure TGIFItem.SaveToFile(const Filename: string); +var + Stream: TStream; +begin + Stream := TFileStream.Create(Filename, fmCreate); + try + SaveToStream(Stream); + finally + Stream.Free; + end; +end; + +//////////////////////////////////////////////////////////////////////////////// +// +// TGIFList +// +//////////////////////////////////////////////////////////////////////////////// +constructor TGIFList.Create(Image: TGIFImage); +begin + inherited Create; + FImage := Image; + FItems := TList.Create; +end; + +destructor TGIFList.Destroy; +begin + Clear; + FItems.Free; + inherited Destroy; +end; + +function TGIFList.GetItem(Index: Integer): TGIFItem; +begin + Result := TGIFItem(FItems[Index]); +end; + +procedure TGIFList.SetItem(Index: Integer; Item: TGIFItem); +begin + FItems[Index] := Item; +end; + +function TGIFList.GetCount: Integer; +begin + Result := FItems.Count; +end; + +function TGIFList.Add(Item: TGIFItem): Integer; +begin + Result := FItems.Add(Item); +end; + +procedure TGIFList.Clear; +begin + while (FItems.Count > 0) do + Delete(0); +end; + +procedure TGIFList.Delete(Index: Integer); +var + Item : TGIFItem; +begin + Item := TGIFItem(FItems[Index]); + // Delete before item is destroyed to avoid recursion + FItems.Delete(Index); + Item.Free; +end; + +procedure TGIFList.Exchange(Index1, Index2: Integer); +begin + FItems.Exchange(Index1, Index2); +end; + +function TGIFList.First: TGIFItem; +begin + Result := TGIFItem(FItems.First); +end; + +function TGIFList.IndexOf(Item: TGIFItem): Integer; +begin + Result := FItems.IndexOf(Item); +end; + +procedure TGIFList.Insert(Index: Integer; Item: TGIFItem); +begin + FItems.Insert(Index, Item); +end; + +function TGIFList.Last: TGIFItem; +begin + Result := TGIFItem(FItems.Last); +end; + +procedure TGIFList.Move(CurIndex, NewIndex: Integer); +begin + FItems.Move(CurIndex, NewIndex); +end; + +function TGIFList.Remove(Item: TGIFItem): Integer; +begin + // Note: TGIFList.Remove must not destroy item + Result := FItems.Remove(Item); +end; + +procedure TGIFList.SaveToStream(Stream: TStream); +var + i : integer; +begin + for i := 0 to FItems.Count-1 do + TGIFItem(FItems[i]).SaveToStream(Stream); +end; + +procedure TGIFList.Warning(Severity: TGIFSeverity; Message: string); +begin + Image.Warning(self, Severity, Message); +end; + +//////////////////////////////////////////////////////////////////////////////// +// +// TGIFGlobalColorMap +// +//////////////////////////////////////////////////////////////////////////////// +type + TGIFGlobalColorMap = class(TGIFColorMap) + private + FHeader : TGIFHeader; + protected + procedure Warning(Severity: TGIFSeverity; Message: string); override; + procedure BuildHistogram(var Histogram: TColormapHistogram); override; + procedure MapImages(var Map: TColormapReverse); override; + public + constructor Create(HeaderItem: TGIFHeader); + function Optimize: boolean; override; + procedure Changed; override; + end; + +constructor TGIFGlobalColorMap.Create(HeaderItem: TGIFHeader); +begin + Inherited Create; + FHeader := HeaderItem; +end; + +procedure TGIFGlobalColorMap.Warning(Severity: TGIFSeverity; Message: string); +begin + FHeader.Image.Warning(self, Severity, Message); +end; + +procedure TGIFGlobalColorMap.BuildHistogram(var Histogram: TColormapHistogram); +var + Pixel , + LastPixel : PChar; + i : integer; +begin + (* + ** Init histogram + *) + for i := 0 to Count-1 do + begin + Histogram[i].Index := i; + Histogram[i].Count := 0; + end; + + for i := 0 to FHeader.Image.Images.Count-1 do + if (FHeader.Image.Images[i].ActiveColorMap = self) then + begin + Pixel := FHeader.Image.Images[i].Data; + LastPixel := Pixel + FHeader.Image.Images[i].Width * FHeader.Image.Images[i].Height; + + (* + ** Sum up usage count for each color + *) + while (Pixel < LastPixel) do + begin + inc(Histogram[ord(Pixel^)].Count); + inc(Pixel); + end; + end; +end; + +procedure TGIFGlobalColorMap.MapImages(var Map: TColormapReverse); +var + Pixel , + LastPixel : PChar; + i : integer; +begin + for i := 0 to FHeader.Image.Images.Count-1 do + if (FHeader.Image.Images[i].ActiveColorMap = self) then + begin + Pixel := FHeader.Image.Images[i].Data; + LastPixel := Pixel + FHeader.Image.Images[i].Width * FHeader.Image.Images[i].Height; + + (* + ** Reorder all pixel to new map + *) + while (Pixel < LastPixel) do + begin + Pixel^ := chr(Map[ord(Pixel^)]); + inc(Pixel); + end; + + (* + ** Reorder transparent colors + *) + if (FHeader.Image.Images[i].Transparent) then + FHeader.Image.Images[i].GraphicControlExtension.TransparentColorIndex := + Map[FHeader.Image.Images[i].GraphicControlExtension.TransparentColorIndex]; + end; +end; + +function TGIFGlobalColorMap.Optimize: boolean; +begin + { Optimize with first image, Remove unused colors if only one image } + if (FHeader.Image.Images.Count > 0) then + Result := DoOptimize + else + Result := False; +end; + +procedure TGIFGlobalColorMap.Changed; +begin + FHeader.Image.Palette := 0; +end; + +//////////////////////////////////////////////////////////////////////////////// +// +// TGIFHeader +// +//////////////////////////////////////////////////////////////////////////////// +constructor TGIFHeader.Create(GIFImage: TGIFImage); +begin + inherited Create(GIFImage); + FColorMap := TGIFGlobalColorMap.Create(self); + Clear; +end; + +destructor TGIFHeader.Destroy; +begin + FColorMap.Free; + inherited Destroy; +end; + +procedure TGIFHeader.Clear; +begin + FColorMap.Clear; + FLogicalScreenDescriptor.ScreenWidth := 0; + FLogicalScreenDescriptor.ScreenHeight := 0; + FLogicalScreenDescriptor.PackedFields := 0; + FLogicalScreenDescriptor.BackgroundColorIndex := 0; + FLogicalScreenDescriptor.AspectRatio := 0; +end; + +procedure TGIFHeader.Assign(Source: TPersistent); +begin + if (Source is TGIFHeader) then + begin + ColorMap.Assign(TGIFHeader(Source).ColorMap); + FLogicalScreenDescriptor := TGIFHeader(Source).FLogicalScreenDescriptor; + end else + if (Source is TGIFColorMap) then + begin + Clear; + ColorMap.Assign(TGIFColorMap(Source)); + end else + inherited Assign(Source); +end; + +type + TGIFHeaderRec = packed record + Signature: array[0..2] of char; { contains 'GIF' } + Version: TGIFVersionRec; { '87a' or '89a' } + end; + +const + { logical screen descriptor packed field masks } + lsdGlobalColorTable = $80; { set if global color table follows L.S.D. } + lsdColorResolution = $70; { Color resolution - 3 bits } + lsdSort = $08; { set if global color table is sorted - 1 bit } + lsdColorTableSize = $07; { size of global color table - 3 bits } + { Actual size = 2^value+1 - value is 3 bits } +procedure TGIFHeader.Prepare; +var + pack : BYTE; +begin + Pack := $00; + if (ColorMap.Count > 0) then + begin + Pack := lsdGlobalColorTable; + if (ColorMap.Optimized) then + Pack := Pack OR lsdSort; + end; + // Note: The SHL below was SHL 5 in the original source, but that looks wrong + Pack := Pack OR ((Image.ColorResolution SHL 4) AND lsdColorResolution); + Pack := Pack OR ((Image.BitsPerPixel-1) AND lsdColorTableSize); + FLogicalScreenDescriptor.PackedFields := Pack; +end; + +procedure TGIFHeader.SaveToStream(Stream: TStream); +var + GifHeader : TGIFHeaderRec; + v : TGIFVersion; +begin + v := Image.Version; + if (v = gvUnknown) then + Error(sBadVersion); + + GifHeader.Signature := 'GIF'; + GifHeader.Version := GIFVersions[v]; + + Prepare; + Stream.Write(GifHeader, sizeof(GifHeader)); + Stream.Write(FLogicalScreenDescriptor, sizeof(FLogicalScreenDescriptor)); + if (FLogicalScreenDescriptor.PackedFields AND lsdGlobalColorTable = lsdGlobalColorTable) then + ColorMap.SaveToStream(Stream); +end; + +procedure TGIFHeader.LoadFromStream(Stream: TStream); +var + GifHeader : TGIFHeaderRec; + ColorCount : integer; + Position : integer; +begin + Position := Stream.Position; + + ReadCheck(Stream, GifHeader, sizeof(GifHeader)); + if (uppercase(GifHeader.Signature) <> 'GIF') then + begin + // Attempt recovery in case we are reading a GIF stored in a form by rxLib + Stream.Position := Position; + // Seek past size stored in stream + Stream.Seek(sizeof(longInt), soFromCurrent); + // Attempt to read signature again + ReadCheck(Stream, GifHeader, sizeof(GifHeader)); + if (uppercase(GifHeader.Signature) <> 'GIF') then + Error(sBadSignature); + end; + + ReadCheck(Stream, FLogicalScreenDescriptor, sizeof(FLogicalScreenDescriptor)); + + if (FLogicalScreenDescriptor.PackedFields AND lsdGlobalColorTable = lsdGlobalColorTable) then + begin + ColorCount := 2 SHL (FLogicalScreenDescriptor.PackedFields AND lsdColorTableSize); + if (ColorCount < 2) or (ColorCount > 256) then + Error(sScreenBadColorSize); + ColorMap.LoadFromStream(Stream, ColorCount) + end else + ColorMap.Clear; +end; + +function TGIFHeader.GetVersion: TGIFVersion; +begin + if (FColorMap.Optimized) or (AspectRatio <> 0) then + Result := gv89a + else + Result := inherited GetVersion; +end; + +function TGIFHeader.GetBackgroundColor: TColor; +begin + Result := FColorMap[BackgroundColorIndex]; +end; + +procedure TGIFHeader.SetBackgroundColor(Color: TColor); +begin + BackgroundColorIndex := FColorMap.AddUnique(Color); +end; + +procedure TGIFHeader.SetBackgroundColorIndex(Index: BYTE); +begin + if ((Index >= FColorMap.Count) and (FColorMap.Count > 0)) then + begin + Warning(gsWarning, sBadColorIndex); + Index := 0; + end; + FLogicalScreenDescriptor.BackgroundColorIndex := Index; +end; + +function TGIFHeader.GetBitsPerPixel: integer; +begin + Result := FColorMap.BitsPerPixel; +end; + +function TGIFHeader.GetColorResolution: integer; +begin + Result := FColorMap.BitsPerPixel-1; +end; + +//////////////////////////////////////////////////////////////////////////////// +// +// TGIFLocalColorMap +// +//////////////////////////////////////////////////////////////////////////////// +type + TGIFLocalColorMap = class(TGIFColorMap) + private + FSubImage : TGIFSubImage; + protected + procedure Warning(Severity: TGIFSeverity; Message: string); override; + procedure BuildHistogram(var Histogram: TColormapHistogram); override; + procedure MapImages(var Map: TColormapReverse); override; + public + constructor Create(SubImage: TGIFSubImage); + function Optimize: boolean; override; + procedure Changed; override; + end; + +constructor TGIFLocalColorMap.Create(SubImage: TGIFSubImage); +begin + Inherited Create; + FSubImage := SubImage; +end; + +procedure TGIFLocalColorMap.Warning(Severity: TGIFSeverity; Message: string); +begin + FSubImage.Image.Warning(self, Severity, Message); +end; + +procedure TGIFLocalColorMap.BuildHistogram(var Histogram: TColormapHistogram); +var + Pixel , + LastPixel : PChar; + i : integer; +begin + Pixel := FSubImage.Data; + LastPixel := Pixel + FSubImage.Width * FSubImage.Height; + + (* + ** Init histogram + *) + for i := 0 to Count-1 do + begin + Histogram[i].Index := i; + Histogram[i].Count := 0; + end; + + (* + ** Sum up usage count for each color + *) + while (Pixel < LastPixel) do + begin + inc(Histogram[ord(Pixel^)].Count); + inc(Pixel); + end; +end; + +procedure TGIFLocalColorMap.MapImages(var Map: TColormapReverse); +var + Pixel , + LastPixel : PChar; +begin + Pixel := FSubImage.Data; + LastPixel := Pixel + FSubImage.Width * FSubImage.Height; + + (* + ** Reorder all pixel to new map + *) + while (Pixel < LastPixel) do + begin + Pixel^ := chr(Map[ord(Pixel^)]); + inc(Pixel); + end; + + (* + ** Reorder transparent colors + *) + if (FSubImage.Transparent) then + FSubImage.GraphicControlExtension.TransparentColorIndex := + Map[FSubImage.GraphicControlExtension.TransparentColorIndex]; +end; + +function TGIFLocalColorMap.Optimize: boolean; +begin + Result := DoOptimize; +end; + +procedure TGIFLocalColorMap.Changed; +begin + FSubImage.Palette := 0; +end; + + +//////////////////////////////////////////////////////////////////////////////// +// +// LZW Decoder +// +//////////////////////////////////////////////////////////////////////////////// +const + GIFCodeBits = 12; // Max number of bits per GIF token code + GIFCodeMax = (1 SHL GIFCodeBits)-1;// Max GIF token code + // 12 bits = 4095 + StackSize = (2 SHL GIFCodeBits); // Size of decompression stack + TableSize = (1 SHL GIFCodeBits); // Size of decompression table + +procedure TGIFSubImage.Decompress(Stream: TStream); +var + table0 : array[0..TableSize-1] of integer; + table1 : array[0..TableSize-1] of integer; + firstcode, oldcode : integer; + buf : array[0..257] of BYTE; + + Dest : PChar; + v , + xpos, ypos, pass : integer; + + stack : array[0..StackSize-1] of integer; + Source : ^integer; + BitsPerCode : integer; // number of CodeTableBits/code + InitialBitsPerCode : BYTE; + + MaxCode : integer; // maximum code, given BitsPerCode + MaxCodeSize : integer; + ClearCode : integer; // Special code to signal "Clear table" + EOFCode : integer; // Special code to signal EOF + step : integer; + i : integer; + + StartBit , // Index of bit buffer start + LastBit , // Index of last bit in buffer + LastByte : integer; // Index of last byte in buffer + get_done , + return_clear , + ZeroBlock : boolean; + ClearValue : BYTE; +{$ifdef DEBUG_DECOMPRESSPERFORMANCE} + TimeStartDecompress , + TimeStopDecompress : DWORD; +{$endif} + + function nextCode(BitsPerCode: integer): integer; + const + masks: array[0..15] of integer = + ($0000, $0001, $0003, $0007, + $000f, $001f, $003f, $007f, + $00ff, $01ff, $03ff, $07ff, + $0fff, $1fff, $3fff, $7fff); + var + StartIndex, EndIndex : integer; + ret : integer; + EndBit : integer; + count : BYTE; + begin + if (return_clear) then + begin + return_clear := False; + Result := ClearCode; + exit; + end; + + EndBit := StartBit + BitsPerCode; + + if (EndBit >= LastBit) then + begin + if (get_done) then + begin + if (StartBit >= LastBit) then + Warning(gsWarning, sDecodeTooFewBits); + Result := -1; + exit; + end; + buf[0] := buf[LastByte-2]; + buf[1] := buf[LastByte-1]; + + if (Stream.Read(count, 1) <> 1) then + begin + Result := -1; + exit; + end; + if (count = 0) then + begin + ZeroBlock := True; + get_done := TRUE; + end else + begin + // Handle premature end of file + if (Stream.Size - Stream.Position < Count) then + begin + Warning(gsWarning, sOutOfData); + // Not enough data left - Just read as much as we can get + Count := Stream.Size - Stream.Position; + end; + if (Count <> 0) then + ReadCheck(Stream, Buf[2], Count); + end; + + LastByte := 2 + count; + StartBit := (StartBit - LastBit) + 16; + LastBit := LastByte * 8; + + EndBit := StartBit + BitsPerCode; + end; + + EndIndex := EndBit DIV 8; + StartIndex := StartBit DIV 8; + + ASSERT(StartIndex <= high(buf), 'StartIndex too large'); + if (StartIndex = EndIndex) then + ret := buf[StartIndex] + else + if (StartIndex + 1 = EndIndex) then + ret := buf[StartIndex] OR (buf[StartIndex+1] SHL 8) + else + ret := buf[StartIndex] OR (buf[StartIndex+1] SHL 8) OR (buf[StartIndex+2] SHL 16); + + ret := (ret SHR (StartBit AND $0007)) AND masks[BitsPerCode]; + + Inc(StartBit, BitsPerCode); + + Result := ret; + end; + + function NextLZW: integer; + var + code, incode : integer; + i : integer; + b : BYTE; + begin + code := nextCode(BitsPerCode); + while (code >= 0) do + begin + if (code = ClearCode) then + begin + ASSERT(ClearCode < TableSize, 'ClearCode too large'); + for i := 0 to ClearCode-1 do + begin + table0[i] := 0; + table1[i] := i; + end; + for i := ClearCode to TableSize-1 do + begin + table0[i] := 0; + table1[i] := 0; + end; + BitsPerCode := InitialBitsPerCode+1; + MaxCodeSize := 2 * ClearCode; + MaxCode := ClearCode + 2; + Source := @stack; + repeat + firstcode := nextCode(BitsPerCode); + oldcode := firstcode; + until (firstcode <> ClearCode); + + Result := firstcode; + exit; + end; + if (code = EOFCode) then + begin + Result := -2; + if (ZeroBlock) then + exit; + // Eat rest of data blocks + if (Stream.Read(b, 1) <> 1) then + exit; + while (b <> 0) do + begin + Stream.Seek(b, soFromCurrent); + if (Stream.Read(b, 1) <> 1) then + exit; + end; + exit; + end; + + incode := code; + + if (code >= MaxCode) then + begin + Source^ := firstcode; + Inc(Source); + code := oldcode; + end; + + ASSERT(Code < TableSize, 'Code too large'); + while (code >= ClearCode) do + begin + Source^ := table1[code]; + Inc(Source); + if (code = table0[code]) then + Error(sDecodeCircular); + code := table0[code]; + ASSERT(Code < TableSize, 'Code too large'); + end; + + firstcode := table1[code]; + Source^ := firstcode; + Inc(Source); + + code := MaxCode; + if (code <= GIFCodeMax) then + begin + table0[code] := oldcode; + table1[code] := firstcode; + Inc(MaxCode); + if ((MaxCode >= MaxCodeSize) and (MaxCodeSize <= GIFCodeMax)) then + begin + MaxCodeSize := MaxCodeSize * 2; + Inc(BitsPerCode); + end; + end; + + oldcode := incode; + + if (longInt(Source) > longInt(@stack)) then + begin + Dec(Source); + Result := Source^; + exit; + end + end; + Result := code; + end; + + function readLZW: integer; + begin + if (longInt(Source) > longInt(@stack)) then + begin + Dec(Source); + Result := Source^; + end else + Result := NextLZW; + end; + +begin + NewImage; + + // Clear image data in case decompress doesn't complete + if (Transparent) then + // Clear to transparent color + ClearValue := GraphicControlExtension.GetTransparentColorIndex + else + // Clear to first color + ClearValue := 0; + + FillChar(FData^, FDataSize, ClearValue); + +{$ifdef DEBUG_DECOMPRESSPERFORMANCE} + TimeStartDecompress := timeGetTime; +{$endif} + + (* + ** Read initial code size in bits from stream + *) + if (Stream.Read(InitialBitsPerCode, 1) <> 1) then + exit; + + (* + ** Initialize the Compression routines + *) + BitsPerCode := InitialBitsPerCode + 1; + ClearCode := 1 SHL InitialBitsPerCode; + EOFCode := ClearCode + 1; + MaxCodeSize := 2 * ClearCode; + MaxCode := ClearCode + 2; + + StartBit := 0; + LastBit := 0; + LastByte := 2; + + ZeroBlock := False; + get_done := False; + return_clear := TRUE; + + Source := @stack; + + try + if (Interlaced) then + begin + ypos := 0; + pass := 0; + step := 8; + + for i := 0 to Height-1 do + begin + Dest := FData + Width * ypos; + for xpos := 0 to width-1 do + begin + v := readLZW; + if (v < 0) then + exit; + Dest^ := char(v); + Inc(Dest); + end; + Inc(ypos, step); + if (ypos >= height) then + repeat + if (pass > 0) then + step := step DIV 2; + Inc(pass); + ypos := step DIV 2; + until (ypos < height); + end; + end else + begin + Dest := FData; + for ypos := 0 to (height * width)-1 do + begin + v := readLZW; + if (v < 0) then + exit; + Dest^ := char(v); + Inc(Dest); + end; + end; + finally + if (readLZW >= 0) then + ; +// raise GIFException.Create('Too much input data, ignoring extra...'); + end; +{$ifdef DEBUG_DECOMPRESSPERFORMANCE} + TimeStopDecompress := timeGetTime; + ShowMessage(format('Decompressed %d pixels in %d mS, Rate %d pixels/mS', + [Height*Width, TimeStopDecompress-TimeStartDecompress, + (Height*Width) DIV (TimeStopDecompress-TimeStartDecompress+1)])); +{$endif} +end; + +//////////////////////////////////////////////////////////////////////////////// +// +// LZW Encoder stuff +// +//////////////////////////////////////////////////////////////////////////////// + +//////////////////////////////////////////////////////////////////////////////// +// LZW Encoder THashTable +//////////////////////////////////////////////////////////////////////////////// +const + HashKeyBits = 13; // Max number of bits per Hash Key + + HashSize = 8009; // Size of hash table + // Must be prime + // Must be > than HashMaxCode + // Must be < than HashMaxKey + + HashKeyMax = (1 SHL HashKeyBits)-1;// Max hash key value + // 13 bits = 8191 + + HashKeyMask = HashKeyMax; // $1FFF + GIFCodeMask = GIFCodeMax; // $0FFF + + HashEmpty = $000FFFFF; // 20 bits + +type + // A Hash Key is 20 bits wide. + // - The lower 8 bits are the postfix character (the new pixel). + // - The upper 12 bits are the prefix code (the GIF token). + // A KeyInt must be able to represent the integer values -1..(2^20)-1 + KeyInt = longInt; // 32 bits + CodeInt = SmallInt; // 16 bits + + THashArray = array[0..HashSize-1] of KeyInt; + PHashArray = ^THashArray; + + THashTable = class +{$ifdef DEBUG_HASHPERFORMANCE} + CountLookupFound : longInt; + CountMissFound : longInt; + CountLookupNotFound : longInt; + CountMissNotFound : longInt; +{$endif} + HashTable: PHashArray; + public + constructor Create; + destructor Destroy; override; + procedure Clear; + procedure Insert(Key: KeyInt; Code: CodeInt); + function Lookup(Key: KeyInt): CodeInt; + end; + +function HashKey(Key: KeyInt): CodeInt; +begin + Result := ((Key SHR (GIFCodeBits-8)) XOR Key) MOD HashSize; +end; + +function NextHashKey(HKey: CodeInt): CodeInt; +var + disp : CodeInt; +begin + (* + ** secondary hash (after G. Knott) + *) + disp := HashSize - HKey; + if (HKey = 0) then + disp := 1; +// disp := 13; // disp should be prime relative to HashSize, but + // it doesn't seem to matter here... + dec(HKey, disp); + if (HKey < 0) then + inc(HKey, HashSize); + Result := HKey; +end; + + +constructor THashTable.Create; +begin + ASSERT(longInt($FFFFFFFF) = -1, 'TGIFImage implementation assumes $FFFFFFFF = -1'); + + inherited Create; + GetMem(HashTable, sizeof(THashArray)); + Clear; +{$ifdef DEBUG_HASHPERFORMANCE} + CountLookupFound := 0; + CountMissFound := 0; + CountLookupNotFound := 0; + CountMissNotFound := 0; +{$endif} +end; + +destructor THashTable.Destroy; +begin +{$ifdef DEBUG_HASHPERFORMANCE} + ShowMessage( + Format('Found: %d HitRate: %.2f', + [CountLookupFound, (CountLookupFound+1)/(CountMissFound+1)])+#13+ + Format('Not found: %d HitRate: %.2f', + [CountLookupNotFound, (CountLookupNotFound+1)/(CountMissNotFound+1)])); +{$endif} + FreeMem(HashTable); + inherited Destroy; +end; + +// Clear hash table and fill with empty slots (doh!) +procedure THashTable.Clear; +{$ifdef DEBUG_HASHFILLFACTOR} +var + i , + Count : longInt; +{$endif} +begin +{$ifdef DEBUG_HASHFILLFACTOR} + Count := 0; + for i := 0 to HashSize-1 do + if (HashTable[i] SHR GIFCodeBits <> HashEmpty) then + inc(Count); + ShowMessage(format('Size: %d, Filled: %d, Rate %.4f', + [HashSize, Count, Count/HashSize])); +{$endif} + + FillChar(HashTable^, sizeof(THashArray), $FF); +end; + +// Insert new key/value pair into hash table +procedure THashTable.Insert(Key: KeyInt; Code: CodeInt); +var + HKey : CodeInt; +begin + // Create hash key from prefix string + HKey := HashKey(Key); + + // Scan for empty slot + // while (HashTable[HKey] SHR GIFCodeBits <> HashEmpty) do { Unoptimized } + while (HashTable[HKey] AND (HashEmpty SHL GIFCodeBits) <> (HashEmpty SHL GIFCodeBits)) do { Optimized } + HKey := NextHashKey(HKey); + // Fill slot with key/value pair + HashTable[HKey] := (Key SHL GIFCodeBits) OR (Code AND GIFCodeMask); +end; + +// Search for key in hash table. +// Returns value if found or -1 if not +function THashTable.Lookup(Key: KeyInt): CodeInt; +var + HKey : CodeInt; + HTKey : KeyInt; +{$ifdef DEBUG_HASHPERFORMANCE} + n : LongInt; +{$endif} +begin + // Create hash key from prefix string + HKey := HashKey(Key); + +{$ifdef DEBUG_HASHPERFORMANCE} + n := 0; +{$endif} + // Scan table for key + // HTKey := HashTable[HKey] SHR GIFCodeBits; { Unoptimized } + Key := Key SHL GIFCodeBits; { Optimized } + HTKey := HashTable[HKey] AND (HashEmpty SHL GIFCodeBits); { Optimized } + // while (HTKey <> HashEmpty) do { Unoptimized } + while (HTKey <> HashEmpty SHL GIFCodeBits) do { Optimized } + begin + if (Key = HTKey) then + begin + // Extract and return value + Result := HashTable[HKey] AND GIFCodeMask; +{$ifdef DEBUG_HASHPERFORMANCE} + inc(CountLookupFound); + inc(CountMissFound, n); +{$endif} + exit; + end; +{$ifdef DEBUG_HASHPERFORMANCE} + inc(n); +{$endif} + // Try next slot + HKey := NextHashKey(HKey); + // HTKey := HashTable[HKey] SHR GIFCodeBits; { Unoptimized } + HTKey := HashTable[HKey] AND (HashEmpty SHL GIFCodeBits); { Optimized } + end; + // Found empty slot - key doesn't exist + Result := -1; +{$ifdef DEBUG_HASHPERFORMANCE} + inc(CountLookupNotFound); + inc(CountMissNotFound, n); +{$endif} +end; + +//////////////////////////////////////////////////////////////////////////////// +// TGIFStream - Abstract GIF block stream +// +// Descendants from TGIFStream either reads or writes data in blocks +// of up to 255 bytes. These blocks are organized as a leading byte +// containing the number of bytes in the block (exclusing the count +// byte itself), followed by the data (up to 254 bytes of data). +//////////////////////////////////////////////////////////////////////////////// +type + TGIFStream = class(TStream) + private + FOnWarning : TGIFWarning; + FStream : TStream; + FOnProgress : TNotifyEvent; + FBuffer : array [BYTE] of Char; + FBufferCount : integer; + + protected + constructor Create(Stream: TStream); + + function Read(var Buffer; Count: Longint): Longint; override; + function Write(const Buffer; Count: Longint): Longint; override; + function Seek(Offset: Longint; Origin: Word): Longint; override; + + procedure Progress(Sender: TObject); dynamic; + property OnProgress: TNotifyEvent read FOnProgress write FOnProgress; + public + property Warning: TGIFWarning read FOnWarning write FOnWarning; + end; + +constructor TGIFStream.Create(Stream: TStream); +begin + inherited Create; + FStream := Stream; + FBufferCount := 1; // Reserve first byte of buffer for length +end; + +procedure TGIFStream.Progress(Sender: TObject); +begin + if Assigned(FOnProgress) then + FOnProgress(Sender); +end; + +function TGIFStream.Write(const Buffer; Count: Longint): Longint; +begin + raise Exception.Create(sInvalidStream); +end; + +function TGIFStream.Read(var Buffer; Count: Longint): Longint; +begin + raise Exception.Create(sInvalidStream); +end; + +function TGIFStream.Seek(Offset: Longint; Origin: Word): Longint; +begin + raise Exception.Create(sInvalidStream); +end; + +//////////////////////////////////////////////////////////////////////////////// +// TGIFReader - GIF block reader +//////////////////////////////////////////////////////////////////////////////// +type + TGIFReader = class(TGIFStream) + public + constructor Create(Stream: TStream); + + function Read(var Buffer; Count: Longint): Longint; override; + end; + +constructor TGIFReader.Create(Stream: TStream); +begin + inherited Create(Stream); + FBufferCount := 0; +end; + +function TGIFReader.Read(var Buffer; Count: Longint): Longint; +var + n : integer; + Dst : PChar; + size : BYTE; +begin + Dst := @Buffer; + Result := 0; + + while (Count > 0) do + begin + // Get data from buffer + while (FBufferCount > 0) and (Count > 0) do + begin + if (FBufferCount > Count) then + n := Count + else + n := FBufferCount; + Move(FBuffer, Dst^, n); + dec(FBufferCount, n); + dec(Count, n); + inc(Result, n); + inc(Dst, n); + end; + + // Refill buffer when it becomes empty + if (FBufferCount <= 0) then + begin + FStream.Read(size, 1); + { TODO -oanme -cImprovement : Should be handled as a warning instead of an error. } + if (size >= 255) then + Error('GIF block too large'); + FBufferCount := size; + if (FBufferCount > 0) then + begin + n := FStream.Read(FBuffer, size); + if (n = FBufferCount) then + begin + Warning(self, gsWarning, sOutOfData); + break; + end; + end else + break; + end; + end; +end; + +//////////////////////////////////////////////////////////////////////////////// +// TGIFWriter - GIF block writer +//////////////////////////////////////////////////////////////////////////////// +type + TGIFWriter = class(TGIFStream) + private + FOutputDirty : boolean; + + protected + procedure FlushBuffer; + + public + constructor Create(Stream: TStream); + destructor Destroy; override; + + function Write(const Buffer; Count: Longint): Longint; override; + function WriteByte(Value: BYTE): Longint; + end; + +constructor TGIFWriter.Create(Stream: TStream); +begin + inherited Create(Stream); + FBufferCount := 1; // Reserve first byte of buffer for length + FOutputDirty := False; +end; + +destructor TGIFWriter.Destroy; +begin + inherited Destroy; + if (FOutputDirty) then + FlushBuffer; +end; + +procedure TGIFWriter.FlushBuffer; +begin + if (FBufferCount <= 0) then + exit; + + FBuffer[0] := char(FBufferCount-1); // Block size excluding the count + FStream.WriteBuffer(FBuffer, FBufferCount); + FBufferCount := 1; // Reserve first byte of buffer for length + FOutputDirty := False; +end; + +function TGIFWriter.Write(const Buffer; Count: Longint): Longint; +var + n : integer; + Src : PChar; +begin + Result := Count; + FOutputDirty := True; + Src := @Buffer; + while (Count > 0) do + begin + // Move data to the internal buffer in 255 byte chunks + while (FBufferCount < sizeof(FBuffer)) and (Count > 0) do + begin + n := sizeof(FBuffer) - FBufferCount; + if (n > Count) then + n := Count; + Move(Src^, FBuffer[FBufferCount], n); + inc(Src, n); + inc(FBufferCount, n); + dec(Count, n); + end; + + // Flush the buffer when it is full + if (FBufferCount >= sizeof(FBuffer)) then + FlushBuffer; + end; +end; + +function TGIFWriter.WriteByte(Value: BYTE): Longint; +begin + Result := Write(Value, 1); +end; + +//////////////////////////////////////////////////////////////////////////////// +// TGIFEncoder - Abstract encoder +//////////////////////////////////////////////////////////////////////////////// +type + TGIFEncoder = class(TObject) + protected + FOnWarning : TGIFWarning; + MaxColor : integer; + BitsPerPixel : BYTE; // Bits per pixel of image + Stream : TStream; // Output stream + Width , // Width of image in pixels + Height : integer; // height of image in pixels + Interlace : boolean; // Interlace flag (True = interlaced image) + Data : PChar; // Pointer to pixel data + GIFStream : TGIFWriter; // Output buffer + + OutputBucket : longInt; // Output bit bucket + OutputBits : integer; // Current # of bits in bucket + + ClearFlag : Boolean; // True if dictionary has just been cleared + BitsPerCode , // Current # of bits per code + InitialBitsPerCode : integer; // Initial # of bits per code after + // dictionary has been cleared + MaxCode : CodeInt; // maximum code, given BitsPerCode + ClearCode : CodeInt; // Special output code to signal "Clear table" + EOFCode : CodeInt; // Special output code to signal EOF + BaseCode : CodeInt; // ... + + Pixel : PChar; // Pointer to current pixel + + cX , // Current X counter (Width - X) + Y : integer; // Current Y + Pass : integer; // Interlace pass + + function MaxCodesFromBits(Bits: integer): CodeInt; + procedure Output(Value: integer); virtual; + procedure Clear; virtual; + function BumpPixel: boolean; + procedure DoCompress; virtual; abstract; + public + procedure Compress(AStream: TStream; ABitsPerPixel: integer; + AWidth, AHeight: integer; AInterlace: boolean; AData: PChar; AMaxColor: integer); + property Warning: TGIFWarning read FOnWarning write FOnWarning; + end; + +// Calculate the maximum number of codes that a given number of bits can represent +// MaxCodes := (1^bits)-1 +function TGIFEncoder.MaxCodesFromBits(Bits: integer): CodeInt; +begin + Result := (CodeInt(1) SHL Bits) - 1; +end; + +// Stuff bits (variable sized codes) into a buffer and output them +// a byte at a time +procedure TGIFEncoder.Output(Value: integer); +const + BitBucketMask: array[0..16] of longInt = + ($0000, + $0001, $0003, $0007, $000F, + $001F, $003F, $007F, $00FF, + $01FF, $03FF, $07FF, $0FFF, + $1FFF, $3FFF, $7FFF, $FFFF); +begin + if (OutputBits > 0) then + OutputBucket := + (OutputBucket AND BitBucketMask[OutputBits]) OR (longInt(Value) SHL OutputBits) + else + OutputBucket := Value; + + inc(OutputBits, BitsPerCode); + + while (OutputBits >= 8) do + begin + GIFStream.WriteByte(OutputBucket AND $FF); + OutputBucket := OutputBucket SHR 8; + dec(OutputBits, 8); + end; + + if (Value = EOFCode) then + begin + // At EOF, write the rest of the buffer. + while (OutputBits > 0) do + begin + GIFStream.WriteByte(OutputBucket AND $FF); + OutputBucket := OutputBucket SHR 8; + dec(OutputBits, 8); + end; + end; +end; + +procedure TGIFEncoder.Clear; +begin + // just_cleared = 1; + ClearFlag := TRUE; + Output(ClearCode); +end; + +// Bump (X,Y) and data pointer to point to the next pixel +function TGIFEncoder.BumpPixel: boolean; +begin + // Bump the current X position + dec(cX); + + // If we are at the end of a scan line, set cX back to the beginning + // If we are interlaced, bump Y to the appropriate spot, otherwise, + // just increment it. + if (cX <= 0) then + begin + + if not(Interlace) then + begin + // Done - no more data + Result := False; + exit; + end; + + cX := Width; + case (Pass) of + 0: + begin + inc(Y, 8); + if (Y >= Height) then + begin + inc(Pass); + Y := 4; + end; + end; + 1: + begin + inc(Y, 8); + if (Y >= Height) then + begin + inc(Pass); + Y := 2; + end; + end; + 2: + begin + inc(Y, 4); + if (Y >= Height) then + begin + inc(Pass); + Y := 1; + end; + end; + 3: + inc(Y, 2); + end; + + if (Y >= height) then + begin + // Done - No more data + Result := False; + exit; + end; + Pixel := Data + (Y * Width); + end; + Result := True; +end; + + +procedure TGIFEncoder.Compress(AStream: TStream; ABitsPerPixel: integer; + AWidth, AHeight: integer; AInterlace: boolean; AData: PChar; AMaxColor: integer); +const + EndBlockByte = $00; // End of block marker +{$ifdef DEBUG_COMPRESSPERFORMANCE} +var + TimeStartCompress , + TimeStopCompress : DWORD; +{$endif} +begin + MaxColor := AMaxColor; + Stream := AStream; + BitsPerPixel := ABitsPerPixel; + Width := AWidth; + Height := AHeight; + Interlace := AInterlace; + Data := AData; + + if (BitsPerPixel <= 1) then + BitsPerPixel := 2; + + InitialBitsPerCode := BitsPerPixel + 1; + Stream.Write(BitsPerPixel, 1); + + // out_bits_init = init_bits; + BitsPerCode := InitialBitsPerCode; + MaxCode := MaxCodesFromBits(BitsPerCode); + + ClearCode := (1 SHL (InitialBitsPerCode - 1)); + EOFCode := ClearCode + 1; + BaseCode := EOFCode + 1; + + // Clear bit bucket + OutputBucket := 0; + OutputBits := 0; + + // Reset pixel counter + if (Interlace) then + cX := Width + else + cX := Width*Height; + // Reset row counter + Y := 0; + Pass := 0; + + GIFStream := TGIFWriter.Create(AStream); + try + GIFStream.Warning := Warning; + if (Data <> nil) and (Height > 0) and (Width > 0) then + begin +{$ifdef DEBUG_COMPRESSPERFORMANCE} + TimeStartCompress := timeGetTime; +{$endif} + + // Call compress implementation + DoCompress; + +{$ifdef DEBUG_COMPRESSPERFORMANCE} + TimeStopCompress := timeGetTime; + ShowMessage(format('Compressed %d pixels in %d mS, Rate %d pixels/mS', + [Height*Width, TimeStopCompress-TimeStartCompress, + DWORD(Height*Width) DIV (TimeStopCompress-TimeStartCompress+1)])); +{$endif} + // Output the final code. + Output(EOFCode); + end else + // Output the final code (and nothing else). + TGIFEncoder(self).Output(EOFCode); + finally + GIFStream.Free; + end; + + WriteByte(Stream, EndBlockByte); +end; + +//////////////////////////////////////////////////////////////////////////////// +// TRLEEncoder - RLE encoder +//////////////////////////////////////////////////////////////////////////////// +type + TRLEEncoder = class(TGIFEncoder) + private + MaxCodes : integer; + OutBumpInit , + OutClearInit : integer; + Prefix : integer; // Current run color + RunLengthTableMax , + RunLengthTablePixel , + OutCount , + OutClear , + OutBump : integer; + protected + function ComputeTriangleCount(count: integer; nrepcodes: integer): integer; + procedure MaxOutClear; + procedure ResetOutClear; + procedure FlushFromClear(Count: integer); + procedure FlushClearOrRepeat(Count: integer); + procedure FlushWithTable(Count: integer); + procedure Flush(RunLengthCount: integer); + procedure OutputPlain(Value: integer); + procedure Clear; override; + procedure DoCompress; override; + end; + + +procedure TRLEEncoder.Clear; +begin + OutBump := OutBumpInit; + OutClear := OutClearInit; + OutCount := 0; + RunLengthTableMax := 0; + + inherited Clear; + + BitsPerCode := InitialBitsPerCode; +end; + +procedure TRLEEncoder.OutputPlain(Value: integer); +begin + ClearFlag := False; + Output(Value); + inc(OutCount); + + if (OutCount >= OutBump) then + begin + inc(BitsPerCode); + inc(OutBump, 1 SHL (BitsPerCode - 1)); + end; + + if (OutCount >= OutClear) then + Clear; +end; + +function TRLEEncoder.ComputeTriangleCount(count: integer; nrepcodes: integer): integer; +var + PerRepeat : integer; + n : integer; + + function iSqrt(x: integer): integer; + var + r, v : integer; + begin + if (x < 2) then + begin + Result := x; + exit; + end else + begin + v := x; + r := 1; + while (v > 0) do + begin + v := v DIV 4; + r := r * 2; + end; + end; + + while (True) do + begin + v := ((x DIV r) + r) DIV 2; + if ((v = r) or (v = r+1)) then + begin + Result := r; + exit; + end; + r := v; + end; + end; + +begin + Result := 0; + PerRepeat := (nrepcodes * (nrepcodes+1)) DIV 2; + + while (Count >= PerRepeat) do + begin + inc(Result, nrepcodes); + dec(Count, PerRepeat); + end; + + if (Count > 0) then + begin + n := iSqrt(Count); + while ((n * (n+1)) >= 2*Count) do + dec(n); + while ((n * (n+1)) < 2*Count) do + inc(n); + inc(Result, n); + end; +end; + +procedure TRLEEncoder.MaxOutClear; +begin + OutClear := MaxCodes; +end; + +procedure TRLEEncoder.ResetOutClear; +begin + OutClear := OutClearInit; + if (OutCount >= OutClear) then + Clear; +end; + +procedure TRLEEncoder.FlushFromClear(Count: integer); +var + n : integer; +begin + MaxOutClear; + RunLengthTablePixel := Prefix; + n := 1; + while (Count > 0) do + begin + if (n = 1) then + begin + RunLengthTableMax := 1; + OutputPlain(Prefix); + dec(Count); + end else + if (Count >= n) then + begin + RunLengthTableMax := n; + OutputPlain(BaseCode + n - 2); + dec(Count, n); + end else + if (Count = 1) then + begin + inc(RunLengthTableMax); + OutputPlain(Prefix); + break; + end else + begin + inc(RunLengthTableMax); + OutputPlain(BaseCode + Count - 2); + break; + end; + + if (OutCount = 0) then + n := 1 + else + inc(n); + end; + ResetOutClear; +end; + +procedure TRLEEncoder.FlushClearOrRepeat(Count: integer); +var + WithClear : integer; +begin + WithClear := 1 + ComputeTriangleCount(Count, MaxCodes); + + if (WithClear < Count) then + begin + Clear; + FlushFromClear(Count); + end else + while (Count > 0) do + begin + OutputPlain(Prefix); + dec(Count); + end; +end; + +procedure TRLEEncoder.FlushWithTable(Count: integer); +var + RepeatMax , + RepeatLeft , + LeftOver : integer; +begin + RepeatMax := Count DIV RunLengthTableMax; + LeftOver := Count MOD RunLengthTableMax; + if (LeftOver <> 0) then + RepeatLeft := 1 + else + RepeatLeft := 0; + + if (OutCount + RepeatMax + RepeatLeft > MaxCodes) then + begin + RepeatMax := MaxCodes - OutCount; + LeftOver := Count - (RepeatMax * RunLengthTableMax); + RepeatLeft := 1 + ComputeTriangleCount(LeftOver, MaxCodes); + end; + + if (1 + ComputeTriangleCount(Count, MaxCodes) < RepeatMax + RepeatLeft) then + begin + Clear; + FlushFromClear(Count); + exit; + end; + MaxOutClear; + + while (RepeatMax > 0) do + begin + OutputPlain(BaseCode + RunLengthTableMax-2); + dec(RepeatMax); + end; + + if (LeftOver > 0) then + begin + if (ClearFlag) then + FlushFromClear(LeftOver) + else if (LeftOver = 1) then + OutputPlain(Prefix) + else + OutputPlain(BaseCode + LeftOver - 2); + end; + ResetOutClear; +end; + +procedure TRLEEncoder.Flush(RunLengthCount: integer); +begin + if (RunLengthCount = 1) then + begin + OutputPlain(Prefix); + exit; + end; + + if (ClearFlag) then + FlushFromClear(RunLengthCount) + else if ((RunLengthTableMax < 2) or (RunLengthTablePixel <> Prefix)) then + FlushClearOrRepeat(RunLengthCount) + else + FlushWithTable(RunLengthCount); +end; + +procedure TRLEEncoder.DoCompress; +var + Color : CodeInt; + RunLengthCount : integer; + +begin + OutBumpInit := ClearCode - 1; + + // For images with a lot of runs, making OutClearInit larger will + // give better compression. + if (BitsPerPixel <= 3) then + OutClearInit := 9 + else + OutClearInit := OutBumpInit - 1; + + // max_ocodes = (1 << GIFBITS) - ((1 << (out_bits_init - 1)) + 3); + // <=> MaxCodes := (1 SHL GIFCodeBits) - ((1 SHL (BitsPerCode - 1)) + 3); + // <=> MaxCodes := (1 SHL GIFCodeBits) - ((1 SHL (InitialBitsPerCode - 1)) + 3); + // <=> MaxCodes := (1 SHL GIFCodeBits) - (ClearCode + 3); + // <=> MaxCodes := (1 SHL GIFCodeBits) - (EOFCode + 2); + // <=> MaxCodes := (1 SHL GIFCodeBits) - (BaseCode + 1); + // <=> MaxCodes := MaxCodesFromBits(GIFCodeBits) - BaseCode; + MaxCodes := MaxCodesFromBits(GIFCodeBits) - BaseCode; + + Clear; + RunLengthCount := 0; + + Pixel := Data; + Prefix := -1; // Dummy value to make Color <> Prefix + repeat + // Fetch the next pixel + Color := CodeInt(Pixel^); + inc(Pixel); + + if (Color >= MaxColor) then + Error(sInvalidColor); + + if (RunLengthCount > 0) and (Color <> Prefix) then + begin + // End of current run + Flush(RunLengthCount); + RunLengthCount := 0; + end; + + if (Color = Prefix) then + // Increment run length + inc(RunLengthCount) + else + begin + // Start new run + Prefix := Color; + RunLengthCount := 1; + end; + until not(BumpPixel); + Flush(RunLengthCount); +end; + +//////////////////////////////////////////////////////////////////////////////// +// TLZWEncoder - LZW encoder +//////////////////////////////////////////////////////////////////////////////// +const + TableMaxMaxCode = (1 SHL GIFCodeBits); // + TableMaxFill = TableMaxMaxCode-1; // Clear table when it fills to + // this point. + // Note: Must be <= GIFCodeMax +type + TLZWEncoder = class(TGIFEncoder) + private + Prefix : CodeInt; // Current run color + FreeEntry : CodeInt; // next unused code in table + HashTable : THashTable; + protected + procedure Output(Value: integer); override; + procedure Clear; override; + procedure DoCompress; override; + end; + + +procedure TLZWEncoder.Output(Value: integer); +begin + inherited Output(Value); + + // If the next entry is going to be too big for the code size, + // then increase it, if possible. + if (FreeEntry > MaxCode) or (ClearFlag) then + begin + if (ClearFlag) then + begin + BitsPerCode := InitialBitsPerCode; + MaxCode := MaxCodesFromBits(BitsPerCode); + ClearFlag := False; + end else + begin + inc(BitsPerCode); + if (BitsPerCode = GIFCodeBits) then + MaxCode := TableMaxMaxCode + else + MaxCode := MaxCodesFromBits(BitsPerCode); + end; + end; +end; + +procedure TLZWEncoder.Clear; +begin + inherited Clear; + HashTable.Clear; + FreeEntry := ClearCode + 2; +end; + + +procedure TLZWEncoder.DoCompress; +var + Color : char; + NewKey : KeyInt; + NewCode : CodeInt; + +begin + HashTable := THashTable.Create; + try + // clear hash table and sync decoder + Clear; + + Pixel := Data; + Prefix := CodeInt(Pixel^); + inc(Pixel); + if (Prefix >= MaxColor) then + Error(sInvalidColor); + while (BumpPixel) do + begin + // Fetch the next pixel + Color := Pixel^; + inc(Pixel); + if (ord(Color) >= MaxColor) then + Error(sInvalidColor); + + // Append Postfix to Prefix and lookup in table... + NewKey := (KeyInt(Prefix) SHL 8) OR ord(Color); + NewCode := HashTable.Lookup(NewKey); + if (NewCode >= 0) then + begin + // ...if found, get next pixel + Prefix := NewCode; + continue; + end; + + // ...if not found, output and start over + Output(Prefix); + Prefix := CodeInt(Color); + + if (FreeEntry < TableMaxFill) then + begin + HashTable.Insert(NewKey, FreeEntry); + inc(FreeEntry); + end else + Clear; + end; + Output(Prefix); + finally + HashTable.Free; + end; +end; + +//////////////////////////////////////////////////////////////////////////////// +// +// TGIFSubImage +// +//////////////////////////////////////////////////////////////////////////////// + +///////////////////////////////////////////////////////////////////////// +// TGIFSubImage.Compress +///////////////////////////////////////////////////////////////////////// +procedure TGIFSubImage.Compress(Stream: TStream); +var + Encoder : TGIFEncoder; + BitsPerPixel : BYTE; + MaxColors : integer; +begin + if (ColorMap.Count > 0) then + begin + MaxColors := ColorMap.Count; + BitsPerPixel := ColorMap.BitsPerPixel + end else + begin + BitsPerPixel := Image.BitsPerPixel; + MaxColors := 1 SHL BitsPerPixel; + end; + + // Create a RLE or LZW GIF encoder + if (Image.Compression = gcRLE) then + Encoder := TRLEEncoder.Create + else + Encoder := TLZWEncoder.Create; + try + Encoder.Warning := Image.Warning; + Encoder.Compress(Stream, BitsPerPixel, Width, Height, Interlaced, FData, MaxColors); + finally + Encoder.Free; + end; +end; + +function TGIFExtensionList.GetExtension(Index: Integer): TGIFExtension; +begin + Result := TGIFExtension(Items[Index]); +end; + +procedure TGIFExtensionList.SetExtension(Index: Integer; Extension: TGIFExtension); +begin + Items[Index] := Extension; +end; + +procedure TGIFExtensionList.LoadFromStream(Stream: TStream; Parent: TObject); +var + b : BYTE; + Extension : TGIFExtension; + ExtensionClass : TGIFExtensionClass; +begin + // Peek ahead to determine block type + if (Stream.Read(b, 1) <> 1) then + exit; + while not(b in [bsTrailer, bsImageDescriptor]) do + begin + if (b = bsExtensionIntroducer) then + begin + ExtensionClass := TGIFExtension.FindExtension(Stream); + if (ExtensionClass = nil) then + Error(sUnknownExtension); + Stream.Seek(-1, soFromCurrent); + Extension := ExtensionClass.Create(Parent as TGIFSubImage); + try + Extension.LoadFromStream(Stream); + Add(Extension); + except + Extension.Free; + raise; + end; + end else + begin + Warning(gsWarning, sBadExtensionLabel); + break; + end; + if (Stream.Read(b, 1) <> 1) then + exit; + end; + Stream.Seek(-1, soFromCurrent); +end; + +const + { image descriptor bit masks } + idLocalColorTable = $80; { set if a local color table follows } + idInterlaced = $40; { set if image is interlaced } + idSort = $20; { set if color table is sorted } + idReserved = $0C; { reserved - must be set to $00 } + idColorTableSize = $07; { size of color table as above } + +constructor TGIFSubImage.Create(GIFImage: TGIFImage); +begin + inherited Create(GIFImage); + FExtensions := TGIFExtensionList.Create(GIFImage); + FColorMap := TGIFLocalColorMap.Create(self); + FImageDescriptor.Separator := bsImageDescriptor; + FImageDescriptor.Left := 0; + FImageDescriptor.Top := 0; + FImageDescriptor.Width := 0; + FImageDescriptor.Height := 0; + FImageDescriptor.PackedFields := 0; + FBitmap := nil; + FMask := 0; + FNeedMask := True; + FData := nil; + FDataSize := 0; + FTransparent := False; + FGCE := nil; + // Remember to synchronize with TGIFSubImage.Clear +end; + +destructor TGIFSubImage.Destroy; +begin + if (FGIFImage <> nil) then + FGIFImage.Images.Remove(self); + Clear; + FExtensions.Free; + FColorMap.Free; + if (FLocalPalette <> 0) then + DeleteObject(FLocalPalette); + inherited Destroy; +end; + +procedure TGIFSubImage.Clear; +begin + FExtensions.Clear; + FColorMap.Clear; + FreeImage; + Height := 0; + Width := 0; + FTransparent := False; + FGCE := nil; + FreeBitmap; + FreeMask; + // Remember to synchronize with TGIFSubImage.Create +end; + +function TGIFSubImage.GetEmpty: Boolean; +begin + Result := ((FData = nil) or (FDataSize = 0) or (Height = 0) or (Width = 0)); +end; + +function TGIFSubImage.GetPalette: HPALETTE; +begin + if (FBitmap <> nil) and (FBitmap.Palette <> 0) then + // Use bitmaps own palette if possible + Result := FBitmap.Palette + else if (FLocalPalette <> 0) then + // Or a previously exported local palette + Result := FLocalPalette + else if (Image.DoDither) then + begin + // or create a new dither palette + FLocalPalette := WebPalette; + Result := FLocalPalette; + end + else if (ColorMap.Count > 0) then + begin + // or create a new if first time + FLocalPalette := ColorMap.ExportPalette; + Result := FLocalPalette; + end else + // Use global palette if everything else fails + Result := Image.Palette; +end; + +procedure TGIFSubImage.SetPalette(Value: HPalette); +var + NeedNewBitmap : boolean; +begin + if (Value <> FLocalPalette) then + begin + // Zap old palette + if (FLocalPalette <> 0) then + DeleteObject(FLocalPalette); + // Zap bitmap unless new palette is same as bitmaps own + NeedNewBitmap := (FBitmap <> nil) and (Value <> FBitmap.Palette); + + // Use new palette + FLocalPalette := Value; + if (NeedNewBitmap) then + begin + // Need to create new bitmap and repaint + FreeBitmap; + Image.PaletteModified := True; + Image.Changed(Self); + end; + end; +end; + +procedure TGIFSubImage.NeedImage; +begin + if (FData = nil) then + NewImage; + if (FDataSize = 0) then + Error(sEmptyImage); +end; + +procedure TGIFSubImage.NewImage; +var + NewSize : longInt; +begin + FreeImage; + NewSize := Height * Width; + if (NewSize <> 0) then + begin + GetMem(FData, NewSize); + FillChar(FData^, NewSize, 0); + end else + FData := nil; + FDataSize := NewSize; +end; + +procedure TGIFSubImage.FreeImage; +begin + if (FData <> nil) then + FreeMem(FData); + FDataSize := 0; + FData := nil; +end; + +function TGIFSubImage.GetHasBitmap: boolean; +begin + Result := (FBitmap <> nil); +end; + +procedure TGIFSubImage.SetHasBitmap(Value: boolean); +begin + if (Value <> (FBitmap <> nil)) then + begin + if (Value) then + Bitmap // Referencing Bitmap will automatically create it + else + FreeBitmap; + end; +end; + +procedure TGIFSubImage.NewBitmap; +begin + FreeBitmap; + FBitmap := TBitmap.Create; +end; + +procedure TGIFSubImage.FreeBitmap; +begin + if (FBitmap <> nil) then + begin + FBitmap.Free; + FBitmap := nil; + end; +end; + +procedure TGIFSubImage.FreeMask; +begin + if (FMask <> 0) then + begin + DeleteObject(FMask); + FMask := 0; + end; + FNeedMask := True; +end; + +function TGIFSubImage.HasMask: boolean; +begin + if (FNeedMask) and (Transparent) then + begin + // Zap old bitmap + FreeBitmap; + // Create new bitmap and mask + GetBitmap; + end; + Result := (FMask <> 0); +end; + +function TGIFSubImage.GetBounds(Index: integer): WORD; +begin + case (Index) of + 1: Result := FImageDescriptor.Left; + 2: Result := FImageDescriptor.Top; + 3: Result := FImageDescriptor.Width; + 4: Result := FImageDescriptor.Height; + else + Result := 0; // To avoid compiler warnings + end; +end; + +procedure TGIFSubImage.SetBounds(Index: integer; Value: WORD); +begin + case (Index) of + 1: DoSetBounds(Value, FImageDescriptor.Top, FImageDescriptor.Width, FImageDescriptor.Height); + 2: DoSetBounds(FImageDescriptor.Left, Value, FImageDescriptor.Width, FImageDescriptor.Height); + 3: DoSetBounds(FImageDescriptor.Left, FImageDescriptor.Top, Value, FImageDescriptor.Height); + 4: DoSetBounds(FImageDescriptor.Left, FImageDescriptor.Top, FImageDescriptor.Width, Value); + end; +end; + +{$IFOPT R+} + {$DEFINE R_PLUS} + {$RANGECHECKS OFF} +{$ENDIF} +function TGIFSubImage.DoGetDitherBitmap: TBitmap; +var + ColorLookup : TColorLookup; + Ditherer : TDitherEngine; + DIBResult : TDIB; + Src : PChar; + Dst : PChar; + + Row : integer; + Color : TGIFColor; + ColMap : PColorMap; + Index : byte; + TransparentIndex : byte; + IsTransparent : boolean; + WasTransparent : boolean; + MappedTransparentIndex: char; + + MaskBits : PChar; + MaskDest : PChar; + MaskRow : PChar; + MaskRowWidth , + MaskRowBitWidth : integer; + Bit , + RightBit : BYTE; + +begin + Result := TBitmap.Create; + try + +{$IFNDEF VER9x} + if (Width*Height > BitmapAllocationThreshold) then + SetPixelFormat(Result, pf1bit); // To reduce resource consumption of resize +{$ENDIF} + + if (Empty) then + begin + // Set bitmap width and height + Result.Width := Width; + Result.Height := Height; + + // Build and copy palette to bitmap + Result.Palette := CopyPalette(Palette); + + exit; + end; + + ColorLookup := nil; + Ditherer := nil; + DIBResult := nil; + try // Protect above resources + ColorLookup := TNetscapeColorLookup.Create(Palette); + Ditherer := TFloydSteinbergDitherer.Create(Width, ColorLookup); + // Get DIB buffer for scanline operations + // It is assumed that the source palette is the 216 color Netscape palette + DIBResult := TDIBWriter.Create(Result, pf8bit, Width, Height, Palette); + + // Determine if this image is transparent + ColMap := ActiveColorMap.Data; + IsTransparent := FNeedMask and Transparent; + WasTransparent := False; + FNeedMask := False; + TransparentIndex := 0; + MappedTransparentIndex := #0; + if (FMask = 0) and (IsTransparent) then + begin + IsTransparent := True; + TransparentIndex := GraphicControlExtension.TransparentColorIndex; + Color := ColMap[ord(TransparentIndex)]; + MappedTransparentIndex := char(Color.Blue DIV 51 + + MulDiv(6, Color.Green, 51) + MulDiv(36, Color.Red, 51)+1); + end; + + // Allocate bit buffer for transparency mask + MaskDest := nil; + Bit := $00; + if (IsTransparent) then + begin + MaskRowWidth := ((Width+15) DIV 16) * 2; + MaskRowBitWidth := (Width+7) DIV 8; + RightBit := $01 SHL ((8 - (Width AND $0007)) AND $0007); + GetMem(MaskBits, MaskRowWidth * Height); + FillChar(MaskBits^, MaskRowWidth * Height, 0); + end else + begin + MaskBits := nil; + MaskRowWidth := 0; + MaskRowBitWidth := 0; + RightBit := $00; + end; + + try + // Process the image + Row := 0; + MaskRow := MaskBits; + Src := FData; + while (Row < Height) do + begin + if ((Row AND $1F) = 0) then + Image.Progress(Self, psRunning, MulDiv(Row, 100, Height), + False, Rect(0,0,0,0), sProgressRendering); + + Dst := DIBResult.ScanLine[Row]; + if (IsTransparent) then + begin + // Preset all pixels to transparent + FillChar(Dst^, Width, ord(MappedTransparentIndex)); + if (Ditherer.Direction = 1) then + begin + MaskDest := MaskRow; + Bit := $80; + end else + begin + MaskDest := MaskRow + MaskRowBitWidth-1; + Bit := RightBit; + end; + end; + inc(Dst, Ditherer.Column); + + while (Ditherer.Column < Ditherer.Width) and (Ditherer.Column >= 0) do + begin + Index := ord(Src^); + Color := ColMap[ord(Index)]; + + if (IsTransparent) and (Index = TransparentIndex) then + begin + MaskDest^ := char(byte(MaskDest^) OR Bit); + WasTransparent := True; + Ditherer.NextColumn; + end else + begin + // Dither and map a single pixel + Dst^ := Ditherer.Dither(Color.Red, Color.Green, Color.Blue, + Color.Red, Color.Green, Color.Blue); + end; + + if (IsTransparent) then + begin + if (Ditherer.Direction = 1) then + begin + Bit := Bit SHR 1; + if (Bit = $00) then + begin + Bit := $80; + inc(MaskDest, 1); + end; + end else + begin + Bit := Bit SHL 1; + if (Bit = $00) then + begin + Bit := $01; + dec(MaskDest, 1); + end; + end; + end; + + inc(Src, Ditherer.Direction); + inc(Dst, Ditherer.Direction); + end; + + if (IsTransparent) then + Inc(MaskRow, MaskRowWidth); + Inc(Row); + inc(Src, Width-Ditherer.Direction); + Ditherer.NextLine; + end; + + // Transparent paint needs a mask bitmap + if (IsTransparent) and (WasTransparent) then + FMask := CreateBitmap(Width, Height, 1, 1, MaskBits); + finally + if (MaskBits <> nil) then + FreeMem(MaskBits); + end; + finally + if (ColorLookup <> nil) then + ColorLookup.Free; + if (Ditherer <> nil) then + Ditherer.Free; + if (DIBResult <> nil) then + DIBResult.Free; + end; + except + Result.Free; + raise; + end; +end; +{$IFDEF R_PLUS} + {$RANGECHECKS ON} + {$UNDEF R_PLUS} +{$ENDIF} + +function TGIFSubImage.DoGetBitmap: TBitmap; +var + ScanLineRow : Integer; + DIBResult : TDIB; + DestScanLine , + Src : PChar; + TransparentIndex : byte; + IsTransparent : boolean; + WasTransparent : boolean; + + MaskBits : PChar; + MaskDest : PChar; + MaskRow : PChar; + MaskRowWidth : integer; + Col : integer; + MaskByte : byte; + Bit : byte; +begin + Result := TBitmap.Create; + try + +{$IFNDEF VER9x} + if (Width*Height > BitmapAllocationThreshold) then + SetPixelFormat(Result, pf1bit); // To reduce resource consumption of resize +{$ENDIF} + + if (Empty) then + begin + // Set bitmap width and height + Result.Width := Width; + Result.Height := Height; + + // Build and copy palette to bitmap + Result.Palette := CopyPalette(Palette); + + exit; + end; + + // Get DIB buffer for scanline operations + DIBResult := TDIBWriter.Create(Result, pf8bit, Width, Height, Palette); + try + + // Determine if this image is transparent + IsTransparent := FNeedMask and Transparent; + WasTransparent := False; + FNeedMask := False; + TransparentIndex := 0; + if (FMask = 0) and (IsTransparent) then + begin + IsTransparent := True; + TransparentIndex := GraphicControlExtension.TransparentColorIndex; + end; + // Allocate bit buffer for transparency mask + if (IsTransparent) then + begin + MaskRowWidth := ((Width+15) DIV 16) * 2; + GetMem(MaskBits, MaskRowWidth * Height); + FillChar(MaskBits^, MaskRowWidth * Height, 0); + IsTransparent := (MaskBits <> nil); + end else + begin + MaskBits := nil; + MaskRowWidth := 0; + end; + + try + ScanLineRow := 0; + Src := FData; + MaskRow := MaskBits; + while (ScanLineRow < Height) do + begin + DestScanline := DIBResult.ScanLine[ScanLineRow]; + + if ((ScanLineRow AND $1F) = 0) then + Image.Progress(Self, psRunning, MulDiv(ScanLineRow, 100, Height), + False, Rect(0,0,0,0), sProgressRendering); + + Move(Src^, DestScanline^, Width); + Inc(ScanLineRow); + + if (IsTransparent) then + begin + Bit := $80; + MaskDest := MaskRow; + MaskByte := 0; + for Col := 0 to Width-1 do + begin + // Set a bit in the mask if the pixel is transparent + if (Src^ = char(TransparentIndex)) then + MaskByte := MaskByte OR Bit; + + Bit := Bit SHR 1; + if (Bit = $00) then + begin + // Store a mask byte for each 8 pixels + Bit := $80; + WasTransparent := WasTransparent or (MaskByte <> 0); + MaskDest^ := char(MaskByte); + inc(MaskDest); + MaskByte := 0; + end; + Inc(Src); + end; + // Save the last mask byte in case the width isn't divisable by 8 + if (MaskByte <> 0) then + begin + WasTransparent := True; + MaskDest^ := char(MaskByte); + end; + Inc(MaskRow, MaskRowWidth); + end else + Inc(Src, Width); + end; + + // Transparent paint needs a mask bitmap + if (IsTransparent) and (WasTransparent) then + FMask := CreateBitmap(Width, Height, 1, 1, MaskBits); + finally + if (MaskBits <> nil) then + FreeMem(MaskBits); + end; + finally + // Free DIB buffer used for scanline operations + DIBResult.Free; + end; + except + Result.Free; + raise; + end; +end; + +{$ifdef DEBUG_RENDERPERFORMANCE} +var + ImageCount : DWORD = 0; + RenderTime : DWORD = 0; +{$endif} +function TGIFSubImage.GetBitmap: TBitmap; +var + n : integer; +{$ifdef DEBUG_RENDERPERFORMANCE} + RenderStartTime : DWORD; +{$endif} +begin +{$ifdef DEBUG_RENDERPERFORMANCE} + if (GetAsyncKeyState(VK_CONTROL) <> 0) then + begin + ShowMessage(format('Render %d images in %d mS, Rate %d mS/image (%d images/S)', + [ImageCount, RenderTime, + RenderTime DIV (ImageCount+1), + MulDiv(ImageCount, 1000, RenderTime+1)])); + end; +{$endif} + Result := FBitmap; + if (Result <> nil) or (Empty) then + Exit; + +{$ifdef DEBUG_RENDERPERFORMANCE} + inc(ImageCount); + RenderStartTime := timeGetTime; +{$endif} + try + Image.Progress(Self, psStarting, 0, False, Rect(0,0,0,0), sProgressRendering); + try + + if (Image.DoDither) then + // Create dithered bitmap + FBitmap := DoGetDitherBitmap + else + // Create "regular" bitmap + FBitmap := DoGetBitmap; + + Result := FBitmap; + + finally + if ExceptObject = nil then + n := 100 + else + n := 0; + Image.Progress(Self, psEnding, n, Image.PaletteModified, Rect(0,0,0,0), + sProgressRendering); + // Make sure new palette gets realized, in case OnProgress event didn't. + if Image.PaletteModified then + Image.Changed(Self); + end; + except + on EAbort do ; // OnProgress can raise EAbort to cancel image load + end; +{$ifdef DEBUG_RENDERPERFORMANCE} + inc(RenderTime, timeGetTime-RenderStartTime); +{$endif} +end; + +procedure TGIFSubImage.SetBitmap(Value: TBitmap); +begin + FreeBitmap; + if (Value <> nil) then + Assign(Value); +end; + +function TGIFSubImage.GetActiveColorMap: TGIFColorMap; +begin + if (ColorMap.Count > 0) or (Image.GlobalColorMap.Count = 0) then + Result := ColorMap + else + Result := Image.GlobalColorMap; +end; + +function TGIFSubImage.GetInterlaced: boolean; +begin + Result := (FImageDescriptor.PackedFields AND idInterlaced) <> 0; +end; + +procedure TGIFSubImage.SetInterlaced(Value: boolean); +begin + if (Value) then + FImageDescriptor.PackedFields := FImageDescriptor.PackedFields OR idInterlaced + else + FImageDescriptor.PackedFields := FImageDescriptor.PackedFields AND NOT(idInterlaced); +end; + +function TGIFSubImage.GetVersion: TGIFVersion; +var + v : TGIFVersion; + i : integer; +begin + if (ColorMap.Optimized) then + Result := gv89a + else + Result := inherited GetVersion; + i := 0; + while (Result < high(TGIFVersion)) and (i < FExtensions.Count) do + begin + v := FExtensions[i].Version; + if (v > Result) then + Result := v; + end; +end; + +function TGIFSubImage.GetColorResolution: integer; +begin + Result := ColorMap.BitsPerPixel-1; +end; + +function TGIFSubImage.GetBitsPerPixel: integer; +begin + Result := ColorMap.BitsPerPixel; +end; + +function TGIFSubImage.GetBoundsRect: TRect; +begin + Result := Rect(FImageDescriptor.Left, + FImageDescriptor.Top, + FImageDescriptor.Left+FImageDescriptor.Width, + FImageDescriptor.Top+FImageDescriptor.Height); +end; + +procedure TGIFSubImage.DoSetBounds(ALeft, ATop, AWidth, AHeight: integer); +var + TooLarge : boolean; + Zap : boolean; +begin + Zap := (FImageDescriptor.Width <> Width) or (FImageDescriptor.Height <> AHeight); + FImageDescriptor.Left := ALeft; + FImageDescriptor.Top := ATop; + FImageDescriptor.Width := AWidth; + FImageDescriptor.Height := AHeight; + + // Delete existing image and bitmaps if size has changed + if (Zap) then + begin + FreeBitmap; + FreeMask; + FreeImage; + // ...and allocate a new image + NewImage; + end; + + TooLarge := False; + // Set width & height if added image is larger than existing images +{$IFDEF STRICT_MOZILLA} + // From Mozilla source: + // Work around broken GIF files where the logical screen + // size has weird width or height. [...] + if (Image.Width < AWidth) or (Image.Height < AHeight) then + begin + TooLarge := True; + Image.Width := AWidth; + Image.Height := AHeight; + Left := 0; + Top := 0; + end; +{$ELSE} + if (Image.Width < ALeft+AWidth) then + begin + if (Image.Width > 0) then + begin + TooLarge := True; + Warning(gsWarning, sBadWidth) + end; + Image.Width := ALeft+AWidth; + end; + if (Image.Height < ATop+AHeight) then + begin + if (Image.Height > 0) then + begin + TooLarge := True; + Warning(gsWarning, sBadHeight) + end; + Image.Height := ATop+AHeight; + end; +{$ENDIF} + + if (TooLarge) then + Warning(gsWarning, sScreenSizeExceeded); +end; + +procedure TGIFSubImage.SetBoundsRect(const Value: TRect); +begin + DoSetBounds(Value.Left, Value.Top, Value.Right-Value.Left+1, Value.Bottom-Value.Top+1); +end; + +function TGIFSubImage.GetClientRect: TRect; +begin + Result := Rect(0, 0, FImageDescriptor.Width, FImageDescriptor.Height); +end; + +function TGIFSubImage.GetPixel(x, y: integer): BYTE; +begin + if (x < 0) or (x > Width-1) then + Error(sBadPixelCoordinates); + Result := BYTE(PChar(longInt(Scanline[y]) + x)^); +end; + +function TGIFSubImage.GetScanline(y: integer): pointer; +begin + if (y < 0) or (y > Height-1) then + Error(sBadPixelCoordinates); + NeedImage; + Result := pointer(longInt(FData) + y * Width); +end; + +procedure TGIFSubImage.Prepare; +var + Pack : BYTE; +begin + Pack := FImageDescriptor.PackedFields; + if (ColorMap.Count > 0) then + begin + Pack := idLocalColorTable; + if (ColorMap.Optimized) then + Pack := Pack OR idSort; + Pack := (Pack AND NOT(idColorTableSize)) OR (ColorResolution AND idColorTableSize); + end else + Pack := Pack AND NOT(idLocalColorTable OR idSort OR idColorTableSize); + FImageDescriptor.PackedFields := Pack; +end; + +procedure TGIFSubImage.SaveToStream(Stream: TStream); +begin + FExtensions.SaveToStream(Stream); + if (Empty) then + exit; + Prepare; + Stream.Write(FImageDescriptor, sizeof(TImageDescriptor)); + ColorMap.SaveToStream(Stream); + Compress(Stream); +end; + +procedure TGIFSubImage.LoadFromStream(Stream: TStream); +var + ColorCount : integer; + b : BYTE; +begin + Clear; + FExtensions.LoadFromStream(Stream, self); + // Check for extension without image + if (Stream.Read(b, 1) <> 1) then + exit; + Stream.Seek(-1, soFromCurrent); + if (b = bsTrailer) or (b = 0) then + exit; + + ReadCheck(Stream, FImageDescriptor, sizeof(TImageDescriptor)); + + // From Mozilla source: + // Work around more broken GIF files that have zero image + // width or height + if (FImageDescriptor.Height = 0) or (FImageDescriptor.Width = 0) then + begin + FImageDescriptor.Height := Image.Height; + FImageDescriptor.Width := Image.Width; + Warning(gsWarning, sScreenSizeExceeded); + end; + + if (FImageDescriptor.PackedFields AND idLocalColorTable = idLocalColorTable) then + begin + ColorCount := 2 SHL (FImageDescriptor.PackedFields AND idColorTableSize); + if (ColorCount < 2) or (ColorCount > 256) then + Error(sImageBadColorSize); + ColorMap.LoadFromStream(Stream, ColorCount); + end; + + Decompress(Stream); + + // On-load rendering + if (GIFImageRenderOnLoad) then + // Touch bitmap to force frame to be rendered + Bitmap; +end; + +procedure TGIFSubImage.AssignTo(Dest: TPersistent); +begin + if (Dest is TBitmap) then + Dest.Assign(Bitmap) + else + inherited AssignTo(Dest); +end; + +procedure TGIFSubImage.Assign(Source: TPersistent); +var + MemoryStream : TMemoryStream; + i : integer; + PixelFormat : TPixelFormat; + DIBSource : TDIB; + ABitmap : TBitmap; + + procedure Import8Bit(Dest: PChar); + var + y : integer; + begin + // Copy colormap +{$ifdef VER10_PLUS} + if (FBitmap.HandleType = bmDIB) then + FColorMap.ImportDIBColors(FBitmap.Canvas.Handle) + else +{$ENDIF} + FColorMap.ImportPalette(FBitmap.Palette); + // Copy pixels + for y := 0 to Height-1 do + begin + if ((y AND $1F) = 0) then + Image.Progress(Self, psRunning, MulDiv(y, 100, Height), False, Rect(0,0,0,0), sProgressConverting); + Move(DIBSource.Scanline[y]^, Dest^, Width); + inc(Dest, Width); + end; + end; + + procedure Import4Bit(Dest: PChar); + var + x, y : integer; + Scanline : PChar; + begin + // Copy colormap + FColorMap.ImportPalette(FBitmap.Palette); + // Copy pixels + for y := 0 to Height-1 do + begin + if ((y AND $1F) = 0) then + Image.Progress(Self, psRunning, MulDiv(y, 100, Height), False, Rect(0,0,0,0), sProgressConverting); + ScanLine := DIBSource.Scanline[y]; + for x := 0 to Width-1 do + begin + if (x AND $01 = 0) then + Dest^ := chr(ord(ScanLine^) SHR 4) + else + begin + Dest^ := chr(ord(ScanLine^) AND $0F); + inc(ScanLine); + end; + inc(Dest); + end; + end; + end; + + procedure Import1Bit(Dest: PChar); + var + x, y : integer; + Scanline : PChar; + Bit : integer; + Byte : integer; + begin + // Copy colormap + FColorMap.ImportPalette(FBitmap.Palette); + // Copy pixels + for y := 0 to Height-1 do + begin + if ((y AND $1F) = 0) then + Image.Progress(Self, psRunning, MulDiv(y, 100, Height), False, Rect(0,0,0,0), sProgressConverting); + ScanLine := DIBSource.Scanline[y]; + x := Width; + Bit := 0; + Byte := 0; // To avoid compiler warning + while (x > 0) do + begin + if (Bit = 0) then + begin + Bit := 8; + Byte := ord(ScanLine^); + inc(Scanline); + end; + Dest^ := chr((Byte AND $80) SHR 7); + Byte := Byte SHL 1; + inc(Dest); + dec(Bit); + dec(x); + end; + end; + end; + + procedure Import24Bit(Dest: PChar); + type + TCacheEntry = record + Color : TColor; + Index : integer; + end; + const + // Size of palette cache. Must be 2^n. + // The cache holds the palette index of the last "CacheSize" colors + // processed. Hopefully the cache can speed things up a bit... Initial + // testing shows that this is indeed the case at least for non-dithered + // bitmaps. + // All the same, a small hash table would probably be much better. + CacheSize = 8; + var + i : integer; + Cache : array[0..CacheSize-1] of TCacheEntry; + LastEntry : integer; + Scanline : PRGBTriple; + Pixel : TColor; + RGBTriple : TRGBTriple absolute Pixel; + x, y : integer; + ColorMap : PColorMap; + t : byte; + label + NextPixel; + begin + for i := 0 to CacheSize-1 do + Cache[i].Index := -1; + LastEntry := 0; + + // Copy all pixels and build colormap + for y := 0 to Height-1 do + begin + if ((y AND $1F) = 0) then + Image.Progress(Self, psRunning, MulDiv(y, 100, Height), False, Rect(0,0,0,0), sProgressConverting); + ScanLine := DIBSource.Scanline[y]; + for x := 0 to Width-1 do + begin + Pixel := 0; + RGBTriple := Scanline^; + // Scan cache for color from most recently processed color to last + // recently processed. This is done because TColorMap.AddUnique is very slow. + i := LastEntry; + repeat + if (Cache[i].Index = -1) then + break; + if (Cache[i].Color = Pixel) then + begin + Dest^ := chr(Cache[i].Index); + LastEntry := i; + goto NextPixel; + end; + if (i = 0) then + i := CacheSize-1 + else + dec(i); + until (i = LastEntry); + // Color not found in cache, do it the slow way instead + Dest^ := chr(FColorMap.AddUnique(Pixel)); + // Add color and index to cache + LastEntry := (LastEntry + 1) AND (CacheSize-1); + Cache[LastEntry].Color := Pixel; + Cache[LastEntry].Index := ord(Dest^); + + NextPixel: + Inc(Dest); + Inc(Scanline); + end; + end; + // Convert colors in colormap from BGR to RGB + ColorMap := FColorMap.Data; + i := FColorMap.Count; + while (i > 0) do + begin + t := ColorMap^[0].Red; + ColorMap^[0].Red := ColorMap^[0].Blue; + ColorMap^[0].Blue := t; + inc(integer(ColorMap), sizeof(TGIFColor)); + dec(i); + end; + end; + + procedure ImportViaDraw(ABitmap: TBitmap; Graphic: TGraphic); + begin + ABitmap.Height := Graphic.Height; + ABitmap.Width := Graphic.Width; + + // Note: Disable the call to SafeSetPixelFormat below to import + // in max number of colors with the risk of having to use + // TCanvas.Pixels to do it (very slow). + + // Make things a little easier for TGIFSubImage.Assign by converting + // pfDevice to a more import friendly format +{$ifdef SLOW_BUT_SAFE} + SafeSetPixelFormat(ABitmap, pf8bit); +{$else} +{$ifndef VER9x} + SetPixelFormat(ABitmap, pf24bit); +{$endif} +{$endif} + ABitmap.Canvas.Draw(0, 0, Graphic); + end; + + procedure AddMask(Mask: TBitmap); + var + DIBReader : TDIBReader; + TransparentIndex : integer; + i , + j : integer; + GIFPixel , + MaskPixel : PChar; + WasTransparent : boolean; + GCE : TGIFGraphicControlExtension; + begin + // Optimize colormap to make room for transparent color + ColorMap.Optimize; + // Can't make transparent if no color or colormap full + if (ColorMap.Count = 0) or (ColorMap.Count = 256) then + exit; + + // Add the transparent color to the color map + TransparentIndex := ColorMap.Add(TColor(0)); + WasTransparent := False; + + DIBReader := TDIBReader.Create(Mask, pf8bit); + try + for i := 0 to Height-1 do + begin + MaskPixel := DIBReader.Scanline[i]; + GIFPixel := Scanline[i]; + for j := 0 to Width-1 do + begin + // Change all unmasked pixels to transparent + if (MaskPixel^ <> #0) then + begin + GIFPixel^ := chr(TransparentIndex); + WasTransparent := True; + end; + inc(MaskPixel); + inc(GIFPixel); + end; + end; + finally + DIBReader.Free; + end; + + // Add a Graphic Control Extension if any part of the mask was transparent + if (WasTransparent) then + begin + GCE := TGIFGraphicControlExtension.Create(self); + GCE.Transparent := True; + GCE.TransparentColorIndex := TransparentIndex; + Extensions.Add(GCE); + end else + // Otherwise removed the transparency color since it wasn't used + ColorMap.Delete(TransparentIndex); + end; + + procedure AddMaskOnly(hMask: hBitmap); + var + Mask : TBitmap; + begin + if (hMask = 0) then + exit; + + // Encapsulate the mask + Mask := TBitmap.Create; + try + Mask.Handle := hMask; + AddMask(Mask); + finally + Mask.ReleaseHandle; + Mask.Free; + end; + end; + + procedure AddIconMask(Icon: TIcon); + var + IconInfo : TIconInfo; + begin + if (not GetIconInfo(Icon.Handle, IconInfo)) then + exit; + + // Extract the icon mask + AddMaskOnly(IconInfo.hbmMask); + end; + + procedure AddMetafileMask(Metafile: TMetaFile); + var + Mask1 , + Mask2 : TBitmap; + + procedure DrawMetafile(ABitmap: TBitmap; Background: TColor); + begin + ABitmap.Width := Metafile.Width; + ABitmap.Height := Metafile.Height; +{$ifndef VER9x} + SetPixelFormat(ABitmap, pf24bit); +{$endif} + ABitmap.Canvas.Brush.Color := Background; + ABitmap.Canvas.Brush.Style := bsSolid; + ABitmap.Canvas.FillRect(ABitmap.Canvas.ClipRect); + ABitmap.Canvas.Draw(0,0, Metafile); + end; + + begin + // Create the metafile mask + Mask1 := TBitmap.Create; + try + Mask2 := TBitmap.Create; + try + DrawMetafile(Mask1, clWhite); + DrawMetafile(Mask2, clBlack); + Mask1.Canvas.CopyMode := cmSrcInvert; + Mask1.Canvas.Draw(0,0, Mask2); + AddMask(Mask1); + finally + Mask2.Free; + end; + finally + Mask1.Free; + end; + end; + +begin + if (Source = self) then + exit; + if (Source = nil) then + begin + Clear; + end else + // + // TGIFSubImage import + // + if (Source is TGIFSubImage) then + begin + // Zap existing colormap, extensions and bitmap + Clear; + if (TGIFSubImage(Source).Empty) then + exit; + // Copy source data + FImageDescriptor := TGIFSubImage(Source).FImageDescriptor; + FTransparent := TGIFSubImage(Source).Transparent; + // Copy image data + NewImage; + if (FData <> nil) and (TGIFSubImage(Source).Data <> nil) then + Move(TGIFSubImage(Source).Data^, FData^, FDataSize); + // Copy palette + FColorMap.Assign(TGIFSubImage(Source).ColorMap); + // Copy extensions + if (TGIFSubImage(Source).Extensions.Count > 0) then + begin + MemoryStream := TMemoryStream.Create; + try + TGIFSubImage(Source).Extensions.SaveToStream(MemoryStream); + MemoryStream.Seek(0, soFromBeginning); + Extensions.LoadFromStream(MemoryStream, Self); + finally + MemoryStream.Free; + end; + end; + + // Copy bitmap representation + // (Not really nescessary but improves performance if the bitmap is needed + // later on) + if (TGIFSubImage(Source).HasBitmap) then + begin + NewBitmap; + FBitmap.Assign(TGIFSubImage(Source).Bitmap); + end; + end else + // + // Bitmap import + // + if (Source is TBitmap) then + begin + // Zap existing colormap, extensions and bitmap + Clear; + if (TBitmap(Source).Empty) then + exit; + + Width := TBitmap(Source).Width; + Height := TBitmap(Source).Height; + + PixelFormat := GetPixelFormat(TBitmap(Source)); +{$ifdef VER9x} + // Delphi 2 TBitmaps are always DDBs. This means that if a 24 bit + // bitmap is loaded in 8 bit device mode, TBitmap.PixelFormat will + // be pf8bit, but TBitmap.Palette will be 0! + if (TBitmap(Source).Palette = 0) then + PixelFormat := pfDevice; +{$endif} + if (PixelFormat > pf8bit) or (PixelFormat = pfDevice) then + begin + // Convert image to 8 bits/pixel or less + FBitmap := ReduceColors(TBitmap(Source), Image.ColorReduction, + Image.DitherMode, Image.ReductionBits, 0); + PixelFormat := GetPixelFormat(FBitmap); + end else + begin + // Create new bitmap and copy + NewBitmap; + FBitmap.Assign(TBitmap(Source)); + end; + + // Allocate new buffer + NewImage; + + Image.Progress(Self, psStarting, 0, False, Rect(0,0,0,0), sProgressConverting); + try +{$ifdef VER9x} + // This shouldn't happen, but better safe... + if (FBitmap.Palette = 0) then + PixelFormat := pf24bit; +{$endif} + if (not(PixelFormat in [pf1bit, pf4bit, pf8bit, pf24bit])) then + PixelFormat := pf24bit; + DIBSource := TDIBReader.Create(FBitmap, PixelFormat); + try + // Copy pixels + case (PixelFormat) of + pf8bit: Import8Bit(Fdata); + pf4bit: Import4Bit(Fdata); + pf1bit: Import1Bit(Fdata); + else +// Error(sUnsupportedBitmap); + Import24Bit(Fdata); + end; + + finally + DIBSource.Free; + end; + +{$ifdef VER10_PLUS} + // Add mask for transparent bitmaps + if (TBitmap(Source).Transparent) then + AddMaskOnly(TBitmap(Source).MaskHandle); +{$endif} + + finally + if ExceptObject = nil then + i := 100 + else + i := 0; + Image.Progress(Self, psEnding, i, Image.PaletteModified, Rect(0,0,0,0), sProgressConverting); + end; + end else + // + // TGraphic import + // + if (Source is TGraphic) then + begin + // Zap existing colormap, extensions and bitmap + Clear; + if (TGraphic(Source).Empty) then + exit; + + ABitmap := TBitmap.Create; + try + // Import TIcon and TMetafile by drawing them onto a bitmap... + // ...and then importing the bitmap recursively + if (Source is TIcon) or (Source is TMetafile) then + begin + try + ImportViaDraw(ABitmap, TGraphic(Source)) + except + // If import via TCanvas.Draw fails (which it shouldn't), we try the + // Assign mechanism instead + ABitmap.Assign(Source); + end; + end else + try + ABitmap.Assign(Source); + except + // If automatic conversion to bitmap fails, we try and draw the + // graphic on the bitmap instead + ImportViaDraw(ABitmap, TGraphic(Source)); + end; + // Convert the bitmap to a GIF frame recursively + Assign(ABitmap); + finally + ABitmap.Free; + end; + + // Import transparency mask + if (Source is TIcon) then + AddIconMask(TIcon(Source)); + if (Source is TMetaFile) then + AddMetafileMask(TMetaFile(Source)); + + end else + // + // TPicture import + // + if (Source is TPicture) then + begin + // Recursively import TGraphic + Assign(TPicture(Source).Graphic); + end else + // Unsupported format - fall back to Source.AssignTo + inherited Assign(Source); +end; + +// Copied from D3 graphics.pas +// Fixed by Brian Lowe of Acro Technology Inc. 30Jan98 +function TransparentStretchBlt(DstDC: HDC; DstX, DstY, DstW, DstH: Integer; + SrcDC: HDC; SrcX, SrcY, SrcW, SrcH: Integer; MaskDC: HDC; MaskX, + MaskY: Integer): Boolean; +const + ROP_DstCopy = $00AA0029; +var + MemDC , + OrMaskDC : HDC; + MemBmp , + OrMaskBmp : HBITMAP; + Save , + OrMaskSave : THandle; + crText, crBack : TColorRef; + SavePal : HPALETTE; + +begin + Result := True; + if (Win32Platform = VER_PLATFORM_WIN32_NT) and (SrcW = DstW) and (SrcH = DstH) then + begin + MemBmp := GDICheck(CreateCompatibleBitmap(SrcDC, 1, 1)); + MemBmp := SelectObject(MaskDC, MemBmp); + try + MaskBlt(DstDC, DstX, DstY, DstW, DstH, SrcDC, SrcX, SrcY, MemBmp, MaskX, + MaskY, MakeRop4(ROP_DstCopy, SrcCopy)); + finally + MemBmp := SelectObject(MaskDC, MemBmp); + DeleteObject(MemBmp); + end; + Exit; + end; + + SavePal := 0; + MemDC := GDICheck(CreateCompatibleDC(DstDC)); + try + { Color bitmap for combining OR mask with source bitmap } + MemBmp := GDICheck(CreateCompatibleBitmap(DstDC, SrcW, SrcH)); + try + Save := SelectObject(MemDC, MemBmp); + try + { This bitmap needs the size of the source but DC of the dest } + OrMaskDC := GDICheck(CreateCompatibleDC(DstDC)); + try + { Need a monochrome bitmap for OR mask!! } + OrMaskBmp := GDICheck(CreateBitmap(SrcW, SrcH, 1, 1, nil)); + try + OrMaskSave := SelectObject(OrMaskDC, OrMaskBmp); + try + + // OrMask := 1 + // Original: BitBlt(OrMaskDC, SrcX, SrcY, SrcW, SrcH, OrMaskDC, SrcX, SrcY, WHITENESS); + // Replacement, but not needed: PatBlt(OrMaskDC, SrcX, SrcY, SrcW, SrcH, WHITENESS); + // OrMask := OrMask XOR Mask + // Not needed: BitBlt(OrMaskDC, SrcX, SrcY, SrcW, SrcH, MaskDC, SrcX, SrcY, SrcInvert); + // OrMask := NOT Mask + BitBlt(OrMaskDC, SrcX, SrcY, SrcW, SrcH, MaskDC, SrcX, SrcY, NotSrcCopy); + + // Retrieve source palette (with dummy select) + SavePal := SelectPalette(SrcDC, SystemPalette16, False); + // Restore source palette + SelectPalette(SrcDC, SavePal, False); + // Select source palette into memory buffer + if SavePal <> 0 then + SavePal := SelectPalette(MemDC, SavePal, True) + else + SavePal := SelectPalette(MemDC, SystemPalette16, True); + RealizePalette(MemDC); + + // Mem := OrMask + BitBlt(MemDC, SrcX, SrcY, SrcW, SrcH, OrMaskDC, SrcX, SrcY, SrcCopy); + // Mem := Mem AND Src +{$IFNDEF GIF_TESTMASK} // Define GIF_TESTMASK if you want to know what it does... + BitBlt(MemDC, SrcX, SrcY, SrcW, SrcH, SrcDC, SrcX, SrcY, SrcAnd); +{$ELSE} + StretchBlt(DstDC, DstX, DstY, DstW DIV 2, DstH, MemDC, SrcX, SrcY, SrcW, SrcH, SrcCopy); + StretchBlt(DstDC, DstX+DstW DIV 2, DstY, DstW DIV 2, DstH, SrcDC, SrcX, SrcY, SrcW, SrcH, SrcCopy); + exit; +{$ENDIF} + finally + if (OrMaskSave <> 0) then + SelectObject(OrMaskDC, OrMaskSave); + end; + finally + DeleteObject(OrMaskBmp); + end; + finally + DeleteDC(OrMaskDC); + end; + + crText := SetTextColor(DstDC, $00000000); + crBack := SetBkColor(DstDC, $00FFFFFF); + + { All color rendering is done at 1X (no stretching), + then final 2 masks are stretched to dest DC } + // Neat trick! + // Dst := Dst AND Mask + StretchBlt(DstDC, DstX, DstY, DstW, DstH, MaskDC, SrcX, SrcY, SrcW, SrcH, SrcAnd); + // Dst := Dst OR Mem + StretchBlt(DstDC, DstX, DstY, DstW, DstH, MemDC, SrcX, SrcY, SrcW, SrcH, SrcPaint); + + SetTextColor(DstDC, crText); + SetTextColor(DstDC, crBack); + + finally + if (Save <> 0) then + SelectObject(MemDC, Save); + end; + finally + DeleteObject(MemBmp); + end; + finally + if (SavePal <> 0) then + SelectPalette(MemDC, SavePal, False); + DeleteDC(MemDC); + end; +end; + +procedure TGIFSubImage.Draw(ACanvas: TCanvas; const Rect: TRect; + DoTransparent, DoTile: boolean); +begin + if (DoTile) then + StretchDraw(ACanvas, Rect, DoTransparent, DoTile) + else + StretchDraw(ACanvas, ScaleRect(Rect), DoTransparent, DoTile); +end; + +type + // Dummy class used to gain access to protected method TCanvas.Changed + TChangableCanvas = class(TCanvas) + end; + +procedure TGIFSubImage.StretchDraw(ACanvas: TCanvas; const Rect: TRect; + DoTransparent, DoTile: boolean); +var + MaskDC : HDC; + Save : THandle; + Tile : TRect; +{$ifdef DEBUG_DRAWPERFORMANCE} + ImageCount , + TimeStart , + TimeStop : DWORD; +{$endif} + +begin +{$ifdef DEBUG_DRAWPERFORMANCE} + TimeStart := timeGetTime; + ImageCount := 0; +{$endif} + if (DoTransparent) and (Transparent) and (HasMask) then + begin + // Draw transparent using mask + Save := 0; + MaskDC := 0; + try + MaskDC := GDICheck(CreateCompatibleDC(0)); + Save := SelectObject(MaskDC, FMask); + + if (DoTile) then + begin + Tile.Left := Rect.Left+Left; + Tile.Right := Tile.Left + Width; + while (Tile.Left < Rect.Right) do + begin + Tile.Top := Rect.Top+Top; + Tile.Bottom := Tile.Top + Height; + while (Tile.Top < Rect.Bottom) do + begin + TransparentStretchBlt(ACanvas.Handle, Tile.Left, Tile.Top, Width, Height, + Bitmap.Canvas.Handle, 0, 0, Width, Height, MaskDC, 0, 0); + Tile.Top := Tile.Top + Image.Height; + Tile.Bottom := Tile.Bottom + Image.Height; +{$ifdef DEBUG_DRAWPERFORMANCE} + inc(ImageCount); +{$endif} + end; + Tile.Left := Tile.Left + Image.Width; + Tile.Right := Tile.Right + Image.Width; + end; + end else + TransparentStretchBlt(ACanvas.Handle, Rect.Left, Rect.Top, + Rect.Right - Rect.Left, Rect.Bottom - Rect.Top, + Bitmap.Canvas.Handle, 0, 0, Width, Height, MaskDC, 0, 0); + + // Since we are not using any of the TCanvas functions (only handle) + // we need to fire the TCanvas.Changed method "manually". + TChangableCanvas(ACanvas).Changed; + + finally + if (Save <> 0) then + SelectObject(MaskDC, Save); + if (MaskDC <> 0) then + DeleteDC(MaskDC); + end; + end else + begin + if (DoTile) then + begin + Tile.Left := Rect.Left+Left; + Tile.Right := Tile.Left + Width; + while (Tile.Left < Rect.Right) do + begin + Tile.Top := Rect.Top+Top; + Tile.Bottom := Tile.Top + Height; + while (Tile.Top < Rect.Bottom) do + begin + ACanvas.StretchDraw(Tile, Bitmap); + Tile.Top := Tile.Top + Image.Height; + Tile.Bottom := Tile.Bottom + Image.Height; +{$ifdef DEBUG_DRAWPERFORMANCE} + inc(ImageCount); +{$endif} + end; + Tile.Left := Tile.Left + Image.Width; + Tile.Right := Tile.Right + Image.Width; + end; + end else + ACanvas.StretchDraw(Rect, Bitmap); + end; +{$ifdef DEBUG_DRAWPERFORMANCE} + if (GetAsyncKeyState(VK_CONTROL) <> 0) then + begin + TimeStop := timeGetTime; + ShowMessage(format('Draw %d images in %d mS, Rate %d images/mS (%d images/S)', + [ImageCount, TimeStop-TimeStart, + ImageCount DIV (TimeStop-TimeStart+1), + MulDiv(ImageCount, 1000, TimeStop-TimeStart+1)])); + end; +{$endif} +end; + +// Given a destination rect (DestRect) calculates the +// area covered by this sub image +function TGIFSubImage.ScaleRect(DestRect: TRect): TRect; +var + HeightMul , + HeightDiv : integer; + WidthMul , + WidthDiv : integer; +begin + HeightDiv := Image.Height; + HeightMul := DestRect.Bottom-DestRect.Top; + WidthDiv := Image.Width; + WidthMul := DestRect.Right-DestRect.Left; + + Result.Left := DestRect.Left + muldiv(Left, WidthMul, WidthDiv); + Result.Top := DestRect.Top + muldiv(Top, HeightMul, HeightDiv); + Result.Right := DestRect.Left + muldiv(Left+Width, WidthMul, WidthDiv); + Result.Bottom := DestRect.Top + muldiv(Top+Height, HeightMul, HeightDiv); +end; + +procedure TGIFSubImage.Crop; +var + TransparentColorIndex : byte; + CropLeft , + CropTop , + CropRight , + CropBottom : integer; + WasTransparent : boolean; + i : integer; + NewSize : integer; + NewData : PChar; + NewWidth , + NewHeight : integer; + pSource , + pDest : PChar; +begin + if (Empty) or (not Transparent) then + exit; + TransparentColorIndex := GraphicControlExtension.TransparentColorIndex; + CropLeft := 0; + CropRight := Width - 1; + CropTop := 0; + CropBottom := Height - 1; + // Find left edge + WasTransparent := True; + while (CropLeft <= CropRight) and (WasTransparent) do + begin + for i := CropTop to CropBottom do + if (Pixels[CropLeft, i] <> TransparentColorIndex) then + begin + WasTransparent := False; + break; + end; + if (WasTransparent) then + inc(CropLeft); + end; + // Find right edge + WasTransparent := True; + while (CropLeft <= CropRight) and (WasTransparent) do + begin + for i := CropTop to CropBottom do + if (pixels[CropRight, i] <> TransparentColorIndex) then + begin + WasTransparent := False; + break; + end; + if (WasTransparent) then + dec(CropRight); + end; + if (CropLeft <= CropRight) then + begin + // Find top edge + WasTransparent := True; + while (CropTop <= CropBottom) and (WasTransparent) do + begin + for i := CropLeft to CropRight do + if (pixels[i, CropTop] <> TransparentColorIndex) then + begin + WasTransparent := False; + break; + end; + if (WasTransparent) then + inc(CropTop); + end; + // Find bottom edge + WasTransparent := True; + while (CropTop <= CropBottom) and (WasTransparent) do + begin + for i := CropLeft to CropRight do + if (pixels[i, CropBottom] <> TransparentColorIndex) then + begin + WasTransparent := False; + break; + end; + if (WasTransparent) then + dec(CropBottom); + end; + end; + + if (CropLeft > CropRight) or (CropTop > CropBottom) then + begin + // Cropped to nothing - frame is invisible + Clear; + end else + begin + // Crop frame - move data + NewWidth := CropRight - CropLeft + 1; + Newheight := CropBottom - CropTop + 1; + NewSize := NewWidth * NewHeight; + GetMem(NewData, NewSize); + pSource := PChar(integer(FData) + CropTop * Width + CropLeft); + pDest := NewData; + for i := 0 to NewHeight-1 do + begin + Move(pSource^, pDest^, NewWidth); + inc(pSource, Width); + inc(pDest, NewWidth); + end; + FreeImage; + FData := NewData; + FDataSize := NewSize; + inc(FImageDescriptor.Left, CropLeft); + inc(FImageDescriptor.Top, CropTop); + FImageDescriptor.Width := NewWidth; + FImageDescriptor.Height := NewHeight; + FreeBitmap; + FreeMask + end; +end; + +procedure TGIFSubImage.Merge(Previous: TGIFSubImage); +var + SourceIndex , + DestIndex : byte; + SourceTransparent : boolean; + NeedTransparentColorIndex: boolean; + PreviousRect , + ThisRect , + MergeRect : TRect; + PreviousY , + X , + Y : integer; + pSource , + pDest : PChar; + pSourceMap , + pDestMap : PColorMap; + GCE : TGIFGraphicControlExtension; + + function CanMakeTransparent: boolean; + begin + // Is there a local color map... + if (ColorMap.Count > 0) then + // ...and is there room in it? + Result := (ColorMap.Count < 256) + // Is there a global color map... + else if (Image.GlobalColorMap.Count > 0) then + // ...and is there room in it? + Result := (Image.GlobalColorMap.Count < 256) + else + Result := False; + end; + + function GetTransparentColorIndex: byte; + var + i : integer; + begin + if (ColorMap.Count > 0) then + begin + // Get the transparent color from the local color map + Result := ColorMap.Add(TColor(0)); + end else + begin + // Are any other frames using the global color map for transparency + for i := 0 to Image.Images.Count-1 do + if (Image.Images[i] <> self) and (Image.Images[i].Transparent) and + (Image.Images[i].ColorMap.Count = 0) then + begin + // Use the same transparency color as the other frame + Result := Image.Images[i].GraphicControlExtension.TransparentColorIndex; + exit; + end; + // Get the transparent color from the global color map + Result := Image.GlobalColorMap.Add(TColor(0)); + end; + end; + +begin + // Determine if it is possible to merge this frame + if (Empty) or (Previous = nil) or (Previous.Empty) or + ((Previous.GraphicControlExtension <> nil) and + (Previous.GraphicControlExtension.Disposal in [dmBackground, dmPrevious])) then + exit; + + PreviousRect := Previous.BoundsRect; + ThisRect := BoundsRect; + + // Cannot merge unless the frames intersect + if (not IntersectRect(MergeRect, PreviousRect, ThisRect)) then + exit; + + // If the frame isn't already transparent, determine + // if it is possible to make it so + if (Transparent) then + begin + DestIndex := GraphicControlExtension.TransparentColorIndex; + NeedTransparentColorIndex := False; + end else + begin + if (not CanMakeTransparent) then + exit; + DestIndex := 0; // To avoid compiler warning + NeedTransparentColorIndex := True; + end; + + SourceTransparent := Previous.Transparent; + if (SourceTransparent) then + SourceIndex := Previous.GraphicControlExtension.TransparentColorIndex + else + SourceIndex := 0; // To avoid compiler warning + + PreviousY := MergeRect.Top - Previous.Top; + + pSourceMap := Previous.ActiveColorMap.Data; + pDestMap := ActiveColorMap.Data; + + for Y := MergeRect.Top - Top to MergeRect.Bottom - Top-1 do + begin + pSource := PChar(integer(Previous.Scanline[PreviousY]) + MergeRect.Left - Previous.Left); + pDest := PChar(integer(Scanline[Y]) + MergeRect.Left - Left); + + for X := MergeRect.Left to MergeRect.Right-1 do + begin + // Ignore pixels if either this frame's or the previous frame's pixel is transparent + if ( + not( + ((not NeedTransparentColorIndex) and (pDest^ = char(DestIndex))) or + ((SourceTransparent) and (pSource^ = char(SourceIndex))) + ) + ) and ( + // Replace same colored pixels with transparency + ((pDestMap = pSourceMap) and (pDest^ = pSource^)) or + (CompareMem(@(pDestMap^[ord(pDest^)]), @(pSourceMap^[ord(pSource^)]), sizeof(TGIFColor))) + ) then + begin + if (NeedTransparentColorIndex) then + begin + NeedTransparentColorIndex := False; + DestIndex := GetTransparentColorIndex; + end; + pDest^ := char(DestIndex); + end; + inc(pDest); + inc(pSource); + end; + inc(PreviousY); + end; + + (* + ** Create a GCE if the frame wasn't already transparent and any + ** pixels were made transparent + *) + if (not Transparent) and (not NeedTransparentColorIndex) then + begin + if (GraphicControlExtension = nil) then + begin + GCE := TGIFGraphicControlExtension.Create(self); + Extensions.Add(GCE); + end else + GCE := GraphicControlExtension; + GCE.Transparent := True; + GCE.TransparentColorIndex := DestIndex; + end; + + FreeBitmap; + FreeMask +end; + +//////////////////////////////////////////////////////////////////////////////// +// +// TGIFTrailer +// +//////////////////////////////////////////////////////////////////////////////// +procedure TGIFTrailer.SaveToStream(Stream: TStream); +begin + WriteByte(Stream, bsTrailer); +end; + +procedure TGIFTrailer.LoadFromStream(Stream: TStream); +var + b : BYTE; +begin + if (Stream.Read(b, 1) <> 1) then + exit; + if (b <> bsTrailer) then + Warning(gsWarning, sBadTrailer); +end; + +//////////////////////////////////////////////////////////////////////////////// +// +// TGIFExtension registration database +// +//////////////////////////////////////////////////////////////////////////////// +type + TExtensionLeadIn = packed record + Introducer: byte; { always $21 } + ExtensionLabel: byte; + end; + + PExtRec = ^TExtRec; + TExtRec = record + ExtClass: TGIFExtensionClass; + ExtLabel: BYTE; + end; + + TExtensionList = class(TList) + public + constructor Create; + destructor Destroy; override; + procedure Add(eLabel: BYTE; eClass: TGIFExtensionClass); + function FindExt(eLabel: BYTE): TGIFExtensionClass; + procedure Remove(eClass: TGIFExtensionClass); + end; + +constructor TExtensionList.Create; +begin + inherited Create; + Add(bsPlainTextExtension, TGIFTextExtension); + Add(bsGraphicControlExtension, TGIFGraphicControlExtension); + Add(bsCommentExtension, TGIFCommentExtension); + Add(bsApplicationExtension, TGIFApplicationExtension); +end; + +destructor TExtensionList.Destroy; +var + I: Integer; +begin + for I := 0 to Count-1 do + Dispose(PExtRec(Items[I])); + inherited Destroy; +end; + +procedure TExtensionList.Add(eLabel: BYTE; eClass: TGIFExtensionClass); +var + NewRec: PExtRec; +begin + New(NewRec); + with NewRec^ do + begin + ExtLabel := eLabel; + ExtClass := eClass; + end; + inherited Add(NewRec); +end; + +function TExtensionList.FindExt(eLabel: BYTE): TGIFExtensionClass; +var + I: Integer; +begin + for I := Count-1 downto 0 do + with PExtRec(Items[I])^ do + if ExtLabel = eLabel then + begin + Result := ExtClass; + Exit; + end; + Result := nil; +end; + +procedure TExtensionList.Remove(eClass: TGIFExtensionClass); +var + I: Integer; + P: PExtRec; +begin + for I := Count-1 downto 0 do + begin + P := PExtRec(Items[I]); + if P^.ExtClass.InheritsFrom(eClass) then + begin + Dispose(P); + Delete(I); + end; + end; +end; + +var + ExtensionList: TExtensionList = nil; + +function GetExtensionList: TExtensionList; +begin + if (ExtensionList = nil) then + ExtensionList := TExtensionList.Create; + Result := ExtensionList; +end; + +//////////////////////////////////////////////////////////////////////////////// +// +// TGIFExtension +// +//////////////////////////////////////////////////////////////////////////////// +function TGIFExtension.GetVersion: TGIFVersion; +begin + Result := gv89a; +end; + +class procedure TGIFExtension.RegisterExtension(eLabel: BYTE; eClass: TGIFExtensionClass); +begin + GetExtensionList.Add(eLabel, eClass); +end; + +class function TGIFExtension.FindExtension(Stream: TStream): TGIFExtensionClass; +var + eLabel : BYTE; + SubClass : TGIFExtensionClass; + Pos : LongInt; +begin + Pos := Stream.Position; + if (Stream.Read(eLabel, 1) <> 1) then + begin + Result := nil; + exit; + end; + Result := GetExtensionList.FindExt(eLabel); + while (Result <> nil) do + begin + SubClass := Result.FindSubExtension(Stream); + if (SubClass = Result) then + break; + Result := SubClass; + end; + Stream.Position := Pos; +end; + +class function TGIFExtension.FindSubExtension(Stream: TStream): TGIFExtensionClass; +begin + Result := self; +end; + +constructor TGIFExtension.Create(ASubImage: TGIFSubImage); +begin + inherited Create(ASubImage.Image); + FSubImage := ASubImage; +end; + +destructor TGIFExtension.Destroy; +begin + if (FSubImage <> nil) then + FSubImage.Extensions.Remove(self); + inherited Destroy; +end; + +procedure TGIFExtension.SaveToStream(Stream: TStream); +var + ExtensionLeadIn : TExtensionLeadIn; +begin + ExtensionLeadIn.Introducer := bsExtensionIntroducer; + ExtensionLeadIn.ExtensionLabel := ExtensionType; + Stream.Write(ExtensionLeadIn, sizeof(ExtensionLeadIn)); +end; + +function TGIFExtension.DoReadFromStream(Stream: TStream): TGIFExtensionType; +var + ExtensionLeadIn : TExtensionLeadIn; +begin + ReadCheck(Stream, ExtensionLeadIn, sizeof(ExtensionLeadIn)); + if (ExtensionLeadIn.Introducer <> bsExtensionIntroducer) then + Error(sBadExtensionLabel); + Result := ExtensionLeadIn.ExtensionLabel; +end; + +procedure TGIFExtension.LoadFromStream(Stream: TStream); +begin + // Seek past lead-in + // Stream.Seek(sizeof(TExtensionLeadIn), soFromCurrent); + if (DoReadFromStream(Stream) <> ExtensionType) then + Error(sBadExtensionInstance); +end; + +//////////////////////////////////////////////////////////////////////////////// +// +// TGIFGraphicControlExtension +// +//////////////////////////////////////////////////////////////////////////////// +const + { Extension flag bit masks } + efInputFlag = $02; { 00000010 } + efDisposal = $1C; { 00011100 } + efTransparent = $01; { 00000001 } + efReserved = $E0; { 11100000 } + +constructor TGIFGraphicControlExtension.Create(ASubImage: TGIFSubImage); +begin + inherited Create(ASubImage); + + FGCExtension.BlockSize := 4; + FGCExtension.PackedFields := $00; + FGCExtension.DelayTime := 0; + FGCExtension.TransparentColorIndex := 0; + FGCExtension.Terminator := 0; + if (ASubImage.FGCE = nil) then + ASubImage.FGCE := self; +end; + +destructor TGIFGraphicControlExtension.Destroy; +begin + // Clear transparent flag in sub image + if (Transparent) then + SubImage.FTransparent := False; + + if (SubImage.FGCE = self) then + SubImage.FGCE := nil; + + inherited Destroy; +end; + +function TGIFGraphicControlExtension.GetExtensionType: TGIFExtensionType; +begin + Result := bsGraphicControlExtension; +end; + +function TGIFGraphicControlExtension.GetTransparent: boolean; +begin + Result := (FGCExtension.PackedFields AND efTransparent) <> 0; +end; + +procedure TGIFGraphicControlExtension.SetTransparent(Value: boolean); +begin + // Set transparent flag in sub image + SubImage.FTransparent := Value; + if (Value) then + FGCExtension.PackedFields := FGCExtension.PackedFields OR efTransparent + else + FGCExtension.PackedFields := FGCExtension.PackedFields AND NOT(efTransparent); +end; + +function TGIFGraphicControlExtension.GetTransparentColor: TColor; +begin + Result := SubImage.ActiveColorMap[TransparentColorIndex]; +end; + +procedure TGIFGraphicControlExtension.SetTransparentColor(Color: TColor); +begin + FGCExtension.TransparentColorIndex := Subimage.ActiveColorMap.AddUnique(Color); +end; + +function TGIFGraphicControlExtension.GetTransparentColorIndex: BYTE; +begin + Result := FGCExtension.TransparentColorIndex; +end; + +procedure TGIFGraphicControlExtension.SetTransparentColorIndex(Value: BYTE); +begin + if ((Value >= SubImage.ActiveColorMap.Count) and (SubImage.ActiveColorMap.Count > 0)) then + begin + Warning(gsWarning, sBadColorIndex); + Value := 0; + end; + FGCExtension.TransparentColorIndex := Value; +end; + +function TGIFGraphicControlExtension.GetDelay: WORD; +begin + Result := FGCExtension.DelayTime; +end; +procedure TGIFGraphicControlExtension.SetDelay(Value: WORD); +begin + FGCExtension.DelayTime := Value; +end; + +function TGIFGraphicControlExtension.GetUserInput: boolean; +begin + Result := (FGCExtension.PackedFields AND efInputFlag) <> 0; +end; + +procedure TGIFGraphicControlExtension.SetUserInput(Value: boolean); +begin + if (Value) then + FGCExtension.PackedFields := FGCExtension.PackedFields OR efInputFlag + else + FGCExtension.PackedFields := FGCExtension.PackedFields AND NOT(efInputFlag); +end; + +function TGIFGraphicControlExtension.GetDisposal: TDisposalMethod; +begin + Result := TDisposalMethod((FGCExtension.PackedFields AND efDisposal) SHR 2); +end; + +procedure TGIFGraphicControlExtension.SetDisposal(Value: TDisposalMethod); +begin + FGCExtension.PackedFields := FGCExtension.PackedFields AND NOT(efDisposal) + OR ((ord(Value) SHL 2) AND efDisposal); +end; + +procedure TGIFGraphicControlExtension.SaveToStream(Stream: TStream); +begin + inherited SaveToStream(Stream); + Stream.Write(FGCExtension, sizeof(FGCExtension)); +end; + +procedure TGIFGraphicControlExtension.LoadFromStream(Stream: TStream); +begin + inherited LoadFromStream(Stream); + if (Stream.Read(FGCExtension, sizeof(FGCExtension)) <> sizeof(FGCExtension)) then + begin + Warning(gsWarning, sOutOfData); + exit; + end; + // Set transparent flag in sub image + if (Transparent) then + SubImage.FTransparent := True; +end; + +//////////////////////////////////////////////////////////////////////////////// +// +// TGIFTextExtension +// +//////////////////////////////////////////////////////////////////////////////// +constructor TGIFTextExtension.Create(ASubImage: TGIFSubImage); +begin + inherited Create(ASubImage); + FText := TStringList.Create; + FPlainTextExtension.BlockSize := 12; + FPlainTextExtension.Left := 0; + FPlainTextExtension.Top := 0; + FPlainTextExtension.Width := 0; + FPlainTextExtension.Height := 0; + FPlainTextExtension.CellWidth := 0; + FPlainTextExtension.CellHeight := 0; + FPlainTextExtension.TextFGColorIndex := 0; + FPlainTextExtension.TextBGColorIndex := 0; +end; + +destructor TGIFTextExtension.Destroy; +begin + FText.Free; + inherited Destroy; +end; + +function TGIFTextExtension.GetExtensionType: TGIFExtensionType; +begin + Result := bsPlainTextExtension; +end; + +function TGIFTextExtension.GetForegroundColor: TColor; +begin + Result := SubImage.ColorMap[ForegroundColorIndex]; +end; + +procedure TGIFTextExtension.SetForegroundColor(Color: TColor); +begin + ForegroundColorIndex := SubImage.ActiveColorMap.AddUnique(Color); +end; + +function TGIFTextExtension.GetBackgroundColor: TColor; +begin + Result := SubImage.ActiveColorMap[BackgroundColorIndex]; +end; + +procedure TGIFTextExtension.SetBackgroundColor(Color: TColor); +begin + BackgroundColorIndex := SubImage.ColorMap.AddUnique(Color); +end; + +function TGIFTextExtension.GetBounds(Index: integer): WORD; +begin + case (Index) of + 1: Result := FPlainTextExtension.Left; + 2: Result := FPlainTextExtension.Top; + 3: Result := FPlainTextExtension.Width; + 4: Result := FPlainTextExtension.Height; + else + Result := 0; // To avoid compiler warnings + end; +end; + +procedure TGIFTextExtension.SetBounds(Index: integer; Value: WORD); +begin + case (Index) of + 1: FPlainTextExtension.Left := Value; + 2: FPlainTextExtension.Top := Value; + 3: FPlainTextExtension.Width := Value; + 4: FPlainTextExtension.Height := Value; + end; +end; + +function TGIFTextExtension.GetCharWidthHeight(Index: integer): BYTE; +begin + case (Index) of + 1: Result := FPlainTextExtension.CellWidth; + 2: Result := FPlainTextExtension.CellHeight; + else + Result := 0; // To avoid compiler warnings + end; +end; + +procedure TGIFTextExtension.SetCharWidthHeight(Index: integer; Value: BYTE); +begin + case (Index) of + 1: FPlainTextExtension.CellWidth := Value; + 2: FPlainTextExtension.CellHeight := Value; + end; +end; + +function TGIFTextExtension.GetColorIndex(Index: integer): BYTE; +begin + case (Index) of + 1: Result := FPlainTextExtension.TextFGColorIndex; + 2: Result := FPlainTextExtension.TextBGColorIndex; + else + Result := 0; // To avoid compiler warnings + end; +end; + +procedure TGIFTextExtension.SetColorIndex(Index: integer; Value: BYTE); +begin + case (Index) of + 1: FPlainTextExtension.TextFGColorIndex := Value; + 2: FPlainTextExtension.TextBGColorIndex := Value; + end; +end; + +procedure TGIFTextExtension.SaveToStream(Stream: TStream); +begin + inherited SaveToStream(Stream); + Stream.Write(FPlainTextExtension, sizeof(FPlainTextExtension)); + WriteStrings(Stream, FText); +end; + +procedure TGIFTextExtension.LoadFromStream(Stream: TStream); +begin + inherited LoadFromStream(Stream); + ReadCheck(Stream, FPlainTextExtension, sizeof(FPlainTextExtension)); + ReadStrings(Stream, FText); +end; + +//////////////////////////////////////////////////////////////////////////////// +// +// TGIFCommentExtension +// +//////////////////////////////////////////////////////////////////////////////// +constructor TGIFCommentExtension.Create(ASubImage: TGIFSubImage); +begin + inherited Create(ASubImage); + FText := TStringList.Create; +end; + +destructor TGIFCommentExtension.Destroy; +begin + FText.Free; + inherited Destroy; +end; + +function TGIFCommentExtension.GetExtensionType: TGIFExtensionType; +begin + Result := bsCommentExtension; +end; + +procedure TGIFCommentExtension.SaveToStream(Stream: TStream); +begin + inherited SaveToStream(Stream); + WriteStrings(Stream, FText); +end; + +procedure TGIFCommentExtension.LoadFromStream(Stream: TStream); +begin + inherited LoadFromStream(Stream); + ReadStrings(Stream, FText); +end; + +//////////////////////////////////////////////////////////////////////////////// +// +// TGIFApplicationExtension registration database +// +//////////////////////////////////////////////////////////////////////////////// +type + PAppExtRec = ^TAppExtRec; + TAppExtRec = record + AppClass: TGIFAppExtensionClass; + Ident: TGIFApplicationRec; + end; + + TAppExtensionList = class(TList) + public + constructor Create; + destructor Destroy; override; + procedure Add(eIdent: TGIFApplicationRec; eClass: TGIFAppExtensionClass); + function FindExt(eIdent: TGIFApplicationRec): TGIFAppExtensionClass; + procedure Remove(eClass: TGIFAppExtensionClass); + end; + +constructor TAppExtensionList.Create; +const + NSLoopIdent: array[0..1] of TGIFApplicationRec = + ((Identifier: 'NETSCAPE'; Authentication: '2.0'), + (Identifier: 'ANIMEXTS'; Authentication: '1.0')); +begin + inherited Create; + Add(NSLoopIdent[0], TGIFAppExtNSLoop); + Add(NSLoopIdent[1], TGIFAppExtNSLoop); +end; + +destructor TAppExtensionList.Destroy; +var + I: Integer; +begin + for I := 0 to Count-1 do + Dispose(PAppExtRec(Items[I])); + inherited Destroy; +end; + +procedure TAppExtensionList.Add(eIdent: TGIFApplicationRec; eClass: TGIFAppExtensionClass); +var + NewRec: PAppExtRec; +begin + New(NewRec); + NewRec^.Ident := eIdent; + NewRec^.AppClass := eClass; + inherited Add(NewRec); +end; + +function TAppExtensionList.FindExt(eIdent: TGIFApplicationRec): TGIFAppExtensionClass; +var + I: Integer; +begin + for I := Count-1 downto 0 do + with PAppExtRec(Items[I])^ do + if CompareMem(@Ident, @eIdent, sizeof(TGIFApplicationRec)) then + begin + Result := AppClass; + Exit; + end; + Result := nil; +end; + +procedure TAppExtensionList.Remove(eClass: TGIFAppExtensionClass); +var + I: Integer; + P: PAppExtRec; +begin + for I := Count-1 downto 0 do + begin + P := PAppExtRec(Items[I]); + if P^.AppClass.InheritsFrom(eClass) then + begin + Dispose(P); + Delete(I); + end; + end; +end; + +var + AppExtensionList: TAppExtensionList = nil; + +function GetAppExtensionList: TAppExtensionList; +begin + if (AppExtensionList = nil) then + AppExtensionList := TAppExtensionList.Create; + Result := AppExtensionList; +end; + +class procedure TGIFApplicationExtension.RegisterExtension(eIdent: TGIFApplicationRec; + eClass: TGIFAppExtensionClass); +begin + GetAppExtensionList.Add(eIdent, eClass); +end; + +class function TGIFApplicationExtension.FindSubExtension(Stream: TStream): TGIFExtensionClass; +var + eIdent : TGIFApplicationRec; + OldPos : longInt; + Size : BYTE; +begin + OldPos := Stream.Position; + Result := nil; + if (Stream.Read(Size, 1) <> 1) then + exit; + + // Some old Adobe export filters mistakenly uses a value of 10 + if (Size = 10) then + begin + { TODO -oanme -cImprovement : replace with seek or read and check contents = 'Adobe' } + if (Stream.Read(eIdent, 10) <> 10) then + exit; + Result := TGIFUnknownAppExtension; + exit; + end else + if (Size <> sizeof(TGIFApplicationRec)) or + (Stream.Read(eIdent, sizeof(eIdent)) <> sizeof(eIdent)) then + begin + Stream.Position := OldPos; + Result := inherited FindSubExtension(Stream); + end else + begin + Result := GetAppExtensionList.FindExt(eIdent); + if (Result = nil) then + Result := TGIFUnknownAppExtension; + end; +end; + +//////////////////////////////////////////////////////////////////////////////// +// +// TGIFApplicationExtension +// +//////////////////////////////////////////////////////////////////////////////// +constructor TGIFApplicationExtension.Create(ASubImage: TGIFSubImage); +begin + inherited Create(ASubImage); + FillChar(FIdent, sizeof(FIdent), 0); +end; + +destructor TGIFApplicationExtension.Destroy; +begin + inherited Destroy; +end; + +function TGIFApplicationExtension.GetExtensionType: TGIFExtensionType; +begin + Result := bsApplicationExtension; +end; + +function TGIFApplicationExtension.GetAuthentication: string; +begin + Result := FIdent.Authentication; +end; + +procedure TGIFApplicationExtension.SetAuthentication(const Value: string); +begin + if (Length(Value) < sizeof(TGIFAuthenticationCode)) then + FillChar(FIdent.Authentication, sizeof(TGIFAuthenticationCode), 32); + StrLCopy(@(FIdent.Authentication[0]), PChar(Value), sizeof(TGIFAuthenticationCode)); +end; + +function TGIFApplicationExtension.GetIdentifier: string; +begin + Result := FIdent.Identifier; +end; + +procedure TGIFApplicationExtension.SetIdentifier(const Value: string); +begin + if (Length(Value) < sizeof(TGIFIdentifierCode)) then + FillChar(FIdent.Identifier, sizeof(TGIFIdentifierCode), 32); + StrLCopy(@(FIdent.Identifier[0]), PChar(Value), sizeof(TGIFIdentifierCode)); +end; + +procedure TGIFApplicationExtension.SaveToStream(Stream: TStream); +begin + inherited SaveToStream(Stream); + WriteByte(Stream, sizeof(FIdent)); // Block size + Stream.Write(FIdent, sizeof(FIdent)); + SaveData(Stream); +end; + +procedure TGIFApplicationExtension.LoadFromStream(Stream: TStream); +var + i : integer; +begin + inherited LoadFromStream(Stream); + i := ReadByte(Stream); + // Some old Adobe export filters mistakenly uses a value of 10 + if (i = 10) then + FillChar(FIdent, sizeOf(FIdent), 0) + else + if (i < 11) then + Error(sBadBlockSize); + + ReadCheck(Stream, FIdent, sizeof(FIdent)); + + Dec(i, sizeof(FIdent)); + // Ignore extra data + Stream.Seek(i, soFromCurrent); + + // ***FIXME*** + // If self class is TGIFApplicationExtension, this will cause an "abstract + // error". + // TGIFApplicationExtension.LoadData should read and ignore rest of block. + LoadData(Stream); +end; + +//////////////////////////////////////////////////////////////////////////////// +// +// TGIFUnknownAppExtension +// +//////////////////////////////////////////////////////////////////////////////// +constructor TGIFBlock.Create(ASize: integer); +begin + inherited Create; + FSize := ASize; + GetMem(FData, FSize); + FillChar(FData^, FSize, 0); +end; + +destructor TGIFBlock.Destroy; +begin + FreeMem(FData); + inherited Destroy; +end; + +procedure TGIFBlock.SaveToStream(Stream: TStream); +begin + Stream.Write(FSize, 1); + Stream.Write(FData^, FSize); +end; + +procedure TGIFBlock.LoadFromStream(Stream: TStream); +begin + ReadCheck(Stream, FData^, FSize); +end; + +constructor TGIFUnknownAppExtension.Create(ASubImage: TGIFSubImage); +begin + inherited Create(ASubImage); + FBlocks := TList.Create; +end; + +destructor TGIFUnknownAppExtension.Destroy; +var + i : integer; +begin + for i := 0 to FBlocks.Count-1 do + TGIFBlock(FBlocks[i]).Free; + FBlocks.Free; + inherited Destroy; +end; + + +procedure TGIFUnknownAppExtension.SaveData(Stream: TStream); +var + i : integer; +begin + for i := 0 to FBlocks.Count-1 do + TGIFBlock(FBlocks[i]).SaveToStream(Stream); + // Terminating zero + WriteByte(Stream, 0); +end; + +procedure TGIFUnknownAppExtension.LoadData(Stream: TStream); +var + b : BYTE; + Block : TGIFBlock; + i : integer; +begin + // Zap old blocks + for i := 0 to FBlocks.Count-1 do + TGIFBlock(FBlocks[i]).Free; + FBlocks.Clear; + + // Read blocks + if (Stream.Read(b, 1) <> 1) then + exit; + while (b <> 0) do + begin + Block := TGIFBlock.Create(b); + try + Block.LoadFromStream(Stream); + except + Block.Free; + raise; + end; + FBlocks.Add(Block); + if (Stream.Read(b, 1) <> 1) then + exit; + end; +end; + +//////////////////////////////////////////////////////////////////////////////// +// +// TGIFAppExtNSLoop +// +//////////////////////////////////////////////////////////////////////////////// +const + // Netscape sub block types + nbLoopExtension = 1; + nbBufferExtension = 2; + +constructor TGIFAppExtNSLoop.Create(ASubImage: TGIFSubImage); +const + NSLoopIdent: TGIFApplicationRec = (Identifier: 'NETSCAPE'; Authentication: '2.0'); +begin + inherited Create(ASubImage); + FIdent := NSLoopIdent; +end; + +procedure TGIFAppExtNSLoop.SaveData(Stream: TStream); +begin + // Write loop count + WriteByte(Stream, 1 + sizeof(FLoops)); // Size of block + WriteByte(Stream, nbLoopExtension); // Identify sub block as looping extension data + Stream.Write(FLoops, sizeof(FLoops)); // Loop count + + // Write buffer size if specified + if (FBufferSize > 0) then + begin + WriteByte(Stream, 1 + sizeof(FBufferSize)); // Size of block + WriteByte(Stream, nbBufferExtension); // Identify sub block as buffer size data + Stream.Write(FBufferSize, sizeof(FBufferSize)); // Buffer size + end; + + WriteByte(Stream, 0); // Terminating zero +end; + +procedure TGIFAppExtNSLoop.LoadData(Stream: TStream); +var + BlockSize : integer; + BlockType : integer; +begin + // Read size of first block or terminating zero + BlockSize := ReadByte(Stream); + while (BlockSize <> 0) do + begin + BlockType := ReadByte(Stream); + dec(BlockSize); + + case (BlockType AND $07) of + nbLoopExtension: + begin + if (BlockSize < sizeof(FLoops)) then + Error(sInvalidData); + // Read loop count + ReadCheck(Stream, FLoops, sizeof(FLoops)); + dec(BlockSize, sizeof(FLoops)); + end; + nbBufferExtension: + begin + if (BlockSize < sizeof(FBufferSize)) then + Error(sInvalidData); + // Read buffer size + ReadCheck(Stream, FBufferSize, sizeof(FBufferSize)); + dec(BlockSize, sizeof(FBufferSize)); + end; + end; + + // Skip/ignore unread data + if (BlockSize > 0) then + Stream.Seek(BlockSize, soFromCurrent); + + // Read size of next block or terminating zero + BlockSize := ReadByte(Stream); + end; +end; + + +//////////////////////////////////////////////////////////////////////////////// +// +// TGIFImageList +// +//////////////////////////////////////////////////////////////////////////////// +function TGIFImageList.GetImage(Index: Integer): TGIFSubImage; +begin + Result := TGIFSubImage(Items[Index]); +end; + +procedure TGIFImageList.SetImage(Index: Integer; SubImage: TGIFSubImage); +begin + Items[Index] := SubImage; +end; + +procedure TGIFImageList.LoadFromStream(Stream: TStream; Parent: TObject); +var + b : BYTE; + SubImage : TGIFSubImage; +begin + // Peek ahead to determine block type + repeat + if (Stream.Read(b, 1) <> 1) then + exit; + until (b <> 0); // Ignore 0 padding (non-compliant) + + while (b <> bsTrailer) do + begin + Stream.Seek(-1, soFromCurrent); + if (b in [bsExtensionIntroducer, bsImageDescriptor]) then + begin + SubImage := TGIFSubImage.Create(Parent as TGIFImage); + try + SubImage.LoadFromStream(Stream); + Add(SubImage); + Image.Progress(Self, psRunning, MulDiv(Stream.Position, 100, Stream.Size), + GIFImageRenderOnLoad, Rect(0,0,0,0), sProgressLoading); + except + SubImage.Free; + raise; + end; + end else + begin + Warning(gsWarning, sBadBlock); + break; + end; + repeat + if (Stream.Read(b, 1) <> 1) then + exit; + until (b <> 0); // Ignore 0 padding (non-compliant) + end; + Stream.Seek(-1, soFromCurrent); +end; + +procedure TGIFImageList.SaveToStream(Stream: TStream); +var + i : integer; +begin + for i := 0 to Count-1 do + begin + TGIFItem(Items[i]).SaveToStream(Stream); + Image.Progress(Self, psRunning, MulDiv((i+1), 100, Count), False, Rect(0,0,0,0), sProgressSaving); + end; +end; + +//////////////////////////////////////////////////////////////////////////////// +// +// TGIFPainter +// +//////////////////////////////////////////////////////////////////////////////// +constructor TGIFPainter.CreateRef(Painter: PGIFPainter; AImage: TGIFImage; + ACanvas: TCanvas; ARect: TRect; Options: TGIFDrawOptions); +begin + Create(AImage, ACanvas, ARect, Options); + PainterRef := Painter; + if (PainterRef <> nil) then + PainterRef^ := self; +end; + +constructor TGIFPainter.Create(AImage: TGIFImage; ACanvas: TCanvas; ARect: TRect; + Options: TGIFDrawOptions); +var + i : integer; + BackgroundColor : TColor; + Disposals : set of TDisposalMethod; +begin + inherited Create(True); + FreeOnTerminate := True; + Onterminate := DoOnTerminate; + FImage := AImage; + FCanvas := ACanvas; + FRect := ARect; + FActiveImage := -1; + FDrawOptions := Options; + FStarted := False; + BackupBuffer := nil; + FrameBuffer := nil; + Background := nil; + FEventHandle := 0; + // This should be a parameter, but I think I've got enough of them already... + FAnimationSpeed := FImage.AnimationSpeed; + + // An event handle is used for animation delays + if (FDrawOptions >= [goAnimate, goAsync]) and (FImage.Images.Count > 1) and + (FAnimationSpeed >= 0) then + FEventHandle := CreateEvent(nil, False, False, nil); + + // Preprocessing of extensions to determine if we need frame buffers + Disposals := []; + if (FImage.DrawBackgroundColor = clNone) then + begin + if (FImage.GlobalColorMap.Count > 0) then + BackgroundColor := FImage.BackgroundColor + else + BackgroundColor := ColorToRGB(clWindow); + end else + BackgroundColor := ColorToRGB(FImage.DrawBackgroundColor); + + // Need background buffer to clear on loop + if (goClearOnLoop in FDrawOptions) then + Include(Disposals, dmBackground); + + for i := 0 to FImage.Images.Count-1 do + if (FImage.Images[i].GraphicControlExtension <> nil) then + with (FImage.Images[i].GraphicControlExtension) do + Include(Disposals, Disposal); + + // Need background buffer to draw transparent on background + if (dmBackground in Disposals) and (goTransparent in FDrawOptions) then + begin + Background := TBitmap.Create; + Background.Height := FRect.Bottom-FRect.Top; + Background.Width := FRect.Right-FRect.Left; + // Copy background immediately + Background.Canvas.CopyMode := cmSrcCopy; + Background.Canvas.CopyRect(Background.Canvas.ClipRect, FCanvas, FRect); + end; + // Need frame- and backup buffer to restore to previous and background + if ((Disposals * [dmPrevious, dmBackground]) <> []) then + begin + BackupBuffer := TBitmap.Create; + BackupBuffer.Height := FRect.Bottom-FRect.Top; + BackupBuffer.Width := FRect.Right-FRect.Left; + BackupBuffer.Canvas.CopyMode := cmSrcCopy; + BackupBuffer.Canvas.Brush.Color := BackgroundColor; + BackupBuffer.Canvas.Brush.Style := bsSolid; +{$IFDEF DEBUG} + BackupBuffer.Canvas.Brush.Color := clBlack; + BackupBuffer.Canvas.Brush.Style := bsDiagCross; +{$ENDIF} + // Step 1: Copy destination to backup buffer + // Always executed before first frame and only once. + BackupBuffer.Canvas.CopyRect(BackupBuffer.Canvas.ClipRect, FCanvas, FRect); + FrameBuffer := TBitmap.Create; + FrameBuffer.Height := FRect.Bottom-FRect.Top; + FrameBuffer.Width := FRect.Right-FRect.Left; + FrameBuffer.Canvas.CopyMode := cmSrcCopy; + FrameBuffer.Canvas.Brush.Color := BackgroundColor; + FrameBuffer.Canvas.Brush.Style := bsSolid; +{$IFDEF DEBUG} + FrameBuffer.Canvas.Brush.Color := clBlack; + FrameBuffer.Canvas.Brush.Style := bsDiagCross; +{$ENDIF} + end; +end; + +destructor TGIFPainter.Destroy; +begin + // OnTerminate isn't called if we are running in main thread, so we must call + // it manually + if not(goAsync in DrawOptions) then + DoOnTerminate(self); + // Reraise any exptions that were eaten in the Execute method + if (ExceptObject <> nil) then + raise ExceptObject at ExceptAddress; + inherited Destroy; +end; + +procedure TGIFPainter.SetAnimationSpeed(Value: integer); +begin + if (Value < 0) then + Value := 0 + else if (Value > 1000) then + Value := 1000; + if (Value <> FAnimationSpeed) then + begin + FAnimationSpeed := Value; + // Signal WaitForSingleObject delay to abort + if (FEventHandle <> 0) then + SetEvent(FEventHandle) + else + DoRestart := True; + end; +end; + +procedure TGIFPainter.SetActiveImage(const Value: integer); +begin + if (Value >= 0) and (Value < FImage.Images.Count) then + FActiveImage := Value; +end; + +// Conditional Synchronize +procedure TGIFPainter.DoSynchronize(Method: TThreadMethod); +begin + if (Terminated) then + exit; + if (goAsync in FDrawOptions) then + // Execute Synchronized if requested... + Synchronize(Method) + else + // ...Otherwise just execute in current thread (probably main thread) + Method; +end; + +// Delete frame buffers - Executed in main thread +procedure TGIFPainter.DoOnTerminate(Sender: TObject); +begin + // It shouldn't really be nescessary to protect PainterRef in this manner + // since we are running in the main thread at this point, but I'm a little + // paranoid about the way PainterRef is being used... + if Image <> nil then // 2001.02.23 + begin // 2001.02.23 + with Image.Painters.LockList do + try + // Zap pointer to self and remove from painter list + if (PainterRef <> nil) and (PainterRef^ = self) then + PainterRef^ := nil; + finally + Image.Painters.UnLockList; + end; + Image.Painters.Remove(self); + FImage := nil; + end; // 2001.02.23 + + // Free buffers + if (BackupBuffer <> nil) then + BackupBuffer.Free; + if (FrameBuffer <> nil) then + FrameBuffer.Free; + if (Background <> nil) then + Background.Free; + + // Delete event handle + if (FEventHandle <> 0) then + CloseHandle(FEventHandle); +end; + +// Event "dispatcher" - Executed in main thread +procedure TGIFPainter.DoEvent; +begin + if (Assigned(FEvent)) then + FEvent(self); +end; + +// Non-buffered paint - Executed in main thread +procedure TGIFPainter.DoPaint; +begin + FImage.Images[ActiveImage].Draw(FCanvas, FRect, (goTransparent in FDrawOptions), + (goTile in FDrawOptions)); + FStarted := True; +end; + +// Buffered paint - Executed in main thread +procedure TGIFPainter.DoPaintFrame; +var + DrawDestination : TCanvas; + DrawRect : TRect; + DoStep2 , + DoStep3 , + DoStep5 , + DoStep6 : boolean; + SavePal , + SourcePal : HPALETTE; + + procedure ClearBackup; + var + r , + Tile : TRect; + FrameTop , + FrameHeight : integer; + ImageWidth , + ImageHeight : integer; + begin + + if (goTransparent in FDrawOptions) then + begin + // If the frame is transparent, we must remove it by copying the + // background buffer over it + if (goTile in FDrawOptions) then + begin + FrameTop := FImage.Images[ActiveImage].Top; + FrameHeight := FImage.Images[ActiveImage].Height; + ImageWidth := FImage.Width; + ImageHeight := FImage.Height; + + Tile.Left := FRect.Left + FImage.Images[ActiveImage].Left; + Tile.Right := Tile.Left + FImage.Images[ActiveImage].Width; + while (Tile.Left < FRect.Right) do + begin + Tile.Top := FRect.Top + FrameTop; + Tile.Bottom := Tile.Top + FrameHeight; + while (Tile.Top < FRect.Bottom) do + begin + BackupBuffer.Canvas.CopyRect(Tile, Background.Canvas, Tile); + Tile.Top := Tile.Top + ImageHeight; + Tile.Bottom := Tile.Bottom + ImageHeight; + end; + Tile.Left := Tile.Left + ImageWidth; + Tile.Right := Tile.Right + ImageWidth; + end; + end else + begin + r := FImage.Images[ActiveImage].ScaleRect(BackupBuffer.Canvas.ClipRect); + BackupBuffer.Canvas.CopyRect(r, Background.Canvas, r) + end; + end else + begin + // If the frame isn't transparent, we just clear the area covered by + // it to the background color. + // Tile the background unless the frame covers all of the image + if (goTile in FDrawOptions) and + ((FImage.Width <> FImage.Images[ActiveImage].Width) and + (FImage.height <> FImage.Images[ActiveImage].Height)) then + begin + FrameTop := FImage.Images[ActiveImage].Top; + FrameHeight := FImage.Images[ActiveImage].Height; + ImageWidth := FImage.Width; + ImageHeight := FImage.Height; + // ***FIXME*** I don't think this does any difference + BackupBuffer.Canvas.Brush.Color := FImage.DrawBackgroundColor; + + Tile.Left := FRect.Left + FImage.Images[ActiveImage].Left; + Tile.Right := Tile.Left + FImage.Images[ActiveImage].Width; + while (Tile.Left < FRect.Right) do + begin + Tile.Top := FRect.Top + FrameTop; + Tile.Bottom := Tile.Top + FrameHeight; + while (Tile.Top < FRect.Bottom) do + begin + BackupBuffer.Canvas.FillRect(Tile); + + Tile.Top := Tile.Top + ImageHeight; + Tile.Bottom := Tile.Bottom + ImageHeight; + end; + Tile.Left := Tile.Left + ImageWidth; + Tile.Right := Tile.Right + ImageWidth; + end; + end else + BackupBuffer.Canvas.FillRect(FImage.Images[ActiveImage].ScaleRect(FRect)); + end; + end; + +begin + if (goValidateCanvas in FDrawOptions) then + if (GetObjectType(ValidateDC) <> OBJ_DC) then + begin + Terminate; + exit; + end; + + DrawDestination := nil; + DoStep2 := (goClearOnLoop in FDrawOptions) and (FActiveImage = 0); + DoStep3 := False; + DoStep5 := False; + DoStep6 := False; +{ +Disposal mode algorithm: + +Step 1: Copy destination to backup buffer + Always executed before first frame and only once. + Done in constructor. +Step 2: Clear previous frame (implementation is same as step 6) + Done implicitly by implementation. + Only done explicitly on first frame if goClearOnLoop option is set. +Step 3: Copy backup buffer to frame buffer +Step 4: Draw frame +Step 5: Copy buffer to destination +Step 6: Clear frame from backup buffer ++------------+------------------+---------------------+------------------------+ +|New \ Old | dmNone | dmBackground | dmPrevious | ++------------+------------------+---------------------+------------------------+ +|dmNone | | | | +| |4. Paint on backup|4. Paint on backup |4. Paint on backup | +| |5. Restore |5. Restore |5. Restore | ++------------+------------------+---------------------+------------------------+ +|dmBackground| | | | +| |4. Paint on backup|4. Paint on backup |4. Paint on backup | +| |5. Restore |5. Restore |5. Restore | +| |6. Clear backup |6. Clear backup |6. Clear backup | ++------------+------------------+---------------------+------------------------+ +|dmPrevious | | | | +| | |3. Copy backup to buf|3. Copy backup to buf | +| |4. Paint on dest |4. Paint on buf |4. Paint on buf | +| | |5. Copy buf to dest |5. Copy buf to dest | ++------------+------------------+---------------------+------------------------+ +} + case (Disposal) of + dmNone, dmNoDisposal: + begin + DrawDestination := BackupBuffer.Canvas; + DrawRect := BackupBuffer.Canvas.ClipRect; + DoStep5 := True; + end; + dmBackground: + begin + DrawDestination := BackupBuffer.Canvas; + DrawRect := BackupBuffer.Canvas.ClipRect; + DoStep5 := True; + DoStep6 := True; + end; + dmPrevious: + case (OldDisposal) of + dmNone, dmNoDisposal: + begin + DrawDestination := FCanvas; + DrawRect := FRect; + end; + dmBackground, dmPrevious: + begin + DrawDestination := FrameBuffer.Canvas; + DrawRect := FrameBuffer.Canvas.ClipRect; + DoStep3 := True; + DoStep5 := True; + end; + end; + end; + + // Find source palette + SourcePal := FImage.Images[ActiveImage].Palette; + if (SourcePal = 0) then + SourcePal := SystemPalette16; // This should never happen + + SavePal := SelectPalette(DrawDestination.Handle, SourcePal, False); + RealizePalette(DrawDestination.Handle); + + // Step 2: Clear previous frame + if (DoStep2) then + ClearBackup; + + // Step 3: Copy backup buffer to frame buffer + if (DoStep3) then + FrameBuffer.Canvas.CopyRect(FrameBuffer.Canvas.ClipRect, + BackupBuffer.Canvas, BackupBuffer.Canvas.ClipRect); + + // Step 4: Draw frame + if (DrawDestination <> nil) then + FImage.Images[ActiveImage].Draw(DrawDestination, DrawRect, + (goTransparent in FDrawOptions), (goTile in FDrawOptions)); + + // Step 5: Copy buffer to destination + if (DoStep5) then + begin + FCanvas.CopyMode := cmSrcCopy; + FCanvas.CopyRect(FRect, DrawDestination, DrawRect); + end; + + if (SavePal <> 0) then + SelectPalette(DrawDestination.Handle, SavePal, False); + + // Step 6: Clear frame from backup buffer + if (DoStep6) then + ClearBackup; + + FStarted := True; +end; + +// Prefetch bitmap +// Used to force the GIF image to be rendered as a bitmap +{$ifdef SERIALIZE_RENDER} +procedure TGIFPainter.PrefetchBitmap; +begin + // Touch current bitmap to force bitmap to be rendered + if not((FImage.Images[ActiveImage].Empty) or (FImage.Images[ActiveImage].HasBitmap)) then + FImage.Images[ActiveImage].Bitmap; +end; +{$endif} + +// Main thread execution loop - This is where it all happens... +procedure TGIFPainter.Execute; +var + i : integer; + LoopCount , + LoopPoint : integer; + Looping : boolean; + Ext : TGIFExtension; + Msg : TMsg; + Delay , + OldDelay , + DelayUsed : longInt; + DelayStart , + NewDelayStart : DWORD; + + procedure FireEvent(Event: TNotifyEvent); + begin + if not(Assigned(Event)) then + exit; + FEvent := Event; + try + DoSynchronize(DoEvent); + finally + FEvent := nil; + end; + end; + +begin +{ + Disposal: + dmNone: Same as dmNodisposal + dmNoDisposal: Do not dispose + dmBackground: Clear with background color *) + dmPrevious: Previous image + *) Note: Background color should either be a BROWSER SPECIFIED Background + color (DrawBackgroundColor) or the background image if any frames are + transparent. +} + try + try + if (goValidateCanvas in FDrawOptions) then + ValidateDC := FCanvas.Handle; + DoRestart := True; + + // Loop to restart paint + while (DoRestart) and not(Terminated) do + begin + FActiveImage := 0; + // Fire OnStartPaint event + // Note: ActiveImage may be altered by the event handler + FireEvent(FOnStartPaint); + + FStarted := False; + DoRestart := False; + LoopCount := 1; + LoopPoint := FActiveImage; + Looping := False; + if (goAsync in DrawOptions) then + Delay := 0 + else + Delay := 1; // Dummy to process messages + OldDisposal := dmNoDisposal; + // Fetch delay start time + DelayStart := timeGetTime; + OldDelay := 0; + + // Loop to loop - duh! + while ((LoopCount <> 0) or (goLoopContinously in DrawOptions)) and + not(Terminated or DoRestart) do + begin + FActiveImage := LoopPoint; + + // Fire OnLoopPaint event + // Note: ActiveImage may be altered by the event handler + if (FStarted) then + FireEvent(FOnLoop); + + // Loop to animate + while (ActiveImage < FImage.Images.Count) and not(Terminated or DoRestart) do + begin + // Ignore empty images + if (FImage.Images[ActiveImage].Empty) then + break; + // Delay from previous image + if (Delay > 0) then + begin + // Prefetch frame bitmap +{$ifdef SERIALIZE_RENDER} + DoSynchronize(PrefetchBitmap); +{$else} + FImage.Images[ActiveImage].Bitmap; +{$endif} + + // Calculate inter frame delay + NewDelayStart := timeGetTime; + if (FAnimationSpeed > 0) then + begin + // Calculate number of mS used in prefetch and display + try + DelayUsed := integer(NewDelayStart-DelayStart)-OldDelay; + // Prevent feedback oscillations caused by over/undercompensation. + DelayUsed := DelayUsed DIV 2; + // Convert delay value to mS and... + // ...Adjust for time already spent converting GIF to bitmap and... + // ...Adjust for Animation Speed factor. + Delay := MulDiv(Delay * GIFDelayExp - DelayUsed, 100, FAnimationSpeed); + OldDelay := Delay; + except + Delay := GIFMaximumDelay * GIFDelayExp; + OldDelay := 0; + end; + end else + begin + if (goAsync in DrawOptions) then + Delay := longInt(INFINITE) + else + Delay := GIFMaximumDelay * GIFDelayExp; + end; + // Fetch delay start time + DelayStart := NewDelayStart; + + // Sleep in one chunk if we are running in a thread + if (goAsync in DrawOptions) then + begin + // Use of WaitForSingleObject allows TGIFPainter.Stop to wake us up + if (Delay > 0) or (FAnimationSpeed = 0) then + begin + if (WaitForSingleObject(FEventHandle, DWORD(Delay)) <> WAIT_TIMEOUT) then + begin + // Don't use interframe delay feedback adjustment if delay + // were prematurely aborted (e.g. because the animation + // speed were changed) + OldDelay := 0; + DelayStart := longInt(timeGetTime); + end; + end; + end else + begin + if (Delay <= 0) then + Delay := 1; + // Fetch start time + NewDelayStart := timeGetTime; + // If we are not running in a thread we Sleep in small chunks + // and give the user a chance to abort + while (Delay > 0) and not(Terminated or DoRestart) do + begin + if (Delay < 100) then + Sleep(Delay) + else + Sleep(100); + // Calculate number of mS delayed in this chunk + DelayUsed := integer(timeGetTime - NewDelayStart); + dec(Delay, DelayUsed); + // Reset start time for chunk + NewDelaySTart := timeGetTime; + // Application.ProcessMessages wannabe + while (not(Terminated or DoRestart)) and + (PeekMessage(Msg, 0, 0, 0, PM_REMOVE)) do + begin + if (Msg.Message <> WM_QUIT) then + begin + TranslateMessage(Msg); + DispatchMessage(Msg); + end else + begin + // Put WM_QUIT back in queue and get out of here fast + PostQuitMessage(Msg.WParam); + Terminate; + end; + end; + end; + end; + end else + Sleep(0); // Yield + if (Terminated) then + break; + + // Fire OnPaint event + // Note: ActiveImage may be altered by the event handler + FireEvent(FOnPaint); + if (Terminated) then + break; + + // Pre-draw processing of extensions + Disposal := dmNoDisposal; + for i := 0 to FImage.Images[ActiveImage].Extensions.Count-1 do + begin + Ext := FImage.Images[ActiveImage].Extensions[i]; + if (Ext is TGIFAppExtNSLoop) then + begin + // Recursive loops not supported (or defined) + if (Looping) then + continue; + Looping := True; + LoopCount := TGIFAppExtNSLoop(Ext).Loops; + if ((LoopCount = 0) or (goLoopContinously in DrawOptions)) and + (goAsync in DrawOptions) then + LoopCount := -1; // Infinite if running in separate thread +{$IFNDEF STRICT_MOZILLA} + // Loop from this image and on + // Note: This is not standard behavior + LoopPoint := ActiveImage; +{$ENDIF} + end else + if (Ext is TGIFGraphicControlExtension) then + Disposal := TGIFGraphicControlExtension(Ext).Disposal; + end; + + // Paint the image + if (BackupBuffer <> nil) then + DoSynchronize(DoPaintFrame) + else + DoSynchronize(DoPaint); + OldDisposal := Disposal; + + if (Terminated) then + break; + + Delay := GIFDefaultDelay; // Default delay + // Post-draw processing of extensions + if (FImage.Images[ActiveImage].GraphicControlExtension <> nil) then + if (FImage.Images[ActiveImage].GraphicControlExtension.Delay > 0) then + begin + Delay := FImage.Images[ActiveImage].GraphicControlExtension.Delay; + + // Enforce minimum animation delay in compliance with Mozilla + if (Delay < GIFMinimumDelay) then + Delay := GIFMinimumDelay; + + // Do not delay more than 10 seconds if running in main thread + if (Delay > GIFMaximumDelay) and not(goAsync in DrawOptions) then + Delay := GIFMaximumDelay; // Max 10 seconds + end; + // Fire OnAfterPaint event + // Note: ActiveImage may be altered by the event handler + i := FActiveImage; + FireEvent(FOnAfterPaint); + if (Terminated) then + break; + // Don't increment frame counter if event handler modified + // current frame + if (FActiveImage = i) then + Inc(FActiveImage); + // Nothing more to do unless we are animating + if not(goAnimate in DrawOptions) then + break; + end; + + if (LoopCount > 0) then + Dec(LoopCount); + if ([goAnimate, goLoop] * DrawOptions <> [goAnimate, goLoop]) then + break; + end; + if (Terminated) then // 2001.07.23 + break; // 2001.07.23 + end; + FActiveImage := -1; + // Fire OnEndPaint event + FireEvent(FOnEndPaint); + finally + // If we are running in the main thread we will have to zap our self + if not(goAsync in DrawOptions) then + Free; + end; + except + on E: Exception do + begin + // Eat exception and terminate thread... + // If we allow the exception to abort the thread at this point, the + // application will hang since the thread destructor will never be called + // and the application will wait forever for the thread to die! + Terminate; + // Clone exception + ExceptObject := E.Create(E.Message); + ExceptAddress := ExceptAddr; + end; + end; +end; + +procedure TGIFPainter.Start; +begin + if (goAsync in FDrawOptions) then + Resume; +end; + +procedure TGIFPainter.Stop; +begin + Terminate; + if (goAsync in FDrawOptions) then + begin + // Signal WaitForSingleObject delay to abort + if (FEventHandle <> 0) then + SetEvent(FEventHandle); + Priority := tpNormal; + if (Suspended) then + Resume; // Must be running before we can terminate + end; +end; + +procedure TGIFPainter.Restart; +begin + DoRestart := True; + if (Suspended) and (goAsync in FDrawOptions) then + Resume; // Must be running before we can terminate +end; + +//////////////////////////////////////////////////////////////////////////////// +// +// TColorMapOptimizer +// +//////////////////////////////////////////////////////////////////////////////// +// Used by TGIFImage to optimize local color maps to a single global color map. +// The following algorithm is used: +// 1) Build a histogram for each image +// 2) Merge histograms +// 3) Sum equal colors and adjust max # of colors +// 4) Map entries > max to entries <= 256 +// 5) Build new color map +// 6) Map images to new color map +//////////////////////////////////////////////////////////////////////////////// + +type + + POptimizeEntry = ^TOptimizeEntry; + TColorRec = record + case byte of + 0: (Value: integer); + 1: (Color: TGIFColor); + 2: (SameAs: POptimizeEntry); // Used if TOptimizeEntry.Count = 0 + end; + + TOptimizeEntry = record + Count : integer; // Usage count + OldIndex : integer; // Color OldIndex + NewIndex : integer; // NewIndex color OldIndex + Color : TColorRec; // Color value + end; + + TOptimizeEntries = array[0..255] of TOptimizeEntry; + POptimizeEntries = ^TOptimizeEntries; + + THistogram = class(TObject) + private + PHistogram : POptimizeEntries; + FCount : integer; + FColorMap : TGIFColorMap; + FList : TList; + FImages : TList; + public + constructor Create(AColorMap: TGIFColorMap); + destructor Destroy; override; + function ProcessSubImage(Image: TGIFSubImage): boolean; + function Prune: integer; + procedure MapImages(UseTransparency: boolean; NewTransparentColorIndex: byte); + property Count: integer read FCount; + property ColorMap: TGIFColorMap read FColorMap; + property List: TList read FList; + end; + + TColorMapOptimizer = class(TObject) + private + FImage : TGIFImage; + FHistogramList : TList; + FHistogram : TList; + FColorMap : TColorMap; + FFinalCount : integer; + FUseTransparency : boolean; + FNewTransparentColorIndex: byte; + protected + procedure ProcessImage; + procedure MergeColors; + procedure MapColors; + procedure ReplaceColorMaps; + public + constructor Create(AImage: TGIFImage); + destructor Destroy; override; + procedure Optimize; + end; + +function CompareColor(Item1, Item2: Pointer): integer; +begin + Result := POptimizeEntry(Item2)^.Color.Value - POptimizeEntry(Item1)^.Color.Value; +end; + +function CompareCount(Item1, Item2: Pointer): integer; +begin + Result := POptimizeEntry(Item2)^.Count - POptimizeEntry(Item1)^.Count; +end; + +constructor THistogram.Create(AColorMap: TGIFColorMap); +var + i : integer; +begin + inherited Create; + + FCount := AColorMap.Count; + FColorMap := AColorMap; + + FImages := TList.Create; + + // Allocate memory for histogram + GetMem(PHistogram, FCount * sizeof(TOptimizeEntry)); + FList := TList.Create; + + FList.Capacity := FCount; + + // Move data to histogram and initialize + for i := 0 to FCount-1 do + with PHistogram^[i] do + begin + FList.Add(@PHistogram^[i]); + OldIndex := i; + Count := 0; + Color.Value := 0; + Color.Color := AColorMap.Data^[i]; + NewIndex := 256; // Used to signal unmapped + end; +end; + +destructor THistogram.Destroy; +begin + FImages.Free; + FList.Free; + FreeMem(PHistogram); + inherited Destroy; +end; + +//: Build a color histogram +function THistogram.ProcessSubImage(Image: TGIFSubImage): boolean; +var + Size : integer; + Pixel : PChar; + IsTransparent , + WasTransparent : boolean; + OldTransparentColorIndex: byte; +begin + Result := False; + if (Image.Empty) then + exit; + + FImages.Add(Image); + + Pixel := Image.data; + Size := Image.Width * Image.Height; + + IsTransparent := Image.Transparent; + if (IsTransparent) then + OldTransparentColorIndex := Image.GraphicControlExtension.TransparentColorIndex + else + OldTransparentColorIndex := 0; // To avoid compiler warning + WasTransparent := False; + + (* + ** Sum up usage count for each color + *) + while (Size > 0) do + begin + // Ignore transparent pixels + if (not IsTransparent) or (ord(Pixel^) <> OldTransparentColorIndex) then + begin + // Check for invalid color index + if (ord(Pixel^) >= FCount) then + begin + Pixel^ := #0; // ***FIXME*** Isn't this an error condition? + Image.Warning(gsWarning, sInvalidColor); + end; + + with PHistogram^[ord(Pixel^)] do + begin + // Stop if any color reaches the max count + if (Count = high(integer)) then + break; + inc(Count); + end; + end else + WasTransparent := WasTransparent or IsTransparent; + inc(Pixel); + dec(Size); + end; + + (* + ** Clear frames transparency flag if the frame claimed to + ** be transparent, but wasn't + *) + if (IsTransparent and not WasTransparent) then + begin + Image.GraphicControlExtension.TransparentColorIndex := 0; + Image.GraphicControlExtension.Transparent := False; + end; + + Result := WasTransparent; +end; + +//: Removed unused color entries from the histogram +function THistogram.Prune: integer; +var + i, j : integer; +begin + (* + ** Sort by usage count + *) + FList.Sort(CompareCount); + + (* + ** Determine number of used colors + *) + for i := 0 to FCount-1 do + // Find first unused color entry + if (POptimizeEntry(FList[i])^.Count = 0) then + begin + // Zap unused colors + for j := i to FCount-1 do + POptimizeEntry(FList[j])^.Count := -1; // Use -1 to signal unused entry + // Remove unused entries + FCount := i; + FList.Count := FCount; + break; + end; + + Result := FCount; +end; + +//: Convert images from old color map to new color map +procedure THistogram.MapImages(UseTransparency: boolean; NewTransparentColorIndex: byte); +var + i : integer; + Size : integer; + Pixel : PChar; + ReverseMap : array[byte] of byte; + IsTransparent : boolean; + OldTransparentColorIndex: byte; +begin + (* + ** Build NewIndex map + *) + for i := 0 to List.Count-1 do + ReverseMap[POptimizeEntry(List[i])^.OldIndex] := POptimizeEntry(List[i])^.NewIndex; + + (* + ** Reorder all images using this color map + *) + for i := 0 to FImages.Count-1 do + with TGIFSubImage(FImages[i]) do + begin + Pixel := Data; + Size := Width * Height; + + // Determine frame transparency + IsTransparent := (Transparent) and (UseTransparency); + if (IsTransparent) then + begin + OldTransparentColorIndex := GraphicControlExtension.TransparentColorIndex; + // Map transparent color + GraphicControlExtension.TransparentColorIndex := NewTransparentColorIndex; + end else + OldTransparentColorIndex := 0; // To avoid compiler warning + + // Map all pixels to new color map + while (Size > 0) do + begin + // Map transparent pixels to the new transparent color index and... + if (IsTransparent) and (ord(Pixel^) = OldTransparentColorIndex) then + Pixel^ := char(NewTransparentColorIndex) + else + // ... all other pixels to their new color index + Pixel^ := char(ReverseMap[ord(Pixel^)]); + dec(size); + inc(Pixel); + end; + end; +end; + +constructor TColorMapOptimizer.Create(AImage: TGIFImage); +begin + inherited Create; + FImage := AImage; + FHistogramList := TList.Create; + FHistogram := TList.Create; +end; + +destructor TColorMapOptimizer.Destroy; +var + i : integer; +begin + FHistogram.Free; + + for i := FHistogramList.Count-1 downto 0 do + THistogram(FHistogramList[i]).Free; + FHistogramList.Free; + + inherited Destroy; +end; + +procedure TColorMapOptimizer.ProcessImage; +var + Hist : THistogram; + i : integer; + ProcessedImage : boolean; +begin + FUseTransparency := False; + (* + ** First process images using global color map + *) + if (FImage.GlobalColorMap.Count > 0) then + begin + Hist := THistogram.Create(FImage.GlobalColorMap); + ProcessedImage := False; + // Process all images that are using the global color map + for i := 0 to FImage.Images.Count-1 do + if (FImage.Images[i].ColorMap.Count = 0) and (not FImage.Images[i].Empty) then + begin + ProcessedImage := True; + // Note: Do not change order of statements. Shortcircuit evaluation not desired! + FUseTransparency := Hist.ProcessSubImage(FImage.Images[i]) or FUseTransparency; + end; + // Keep the histogram if any images used the global color map... + if (ProcessedImage) then + FHistogramList.Add(Hist) + else // ... otherwise delete it + Hist.Free; + end; + + (* + ** Next process images that have a local color map + *) + for i := 0 to FImage.Images.Count-1 do + if (FImage.Images[i].ColorMap.Count > 0) and (not FImage.Images[i].Empty) then + begin + Hist := THistogram.Create(FImage.Images[i].ColorMap); + FHistogramList.Add(Hist); + // Note: Do not change order of statements. Shortcircuit evaluation not desired! + FUseTransparency := Hist.ProcessSubImage(FImage.Images[i]) or FUseTransparency; + end; +end; + +procedure TColorMapOptimizer.MergeColors; +var + Entry, SameEntry : POptimizeEntry; + i : integer; +begin + (* + ** Sort by color value + *) + FHistogram.Sort(CompareColor); + + (* + ** Merge same colors + *) + SameEntry := POptimizeEntry(FHistogram[0]); + for i := 1 to FHistogram.Count-1 do + begin + Entry := POptimizeEntry(FHistogram[i]); + ASSERT(Entry^.Count > 0, 'Unused entry exported from THistogram'); + if (Entry^.Color.Value = SameEntry^.Color.Value) then + begin + // Transfer usage count to first entry + inc(SameEntry^.Count, Entry^.Count); + Entry^.Count := 0; // Use 0 to signal merged entry + Entry^.Color.SameAs := SameEntry; // Point to master + end else + SameEntry := Entry; + end; +end; + +procedure TColorMapOptimizer.MapColors; +var + i, j : integer; + Delta, BestDelta : integer; + BestIndex : integer; + MaxColors : integer; +begin + (* + ** Sort by usage count + *) + FHistogram.Sort(CompareCount); + + (* + ** Handle transparency + *) + if (FUseTransparency) then + MaxColors := 255 + else + MaxColors := 256; + + (* + ** Determine number of colors used (max 256) + *) + FFinalCount := FHistogram.Count; + for i := 0 to FFinalCount-1 do + if (i >= MaxColors) or (POptimizeEntry(FHistogram[i])^.Count = 0) then + begin + FFinalCount := i; + break; + end; + + (* + ** Build color map and reverse map for final entries + *) + for i := 0 to FFinalCount-1 do + begin + POptimizeEntry(FHistogram[i])^.NewIndex := i; + FColorMap[i] := POptimizeEntry(FHistogram[i])^.Color.Color; + end; + + (* + ** Map colors > 256 to colors <= 256 and build NewIndex color map + *) + for i := FFinalCount to FHistogram.Count-1 do + with POptimizeEntry(FHistogram[i])^ do + begin + // Entries with a usage count of -1 is unused + ASSERT(Count <> -1, 'Internal error: Unused entry exported'); + // Entries with a usage count of 0 has been merged with another entry + if (Count = 0) then + begin + // Use mapping of master entry + ASSERT(Color.SameAs.NewIndex < 256, 'Internal error: Mapping to unmapped color'); + NewIndex := Color.SameAs.NewIndex; + end else + begin + // Search for entry with nearest color value + BestIndex := 0; + BestDelta := 255*3; + for j := 0 to FFinalCount-1 do + begin + Delta := ABS((POptimizeEntry(FHistogram[j])^.Color.Color.Red - Color.Color.Red) + + (POptimizeEntry(FHistogram[j])^.Color.Color.Green - Color.Color.Green) + + (POptimizeEntry(FHistogram[j])^.Color.Color.Blue - Color.Color.Blue)); + if (Delta < BestDelta) then + begin + BestDelta := Delta; + BestIndex := j; + end; + end; + NewIndex := POptimizeEntry(FHistogram[BestIndex])^.NewIndex;; + end; + end; + + (* + ** Add transparency color to new color map + *) + if (FUseTransparency) then + begin + FNewTransparentColorIndex := FFinalCount; + FColorMap[FFinalCount].Red := 0; + FColorMap[FFinalCount].Green := 0; + FColorMap[FFinalCount].Blue := 0; + inc(FFinalCount); + end; +end; + +procedure TColorMapOptimizer.ReplaceColorMaps; +var + i : integer; +begin + // Zap all local color maps + for i := 0 to FImage.Images.Count-1 do + if (FImage.Images[i].ColorMap <> nil) then + FImage.Images[i].ColorMap.Clear; + // Store optimized global color map + FImage.GlobalColorMap.ImportColorMap(FColorMap, FFinalCount); + FImage.GlobalColorMap.Optimized := True; +end; + +procedure TColorMapOptimizer.Optimize; +var + Total : integer; + i, j : integer; +begin + // Stop all painters during optimize... + FImage.PaintStop; + // ...and prevent any new from starting while we are doing our thing + FImage.Painters.LockList; + try + + (* + ** Process all sub images + *) + ProcessImage; + + // Prune histograms and calculate total number of colors + Total := 0; + for i := 0 to FHistogramList.Count-1 do + inc(Total, THistogram(FHistogramList[i]).Prune); + + // Allocate global histogram + FHistogram.Clear; + FHistogram.Capacity := Total; + + // Move data pointers from local histograms to global histogram + for i := 0 to FHistogramList.Count-1 do + with THistogram(FHistogramList[i]) do + for j := 0 to Count-1 do + begin + ASSERT(POptimizeEntry(List[j])^.Count > 0, 'Unused entry exported from THistogram'); + FHistogram.Add(List[j]); + end; + + (* + ** Merge same colors + *) + MergeColors; + + (* + ** Build color map and NewIndex map for final entries + *) + MapColors; + + (* + ** Replace local colormaps with global color map + *) + ReplaceColorMaps; + + (* + ** Process images for each color map + *) + for i := 0 to FHistogramList.Count-1 do + THistogram(FHistogramList[i]).MapImages(FUseTransparency, FNewTransparentColorIndex); + + (* + ** Delete the frame's old bitmaps and palettes + *) + for i := 0 to FImage.Images.Count-1 do + begin + FImage.Images[i].HasBitmap := False; + FImage.Images[i].Palette := 0; + end; + + finally + FImage.Painters.UnlockList; + end; +end; + +//////////////////////////////////////////////////////////////////////////////// +// +// TGIFImage +// +//////////////////////////////////////////////////////////////////////////////// +constructor TGIFImage.Create; +begin + inherited Create; + FImages := TGIFImageList.Create(self); + FHeader := TGIFHeader.Create(self); + FPainters := TThreadList.Create; + FGlobalPalette := 0; + // Load defaults + FDrawOptions := GIFImageDefaultDrawOptions; + ColorReduction := GIFImageDefaultColorReduction; + FReductionBits := GIFImageDefaultColorReductionBits; + FDitherMode := GIFImageDefaultDitherMode; + FCompression := GIFImageDefaultCompression; + FThreadPriority := GIFImageDefaultThreadPriority; + FAnimationSpeed := GIFImageDefaultAnimationSpeed; + + FDrawBackgroundColor := clNone; + IsDrawing := False; + IsInsideGetPalette := False; + NewImage; +end; + +destructor TGIFImage.Destroy; +var + i : integer; +begin + PaintStop; + with FPainters.LockList do + try + for i := Count-1 downto 0 do + TGIFPainter(Items[i]).FImage := nil; + finally + FPainters.UnLockList; + end; + + Clear; + FPainters.Free; + FImages.Free; + FHeader.Free; + inherited Destroy; +end; + +procedure TGIFImage.Clear; +begin + PaintStop; + FreeBitmap; + FImages.Clear; + FHeader.ColorMap.Clear; + FHeader.Height := 0; + FHeader.Width := 0; + FHeader.Prepare; + Palette := 0; +end; + +procedure TGIFImage.NewImage; +begin + Clear; +end; + +function TGIFImage.GetVersion: TGIFVersion; +var + v : TGIFVersion; + i : integer; +begin + Result := gvUnknown; + for i := 0 to FImages.Count-1 do + begin + v := FImages[i].Version; + if (v > Result) then + Result := v; + if (v >= high(TGIFVersion)) then + break; + end; +end; + +function TGIFImage.GetColorResolution: integer; +var + i : integer; +begin + Result := FHeader.ColorResolution; + for i := 0 to FImages.Count-1 do + if (FImages[i].ColorResolution > Result) then + Result := FImages[i].ColorResolution; +end; + +function TGIFImage.GetBitsPerPixel: integer; +var + i : integer; +begin + Result := FHeader.BitsPerPixel; + for i := 0 to FImages.Count-1 do + if (FImages[i].BitsPerPixel > Result) then + Result := FImages[i].BitsPerPixel; +end; + +function TGIFImage.GetBackgroundColorIndex: BYTE; +begin + Result := FHeader.BackgroundColorIndex; +end; + +procedure TGIFImage.SetBackgroundColorIndex(const Value: BYTE); +begin + FHeader.BackgroundColorIndex := Value; +end; + +function TGIFImage.GetBackgroundColor: TColor; +begin + Result := FHeader.BackgroundColor; +end; + +procedure TGIFImage.SetBackgroundColor(const Value: TColor); +begin + FHeader.BackgroundColor := Value; +end; + +function TGIFImage.GetAspectRatio: BYTE; +begin + Result := FHeader.AspectRatio; +end; + +procedure TGIFImage.SetAspectRatio(const Value: BYTE); +begin + FHeader.AspectRatio := Value; +end; + +procedure TGIFImage.SetDrawOptions(Value: TGIFDrawOptions); +begin + if (FDrawOptions = Value) then + exit; + + if (DrawPainter <> nil) then + DrawPainter.Stop; + + FDrawOptions := Value; + // Zap all bitmaps + Pack; + Changed(self); +end; + +function TGIFImage.GetAnimate: Boolean; +begin // 2002.07.07 + Result:= goAnimate in DrawOptions; +end; + +procedure TGIFImage.SetAnimate(const Value: Boolean); +begin // 2002.07.07 + if Value then + DrawOptions:= DrawOptions + [goAnimate] + else + DrawOptions:= DrawOptions - [goAnimate]; +end; + +procedure TGIFImage.SetAnimationSpeed(Value: integer); +begin + if (Value < 0) then + Value := 0 + else if (Value > 1000) then + Value := 1000; + if (Value <> FAnimationSpeed) then + begin + FAnimationSpeed := Value; + // Use the FPainters threadlist to protect FDrawPainter from being modified + // by the thread while we mess with it + with FPainters.LockList do + try + if (FDrawPainter <> nil) then + FDrawPainter.AnimationSpeed := FAnimationSpeed; + finally + // Release the lock on FPainters to let paint thread kill itself + FPainters.UnLockList; + end; + end; +end; + +procedure TGIFImage.SetReductionBits(Value: integer); +begin + if (Value < 3) or (Value > 8) then + Error(sInvalidBitSize); + FReductionBits := Value; +end; + +procedure TGIFImage.OptimizeColorMap; +var + ColorMapOptimizer : TColorMapOptimizer; +begin + ColorMapOptimizer := TColorMapOptimizer.Create(self); + try + ColorMapOptimizer.Optimize; + finally + ColorMapOptimizer.Free; + end; +end; + +procedure TGIFImage.Optimize(Options: TGIFOptimizeOptions; + ColorReduction: TColorReduction; DitherMode: TDitherMode; + ReductionBits: integer); +var + i , + j : integer; + Delay : integer; + GCE : TGIFGraphicControlExtension; + ThisRect , + NextRect , + MergeRect : TRect; + Prog , + MaxProg : integer; + + function Scan(Buf: PChar; Value: Byte; Count: integer): boolean; assembler; + asm + PUSH EDI + MOV EDI, Buf + MOV ECX, Count + MOV AL, Value + REPNE SCASB + MOV EAX, False + JNE @@1 + MOV EAX, True +@@1:POP EDI + end; + +begin + if (Empty) then + exit; + // Stop all painters during optimize... + PaintStop; + // ...and prevent any new from starting while we are doing our thing + FPainters.LockList; + try + Progress(Self, psStarting, 0, False, Rect(0,0,0,0), sProgressOptimizing); + try + + Prog := 0; + MaxProg := Images.Count*6; + + // Sort color map by usage and remove unused entries + if (ooColorMap in Options) then + begin + // Optimize global color map + if (GlobalColorMap.Count > 0) then + GlobalColorMap.Optimize; + // Optimize local color maps + for i := 0 to Images.Count-1 do + begin + inc(Prog); + if (Images[i].ColorMap.Count > 0) then + begin + Images[i].ColorMap.Optimize; + Progress(Self, psRunning, MulDiv(Prog, 100, MaxProg), False, + Rect(0,0,0,0), sProgressOptimizing); + end; + end; + end; + + // Remove passive elements, pass 1 + if (ooCleanup in Options) then + begin + // Check for transparency flag without any transparent pixels + for i := 0 to Images.Count-1 do + begin + inc(Prog); + if (Images[i].Transparent) then + begin + if not(Scan(Images[i].Data, + Images[i].GraphicControlExtension.TransparentColorIndex, + Images[i].DataSize)) then + begin + Images[i].GraphicControlExtension.Transparent := False; + Progress(Self, psRunning, MulDiv(Prog, 100, MaxProg), False, + Rect(0,0,0,0), sProgressOptimizing); + end; + end; + end; + + // Change redundant disposal modes + for i := 0 to Images.Count-2 do + begin + inc(Prog); + if (Images[i].GraphicControlExtension <> nil) and + (Images[i].GraphicControlExtension.Disposal in [dmPrevious, dmBackground]) and + (not Images[i+1].Transparent) then + begin + ThisRect := Images[i].BoundsRect; + NextRect := Images[i+1].BoundsRect; + if (not IntersectRect(MergeRect, ThisRect, NextRect)) then + continue; + // If the next frame completely covers the current frame, + // change the disposal mode to dmNone + if (EqualRect(MergeRect, NextRect)) then + Images[i].GraphicControlExtension.Disposal := dmNone; + Progress(Self, psRunning, MulDiv(Prog, 100, MaxProg), False, + Rect(0,0,0,0), sProgressOptimizing); + end; + end; + end else + inc(Prog, 2*Images.Count); + + // Merge layers of equal pixels (remove redundant pixels) + if (ooMerge in Options) then + begin + // Merge from last to first to avoid intefering with merge + for i := Images.Count-1 downto 1 do + begin + inc(Prog); + j := i-1; + // If the "previous" frames uses dmPrevious disposal mode, we must + // instead merge with the frame before the previous + while (j > 0) and + ((Images[j].GraphicControlExtension <> nil) and + (Images[j].GraphicControlExtension.Disposal = dmPrevious)) do + dec(j); + // Merge + Images[i].Merge(Images[j]); + Progress(Self, psRunning, MulDiv(Prog, 100, MaxProg), False, + Rect(0,0,0,0), sProgressOptimizing); + end; + end else + inc(Prog, Images.Count); + + // Crop transparent areas + if (ooCrop in Options) then + begin + for i := Images.Count-1 downto 0 do + begin + inc(Prog); + if (not Images[i].Empty) and (Images[i].Transparent) then + begin + // Remember frames delay in case frame is deleted + Delay := Images[i].GraphicControlExtension.Delay; + // Crop + Images[i].Crop; + // If the frame was completely transparent we remove it + if (Images[i].Empty) then + begin + // Transfer delay to previous frame in case frame was deleted + if (i > 0) and (Images[i-1].Transparent) then + Images[i-1].GraphicControlExtension.Delay := + Images[i-1].GraphicControlExtension.Delay + Delay; + Images.Delete(i); + end; + Progress(Self, psRunning, MulDiv(Prog, 100, MaxProg), False, + Rect(0,0,0,0), sProgressOptimizing); + end; + end; + end else + inc(Prog, Images.Count); + + // Remove passive elements, pass 2 + inc(Prog, Images.Count); + if (ooCleanup in Options) then + begin + for i := Images.Count-1 downto 0 do + begin + // Remove comments and application extensions + for j := Images[i].Extensions.Count-1 downto 0 do + if (Images[i].Extensions[j] is TGIFCommentExtension) or + (Images[i].Extensions[j] is TGIFTextExtension) or + (Images[i].Extensions[j] is TGIFUnknownAppExtension) or + ((Images[i].Extensions[j] is TGIFAppExtNSLoop) and + ((i > 0) or (Images.Count = 1))) then + Images[i].Extensions.Delete(j); + if (Images[i].GraphicControlExtension <> nil) then + begin + GCE := Images[i].GraphicControlExtension; + // Zap GCE if all of the following are true: + // * No delay or only one image + // * Not transparent + // * No prompt + // * No disposal or only one image + if ((GCE.Delay = 0) or (Images.Count = 1)) and + (not GCE.Transparent) and + (not GCE.UserInput) and + ((GCE.Disposal in [dmNone, dmNoDisposal]) or (Images.Count = 1)) then + begin + GCE.Free; + end; + end; + // Zap frame if it has become empty + if (Images[i].Empty) and (Images[i].Extensions.Count = 0) then + Images[i].Free; + end; + Progress(Self, psRunning, MulDiv(Prog, 100, MaxProg), False, + Rect(0,0,0,0), sProgressOptimizing); + end else + + // Reduce color depth + if (ooReduceColors in Options) then + begin + if (ColorReduction = rmPalette) then + Error(sInvalidReduction); + { TODO -oanme -cFeature : Implement ooReduceColors option. } + // Not implemented! + end; + finally + if ExceptObject = nil then + i := 100 + else + i := 0; + Progress(Self, psEnding, i, False, Rect(0,0,0,0), sProgressOptimizing); + end; + finally + FPainters.UnlockList; + end; +end; + +procedure TGIFImage.Pack; +var + i : integer; +begin + // Zap bitmaps and palettes + FreeBitmap; + Palette := 0; + for i := 0 to FImages.Count-1 do + begin + FImages[i].Bitmap := nil; + FImages[i].Palette := 0; + end; + + // Only pack if no global colormap and a single image + if (FHeader.ColorMap.Count > 0) or (FImages.Count <> 1) then + exit; + + // Copy local colormap to global + FHeader.ColorMap.Assign(FImages[0].ColorMap); + // Zap local colormap + FImages[0].ColorMap.Clear; +end; + +procedure TGIFImage.SaveToStream(Stream: TStream); +var + n : Integer; +begin + Progress(Self, psStarting, 0, False, Rect(0,0,0,0), sProgressSaving); + try + // Write header + FHeader.SaveToStream(Stream); + // Write images + FImages.SaveToStream(Stream); + // Write trailer + with TGIFTrailer.Create(self) do + try + SaveToStream(Stream); + finally + Free; + end; + finally + if ExceptObject = nil then + n := 100 + else + n := 0; + Progress(Self, psEnding, n, True, Rect(0,0,0,0), sProgressSaving); + end; +end; + +procedure TGIFImage.LoadFromStream(Stream: TStream); +var + n : Integer; + Position : integer; +begin + Progress(Self, psStarting, 0, False, Rect(0,0,0,0), sProgressLoading); + try + // Zap old image + Clear; + Position := Stream.Position; + try + // Read header + FHeader.LoadFromStream(Stream); + // Read images + FImages.LoadFromStream(Stream, self); + // Read trailer + with TGIFTrailer.Create(self) do + try + LoadFromStream(Stream); + finally + Free; + end; + except + // Restore stream position in case of error. + // Not required, but "a nice thing to do" + Stream.Position := Position; + raise; + end; + finally + if ExceptObject = nil then + n := 100 + else + n := 0; + Progress(Self, psEnding, n, True, Rect(0,0,0,0), sProgressLoading); + end; +end; + +procedure TGIFImage.LoadFromResourceName(Instance: THandle; const ResName: String); +// 2002.07.07 +var + Stream: TCustomMemoryStream; +begin + Stream := TResourceStream.Create(Instance, ResName, RT_RCDATA); + try + LoadFromStream(Stream); + finally + Stream.Free; + end; +end; + +function TGIFImage.GetBitmap: TBitmap; +begin + if not(Empty) then + begin + Result := FBitmap; + if (Result <> nil) then + exit; + FBitmap := TBitmap.Create; + Result := FBitmap; + FBitmap.OnChange := Changed; + // Use first image as default + if (Images.Count > 0) then + begin + if (Images[0].Width = Width) and (Images[0].Height = Height) then + begin + // Use first image as it has same dimensions + FBitmap.Assign(Images[0].Bitmap); + end else + begin + // Draw first image on bitmap + FBitmap.Palette := CopyPalette(Palette); + FBitmap.Height := Height; + FBitmap.Width := Width; + Images[0].Draw(FBitmap.Canvas, FBitmap.Canvas.ClipRect, False, False); + end; + end; + end else + Result := nil +end; + +// Create a new (empty) bitmap +function TGIFImage.NewBitmap: TBitmap; +begin + Result := FBitmap; + if (Result <> nil) then + exit; + FBitmap := TBitmap.Create; + Result := FBitmap; + FBitmap.OnChange := Changed; + // Draw first image on bitmap + FBitmap.Palette := CopyPalette(Palette); + FBitmap.Height := Height; + FBitmap.Width := Width; +end; + +procedure TGIFImage.FreeBitmap; +begin + if (DrawPainter <> nil) then + DrawPainter.Stop; + + if (FBitmap <> nil) then + begin + FBitmap.Free; + FBitmap := nil; + end; +end; + +function TGIFImage.Add(Source: TPersistent): integer; +var + Image : TGIFSubImage; +begin + Image := nil; // To avoid compiler warning - not needed. + if (Source is TGraphic) then + begin + Image := TGIFSubImage.Create(self); + try + Image.Assign(Source); + // ***FIXME*** Documentation should explain the inconsistency here: + // TGIFimage does not take ownership of Source after TGIFImage.Add() and + // therefore does not delete Source. + except + Image.Free; + raise; + end; + end else + if (Source is TGIFSubImage) then + Image := TGIFSubImage(Source) + else + Error(sUnsupportedClass); + + Result := FImages.Add(Image); + + FreeBitmap; + Changed(self); +end; + +function TGIFImage.GetEmpty: Boolean; +begin + Result := (FImages.Count = 0); +end; + +function TGIFImage.GetHeight: Integer; +begin + Result := FHeader.Height; +end; + +function TGIFImage.GetWidth: Integer; +begin + Result := FHeader.Width; +end; + +function TGIFImage.GetIsTransparent: Boolean; +var + i : integer; +begin + Result := False; + for i := 0 to Images.Count-1 do + if (Images[i].GraphicControlExtension <> nil) and + (Images[i].GraphicControlExtension.Transparent) then + begin + Result := True; + exit; + end; +end; + +function TGIFImage.Equals(Graphic: TGraphic): Boolean; +begin + Result := (Graphic = self); +end; + +function TGIFImage.GetPalette: HPALETTE; +begin + // Check for recursion + // (TGIFImage.GetPalette->TGIFSubImage.GetPalette->TGIFImage.GetPalette etc...) + if (IsInsideGetPalette) then + Error(sNoColorTable); + IsInsideGetPalette := True; + try + Result := 0; + if (FBitmap <> nil) and (FBitmap.Palette <> 0) then + // Use bitmaps own palette if possible + Result := FBitmap.Palette + else if (FGlobalPalette <> 0) then + // Or a previously exported global palette + Result := FGlobalPalette + else if (DoDither) then + begin + // or create a new dither palette + FGlobalPalette := WebPalette; + Result := FGlobalPalette; + end else + if (FHeader.ColorMap.Count > 0) then + begin + // or create a new if first time + FGlobalPalette := FHeader.ColorMap.ExportPalette; + Result := FGlobalPalette; + end else + if (FImages.Count > 0) then + // This can cause a recursion if no global palette exist and image[0] + // hasn't got one either. Checked by the IsInsideGetPalette semaphor. + Result := FImages[0].Palette; + finally + IsInsideGetPalette := False; + end; +end; + +procedure TGIFImage.SetPalette(Value: HPalette); +var + NeedNewBitmap : boolean; +begin + if (Value <> FGlobalPalette) then + begin + // Zap old palette + if (FGlobalPalette <> 0) then + DeleteObject(FGlobalPalette); + + // Zap bitmap unless new palette is same as bitmaps own + NeedNewBitmap := (FBitmap <> nil) and (Value <> FBitmap.Palette); + + // Use new palette + FGlobalPalette := Value; + + if (NeedNewBitmap) then + begin + // Need to create new bitmap and repaint + FreeBitmap; + PaletteModified := True; + Changed(Self); + end; + end; +end; + +// Obsolete +// procedure TGIFImage.Changed(Sender: TObject); +// begin +// inherited Changed(Sender); +// end; + +procedure TGIFImage.SetHeight(Value: Integer); +var + i : integer; +begin + for i := 0 to Images.Count-1 do + if (Images[i].Top + Images[i].Height > Value) then + Error(sBadHeight); + if (Value <> Header.Height) then + begin + Header.Height := Value; + FreeBitmap; + Changed(self); + end; +end; + +procedure TGIFImage.SetWidth(Value: Integer); +var + i : integer; +begin + for i := 0 to Images.Count-1 do + if (Images[i].Left + Images[i].Width > Value) then + Error(sBadWidth); + if (Value <> Header.Width) then + begin + Header.Width := Value; + FreeBitmap; + Changed(self); + end; +end; + +procedure TGIFImage.WriteData(Stream: TStream); +begin + if (GIFImageOptimizeOnStream) then + Optimize([ooCrop, ooMerge, ooCleanup, ooColorMap, ooReduceColors], rmNone, dmNearest, 8); + + inherited WriteData(Stream); +end; + +procedure TGIFImage.AssignTo(Dest: TPersistent); +begin + if (Dest is TBitmap) then + Dest.Assign(Bitmap) + else + inherited AssignTo(Dest); +end; + +{ TODO 1 -oanme -cImprovement : Better handling of TGIFImage.Assign(Empty TBitmap). } +procedure TGIFImage.Assign(Source: TPersistent); +var + i : integer; + Image : TGIFSubImage; +begin + if (Source = self) then + exit; + if (Source = nil) then + begin + Clear; + end else + // + // TGIFImage import + // + if (Source is TGIFImage) then + begin + Clear; + // Temporarily copy event handlers to be able to generate progress events + // during the copy and handle copy errors + OnProgress := TGIFImage(Source).OnProgress; + try + FOnWarning := TGIFImage(Source).OnWarning; + Progress(Self, psStarting, 0, False, Rect(0,0,0,0), sProgressCopying); + try + FHeader.Assign(TGIFImage(Source).Header); + FThreadPriority := TGIFImage(Source).ThreadPriority; + FDrawBackgroundColor := TGIFImage(Source).DrawBackgroundColor; + FDrawOptions := TGIFImage(Source).DrawOptions; + FColorReduction := TGIFImage(Source).ColorReduction; + FDitherMode := TGIFImage(Source).DitherMode; +// 2002.07.07 -> + FOnWarning:= TGIFImage(Source).FOnWarning; + FOnStartPaint:= TGIFImage(Source).FOnStartPaint; + FOnPaint:= TGIFImage(Source).FOnPaint; + FOnEndPaint:= TGIFImage(Source).FOnEndPaint; + FOnAfterPaint:= TGIFImage(Source).FOnAfterPaint; + FOnLoop:= TGIFImage(Source).FOnLoop; +// 2002.07.07 <- + for i := 0 to TGIFImage(Source).Images.Count-1 do + begin + Image := TGIFSubImage.Create(self); + Image.Assign(TGIFImage(Source).Images[i]); + Add(Image); + Progress(Self, psRunning, MulDiv((i+1), 100, TGIFImage(Source).Images.Count), + False, Rect(0,0,0,0), sProgressCopying); + end; + finally + if ExceptObject = nil then + i := 100 + else + i := 0; + Progress(Self, psEnding, i, False, Rect(0,0,0,0), sProgressCopying); + end; + finally + // Reset event handlers + FOnWarning := nil; + OnProgress := nil; + end; + end else + // + // Import via TGIFSubImage.Assign + // + begin + Clear; + Image := TGIFSubImage.Create(self); + try + Image.Assign(Source); + Add(Image); + except + on E: EConvertError do + begin + Image.Free; + // Unsupported format - fall back to Source.AssignTo + inherited Assign(Source); + end; + else + // Unknown conversion error + Image.Free; + raise; + end; + end; +end; + +procedure TGIFImage.LoadFromClipboardFormat(AFormat: Word; AData: THandle; + APalette: HPALETTE); +{$IFDEF REGISTER_TGIFIMAGE} +var + Size : Longint; + Buffer : Pointer; + Stream : TMemoryStream; + Bmp : TBitmap; +{$ENDIF} // 2002.07.07 +begin // 2002.07.07 +{$IFDEF REGISTER_TGIFIMAGE} // 2002.07.07 + if (AData = 0) then + AData := GetClipboardData(AFormat); + if (AData <> 0) and (AFormat = CF_GIF) then + begin + // Get size and pointer to data + Size := GlobalSize(AData); + Buffer := GlobalLock(AData); + try + Stream := TMemoryStream.Create; + try + // Copy data to a stream + Stream.SetSize(Size); + Move(Buffer^, Stream.Memory^, Size); + // Load GIF from stream + LoadFromStream(Stream); + finally + Stream.Free; + end; + finally + GlobalUnlock(AData); + end; + end else + if (AData <> 0) and (AFormat = CF_BITMAP) then + begin + // No GIF on clipboard - try loading a bitmap instead + Bmp := TBitmap.Create; + try + Bmp.LoadFromClipboardFormat(AFormat, AData, APalette); + Assign(Bmp); + finally + Bmp.Free; + end; + end else + Error(sUnknownClipboardFormat); +{$ELSE} // 2002.07.07 + Error(sGIFToClipboard); // 2002.07.07 +{$ENDIF} // 2002.07.07 +end; + +procedure TGIFImage.SaveToClipboardFormat(var AFormat: Word; var AData: THandle; + var APalette: HPALETTE); +{$IFDEF REGISTER_TGIFIMAGE} +var + Stream : TMemoryStream; + Data : THandle; + Buffer : Pointer; +{$ENDIF} // 2002.07.07 +begin // 2002.07.07 +{$IFDEF REGISTER_TGIFIMAGE} // 2002.07.07 + if (Empty) then + exit; + // First store a bitmap version on the clipboard... + Bitmap.SaveToClipboardFormat(AFormat, AData, APalette); + // ...then store a GIF + Stream := TMemoryStream.Create; + try + // Save the GIF to a memory stream + SaveToStream(Stream); + Stream.Position := 0; + // Allocate some memory for the GIF data + Data := GlobalAlloc(HeapAllocFlags, Stream.Size); + try + if (Data <> 0) then + begin + Buffer := GlobalLock(Data); + try + // Copy GIF data from stream memory to clipboard memory + Move(Stream.Memory^, Buffer^, Stream.Size); + finally + GlobalUnlock(Data); + end; + // Transfer data to clipboard + if (SetClipboardData(CF_GIF, Data) = 0) then + Error(sFailedPaste); + end; + except + GlobalFree(Data); + raise; + end; + finally + Stream.Free; + end; +{$ELSE} // 2002.07.07 + Error(sGIFToClipboard); // 2002.07.07 +{$ENDIF} // 2002.07.07 +end; + +function TGIFImage.GetColorMap: TGIFColorMap; +begin + Result := FHeader.ColorMap; +end; + +function TGIFImage.GetDoDither: boolean; +begin + Result := (goDither in DrawOptions) and + (((goAutoDither in DrawOptions) and DoAutoDither) or + not(goAutoDither in DrawOptions)); +end; + +{$IFDEF VER9x} +procedure TGIFImage.Progress(Sender: TObject; Stage: TProgressStage; + PercentDone: Byte; RedrawNow: Boolean; const R: TRect; const Msg: string); +begin + if Assigned(FOnProgress) then + FOnProgress(Sender, Stage, PercentDone, RedrawNow, R, Msg); +end; +{$ENDIF} + +procedure TGIFImage.StopDraw; +{$IFNDEF VER14_PLUS} // 2001.07.23 +var + Msg : TMsg; + ThreadWindow : HWND; +{$ENDIF} // 2001.07.23 +begin + repeat + // Use the FPainters threadlist to protect FDrawPainter from being modified + // by the thread while we mess with it + with FPainters.LockList do + try + if (FDrawPainter = nil) then + break; + + // Tell thread to terminate + FDrawPainter.Stop; + + // No need to wait for "thread" to terminate if running in main thread + if not(goAsync in FDrawPainter.DrawOptions) then + break; + + finally + // Release the lock on FPainters to let paint thread kill itself + FPainters.UnLockList; + end; + +{$IFDEF VER14_PLUS} +// 2002.07.07 + if (GetCurrentThreadID = MainThreadID) then + while CheckSynchronize do {loop}; +{$ELSE} + // Process Messages to make Synchronize work + // (Instead of Application.ProcessMessages) +//{$IFDEF VER14_PLUS} // 2001.07.23 +// Break; // 2001.07.23 +// Sleep(0); // Yield // 2001.07.23 +//{$ELSE} // 2001.07.23 + ThreadWindow := FindWindow('TThreadWindow', nil); + while PeekMessage(Msg, ThreadWindow, CM_DESTROYWINDOW, CM_EXECPROC, PM_REMOVE) do + begin + if (Msg.Message <> WM_QUIT) then + begin + TranslateMessage(Msg); + DispatchMessage(Msg); + end else + begin + PostQuitMessage(Msg.WParam); + exit; + end; + end; +{$ENDIF} // 2001.07.23 + Sleep(0); // Yield + + until (False); + FreeBitmap; +end; + +procedure TGIFImage.Draw(ACanvas: TCanvas; const Rect: TRect); +var + Canvas : TCanvas; + DestRect : TRect; +{$IFNDEF VER14_PLUS} // 2001.07.23 + Msg : TMsg; + ThreadWindow : HWND; +{$ENDIF} // 2001.07.23 + + procedure DrawTile(Rect: TRect; Bitmap: TBitmap); + var + Tile : TRect; + begin + if (goTile in FDrawOptions) then + begin + // Note: This design does not handle transparency correctly! + Tile.Left := Rect.Left; + Tile.Right := Tile.Left + Width; + while (Tile.Left < Rect.Right) do + begin + Tile.Top := Rect.Top; + Tile.Bottom := Tile.Top + Height; + while (Tile.Top < Rect.Bottom) do + begin + ACanvas.StretchDraw(Tile, Bitmap); + Tile.Top := Tile.Top + Height; + Tile.Bottom := Tile.Top + Height; + end; + Tile.Left := Tile.Left + Width; + Tile.Right := Tile.Left + Width; + end; + end else + ACanvas.StretchDraw(Rect, Bitmap); + end; + +begin + // Prevent recursion(s(s(s))) + if (IsDrawing) or (FImages.Count = 0) then + exit; + + IsDrawing := True; + try + // Copy bitmap to canvas if we are already drawing + // (or have drawn but are finished) + if (FImages.Count = 1) or // Only one image + (not (goAnimate in FDrawOptions)) then // Don't animate + begin + FImages[0].Draw(ACanvas, Rect, (goTransparent in FDrawOptions), + (goTile in FDrawOptions)); + exit; + end else + if (FBitmap <> nil) and not(goDirectDraw in FDrawOptions) then + begin + DrawTile(Rect, Bitmap); + exit; + end; + + // Use the FPainters threadlist to protect FDrawPainter from being modified + // by the thread while we mess with it + with FPainters.LockList do + try + // If we are already painting on the canvas in goDirectDraw mode + // and at the same location, just exit and let the painter do + // its thing when it's ready + if (FDrawPainter <> nil) and (FDrawPainter.Canvas = ACanvas) and + EqualRect(FDrawPainter.Rect, Rect) then + exit; + + // Kill the current paint thread + StopDraw; + + if not(goDirectDraw in FDrawOptions) then + begin + // Create a bitmap to draw on + NewBitmap; + Canvas := FBitmap.Canvas; + DestRect := Canvas.ClipRect; + // Initialize bitmap canvas with background image + Canvas.CopyRect(DestRect, ACanvas, Rect); + end else + begin + Canvas := ACanvas; + DestRect := Rect; + end; + + // Create new paint thread + InternalPaint(@FDrawPainter, Canvas, DestRect, FDrawOptions); + + if (FDrawPainter <> nil) then + begin + // Launch thread + FDrawPainter.Start; + + if not(goDirectDraw in FDrawOptions) then + begin +{$IFDEF VER14_PLUS} +// 2002.07.07 + while (FDrawPainter <> nil) and (not FDrawPainter.Terminated) and + (not FDrawPainter.Started) do + begin + if not CheckSynchronize then + Sleep(0); // Yield + end; +{$ELSE} +//{$IFNDEF VER14_PLUS} // 2001.07.23 + ThreadWindow := FindWindow('TThreadWindow', nil); + // Wait for thread to render first frame + while (FDrawPainter <> nil) and (not FDrawPainter.Terminated) and + (not FDrawPainter.Started) do + // Process Messages to make Synchronize work + // (Instead of Application.ProcessMessages) + if PeekMessage(Msg, ThreadWindow, CM_DESTROYWINDOW, CM_EXECPROC, PM_REMOVE) then + begin + if (Msg.Message <> WM_QUIT) then + begin + TranslateMessage(Msg); + DispatchMessage(Msg); + end else + begin + PostQuitMessage(Msg.WParam); + exit; + end; + end else + Sleep(0); // Yield +{$ENDIF} // 2001.07.23 + // Draw frame to destination + DrawTile(Rect, Bitmap); + end; + end; + finally + FPainters.UnLockList; + end; + + finally + IsDrawing := False; + end; +end; + +// Internal pain(t) routine used by Draw() +function TGIFImage.InternalPaint(Painter: PGifPainter; ACanvas: TCanvas; + const Rect: TRect; Options: TGIFDrawOptions): TGIFPainter; +begin + if (Empty) or (Rect.Left >= Rect.Right) or (Rect.Top >= Rect.Bottom) then + begin + Result := nil; + if (Painter <> nil) then + Painter^ := Result; + exit; + end; + + // Draw in main thread if only one image + if (Images.Count = 1) then + Options := Options - [goAsync, goAnimate]; + + Result := TGIFPainter.CreateRef(Painter, self, ACanvas, Rect, Options); + FPainters.Add(Result); + Result.OnStartPaint := FOnStartPaint; + Result.OnPaint := FOnPaint; + Result.OnAfterPaint := FOnAfterPaint; + Result.OnLoop := FOnLoop; + Result.OnEndPaint := FOnEndPaint; + + if not(goAsync in Options) then + begin + // Run in main thread + Result.Execute; + // Note: Painter threads executing in the main thread are freed upon exit + // from the Execute method, so no need to do it here. + Result := nil; + if (Painter <> nil) then + Painter^ := Result; + end else + Result.Priority := FThreadPriority; +end; + +function TGIFImage.Paint(ACanvas: TCanvas; const Rect: TRect; + Options: TGIFDrawOptions): TGIFPainter; +begin + Result := InternalPaint(nil, ACanvas, Rect, Options); + if (Result <> nil) then + // Run in separate thread + Result.Start; +end; + +procedure TGIFImage.PaintStart; +var + i : integer; +begin + with FPainters.LockList do + try + for i := 0 to Count-1 do + TGIFPainter(Items[i]).Start; + finally + FPainters.UnLockList; + end; +end; + +procedure TGIFImage.PaintStop; +var + Ghosts : integer; + i : integer; +{$IFNDEF VER14_PLUS} // 2001.07.23 + Msg : TMsg; + ThreadWindow : HWND; +{$ENDIF} // 2001.07.23 + +{$IFNDEF VER14_PLUS} // 2001.07.23 + procedure KillThreads; + var + i : integer; + begin + with FPainters.LockList do + try + for i := Count-1 downto 0 do + if (goAsync in TGIFPainter(Items[i]).DrawOptions) then + begin + TerminateThread(TGIFPainter(Items[i]).Handle, 0); + Delete(i); + end; + finally + FPainters.UnLockList; + end; + end; +{$ENDIF} // 2001.07.23 + +begin + try + // Loop until all have died + repeat + with FPainters.LockList do + try + if (Count = 0) then + exit; + + // Signal painters to terminate + // Painters will attempt to remove them self from the + // painter list when they die + Ghosts := Count; + for i := Ghosts-1 downto 0 do + begin + if not(goAsync in TGIFPainter(Items[i]).DrawOptions) then + dec(Ghosts); + TGIFPainter(Items[i]).Stop; + end; + finally + FPainters.UnLockList; + end; + + // If all painters were synchronous, there's no purpose waiting for them + // to terminate, because they are running in the main thread. + if (Ghosts = 0) then + exit; +{$IFDEF VER14_PLUS} +// 2002.07.07 + if (GetCurrentThreadID = MainThreadID) then + while CheckSynchronize do {loop}; +{$ELSE} + // Process Messages to make TThread.Synchronize work + // (Instead of Application.ProcessMessages) +//{$IFDEF VER14_PLUS} // 2001.07.23 +// Exit; // 2001.07.23 +//{$ELSE} // 2001.07.23 + ThreadWindow := FindWindow('TThreadWindow', nil); + if (ThreadWindow = 0) then + begin + KillThreads; + Exit; + end; + while PeekMessage(Msg, ThreadWindow, CM_DESTROYWINDOW, CM_EXECPROC, PM_REMOVE) do + begin + if (Msg.Message <> WM_QUIT) then + begin + TranslateMessage(Msg); + DispatchMessage(Msg); + end else + begin + KillThreads; + Exit; + end; + end; +{$ENDIF} // 2001.07.23 + Sleep(0); + until (False); + finally + FreeBitmap; + end; +end; + +procedure TGIFImage.PaintPause; +var + i : integer; +begin + with FPainters.LockList do + try + for i := 0 to Count-1 do + TGIFPainter(Items[i]).Suspend; + finally + FPainters.UnLockList; + end; +end; + +procedure TGIFImage.PaintResume; +var + i : integer; +begin + // Implementation is currently same as PaintStart, but don't call PaintStart + // in case its implementation changes + with FPainters.LockList do + try + for i := 0 to Count-1 do + TGIFPainter(Items[i]).Start; + finally + FPainters.UnLockList; + end; +end; + +procedure TGIFImage.PaintRestart; +var + i : integer; +begin + with FPainters.LockList do + try + for i := 0 to Count-1 do + TGIFPainter(Items[i]).Restart; + finally + FPainters.UnLockList; + end; +end; + +procedure TGIFImage.Warning(Sender: TObject; Severity: TGIFSeverity; Message: string); +begin + if (Assigned(FOnWarning)) then + FOnWarning(Sender, Severity, Message); +end; + +{$IFDEF VER12_PLUS} + {$IFNDEF VER14_PLUS} // not anymore need for Delphi 6 and up // 2001.07.23 +type + TDummyThread = class(TThread) + protected + procedure Execute; override; + end; +procedure TDummyThread.Execute; +begin +end; + {$ENDIF} // 2001.07.23 +{$ENDIF} + +var + DesktopDC: HDC; +{$IFDEF VER12_PLUS} + {$IFNDEF VER14_PLUS} // not anymore need for Delphi 6 and up // 2001.07.23 + DummyThread: TThread; + {$ENDIF} // 2001.07.23 +{$ENDIF} + +//////////////////////////////////////////////////////////////////////////////// +// +// Initialization +// +//////////////////////////////////////////////////////////////////////////////// + +initialization +{$IFDEF REGISTER_TGIFIMAGE} + TPicture.RegisterFileFormat('GIF', sGIFImageFile, TGIFImage); + CF_GIF := RegisterClipboardFormat(PChar(sGIFImageFile)); + TPicture.RegisterClipboardFormat(CF_GIF, TGIFImage); +{$ENDIF} + DesktopDC := GetDC(0); + try + PaletteDevice := (GetDeviceCaps(DesktopDC, BITSPIXEL) * GetDeviceCaps(DesktopDC, PLANES) <= 8); + DoAutoDither := PaletteDevice; + finally + ReleaseDC(0, DesktopDC); + end; + +{$IFDEF VER9x} + // Note: This doesn't return the same palette as the Delphi 3 system palette + // since the true system palette contains 20 entries and the Delphi 3 system + // palette only contains 16. + // For our purpose this doesn't matter since we do not care about the actual + // colors (or their number) in the palette. + // Stock objects doesn't have to be deleted. + SystemPalette16 := GetStockObject(DEFAULT_PALETTE); +{$ENDIF} +{$IFDEF VER12_PLUS} + // Make sure that at least one thread always exist. + // This is done to circumvent a race condition bug in Delphi 4.x and later: + // When threads are deleted and created in rapid succesion, a situation might + // arise where the thread window is deleted *after* the threads it controls + // has been created. See the Delphi Bug Lists for more information. + {$IFNDEF VER14_PLUS} // not anymore need for Delphi 6 and up // 2001.07.23 + DummyThread := TDummyThread.Create(True); + {$ENDIF} // 2001.07.23 +{$ENDIF} + +//////////////////////////////////////////////////////////////////////////////// +// +// Finalization +// +//////////////////////////////////////////////////////////////////////////////// +finalization + ExtensionList.Free; + AppExtensionList.Free; +{$IFNDEF VER9x} + {$IFDEF REGISTER_TGIFIMAGE} + TPicture.UnregisterGraphicClass(TGIFImage); + {$ENDIF} + {$IFDEF VER100} + if (pf8BitBitmap <> nil) then + pf8BitBitmap.Free; + {$ENDIF} +{$ENDIF} +{$IFDEF VER12_PLUS} + {$IFNDEF VER14_PLUS} // not anymore need for Delphi 6 and up // 2001.07.23 + if (DummyThread <> nil) then + DummyThread.Free; + {$ENDIF} // 2001.07.23 +{$ENDIF} +end. + diff --git a/exgui/Main.pas b/exgui/Main.pas index 4b7b6a8..c2b86e9 100644 --- a/exgui/Main.pas +++ b/exgui/Main.pas @@ -5,7 +5,7 @@ interface uses Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms, Dialogs, ExtCtrls, XPMan, ImgList, Menus, ComCtrls, StdCtrls, TeEngine, - Series, TeeProcs, Chart, Tabs, ToolWin; + Series, TeeProcs, Chart, Tabs, ToolWin, GIFImage; type TfmMain = class(TForm)