Commit 74c595f1 authored by Robert Ancell's avatar Robert Ancell

gif: Render GIF frames on demand

The previous code loaded all the frames into memory, which causes a memory
and CPU explosion when a large GIF was loaded.

From the existing code:
/* The below reflects the "use hell of a lot of RAM" philosophy of coding */

The updated code now generates each image on demand by storing the compressed
data and decompressing and combining with the last image. This uses more CPU
than the old method, but allows arbitraritly large GIFs to play.

The stop_after_first_frame hack that worked around this is no longer required
and removed. This hack didn't work for progressive loading.

If a GIF was played with frames out of order (e.g. in reverse) this would be
quite slow, as repeated rendering would occur. This seems unlikely in the
GIF use case but if it was a problem storing some key frames that would
normally be discarded would reduce this while not using too much memory.

A render cache could also be helpful in making a CPU / RAM trade-off for
small repeating GIFs that have few frames.
parent 036f0214
Pipeline #45322 failed with stage
in 1 minute and 21 seconds
......@@ -24,6 +24,7 @@
#include <errno.h>
#include "gdk-pixbuf-transform.h"
#include "io-gif-animation.h"
#include "lzw.h"
static void gdk_pixbuf_gif_anim_finalize (GObject *object);
......@@ -35,6 +36,7 @@ static void gdk_pixbuf_gif_anim_get_size (GdkPixbufAnimation
int *height);
static GdkPixbufAnimationIter* gdk_pixbuf_gif_anim_get_iter (GdkPixbufAnimation *anim,
const GTimeVal *start_time);
static GdkPixbuf* gdk_pixbuf_gif_anim_iter_get_pixbuf (GdkPixbufAnimationIter *iter);
......@@ -70,16 +72,17 @@ gdk_pixbuf_gif_anim_finalize (GObject *object)
for (l = gif_anim->frames; l; l = l->next) {
frame = l->data;
g_object_unref (frame->pixbuf);
if (frame->composited)
g_object_unref (frame->composited);
if (frame->revert)
g_object_unref (frame->revert);
g_byte_array_unref (frame->lzw_data);
if (frame->color_map_allocated)
g_free (frame->color_map);
g_free (frame);
}
g_list_free (gif_anim->frames);
g_clear_object (&gif_anim->last_frame_data);
g_clear_object (&gif_anim->last_frame_revert_data);
G_OBJECT_CLASS (gdk_pixbuf_gif_anim_parent_class)->finalize (object);
}
......@@ -98,13 +101,16 @@ static GdkPixbuf*
gdk_pixbuf_gif_anim_get_static_image (GdkPixbufAnimation *animation)
{
GdkPixbufGifAnim *gif_anim;
GdkPixbufAnimationIter *iter;
GTimeVal start_time = { 0, 0 };
gif_anim = GDK_PIXBUF_GIF_ANIM (animation);
if (gif_anim->frames == NULL)
return NULL;
else
return GDK_PIXBUF (((GdkPixbufFrame*)gif_anim->frames->data)->pixbuf);
iter = gdk_pixbuf_gif_anim_get_iter (animation, &start_time);
return gdk_pixbuf_gif_anim_iter_get_pixbuf (iter);
}
static void
......@@ -164,7 +170,6 @@ gdk_pixbuf_gif_anim_get_iter (GdkPixbufAnimation *anim,
static void gdk_pixbuf_gif_anim_iter_finalize (GObject *object);
static int gdk_pixbuf_gif_anim_iter_get_delay_time (GdkPixbufAnimationIter *iter);
static GdkPixbuf* gdk_pixbuf_gif_anim_iter_get_pixbuf (GdkPixbufAnimationIter *iter);
static gboolean gdk_pixbuf_gif_anim_iter_on_currently_loading_frame (GdkPixbufAnimationIter *iter);
static gboolean gdk_pixbuf_gif_anim_iter_advance (GdkPixbufAnimationIter *iter,
const GTimeVal *current_time);
......@@ -205,47 +210,6 @@ gdk_pixbuf_gif_anim_iter_finalize (GObject *object)
G_OBJECT_CLASS (gdk_pixbuf_gif_anim_iter_parent_class)->finalize (object);
}
static void
gtk_pixpuf_gif_reuse_old_composited_buffer (GdkPixbufFrame *old,
GdkPixbufFrame *new)
{
/* Move old buffer to new frame to reuse memory and
* minimise footprint */
new->composited = old->composited;
old->composited = NULL;
}
static void
gdk_pixbuf_gif_anim_iter_clean_previous (GList *initial)
{
GdkPixbufFrame *frame;
GList *tmp;
/* Cleanup previous frames until first cleaned frame */
frame = initial->data;
/* Check the current frame */
if (!frame->composited || frame->need_recomposite) {
/* The composite frame doesn't exist,
* nothing to cleanup */
return;
}
/* Work with previous frames */
tmp = initial->prev;
while (tmp != NULL) {
frame = tmp->data;
if (!frame->composited || frame->need_recomposite) {
/* Composite for previous frame doesn't exist */
break;
}
/* delete cached pixbuf */
g_clear_object (&frame->composited);
tmp = tmp->prev;
}
}
static gboolean
gdk_pixbuf_gif_anim_iter_advance (GdkPixbufAnimationIter *anim_iter,
const GTimeVal *current_time)
......@@ -306,8 +270,6 @@ gdk_pixbuf_gif_anim_iter_advance (GdkPixbufAnimationIter *anim_iter,
break;
tmp = tmp->next;
if (tmp)
gdk_pixbuf_gif_anim_iter_clean_previous(tmp);
}
old = iter->current_frame;
......@@ -341,245 +303,146 @@ gdk_pixbuf_gif_anim_iter_get_delay_time (GdkPixbufAnimationIter *anim_iter)
return -1; /* show last frame forever */
}
void
gdk_pixbuf_gif_anim_frame_composite (GdkPixbufGifAnim *gif_anim,
GdkPixbufFrame *frame)
static guint
interlace (guint row, guint height)
{
GList *link;
GList *tmp;
link = g_list_find (gif_anim->frames, frame);
if (frame->need_recomposite || frame->composited == NULL) {
/* For now, to composite we start with the last
* composited frame and composite everything up to
* here.
*/
/* Rewind to last composited frame. */
tmp = link;
while (tmp != NULL) {
GdkPixbufFrame *f = tmp->data;
if (f->need_recomposite) {
if (f->composited) {
g_object_unref (f->composited);
f->composited = NULL;
}
}
if (f->composited != NULL)
break;
tmp = tmp->prev;
}
/* Go forward, compositing all frames up to the current frame */
if (tmp == NULL)
tmp = gif_anim->frames;
while (tmp != NULL) {
GdkPixbufFrame *f = tmp->data;
gint clipped_width, clipped_height;
if (f->pixbuf == NULL)
return;
clipped_width = MIN (gif_anim->width - f->x_offset, gdk_pixbuf_get_width (f->pixbuf));
clipped_height = MIN (gif_anim->height - f->y_offset, gdk_pixbuf_get_height (f->pixbuf));
if (f->need_recomposite) {
if (f->composited) {
g_object_unref (f->composited);
f->composited = NULL;
}
}
if (f->composited != NULL)
goto next;
if (tmp->prev == NULL) {
/* First frame may be smaller than the whole image;
* if so, we make the area outside it full alpha if the
* image has alpha, and background color otherwise.
* GIF spec doesn't actually say what to do about this.
*/
f->composited = gdk_pixbuf_new (GDK_COLORSPACE_RGB,
TRUE,
8, gif_anim->width, gif_anim->height);
if (f->composited == NULL)
return;
/* alpha gets dumped if f->composited has no alpha */
gdk_pixbuf_fill (f->composited,
((unsigned) gif_anim->bg_red << 24) |
(gif_anim->bg_green << 16) |
(gif_anim->bg_blue << 8));
if (clipped_width > 0 && clipped_height > 0)
gdk_pixbuf_composite (f->pixbuf,
f->composited,
f->x_offset,
f->y_offset,
clipped_width,
clipped_height,
f->x_offset, f->y_offset,
1.0, 1.0,
GDK_INTERP_BILINEAR,
255);
if (f->action == GDK_PIXBUF_FRAME_REVERT)
g_warning ("First frame of GIF has bad dispose mode, GIF loader should not have loaded this image");
f->need_recomposite = FALSE;
} else {
GdkPixbufFrame *prev_frame;
gint prev_clipped_width;
gint prev_clipped_height;
prev_frame = tmp->prev->data;
prev_clipped_width = MIN (gif_anim->width - prev_frame->x_offset, gdk_pixbuf_get_width (prev_frame->pixbuf));
prev_clipped_height = MIN (gif_anim->height - prev_frame->y_offset, gdk_pixbuf_get_height (prev_frame->pixbuf));
/* Init f->composited with what we should have after the previous
* frame
*/
if (prev_frame->action == GDK_PIXBUF_FRAME_RETAIN) {
gtk_pixpuf_gif_reuse_old_composited_buffer (prev_frame, f);
if (f->composited == NULL)
return;
} else if (prev_frame->action == GDK_PIXBUF_FRAME_DISPOSE) {
gtk_pixpuf_gif_reuse_old_composited_buffer (prev_frame, f);
if (f->composited == NULL)
return;
if (prev_clipped_width > 0 && prev_clipped_height > 0) {
/* Clear area of previous frame to background */
GdkPixbuf *area;
area = gdk_pixbuf_new_subpixbuf (f->composited,
prev_frame->x_offset,
prev_frame->y_offset,
prev_clipped_width,
prev_clipped_height);
if (area == NULL)
return;
gdk_pixbuf_fill (area,
(gif_anim->bg_red << 24) |
(gif_anim->bg_green << 16) |
(gif_anim->bg_blue << 8));
g_object_unref (area);
}
} else if (prev_frame->action == GDK_PIXBUF_FRAME_REVERT) {
gtk_pixpuf_gif_reuse_old_composited_buffer (prev_frame, f);
if (f->composited == NULL)
return;
if (prev_frame->revert != NULL &&
prev_clipped_width > 0 && prev_clipped_height > 0) {
/* Copy in the revert frame */
gdk_pixbuf_copy_area (prev_frame->revert,
0, 0,
gdk_pixbuf_get_width (prev_frame->revert),
gdk_pixbuf_get_height (prev_frame->revert),
f->composited,
prev_frame->x_offset,
prev_frame->y_offset);
}
} else {
g_warning ("Unknown revert action for GIF frame");
}
if (f->revert == NULL &&
f->action == GDK_PIXBUF_FRAME_REVERT) {
if (clipped_width > 0 && clipped_height > 0) {
/* We need to save the contents before compositing */
GdkPixbuf *area;
area = gdk_pixbuf_new_subpixbuf (f->composited,
f->x_offset,
f->y_offset,
clipped_width,
clipped_height);
if (area == NULL)
return;
f->revert = gdk_pixbuf_copy (area);
g_object_unref (area);
if (f->revert == NULL)
return;
}
}
if (clipped_width > 0 && clipped_height > 0 &&
f->pixbuf != NULL && f->composited != NULL) {
/* Put current frame onto f->composited */
gdk_pixbuf_composite (f->pixbuf,
f->composited,
f->x_offset,
f->y_offset,
clipped_width,
clipped_height,
f->x_offset, f->y_offset,
1.0, 1.0,
GDK_INTERP_NEAREST,
255);
}
f->need_recomposite = FALSE;
}
guint i = 0, y = 0;
while (i < row && y < height)
y += 8, i++;
if (y >= height)
y = 4;
while (i < row && y < height)
y += 8, i++;
if (y >= height)
y = 2;
while (i < row && y < height)
y += 4, i++;
if (y >= height)
y = 1;
while (i < row && y < height)
y += 2, i++;
return y;
}
next:
if (tmp == link)
break;
static void
composite_frame (GdkPixbufGifAnim *anim, GdkPixbufFrame *frame)
{
g_autoptr(LZWDecoder) lzw_decoder = NULL;
g_autofree guint8 *index_buffer = NULL;
gsize n_indexes, i;
guchar *pixels;
anim->last_frame = frame;
/* Store overwritten data if required */
g_clear_object (&anim->last_frame_revert_data);
if (frame->action == GDK_PIXBUF_FRAME_REVERT) {
anim->last_frame_revert_data = gdk_pixbuf_new (GDK_COLORSPACE_RGB, TRUE, 8, frame->width, frame->height);
gdk_pixbuf_copy_area (anim->last_frame_data,
frame->x_offset, frame->y_offset, frame->width, frame->height,
anim->last_frame_revert_data,
0, 0);
}
tmp = tmp->next;
if (tmp)
gdk_pixbuf_gif_anim_iter_clean_previous(tmp);
}
lzw_decoder = lzw_decoder_new (frame->lzw_code_size + 1);
index_buffer = g_new (guint8, frame->width * frame->height);
n_indexes = lzw_decoder_feed (lzw_decoder, frame->lzw_data->data, frame->lzw_data->len, index_buffer, frame->width * frame->height);
pixels = gdk_pixbuf_get_pixels (anim->last_frame_data);
for (i = 0; i < n_indexes; i++) {
guint8 index = index_buffer[i];
guint x, y;
int offset;
if (index == frame->transparent_index)
continue;
x = i % frame->width + frame->x_offset;
if (frame->interlace)
y = interlace (i / frame->width, frame->height) + frame->y_offset;
else
y = i / frame->width + frame->y_offset;
if (x >= anim->width || y >= anim->height)
continue;
offset = y * gdk_pixbuf_get_rowstride (anim->last_frame_data) + x * 4;
pixels[offset + 0] = frame->color_map[index * 3 + 0];
pixels[offset + 1] = frame->color_map[index * 3 + 1];
pixels[offset + 2] = frame->color_map[index * 3 + 2];
pixels[offset + 3] = 255;
}
}
GdkPixbuf*
gdk_pixbuf_gif_anim_iter_get_pixbuf (GdkPixbufAnimationIter *anim_iter)
{
GdkPixbufGifAnimIter *iter;
GdkPixbufFrame *frame;
GdkPixbufGifAnimIter *iter = GDK_PIXBUF_GIF_ANIM_ITER (anim_iter);
GdkPixbufGifAnim *anim = iter->gif_anim;
GdkPixbufFrame *requested_frame;
GList *link;
iter = GDK_PIXBUF_GIF_ANIM_ITER (anim_iter);
if (iter->current_frame != NULL)
requested_frame = iter->current_frame->data;
else
requested_frame = g_list_last (anim->frames)->data;
/* If the previously rendered frame is not before this one, then throw it away */
if (anim->last_frame != NULL) {
link = g_list_find (anim->frames, anim->last_frame);
while (link != NULL && link->data != requested_frame)
link = link->next;
if (link == NULL)
anim->last_frame = NULL;
}
frame = iter->current_frame ? iter->current_frame->data : g_list_last (iter->gif_anim->frames)->data;
/* If no rendered frame, render the first frame */
if (anim->last_frame == NULL) {
if (anim->last_frame_data == NULL)
anim->last_frame_data = gdk_pixbuf_new (GDK_COLORSPACE_RGB, TRUE, 8, anim->width, anim->height);
memset (gdk_pixbuf_get_pixels (anim->last_frame_data), 0, gdk_pixbuf_get_rowstride (anim->last_frame_data) * anim->height);
composite_frame (anim, g_list_nth_data (anim->frames, 0));
}
#if 0
if (FALSE && frame)
g_print ("current frame %d dispose mode %d %d x %d\n",
g_list_index (iter->gif_anim->frames,
frame),
frame->action,
gdk_pixbuf_get_width (frame->pixbuf),
gdk_pixbuf_get_height (frame->pixbuf));
#endif
/* If the requested frame is already rendered, then no action required */
if (requested_frame == anim->last_frame)
return anim->last_frame_data;
if (frame == NULL)
return NULL;
/* Starting from the last rendered frame, render to the current frame */
for (link = g_list_find (anim->frames, anim->last_frame); link->next != NULL && link->data != requested_frame; link = link->next) {
GdkPixbufFrame *frame = link->data;
guchar *pixels;
int y, x_end, y_end;
gdk_pixbuf_gif_anim_frame_composite (iter->gif_anim, frame);
/* Remove last frame if required */
switch (frame->action) {
case GDK_PIXBUF_FRAME_RETAIN:
break;
case GDK_PIXBUF_FRAME_DISPOSE:
/* Replace previous area with background */
pixels = gdk_pixbuf_get_pixels (anim->last_frame_data);
x_end = MIN (anim->last_frame->x_offset + anim->last_frame->width, anim->width);
y_end = MIN (anim->last_frame->y_offset + anim->last_frame->height, anim->height);
for (y = anim->last_frame->y_offset; y < y_end; y++) {
guchar *line = pixels + y * gdk_pixbuf_get_rowstride (anim->last_frame_data) + anim->last_frame->x_offset * 4;
memset (line, 0, (x_end - anim->last_frame->x_offset) * 4);
}
break;
case GDK_PIXBUF_FRAME_REVERT:
/* Replace previous area with last retained area */
if (anim->last_frame_revert_data != NULL)
gdk_pixbuf_copy_area (anim->last_frame_revert_data,
0, 0, anim->last_frame->width, anim->last_frame->height,
anim->last_frame_data,
anim->last_frame->x_offset, anim->last_frame->y_offset);
break;
}
/* Render next frame */
composite_frame (anim, link->next->data);
}
return frame->composited;
return anim->last_frame_data;
}
static gboolean
......
......@@ -59,23 +59,24 @@ typedef struct _GdkPixbufFrame GdkPixbufFrame;
struct _GdkPixbufGifAnim {
GdkPixbufAnimation parent_instance;
/* Number of frames */
int n_frames;
/* Total length of animation */
int total_time;
/* Color map */
guchar color_map[256 * 3];
/* List of GdkPixbufFrame structures */
GList *frames;
/* bounding box size */
int width, height;
guchar bg_red;
guchar bg_green;
guchar bg_blue;
int loop;
/* Last rendered frames */
GdkPixbuf *last_frame_data;
GdkPixbufFrame *last_frame;
GdkPixbuf *last_frame_revert_data;
};
struct _GdkPixbufGifAnimClass {
......@@ -125,12 +126,25 @@ GType gdk_pixbuf_gif_anim_iter_get_type (void) G_GNUC_CONST;
struct _GdkPixbufFrame {
/* The pixbuf with this frame's image data */
GdkPixbuf *pixbuf;
/* Compressed frame data */
GByteArray *lzw_data;
guint8 lzw_code_size;
/* Offsets for overlaying onto the GIF graphic area */
/* Position of frame data in image */
int x_offset;
int y_offset;
guint16 width;
guint16 height;
/* Layout of pixels */
gboolean interlace;
/* Color map */
gboolean color_map_allocated;
guchar *color_map;
/* Transparency */
int transparent_index;
/* Frame duration in ms */
int delay_time;
......@@ -140,30 +154,6 @@ struct _GdkPixbufFrame {
/* Overlay mode */
GdkPixbufFrameAction action;
/* TRUE if the pixbuf has been modified since
* the last frame composite operation
*/
gboolean need_recomposite;
/* The below reflects the "use hell of a lot of RAM"
* philosophy of coding
*/
/* Cached composite image (the image you actually display
* for this frame)
*/
GdkPixbuf *composited;
/* Cached revert image (the contents of the area
* covered by the frame prior to compositing;
* same size as pixbuf, not as the composite image; only
* used for FRAME_REVERT frames)
*/
GdkPixbuf *revert;
};
void gdk_pixbuf_gif_anim_frame_composite (GdkPixbufGifAnim *gif_anim,
GdkPixbufFrame *frame);
#endif
......@@ -58,7 +58,6 @@
#include <glib/gi18n-lib.h>
#include "gdk-pixbuf-io.h"
#include "io-gif-animation.h"
#include "lzw.h"
......@@ -108,12 +107,10 @@ struct _GifContext
gboolean has_global_cmap;
CMap global_color_map;
gint global_colormap_size;
unsigned int global_bit_pixel;
unsigned int global_color_resolution;
unsigned int background_index;
gboolean stop_after_first_frame;
gboolean frame_cmap_active;
CMap frame_color_map;
......@@ -156,14 +153,6 @@ struct _GifContext
gint block_ptr;
guchar lzw_set_code_size;
LZWDecoder *lzw_decoder;
guint8 *index_buffer;
gsize index_buffer_length;
/* painting context */
gint draw_xpos;
gint draw_ypos;
gint draw_pass;
/* error pointer */
GError **error;
......@@ -260,15 +249,9 @@ gif_get_colormap (GifContext *context)
return -1;
}
context->global_color_map[0][context->global_colormap_size] = rgb[0];
context->global_color_map[1][context->global_colormap_size] = rgb[1];
context->global_color_map[2][context->global_colormap_size] = rgb[2];
if (context->global_colormap_size == context->background_index) {
context->animation->bg_red = rgb[0];
context->animation->bg_green = rgb[1];
context->animation->bg_blue = rgb[2];
}
context->animation->color_map[context->global_colormap_size * 3 + 0] = rgb[0];
context->animation->color_map[context->global_colormap_size * 3 + 1] = rgb[1];
context->animation->color_map[context->global_colormap_size * 3 + 2] = rgb[2];
context->global_colormap_size ++;
}
......@@ -432,184 +415,39 @@ static void
gif_set_get_lzw (GifContext *context)
{
context->state = GIF_GET_LZW;
context->draw_xpos = 0;
context->draw_ypos = 0;
context->draw_pass = 0;
}
static void
gif_fill_in_pixels (GifContext *context, guchar *dest, gint offset, guchar v)
{
guchar *pixel = NULL;
guchar (*cmap)[MAXCOLORMAPSIZE];
if (context->frame_cmap_active)
cmap = context->frame_color_map;
else
cmap = context->global_color_map;
if (context->gif89.transparent != -1) {
pixel = dest + (context->draw_ypos + offset) * gdk_pixbuf_get_rowstride (context->frame->pixbuf) + context->draw_xpos * 4;
*pixel = cmap [0][(guchar) v];
*(pixel+1) = cmap [1][(guchar) v];
*(pixel+2) = cmap [2][(guchar) v];
*(pixel+3) = (guchar) ((v == context->gif89.transparent) ? 0 : 255);
} else {
pixel = dest + (context->draw_ypos + offset) * gdk_pixbuf_get_rowstride (context->frame->pixbuf) + context->draw_xpos * 3;
*pixel = cmap [0][(guchar) v];
*(pixel+1) = cmap [1][(guchar) v];
*(pixel+2) = cmap [2][(guchar) v];
}
}
/* only called if progressive and interlaced */
static void
gif_fill_in_lines (GifContext *context, guchar *dest, guchar v)
{
switch (context->draw_pass) {
case 0:
if (context->draw_ypos > 4) {
gif_fill_in_pixels (context, dest, -4, v);
gif_fill_in_pixels (context, dest, -3, v);
}
if (context->draw_ypos < (context->frame_height - 4)) {
gif_fill_in_pixels (context, dest, 3, v);
gif_fill_in_pixels (context, dest, 4, v);
}
/* we don't need a break here. We draw the outer pixels first, then the
* inner ones, then the innermost ones. case 0 needs to draw all 3 bands.
* case 1, just the last two, and case 2 just draws the last one*/
case 1:
if (context->draw_ypos > 2)
gif_fill_in_pixels (context, dest, -2, v);
if (context->draw_ypos < (context->frame_height - 2))
gif_fill_in_pixels (context, dest, 2, v);
/* no break as above. */
case 2:
if (context->draw_ypos > 1)
gif_fill_in_pixels (context, dest, -1, v);
if (context->draw_ypos < (context->frame_height - 1))
gif_fill_in_pixels (context, dest, 1, v);
case 3:
default:
break;
}
}
/* Clips a rectancle to the base dimensions. Returns TRUE if the clipped rectangle is non-empty. */
static gboolean
clip_frame (GifContext *context,
gint *x,
gint *y,
gint *width,
gint *height)
{
gint orig_x, orig_y;
orig_x = *x;
orig_y = *y;
*x = MAX (0, *x);
*y = MAX (0, *y);
*width = MIN (context->width, orig_x + *width) - *x;
*height = MIN (context->height, orig_y + *height) - *y;
if (*width > 0 && *height > 0)
return TRUE;
/* The frame is completely off-bounds */