Line | Count | Source |
1 | | #include <stdint.h> |
2 | | #include <stdio.h> |
3 | | #include <string.h> |
4 | | #include <stdlib.h> |
5 | | |
6 | | #include "cgif.h" |
7 | | #include "cgif_raw.h" |
8 | | |
9 | 4.76M | #define MULU16(a, b) (((uint32_t)a) * ((uint32_t)b)) // helper macro to correctly multiply two U16's without default signed int promotion |
10 | 35.0k | #define SIZE_FRAME_QUEUE (3) |
11 | | |
12 | | // CGIF_Frame type |
13 | | // note: internal sections, subject to change in future versions |
14 | | typedef struct { |
15 | | CGIF_FrameConfig config; |
16 | | uint8_t disposalMethod; |
17 | | uint8_t transIndex; |
18 | | } CGIF_Frame; |
19 | | |
20 | | // CGIF type |
21 | | // note: internal sections, subject to change in future versions |
22 | | struct st_gif { |
23 | | CGIF_Frame* aFrames[SIZE_FRAME_QUEUE]; // (internal) we need to keep the last three frames in memory. |
24 | | CGIF_Config config; // (internal) configuration parameters of the GIF |
25 | | CGIFRaw* pGIFRaw; // (internal) raw GIF stream |
26 | | FILE* pFile; |
27 | | cgif_result curResult; |
28 | | int iHEAD; // (internal) index to current HEAD frame in aFrames queue |
29 | | }; |
30 | | |
31 | | // dimension result type |
32 | | typedef struct { |
33 | | uint16_t width; |
34 | | uint16_t height; |
35 | | uint16_t top; |
36 | | uint16_t left; |
37 | | } DimResult; |
38 | | |
39 | | /* calculate next power of two exponent of given number (n MUST be <= 256) */ |
40 | 990 | static uint8_t calcNextPower2Ex(uint16_t n) { |
41 | 990 | uint8_t nextPow2; |
42 | | |
43 | 2.36k | for (nextPow2 = 0; n > (1uL << nextPow2); ++nextPow2); |
44 | 990 | return nextPow2; |
45 | 990 | } |
46 | | |
47 | | /* write callback. returns 0 on success or -1 on error. */ |
48 | 46.5k | static int writecb(void* pContext, const uint8_t* pData, const size_t numBytes) { |
49 | 46.5k | CGIF* pGIF; |
50 | 46.5k | size_t r; |
51 | | |
52 | 46.5k | pGIF = (CGIF*)pContext; |
53 | 46.5k | if(pGIF->pFile) { |
54 | 0 | r = fwrite(pData, 1, numBytes, pGIF->pFile); |
55 | 0 | if(r == numBytes) return 0; |
56 | 0 | else return -1; |
57 | 46.5k | } else if(pGIF->config.pWriteFn) { |
58 | 46.5k | return pGIF->config.pWriteFn(pGIF->config.pContext, pData, numBytes); |
59 | 46.5k | } |
60 | 0 | return 0; |
61 | 46.5k | } |
62 | | |
63 | | /* free space allocated for CGIF struct */ |
64 | 2.09k | static void freeCGIF(CGIF* pGIF) { |
65 | 2.09k | if((pGIF->config.attrFlags & CGIF_ATTR_NO_GLOBAL_TABLE) == 0) { |
66 | 1.97k | free(pGIF->config.pGlobalPalette); |
67 | 1.97k | } |
68 | 2.09k | free(pGIF); |
69 | 2.09k | } |
70 | | |
71 | | /* create a new GIF */ |
72 | 2.09k | CGIF* cgif_newgif(CGIF_Config* pConfig) { |
73 | 2.09k | FILE* pFile; |
74 | 2.09k | CGIF* pGIF; |
75 | 2.09k | CGIFRaw* pGIFRaw; // raw GIF stream |
76 | 2.09k | CGIFRaw_Config rawConfig = {0}; |
77 | | // width or heigth cannot be zero |
78 | 2.09k | if(!pConfig->width || !pConfig->height) { |
79 | 7 | return NULL; |
80 | 7 | } |
81 | 2.09k | pFile = NULL; |
82 | | // open output file (if necessary) |
83 | 2.09k | if(pConfig->path) { |
84 | 0 | pFile = fopen(pConfig->path, "wb"); |
85 | 0 | if(pFile == NULL) { |
86 | 0 | return NULL; // error: fopen failed |
87 | 0 | } |
88 | 0 | } |
89 | | // allocate space for CGIF context |
90 | 2.09k | pGIF = malloc(sizeof(CGIF)); |
91 | 2.09k | if(pGIF == NULL) { |
92 | 0 | if(pFile) { |
93 | 0 | fclose(pFile); |
94 | 0 | } |
95 | 0 | return NULL; // error -> malloc failed |
96 | 0 | } |
97 | | |
98 | 2.09k | memset(pGIF, 0, sizeof(CGIF)); |
99 | 2.09k | pGIF->pFile = pFile; |
100 | 2.09k | pGIF->iHEAD = 1; |
101 | 2.09k | memcpy(&(pGIF->config), pConfig, sizeof(CGIF_Config)); |
102 | | // make a deep copy of global color tabele (GCT), if required. |
103 | 2.09k | if((pConfig->attrFlags & CGIF_ATTR_NO_GLOBAL_TABLE) == 0) { |
104 | 1.97k | pGIF->config.pGlobalPalette = malloc(pConfig->numGlobalPaletteEntries * 3); |
105 | 1.97k | if(pGIF->config.pGlobalPalette == NULL) { |
106 | 0 | if(pFile) { |
107 | 0 | fclose(pFile); |
108 | 0 | } |
109 | 0 | free(pGIF); |
110 | 0 | return NULL; |
111 | 0 | } |
112 | 1.97k | memcpy(pGIF->config.pGlobalPalette, pConfig->pGlobalPalette, pConfig->numGlobalPaletteEntries * 3); |
113 | 1.97k | } |
114 | | |
115 | 2.09k | rawConfig.pGCT = pConfig->pGlobalPalette; |
116 | 2.09k | rawConfig.sizeGCT = (pConfig->attrFlags & CGIF_ATTR_NO_GLOBAL_TABLE) ? 0 : pConfig->numGlobalPaletteEntries; |
117 | | // translate CGIF_ATTR_* to CGIF_RAW_ATTR_* flags |
118 | 2.09k | rawConfig.attrFlags = (pConfig->attrFlags & CGIF_ATTR_IS_ANIMATED) ? CGIF_RAW_ATTR_IS_ANIMATED : 0; |
119 | 2.09k | rawConfig.attrFlags |= (pConfig->attrFlags & CGIF_ATTR_NO_LOOP) ? CGIF_RAW_ATTR_NO_LOOP : 0; |
120 | 2.09k | rawConfig.width = pConfig->width; |
121 | 2.09k | rawConfig.height = pConfig->height; |
122 | 2.09k | rawConfig.numLoops = pConfig->numLoops; |
123 | 2.09k | rawConfig.pWriteFn = writecb; |
124 | 2.09k | rawConfig.pContext = (void*)pGIF; |
125 | | // pass config down and create a new raw GIF stream. |
126 | 2.09k | pGIFRaw = cgif_raw_newgif(&rawConfig); |
127 | | // check for errors |
128 | 2.09k | if(pGIFRaw == NULL) { |
129 | 13 | if(pFile) { |
130 | 0 | fclose(pFile); |
131 | 0 | } |
132 | 13 | freeCGIF(pGIF); |
133 | 13 | return NULL; |
134 | 13 | } |
135 | | |
136 | 2.07k | pGIF->pGIFRaw = pGIFRaw; |
137 | | // assume error per default. |
138 | | // set to CGIF_OK by the first successful cgif_addframe() call, as a GIF without frames is invalid. |
139 | 2.07k | pGIF->curResult = CGIF_PENDING; |
140 | 2.07k | return pGIF; |
141 | 2.09k | } |
142 | | |
143 | | /* compare given pixel indices using the correct local or global color table; returns 0 if the two pixels are RGB equal */ |
144 | 1.19M | static int cmpPixel(const CGIF* pGIF, const CGIF_FrameConfig* pCur, const CGIF_FrameConfig* pBef, const uint8_t iCur, const uint8_t iBef) { |
145 | 1.19M | uint8_t* pBefCT; // color table to use for pBef |
146 | 1.19M | uint8_t* pCurCT; // color table to use for pCur |
147 | | |
148 | 1.19M | if((pCur->attrFlags & CGIF_FRAME_ATTR_HAS_SET_TRANS) && iCur == pCur->transIndex) { |
149 | 44.6k | return 0; // identical |
150 | 44.6k | } |
151 | 1.15M | if((pBef->attrFlags & CGIF_FRAME_ATTR_HAS_SET_TRANS) && iBef == pBef->transIndex) { |
152 | 5.33k | return 1; // done: cannot compare |
153 | 5.33k | } |
154 | | // safety bounds check |
155 | 1.14M | const uint16_t sizeCTBef = (pBef->attrFlags & CGIF_FRAME_ATTR_USE_LOCAL_TABLE) ? pBef->numLocalPaletteEntries : pGIF->config.numGlobalPaletteEntries; |
156 | 1.14M | const uint16_t sizeCTCur = (pCur->attrFlags & CGIF_FRAME_ATTR_USE_LOCAL_TABLE) ? pCur->numLocalPaletteEntries : pGIF->config.numGlobalPaletteEntries; |
157 | 1.14M | if((iBef >= sizeCTBef) || (iCur >= sizeCTCur)) { |
158 | 806k | return 1; // error: out-of-bounds - cannot compare |
159 | 806k | } |
160 | 338k | pBefCT = (pBef->attrFlags & CGIF_FRAME_ATTR_USE_LOCAL_TABLE) ? pBef->pLocalPalette : pGIF->config.pGlobalPalette; // local or global table used? |
161 | 338k | pCurCT = (pCur->attrFlags & CGIF_FRAME_ATTR_USE_LOCAL_TABLE) ? pCur->pLocalPalette : pGIF->config.pGlobalPalette; // local or global table used? |
162 | 338k | return memcmp(pBefCT + iBef * 3, pCurCT + iCur * 3, 3); |
163 | 1.14M | } |
164 | | |
165 | | // compare given frames; returns 0 if frames are equal and 1 if they differ. If they differ, pResult returns area of difference |
166 | 1.19k | static int getDiffArea(CGIF* pGIF, CGIF_FrameConfig* pCur, CGIF_FrameConfig* pBef, DimResult *pResult) { |
167 | 1.19k | const uint8_t* pCurImageData; |
168 | 1.19k | const uint8_t* pBefImageData; |
169 | 1.19k | uint16_t i, x; |
170 | 1.19k | uint16_t newHeight, newWidth, newLeft, newTop; |
171 | 1.19k | const uint16_t width = pGIF->config.width; |
172 | 1.19k | const uint16_t height = pGIF->config.height; |
173 | 1.19k | uint8_t iCur, iBef; |
174 | | |
175 | 1.19k | pCurImageData = pCur->pImageData; |
176 | 1.19k | pBefImageData = pBef->pImageData; |
177 | | // find top |
178 | 1.19k | i = 0; |
179 | 59.7k | while(i < height) { |
180 | 119k | for(int c = 0; c < width; ++c) { |
181 | 61.1k | iCur = *(pCurImageData + MULU16(i, width) + c); |
182 | 61.1k | iBef = *(pBefImageData + MULU16(i, width) + c); |
183 | 61.1k | if(cmpPixel(pGIF, pCur, pBef, iCur, iBef) != 0) { |
184 | 1.06k | goto FoundTop; |
185 | 1.06k | } |
186 | 61.1k | } |
187 | 58.5k | ++i; |
188 | 58.5k | } |
189 | 1.19k | FoundTop: |
190 | 1.19k | if(i == height) { |
191 | 132 | return 0; |
192 | 132 | } |
193 | 1.06k | newTop = i; |
194 | | |
195 | | // find actual height |
196 | 1.06k | i = height - 1; |
197 | 14.5k | while(i > newTop) { |
198 | 28.0k | for(int c = 0; c < width; ++c) { |
199 | 14.5k | iCur = *(pCurImageData + MULU16(i, width) + c); |
200 | 14.5k | iBef = *(pBefImageData + MULU16(i, width) + c); |
201 | 14.5k | if(cmpPixel(pGIF, pCur, pBef, iCur, iBef) != 0) { |
202 | 401 | goto FoundHeight; |
203 | 401 | } |
204 | 14.5k | } |
205 | 13.5k | --i; |
206 | 13.5k | } |
207 | 1.06k | FoundHeight: |
208 | 1.06k | newHeight = (i + 1) - newTop; |
209 | | |
210 | | // find left |
211 | 1.06k | i = newTop; |
212 | 1.06k | x = 0; |
213 | 2.61k | while(cmpPixel(pGIF, pCur, pBef, pCurImageData[MULU16(i, width) + x], pBefImageData[MULU16(i, width) + x]) == 0) { |
214 | 1.55k | ++i; |
215 | 1.55k | if(i > (newTop + newHeight - 1)) { |
216 | 1.33k | ++x; //(x==width cannot happen as return 0 is trigged in the only possible case before) |
217 | 1.33k | i = newTop; |
218 | 1.33k | } |
219 | 1.55k | } |
220 | 1.06k | newLeft = x; |
221 | | |
222 | | // find actual width |
223 | 1.06k | i = newTop; |
224 | 1.06k | x = width - 1; |
225 | 2.50k | while(cmpPixel(pGIF, pCur, pBef, pCurImageData[MULU16(i, width) + x], pBefImageData[MULU16(i, width) + x]) == 0) { |
226 | 1.44k | ++i; |
227 | 1.44k | if(i > (newTop + newHeight - 1)) { |
228 | 1.23k | --x; //(x<newLeft cannot happen as return 0 is trigged in the only possible case before) |
229 | 1.23k | i = newTop; |
230 | 1.23k | } |
231 | 1.44k | } |
232 | 1.06k | newWidth = (x + 1) - newLeft; |
233 | | |
234 | 1.06k | pResult->width = newWidth; |
235 | 1.06k | pResult->height = newHeight; |
236 | 1.06k | pResult->top = newTop; |
237 | 1.06k | pResult->left = newLeft; |
238 | 1.06k | return 1; |
239 | 1.06k | } |
240 | | |
241 | | // compare given global palette frames; returns 0 if frames are equal and 1 if they differ. If they differ, pResult returns area of difference |
242 | 573 | static int getDiffAreaGlobalPalette(CGIF* pGIF, CGIF_FrameConfig* pCur, CGIF_FrameConfig* pBef, DimResult *pResult) { |
243 | 573 | const uint8_t* pCurImageData; |
244 | 573 | const uint8_t* pBefImageData; |
245 | 573 | uint32_t offset; |
246 | 573 | uint16_t i, x; |
247 | 573 | uint16_t newHeight, newWidth, newLeft, newTop; |
248 | 573 | const uint16_t width = pGIF->config.width; |
249 | 573 | const uint16_t height = pGIF->config.height; |
250 | | |
251 | 573 | pCurImageData = pCur->pImageData; |
252 | 573 | pBefImageData = pBef->pImageData; |
253 | | // find top |
254 | 573 | i = 0; |
255 | 573 | offset = 0; |
256 | 15.0k | while(i < height) { |
257 | 14.9k | if (memcmp(pCurImageData + offset, pBefImageData + offset, width)) { |
258 | 518 | break; |
259 | 518 | } |
260 | 14.4k | ++i; |
261 | 14.4k | offset += width; |
262 | 14.4k | } |
263 | | |
264 | 573 | if(i == height) { |
265 | 55 | return 0; |
266 | 55 | } |
267 | 518 | newTop = i; |
268 | | |
269 | | // find actual height |
270 | 518 | i = height - 1; |
271 | 518 | offset = MULU16(i, width); |
272 | 833 | while(i > newTop) { |
273 | 522 | if (memcmp(pCurImageData + offset, pBefImageData + offset, width)) { |
274 | 207 | break; |
275 | 207 | } |
276 | 315 | --i; |
277 | 315 | offset -= width; |
278 | 315 | } |
279 | 518 | newHeight = (i + 1) - newTop; |
280 | | |
281 | | // find left |
282 | 518 | i = newTop; |
283 | 518 | x = 0; |
284 | 518 | offset = MULU16(i, width); |
285 | 21.7k | while(pCurImageData[offset + x] == pBefImageData[offset + x]) { |
286 | 21.1k | ++i; |
287 | 21.1k | offset += width; |
288 | 21.1k | if(i > (newTop + newHeight - 1)) { |
289 | 1.05k | ++x; //(x==width cannot happen as return 0 is triggered in the only possible case before) |
290 | 1.05k | i = newTop; |
291 | 1.05k | offset = MULU16(i, width); |
292 | 1.05k | } |
293 | 21.1k | } |
294 | 518 | newLeft = x; |
295 | | |
296 | | // find actual width |
297 | 518 | i = newTop; |
298 | 518 | x = width - 1; |
299 | 518 | offset = MULU16(i, width); |
300 | 17.8k | while(pCurImageData[offset + x] == pBefImageData[offset + x]) { |
301 | 17.3k | ++i; |
302 | 17.3k | offset += width; |
303 | 17.3k | if(i > (newTop + newHeight - 1)) { |
304 | 332 | --x; //(x<newLeft cannot happen as return 0 is triggered in the only possible case before) |
305 | 332 | i = newTop; |
306 | 332 | offset = MULU16(i, width); |
307 | 332 | } |
308 | 17.3k | } |
309 | 518 | newWidth = (x + 1) - newLeft; |
310 | | |
311 | 518 | pResult->width = newWidth; |
312 | 518 | pResult->height = newHeight; |
313 | 518 | pResult->top = newTop; |
314 | 518 | pResult->left = newLeft; |
315 | 518 | return 1; |
316 | 573 | } |
317 | | |
318 | | /* optimize GIF file size by only redrawing the rectangular area that differs from previous frame */ |
319 | 1.76k | static uint8_t* doWidthHeightOptim(CGIF* pGIF, CGIF_FrameConfig* pCur, CGIF_FrameConfig* pBef, DimResult* pResult) { |
320 | 1.76k | uint16_t i; |
321 | 1.76k | uint8_t* pNewImageData; |
322 | 1.76k | const uint16_t width = pGIF->config.width; |
323 | 1.76k | const uint8_t* pCurImageData = pCur->pImageData; |
324 | 1.76k | int diffFrame; |
325 | | |
326 | 1.76k | if ((pBef->attrFlags & CGIF_FRAME_ATTR_USE_LOCAL_TABLE) == 0 && (pCur->attrFlags & CGIF_FRAME_ATTR_USE_LOCAL_TABLE) == 0 |
327 | 1.16k | && (pBef->attrFlags & CGIF_FRAME_ATTR_HAS_SET_TRANS) == 0 && (pCur->attrFlags & CGIF_FRAME_ATTR_HAS_SET_TRANS) == 0) { |
328 | | // Both frames use global palette; use fast comparison. |
329 | 573 | diffFrame = getDiffAreaGlobalPalette(pGIF, pCur, pBef, pResult); |
330 | 1.19k | } else { |
331 | 1.19k | diffFrame = getDiffArea(pGIF, pCur, pBef, pResult); |
332 | 1.19k | } |
333 | | |
334 | 1.76k | if (diffFrame == 0) { // need dummy pixel (frame is identical with one before) |
335 | | // TBD we might make it possible to merge identical frames in the future |
336 | 187 | pResult->width = 1; |
337 | 187 | pResult->height = 1; |
338 | 187 | pResult->left = 0; |
339 | 187 | pResult->top = 0; |
340 | 187 | } |
341 | | |
342 | | // create new image data |
343 | 1.76k | pNewImageData = malloc(MULU16(pResult->width, pResult->height)); |
344 | 1.76k | if(pNewImageData == NULL) { |
345 | 0 | return NULL; // allocation failed |
346 | 0 | } |
347 | 1.19M | for (i = 0; i < pResult->height; ++i) { |
348 | 1.18M | memcpy(pNewImageData + MULU16(i, pResult->width), pCurImageData + MULU16((i + pResult->top), width) + pResult->left, pResult->width); |
349 | 1.18M | } |
350 | | |
351 | 1.76k | return pNewImageData; |
352 | 1.76k | } |
353 | | |
354 | | /* move frame down to the raw GIF API */ |
355 | 4.68k | static cgif_result flushFrame(CGIF* pGIF, CGIF_Frame* pCur, CGIF_Frame* pBef) { |
356 | 4.68k | CGIFRaw_FrameConfig rawConfig; |
357 | 4.68k | DimResult dimResult; |
358 | 4.68k | uint8_t* pTmpImageData; |
359 | 4.68k | uint8_t* pBefImageData; |
360 | 4.68k | int isFirstFrame, useLCT, hasAlpha, hasSetTransp; |
361 | 4.68k | uint16_t numPaletteEntries; |
362 | 4.68k | uint16_t imageWidth, imageHeight, width, height, top, left; |
363 | 4.68k | uint8_t transIndex, disposalMethod; |
364 | 4.68k | cgif_result r; |
365 | | |
366 | 4.68k | imageWidth = pGIF->config.width; |
367 | 4.68k | imageHeight = pGIF->config.height; |
368 | 4.68k | isFirstFrame = (pBef == NULL) ? 1 : 0; |
369 | 4.68k | useLCT = (pCur->config.attrFlags & CGIF_FRAME_ATTR_USE_LOCAL_TABLE) ? 1 : 0; // LCT stands for "local color table" |
370 | 4.68k | hasAlpha = ((pGIF->config.attrFlags & CGIF_ATTR_HAS_TRANSPARENCY) || (pCur->config.attrFlags & CGIF_FRAME_ATTR_HAS_ALPHA)) ? 1 : 0; |
371 | 4.68k | hasSetTransp = (pCur->config.attrFlags & CGIF_FRAME_ATTR_HAS_SET_TRANS) ? 1 : 0; |
372 | 4.68k | disposalMethod = pCur->disposalMethod; |
373 | 4.68k | transIndex = pCur->transIndex; |
374 | | // deactivate impossible size optimizations |
375 | | // => in case alpha channel is used |
376 | | // CGIF_FRAME_GEN_USE_TRANSPARENCY and CGIF_FRAME_GEN_USE_DIFF_WINDOW are not possible |
377 | 4.68k | if(isFirstFrame || hasAlpha) { |
378 | 2.23k | pCur->config.genFlags &= ~(CGIF_FRAME_GEN_USE_TRANSPARENCY | CGIF_FRAME_GEN_USE_DIFF_WINDOW); |
379 | 2.23k | } |
380 | | // transparency setting (which areas are identical to the frame before) provided by user: |
381 | | // CGIF_FRAME_GEN_USE_TRANSPARENCY not possible |
382 | 4.68k | if(hasSetTransp) { |
383 | 1.22k | pCur->config.genFlags &= ~(CGIF_FRAME_GEN_USE_TRANSPARENCY); |
384 | 1.22k | } |
385 | 4.68k | numPaletteEntries = (useLCT) ? pCur->config.numLocalPaletteEntries : pGIF->config.numGlobalPaletteEntries; |
386 | | // switch off transparency optimization if color table is full (no free spot for the transparent index), TBD: count used colors, adapt table |
387 | 4.68k | if(numPaletteEntries == 256) { |
388 | 179 | pCur->config.genFlags &= ~CGIF_FRAME_GEN_USE_TRANSPARENCY; |
389 | 179 | } |
390 | | |
391 | | // purge overlap of current frame and frame before (width - height optim), if required (CGIF_FRAME_GEN_USE_DIFF_WINDOW set) |
392 | 4.68k | if(pCur->config.genFlags & CGIF_FRAME_GEN_USE_DIFF_WINDOW) { |
393 | 1.76k | pTmpImageData = doWidthHeightOptim(pGIF, &pCur->config, &pBef->config, &dimResult); |
394 | 1.76k | if(pTmpImageData == NULL) { |
395 | 0 | return CGIF_EALLOC; // allocation failed in doWidthHeightOptim |
396 | 0 | } |
397 | 1.76k | width = dimResult.width; |
398 | 1.76k | height = dimResult.height; |
399 | 1.76k | top = dimResult.top; |
400 | 1.76k | left = dimResult.left; |
401 | 2.91k | } else { |
402 | 2.91k | pTmpImageData = NULL; |
403 | 2.91k | width = imageWidth; |
404 | 2.91k | height = imageHeight; |
405 | 2.91k | top = 0; |
406 | 2.91k | left = 0; |
407 | 2.91k | } |
408 | | |
409 | | // mark matching areas of the previous frame as transparent, if required (CGIF_FRAME_GEN_USE_TRANSPARENCY set) |
410 | 4.68k | if(pCur->config.genFlags & CGIF_FRAME_GEN_USE_TRANSPARENCY) { |
411 | | // set transIndex to next free index |
412 | 990 | int pow2 = calcNextPower2Ex(numPaletteEntries); |
413 | 990 | pow2 = (pow2 < 2) ? 2 : pow2; // TBD keep transparency index behavior as in V0.1.0 (for now) |
414 | 990 | transIndex = (1 << pow2) - 1; |
415 | 990 | if(transIndex < numPaletteEntries) { |
416 | 110 | transIndex = (1 << (pow2 + 1)) - 1; |
417 | 110 | } |
418 | 990 | if(pTmpImageData == NULL) { |
419 | 214 | pTmpImageData = malloc(MULU16(imageWidth, imageHeight)); |
420 | 214 | if(pTmpImageData == NULL) { |
421 | 0 | return CGIF_EALLOC; // allocation failed |
422 | 0 | } |
423 | 214 | memcpy(pTmpImageData, pCur->config.pImageData, MULU16(imageWidth, imageHeight)); |
424 | 214 | } |
425 | 990 | pBefImageData = pBef->config.pImageData; |
426 | 538k | for(int i = 0; i < height; ++i) { |
427 | 1.59M | for(int x = 0; x < width; ++x) { |
428 | 1.05M | if(cmpPixel(pGIF, &pCur->config, &pBef->config, pTmpImageData[MULU16(i, width) + x], pBefImageData[MULU16(top + i, imageWidth) + (left + x)]) == 0) { |
429 | 38.8k | pTmpImageData[MULU16(i, width) + x] = transIndex; |
430 | 38.8k | } |
431 | 1.05M | } |
432 | 537k | } |
433 | 990 | } |
434 | | |
435 | | // move frame down to GIF raw API |
436 | 4.68k | rawConfig.pLCT = pCur->config.pLocalPalette; |
437 | 4.68k | rawConfig.pImageData = (pTmpImageData) ? pTmpImageData : pCur->config.pImageData; |
438 | 4.68k | rawConfig.attrFlags = 0; |
439 | 4.68k | if(hasAlpha || (pCur->config.genFlags & CGIF_FRAME_GEN_USE_TRANSPARENCY) || hasSetTransp) { |
440 | 3.03k | rawConfig.attrFlags |= CGIF_RAW_FRAME_ATTR_HAS_TRANS; |
441 | 3.03k | } |
442 | 4.68k | rawConfig.attrFlags |= (pCur->config.attrFlags & CGIF_FRAME_ATTR_INTERLACED) ? CGIF_RAW_FRAME_ATTR_INTERLACED : 0; |
443 | 4.68k | rawConfig.width = width; |
444 | 4.68k | rawConfig.height = height; |
445 | 4.68k | rawConfig.top = top; |
446 | 4.68k | rawConfig.left = left; |
447 | 4.68k | rawConfig.delay = pCur->config.delay; |
448 | 4.68k | rawConfig.sizeLCT = (useLCT) ? pCur->config.numLocalPaletteEntries : 0; |
449 | 4.68k | rawConfig.disposalMethod = disposalMethod; |
450 | 4.68k | rawConfig.transIndex = transIndex; |
451 | 4.68k | r = cgif_raw_addframe(pGIF->pGIFRaw, &rawConfig); |
452 | 4.68k | free(pTmpImageData); |
453 | 4.68k | return r; |
454 | 4.68k | } |
455 | | |
456 | 7.80k | static void freeFrame(CGIF_Frame* pFrame) { |
457 | 7.80k | if(pFrame) { |
458 | 4.86k | free(pFrame->config.pImageData); |
459 | 4.86k | if(pFrame->config.attrFlags & CGIF_FRAME_ATTR_USE_LOCAL_TABLE) { |
460 | 1.00k | free(pFrame->config.pLocalPalette); |
461 | 1.00k | } |
462 | 4.86k | free(pFrame); |
463 | 4.86k | } |
464 | 7.80k | } |
465 | | |
466 | 4.86k | static void copyFrameConfig(CGIF_FrameConfig* pDest, CGIF_FrameConfig* pSrc) { |
467 | 4.86k | pDest->pLocalPalette = pSrc->pLocalPalette; // might need a deep copy |
468 | 4.86k | pDest->pImageData = pSrc->pImageData; // might need a deep copy |
469 | 4.86k | pDest->attrFlags = pSrc->attrFlags; |
470 | 4.86k | pDest->genFlags = pSrc->genFlags; |
471 | 4.86k | pDest->delay = pSrc->delay; |
472 | 4.86k | pDest->numLocalPaletteEntries = pSrc->numLocalPaletteEntries; |
473 | | // copy transIndex if necessary (field added with V0.2.0; avoid binary incompatibility) |
474 | 4.86k | if(pSrc->attrFlags & (CGIF_FRAME_ATTR_HAS_ALPHA | CGIF_FRAME_ATTR_HAS_SET_TRANS)) { |
475 | 1.94k | pDest->transIndex = pSrc->transIndex; |
476 | 1.94k | } |
477 | 4.86k | } |
478 | | |
479 | | /* queue a new GIF frame */ |
480 | 5.50k | int cgif_addframe(CGIF* pGIF, CGIF_FrameConfig* pConfig) { |
481 | 5.50k | CGIF_Frame* pNewFrame; |
482 | 5.50k | int hasAlpha, hasSetTransp; |
483 | 5.50k | uint32_t i; |
484 | 5.50k | cgif_result r; |
485 | | |
486 | | // check for previous errors |
487 | 5.50k | if(pGIF->curResult != CGIF_OK && pGIF->curResult != CGIF_PENDING) { |
488 | 343 | return pGIF->curResult; |
489 | 343 | } |
490 | 5.16k | hasAlpha = ((pGIF->config.attrFlags & CGIF_ATTR_HAS_TRANSPARENCY) || (pConfig->attrFlags & CGIF_FRAME_ATTR_HAS_ALPHA)) ? 1 : 0; // alpha channel is present |
491 | 5.16k | hasSetTransp = (pConfig->attrFlags & CGIF_FRAME_ATTR_HAS_SET_TRANS) ? 1 : 0; // user provided transparency setting (identical areas marked by user) |
492 | | // check for invalid configs: |
493 | | // cannot set alpha channel and user-provided transparency at the same time. |
494 | 5.16k | if(hasAlpha && hasSetTransp) { |
495 | 26 | pGIF->curResult = CGIF_ERROR; |
496 | 26 | return pGIF->curResult; |
497 | 26 | } |
498 | | // cannot set global and local alpha channel at the same time |
499 | 5.13k | if((pGIF->config.attrFlags & CGIF_ATTR_HAS_TRANSPARENCY) && (pConfig->attrFlags & CGIF_FRAME_ATTR_HAS_ALPHA)) { |
500 | 3 | pGIF->curResult = CGIF_ERROR; |
501 | 3 | return pGIF->curResult; |
502 | 3 | } |
503 | | // sanity check: |
504 | | // at least one valid CT needed (global or local) |
505 | 5.13k | if(!(pConfig->attrFlags & CGIF_FRAME_ATTR_USE_LOCAL_TABLE) && (pGIF->config.attrFlags & CGIF_ATTR_NO_GLOBAL_TABLE)) { |
506 | 7 | pGIF->curResult = CGIF_ERROR; |
507 | 7 | return CGIF_ERROR; // invalid config |
508 | 7 | } |
509 | | |
510 | | // if frame matches previous frame, drop it completely and sum the frame delay |
511 | 5.12k | if(pGIF->aFrames[pGIF->iHEAD] != NULL) { |
512 | 3.20k | const uint32_t frameDelay = pConfig->delay + pGIF->aFrames[pGIF->iHEAD]->config.delay; |
513 | 3.20k | if(frameDelay <= 0xFFFF && !(pGIF->config.genFlags & CGIF_GEN_KEEP_IDENT_FRAMES)) { |
514 | 1.83k | int sameFrame = 1; |
515 | 1.83k | if ((pConfig->attrFlags & CGIF_FRAME_ATTR_USE_LOCAL_TABLE) == 0 && (pGIF->aFrames[pGIF->iHEAD]->config.attrFlags & CGIF_FRAME_ATTR_USE_LOCAL_TABLE) == 0 |
516 | 1.18k | && (pConfig->attrFlags & CGIF_FRAME_ATTR_HAS_SET_TRANS) == 0 && (pGIF->aFrames[pGIF->iHEAD]->config.attrFlags & CGIF_FRAME_ATTR_HAS_SET_TRANS) == 0) { |
517 | 683 | if (memcmp(pConfig->pImageData, pGIF->aFrames[pGIF->iHEAD]->config.pImageData, MULU16(pGIF->config.width, pGIF->config.height))) { |
518 | 605 | sameFrame = 0; |
519 | 605 | } |
520 | 1.15k | } else { |
521 | 61.6k | for(i = 0; i < MULU16(pGIF->config.width, pGIF->config.height); i++) { |
522 | 61.5k | if(cmpPixel(pGIF, pConfig, &pGIF->aFrames[pGIF->iHEAD]->config, pConfig->pImageData[i], pGIF->aFrames[pGIF->iHEAD]->config.pImageData[i])) { |
523 | 1.02k | sameFrame = 0; |
524 | 1.02k | break; |
525 | 1.02k | } |
526 | 61.5k | } |
527 | 1.15k | } |
528 | | |
529 | 1.83k | if (sameFrame) { |
530 | 211 | pGIF->aFrames[pGIF->iHEAD]->config.delay = frameDelay; |
531 | 211 | return CGIF_OK; |
532 | 211 | } |
533 | 1.83k | } |
534 | 3.20k | } |
535 | | |
536 | | // search for free slot in frame queue |
537 | 7.91k | for(i = pGIF->iHEAD; i < SIZE_FRAME_QUEUE && pGIF->aFrames[i] != NULL; ++i); |
538 | | // check whether the queue is full |
539 | | // when queue is full: we need to flush one frame. |
540 | 4.91k | if(i == SIZE_FRAME_QUEUE) { |
541 | 1.56k | r = flushFrame(pGIF, pGIF->aFrames[1], pGIF->aFrames[0]); |
542 | 1.56k | freeFrame(pGIF->aFrames[0]); |
543 | 1.56k | pGIF->aFrames[0] = NULL; // avoid potential double free in cgif_close |
544 | | // check for errors |
545 | 1.56k | if(r != CGIF_OK) { |
546 | 53 | pGIF->curResult = r; |
547 | 53 | return pGIF->curResult; |
548 | 53 | } |
549 | 1.51k | i = SIZE_FRAME_QUEUE - 1; |
550 | | // keep the flushed frame in memory, as we might need it to write the next one. |
551 | 1.51k | pGIF->aFrames[0] = pGIF->aFrames[1]; |
552 | 1.51k | pGIF->aFrames[1] = pGIF->aFrames[2]; |
553 | 1.51k | pGIF->aFrames[2] = NULL; |
554 | 1.51k | } |
555 | | // create new Frame struct + make a deep copy of pConfig. |
556 | 4.86k | pNewFrame = malloc(sizeof(CGIF_Frame)); |
557 | 4.86k | if(pNewFrame == NULL) { |
558 | 0 | pGIF->curResult = CGIF_EALLOC; |
559 | 0 | return pGIF->curResult; |
560 | 0 | } |
561 | 4.86k | copyFrameConfig(&(pNewFrame->config), pConfig); |
562 | 4.86k | pNewFrame->config.pImageData = malloc(MULU16(pGIF->config.width, pGIF->config.height)); |
563 | 4.86k | if(pNewFrame->config.pImageData == NULL) { |
564 | 0 | free(pNewFrame); |
565 | 0 | pGIF->curResult = CGIF_EALLOC; |
566 | 0 | return pGIF->curResult; |
567 | 0 | } |
568 | 4.86k | memcpy(pNewFrame->config.pImageData, pConfig->pImageData, MULU16(pGIF->config.width, pGIF->config.height)); |
569 | | // make a deep copy of the local color table, if required. |
570 | 4.86k | if(pConfig->attrFlags & CGIF_FRAME_ATTR_USE_LOCAL_TABLE) { |
571 | 1.00k | pNewFrame->config.pLocalPalette = malloc(pConfig->numLocalPaletteEntries * 3); |
572 | 1.00k | if(pNewFrame->config.pLocalPalette == NULL) { |
573 | 0 | free(pNewFrame->config.pImageData); |
574 | 0 | free(pNewFrame); |
575 | 0 | pGIF->curResult = CGIF_EALLOC; |
576 | 0 | return pGIF->curResult; |
577 | 0 | } |
578 | 1.00k | memcpy(pNewFrame->config.pLocalPalette, pConfig->pLocalPalette, pConfig->numLocalPaletteEntries * 3); |
579 | 1.00k | } |
580 | 4.86k | pNewFrame->disposalMethod = DISPOSAL_METHOD_LEAVE; |
581 | 4.86k | pNewFrame->transIndex = 0; |
582 | 4.86k | pGIF->aFrames[i] = pNewFrame; // add frame to queue |
583 | 4.86k | pGIF->iHEAD = i; // update HEAD index |
584 | | // check whether we need to adapt the disposal method of the frame before. |
585 | 4.86k | if(pGIF->config.attrFlags & CGIF_ATTR_HAS_TRANSPARENCY) { |
586 | 163 | pGIF->aFrames[i]->disposalMethod = DISPOSAL_METHOD_BACKGROUND; // TBD might be removed |
587 | 163 | pGIF->aFrames[i]->transIndex = 0; |
588 | 163 | if(pGIF->aFrames[i - 1] != NULL) { |
589 | 112 | pGIF->aFrames[i - 1]->config.genFlags &= ~(CGIF_FRAME_GEN_USE_TRANSPARENCY | CGIF_FRAME_GEN_USE_DIFF_WINDOW); |
590 | 112 | pGIF->aFrames[i - 1]->disposalMethod = DISPOSAL_METHOD_BACKGROUND; // restore to background color |
591 | 112 | } |
592 | 163 | } |
593 | | // set per-frame alpha channel (we need to adapt the disposal method of the frame before) |
594 | 4.86k | if(pConfig->attrFlags & CGIF_FRAME_ATTR_HAS_ALPHA) { |
595 | 697 | pGIF->aFrames[i]->transIndex = pConfig->transIndex; |
596 | 697 | if(pGIF->aFrames[i - 1] != NULL) { |
597 | 252 | pGIF->aFrames[i - 1]->config.genFlags &= ~(CGIF_FRAME_GEN_USE_DIFF_WINDOW); // width/height optim not possible for frame before |
598 | 252 | pGIF->aFrames[i - 1]->disposalMethod = DISPOSAL_METHOD_BACKGROUND; // restore to background color |
599 | 252 | } |
600 | 697 | } |
601 | | // user provided transparency setting |
602 | 4.86k | if(hasSetTransp) { |
603 | 1.25k | pGIF->aFrames[i]->transIndex = pConfig->transIndex; |
604 | 1.25k | } |
605 | 4.86k | pGIF->curResult = CGIF_OK; |
606 | 4.86k | return pGIF->curResult; |
607 | 4.86k | } |
608 | | |
609 | | /* close the GIF-file and free allocated space */ |
610 | 2.07k | int cgif_close(CGIF* pGIF) { |
611 | 2.07k | int r; |
612 | 2.07k | cgif_result result; |
613 | | |
614 | | // check for previous errors |
615 | 2.07k | if(pGIF->curResult != CGIF_OK) { |
616 | 233 | goto CGIF_CLOSE_Cleanup; |
617 | 233 | } |
618 | | |
619 | | // flush all remaining frames in queue |
620 | 4.53k | for(int i = 1; i < SIZE_FRAME_QUEUE; ++i) { |
621 | 3.52k | if(pGIF->aFrames[i] != NULL) { |
622 | 3.11k | r = flushFrame(pGIF, pGIF->aFrames[i], pGIF->aFrames[i - 1]); |
623 | 3.11k | if(r != CGIF_OK) { |
624 | 837 | pGIF->curResult = r; |
625 | 837 | break; |
626 | 837 | } |
627 | 3.11k | } |
628 | 3.52k | } |
629 | | |
630 | | // cleanup |
631 | 2.07k | CGIF_CLOSE_Cleanup: |
632 | 2.07k | r = cgif_raw_close(pGIF->pGIFRaw); // close raw GIF stream |
633 | | // check for errors |
634 | 2.07k | if(r != CGIF_OK) { |
635 | 1.05k | pGIF->curResult = r; |
636 | 1.05k | } |
637 | | |
638 | 2.07k | if(pGIF->pFile) { |
639 | 0 | r = fclose(pGIF->pFile); // we are done at this point => close the file |
640 | 0 | if(r) { |
641 | 0 | pGIF->curResult = CGIF_ECLOSE; // error: fclose failed |
642 | 0 | } |
643 | 0 | } |
644 | 8.30k | for(int i = 0; i < SIZE_FRAME_QUEUE; ++i) { |
645 | 6.23k | freeFrame(pGIF->aFrames[i]); |
646 | 6.23k | } |
647 | | |
648 | 2.07k | result = pGIF->curResult; |
649 | 2.07k | freeCGIF(pGIF); |
650 | | // catch internal value CGIF_PENDING |
651 | 2.07k | if(result == CGIF_PENDING) { |
652 | 166 | result = CGIF_ERROR; |
653 | 166 | } |
654 | 2.07k | return result; // return previous result |
655 | 1.84k | } |