1/*
2 Large image load library -- GIF decoder
3
4 Copyright (C) 2004 Maksim Orlovich <maksim@kde.org>
5 Based almost fully on animated gif playback code,
6 (C) 2004 Daniel Duley (Mosfet) <dan.duley@verizon.net>
7
8 Permission is hereby granted, free of charge, to any person obtaining a copy
9 of this software and associated documentation files (the "Software"), to deal
10 in the Software without restriction, including without limitation the rights
11 to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
12 copies of the Software, and to permit persons to whom the Software is
13 furnished to do so, subject to the following conditions:
14
15 The above copyright notice and this permission notice shall be included in
16 all copies or substantial portions of the Software.
17
18 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
21 AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
22 AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
23 CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
24
25*/
26
27#include "gifloader.h"
28
29#include "animprovider.h"
30
31#include "imageloader.h"
32#include "imagemanager.h"
33#include "pixmapplane.h"
34#include "updater.h"
35
36#include <QByteArray>
37#include <QPainter>
38#include <QVector>
39#include "khtml_debug.h"
40
41#include <stdlib.h>
42
43extern "C" {
44#include <gif_lib.h>
45}
46
47/* avoid cpp warning about undefined macro, old giflib had no GIFLIB_MAJOR */
48#ifndef GIFLIB_MAJOR
49#define GIFLIB_MAJOR 4
50#endif
51
52// #define DEBUG_GIFLOADER
53
54namespace khtmlImLoad
55{
56
57static int INTERLACED_OFFSET[] = { 0, 4, 2, 1 };
58static int INTERLACED_JUMP [] = { 8, 8, 4, 2 };
59
60enum GIFConstants {
61 //Graphics control extension has most of animation info
62 GCE_Code = 0xF9,
63 GCE_Size = 4,
64 //Fields of the above
65 GCE_Flags = 0,
66 GCE_Delay = 1,
67 GCE_TransColor = 3,
68 //Contents of mask
69 GCE_DisposalMask = 0x1C,
70 GCE_DisposalUnspecified = 0x00,
71 GCE_DisposalLeave = 0x04,
72 GCE_DisposalBG = 0x08,
73 GCE_DisposalRestore = 0x0C,
74 GCE_UndocumentedMode4 = 0x10,
75 GCE_TransColorMask = 0x01
76};
77
78struct GIFFrameInfo {
79 bool trans;
80 QColor bg;
81 QRect geom;
82 unsigned int delay;
83 char mode;
84 //###
85};
86
87/**
88 An anim provider for the animated GIFs. We keep a backing store for
89 the screen.
90*/
91class GIFAnimProvider: public AnimProvider
92{
93protected:
94 QVector<GIFFrameInfo> frameInfo;
95 int frame; // refers to the /current/ frame
96
97 // State of gif screen after the previous image.
98 // we paint the current frame on top of it, and don't touch until
99 // the current frame is disposed
100 QPixmap canvas;
101 QColor bgColor;
102 bool firstTime;
103
104 // Previous mode being background seems to trigger an OpSrc rather than OpOver updating
105 bool previousWasBG;
106public:
107 GIFAnimProvider(PixmapPlane *plane, Image *img, QVector<GIFFrameInfo> _frames, QColor bg):
108 AnimProvider(plane, img), bgColor(bg), firstTime(true), previousWasBG(false)
109 {
110 frameInfo = _frames;
111 frame = 0;
112 canvas = QPixmap(img->size());
113 canvas.fill(bgColor);
114 }
115
116 // Renders a portion of the current frame's image on the painter..
117 void renderCurImage(int dx, int dy, QPainter *p, int sx, int sy, int width, int height)
118 {
119 QRect frameGeom = frameInfo[frame].geom;
120
121 // Take the passed paint rectangle in gif screen coordinates, and
122 // clip it to the frame's geometry
123 QRect screenPaintRect = QRect(sx, sy, width, height) & frameGeom;
124
125 // Same thing but in the frame's coordinate system
126 QRect framePaintRect = screenPaintRect.translated(-frameGeom.topLeft());
127
128 curFrame->paint(dx + screenPaintRect.x() - sx, dy + screenPaintRect.y() - sy, p,
129 framePaintRect.x(), framePaintRect.y(),
130 framePaintRect.width(), framePaintRect.height());
131 }
132
133 // Renders current gif screen state on the painter
134 void renderCurScreen(int dx, int dy, QPainter *p, int sx, int sy, int width, int height)
135 {
136 // Depending on the previous frame's mode, we may have to cut out a hole when
137 // painting the canvas, since if previous frame had BG disposal, we have to do OpSrc.
138 if (previousWasBG) {
139 QRegion canvasDrawRegion(sx, sy, width, height);
140 canvasDrawRegion -= frameInfo[frame].geom;
141 QVector<QRect> srcRects = canvasDrawRegion.rects();
142
143 foreach (const QRect &r, srcRects) {
144 p->drawPixmap(QPoint(dx + r.x() - sx, dy + r.y() - sy), canvas, r);
145 }
146 } else {
147 p->drawPixmap(dx, dy, canvas, sx, sy, width, height);
148 }
149
150 // Now render the current frame's overlay
151 renderCurImage(dx, dy, p, sx, sy, width, height);
152 }
153
154 // Update screen, incorporating the dispose operator for current image
155 void updateScreenAfterDispose()
156 {
157 previousWasBG = false;
158
159 // If we're the last frame, just clear the canvas...
160 if (frame == frameInfo.size() - 1) {
161 canvas.fill(bgColor);
162 return;
163 }
164
165 switch (frameInfo[frame].mode) {
166 case GCE_DisposalRestore:
167 case GCE_UndocumentedMode4: // Not in the spec, mozilla inteprets as above
168 // "restore" means the state of the canvas should be the
169 // same as before the current frame.. But we don't touch it
170 // when painting, so it's a no-op.
171 return;
172
173 case GCE_DisposalLeave:
174 case GCE_DisposalUnspecified: { // Qt3 appears to interpret this as leave
175 // Update the canvas with current image.
176 QPainter p(&canvas);
177 if (previousWasBG) {
178 p.setCompositionMode(QPainter::CompositionMode_Source);
179 }
180 renderCurImage(0, 0, &p, 0, 0, canvas.width(), canvas.height());
181 return;
182 }
183
184 case GCE_DisposalBG: {
185 previousWasBG = true;
186 // Clear with bg color --- the frame rect only
187 QPainter p(&canvas);
188 p.setCompositionMode(QPainter::CompositionMode_Source);
189 p.fillRect(frameInfo[frame].geom, bgColor);
190 return;
191 }
192
193 default:
194 // Including GCE_DisposalUnspecified -- ???
195 break;
196 }
197 }
198
199 void paint(int dx, int dy, QPainter *p, int sx, int sy, int width, int height) override
200 {
201 if (!width || !height) {
202 return; //Nothing to draw.
203 }
204
205 // Move over to next frame if need be, incorporating
206 // the change effect of current one onto the screen.
207 if (shouldSwitchFrame) {
208 updateScreenAfterDispose();
209
210 ++frame;
211 if (frame >= frameInfo.size()) {
212 if (animationAdvice == KHTMLSettings::KAnimationLoopOnce) {
213 animationAdvice = KHTMLSettings::KAnimationDisabled;
214 }
215 frame = 0;
216 }
217 nextFrame();
218 }
219
220 // Request next frame to be drawn...
221 if (shouldSwitchFrame || firstTime) {
222 shouldSwitchFrame = false;
223 firstTime = false;
224
225 // ### FIXME: adjust based on actual interframe timing -- the jitter is
226 // likely to be quite big
227 ImageManager::animTimer()->nextFrameIn(this, frameInfo[frame].delay);
228 }
229
230 // Render the currently active frame
231 renderCurScreen(dx, dy, p, sx, sy, width, height);
232
233#ifdef DEBUG_GIFLOADER
234 p->drawText(QPoint(dx - sx, dy - sy + p->fontMetrics().height()), QString::number(frame));
235#endif
236 }
237
238 AnimProvider *clone(PixmapPlane *plane) override
239 {
240 if (frame0->height == 0 || frame0->width == 0 ||
241 plane->height == 0 || plane->width == 0) {
242 return nullptr;
243 }
244
245 float heightRatio = frame0->height / plane->height;
246 float widthRatio = frame0->width / plane->width;
247
248 QVector<GIFFrameInfo> newFrameInfo;
249 Q_FOREACH (const GIFFrameInfo &oldFrame, frameInfo) {
250 GIFFrameInfo newFrame(oldFrame);
251
252 newFrame.geom.setWidth(oldFrame.geom.width() * widthRatio);
253 newFrame.geom.setHeight(oldFrame.geom.height() * heightRatio);
254 newFrame.geom.setX(oldFrame.geom.x() * widthRatio);
255 newFrame.geom.setY(oldFrame.geom.y() * heightRatio);
256 newFrameInfo.append(newFrame);
257 }
258
259 return new GIFAnimProvider(plane, image, newFrameInfo, bgColor);
260 }
261};
262
263class GIFLoader: public ImageLoader
264{
265 QByteArray buffer;
266 int bufferReadPos;
267public:
268 GIFLoader()
269 {
270 bufferReadPos = 0;
271 }
272
273 ~GIFLoader()
274 {
275 }
276
277 int processData(uchar *data, int length) override
278 {
279 //Collect data in the buffer
280 int pos = buffer.size();
281 buffer.resize(buffer.size() + length);
282 memcpy(buffer.data() + pos, data, length);
283 return length;
284 }
285
286 static int gifReaderBridge(GifFileType *gifInfo, GifByteType *data, int limit)
287 {
288 GIFLoader *me = static_cast<GIFLoader *>(gifInfo->UserData);
289
290 int remBytes = me->buffer.size() - me->bufferReadPos;
291 int toRet = qMin(remBytes, limit);
292
293 memcpy(data, me->buffer.data() + me->bufferReadPos, toRet);
294 me->bufferReadPos += toRet;
295
296 return toRet;
297 }
298
299#if GIFLIB_MAJOR >= 5
300 static unsigned int decode16Bit(unsigned char *signedLoc)
301#else
302 static unsigned int decode16Bit(char *signedLoc)
303#endif
304 {
305 unsigned char *loc = reinterpret_cast<unsigned char *>(signedLoc);
306
307 //GIFs are little-endian
308 return loc[0] | (((unsigned int)loc[1]) << 8);
309 }
310
311 static void palettedToRGB(uchar *out, uchar *in, ImageFormat &format, int w)
312 {
313 int outPos = 0;
314 for (int x = 0; x < w; ++x) {
315 int colorCode = in[x];
316 QRgb color = 0;
317 if (colorCode < format.palette.size()) {
318 color = format.palette[colorCode];
319 }
320
321 *reinterpret_cast<QRgb *>(&out[outPos]) = color;
322 outPos += 4;
323 }
324 }
325
326 // Read a color from giflib palette, with range checking
327 static QColor colorMapColor(ColorMapObject *map, int index)
328 {
329 QColor col(Qt::black);
330 if (!map) {
331 return col;
332 }
333
334 if (index < map->ColorCount)
335 col = QColor(map->Colors[index].Red,
336 map->Colors[index].Green,
337 map->Colors[index].Blue);
338 return col;
339 }
340
341 static void printColorMap(ColorMapObject *map)
342 {
343 for (int c = 0; c < map->ColorCount; ++c)
344 qCDebug(KHTML_LOG) << " " << map << c << map->Colors[c].Red
345 << map->Colors[c].Green
346 << map->Colors[c].Blue;
347 }
348
349 int processEOF() override
350 {
351 //Feed the buffered data to libUnGif
352#if GIFLIB_MAJOR >= 5
353 int errorCode;
354 GifFileType *file = DGifOpen(this, gifReaderBridge, &errorCode);
355#else
356 GifFileType *file = DGifOpen(this, gifReaderBridge);
357#endif
358
359 if (!file) {
360 return Error;
361 }
362
363 if (DGifSlurp(file) == GIF_ERROR) {
364#if GIFLIB_MAJOR >= 5 && GIFLIB_MINOR >= 1
365 DGifCloseFile(file, &errorCode);
366 return errorCode;
367#else
368 DGifCloseFile(file);
369 return Error;
370#endif
371 }
372
373 //We use canvas size only for animations
374 if (file->ImageCount > 1) {
375 // Verify it..
376 if (!ImageManager::isAcceptableSize(file->SWidth, file->SHeight)) {
377#if GIFLIB_MAJOR >= 5 && GIFLIB_MINOR >= 1
378 DGifCloseFile(file, &errorCode);
379 return errorCode;
380#else
381 DGifCloseFile(file);
382 return Error;
383#endif
384 }
385 notifyImageInfo(file->SWidth, file->SHeight);
386 }
387
388 // Check each frame to be within the size limit policy
389 for (int frame = 0; frame < file->ImageCount; ++frame) {
390 //Extract colormap, geometry, so that we can create the frame
391 SavedImage *curFrame = &file->SavedImages[frame];
392 if (!ImageManager::isAcceptableSize(curFrame->ImageDesc.Width, curFrame->ImageDesc.Height)) {
393#if GIFLIB_MAJOR >= 5 && GIFLIB_MINOR >= 1
394 DGifCloseFile(file, &errorCode);
395 return errorCode;
396#else
397 DGifCloseFile(file);
398 return Error;
399#endif
400 }
401 }
402
403 QVector<GIFFrameInfo> frameProps;
404
405 // First, use the screen color map...
406 ColorMapObject *globalColorMap = file->SColorMap;
407
408 // If for some reason there is none, pick one from an image,
409 // and pray it works.
410 if (!globalColorMap) {
411 globalColorMap = file->Image.ColorMap;
412 }
413
414 QColor bgColor = colorMapColor(globalColorMap, file->SBackGroundColor);
415
416 //Extract out all the frames
417 for (int frame = 0; frame < file->ImageCount; ++frame) {
418 //Extract colormap, geometry, so that we can create the frame
419 SavedImage *curFrame = &file->SavedImages[frame];
420 int w = curFrame->ImageDesc.Width;
421 int h = curFrame->ImageDesc.Height;
422
423 //For non-animated images, use the frame size for dimension
424 if (frame == 0 && file->ImageCount == 1) {
425 notifyImageInfo(w, h);
426 }
427
428 ColorMapObject *colorMap = curFrame->ImageDesc.ColorMap;
429 if (!colorMap) {
430 colorMap = globalColorMap;
431 }
432
433 GIFFrameInfo frameInf;
434 int trans = -1;
435 frameInf.delay = 100;
436 frameInf.mode = GCE_DisposalUnspecified;
437
438 //Go through the extension blocks to see whether there is a color key,
439 //and animation info inside the graphics control extension (GCE) block
440 for (int ext = 0; ext < curFrame->ExtensionBlockCount; ++ext) {
441 ExtensionBlock *curExt = &curFrame->ExtensionBlocks[ext];
442 if ((curExt->Function == GCE_Code) && (curExt->ByteCount >= GCE_Size)) {
443 if (curExt->Bytes[GCE_Flags] & GCE_TransColorMask) {
444 trans = ((unsigned char)curExt->Bytes[GCE_TransColor]);
445 }
446
447 frameInf.mode = curExt->Bytes[GCE_Flags] & GCE_DisposalMask;
448 frameInf.delay = decode16Bit(&curExt->Bytes[GCE_Delay]) * 10;
449
450 if (frameInf.delay < 100) {
451 frameInf.delay = 100;
452 }
453 }
454 }
455
456#ifdef DEBUG_GIFLOADER
457 frameInf.delay = 1500;
458#endif
459
460 // The only thing I found resembling an explanation suggests that we should
461 // set bgColor to transparent if the first frame's GCE is such...
462 // Let's hope this is what actually happens.. (Man, I wish testcasing GIFs was manageable)
463 if (frame == 0 && trans != -1) {
464 bgColor = QColor(Qt::transparent);
465 }
466
467 // If we have transparency, we need to go an RGBA mode.
468 ImageFormat format;
469 if (trans != -1) {
470 format.type = ImageFormat::Image_ARGB_32; // Premultiply always OK, since we only have 0/1 alpha
471 } else {
472 format.type = ImageFormat::Image_Palette_8;
473 }
474
475 // Read in colors for the palette... Don't waste memory on
476 // any extra ones beyond 256, though
477 int colorCount = colorMap ? colorMap->ColorCount : 0;
478 for (int c = 0; c < colorCount && c < 256; ++c) {
479 format.palette.append(qRgba(colorMap->Colors[c].Red,
480 colorMap->Colors[c].Green,
481 colorMap->Colors[c].Blue, 255));
482 }
483
484 // Pad with black as a precaution
485 for (int c = colorCount; c < 256; ++c) {
486 format.palette.append(qRgba(0, 0, 0, 255));
487 }
488
489 //Put in the colorkey color
490 if (trans != -1) {
491 format.palette[trans] = qRgba(0, 0, 0, 0);
492 }
493
494 //Now we can declare frame format
495 notifyAppendFrame(w, h, format);
496
497 frameInf.bg = bgColor;
498 frameInf.geom = QRect(curFrame->ImageDesc.Left,
499 curFrame->ImageDesc.Top,
500 w, h);
501
502 frameInf.trans = format.hasAlpha();
503 frameProps.append(frameInf);
504
505#ifdef DEBUG_GIFLOADER
506 qDebug("frame:%d:%d,%d:%dx%d, trans:%d, mode:%d", frame, frameInf.geom.x(), frameInf.geom.y(), w, h, trans, frameInf.mode);
507#endif
508
509 //Decode the scanlines
510 uchar *buf;
511 if (format.hasAlpha()) {
512 buf = new uchar[w * 4];
513 } else {
514 buf = new uchar[w];
515 }
516
517 if (curFrame->ImageDesc.Interlace) {
518 // Interlaced. Considering we don't do progressive loading of gif's,
519 // a useless annoyance... The way it works is that on the first pass
520 // it renders scanlines 8*n, on second 8*n + 4,
521 // third then 4*n + 2, and finally 2*n + 1 (the odd lines)
522 // e.g.:
523 // 0, 8, 16, ...
524 // 4, 12, 20, ...
525 // 2, 6, 10, 14, ...
526 // 1, 3, 5, 7, ...
527 //
528 // Anyway, the bottom line is that INTERLACED_OFFSET contains
529 // the initial position, and INTERLACED_JUMP has the increment.
530 // However, imload expects a top-bottom scan of the image...
531 // so what we do it is keep track of which lines are actually
532 // new via nextNewLine variable, and leave others unchanged.
533
534 int interlacedImageScanline = 0; // scanline in interlaced image we are reading from
535 for (int pass = 0; pass < 4; ++pass) {
536 int nextNewLine = INTERLACED_OFFSET[pass];
537
538 for (int line = 0; line < h; ++line) {
539 if (line == nextNewLine) {
540 uchar *toFeed = (uchar *) curFrame->RasterBits + w * interlacedImageScanline;
541 if (format.hasAlpha()) {
542 palettedToRGB(buf, toFeed, format, w);
543 toFeed = buf;
544 }
545
546 notifyScanline(pass + 1, toFeed);
547 ++interlacedImageScanline;
548 nextNewLine += INTERLACED_JUMP[pass];
549 } else {
550 // No new information for this scanline, so just
551 // get it from loader, and feed it right back in
552 requestScanline(line, buf);
553 notifyScanline(pass + 1, buf);
554 }
555 } // for every scanline
556 } // for pass..
557 } // if interlaced
558 else {
559 for (int line = 0; line < h; ++line) {
560 uchar *toFeed = (uchar *) file->SavedImages[frame].RasterBits + w * line;
561 if (format.hasAlpha()) {
562 palettedToRGB(buf, toFeed, format, w);
563 toFeed = buf;
564 }
565 notifyScanline(1, toFeed);
566 }
567 }
568 delete[] buf;
569 }
570
571 if (file->ImageCount > 1) {
572 //need animation provider
573 PixmapPlane *frame0 = requestFrame0();
574 frame0->animProvider = new GIFAnimProvider(frame0, image, frameProps, bgColor);
575 }
576
577#if GIFLIB_MAJOR >= 5 && GIFLIB_MINOR >= 1
578 DGifCloseFile(file, &errorCode);
579#else
580 DGifCloseFile(file);
581#endif
582
583 return Done;
584 }
585};
586
587ImageLoaderProvider::Type GIFLoaderProvider::type()
588{
589 return Efficient;
590}
591
592ImageLoader *GIFLoaderProvider::loaderFor(const QByteArray &prefix)
593{
594 uchar *data = (uchar *)prefix.data();
595 if (prefix.size() < 6) {
596 return nullptr;
597 }
598
599 if (data[0] == 'G' &&
600 data[1] == 'I' &&
601 data[2] == 'F' &&
602 data[3] == '8' &&
603 ((data[4] == '7') || (data[4] == '9')) &&
604 data[5] == 'a') {
605 return new GIFLoader;
606 }
607
608 return nullptr;
609}
610
611}
612
613