| 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"  |
| 23 | using namespace tSystem;  |
| 24 | namespace tImage  |
| 25 | {  |
| 26 |   |
| 27 |   |
| 28 | // This callback is a essentially the example code from gif_load.  |
| 29 | void 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 |   |
| 119 | bool 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 |   |
| 138 | bool 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 |   |
| 176 | bool 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 |   |
| 205 | bool 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 |   |
| 229 | bool 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 |   |
| 251 | bool 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 |   |
| 273 | tFrame* tImageGIF::GetFrame(bool steal)  |
| 274 | {  |
| 275 | if (!IsValid())  |
| 276 | return nullptr;  |
| 277 |   |
| 278 | return steal ? Frames.Remove() : new tFrame( *Frames.First() );  |
| 279 | }  |
| 280 |   |
| 281 |   |
| 282 | bool 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 = 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 = 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 | |