diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 03:13:10 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 03:13:10 +0000 |
commit | 3c57dd931145d43f2b0aef96c4d178135956bf91 (patch) | |
tree | 3de698981e9f0cc2c4f9569b19a5f3595e741f6b /plug-ins/common/file-gif-save.c | |
parent | Initial commit. (diff) | |
download | gimp-3c57dd931145d43f2b0aef96c4d178135956bf91.tar.xz gimp-3c57dd931145d43f2b0aef96c4d178135956bf91.zip |
Adding upstream version 2.10.36.upstream/2.10.36
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'plug-ins/common/file-gif-save.c')
-rw-r--r-- | plug-ins/common/file-gif-save.c | 2589 |
1 files changed, 2589 insertions, 0 deletions
diff --git a/plug-ins/common/file-gif-save.c b/plug-ins/common/file-gif-save.c new file mode 100644 index 0000000..3563172 --- /dev/null +++ b/plug-ins/common/file-gif-save.c @@ -0,0 +1,2589 @@ +/* GIF exporting file filter for GIMP + * + * Copyright + * - Adam D. Moss + * - Peter Mattis + * - Spencer Kimball + * + * Based around original GIF code by David Koblas. + * + * + * Version 4.1.0 - 2003-06-16 + * Adam D. Moss - <adam@gimp.org> <adam@foxbox.org> + */ +/* + * This filter uses code taken from the "giftopnm" and "ppmtogif" programs + * which are part of the "netpbm" package. + */ +/* + * "The Graphics Interchange Format(c) is the Copyright property of + * CompuServe Incorporated. GIF(sm) is a Service Mark property of + * CompuServe Incorporated." + */ +/* Copyright notice for GIF code from which this plugin was long ago */ +/* derived (David Koblas has granted permission to relicense): */ +/* +-------------------------------------------------------------------+ */ +/* | Copyright 1990, 1991, 1993, David Koblas. (koblas@extra.com) | */ +/* +-------------------------------------------------------------------+ */ + +#include "config.h" + +#include <string.h> + +#include <libgimp/gimp.h> +#include <libgimp/gimpui.h> + +#include "libgimp/stdplugins-intl.h" + + +#define SAVE_PROC "file-gif-save" +#define SAVE2_PROC "file-gif-save2" +#define PLUG_IN_BINARY "file-gif-save" +#define PLUG_IN_ROLE "gimp-file-gif-save" + + +/* uncomment the line below for a little debugging info */ +/* #define GIFDEBUG yesplease */ + + +enum +{ + DISPOSE_STORE_VALUE_COLUMN, + DISPOSE_STORE_LABEL_COLUMN +}; + +enum +{ + DISPOSE_UNSPECIFIED, + DISPOSE_COMBINE, + DISPOSE_REPLACE +}; + +typedef struct +{ + gint interlace; + gint save_comment; + gint loop; + gint default_delay; + gint default_dispose; + gboolean always_use_default_delay; + gboolean always_use_default_dispose; + gboolean as_animation; +} GIFSaveVals; + + +/* Declare some local functions. + */ +static void query (void); +static void run (const gchar *name, + gint nparams, + const GimpParam *param, + gint *nreturn_vals, + GimpParam **return_vals); + +static gboolean save_image (GFile *file, + gint32 image_ID, + gint32 drawable_ID, + gint32 orig_image_ID, + GError **error); + +static GimpPDBStatusType sanity_check (GFile *file, + gint32 *image_ID, + GimpRunMode run_mode, + GError **error); +static gboolean bad_bounds_dialog (void); + +static gboolean save_dialog (gint32 image_ID); +static void comment_entry_callback (GtkTextBuffer *buffer); + + +static gboolean comment_was_edited = FALSE; +static gchar *globalcomment = NULL; +static gint Interlace; /* For compression code */ + + +const GimpPlugInInfo PLUG_IN_INFO = +{ + NULL, /* init_proc */ + NULL, /* quit_proc */ + query, /* query_proc */ + run, /* run_proc */ +}; + +static GIFSaveVals gsvals = +{ + FALSE, /* interlace */ + TRUE, /* save comment */ + TRUE, /* loop infinitely */ + 100, /* default_delay between frames (100ms) */ + 0, /* default_dispose = "don't care" */ + FALSE, /* don't always use default_delay */ + FALSE, /* don't always use default_dispose */ + FALSE /* as_animation */ +}; + + +MAIN () + +#define COMMON_SAVE_ARGS \ + { GIMP_PDB_INT32, "run-mode", "The run mode { RUN-INTERACTIVE (0), RUN-NONINTERACTIVE (1) }" }, \ + { GIMP_PDB_IMAGE, "image", "Image to export" }, \ + { GIMP_PDB_DRAWABLE, "drawable", "Drawable to export" }, \ + { GIMP_PDB_STRING, "uri", "The name of the URI to export the image in" }, \ + { GIMP_PDB_STRING, "raw-uri", "The name of the URI to export the image in" }, \ + { GIMP_PDB_INT32, "interlace", "Try to export as interlaced" }, \ + { GIMP_PDB_INT32, "loop", "(animated gif) loop infinitely" }, \ + { GIMP_PDB_INT32, "default-delay", "(animated gif) Default delay between frames in milliseconds" }, \ + { GIMP_PDB_INT32, "default-dispose", "(animated gif) Default disposal type (0=`don't care`, 1=combine, 2=replace)" } + +static void +query (void) +{ + static const GimpParamDef save_args[] = + { + COMMON_SAVE_ARGS + }; + + static const GimpParamDef save2_args[] = + { + COMMON_SAVE_ARGS, + { GIMP_PDB_INT32, "as-animation", "Export GIF as animation?" }, + { GIMP_PDB_INT32, "force-delay", "(animated gif) Use specified delay for all frames?" }, + { GIMP_PDB_INT32, "force-dispose", "(animated gif) Use specified disposal for all frames?" } + }; + + gimp_install_procedure (SAVE_PROC, + "exports files in Compuserve GIF file format", + "Export a file in Compuserve GIF format, with " + "possible animation, transparency, and comment. " + "To export an animation, operate on a multi-layer " + "file. The plug-in will interpret <50% alpha as " + "transparent. When run non-interactively, the " + "value for the comment is taken from the " + "'gimp-comment' parasite. ", + "Spencer Kimball, Peter Mattis, Adam Moss, David Koblas", + "Spencer Kimball, Peter Mattis, Adam Moss, David Koblas", + "1995-1997", + N_("GIF image"), + "INDEXED*, GRAY*", + GIMP_PLUGIN, + G_N_ELEMENTS (save_args), 0, + save_args, NULL); + + gimp_install_procedure (SAVE2_PROC, + "exports files in Compuserve GIF file format", + "Export a file in Compuserve GIF format, with " + "possible animation, transparency, and comment. " + "To export an animation, operate on a multi-layer " + "file and give the 'as-animation' parameter " + "as TRUE. The plug-in will interpret <50% " + "alpha as transparent. When run " + "non-interactively, the value for the comment " + "is taken from the 'gimp-comment' parasite. ", + "Spencer Kimball, Peter Mattis, Adam Moss, David Koblas", + "Spencer Kimball, Peter Mattis, Adam Moss, David Koblas", + "1995-1997", + N_("GIF image"), + "INDEXED*, GRAY*", + GIMP_PLUGIN, + G_N_ELEMENTS (save2_args), 0, + save2_args, NULL); + + gimp_register_file_handler_mime (SAVE_PROC, "image/gif"); + gimp_register_save_handler (SAVE_PROC, "gif", ""); + gimp_register_file_handler_uri (SAVE_PROC); + + gimp_register_save_handler (SAVE2_PROC, "gif", ""); + gimp_register_file_handler_uri (SAVE2_PROC); +} + +static void +run (const gchar *name, + gint nparams, + const GimpParam *param, + gint *nreturn_vals, + GimpParam **return_vals) +{ + static GimpParam values[2]; + GimpRunMode run_mode; + GimpPDBStatusType status = GIMP_PDB_SUCCESS; + GimpExportReturn export = GIMP_EXPORT_CANCEL; + GError *error = NULL; + + INIT_I18N (); + gegl_init (NULL, NULL); + + run_mode = param[0].data.d_int32; + + *nreturn_vals = 1; + *return_vals = values; + + values[0].type = GIMP_PDB_STATUS; + values[0].data.d_status = GIMP_PDB_EXECUTION_ERROR; + + if (strcmp (name, SAVE_PROC) == 0 || + strcmp (name, SAVE2_PROC) == 0) + { + GFile *file; + gint32 image_ID; + gint32 orig_image_ID; + gint32 sanitized_image_ID = 0; + gint32 drawable_ID; + + image_ID = orig_image_ID = param[1].data.d_int32; + drawable_ID = param[2].data.d_int32; + file = g_file_new_for_uri (param[3].data.d_string); + + if (run_mode == GIMP_RUN_INTERACTIVE || + run_mode == GIMP_RUN_WITH_LAST_VALS) + gimp_ui_init (PLUG_IN_BINARY, FALSE); + + status = sanity_check (file, &image_ID, run_mode, &error); + + /* Get the export options */ + if (status == GIMP_PDB_SUCCESS) + { + /* If the sanity check succeeded, the image_ID will point to + * a duplicate image to delete later. + */ + sanitized_image_ID = image_ID; + + switch (run_mode) + { + case GIMP_RUN_INTERACTIVE: + /* Possibly retrieve data */ + gimp_get_data (SAVE_PROC, &gsvals); + + /* First acquire information with a dialog */ + if (! save_dialog (image_ID)) + { + gimp_image_delete (sanitized_image_ID); + status = GIMP_PDB_CANCEL; + } + break; + + case GIMP_RUN_NONINTERACTIVE: + /* Make sure all the arguments are there! */ + if (nparams != 9 && nparams != 12) + { + status = GIMP_PDB_CALLING_ERROR; + } + else + { + gsvals.interlace = (param[5].data.d_int32) ? TRUE : FALSE; + gsvals.save_comment = TRUE; /* no way to to specify that through the PDB */ + gsvals.loop = (param[6].data.d_int32) ? TRUE : FALSE; + gsvals.default_delay = param[7].data.d_int32; + gsvals.default_dispose = param[8].data.d_int32; + if (nparams == 12) + { + gsvals.as_animation = (param[9].data.d_int32) ? TRUE : FALSE; + gsvals.always_use_default_delay = (param[10].data.d_int32) ? TRUE : FALSE; + gsvals.always_use_default_dispose = (param[11].data.d_int32) ? TRUE : FALSE; + } + } + break; + + case GIMP_RUN_WITH_LAST_VALS: + /* Possibly retrieve data */ + gimp_get_data (SAVE_PROC, &gsvals); + break; + + default: + break; + } + } + + if (status == GIMP_PDB_SUCCESS) + { + /* Create an exportable image based on the export options */ + switch (run_mode) + { + case GIMP_RUN_INTERACTIVE: + case GIMP_RUN_WITH_LAST_VALS: + { + GimpExportCapabilities capabilities = + GIMP_EXPORT_CAN_HANDLE_INDEXED | + GIMP_EXPORT_CAN_HANDLE_GRAY | + GIMP_EXPORT_CAN_HANDLE_ALPHA; + + if (gsvals.as_animation) + capabilities |= GIMP_EXPORT_CAN_HANDLE_LAYERS; + + export = gimp_export_image (&image_ID, &drawable_ID, "GIF", + capabilities); + + if (export == GIMP_EXPORT_CANCEL) + { + values[0].data.d_status = GIMP_PDB_CANCEL; + if (sanitized_image_ID) + gimp_image_delete (sanitized_image_ID); + return; + } + } + break; + default: + break; + } + + /* Load comment from parasite for non-interactive export */ + if (run_mode != GIMP_RUN_INTERACTIVE) + { + GimpParasite *parasite; + + parasite = gimp_image_get_parasite (image_ID, "gimp-comment"); + if (parasite) + { + globalcomment = g_strndup (gimp_parasite_data (parasite), + gimp_parasite_data_size (parasite)); + gimp_parasite_free (parasite); + } + } + + /* Write the image to file */ + if (save_image (file, + image_ID, drawable_ID, orig_image_ID, + &error)) + { + /* Store psvals data */ + gimp_set_data (SAVE_PROC, &gsvals, sizeof (GIFSaveVals)); + } + else + { + status = GIMP_PDB_EXECUTION_ERROR; + } + + gimp_image_delete (sanitized_image_ID); + } + + if (export == GIMP_EXPORT_EXPORT) + gimp_image_delete (image_ID); + + g_object_unref (file); + } + + if (status != GIMP_PDB_SUCCESS && error) + { + *nreturn_vals = 2; + values[1].type = GIMP_PDB_STRING; + values[1].data.d_string = error->message; + } + + values[0].data.d_status = status; +} + + +/* ppmtogif.c - read a portable pixmap and produce a GIF file +** +** Based on GIFENCOD by David Rowley <mgardi@watdscu.waterloo.edu>. A +** Lempel-Ziv compression based on "compress". +** +** Modified by Marcel Wijkstra <wijkstra@fwi.uva.nl> +** +** +** Copyright (C) 1989 by Jef Poskanzer. +** +** Permission to use, copy, modify, and distribute this software and its +** documentation for any purpose and without fee is hereby granted, provided +** that the above copyright notice appear in all copies and that both that +** copyright notice and this permission notice appear in supporting +** documentation. This software is provided "as is" without express or +** implied warranty. +** +** The Graphics Interchange Format(c) is the Copyright property of +** CompuServe Incorporated. GIF(sm) is a Service Mark property of +** CompuServe Incorporated. +*/ + +#define MAXCOLORS 256 + +/* + * Pointer to function returning an int + */ +typedef gint (* ifunptr) (gint x, + gint y); + + +static gint find_unused_ia_color (const guchar *pixels, + gint numpixels, + gint num_indices, + gint *colors); + +static void special_flatten_indexed_alpha (guchar *pixels, + gint transparent, + gint numpixels); + +static gint colors_to_bpp (gint colors); +static gint bpp_to_colors (gint bpp); +static gint get_pixel (gint x, + gint y); +static gint gif_next_pixel (ifunptr getpixel); +static void bump_pixel (void); + +static gboolean gif_encode_header (GOutputStream *output, + gboolean gif89, + gint width, + gint height, + gint background, + gint bpp, + gint *red, + gint *green, + gint *blue, + ifunptr get_pixel, + GError **error); +static gboolean gif_encode_graphic_control_ext (GOutputStream *output, + gint disposal, + gint delay89, + gint n_frames, + gint width, + gint height, + gint transparent, + gint bpp, + ifunptr get_pixel, + GError **error); +static gboolean gif_encode_image_data (GOutputStream *output, + gint width, + gint height, + gint interlace, + gint bpp, + ifunptr get_pixel, + gint offset_x, + gint offset_y, + GError **error); +static gboolean gif_encode_close (GOutputStream *output, + GError **error); +static gboolean gif_encode_loop_ext (GOutputStream *output, + guint n_loops, + GError **error); +static gboolean gif_encode_comment_ext (GOutputStream *output, + const gchar *comment, + GError **error); + +static gint rowstride; +static guchar *pixels; +static gint cur_progress; +static gint max_progress; + +static gboolean compress (GOutputStream *output, + gint init_bits, + ifunptr ReadValue, + GError **error); +static gboolean no_compress (GOutputStream *output, + gint init_bits, + ifunptr ReadValue, + GError **error); +static gboolean rle_compress (GOutputStream *output, + gint init_bits, + ifunptr ReadValue, + GError **error); +static gboolean normal_compress (GOutputStream *output, + gint init_bits, + ifunptr ReadValue, + GError **error); + +static gboolean put_byte (GOutputStream *output, + guchar b, + GError **error); +static gboolean put_word (GOutputStream *output, + gint w, + GError **error); +static gboolean put_string (GOutputStream *output, + const gchar *s, + GError **error); +static gboolean output_code (GOutputStream *output, + gint code, + GError **error); +static gboolean cl_block (GOutputStream *output, + GError **error); +static void cl_hash (glong hsize); + +static void char_init (void); +static gboolean char_out (GOutputStream *output, + gint c, + GError **error); +static gboolean char_flush (GOutputStream *output, + GError **error); + + +static gint +find_unused_ia_color (const guchar *pixels, + gint numpixels, + gint num_indices, + gint *colors) +{ + gboolean ix_used[256]; + gint i; + +#ifdef GIFDEBUG + g_printerr ("GIF: fuiac: Image claims to use %d/%d indices - finding free " + "index...\n", *colors, num_indices); +#endif + + for (i = 0; i < 256; i++) + ix_used[i] = FALSE; + + for (i = 0; i < numpixels; i++) + { + if (pixels[i * 2 + 1]) + ix_used[pixels[i * 2]] = TRUE; + } + + for (i = num_indices - 1; i >= 0; i--) + { + if (! ix_used[i]) + { +#ifdef GIFDEBUG + g_printerr ("GIF: Found unused color index %d.\n", (int) i); +#endif + return i; + } + } + + /* Couldn't find an unused color index within the number of bits per + * pixel we wanted. Will have to increment the number of colors in + * the image and assign a transparent pixel there. + */ + if (*colors < 256) + { + (*colors)++; + + g_printerr ("GIF: 2nd pass " + "- Increasing bounds and using color index %d.\n", + *colors - 1); + return ((*colors) - 1); + } + + g_message (_("Couldn't simply reduce colors further. Exporting as opaque.")); + + return -1; +} + + +static void +special_flatten_indexed_alpha (guchar *pixels, + gint transparent, + gint numpixels) +{ + guint32 i; + + /* Each transparent pixel in the image is mapped to a uniform value + * for encoding, if image already has <=255 colors + */ + + if (transparent == -1) /* tough, no indices left for the trans. index */ + { + for (i = 0; i < numpixels; i++) + pixels[i] = pixels[i * 2]; + } + else /* make transparent */ + { + for (i = 0; i < numpixels; i++) + { + if (! (pixels[i * 2 + 1] & 128)) + { + pixels[i] = (guchar) transparent; + } + else + { + pixels[i] = pixels[i * 2]; + } + } + } +} + + +static gint +parse_ms_tag (const gchar *str) +{ + gint sum = 0; + gint offset = 0; + gint length; + + length = strlen (str); + + find_another_bra: + + while ((offset < length) && (str[offset] != '(')) + offset++; + + if (offset >= length) + return(-1); + + if (! g_ascii_isdigit (str[++offset])) + goto find_another_bra; + + do + { + sum *= 10; + sum += str[offset] - '0'; + offset++; + } + while ((offset < length) && (g_ascii_isdigit (str[offset]))); + + if (length - offset <= 2) + return(-3); + + if ((g_ascii_toupper (str[offset]) != 'M') || + (g_ascii_toupper (str[offset + 1]) != 'S')) + return -4; + + return sum; +} + + +static gint +parse_disposal_tag (const gchar *str) +{ + gint offset = 0; + gint length; + + length = strlen (str); + + while ((offset + 9) <= length) + { + if (strncmp (&str[offset], "(combine)", 9) == 0) + return 0x01; + + if (strncmp (&str[offset], "(replace)", 9) == 0) + return 0x02 ; + + offset++; + } + + return gsvals.default_dispose; +} + + +static GimpPDBStatusType +sanity_check (GFile *file, + gint32 *image_ID, + GimpRunMode run_mode, + GError **error) +{ + gint32 *layers; + gint nlayers; + gint image_width; + gint image_height; + gint i; + + image_width = gimp_image_width (*image_ID); + image_height = gimp_image_height (*image_ID); + + if (image_width > G_MAXUSHORT || image_height > G_MAXUSHORT) + { + g_set_error (error, 0, 0, + _("Unable to export '%s'. " + "The GIF file format does not support images that are " + "more than %d pixels wide or tall."), + gimp_file_get_utf8_name (file), G_MAXUSHORT); + + return GIMP_PDB_EXECUTION_ERROR; + } + + /*** Iterate through the layers to make sure they're all ***/ + /*** within the bounds of the image ***/ + + *image_ID = gimp_image_duplicate (*image_ID); + layers = gimp_image_get_layers (*image_ID, &nlayers); + + for (i = 0; i < nlayers; i++) + { + gint offset_x; + gint offset_y; + + gimp_drawable_offsets (layers[i], &offset_x, &offset_y); + + if (offset_x < 0 || + offset_y < 0 || + offset_x + gimp_drawable_width (layers[i]) > image_width || + offset_y + gimp_drawable_height (layers[i]) > image_height) + { + g_free (layers); + + /* Image has illegal bounds - ask the user what it wants to do */ + + /* Do the crop if we can't talk to the user, or if we asked + * the user and they said yes. + */ + if ((run_mode == GIMP_RUN_NONINTERACTIVE) || bad_bounds_dialog ()) + { + gimp_image_crop (*image_ID, image_width, image_height, 0, 0); + return GIMP_PDB_SUCCESS; + } + else + { + gimp_image_delete (*image_ID); + return GIMP_PDB_CANCEL; + } + } + } + + g_free (layers); + + return GIMP_PDB_SUCCESS; +} + + +static gboolean +save_image (GFile *file, + gint32 image_ID, + gint32 drawable_ID, + gint32 orig_image_ID, + GError **error) +{ + GeglBuffer *buffer; + GimpImageType drawable_type; + const Babl *format = NULL; + GOutputStream *output; + gint Red[MAXCOLORS]; + gint Green[MAXCOLORS]; + gint Blue[MAXCOLORS]; + guchar *cmap; + guint rows, cols; + gint BitsPerPixel; + gint liberalBPP = 0; + gint useBPP = 0; + gint colors; + gint i; + gint transparent; + gint offset_x, offset_y; + + gint32 *layers; + gint nlayers; + + gboolean is_gif89 = FALSE; + + gint Delay89; + gint Disposal; + gchar *layer_name; + + GimpRGB background; + guchar bgred, bggreen, bgblue; + guchar bgindex = 0; + guint best_error = 0xFFFFFFFF; + + /* Save the comment back to the ImageID, if appropriate */ + if (globalcomment != NULL && comment_was_edited) + { + GimpParasite *parasite; + + parasite = gimp_parasite_new ("gimp-comment", + GIMP_PARASITE_PERSISTENT, + strlen (globalcomment) + 1, + (gpointer) globalcomment); + gimp_image_attach_parasite (orig_image_ID, parasite); + gimp_parasite_free (parasite); + } + + /* The GIF spec says 7bit ASCII for the comment block. */ + if (gsvals.save_comment && globalcomment) + { + const gchar *c = globalcomment; + gint len; + + for (len = strlen (c); len; c++, len--) + { + if ((guchar) *c > 127) + { + g_message (_("The GIF format only supports comments in " + "7bit ASCII encoding. No comment is saved.")); + + g_free (globalcomment); + globalcomment = NULL; + + break; + } + } + } + + /* get a list of layers for this image_ID */ + layers = gimp_image_get_layers (image_ID, &nlayers); + + drawable_type = gimp_drawable_type (layers[0]); + + /* If the image has multiple layers (i.e. will be animated), a + * comment, or transparency, then it must be encoded as a GIF89a + * file, not a vanilla GIF87a. + */ + if (nlayers > 1) + { + is_gif89 = TRUE; + + /* Layers can be with or without alpha channel. Make sure we set + * alpha if there is at least one layer with alpha channel. */ + if (drawable_type == GIMP_GRAY_IMAGE || + drawable_type == GIMP_INDEXED_IMAGE) + { + for (i = nlayers - 1; i >= 0; i--) + { + GimpImageType dr_type = gimp_drawable_type (layers[i]); + + if (dr_type == GIMP_GRAYA_IMAGE || + dr_type == GIMP_INDEXEDA_IMAGE) + { + drawable_type = dr_type; + break; + } + } + } + } + + if (gsvals.save_comment) + is_gif89 = TRUE; + + switch (drawable_type) + { + case GIMP_INDEXEDA_IMAGE: + is_gif89 = TRUE; + case GIMP_INDEXED_IMAGE: + cmap = gimp_image_get_colormap (image_ID, &colors); + + gimp_context_get_background (&background); + gimp_rgb_get_uchar (&background, &bgred, &bggreen, &bgblue); + + for (i = 0; i < colors; i++) + { + Red[i] = *cmap++; + Green[i] = *cmap++; + Blue[i] = *cmap++; + } + for ( ; i < 256; i++) + { + Red[i] = bgred; + Green[i] = bggreen; + Blue[i] = bgblue; + } + break; + case GIMP_GRAYA_IMAGE: + is_gif89 = TRUE; + case GIMP_GRAY_IMAGE: + colors = 256; /* FIXME: Not ideal. */ + for ( i = 0; i < 256; i++) + { + Red[i] = Green[i] = Blue[i] = i; + } + break; + + default: + g_message (_("Cannot export RGB color images. Convert to " + "indexed color or grayscale first.")); + return FALSE; + } + + + /* find earliest index in palette which is closest to the background + * color, and ATTEMPT to use that as the GIF's default background + * color. + */ + for (i = 255; i >= 0; --i) + { + guint local_error = 0; + + local_error += (Red[i] - bgred) * (Red[i] - bgred); + local_error += (Green[i] - bggreen) * (Green[i] - bggreen); + local_error += (Blue[i] - bgblue) * (Blue[i] - bgblue); + + if (local_error <= best_error) + { + bgindex = i; + best_error = local_error; + } + } + + + /* init the progress meter */ + gimp_progress_init_printf (_("Exporting '%s'"), + gimp_file_get_utf8_name (file)); + + + /* open the destination file for writing */ + output = G_OUTPUT_STREAM (g_file_replace (file, + NULL, FALSE, G_FILE_CREATE_NONE, + NULL, error)); + if (output) + { + GDataOutputStream *data_output; + + data_output = g_data_output_stream_new (output); + g_object_unref (output); + + g_data_output_stream_set_byte_order (data_output, + G_DATA_STREAM_BYTE_ORDER_LITTLE_ENDIAN); + + output = G_OUTPUT_STREAM (data_output); + } + else + { + return FALSE; + } + + + /* write the GIFheader */ + + if (colors < 256) + { + /* we keep track of how many bits we promised to have in + * liberalBPP, so that we don't accidentally come under this + * when doing clever transparency stuff where we can re-use + * wasted indices. + */ + liberalBPP = BitsPerPixel = + colors_to_bpp (colors + ((drawable_type==GIMP_INDEXEDA_IMAGE) ? 1 : 0)); + } + else + { + liberalBPP = BitsPerPixel = + colors_to_bpp (256); + + if (drawable_type == GIMP_INDEXEDA_IMAGE) + { + g_printerr ("GIF: Too many colors?\n"); + } + } + + cols = gimp_image_width (image_ID); + rows = gimp_image_height (image_ID); + Interlace = gsvals.interlace; + if (! gif_encode_header (output, is_gif89, cols, rows, bgindex, + BitsPerPixel, Red, Green, Blue, get_pixel, + error)) + return FALSE; + + + /* If the image has multiple layers it'll be made into an animated + * GIF, so write out the infinite-looping extension + */ + if ((nlayers > 1) && (gsvals.loop)) + if (! gif_encode_loop_ext (output, 0, error)) + return FALSE; + + /* Write comment extension - mustn't be written before the looping ext. */ + if (gsvals.save_comment && globalcomment) + { + if (! gif_encode_comment_ext (output, globalcomment, error)) + return FALSE; + } + + + /*** Now for each layer in the image, save an image in a compound GIF ***/ + /************************************************************************/ + + cur_progress = 0; + max_progress = nlayers * rows; + + for (i = nlayers - 1; i >= 0; i--, cur_progress = (nlayers - i) * rows) + { + drawable_type = gimp_drawable_type (layers[i]); + if (drawable_type == GIMP_GRAYA_IMAGE) + { + format = babl_format ("Y'A u8"); + } + else if (drawable_type == GIMP_GRAY_IMAGE) + { + format = babl_format ("Y' u8"); + } + buffer = gimp_drawable_get_buffer (layers[i]); + gimp_drawable_offsets (layers[i], &offset_x, &offset_y); + cols = gimp_drawable_width (layers[i]); + rows = gimp_drawable_height (layers[i]); + rowstride = cols; + + pixels = g_new (guchar, (cols * rows * + (((drawable_type == GIMP_INDEXEDA_IMAGE) || + (drawable_type == GIMP_GRAYA_IMAGE)) ? 2 : 1))); + + gegl_buffer_get (buffer, GEGL_RECTANGLE (0, 0, cols, rows), 1.0, + format, pixels, + GEGL_AUTO_ROWSTRIDE, GEGL_ABYSS_NONE); + + /* sort out whether we need to do transparency jiggery-pokery */ + if ((drawable_type == GIMP_INDEXEDA_IMAGE) || + (drawable_type == GIMP_GRAYA_IMAGE)) + { + /* Try to find an entry which isn't actually used in the + * image, for a transparency index. + */ + + transparent = + find_unused_ia_color (pixels, + cols * rows, + bpp_to_colors (colors_to_bpp (colors)), + &colors); + + special_flatten_indexed_alpha (pixels, + transparent, + cols * rows); + } + else + { + transparent = -1; + } + + BitsPerPixel = colors_to_bpp (colors); + + if (BitsPerPixel != liberalBPP) + { + /* We were able to re-use an index within the existing + * bitspace, whereas the estimate in the header was + * pessimistic but still needs to be upheld... + */ +#ifdef GIFDEBUG + static gboolean onceonly = FALSE; + + if (! onceonly) + { + g_warning ("Promised %d bpp, pondered writing chunk with %d bpp!", + liberalBPP, BitsPerPixel); + onceonly = TRUE; + } +#endif + } + + useBPP = (BitsPerPixel > liberalBPP) ? BitsPerPixel : liberalBPP; + + if (is_gif89) + { + if (i > 0 && ! gsvals.always_use_default_dispose) + { + layer_name = gimp_item_get_name (layers[i - 1]); + Disposal = parse_disposal_tag (layer_name); + g_free (layer_name); + } + else + { + Disposal = gsvals.default_dispose; + } + + layer_name = gimp_item_get_name (layers[i]); + Delay89 = parse_ms_tag (layer_name); + g_free (layer_name); + + if (Delay89 < 0 || gsvals.always_use_default_delay) + Delay89 = (gsvals.default_delay + 5) / 10; + else + Delay89 = (Delay89 + 5) / 10; + + /* don't allow a CPU-sucking completely 0-delay looping anim */ + if ((nlayers > 1) && gsvals.loop && (Delay89 == 0)) + { + static gboolean onceonly = FALSE; + + if (! onceonly) + { + g_message (_("Delay inserted to prevent evil " + "CPU-sucking animation.")); + onceonly = TRUE; + } + + Delay89 = 1; + } + + if (! gif_encode_graphic_control_ext (output, + Disposal, Delay89, nlayers, + cols, rows, + transparent, + useBPP, + get_pixel, + error)) + return FALSE; + } + + if (! gif_encode_image_data (output, cols, rows, + (rows > 4) ? gsvals.interlace : 0, + useBPP, + get_pixel, + offset_x, offset_y, + error)) + return FALSE; + + gimp_progress_update (1.0); + + g_object_unref (buffer); + + g_free (pixels); + } + + g_free (layers); + + if (! gif_encode_close (output, error)) + return FALSE; + + return TRUE; +} + +static gboolean +bad_bounds_dialog (void) +{ + GtkWidget *dialog; + gboolean crop; + + dialog = gtk_message_dialog_new (NULL, 0, + GTK_MESSAGE_WARNING, GTK_BUTTONS_NONE, + _("The image you are trying to export as a " + "GIF contains layers which extend beyond " + "the actual borders of the image.")); + + gtk_dialog_add_buttons (GTK_DIALOG (dialog), + _("_Cancel"), GTK_RESPONSE_CANCEL, + _("Cr_op"), GTK_RESPONSE_OK, + NULL); + + gtk_dialog_set_alternative_button_order (GTK_DIALOG (dialog), + GTK_RESPONSE_OK, + GTK_RESPONSE_CANCEL, + -1); + + gimp_window_set_transient (GTK_WINDOW (dialog)); + + gtk_message_dialog_format_secondary_text (GTK_MESSAGE_DIALOG (dialog), + _("The GIF file format does not " + "allow this. You may choose " + "whether to crop all of the " + "layers to the image borders, " + "or cancel this export.")); + + gtk_widget_show (dialog); + + crop = (gtk_dialog_run (GTK_DIALOG (dialog)) == GTK_RESPONSE_OK); + + gtk_widget_destroy (dialog); + + return crop; +} + +static GtkWidget * +file_gif_toggle_button_init (GtkBuilder *builder, + const gchar *name, + gboolean initial_value, + gboolean *value_pointer) +{ + GtkWidget *toggle = NULL; + + toggle = GTK_WIDGET (gtk_builder_get_object (builder, name)); + gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (toggle), initial_value); + g_signal_connect (toggle, "toggled", + G_CALLBACK (gimp_toggle_button_update), + value_pointer); + + return toggle; +} + +static GtkWidget * +file_gif_spin_button_int_init (GtkBuilder *builder, + const gchar *name, + int initial_value, + int *value_pointer) +{ + GtkWidget *spin_button = NULL; + GtkAdjustment *adjustment = NULL; + + spin_button = GTK_WIDGET (gtk_builder_get_object (builder, name)); + + adjustment = gtk_spin_button_get_adjustment (GTK_SPIN_BUTTON (spin_button)); + gtk_adjustment_set_value (adjustment, initial_value); + g_signal_connect (adjustment, "value-changed", + G_CALLBACK (gimp_int_adjustment_update), + value_pointer); + + return spin_button; +} + +static void +file_gif_combo_box_int_update_value (GtkComboBox *combo, + gint *value) +{ + GtkTreeIter iter; + + if (gtk_combo_box_get_active_iter (GTK_COMBO_BOX (combo), &iter)) + { + gtk_tree_model_get (gtk_combo_box_get_model (GTK_COMBO_BOX (combo)), + &iter, + DISPOSE_STORE_VALUE_COLUMN, value, + -1); + } +} + +static GtkWidget * +file_gif_combo_box_int_init (GtkBuilder *builder, + const gchar *name, + int initial_value, + int *value_pointer, + const gchar *first_label, + gint first_value, + ...) +{ + GtkWidget *combo = NULL; + GtkListStore *store = NULL; + const gchar *label = NULL; + gint value = 0; + GtkTreeIter iter = { 0, }; + va_list values; + + combo = GTK_WIDGET (gtk_builder_get_object (builder, name)); + store = GTK_LIST_STORE (gtk_combo_box_get_model (GTK_COMBO_BOX (combo))); + + /* Populate */ + va_start (values, first_value); + for (label = first_label, value = first_value; + label; + label = va_arg (values, const gchar *), value = va_arg (values, gint)) + { + gtk_list_store_append (store, &iter); + gtk_list_store_set (store, &iter, + DISPOSE_STORE_VALUE_COLUMN, value, + DISPOSE_STORE_LABEL_COLUMN, label, + -1); + } + va_end (values); + + /* Set initial value */ + gtk_tree_model_iter_nth_child (GTK_TREE_MODEL (store), + &iter, + NULL, + initial_value); + gtk_combo_box_set_active_iter (GTK_COMBO_BOX (combo), &iter); + + /* Arrange update of value */ + g_signal_connect (combo, "changed", + G_CALLBACK (file_gif_combo_box_int_update_value), + value_pointer); + + return combo; +} + +static gboolean +save_dialog (gint32 image_ID) +{ + GtkBuilder *builder = NULL; + gchar *ui_file = NULL; + GError *error = NULL; + GtkWidget *dialog; + GtkWidget *text_view; + GtkTextBuffer *text_buffer; + GtkWidget *toggle; + GtkWidget *frame; + GimpParasite *GIF2_CMNT; + gint32 nlayers; + gboolean animation_supported = FALSE; + gboolean run; + + g_free (gimp_image_get_layers (image_ID, &nlayers)); + animation_supported = nlayers > 1; + + dialog = gimp_export_dialog_new (_("GIF"), PLUG_IN_BINARY, SAVE_PROC); + + /* GtkBuilder init */ + builder = gtk_builder_new (); + ui_file = g_build_filename (gimp_data_directory (), + "ui/plug-ins/plug-in-file-gif.ui", + NULL); + if (! gtk_builder_add_from_file (builder, ui_file, &error)) + g_printerr (_("Error loading UI file '%s':\n%s"), + ui_file, error ? error->message : "???"); + g_free (ui_file); + + /* Main vbox */ + gtk_box_pack_start (GTK_BOX (gimp_export_dialog_get_content_area (dialog)), + GTK_WIDGET (gtk_builder_get_object (builder, "main-vbox")), + TRUE, TRUE, 0); + + /* regular gif parameter settings */ + file_gif_toggle_button_init (builder, "interlace", + gsvals.interlace, &gsvals.interlace); + file_gif_toggle_button_init (builder, "save-comment", + gsvals.save_comment, &gsvals.save_comment); + file_gif_toggle_button_init (builder, "as-animation", + gsvals.as_animation, &gsvals.as_animation); + + text_view = GTK_WIDGET (gtk_builder_get_object (builder, "comment")); + text_buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW (text_view)); + + if (globalcomment) + g_free (globalcomment); + + GIF2_CMNT = gimp_image_get_parasite (image_ID, "gimp-comment"); + if (GIF2_CMNT) + { + globalcomment = g_strndup (gimp_parasite_data (GIF2_CMNT), + gimp_parasite_data_size (GIF2_CMNT)); + gimp_parasite_free (GIF2_CMNT); + } + else + { + globalcomment = gimp_get_default_comment (); + } + + if (globalcomment) + gtk_text_buffer_set_text (text_buffer, globalcomment, -1); + + g_signal_connect (text_buffer, "changed", + G_CALLBACK (comment_entry_callback), + NULL); + + /* additional animated gif parameter settings */ + file_gif_toggle_button_init (builder, "loop-forever", + gsvals.loop, &gsvals.loop); + + /* default_delay entry field */ + file_gif_spin_button_int_init (builder, "delay-spin", + gsvals.default_delay, &gsvals.default_delay); + + /* Disposal selector */ + file_gif_combo_box_int_init (builder, "dispose-combo", + gsvals.default_dispose, &gsvals.default_dispose, + _("I don't care"), + DISPOSE_UNSPECIFIED, + _("Cumulative layers (combine)"), + DISPOSE_COMBINE, + _("One frame per layer (replace)"), + DISPOSE_REPLACE, + NULL); + + /* The "Always use default values" toggles */ + file_gif_toggle_button_init (builder, "use-default-delay", + gsvals.always_use_default_delay, + &gsvals.always_use_default_delay); + file_gif_toggle_button_init (builder, "use-default-dispose", + gsvals.always_use_default_dispose, + &gsvals.always_use_default_dispose); + + frame = GTK_WIDGET (gtk_builder_get_object (builder, "animation-frame")); + toggle = GTK_WIDGET (gtk_builder_get_object (builder, "as-animation")); + if (! animation_supported) + { + gimp_help_set_help_data (toggle, + _("You can only export as animation when the " + "image has more than one layer. The image " + "you are trying to export only has one " + "layer."), + NULL); + /* Make sure the checkbox is not checked from session data. */ + gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (toggle), FALSE); + } + gtk_widget_set_sensitive (toggle, animation_supported); + + g_object_bind_property (toggle, "active", + frame, "sensitive", + G_BINDING_SYNC_CREATE); + + gtk_widget_show (dialog); + + run = (gimp_dialog_run (GIMP_DIALOG (dialog)) == GTK_RESPONSE_OK); + + gtk_widget_destroy (dialog); + + return run; +} + +static int +colors_to_bpp (gint colors) +{ + gint bpp; + + if (colors <= 2) + bpp = 1; + else if (colors <= 4) + bpp = 2; + else if (colors <= 8) + bpp = 3; + else if (colors <= 16) + bpp = 4; + else if (colors <= 32) + bpp = 5; + else if (colors <= 64) + bpp = 6; + else if (colors <= 128) + bpp = 7; + else if (colors <= 256) + bpp = 8; + else + { + g_warning ("GIF: colors_to_bpp - Eep! too many colors: %d\n", colors); + return 8; + } + + return bpp; +} + +static int +bpp_to_colors (gint bpp) +{ + gint colors; + + if (bpp > 8) + { + g_warning ("GIF: bpp_to_colors - Eep! bpp==%d !\n", bpp); + return 256; + } + + colors = 1 << bpp; + + return colors; +} + + + +static gint +get_pixel (gint x, + gint y) +{ + return *(pixels + (rowstride * (long) y) + (long) x); +} + + +/***************************************************************************** + * + * GIFENCODE.C - GIF Image compression interface + * + * GIFEncode( FName, GHeight, GWidth, GInterlace, Background, Transparent, + * BitsPerPixel, Red, Green, Blue, get_pixel ) + * + *****************************************************************************/ + +static gint Width, Height; +static gint curx, cury; +static glong CountDown; +static gint Pass = 0; + +/* + * Bump the 'curx' and 'cury' to point to the next pixel + */ +static void +bump_pixel (void) +{ + /* + * Bump the current X position + */ + curx++; + + /* + * If we are at the end of a scan line, set curx back to the beginning + * If we are interlaced, bump the cury to the appropriate spot, + * otherwise, just increment it. + */ + if (curx == Width) + { + cur_progress++; + + if ((cur_progress % 20) == 0) + gimp_progress_update ((gdouble) cur_progress / (gdouble) max_progress); + + curx = 0; + + if (! Interlace) + ++cury; + else + { + switch (Pass) + { + + case 0: + cury += 8; + if (cury >= Height) + { + Pass++; + cury = 4; + } + break; + + case 1: + cury += 8; + if (cury >= Height) + { + Pass++; + cury = 2; + } + break; + + case 2: + cury += 4; + if (cury >= Height) + { + Pass++; + cury = 1; + } + break; + + case 3: + cury += 2; + break; + } + } + } +} + +/* + * Return the next pixel from the image + */ +static gint +gif_next_pixel (ifunptr getpixel) +{ + gint r; + + if (CountDown == 0) + return EOF; + + --CountDown; + + r = (*getpixel) (curx, cury); + + bump_pixel (); + + return r; +} + +/* public */ + +static gboolean +gif_encode_header (GOutputStream *output, + gboolean gif89, + gint GWidth, + gint GHeight, + gint Background, + gint BitsPerPixel, + gint Red[], + gint Green[], + gint Blue[], + ifunptr get_pixel, + GError **error) +{ + gint B; + gint RWidth, RHeight; + gint Resolution; + gint ColorMapSize; + gint i; + + ColorMapSize = 1 << BitsPerPixel; + + RWidth = Width = GWidth; + RHeight = Height = GHeight; + + Resolution = BitsPerPixel; + + /* + * Calculate number of bits we are expecting + */ + CountDown = (long) Width *(long) Height; + + /* + * Indicate which pass we are on (if interlace) + */ + Pass = 0; + + /* + * Set up the current x and y position + */ + curx = cury = 0; + + /* + * Write the Magic header + */ + if (! put_string (output, gif89 ? "GIF89a" : "GIF87a", error)) + return FALSE; + + /* + * Write out the screen width and height + */ + if (! put_word (output, RWidth, error) || + ! put_word (output, RHeight, error)) + return FALSE; + + /* + * Indicate that there is a global color map + */ + B = 0x80; /* Yes, there is a color map */ + + /* + * OR in the resolution + */ + B |= (Resolution - 1) << 5; + + /* + * OR in the Bits per Pixel + */ + B |= (BitsPerPixel - 1); + + /* + * Write it out + */ + if (! put_byte (output, B, error)) + return FALSE; + + /* + * Write out the Background color + */ + if (! put_byte (output, Background, error)) + return FALSE; + + /* + * Byte of 0's (future expansion) + */ + if (! put_byte (output, 0, error)) + return FALSE; + + /* + * Write out the Global Color Map + */ + for (i = 0; i < ColorMapSize; i++) + { + if (! put_byte (output, Red[i], error) || + ! put_byte (output, Green[i], error) || + ! put_byte (output, Blue[i], error)) + return FALSE; + } + + return TRUE; +} + + +static gboolean +gif_encode_graphic_control_ext (GOutputStream *output, + int Disposal, + int Delay89, + int NumFramesInImage, + int GWidth, + int GHeight, + int Transparent, + int BitsPerPixel, + ifunptr get_pixel, + GError **error) +{ + Width = GWidth; + Height = GHeight; + + /* + * Calculate number of bits we are expecting + */ + CountDown = (long) Width *(long) Height; + + /* + * Indicate which pass we are on (if interlace) + */ + Pass = 0; + + /* + * Set up the current x and y position + */ + curx = cury = 0; + + /* + * Write out extension for transparent color index, if necessary. + */ + if ( (Transparent >= 0) || (NumFramesInImage > 1) ) + { + /* Extension Introducer - fixed. */ + if (! put_byte (output, '!', error)) + return FALSE; + + /* Graphic Control Label - fixed. */ + if (! put_byte (output, 0xf9, error)) + return FALSE; + + /* Block Size - fixed. */ + if (! put_byte (output, 4, error)) + return FALSE; + + /* Packed Fields - XXXdddut (d=disposal, u=userInput, t=transFlag) */ + /* s8421 */ + if (! put_byte (output, + ((Transparent >= 0) ? 0x01 : 0x00) /* TRANSPARENCY */ + + /* DISPOSAL */ + | ((NumFramesInImage > 1) ? (Disposal << 2) : 0x00 ), + /* 0x03 or 0x01 build frames cumulatively */ + /* 0x02 clears frame before drawing */ + /* 0x00 'don't care' */ + + error)) + return FALSE; + + if (! put_word (output, Delay89, error)) + return FALSE; + + if (! put_byte (output, Transparent, error) || + ! put_byte (output, 0, error)) + return FALSE; + } + + return TRUE; +} + + +static gboolean +gif_encode_image_data (GOutputStream *output, + int GWidth, + int GHeight, + int GInterlace, + int BitsPerPixel, + ifunptr get_pixel, + gint offset_x, + gint offset_y, + GError **error) +{ + gint LeftOfs, TopOfs; + gint InitCodeSize; + + Interlace = GInterlace; + + Width = GWidth; + Height = GHeight; + LeftOfs = (gint) offset_x; + TopOfs = (gint) offset_y; + + /* + * Calculate number of bits we are expecting + */ + CountDown = (long) Width * (long) Height; + + /* + * Indicate which pass we are on (if interlace) + */ + Pass = 0; + + /* + * The initial code size + */ + if (BitsPerPixel <= 1) + InitCodeSize = 2; + else + InitCodeSize = BitsPerPixel; + + /* + * Set up the current x and y position + */ + curx = cury = 0; + + /* + * Write an Image separator + */ + if (! put_byte (output, ',', error)) + return FALSE; + + /* + * Write the Image header + */ + + if (! put_word (output, LeftOfs, error) || + ! put_word (output, TopOfs, error) || + ! put_word (output, Width, error) || + ! put_word (output, Height, error)) + return FALSE; + + /* + * Write out whether or not the image is interlaced + */ + if (Interlace) + { + if (! put_byte (output, 0x40, error)) + return FALSE; + } + else + { + if (! put_byte (output, 0x00, error)) + return FALSE; + } + + /* + * Write out the initial code size + */ + if (! put_byte (output, InitCodeSize, error)) + return FALSE; + + /* + * Go and actually compress the data + */ + if (! compress (output, InitCodeSize + 1, get_pixel, error)) + return FALSE; + + /* + * Write out a Zero-length packet (to end the series) + */ + if (! put_byte (output, 0, error)) + return FALSE; + +#if 0 + /***************************/ + Interlace = GInterlace; + Width = GWidth; + Height = GHeight; + LeftOfs = TopOfs = 0; + + CountDown = (long) Width *(long) Height; + Pass = 0; + /* + * The initial code size + */ + if (BitsPerPixel <= 1) + InitCodeSize = 2; + else + InitCodeSize = BitsPerPixel; + /* + * Set up the current x and y position + */ + curx = cury = 0; +#endif + + return TRUE; +} + + +static gboolean +gif_encode_close (GOutputStream *output, + GError **error) +{ + /* + * Write the GIF file terminator + */ + if (! put_byte (output, ';', error)) + return FALSE; + + /* + * And close the file + */ + return g_output_stream_close (output, NULL, error); +} + + +static gboolean +gif_encode_loop_ext (GOutputStream *output, + guint num_loops, + GError **error) +{ + return (put_byte (output, 0x21, error) && + put_byte (output, 0xff, error) && + put_byte (output, 0x0b, error) && + put_string (output, "NETSCAPE2.0", error) && + put_byte (output, 0x03, error) && + put_byte (output, 0x01, error) && + put_word (output, num_loops, error) && + put_byte (output, 0x00, error)); + + /* NOTE: num_loops == 0 means 'loop infinitely' */ +} + + +static gboolean +gif_encode_comment_ext (GOutputStream *output, + const gchar *comment, + GError **error) +{ + if (!comment || !*comment) + return TRUE; + + if (strlen (comment) > 240) + { + g_printerr ("GIF: warning:" + "comment too large - comment block not written.\n"); + return TRUE; + } + + return (put_byte (output, 0x21, error) && + put_byte (output, 0xfe, error) && + put_byte (output, strlen (comment), error) && + put_string (output, comment, error) && + put_byte (output, 0x00, error)); +} + + +/* + * Write stuff to the GIF file + */ +static gboolean +put_byte (GOutputStream *output, + guchar b, + GError **error) +{ + return g_data_output_stream_put_byte (G_DATA_OUTPUT_STREAM (output), + b, NULL, error); +} + +static gboolean +put_word (GOutputStream *output, + gint w, + GError **error) +{ + return g_data_output_stream_put_int16 (G_DATA_OUTPUT_STREAM (output), + w, NULL, error); +} + +static gboolean +put_string (GOutputStream *output, + const gchar *s, + GError **error) +{ + return g_data_output_stream_put_string (G_DATA_OUTPUT_STREAM (output), + s, NULL, error); +} + + +/*************************************************************************** + * + * GIFCOMPR.C - GIF Image compression routines + * + * Lempel-Ziv compression based on 'compress'. GIF modifications by + * David Rowley (mgardi@watdcsu.waterloo.edu) + * + ***************************************************************************/ + +/* + * General DEFINEs + */ + +#define GIF_BITS 12 + +#define HSIZE 5003 /* 80% occupancy */ + +/* + * GIF Image compression - modified 'compress' + * + * Based on: compress.c - File compression ala IEEE Computer, June 1984. + * + * By Authors: Spencer W. Thomas (decvax!harpo!utah-cs!utah-gr!thomas) + * Jim McKie (decvax!mcvax!jim) + * Steve Davies (decvax!vax135!petsd!peora!srd) + * Ken Turkowski (decvax!decwrl!turtlevax!ken) + * James A. Woods (decvax!ihnp4!ames!jaw) + * Joe Orost (decvax!vax135!petsd!joe) + * + */ + +static gint n_bits; /* number of bits/code */ +static gint maxbits = GIF_BITS; /* user settable max # bits/code */ +static gint maxcode; /* maximum code, given n_bits */ +static gint maxmaxcode = (gint) 1 << GIF_BITS; /* should NEVER generate this code */ +#ifdef COMPATIBLE /* But wrong! */ +#define MAXCODE(Mn_bits) ((gint) 1 << (Mn_bits) - 1) +#else /*COMPATIBLE */ +#define MAXCODE(Mn_bits) (((gint) 1 << (Mn_bits)) - 1) +#endif /*COMPATIBLE */ + +static glong htab[HSIZE]; +static gushort codetab[HSIZE]; +#define HashTabOf(i) htab[i] +#define CodeTabOf(i) codetab[i] + +static const gint hsize = HSIZE; /* the original reason for this being + variable was "for dynamic table sizing", + but since it was never actually changed + I made it const --Adam. */ + +static gint free_ent = 0; /* first unused entry */ + +/* + * block compression parameters -- after all codes are used up, + * and compression rate changes, start over. + */ +static gint clear_flg = 0; + +static gint offset; +static glong in_count = 1; /* length of input */ +static glong out_count = 0; /* # of codes output (for debugging) */ + +/* + * compress stdin to stdout + * + * Algorithm: use open addressing double hashing (no chaining) on the + * prefix code / next character combination. We do a variant of Knuth's + * algorithm D (vol. 3, sec. 6.4) along with G. Knott's relatively-prime + * secondary probe. Here, the modular division first probe is gives way + * to a faster exclusive-or manipulation. Also do block compression with + * an adaptive reset, whereby the code table is cleared when the compression + * ratio decreases, but after the table fills. The variable-length output + * codes are re-sized at this point, and a special CLEAR code is generated + * for the decompressor. Late addition: construct the table according to + * file size for noticeable speed improvement on small files. Please direct + * questions about this implementation to ames!jaw. + */ + +static gint g_init_bits; + +static gint ClearCode; +static gint EOFCode; + +static gulong cur_accum; +static gint cur_bits; + +static gulong masks[] = +{ + 0x0000, 0x0001, 0x0003, 0x0007, + 0x000F, 0x001F, 0x003F, 0x007F, + 0x00FF, 0x01FF, 0x03FF, 0x07FF, + 0x0FFF, 0x1FFF, 0x3FFF, 0x7FFF, + 0xFFFF +}; + + +static gboolean +compress (GOutputStream *output, + gint init_bits, + ifunptr ReadValue, + GError **error) +{ + if (FALSE) + return no_compress (output, init_bits, ReadValue, error); + else if (FALSE) + return rle_compress (output, init_bits, ReadValue, error); + else + return normal_compress (output, init_bits, ReadValue, error); +} + +static gboolean +no_compress (GOutputStream *output, + gint init_bits, + ifunptr ReadValue, + GError **error) +{ + glong fcode; + gint i /* = 0 */ ; + gint c; + gint ent; + gint hsize_reg; + gint hshift; + + /* + * Set up the globals: g_init_bits - initial number of bits + */ + g_init_bits = init_bits; + + cur_bits = 0; + cur_accum = 0; + + /* + * Set up the necessary values + */ + offset = 0; + out_count = 0; + clear_flg = 0; + in_count = 1; + + ClearCode = (1 << (init_bits - 1)); + EOFCode = ClearCode + 1; + free_ent = ClearCode + 2; + + + /* Had some problems here... should be okay now. --Adam */ + n_bits = g_init_bits; + maxcode = MAXCODE (n_bits); + + + char_init(); + + ent = gif_next_pixel (ReadValue); + + hshift = 0; + for (fcode = (long) hsize; fcode < 65536L; fcode *= 2L) + ++hshift; + hshift = 8 - hshift; /* set hash code range bound */ + + hsize_reg = hsize; + cl_hash ((glong) hsize_reg); /* clear hash table */ + + if (! output_code (output, (gint) ClearCode, error)) + return FALSE; + + while ((c = gif_next_pixel (ReadValue)) != EOF) + { + ++in_count; + + fcode = (long) (((long) c << maxbits) + ent); + i = (((gint) c << hshift) ^ ent); /* xor hashing */ + + if (! output_code (output, (gint) ent, error)) + return FALSE; + + ++out_count; + ent = c; + + if (free_ent < maxmaxcode) + { + CodeTabOf (i) = free_ent++; /* code -> hashtable */ + HashTabOf (i) = fcode; + } + else + { + if (! cl_block (output, error)) + return FALSE; + } + } + + /* + * Put out the final code. + */ + if (! output_code (output, (gint) ent, error)) + return FALSE; + + ++out_count; + + if (! output_code (output, (gint) EOFCode, error)) + return FALSE; + + return TRUE; +} + +static gboolean +rle_compress (GOutputStream *output, + gint init_bits, + ifunptr ReadValue, + GError **error) +{ + glong fcode; + gint i /* = 0 */ ; + gint c, last; + gint ent; + gint disp; + gint hsize_reg; + gint hshift; + + /* + * Set up the globals: g_init_bits - initial number of bits + */ + g_init_bits = init_bits; + + cur_bits = 0; + cur_accum = 0; + + /* + * Set up the necessary values + */ + offset = 0; + out_count = 0; + clear_flg = 0; + in_count = 1; + + ClearCode = (1 << (init_bits - 1)); + EOFCode = ClearCode + 1; + free_ent = ClearCode + 2; + + + /* Had some problems here... should be okay now. --Adam */ + n_bits = g_init_bits; + maxcode = MAXCODE (n_bits); + + + char_init (); + + last = ent = gif_next_pixel (ReadValue); + + hshift = 0; + for (fcode = (long) hsize; fcode < 65536L; fcode *= 2L) + ++hshift; + hshift = 8 - hshift; /* set hash code range bound */ + + hsize_reg = hsize; + cl_hash ((glong) hsize_reg); /* clear hash table */ + + if (! output_code (output, (gint) ClearCode, error)) + return FALSE; + + + while ((c = gif_next_pixel (ReadValue)) != EOF) + { + ++in_count; + + fcode = (long) (((long) c << maxbits) + ent); + i = (((gint) c << hshift) ^ ent); /* xor hashing */ + + + if (last == c) { + if (HashTabOf (i) == fcode) + { + ent = CodeTabOf (i); + continue; + } + else if ((long) HashTabOf (i) < 0) /* empty slot */ + goto nomatch; + disp = hsize_reg - i; /* secondary hash (after G. Knott) */ + if (i == 0) + disp = 1; + probe: + if ((i -= disp) < 0) + i += hsize_reg; + + if (HashTabOf (i) == fcode) + { + ent = CodeTabOf (i); + continue; + } + if ((long) HashTabOf (i) > 0) + goto probe; + } + nomatch: + if (! output_code (output, (gint) ent, error)) + return FALSE; + + ++out_count; + last = ent = c; + if (free_ent < maxmaxcode) + { + CodeTabOf (i) = free_ent++; /* code -> hashtable */ + HashTabOf (i) = fcode; + } + else + { + if (! cl_block (output, error)) + return FALSE; + } + } + + /* + * Put out the final code. + */ + if (! output_code (output, (gint) ent, error)) + return FALSE; + + ++out_count; + + if (! output_code (output, (gint) EOFCode, error)) + return FALSE; + + return TRUE; +} + +static gboolean +normal_compress (GOutputStream *output, + gint init_bits, + ifunptr ReadValue, + GError **error) +{ + glong fcode; + gint i /* = 0 */ ; + gint c; + gint ent; + gint disp; + gint hsize_reg; + gint hshift; + + /* + * Set up the globals: g_init_bits - initial number of bits + */ + g_init_bits = init_bits; + + cur_bits = 0; + cur_accum = 0; + + /* + * Set up the necessary values + */ + offset = 0; + out_count = 0; + clear_flg = 0; + in_count = 1; + + ClearCode = (1 << (init_bits - 1)); + EOFCode = ClearCode + 1; + free_ent = ClearCode + 2; + + + /* Had some problems here... should be okay now. --Adam */ + n_bits = g_init_bits; + maxcode = MAXCODE (n_bits); + + + char_init(); + + ent = gif_next_pixel (ReadValue); + + hshift = 0; + for (fcode = (long) hsize; fcode < 65536L; fcode *= 2L) + ++hshift; + hshift = 8 - hshift; /* set hash code range bound */ + + hsize_reg = hsize; + cl_hash ((glong) hsize_reg); /* clear hash table */ + + if (! output_code (output, (gint) ClearCode, error)) + return FALSE; + + + while ((c = gif_next_pixel (ReadValue)) != EOF) + { + ++in_count; + + fcode = (long) (((long) c << maxbits) + ent); + i = (((gint) c << hshift) ^ ent); /* xor hashing */ + + if (HashTabOf (i) == fcode) + { + ent = CodeTabOf (i); + continue; + } + else if ((long) HashTabOf (i) < 0) /* empty slot */ + goto nomatch; + disp = hsize_reg - i; /* secondary hash (after G. Knott) */ + if (i == 0) + disp = 1; + probe: + if ((i -= disp) < 0) + i += hsize_reg; + + if (HashTabOf (i) == fcode) + { + ent = CodeTabOf (i); + continue; + } + if ((long) HashTabOf (i) > 0) + goto probe; + nomatch: + if (! output_code (output, (gint) ent, error)) + return FALSE; + + ++out_count; + ent = c; + if (free_ent < maxmaxcode) + { + CodeTabOf (i) = free_ent++; /* code -> hashtable */ + HashTabOf (i) = fcode; + } + else + { + if (! cl_block (output, error)) + return FALSE; + } + } + + /* + * Put out the final code. + */ + if (! output_code (output, (gint) ent, error)) + return FALSE; + + ++out_count; + + if (! output_code (output, (gint) EOFCode, error)) + return FALSE; + + return TRUE; +} + + +/***************************************************************** + * TAG( output ) + * + * Output the given code. + * Inputs: + * code: A n_bits-bit integer. If == -1, then EOF. This assumes + * that n_bits =< (long)wordsize - 1. + * Outputs: + * Outputs code to the file. + * Assumptions: + * Chars are 8 bits long. + * Algorithm: + * Maintain a GIF_BITS character long buffer (so that 8 codes will + * fit in it exactly). Use the VAX insv instruction to insert each + * code in turn. When the buffer fills up empty it and start over. + */ + +static gboolean +output_code (GOutputStream *output, + gint code, + GError **error) +{ + cur_accum &= masks[cur_bits]; + + if (cur_bits > 0) + cur_accum |= ((long) code << cur_bits); + else + cur_accum = code; + + cur_bits += n_bits; + + while (cur_bits >= 8) + { + if (! char_out (output, (guchar) (cur_accum & 0xff), error)) + return FALSE; + + cur_accum >>= 8; + cur_bits -= 8; + } + + /* + * If the next entry is going to be too big for the code size, + * then increase it, if possible. + */ + if (free_ent > maxcode || clear_flg) + { + if (clear_flg) + { + maxcode = MAXCODE (n_bits = g_init_bits); + clear_flg = 0; + } + else + { + ++n_bits; + if (n_bits == maxbits) + maxcode = maxmaxcode; + else + maxcode = MAXCODE (n_bits); + } + } + + if (code == EOFCode) + { + /* + * At EOF, write the rest of the buffer. + */ + while (cur_bits > 0) + { + if (! char_out (output, (guchar) (cur_accum & 0xff), error)) + return FALSE; + + cur_accum >>= 8; + cur_bits -= 8; + } + + if (! char_flush (output, error)) + return FALSE; + } + + return TRUE; +} + +/* + * Clear out the hash table + */ +static gboolean +cl_block (GOutputStream *output, + GError **error) /* table clear for block compress */ +{ + cl_hash ((glong) hsize); + free_ent = ClearCode + 2; + clear_flg = 1; + + return output_code (output, (gint) ClearCode, error); +} + +static void +cl_hash (glong hsize) /* reset code table */ +{ + glong *htab_p = htab + hsize; + + long i; + long m1 = -1; + + i = hsize - 16; + do + { /* might use Sys V memset(3) here */ + *(htab_p - 16) = m1; + *(htab_p - 15) = m1; + *(htab_p - 14) = m1; + *(htab_p - 13) = m1; + *(htab_p - 12) = m1; + *(htab_p - 11) = m1; + *(htab_p - 10) = m1; + *(htab_p - 9) = m1; + *(htab_p - 8) = m1; + *(htab_p - 7) = m1; + *(htab_p - 6) = m1; + *(htab_p - 5) = m1; + *(htab_p - 4) = m1; + *(htab_p - 3) = m1; + *(htab_p - 2) = m1; + *(htab_p - 1) = m1; + htab_p -= 16; + } + while ((i -= 16) >= 0); + + for (i += 16; i > 0; --i) + *--htab_p = m1; +} + + +/****************************************************************************** + * GIF Specific routines + ******************************************************************************/ + +/* + * Number of characters so far in this 'packet' + */ +static int a_count; + +/* + * Set up the 'byte output' routine + */ +static void +char_init (void) +{ + a_count = 0; +} + +/* + * Define the storage for the packet accumulator + */ +static char accum[256]; + +/* + * Add a character to the end of the current packet, and if it is 254 + * characters, flush the packet to disk. + */ +static gboolean +char_out (GOutputStream *output, + gint c, + GError **error) +{ + accum[a_count++] = c; + + if (a_count >= 254) + return char_flush (output, error); + + return TRUE; +} + +/* + * Flush the packet to disk, and reset the accumulator + */ +static gboolean +char_flush (GOutputStream *output, + GError **error) +{ + if (a_count > 0) + { + if (! put_byte (output, a_count, error)) + return FALSE; + + if (! g_output_stream_write_all (output, accum, a_count, + NULL, NULL, error)) + return FALSE; + + a_count = 0; + } + + return TRUE; +} + + +/* Save interface functions */ + +static void +comment_entry_callback (GtkTextBuffer *buffer) +{ + GtkTextIter start_iter; + GtkTextIter end_iter; + gchar *text; + + gtk_text_buffer_get_bounds (buffer, &start_iter, &end_iter); + text = gtk_text_buffer_get_text (buffer, &start_iter, &end_iter, FALSE); + +#define MAX_COMMENT 240 + + if (strlen (text) > MAX_COMMENT) + { + /* translators: the %d is *always* 240 here */ + g_message (_("The default comment is limited to %d characters."), + MAX_COMMENT); + + gtk_text_buffer_get_iter_at_offset (buffer, &start_iter, MAX_COMMENT - 1); + gtk_text_buffer_get_end_iter (buffer, &end_iter); + + /* this calls us recursivaly, but in the else branch + */ + gtk_text_buffer_delete (buffer, &start_iter, &end_iter); + } + else + { + g_free (globalcomment); + globalcomment = g_strdup (text); + comment_was_edited = TRUE; + } + + g_free (text); +} |