1// tImageGIF.cpp 
2// 
3// This knows how to load and save gifs. It knows the details of the gif file format and loads the data into multiple 
4// tPixel arrays, one for each frame (gifs may be animated). These arrays may be 'stolen' by tPictures. 
5// 
6// Copyright (c) 2020-2024 Tristan Grimmer. 
7// Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby 
8// granted, provided that the above copyright notice and this permission notice appear in all copies. 
9// 
10// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL 
11// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, 
12// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN 
13// AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR 
14// PERFORMANCE OF THIS SOFTWARE. 
15 
16#include <Foundation/tStandard.h> 
17#include <Foundation/tString.h> 
18#include <System/tFile.h> 
19#include <GifLoad/gif_load.h> 
20#include <gifenc/gifenc.h> 
21#include "Image/tImageGIF.h" 
22#include "Image/tPicture.h" 
23using namespace tSystem
24namespace tImage 
25
26 
27 
28// This callback is a essentially the example code from gif_load. 
29void tImageGIF::FrameLoadCallback(struct GIF_WHDR* whdr
30
31 #define RGBA(i) \ 
32 ( \ 
33 (whdr->bptr[i] == whdr->tran) ? 0x00000000 : \ 
34 ( \ 
35 uint32_t(whdr->cpal[whdr->bptr[i]].B << 16) | \ 
36 uint32_t(whdr->cpal[whdr->bptr[i]].G << 8) | \ 
37 uint32_t(whdr->cpal[whdr->bptr[i]].R << 0) | \ 
38 0xFF000000 \ 
39 ) \ 
40 ) 
41 
42 // Is first frame? 
43 if (whdr->ifrm == 0
44
45 Width = whdr->xdim
46 Height = whdr->ydim
47 FrmPict = new tPixel4b[Width * Height]; 
48 FrmPrev = new tPixel4b[Width * Height]; 
49 
50 // tPixel constructor does not initialize its members for efficiency. Must explicitely clear. 
51 tStd::tMemset(dest: FrmPict, val: 0, numBytes: Width * Height * sizeof(tPixel4b)); 
52 tStd::tMemset(dest: FrmPrev, val: 0, numBytes: Width * Height * sizeof(tPixel4b)); 
53
54 
55 tPixel4b* pict = FrmPict
56 tPixel4b* prev = nullptr
57 
58 uint32 ddst = uint32(whdr->xdim * whdr->fryo + whdr->frxo); 
59 
60 // Interlacing support. 
61 uint32 iter = whdr->intr ? 0 : 4
62 uint32 ifin = !iter ? 4 : 5
63 
64 int y = 0
65 for (uint32 dsrc = (uint32)-1; iter < ifin; iter++) 
66 for (int yoff = 16U >> ((iter > 1) ? iter : 1), y = (8 >> iter) & 7; y < whdr->fryd; y += yoff
67 for (int x = 0; x < whdr->frxd; x++) 
68 if (whdr->tran != (long)whdr->bptr[++dsrc]) 
69 pict[whdr->xdim * y + x + ddst].BP = RGBA(dsrc); 
70 
71 tFrame* frame = new tFrame
72 frame->Width = Width
73 frame->Height = Height
74 frame->Pixels = new tPixel4b[Width*Height]; 
75 frame->PixelFormatSrc = tPixelFormat::PAL8BIT
76 frame->Duration = float(whdr->time) / 100.0f
77 
78 // We store rows starting from the bottom (lower left is 0,0). 
79 for (int row = Height-1; row >= 0; row--) 
80 tStd::tMemcpy(dest: frame->Pixels + (row*Width), src: pict + ((Height-row-1)*Width), numBytes: Width*sizeof(tPixel4b)); 
81 
82 // The frame is ready. Append it. 
83 Frames.Append(item: frame); 
84 
85 if ((whdr->mode == GIF_PREV) && !FrmLast
86
87 whdr->frxd = whdr->xdim
88 whdr->fryd = whdr->ydim
89 whdr->mode = GIF_BKGD
90 ddst = 0
91
92 else 
93
94 FrmLast = (whdr->mode == GIF_PREV) ? FrmLast : (whdr->ifrm + 1); 
95 pict = (whdr->mode == GIF_PREV) ? FrmPict : FrmPrev
96 prev = (whdr->mode == GIF_PREV) ? FrmPrev : FrmPict
97 for (int x = whdr->xdim * whdr->ydim; --x
98 pict[x - 1].BP = prev[x - 1].BP); 
99
100 
101 // Cutting a hole for the next frame. 
102 if (whdr->mode == GIF_BKGD
103
104 int y = 0
105 for 
106
107 whdr->bptr[0] = ((whdr->tran >= 0) ? uint8(whdr->tran) : uint8(whdr->bkgd)), y = 0, pict = FrmPict
108 y < whdr->fryd
109 y++ 
110
111
112 for (int x = 0; x < whdr->frxd; x++) 
113 pict[whdr->xdim * y + x + ddst].BP = RGBA(0); 
114
115
116
117 
118 
119bool tImageGIF::Load(const tString& gifFile
120
121 Clear(); 
122 
123 if (tSystem::tGetFileType(file: gifFile) != tSystem::tFileType::GIF
124 return false
125 
126 if (!tFileExists(file: gifFile)) 
127 return false
128 
129 int numBytes = 0
130 uint8* gifFileInMemory = tLoadFile(file: gifFile, buffer: nullptr, fileSize: &numBytes); 
131 bool success = Load(gifFileInMemory, numBytes); 
132 delete[] gifFileInMemory
133 
134 return success
135
136 
137 
138bool tImageGIF::Load(const uint8* gifFileInMemory, int numBytes
139
140 Clear(); 
141 if ((numBytes <= 0) || !gifFileInMemory
142 return false
143 
144 // This call allocated scratchpad memory pointed to by FrmPict and FrmPrev. 
145 // They are set to null just in case GIF_Load fails to allocate. 
146 FrmPict = nullptr
147 FrmPrev = nullptr
148 int paletteSize = 0
149 int result = GIF_Load(data: (void*)gifFileInMemory, size: numBytes, gwfr: FrameLoadCallbackBridge, eamf: nullptr, anim: (void*)this, skip: 0, largestPaletteSize&: paletteSize); 
150 delete[] FrmPict
151 delete[] FrmPrev
152 if (result <= 0
153 return false
154 
155 PixelFormat = tPixelFormat::R8G8B8A8
156 PixelFormatSrc = tPixelFormat::PAL8BIT
157 switch (paletteSize
158
159 case 2: PixelFormatSrc = tPixelFormat::PAL1BIT; break
160 case 4: PixelFormatSrc = tPixelFormat::PAL2BIT; break
161 case 8: PixelFormatSrc = tPixelFormat::PAL3BIT; break
162 case 16: PixelFormatSrc = tPixelFormat::PAL4BIT; break
163 case 32: PixelFormatSrc = tPixelFormat::PAL5BIT; break
164 case 64: PixelFormatSrc = tPixelFormat::PAL6BIT; break
165 case 128: PixelFormatSrc = tPixelFormat::PAL7BIT; break
166 case 256: PixelFormatSrc = tPixelFormat::PAL8BIT; break
167
168 
169 // GIFs are nearly always ready for display on a computer screen and so are sRGB. 
170 ColourProfileSrc = tColourProfile::sRGB
171 ColourProfile = tColourProfile::sRGB
172 return true
173
174 
175 
176bool tImageGIF::Set(tList<tFrame>& srcFrames, bool stealFrames
177
178 Clear(); 
179 if (srcFrames.GetNumItems() <= 0
180 return false
181 
182 Width = srcFrames.Head()->Width
183 Height = srcFrames.Head()->Height
184 
185 PixelFormatSrc = srcFrames.Head()->PixelFormatSrc
186 PixelFormat = tPixelFormat::R8G8B8A8
187 ColourProfileSrc = tColourProfile::sRGB; // We assume srcFrames must be sRGB. 
188 ColourProfile = tColourProfile::sRGB
189 
190 if (stealFrames
191
192 while (tFrame* frame = srcFrames.Remove()) 
193 Frames.Append(item: frame); 
194
195 else 
196
197 for (tFrame* frame = srcFrames.Head(); frame; frame = frame->Next()) 
198 Frames.Append(item: new tFrame(*frame)); 
199
200 
201 return true
202
203 
204 
205bool tImageGIF::Set(tPixel4b* pixels, int width, int height, bool steal
206
207 Clear(); 
208 if (!pixels || (width <= 0) || (height <= 0)) 
209 return false
210 
211 Width = width
212 Height = height
213 tFrame* frame = new tFrame(); 
214 if (steal
215 frame->StealFrom(src: pixels, width, height); 
216 else 
217 frame->Set(srcPixels: pixels, width, height); 
218 Frames.Append(item: frame); 
219 
220 PixelFormatSrc = tPixelFormat::R8G8B8A8
221 PixelFormat = tPixelFormat::R8G8B8A8
222 ColourProfileSrc = tColourProfile::sRGB; // We assume pixels must be sRGB. 
223 ColourProfile = tColourProfile::sRGB
224 
225 return true
226
227 
228 
229bool tImageGIF::Set(tFrame* frame, bool steal
230
231 Clear(); 
232 if (!frame || !frame->IsValid()) 
233 return false
234 
235 PixelFormatSrc = frame->PixelFormatSrc
236 PixelFormat = tPixelFormat::R8G8B8A8
237 ColourProfileSrc = tColourProfile::sRGB; // We assume frame must be sRGB. 
238 ColourProfile = tColourProfile::sRGB
239 
240 Width = frame->Width
241 Height = frame->Height
242 if (steal
243 Frames.Append(item: frame); 
244 else 
245 Frames.Append(item: new tFrame(*frame)); 
246 
247 return true
248
249 
250 
251bool tImageGIF::Set(tPicture& picture, bool steal
252
253 Clear(); 
254 if (!picture.IsValid()) 
255 return false
256 
257 PixelFormatSrc = picture.PixelFormatSrc
258 PixelFormat = tPixelFormat::R8G8B8A8
259 // We don't know colour profile of tPicture. 
260 
261 // This is worth some explanation. If steal is true the picture becomes invalid and the 
262 // 'set' call will steal the stolen pixels. If steal is false GetPixels is called and the 
263 // 'set' call will memcpy them out... which makes sure the picture is still valid after and 
264 // no-one is sharing the pixel buffer. We don't check the success of 'set' because it must 
265 // succeed if picture was valid. 
266 tPixel4b* pixels = steal ? picture.StealPixels() : picture.GetPixels(); 
267 bool success = Set(pixels, width: picture.GetWidth(), height: picture.GetHeight(), steal); 
268 tAssert(success); 
269 return true
270
271 
272 
273tFrame* tImageGIF::GetFrame(bool steal
274
275 if (!IsValid()) 
276 return nullptr
277 
278 return steal ? Frames.Remove() : new tFrame( *Frames.First() ); 
279
280 
281 
282bool tImageGIF::Save(const tString& gifFile, const SaveParams& saveParams) const 
283
284 SaveParams params = saveParams
285 if (!IsValid() || !tIsPaletteFormat(format: params.Format) || (tGetFileType(file: gifFile) != tFileType::GIF)) 
286 return false
287 
288 int numFrames = GetNumFrames(); 
289  
290 // The Loop int in the params is slightly different to the loop int expected by the encoder. The encoder accepts -1 
291 // to mean no loop information is included in the gif file, while the params has Loop as 0 for infinite loops and 
292 // >0 for a specific number of times. We do it this way because it simplifies the interface and we can check the 
293 // number of frames to see if it can be set to -1. Only one frame -> set to -1. 
294 int loop = params.Loop
295 if (numFrames == 1
296 loop = -1
297  
298 // 2-colour GIFs are not allowed alpha. They would only be allowed to have 1 colour which doesn't make sense. 
299 if (params.Format == tPixelFormat::PAL1BIT
300 params.AlphaThreshold = 255
301 
302 // Before we create a gif with gifenc's ge_new_gif we need to have created a good palette for it to use. This is a 
303 // little tricky for multiframe gifs because gifenc does not support frame-local palettes. The same palette is used 
304 // for all frames so it can apply size optimization. However, quantize calls work on a single image/frame. 
305 // 
306 // Additionally, some quantize methods dither as they go... so palette indexes are created in the same step. To 
307 // solve all this we need to create an 'uber-image' with all frames in one image -- and call quantize on that to 
308 // create both the palette and the indices on one go. 
309 // 
310 // Lastly, before we do this we need to determine if we will be generating a gif with transparency. This affects the 
311 // quantization step because it has one less colour to work with. For example, an 8-bit palette would have 255 
312 // colour entries and 1 transparency entry instead of 256 colour entries. 
313 int gifBitDepth = tGetBitsPerPixel(params.Format); 
314 int gifPaletteSize = tMath::tPow2(n: gifBitDepth); 
315 
316 tPixel4b* pixels = nullptr
317 int width = 0
318 int height = 0
319 
320 if (numFrames == 1
321
322 width = Width
323 height = Height
324 pixels = Frames.First()->Pixels
325
326 else 
327
328 // Create the uber image since numFrames > 1. 
329 width = Width * numFrames
330 height = Height
331 pixels = new tPixel4b[width*height]; 
332 tStd::tMemset(dest: pixels, val: 0, numBytes: width*height*sizeof(tPixel4b)); 
333 int frameNum = 0
334 for (tFrame* frame = Frames.First(); frame; frame = frame->Next(), frameNum++) 
335
336 if ((frame->Width != Width) || (frame->Height != Height)) 
337 continue
338 for (int y = 0; y < Height; y++) 
339
340 for (int x = 0; x < Width; x++) 
341
342 int srcIndex = x + y*Width
343 int dstX = x + (frameNum*Width); 
344 int dstY = y
345 int dstIndex = dstX + dstY*width
346 pixels[dstIndex] = frame->Pixels[srcIndex]; 
347
348
349
350
351 
352 bool gifTransparency = false
353 int alphaThreshold = params.AlphaThreshold
354 
355 // An alpha threshold of -1 means auto-determine if the GIF gets 1-bit transparency. 
356 if (params.AlphaThreshold < 0
357
358 for (int p = 0; p < width*height; p++) 
359
360 if (pixels[p].A < 255
361
362 gifTransparency = true
363 alphaThreshold = 127
364 break
365
366
367
368 else if (params.AlphaThreshold < 255
369
370 gifTransparency = true
371
372 int quantNumColours = gifPaletteSize - (gifTransparency ? 1 : 0); 
373 
374 // Now that width, height, and pixels are correct we can quantize. 
375 tColour3b* gifPalette = new tColour3b[gifPaletteSize]; 
376 gifPalette[gifPaletteSize-1].Set(r: 0, g: 0, b: 0); 
377 uint8* gifIndices = new uint8[width*height]; 
378 bool checkExact = true
379 switch (params.Method
380
381 case tQuantize::Method::Fixed
382 tQuantizeFixed::QuantizeImage(numColours: quantNumColours, width, height, pixels, destPalette: gifPalette, destIndices: gifIndices, checkExact); 
383 break
384 
385 case tQuantize::Method::Neu
386 tQuantizeNeu::QuantizeImage(numColours: quantNumColours, width, height, pixels, destPalette: gifPalette, destIndices: gifIndices, checkExact, sampleFactor: params.SampleFactor); 
387 break
388 
389 case tQuantize::Method::Wu
390 tQuantizeWu::QuantizeImage(numColours: quantNumColours, width, height, pixels, destPalette: gifPalette, destIndices: gifIndices, checkExact); 
391 break
392 
393 case tQuantize::Method::Spatial
394 tQuantizeSpatial::QuantizeImage(numColours: quantNumColours, width, height, pixels, destPalette: gifPalette, destIndices: gifIndices, checkExact, ditherLevel: params.DitherLevel, filterSize: params.FilterSize); 
395 break
396
397 int bgIndex = -1
398 
399 // Now that the indices are worked out, we need to replace any indices that are supposed to be transparent with 
400 // the reserved transparent index. 
401 if (gifTransparency
402
403 bgIndex = gifPaletteSize-1
404 for (int p = 0; p < width*height; p++) 
405 if (pixels[p].A <= alphaThreshold
406 gifIndices[p] = bgIndex
407
408 
409 ge_GIF* gifHandle = ge_new_gif(fname: gifFile.Chr(), width: Width, height: Height, palette: (uint8*)gifPalette, depth: gifBitDepth, bgindex: bgIndex, loop); 
410 
411 // Call ge_add_frame for each frame. 
412 int frameNum = 0
413 for (tFrame* frame = Frames.First(); frame; frame = frame->Next(), frameNum++) 
414
415 if ((frame->Width != Width) || (frame->Height != Height)) 
416 continue
417 
418 for (int y = 0; y < Height; y++) 
419
420 for (int x = 0; x < Width; x++) 
421
422 int dstIndex = x + y*Width
423 int srcX = x + (frameNum*Width); 
424 
425 // The frames need to be given to the encoder from the top row down. 
426 int srcY = Height - y - 1
427 int srcIndex = srcX + srcY*width
428 gifHandle->frame[dstIndex] = gifIndices[srcIndex]; 
429
430
431 // There's some evidence on various websites that delays lower than 2 (2/100 second) do not 
432 // animate at the proper speed in many viewers. Currently we clamp at 2. 
433 int delay = tMath::tClampMin(val: (params.OverrideFrameDuration < 0) ? int(frame->Duration * 100.0f) : params.OverrideFrameDuration, min: 2); 
434 if (numFrames == 1
435 delay = 0
436  
437 ge_add_frame(gif: gifHandle, delay); 
438
439 
440 ge_close_gif(gif: gifHandle); 
441 delete[] gifPalette
442 delete[] gifIndices
443 if (numFrames != 1
444 delete[] pixels
445 
446 return true
447
448 
449 
450
451