The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
#include "imgif.h"
#include <gif_lib.h>
#ifdef _MSC_VER
#include <io.h>
#else
#include <unistd.h>
#endif
#include <errno.h>
#include <string.h>
#include <stdlib.h>
#include <stdio.h>

/*
=head1 NAME

imgif.c - read and write gif files for Imager

=head1 SYNOPSIS

  i_img *img;
  i_img *imgs[count];
  int fd;
  int *colour_table,
  int colours;
  int max_colours; // number of bits per colour
  int pixdev;  // how much noise to add 
  i_color fixed[N]; // fixed palette entries 
  int fixedlen; // number of fixed colours 
  int success; // non-zero on success
  char *data; // a GIF file in memory
  int length; // how big data is 
  int reader(char *, char *, int, int);
  int writer(char *, char *, int);
  char *userdata; // user's data, whatever it is
  i_quantize quant;
  i_gif_opts opts;

  img = i_readgif(fd, &colour_table, &colours);
  success = i_writegif(img, fd, max_colours, pixdev, fixedlen, fixed);
  success = i_writegifmc(img, fd, max_colours);
  img = i_readgif_scalar(data, length, &colour_table, &colours);
  img = i_readgif_callback(cb, userdata, &colour_table, &colours);
  success = i_writegif_gen(&quant, fd, imgs, count, &opts);
  success = i_writegif_callback(&quant, writer, userdata, maxlength, 
                                imgs, count, &opts);

=head1 DESCRIPTION

This source file provides the C level interface to reading and writing
GIF files for Imager.

This has been tested with giflib 3 and 4, though you lose the callback
functionality with giflib3.

=head1 REFERENCE

=over

=cut
*/

#ifdef GIFLIB_MAJOR
#define IMGIFLIB_API_VERSION (GIFLIB_MAJOR * 100 + GIFLIB_MINOR)
#else
/* only matters for pre-5.0 which we either reject, or which contains
   no significant API changes */
#define IMGIFLIB_API_VERSION 0
#endif

#if IMGIFLIB_API_VERSION >= 500
#define POST_SET_VERSION
#define myDGifOpen(userPtr, readFunc, Error) DGifOpen((userPtr), (readFunc), (Error))
#define myEGifOpen(userPtr, readFunc, Error) EGifOpen((userPtr), (readFunc), (Error))
#define myGifError(gif) ((gif)->Error)
#define MakeMapObject GifMakeMapObject
#define FreeMapObject GifFreeMapObject
#define gif_mutex_lock(mutex)
#define gif_mutex_unlock(mutex)
#else
#define PRE_SET_VERSION
static GifFileType *
myDGifOpen(void *userPtr, InputFunc readFunc, int *error) {
  GifFileType *result = DGifOpen(userPtr, readFunc);
  if (!result)
    *error = GifLastError();

  return result;
}
static GifFileType *
myEGifOpen(void *userPtr, OutputFunc outputFunc, int *error) {
  GifFileType *result = EGifOpen(userPtr, outputFunc);
  if (!result)
    *error = GifLastError();

  return result;
}
#define myGifError(gif) GifLastError()
#define gif_mutex_lock(mutex) i_mutex_lock(mutex)
#define gif_mutex_unlock(mutex) i_mutex_unlock(mutex)

#endif

#if IMGIFLIB_API_VERSION >= 501
#define myDGifCloseFile(gif, perror) (DGifCloseFile((gif), (perror)))
#define myEGifCloseFile(gif, perror) (EGifCloseFile((gif), (perror)))
#else
static int
myDGifCloseFile(GifFileType *GifFile, int *ErrorCode) {
  int result = DGifCloseFile(GifFile);
  if (result == GIF_ERROR) {
    if (ErrorCode)
      *ErrorCode = myGifError(GifFile);
    free(GifFile->Private);
    free(GifFile);
  }

  return result;
}

static int
myEGifCloseFile(GifFileType *GifFile, int *ErrorCode) {
  int result = EGifCloseFile(GifFile);
  if (result == GIF_ERROR) {
    if (ErrorCode)
      *ErrorCode = myGifError(GifFile);
    free(GifFile->Private);
    free(GifFile);
  }

  return result;
}
#endif

static char const *gif_error_msg(int code);
static void gif_push_error(int code);

/* Make some variables global, so we could access them faster: */

static const int
  InterlacedOffset[] = { 0, 4, 2, 1 }, /* The way Interlaced image should. */
  InterlacedJumps[] = { 8, 8, 4, 2 };    /* be read - offsets and jumps... */

#if IMGIFLIB_API_VERSION < 500
static i_mutex_t mutex;
#endif

void
i_init_gif(void) {
#if IMGIFLIB_API_VERSION < 500
  mutex = i_mutex_new();
#endif
}

static
void
i_colortable_copy(int **colour_table, int *colours, ColorMapObject *colourmap) {
  GifColorType *mapentry;
  int q;
  int colourmapsize = colourmap->ColorCount;

  if(colours) *colours = colourmapsize;
  if(!colour_table) return;
  
  *colour_table = mymalloc(sizeof(int) * colourmapsize * 3);
  memset(*colour_table, 0, sizeof(int) * colourmapsize * 3);

  for(q=0; q<colourmapsize; q++) {
    mapentry = &colourmap->Colors[q];
    (*colour_table)[q*3 + 0] = mapentry->Red;
    (*colour_table)[q*3 + 1] = mapentry->Green;
    (*colour_table)[q*3 + 2] = mapentry->Blue;
  }
}

#ifdef GIF_LIB_VERSION

static const
char gif_version_str[] = GIF_LIB_VERSION;

double
i_giflib_version(void) {
  const char *p = gif_version_str;

  while (*p && (*p < '0' || *p > '9'))
    ++p;

  if (!*p)
    return 0;

  return strtod(p, NULL);
}

#else

double
i_giflib_version(void) {
  return GIFLIB_MAJOR + GIFLIB_MINOR * 0.1;
}

#endif

/*
=item i_readgif_low(GifFileType *GifFile, int **colour_table, int *colours)

Internal.  Low-level function for reading a GIF file.  The caller must
create the appropriate GifFileType object and pass it in.

=cut
*/

i_img *
i_readgif_low(GifFileType *GifFile, int **colour_table, int *colours) {
  i_img *im;
  int i, j, Size, Row, Col, Width, Height, ExtCode, Count, x;
  int cmapcnt = 0, ImageNum = 0;
  ColorMapObject *ColorMap;
 
  GifRecordType RecordType;
  GifByteType *Extension;
  
  GifRowType GifRow;
  GifColorType *ColorMapEntry;
  i_color col;
  int error;

  mm_log((1,"i_readgif_low(GifFile %p, colour_table %p, colours %p)\n", GifFile, colour_table, colours));

  /* it's possible that the caller has called us with *colour_table being
     non-NULL, but we check that to see if we need to free an allocated
     colour table on error.
  */
  if (colour_table) *colour_table = NULL;

  ColorMap = (GifFile->Image.ColorMap ? GifFile->Image.ColorMap : GifFile->SColorMap);

  if (ColorMap) {
    i_colortable_copy(colour_table, colours, ColorMap);
    cmapcnt++;
  }
  
  if (!i_int_check_image_file_limits(GifFile->SWidth, GifFile->SHeight, 3, sizeof(i_sample_t))) {
    if (colour_table && *colour_table) {
      myfree(*colour_table);
      *colour_table = NULL;
    }
    (void)myDGifCloseFile(GifFile, NULL);
    mm_log((1, "i_readgif: image size exceeds limits\n"));
    return NULL;
  }

  im = i_img_8_new(GifFile->SWidth, GifFile->SHeight, 3);
  if (!im) {
    if (colour_table && *colour_table) {
      myfree(*colour_table);
      *colour_table = NULL;
    }
    (void)myDGifCloseFile(GifFile, NULL);
    return NULL;
  }

  Size = GifFile->SWidth * sizeof(GifPixelType); 
  
  GifRow = mymalloc(Size);

  for (i = 0; i < GifFile->SWidth; i++) GifRow[i] = GifFile->SBackGroundColor;
  
  /* Scan the content of the GIF file and load the image(s) in: */
  do {
    if (DGifGetRecordType(GifFile, &RecordType) == GIF_ERROR) {
      gif_push_error(myGifError(GifFile));
      i_push_error(0, "Unable to get record type");
      if (colour_table && *colour_table) {
	myfree(*colour_table);
	*colour_table = NULL;
      }
      myfree(GifRow);
      i_img_destroy(im);
      (void)myDGifCloseFile(GifFile, NULL);
      return NULL;
    }
    
    switch (RecordType) {
    case IMAGE_DESC_RECORD_TYPE:
      if (DGifGetImageDesc(GifFile) == GIF_ERROR) {
	gif_push_error(myGifError(GifFile));
	i_push_error(0, "Unable to get image descriptor");
	if (colour_table && *colour_table) {
	  myfree(*colour_table);
	  *colour_table = NULL;
	}
	myfree(GifRow);
	i_img_destroy(im);
	(void)myDGifCloseFile(GifFile, NULL);
	return NULL;
      }

      if (( ColorMap = (GifFile->Image.ColorMap ? GifFile->Image.ColorMap : GifFile->SColorMap) )) {
	mm_log((1, "Adding local colormap\n"));
	if ( cmapcnt == 0) {
	  i_colortable_copy(colour_table, colours, ColorMap);
	  cmapcnt++;
	}
      } else {
	/* No colormap and we are about to read in the image - abandon for now */
	mm_log((1, "Going in with no colormap\n"));
	i_push_error(0, "Image does not have a local or a global color map");
	/* we can't have allocated a colour table here */
	myfree(GifRow);
	i_img_destroy(im);
	(void)myDGifCloseFile(GifFile, NULL);
	return NULL;
      }
      
      Row = GifFile->Image.Top; /* Image Position relative to Screen. */
      Col = GifFile->Image.Left;
      Width = GifFile->Image.Width;
      Height = GifFile->Image.Height;
      ImageNum++;
      mm_log((1,"i_readgif_low: Image %d at (%d, %d) [%dx%d]: \n",ImageNum, Col, Row, Width, Height));

      if (GifFile->Image.Left + GifFile->Image.Width > GifFile->SWidth ||
	  GifFile->Image.Top + GifFile->Image.Height > GifFile->SHeight) {
	i_push_errorf(0, "Image %d is not confined to screen dimension, aborted.\n",ImageNum);
	if (colour_table && *colour_table) {
	  myfree(*colour_table);
	  *colour_table = NULL;
	}
	myfree(GifRow);
	i_img_destroy(im);
	(void)myDGifCloseFile(GifFile, NULL);
	return NULL;
      }
      if (GifFile->Image.Interlace) {

	for (Count = i = 0; i < 4; i++) for (j = Row + InterlacedOffset[i]; j < Row + Height; j += InterlacedJumps[i]) {
	  Count++;
	  if (DGifGetLine(GifFile, GifRow, Width) == GIF_ERROR) {
	    gif_push_error(myGifError(GifFile));
	    i_push_error(0, "Reading GIF line");
	    if (colour_table && *colour_table) {
	      myfree(*colour_table);
	      *colour_table = NULL;
	    }
	    myfree(GifRow);
	    i_img_destroy(im);
	    (void)myDGifCloseFile(GifFile, NULL);
	    return NULL;
	  }
	  
	  for (x = 0; x < Width; x++) {
	    ColorMapEntry = &ColorMap->Colors[GifRow[x]];
	    col.rgb.r = ColorMapEntry->Red;
	    col.rgb.g = ColorMapEntry->Green;
	    col.rgb.b = ColorMapEntry->Blue;
	    i_ppix(im,Col+x,j,&col);
	  }
	  
	}
      }
      else {
	for (i = 0; i < Height; i++) {
	  if (DGifGetLine(GifFile, GifRow, Width) == GIF_ERROR) {
	    gif_push_error(myGifError(GifFile));
	    i_push_error(0, "Reading GIF line");
	    if (colour_table && *colour_table) {
	      myfree(*colour_table);
	      *colour_table = NULL;
	    }
	    myfree(GifRow);
	    i_img_destroy(im);
	    (void)myDGifCloseFile(GifFile, NULL);
	    return NULL;
	  }

	  for (x = 0; x < Width; x++) {
	    ColorMapEntry = &ColorMap->Colors[GifRow[x]];
	    col.rgb.r = ColorMapEntry->Red;
	    col.rgb.g = ColorMapEntry->Green;
	    col.rgb.b = ColorMapEntry->Blue;
	    i_ppix(im, Col+x, Row, &col);
	  }
	  Row++;
	}
      }
      break;
    case EXTENSION_RECORD_TYPE:
      /* Skip any extension blocks in file: */
      if (DGifGetExtension(GifFile, &ExtCode, &Extension) == GIF_ERROR) {
	gif_push_error(myGifError(GifFile));
	i_push_error(0, "Reading extension record");
	if (colour_table && *colour_table) {
	  myfree(*colour_table);
	  *colour_table = NULL;
	}
	myfree(GifRow);
	i_img_destroy(im);
	(void)myDGifCloseFile(GifFile, NULL);
	return NULL;
      }
      while (Extension != NULL) {
	if (DGifGetExtensionNext(GifFile, &Extension) == GIF_ERROR) {
	  gif_push_error(myGifError(GifFile));
	  i_push_error(0, "reading next block of extension");
	  if (colour_table && *colour_table) {
	    myfree(*colour_table);
	    *colour_table = NULL;
	  }
	  myfree(GifRow);
	  i_img_destroy(im);
	  (void)myDGifCloseFile(GifFile, NULL);
	  return NULL;
	}
      }
      break;
    case TERMINATE_RECORD_TYPE:
      break;
    default:		    /* Should be traps by DGifGetRecordType. */
      break;
    }
  } while (RecordType != TERMINATE_RECORD_TYPE);
  
  myfree(GifRow);
  
  if (myDGifCloseFile(GifFile, &error) == GIF_ERROR) {
    gif_push_error(error);
    i_push_error(0, "Closing GIF file object");
    if (colour_table && *colour_table) {
      myfree(*colour_table);
      *colour_table = NULL;
    }
    i_img_destroy(im);
    return NULL;
  }

  i_tags_set(&im->tags, "i_format", "gif", -1);

  return im;
}

/*

Internal function called by i_readgif_multi_low() in error handling

*/
static void
free_images(i_img **imgs, int count) {
  int i;
  
  if (count) {
    for (i = 0; i < count; ++i)
      i_img_destroy(imgs[i]);
    myfree(imgs);
  }
}

/*
=item i_readgif_multi_low(GifFileType *gf, int *count, int page)

Reads one of more gif images from the given GIF file.

Returns a pointer to an array of i_img *, and puts the count into 
*count.

If page is not -1 then the given image _only_ is returned from the
file, where the first image is 0, the second 1 and so on.

Unlike the normal i_readgif*() functions the images are paletted
images rather than a combined RGB image.

This functions sets tags on the images returned:

=over

=item gif_left

the offset of the image from the left of the "screen" ("Image Left
Position")

=item gif_top

the offset of the image from the top of the "screen" ("Image Top Position")

=item gif_interlace

non-zero if the image was interlaced ("Interlace Flag")

=item gif_screen_width

=item gif_screen_height

the size of the logical screen ("Logical Screen Width", 
"Logical Screen Height")

=item gif_local_map

Non-zero if this image had a local color map.

=item gif_background

The index in the global colormap of the logical screen's background
color.  This is only set if the current image uses the global
colormap.

=item gif_trans_index

The index of the color in the colormap used for transparency.  If the
image has a transparency then it is returned as a 4 channel image with
the alpha set to zero in this palette entry. ("Transparent Color Index")

=item gif_delay

The delay until the next frame is displayed, in 1/100 of a second. 
("Delay Time").

=item gif_user_input

whether or not a user input is expected before continuing (view dependent) 
("User Input Flag").

=item gif_disposal

how the next frame is displayed ("Disposal Method")

=item gif_loop

the number of loops from the Netscape Loop extension.  This may be zero.

=item gif_comment

the first block of the first gif comment before each image.

=back

Where applicable, the ("name") is the name of that field from the GIF89 
standard.

=cut
*/

i_img **
i_readgif_multi_low(GifFileType *GifFile, int *count, int page) {
  i_img *img;
  int i, j, Size, Width, Height, ExtCode, Count;
  int ImageNum = 0, ColorMapSize = 0;
  ColorMapObject *ColorMap;
 
  GifRecordType RecordType;
  GifByteType *Extension;
  
  GifRowType GifRow;
  int got_gce = 0;
  int trans_index = 0; /* transparent index if we see a GCE */
  int gif_delay = 0; /* delay from a GCE */
  int user_input = 0; /* user input flag from a GCE */
  int disposal = 0; /* disposal method from a GCE */
  int got_ns_loop = 0;
  int ns_loop = 0;
  char *comment = NULL; /* a comment */
  i_img **results = NULL;
  int result_alloc = 0;
  int channels;
  int image_colors = 0;
  i_color black; /* used to expand the palette if needed */
  int error;

  for (i = 0; i < MAXCHANNELS; ++i)
    black.channel[i] = 0;
  
  *count = 0;

  mm_log((1,"i_readgif_multi_low(GifFile %p, , count %p)\n", GifFile, count));

  Size = GifFile->SWidth * sizeof(GifPixelType);
  
  GifRow = (GifRowType) mymalloc(Size);

  /* Scan the content of the GIF file and load the image(s) in: */
  do {
    if (DGifGetRecordType(GifFile, &RecordType) == GIF_ERROR) {
      gif_push_error(myGifError(GifFile));
      i_push_error(0, "Unable to get record type");
      free_images(results, *count);
      (void)myDGifCloseFile(GifFile, NULL);
      myfree(GifRow);
      if (comment)
	myfree(comment);
      return NULL;
    }
    
    switch (RecordType) {
    case IMAGE_DESC_RECORD_TYPE:
      if (DGifGetImageDesc(GifFile) == GIF_ERROR) {
	gif_push_error(myGifError(GifFile));
	i_push_error(0, "Unable to get image descriptor");
        free_images(results, *count);
	(void)myDGifCloseFile(GifFile, NULL);
	myfree(GifRow);
	if (comment)
	  myfree(comment);
	return NULL;
      }

      Width = GifFile->Image.Width;
      Height = GifFile->Image.Height;
      if (page == -1 || page == ImageNum) {
	if (( ColorMap = (GifFile->Image.ColorMap ? GifFile->Image.ColorMap : GifFile->SColorMap) )) {
	  mm_log((1, "Adding local colormap\n"));
	  ColorMapSize = ColorMap->ColorCount;
	} else {
	  /* No colormap and we are about to read in the image - 
	     abandon for now */
	  mm_log((1, "Going in with no colormap\n"));
	  i_push_error(0, "Image does not have a local or a global color map");
	  free_images(results, *count);
	  (void)myDGifCloseFile(GifFile, NULL);
	  myfree(GifRow);
	  if (comment)
	    myfree(comment);
	  return NULL;
	}
	
	channels = 3;
	if (got_gce && trans_index >= 0)
	  channels = 4;
	if (!i_int_check_image_file_limits(Width, Height, channels, sizeof(i_sample_t))) {
	  free_images(results, *count);
	  mm_log((1, "i_readgif: image size exceeds limits\n"));
	  (void)myDGifCloseFile(GifFile, NULL);
	  myfree(GifRow);
	  if (comment)
	    myfree(comment);
	  return NULL;
	}
	img = i_img_pal_new(Width, Height, channels, 256);
	if (!img) {
	  free_images(results, *count);
	  (void)myDGifCloseFile(GifFile, NULL);
	  if (comment)
	    myfree(comment);
	  myfree(GifRow);
	  return NULL;
	}
	/* populate the palette of the new image */
	mm_log((1, "ColorMapSize %d\n", ColorMapSize));
	for (i = 0; i < ColorMapSize; ++i) {
	  i_color col;
	  col.rgba.r = ColorMap->Colors[i].Red;
	  col.rgba.g = ColorMap->Colors[i].Green;
	  col.rgba.b = ColorMap->Colors[i].Blue;
	  if (channels == 4 && trans_index == i)
	    col.rgba.a = 0;
	  else
	    col.rgba.a = 255;
	  
	  i_addcolors(img, &col, 1);
	}
	image_colors = ColorMapSize;
	++*count;
	if (*count > result_alloc) {
	  if (result_alloc == 0) {
	    result_alloc = 5;
	    results = mymalloc(result_alloc * sizeof(i_img *));
	  }
	  else {
	    /* myrealloc never fails (it just dies if it can't allocate) */
	    result_alloc *= 2;
	    results = myrealloc(results, result_alloc * sizeof(i_img *));
	  }
	}
	results[*count-1] = img;
	i_tags_set(&img->tags, "i_format", "gif", -1);
	i_tags_setn(&img->tags, "gif_left", GifFile->Image.Left);
	/**(char *)0 = 1;*/
	i_tags_setn(&img->tags, "gif_top",  GifFile->Image.Top);
	i_tags_setn(&img->tags, "gif_interlace", GifFile->Image.Interlace);
	i_tags_setn(&img->tags, "gif_screen_width", GifFile->SWidth);
	i_tags_setn(&img->tags, "gif_screen_height", GifFile->SHeight);
	i_tags_setn(&img->tags, "gif_colormap_size", ColorMapSize);
	if (GifFile->SColorMap && !GifFile->Image.ColorMap) {
	  i_tags_setn(&img->tags, "gif_background",
		      GifFile->SBackGroundColor);
	}
	if (GifFile->Image.ColorMap) {
	  i_tags_setn(&img->tags, "gif_localmap", 1);
	}
	if (got_gce) {
	  if (trans_index >= 0) {
	    i_color trans;
	    i_tags_setn(&img->tags, "gif_trans_index", trans_index);
	    i_getcolors(img, trans_index, &trans, 1);
	    i_tags_set_color(&img->tags, "gif_trans_color", 0, &trans);
	  }
	  i_tags_setn(&img->tags, "gif_delay", gif_delay);
	  i_tags_setn(&img->tags, "gif_user_input", user_input);
	  i_tags_setn(&img->tags, "gif_disposal", disposal);
	}
	got_gce = 0;
	if (got_ns_loop)
	  i_tags_setn(&img->tags, "gif_loop", ns_loop);
	if (comment) {
	  i_tags_set(&img->tags, "gif_comment", comment, strlen(comment));
	  myfree(comment);
	  comment = NULL;
	}
	
	mm_log((1,"i_readgif_multi_low: Image %d at (%d, %d) [%dx%d]: \n",
		ImageNum, GifFile->Image.Left, GifFile->Image.Top, Width, Height));
	
	if (GifFile->Image.Left + GifFile->Image.Width > GifFile->SWidth ||
	    GifFile->Image.Top + GifFile->Image.Height > GifFile->SHeight) {
	  i_push_errorf(0, "Image %d is not confined to screen dimension, aborted.\n",ImageNum);
	  free_images(results, *count);        
	  (void)myDGifCloseFile(GifFile, NULL);
	  myfree(GifRow);
	  if (comment)
	    myfree(comment);
	  return(0);
	}
	
	if (GifFile->Image.Interlace) {
	  for (Count = i = 0; i < 4; i++) {
	    for (j = InterlacedOffset[i]; j < Height; 
		 j += InterlacedJumps[i]) {
	      Count++;
	      if (DGifGetLine(GifFile, GifRow, Width) == GIF_ERROR) {
		gif_push_error(myGifError(GifFile));
		i_push_error(0, "Reading GIF line");
		free_images(results, *count);
		(void)myDGifCloseFile(GifFile, NULL);
		myfree(GifRow);
		if (comment)
		  myfree(comment);
		return NULL;
	      }

	      /* range check the scanline if needed */
	      if (image_colors != 256) {
		int x;
		for (x = 0; x < Width; ++x) {
		  while (GifRow[x] >= image_colors) {
		    /* expand the palette since a palette index is too big */
		    i_addcolors(img, &black, 1);
		    ++image_colors;
		  }
		}
	      }

	      i_ppal(img, 0, Width, j, GifRow);
	    }
	  }
	}
	else {
	  for (i = 0; i < Height; i++) {
	    if (DGifGetLine(GifFile, GifRow, Width) == GIF_ERROR) {
	      gif_push_error(myGifError(GifFile));
	      i_push_error(0, "Reading GIF line");
	      free_images(results, *count);
	      (void)myDGifCloseFile(GifFile, NULL);
	      myfree(GifRow);
	      if (comment)
		myfree(comment);
	      return NULL;
	    }
	    
	    /* range check the scanline if needed */
	    if (image_colors != 256) {
	      int x;
	      for (x = 0; x < Width; ++x) {
		while (GifRow[x] >= image_colors) {
		  /* expand the palette since a palette index is too big */
		  i_addcolors(img, &black, 1);
		  ++image_colors;
		}
	      }
	    }

	    i_ppal(img, 0, Width, i, GifRow);
	  }
	}

	/* must be only one image wanted and that was it */
	if (page != -1) {
	  myfree(GifRow);
	  (void)myDGifCloseFile(GifFile, NULL);
	  if (comment)
	    myfree(comment);
	  return results;
	}
      }
      else {
	/* skip the image */
	/* whether interlaced or not, it has the same number of lines */
	/* giflib does't have an interface to skip the image data */
	for (i = 0; i < Height; i++) {
	  if (DGifGetLine(GifFile, GifRow, Width) == GIF_ERROR) {
	    gif_push_error(myGifError(GifFile));
	    i_push_error(0, "Reading GIF line");
	    free_images(results, *count);
	    myfree(GifRow);
	    (void)myDGifCloseFile(GifFile, NULL);
	    if (comment) 
	      myfree(comment);
	    return NULL;
	  }
	}

	/* kill the comment so we get the right comment for the page */
	if (comment) {
	  myfree(comment);
	  comment = NULL;
	}
      }
      ImageNum++;
      break;
    case EXTENSION_RECORD_TYPE:
      /* Skip any extension blocks in file: */
      if (DGifGetExtension(GifFile, &ExtCode, &Extension) == GIF_ERROR) {
	gif_push_error(myGifError(GifFile));
	i_push_error(0, "Reading extension record");
        free_images(results, *count);
	myfree(GifRow);
	(void)myDGifCloseFile(GifFile, NULL);
	if (comment)
	  myfree(comment);
	return NULL;
      }
      /* possibly this should be an error, but "be liberal in what you accept" */
      if (!Extension)
	break;
      if (ExtCode == 0xF9) {
        got_gce = 1;
        if (Extension[1] & 1)
          trans_index = Extension[4];
        else
          trans_index = -1;
        gif_delay = Extension[2] + 256 * Extension[3];
        user_input = (Extension[1] & 2) != 0;
        disposal = (Extension[1] >> 2) & 7;
      }
      if (ExtCode == 0xFF && *Extension == 11) {
        if (memcmp(Extension+1, "NETSCAPE2.0", 11) == 0) {
          if (DGifGetExtensionNext(GifFile, &Extension) == GIF_ERROR) {
            gif_push_error(myGifError(GifFile));
            i_push_error(0, "reading loop extension");
            free_images(results, *count);
	    myfree(GifRow);
	    (void)myDGifCloseFile(GifFile, NULL);
	    if (comment)
	      myfree(comment);
            return NULL;
          }
          if (Extension && *Extension == 3) {
            got_ns_loop = 1;
            ns_loop = Extension[2] + 256 * Extension[3];
          }
        }
      }
      else if (ExtCode == 0xFE) {
        /* while it's possible for a GIF file to contain more than one
           comment, I'm only implementing a single comment per image, 
           with the comment saved into the following image.
           If someone wants more than that they can implement it.
           I also don't handle comments that take more than one block.
        */
        if (!comment) {
          comment = mymalloc(*Extension+1);
          memcpy(comment, Extension+1, *Extension);
          comment[*Extension] = '\0';
        }
      }
      while (Extension != NULL) {
	if (DGifGetExtensionNext(GifFile, &Extension) == GIF_ERROR) {
	  gif_push_error(myGifError(GifFile));
	  i_push_error(0, "reading next block of extension");
          free_images(results, *count);
	  myfree(GifRow);
	  (void)myDGifCloseFile(GifFile, NULL);
	  if (comment)
	    myfree(comment);
	  return NULL;
	}
      }
      break;
    case TERMINATE_RECORD_TYPE:
      break;
    default:		    /* Should be trapped by DGifGetRecordType. */
      break;
    }
  } while (RecordType != TERMINATE_RECORD_TYPE);

  if (comment) {
    if (*count) {
      i_tags_set(&(results[*count-1]->tags), "gif_comment", comment, 
                 strlen(comment));
    }
    myfree(comment);
  }
  
  myfree(GifRow);
  
  if (myDGifCloseFile(GifFile, &error) == GIF_ERROR) {
    gif_push_error(error);
    i_push_error(0, "Closing GIF file object");
    free_images(results, *count);
    return NULL;
  }

  if (ImageNum && page != -1) {
    /* there were images, but the page selected wasn't found */
    i_push_errorf(0, "page %d not found (%d total)", page, ImageNum);
    free_images(results, *count);
    return NULL;
  }

  return results;
}

static int io_glue_read_cb(GifFileType *gft, GifByteType *buf, int length);

/*
=item i_readgif_multi_wiol(ig, int *count)

=cut
*/

i_img **
i_readgif_multi_wiol(io_glue *ig, int *count) {
  GifFileType *GifFile;
  int gif_error;
  i_img **result;

  gif_mutex_lock(mutex);

  i_clear_error();
  
  if ((GifFile = myDGifOpen((void *)ig, io_glue_read_cb, &gif_error )) == NULL) {
    gif_push_error(gif_error);
    i_push_error(0, "Cannot create giflib callback object");
    mm_log((1,"i_readgif_multi_wiol: Unable to open callback datasource.\n"));
    gif_mutex_unlock(mutex);
    return NULL;
  }
    
  result = i_readgif_multi_low(GifFile, count, -1);

  gif_mutex_unlock(mutex);

  return result;
}

static int
io_glue_read_cb(GifFileType *gft, GifByteType *buf, int length) {
  io_glue *ig = (io_glue *)gft->UserData;

  return i_io_read(ig, buf, length);
}

i_img *
i_readgif_wiol(io_glue *ig, int **color_table, int *colors) {
  GifFileType *GifFile;
  int gif_error;
  i_img *result;

  gif_mutex_lock(mutex);

  i_clear_error();

  if ((GifFile = myDGifOpen((void *)ig, io_glue_read_cb, &gif_error )) == NULL) {
    gif_push_error(gif_error);
    i_push_error(0, "Cannot create giflib callback object");
    mm_log((1,"i_readgif_wiol: Unable to open callback datasource.\n"));
    gif_mutex_unlock(mutex);
    return NULL;
  }
    
  result = i_readgif_low(GifFile, color_table, colors);

  gif_mutex_unlock(mutex);

  return result;
}

/*
=item i_readgif_single_low(GifFile, page)

Lower level function to read a single image from a GIF.

page must be non-negative.

=cut
*/
static i_img *
i_readgif_single_low(GifFileType *GifFile, int page) {
  int count = 0;
  i_img **imgs;

  imgs = i_readgif_multi_low(GifFile, &count, page);

  if (imgs && count) {
    i_img *result = imgs[0];

    myfree(imgs);
    return result;
  }
  else {
    /* i_readgif_multi_low() handles the errors appropriately */
    return NULL;
  }
}

/*
=item i_readgif_single_wiol(ig, page)

Read a single page from a GIF image file, where the page is indexed
from 0.

Returns NULL if the page isn't found.

=cut
*/

i_img *
i_readgif_single_wiol(io_glue *ig, int page) {
  GifFileType *GifFile;
  int gif_error;
  i_img *result;

  i_clear_error();
  if (page < 0) {
    i_push_error(0, "page must be non-negative");
    return NULL;
  }

  gif_mutex_lock(mutex);

  if ((GifFile = myDGifOpen((void *)ig, io_glue_read_cb, &gif_error )) == NULL) {
    gif_push_error(gif_error);
    i_push_error(0, "Cannot create giflib callback object");
    mm_log((1,"i_readgif_wiol: Unable to open callback datasource.\n"));
    gif_mutex_unlock(mutex);
    return NULL;
  }
    
  result = i_readgif_single_low(GifFile, page);

  gif_mutex_unlock(mutex);

  return result;
}

/*
=item do_write(GifFileType *gf, i_gif_opts *opts, i_img *img, i_palidx *data)

Internal.  Low level image write function.  Writes in interlace if
that was requested in the GIF options.

Returns non-zero on success.

=cut
*/
static undef_int 
do_write(GifFileType *gf, int interlace, i_img *img, i_palidx *data) {
  if (interlace) {
    int i, j;
    for (i = 0; i < 4; ++i) {
      for (j = InterlacedOffset[i]; j < img->ysize; j += InterlacedJumps[i]) {
	if (EGifPutLine(gf, data+j*img->xsize, img->xsize) == GIF_ERROR) {
	  gif_push_error(myGifError(gf));
	  i_push_error(0, "Could not save image data:");
	  mm_log((1, "Error in EGifPutLine\n"));
	  return 0;
	}
      }
    }
  }
  else {
    int y;
    for (y = 0; y < img->ysize; ++y) {
      if (EGifPutLine(gf, data, img->xsize) == GIF_ERROR) {
	gif_push_error(myGifError(gf));
	i_push_error(0, "Could not save image data:");
	mm_log((1, "Error in EGifPutLine\n"));
	return 0;
      }
      data += img->xsize;
    }
  }

  return 1;
}

/*
=item do_gce(GifFileType *gf, int index, i_gif_opts *opts, int want_trans, int trans_index)

Internal. Writes the GIF graphics control extension, if necessary.

Returns non-zero on success.

=cut
*/

static int
do_gce(GifFileType *gf, i_img *img, int want_trans, int trans_index)
{
  unsigned char gce[4] = {0};
  int want_gce = 0;
  int delay;
  int user_input;
  int disposal_method;

  if (want_trans) {
    gce[0] |= 1;
    gce[3] = trans_index;
    ++want_gce;
  }
  if (i_tags_get_int(&img->tags, "gif_delay", 0, &delay)) {
    gce[1] = delay % 256;
    gce[2] = delay / 256;
    ++want_gce;
  }
  if (i_tags_get_int(&img->tags, "gif_user_input", 0, &user_input) 
      && user_input) {
    gce[0] |= 2;
    ++want_gce;
  }
  if (i_tags_get_int(&img->tags, "gif_disposal", 0, &disposal_method)) {
    gce[0] |= (disposal_method & 3) << 2;
    ++want_gce;
  }
  if (want_gce) {
    if (EGifPutExtension(gf, 0xF9, sizeof(gce), gce) == GIF_ERROR) {
      gif_push_error(myGifError(gf));
      i_push_error(0, "Could not save GCE");
    }
  }
  return 1;
}

/*
=item do_comments(gf, img)

Write any comments in the image.

=cut
*/

static int
do_comments(GifFileType *gf, i_img *img) {
  int pos = -1;

  while (i_tags_find(&img->tags, "gif_comment", pos+1, &pos)) {
    if (img->tags.tags[pos].data) {
      if (EGifPutComment(gf, img->tags.tags[pos].data) == GIF_ERROR) {
        return 0;
      }
    }
    else {
      char buf[50];
#ifdef IMAGER_SNPRINTF
      snprintf(buf, sizeof(buf), "%d", img->tags.tags[pos].idata);
#else
      sprintf(buf, "%d", img->tags.tags[pos].idata);
#endif
      if (EGifPutComment(gf, buf) == GIF_ERROR) {
        return 0;
      }
    }
  }

  return 1;
}

/*
=item do_ns_loop(GifFileType *gf, i_gif_opts *opts)

Internal.  Add the Netscape2.0 loop extension block, if requested.

Giflib/libungif prior to 4.1.1 didn't support writing application
extension blocks, so we don't attempt to write them for older versions.

Giflib/libungif prior to 4.1.3 used the wrong write mechanism when
writing extension blocks so that they could only be written to files.

=cut
*/

static int
do_ns_loop(GifFileType *gf, i_img *img)
{
  /* EGifPutExtension() doesn't appear to handle application 
     extension blocks in any way
     Since giflib wraps the fd with a FILE * (and puts that in its
     private data), we can't do an end-run and write the data 
     directly to the fd.
     There's no open interface that takes a FILE * either, so we 
     can't workaround it that way either.
     If giflib's callback interface wasn't broken by default, I'd 
     force file writes to use callbacks, but it is broken by default.
  */
  /* yes this was another attempt at supporting the loop extension */
  int loop_count;
  if (i_tags_get_int(&img->tags, "gif_loop", 0, &loop_count)) {
    unsigned char nsle[12] = "NETSCAPE2.0";
    unsigned char subblock[3];

    subblock[0] = 1;
    subblock[1] = loop_count % 256;
    subblock[2] = loop_count / 256;

#if IMGIFLIB_API_VERSION >= 500
    if (EGifPutExtensionLeader(gf, APPLICATION_EXT_FUNC_CODE) == GIF_ERROR
	|| EGifPutExtensionBlock(gf, 11, nsle) == GIF_ERROR
	|| EGifPutExtensionBlock(gf, 3, subblock) == GIF_ERROR
	|| EGifPutExtensionTrailer(gf) == GIF_ERROR) {
      gif_push_error(myGifError(gf));
      i_push_error(0, "writing loop extension");
      return 0;
    }
	
#else
    if (EGifPutExtensionFirst(gf, APPLICATION_EXT_FUNC_CODE, 11, nsle) == GIF_ERROR) {
      gif_push_error(myGifError(gf));
      i_push_error(0, "writing loop extension");
      return 0;
    }
    if (EGifPutExtensionLast(gf, APPLICATION_EXT_FUNC_CODE, 3, subblock) == GIF_ERROR) {
      gif_push_error(myGifError(gf));
      i_push_error(0, "writing loop extension sub-block");
      return 0;
    }
#endif
  }

  return 1;
}

/*
=item make_gif_map(i_quantize *quant, int want_trans)

Create a giflib color map object from an Imager color map.

=cut
*/

static ColorMapObject *
make_gif_map(i_quantize *quant, i_img *img, int want_trans) {
  GifColorType colors[256];
  int i;
  int size = quant->mc_count;
  int map_size;
  ColorMapObject *map;
  i_color trans;

  for (i = 0; i < quant->mc_count; ++i) {
    colors[i].Red = quant->mc_colors[i].rgb.r;
    colors[i].Green = quant->mc_colors[i].rgb.g;
    colors[i].Blue = quant->mc_colors[i].rgb.b;
  }
  if (want_trans) {
    if (!i_tags_get_color(&img->tags, "gif_trans_color", 0, &trans))
      trans.rgb.r = trans.rgb.g = trans.rgb.b = 0;
    colors[size].Red = trans.rgb.r;
    colors[size].Green = trans.rgb.g;
    colors[size].Blue = trans.rgb.b;
    ++size;
  }
  map_size = 1;
  while (map_size < size)
    map_size <<= 1;
  /* giflib spews for 1 colour maps, reasonable, I suppose */
  if (map_size == 1)
    map_size = 2;
  while (i < map_size) {
    colors[i].Red = colors[i].Green = colors[i].Blue = 0;
    ++i;
  }
  
  map = MakeMapObject(map_size, colors);
  mm_log((1, "XXX map is at %p and colors at %p\n", map, map->Colors));
  if (!map) {
    i_push_error(0, "Could not create color map object");
    return NULL;
  }
#if IMGIFLIB_API_VERSION >= 500
  map->SortFlag = 0;
#endif
  return map;
}

/*
=item need_version_89a(i_quantize *quant, i_img *imgs, int count)

Return true if the file we're creating on these images needs a GIF89a
header.

=cut
*/

static int
need_version_89a(i_quantize *quant, i_img **imgs, int count) {
  int need_89a = 0;
  int temp;
  int i;

  for (i = 0; i < count; ++i) {
    if (quant->transp != tr_none &&
	(imgs[i]->channels == 2 || imgs[i]->channels == 4)) {
      need_89a = 1;
      break;
    }
    if (i_tags_get_int(&imgs[i]->tags, "gif_delay", 0, &temp)) {
      need_89a = 1;
      break;
    }
    if (i_tags_get_int(&imgs[i]->tags, "gif_user_input", 0, &temp) && temp) {
      need_89a = 1; 
      break;
    }
    if (i_tags_get_int(&imgs[i]->tags, "gif_disposal", 0, &temp)) {
      need_89a = 1;
      break;
    }
    if (i_tags_get_int(&imgs[i]->tags, "gif_loop", 0, &temp)) {
      need_89a = 1;
      break;
    }
  }

  return need_89a;
}

static int 
in_palette(i_color *c, i_quantize *quant, int size) {
  int i;

  for (i = 0; i < size; ++i) {
    if (c->channel[0] == quant->mc_colors[i].channel[0]
        && c->channel[1] == quant->mc_colors[i].channel[1]
        && c->channel[2] == quant->mc_colors[i].channel[2]) {
      return i;
    }
  }

  return -1;
}

/*
=item has_common_palette(imgs, count, quant)

Tests if all the given images are paletted and their colors are in the
palette produced.

Previously this would build a consolidated palette from the source,
but that meant that if the caller supplied a static palette (or
specified a fixed palette like "webmap") then we wouldn't be
quantizing to the caller specified palette.

=cut
*/

static int
has_common_palette(i_img **imgs, int count, i_quantize *quant) {
  int i;
  int imgn;
  char used[256];
  int col_count;

  /* we try to build a common palette here, if we can manage that, then
     that's the palette we use */
  for (imgn = 0; imgn < count; ++imgn) {
    int eliminate_unused;
    if (imgs[imgn]->type != i_palette_type)
      return 0;

    if (!i_tags_get_int(&imgs[imgn]->tags, "gif_eliminate_unused", 0, 
                        &eliminate_unused)) {
      eliminate_unused = 1;
    }

    if (eliminate_unused) {
      i_palidx *line = mymalloc(sizeof(i_palidx) * imgs[imgn]->xsize);
      int x, y;
      memset(used, 0, sizeof(used));

      for (y = 0; y < imgs[imgn]->ysize; ++y) {
        i_gpal(imgs[imgn], 0, imgs[imgn]->xsize, y, line);
        for (x = 0; x < imgs[imgn]->xsize; ++x)
          used[line[x]] = 1;
      }

      myfree(line);
    }
    else {
      /* assume all are in use */
      memset(used, 1, sizeof(used));
    }

    col_count = i_colorcount(imgs[imgn]);
    for (i = 0; i < col_count; ++i) {
      i_color c;
      
      i_getcolors(imgs[imgn], i, &c, 1);
      if (used[i]) {
        if (in_palette(&c, quant, quant->mc_count) < 0) {
	  mm_log((1, "  color not found in palette, no palette shortcut\n"));
  
	  return 0;
        }
      }
    }
  }

  mm_log((1, "  all colors found in palette, palette shortcut\n"));

  return 1;
}

static i_palidx *
quant_paletted(i_quantize *quant, i_img *img) {
  i_palidx *data = mymalloc(sizeof(i_palidx) * img->xsize * img->ysize);
  i_palidx *p = data;
  i_palidx trans[256];
  int i;
  i_img_dim x, y;

  /* build a translation table */
  for (i = 0; i < i_colorcount(img); ++i) {
    i_color c;
    i_getcolors(img, i, &c, 1);
    trans[i] = in_palette(&c, quant, quant->mc_count);
  }

  for (y = 0; y < img->ysize; ++y) {
    i_gpal(img, 0, img->xsize, y, data+img->xsize * y);
    for (x = 0; x < img->xsize; ++x) {
      *p = trans[*p];
      ++p;
    }
  }

  return data;
}

/*
=item i_writegif_low(i_quantize *quant, GifFileType *gf, i_img **imgs, int count, i_gif_opts *opts)

Internal.  Low-level function that does the high-level GIF processing
:)

Returns non-zero on success.

=cut
*/

static undef_int
i_writegif_low(i_quantize *quant, GifFileType *gf, i_img **imgs, int count) {
  unsigned char *result = NULL;
  int color_bits;
  ColorMapObject *map;
  int scrw = 0, scrh = 0;
  int imgn, orig_count, orig_size;
  int posx, posy;
  int trans_index = -1;
  int *localmaps = NULL;
  int anylocal;
  i_img **glob_imgs = NULL; /* images that will use the global color map */
  int glob_img_count;
  i_color *orig_colors = quant->mc_colors;
  i_color *glob_colors = NULL;
  int glob_color_count = 0;
  int glob_want_trans;
  int glob_paletted = 0; /* the global map was made from the image palettes */
  int colors_paletted = 0;
  int want_trans = 0;
  int interlace;
  int gif_background;
  int error;

  mm_log((1, "i_writegif_low(quant %p, gf  %p, imgs %p, count %d)\n", 
	  quant, gf, imgs, count));
  
  /* *((char *)0) = 1; */ /* used to break into the debugger */
  
  if (count <= 0) {
    i_push_error(0, "No images provided to write");
    goto fail_cleanup;
  }

  /* sanity is nice */
  if (quant->mc_size > 256) 
    quant->mc_size = 256;
  if (quant->mc_count > quant->mc_size)
    quant->mc_count = quant->mc_size;

  if (!i_tags_get_int(&imgs[0]->tags, "gif_screen_width", 0, &scrw))
    scrw = 0;
  if (!i_tags_get_int(&imgs[0]->tags, "gif_screen_height", 0, &scrh))
    scrh = 0;

  anylocal = 0;
  localmaps = mymalloc(sizeof(int) * count);
  glob_imgs = mymalloc(sizeof(i_img *) * count);
  glob_img_count = 0;
  glob_want_trans = 0;
  for (imgn = 0; imgn < count; ++imgn) {
    i_img *im = imgs[imgn];
    if (im->xsize > 0xFFFF || im->ysize > 0xFFFF) {
      i_push_error(0, "image too large for GIF");
      goto fail_cleanup;
    }
    
    posx = posy = 0;
    i_tags_get_int(&im->tags, "gif_left", 0, &posx);
    if (posx < 0) posx = 0;
    i_tags_get_int(&im->tags, "gif_top", 0, &posy);
    if (posy < 0) posy = 0;
    if (im->xsize + posx > scrw)
      scrw = im->xsize + posx;
    if (im->ysize + posy > scrh)
      scrh = im->ysize + posy;
    if (!i_tags_get_int(&im->tags, "gif_local_map", 0, localmaps+imgn))
      localmaps[imgn] = 0;
    if (localmaps[imgn])
      anylocal = 1;
    else {
      if (im->channels == 4) {
        glob_want_trans = 1;
      }
      glob_imgs[glob_img_count++] = im;
    }
  }
  glob_want_trans = glob_want_trans && quant->transp != tr_none ;

  if (scrw > 0xFFFF || scrh > 0xFFFF) {
    i_push_error(0, "screen size too large for GIF");
    goto fail_cleanup;
  }

  orig_count = quant->mc_count;
  orig_size = quant->mc_size;

  if (glob_img_count) {
    /* this is ugly */
    glob_colors = mymalloc(sizeof(i_color) * quant->mc_size);
    quant->mc_colors = glob_colors;
    memcpy(glob_colors, orig_colors, sizeof(i_color) * quant->mc_count);
    /* we have some images that want to use the global map */
    if (glob_want_trans && quant->mc_count == 256) {
      mm_log((2, "  disabling transparency for global map - no space\n"));
      glob_want_trans = 0;
    }
    if (glob_want_trans && quant->mc_size == 256) {
      mm_log((2, "  reserving color for transparency\n"));
      --quant->mc_size;
    }

    i_quant_makemap(quant, glob_imgs, glob_img_count);
    glob_paletted = has_common_palette(glob_imgs, glob_img_count, quant);
    glob_color_count = quant->mc_count;
    quant->mc_colors = orig_colors;
  }

  /* use the global map if we have one, otherwise use the local map */
  gif_background = 0;
  if (glob_colors) {
    quant->mc_colors = glob_colors;
    quant->mc_count = glob_color_count;
    want_trans = glob_want_trans && imgs[0]->channels == 4;

    if (!i_tags_get_int(&imgs[0]->tags, "gif_background", 0, &gif_background))
      gif_background = 0;
    if (gif_background < 0)
      gif_background = 0;
    if (gif_background >= glob_color_count)
      gif_background = 0;
  }
  else {
    want_trans = quant->transp != tr_none && imgs[0]->channels == 4;
    i_quant_makemap(quant, imgs, 1);
    colors_paletted = has_common_palette(imgs, 1, quant);
  }

  if ((map = make_gif_map(quant, imgs[0], want_trans)) == NULL) {
    mm_log((1, "Error in MakeMapObject"));
    goto fail_cleanup;
  }
  color_bits = 1;
  if (anylocal) {
    /* since we don't know how big some the local palettes could be
       we need to base the bits on the maximum number of colors */
    while (orig_size > (1 << color_bits))
      ++color_bits;
  }
  else {
    int count = quant->mc_count;
    if (want_trans)
      ++count;
    while (count > (1 << color_bits))
      ++color_bits;
  }
  
  if (EGifPutScreenDesc(gf, scrw, scrh, color_bits, 
                        gif_background, map) == GIF_ERROR) {
    gif_push_error(myGifError(gf));
    i_push_error(0, "Could not save screen descriptor");
    FreeMapObject(map);
    mm_log((1, "Error in EGifPutScreenDesc."));
    goto fail_cleanup;
  }
  FreeMapObject(map);

  if (!i_tags_get_int(&imgs[0]->tags, "gif_left", 0, &posx))
    posx = 0;
  if (!i_tags_get_int(&imgs[0]->tags, "gif_top", 0, &posy))
    posy = 0;

  if (!localmaps[0]) {
    map = NULL;
    colors_paletted = glob_paletted;
  }
  else {
    /* if this image has a global map the colors in quant don't
       belong to this image, so build a palette */
    if (glob_colors) {
      /* generate the local map for this image */
      quant->mc_colors = orig_colors;
      quant->mc_size = orig_size;
      quant->mc_count = orig_count;
      want_trans = quant->transp != tr_none && imgs[0]->channels == 4;

      /* if the caller gives us too many colours we can't do transparency */
      if (want_trans && quant->mc_count == 256)
        want_trans = 0;
      /* if they want transparency but give us a big size, make it smaller
         to give room for a transparency colour */
      if (want_trans && quant->mc_size == 256)
        --quant->mc_size;
      i_quant_makemap(quant, imgs, 1);
      colors_paletted = has_common_palette(imgs, 1, quant);
      if ((map = make_gif_map(quant, imgs[0], want_trans)) == NULL) {
        mm_log((1, "Error in MakeMapObject"));
        goto fail_cleanup;
      }
    }
    else {
      /* the map we wrote was the map for this image - don't set the local 
         map */
      map = NULL;
    }
  }

  if (colors_paletted)
    result = quant_paletted(quant, imgs[0]);
  else
    result = i_quant_translate(quant, imgs[0]);
  if (!result) {
    goto fail_cleanup;
  }
  if (want_trans) {
    i_quant_transparent(quant, result, imgs[0], quant->mc_count);
    trans_index = quant->mc_count;
  }

  if (!do_ns_loop(gf, imgs[0])) {
    goto fail_cleanup;
  }

  if (!do_gce(gf, imgs[0], want_trans, trans_index)) {
    goto fail_cleanup;
  }

  if (!do_comments(gf, imgs[0])) {
    goto fail_cleanup;
  }

  if (!i_tags_get_int(&imgs[0]->tags, "gif_interlace", 0, &interlace))
    interlace = 0;
  if (EGifPutImageDesc(gf, posx, posy, imgs[0]->xsize, imgs[0]->ysize, 
                       interlace, map) == GIF_ERROR) {
    if (map)
      FreeMapObject(map);
    gif_push_error(myGifError(gf));
    i_push_error(0, "Could not save image descriptor");
    mm_log((1, "Error in EGifPutImageDesc."));
    goto fail_cleanup;
  }
  if (map)
    FreeMapObject(map);

  if (!do_write(gf, interlace, imgs[0], result)) {
    goto fail_cleanup;
  }
  myfree(result);
  result = NULL;

  /* that first awful image is out of the way, do the rest */
  for (imgn = 1; imgn < count; ++imgn) {
    if (localmaps[imgn]) {
      quant->mc_colors = orig_colors;
      quant->mc_count = orig_count;
      quant->mc_size = orig_size;

      want_trans = quant->transp != tr_none 
	&& imgs[imgn]->channels == 4;
      /* if the caller gives us too many colours we can't do transparency */
      if (want_trans && quant->mc_count == 256)
	want_trans = 0;
      /* if they want transparency but give us a big size, make it smaller
	 to give room for a transparency colour */
      if (want_trans && quant->mc_size == 256)
	--quant->mc_size;

      if (has_common_palette(imgs+imgn, 1, quant)) {
        result = quant_paletted(quant, imgs[imgn]);
      }
      else {
        i_quant_makemap(quant, imgs+imgn, 1);
        result = i_quant_translate(quant, imgs[imgn]);
      }
      if (!result) {
        mm_log((1, "error in i_quant_translate()"));
        goto fail_cleanup;
      }
      if (want_trans) {
        i_quant_transparent(quant, result, imgs[imgn], quant->mc_count);
        trans_index = quant->mc_count;
      }

      if ((map = make_gif_map(quant, imgs[imgn], want_trans)) == NULL) {
        mm_log((1, "Error in MakeMapObject."));
        goto fail_cleanup;
      }
    }
    else {
      quant->mc_colors = glob_colors;
      quant->mc_count = glob_color_count;
      if (glob_paletted)
        result = quant_paletted(quant, imgs[imgn]);
      else
        result = i_quant_translate(quant, imgs[imgn]);
      want_trans = glob_want_trans && imgs[imgn]->channels == 4;
      if (want_trans) {
        i_quant_transparent(quant, result, imgs[imgn], quant->mc_count);
        trans_index = quant->mc_count;
      }
      map = NULL;
    }

    if (!do_gce(gf, imgs[imgn], want_trans, trans_index)) {
      goto fail_cleanup;
    }

    if (!do_comments(gf, imgs[imgn])) {
      goto fail_cleanup;
    }

    if (!i_tags_get_int(&imgs[imgn]->tags, "gif_left", 0, &posx))
      posx = 0;
    if (!i_tags_get_int(&imgs[imgn]->tags, "gif_top", 0, &posy))
      posy = 0;

    if (!i_tags_get_int(&imgs[imgn]->tags, "gif_interlace", 0, &interlace))
      interlace = 0;
    if (EGifPutImageDesc(gf, posx, posy, imgs[imgn]->xsize, 
                         imgs[imgn]->ysize, interlace, map) == GIF_ERROR) {
      gif_push_error(myGifError(gf));
      i_push_error(0, "Could not save image descriptor");
      if (map)
        FreeMapObject(map);
      mm_log((1, "Error in EGifPutImageDesc."));
      goto fail_cleanup;
    }
    if (map)
      FreeMapObject(map);
    
    if (!do_write(gf, interlace, imgs[imgn], result)) {
      goto fail_cleanup;
    }
    myfree(result);
    result = NULL;
  }

  if (myEGifCloseFile(gf, &error) == GIF_ERROR) {
    gif_push_error(error);
    i_push_error(0, "Could not close GIF file");
    goto fail_cleanup;
  }
  if (glob_colors) {
    int i;
    for (i = 0; i < glob_color_count; ++i)
      orig_colors[i] = glob_colors[i];
  }

  myfree(glob_colors);
  myfree(localmaps);
  myfree(glob_imgs);
  quant->mc_colors = orig_colors;

  return 1;

 fail_cleanup:
  quant->mc_colors = orig_colors;
  myfree(result);
  myfree(glob_colors);
  myfree(localmaps);
  myfree(glob_imgs);
  (void)myEGifCloseFile(gf, &error);
  return 0;
}

static int
io_glue_write_cb(GifFileType *gft, const GifByteType *data, int length) {
  io_glue *ig = (io_glue *)gft->UserData;

  return i_io_write(ig, data, length);
}


/*
=item i_writegif_wiol(ig, quant, opts, imgs, count)

=cut
*/
undef_int
i_writegif_wiol(io_glue *ig, i_quantize *quant, i_img **imgs,
                int count) {
  GifFileType *GifFile;
  int gif_error;
  int result;

  gif_mutex_lock(mutex);

  i_clear_error();

#ifdef PRE_SET_VERSION
  EGifSetGifVersion(need_version_89a(quant, imgs, count) ? "89a" : "87a");
#endif
  
  if ((GifFile = myEGifOpen((void *)ig, io_glue_write_cb, &gif_error )) == NULL) {
    gif_push_error(gif_error);
    i_push_error(0, "Cannot create giflib callback object");
    mm_log((1,"i_writegif_wiol: Unable to open callback datasource.\n"));
    gif_mutex_unlock(mutex);
    return 0;
  }
  
#ifdef POST_SET_VERSION
  EGifSetGifVersion(GifFile, need_version_89a(quant, imgs, count));
#endif

  result = i_writegif_low(quant, GifFile, imgs, count);
  
  gif_mutex_unlock(mutex);

  if (i_io_close(ig))
    return 0;
  
  return result;
}

/*
=item gif_error_msg(int code)

Grabs the most recent giflib error code from GifLastError() and 
returns a string that describes that error.

Returns NULL for unknown error codes.

=cut
*/

static char const *
gif_error_msg(int code) {
#if IMGIFLIB_API_VERSION >= 500
  return GifErrorString(code);
#else
  switch (code) {
  case E_GIF_ERR_OPEN_FAILED: /* should not see this */
    return "Failed to open given file";
    
  case E_GIF_ERR_WRITE_FAILED:
    return "Write failed";

  case E_GIF_ERR_HAS_SCRN_DSCR: /* should not see this */
    return "Screen descriptor already passed to giflib";

  case E_GIF_ERR_HAS_IMAG_DSCR: /* should not see this */
    return "Image descriptor already passed to giflib";
    
  case E_GIF_ERR_NO_COLOR_MAP: /* should not see this */
    return "Neither global nor local color map set";

  case E_GIF_ERR_DATA_TOO_BIG: /* should not see this */
    return "Too much pixel data passed to giflib";

  case E_GIF_ERR_NOT_ENOUGH_MEM:
    return "Out of memory";
    
  case E_GIF_ERR_DISK_IS_FULL:
    return "Disk is full";
    
  case E_GIF_ERR_CLOSE_FAILED: /* should not see this */
    return "File close failed";
 
  case E_GIF_ERR_NOT_WRITEABLE: /* should not see this */
    return "File not writable";

  case D_GIF_ERR_OPEN_FAILED:
    return "Failed to open file";
    
  case D_GIF_ERR_READ_FAILED:
    return "Failed to read from file";

  case D_GIF_ERR_NOT_GIF_FILE:
    return "File is not a GIF file";

  case D_GIF_ERR_NO_SCRN_DSCR:
    return "No screen descriptor detected - invalid file";

  case D_GIF_ERR_NO_IMAG_DSCR:
    return "No image descriptor detected - invalid file";

  case D_GIF_ERR_NO_COLOR_MAP:
    return "No global or local color map found";

  case D_GIF_ERR_WRONG_RECORD:
    return "Wrong record type detected - invalid file?";

  case D_GIF_ERR_DATA_TOO_BIG:
    return "Data in file too big for image";

  case D_GIF_ERR_NOT_ENOUGH_MEM:
    return "Out of memory";

  case D_GIF_ERR_CLOSE_FAILED:
    return "Close failed";

  case D_GIF_ERR_NOT_READABLE:
    return "File not opened for read";

  case D_GIF_ERR_IMAGE_DEFECT:
    return "Defective image";

  case D_GIF_ERR_EOF_TOO_SOON:
    return "Unexpected EOF - invalid file";

  default:
    return NULL;
  }
#endif
}

/*
=item gif_push_error(code)

Utility function that takes the current GIF error code, converts it to
an error message and pushes it on the error stack.

=cut
*/

static void
gif_push_error(int code) {
  const char *msg = gif_error_msg(code);
  if (msg)
    i_push_error(code, msg);
  else
    i_push_errorf(code, "Unknown GIF error %d", code);
}

/*
=head1 AUTHOR

Arnar M. Hrafnkelsson, addi@umich.edu

Tony Cook <tonyc@cpan.org>

=head1 SEE ALSO

perl(1), Imager(3)

=cut

*/