/* PIKA - Photo and Image Kooker Application * a rebranding of The GNU Image Manipulation Program (created with heckimp) * A derived work which may be trivial. However, any changes may be (C)2023 by Aldercone Studio * * Original copyright, applying to most contents (license remains unchanged): * Copyright (C) 1995 Spencer Kimball and Peter Mattis * * Multiple-image Network Graphics (MNG) plug-in * * Copyright (C) 2002 Mukund Sivaraman * Portions are copyright of the authors of the file-gif-save, file-png-save * and file-jpeg-save plug-ins' code. The exact ownership of these code * fragments has not been determined. * * This work was sponsored by Xinit Systems Limited, UK. * http://www.xinitsystems.com/ * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * -- * * For now, this MNG plug-in can only save images. It cannot load images. * Save your working copy as .xcf and use this for "exporting" your images * to MNG. Supports animation the same way as animated GIFs. Supports alpha * transparency. Uses the libmng library (http://www.libmng.com/). * The MIME content-type for MNG images is video/x-mng for now. Make sure * your web-server is configured appropriately if you want to serve MNG * images. * * Since libmng cannot write PNG, JNG and delta PNG chunks at this time * (when this text was written), this plug-in uses libpng and jpeglib to * create the data for the chunks. * */ #include "config.h" #include #include #include #include #include #ifdef HAVE_UNISTD_H #include #endif /* libpng and jpeglib are currently used in this plug-in. */ #include #include /* Grrr. The grrr is because the following have to be defined by the * application as well for some reason, although they were enabled * when libmng was compiled. The authors of libmng must look into * this. */ #if !defined(MNG_SUPPORT_FULL) #define MNG_SUPPORT_FULL 1 #endif #if !defined(MNG_SUPPORT_READ) #define MNG_SUPPORT_READ 1 #endif #if !defined(MNG_SUPPORT_WRITE) #define MNG_SUPPORT_WRITE 1 #endif #if !defined(MNG_SUPPORT_DISPLAY) #define MNG_SUPPORT_DISPLAY 1 #endif #if !defined(MNG_ACCESS_CHUNKS) #define MNG_ACCESS_CHUNKS 1 #endif #include #include "libpika/pika.h" #include "libpika/pikaui.h" #include "libpika/stdplugins-intl.h" #define SAVE_PROC "file-mng-save" #define PLUG_IN_BINARY "file-mng" #define PLUG_IN_ROLE "pika-file-mng" #define SCALE_WIDTH 125 enum { CHUNKS_PNG_D, CHUNKS_JNG_D, CHUNKS_PNG, CHUNKS_JNG }; enum { DISPOSE_COMBINE, DISPOSE_REPLACE }; /* These are not saved or restored. */ struct mng_globals_t { gboolean has_trns; png_bytep trans; int num_trans; gboolean has_plte; png_colorp palette; int num_palette; }; /* The output FILE pointer which is used by libmng; passed around as * user data. */ struct mnglib_userdata_t { FILE *fp; }; typedef struct _Mng Mng; typedef struct _MngClass MngClass; struct _Mng { PikaPlugIn parent_instance; }; struct _MngClass { PikaPlugInClass parent_class; }; #define MNG_TYPE (mng_get_type ()) #define MNG(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), MNG_TYPE, Mng)) GType mng_get_type (void) G_GNUC_CONST; static GList * mng_query_procedures (PikaPlugIn *plug_in); static PikaProcedure * mng_create_procedure (PikaPlugIn *plug_in, const gchar *name); static PikaValueArray * mng_save (PikaProcedure *procedure, PikaRunMode run_mode, PikaImage *image, gint n_drawables, PikaDrawable **drawables, GFile *file, PikaMetadata *metadata, PikaProcedureConfig *config, gpointer run_data); static mng_ptr MNG_DECL myalloc (mng_size_t size); static void MNG_DECL myfree (mng_ptr ptr, mng_size_t size); static mng_bool MNG_DECL myopenstream (mng_handle handle); static mng_bool MNG_DECL myclosestream (mng_handle handle); static mng_bool MNG_DECL mywritedata (mng_handle handle, mng_ptr buf, mng_uint32 size, mng_uint32 *written_size); static gint32 parse_chunks_type_from_layer_name (const gchar *str, gint default_chunks); static gint32 parse_disposal_type_from_layer_name (const gchar *str, gint default_dispose); static gint32 parse_ms_tag_from_layer_name (const gchar *str, gint default_delay); static gint find_unused_ia_color (guchar *pixels, gint numpixels, gint *colors); static gboolean ia_has_transparent_pixels (guchar *pixels, gint numpixels); static gboolean respin_cmap (png_structp png_ptr, png_infop png_info_ptr, guchar *remap, PikaImage *image, GeglBuffer *buffer, int *bit_depth); static gboolean mng_save_image (GFile *file, PikaImage *image, gint n_drawables, PikaDrawable **drawables, GObject *config, GError **error); static gboolean mng_save_dialog (PikaImage *image, PikaProcedure *procedure, GObject *config); G_DEFINE_TYPE (Mng, mng, PIKA_TYPE_PLUG_IN) PIKA_MAIN (MNG_TYPE) DEFINE_STD_SET_I18N static struct mng_globals_t mngg; static void mng_class_init (MngClass *klass) { PikaPlugInClass *plug_in_class = PIKA_PLUG_IN_CLASS (klass); plug_in_class->query_procedures = mng_query_procedures; plug_in_class->create_procedure = mng_create_procedure; plug_in_class->set_i18n = STD_SET_I18N; } static void mng_init (Mng *mng) { } static GList * mng_query_procedures (PikaPlugIn *plug_in) { return g_list_append (NULL, g_strdup (SAVE_PROC)); } static PikaProcedure * mng_create_procedure (PikaPlugIn *plug_in, const gchar *name) { PikaProcedure *procedure = NULL; if (! strcmp (name, SAVE_PROC)) { procedure = pika_save_procedure_new (plug_in, name, PIKA_PDB_PROC_TYPE_PLUGIN, FALSE, mng_save, NULL, NULL); pika_procedure_set_image_types (procedure, "*"); pika_procedure_set_menu_label (procedure, _("MNG animation")); pika_file_procedure_set_format_name (PIKA_FILE_PROCEDURE (procedure), _("MNG")); pika_procedure_set_documentation (procedure, _("Saves images in the MNG file format"), _("This plug-in saves images in the " "Multiple-image Network Graphics (MNG) " "format which can be used as a " "replacement for animated GIFs, and " "more."), name); pika_procedure_set_attribution (procedure, "Mukund Sivaraman ", "Mukund Sivaraman ", "November 19, 2002"); pika_file_procedure_set_mime_types (PIKA_FILE_PROCEDURE (procedure), "image/x-mng"); pika_file_procedure_set_extensions (PIKA_FILE_PROCEDURE (procedure), "mng"); PIKA_PROC_ARG_BOOLEAN (procedure, "interlaced", _("_Interlace"), _("Use interlacing"), FALSE, G_PARAM_READWRITE); PIKA_PROC_ARG_INT (procedure, "png-compression", _("_PNG compression level"), _("PNG compression level, choose a high compression " "level for small file size"), 0, 9, 9, G_PARAM_READWRITE); PIKA_PROC_ARG_DOUBLE (procedure, "jpeg-quality", _("JPEG compression _quality"), _("JPEG quality factor"), 0, 1, 0.75, G_PARAM_READWRITE); PIKA_PROC_ARG_DOUBLE (procedure, "jpeg-smoothing", _("_JPEG smoothing factor"), _("JPEG smoothing factor"), 0, 1, 0, G_PARAM_READWRITE); PIKA_PROC_ARG_BOOLEAN (procedure, "loop", _("L_oop"), _("(ANIMATED MNG) Loop infinitely"), TRUE, G_PARAM_READWRITE); PIKA_PROC_ARG_INT (procedure, "default-delay", _("Default fra_me delay"), _("(ANIMATED MNG) Default delay between frames in " "milliseconds"), 1, G_MAXINT, 100, G_PARAM_READWRITE); PIKA_PROC_ARG_INT (procedure, "default-chunks", _("Default chunks t_ype"), _("(ANIMATED MNG) Default chunks type " "(0 = PNG + Delta PNG; 1 = JNG + Delta PNG; " "2 = All PNG; 3 = All JNG)"), 0, 3, CHUNKS_PNG_D, G_PARAM_READWRITE); PIKA_PROC_ARG_INT (procedure, "default-dispose", _("De_fault frame disposal"), _("(ANIMATED MNG) Default dispose type " "(0 = combine; 1 = replace)"), 0, 1, DISPOSE_COMBINE, G_PARAM_READWRITE); PIKA_PROC_ARG_BOOLEAN (procedure, "bkgd", _("Save _background color"), _("Write bKGd (background color) chunk"), FALSE, G_PARAM_READWRITE); PIKA_PROC_ARG_BOOLEAN (procedure, "gama", _("Save _gamma"), _("Write gAMA (gamma) chunk"), FALSE, G_PARAM_READWRITE); PIKA_PROC_ARG_BOOLEAN (procedure, "phys", _("Sa_ve resolution"), _("Write pHYs (image resolution) chunk"), TRUE, G_PARAM_READWRITE); PIKA_PROC_ARG_BOOLEAN (procedure, "time", _("Save creation _time"), _("Write tIME (creation time) chunk"), TRUE, G_PARAM_READWRITE); } return procedure; } static PikaValueArray * mng_save (PikaProcedure *procedure, PikaRunMode run_mode, PikaImage *image, gint n_drawables, PikaDrawable **drawables, GFile *file, PikaMetadata *metadata, PikaProcedureConfig *config, gpointer run_data) { PikaPDBStatusType status = PIKA_PDB_SUCCESS; PikaExportReturn export = PIKA_EXPORT_IGNORE; GError *error = NULL; gegl_init (NULL, NULL); if (run_mode == PIKA_RUN_INTERACTIVE || run_mode == PIKA_RUN_WITH_LAST_VALS) { pika_ui_init (PLUG_IN_BINARY); export = pika_export_image (&image, &n_drawables, &drawables, "MNG", PIKA_EXPORT_CAN_HANDLE_RGB | PIKA_EXPORT_CAN_HANDLE_GRAY | PIKA_EXPORT_CAN_HANDLE_INDEXED | PIKA_EXPORT_CAN_HANDLE_ALPHA | PIKA_EXPORT_CAN_HANDLE_LAYERS); if (export == PIKA_EXPORT_CANCEL) return pika_procedure_new_return_values (procedure, PIKA_PDB_CANCEL, NULL); } if (run_mode == PIKA_RUN_INTERACTIVE) { if (! mng_save_dialog (image, procedure, G_OBJECT (config))) status = PIKA_PDB_CANCEL; } if (status == PIKA_PDB_SUCCESS) { if (! mng_save_image (file, image, n_drawables, drawables, G_OBJECT (config), &error)) { status = PIKA_PDB_EXECUTION_ERROR; } } if (export == PIKA_EXPORT_EXPORT) { pika_image_delete (image); g_free (drawables); } return pika_procedure_new_return_values (procedure, status, error); } /* * Callbacks for libmng */ static mng_ptr MNG_DECL myalloc (mng_size_t size) { gpointer ptr; ptr = g_try_malloc (size); if (ptr != NULL) memset (ptr, 0, size); return ((mng_ptr) ptr); } static void MNG_DECL myfree (mng_ptr ptr, mng_size_t size) { g_free (ptr); } static mng_bool MNG_DECL myopenstream (mng_handle handle) { return MNG_TRUE; } static mng_bool MNG_DECL myclosestream (mng_handle handle) { return MNG_TRUE; } static mng_bool MNG_DECL mywritedata (mng_handle handle, mng_ptr buf, mng_uint32 size, mng_uint32 *written_size) { struct mnglib_userdata_t *userdata = (struct mnglib_userdata_t *) mng_get_userdata (handle); *written_size = (mng_uint32) fwrite ((void *) buf, 1, (size_t) size, userdata->fp); return MNG_TRUE; } /* Parses which output chunk type to use for this layer from the layer * name. */ static gint32 parse_chunks_type_from_layer_name (const gchar *str, gint default_chunks) { guint i; for (i = 0; (i + 5) <= strlen (str); i++) { if (g_ascii_strncasecmp (str + i, "(png)", 5) == 0) return CHUNKS_PNG; else if (g_ascii_strncasecmp (str + i, "(jng)", 5) == 0) return CHUNKS_JNG; } return default_chunks; } /* Parses which disposal type to use for this layer from the layer * name. */ static gint32 parse_disposal_type_from_layer_name (const gchar *str, gint default_dispose) { guint i; for (i = 0; (i + 9) <= strlen (str); i++) { if (g_ascii_strncasecmp (str + i, "(combine)", 9) == 0) return DISPOSE_COMBINE; else if (g_ascii_strncasecmp (str + i, "(replace)", 9) == 0) return DISPOSE_REPLACE; } return default_dispose; } /* Parses the millisecond delay to use for this layer * from the layer name. */ static gint32 parse_ms_tag_from_layer_name (const gchar *str, gint default_delay) { guint offset = 0; gint32 sum = 0; guint length = strlen (str); while (TRUE) { while ((offset < length) && (str[offset] != '(')) offset++; if (offset >= length) return default_delay; offset++; if (g_ascii_isdigit (str[offset])) break; } do { sum *= 10; sum += str[offset] - '0'; offset++; } while ((offset < length) && (g_ascii_isdigit (str[offset]))); if ((length - offset) <= 2) return default_delay; if ((g_ascii_toupper (str[offset]) != 'M') || (g_ascii_toupper (str[offset + 1]) != 'S')) return default_delay; return sum; } /* Try to find a color in the palette which isn't actually * used in the image, so that we can use it as the transparency * index. Taken from png.c */ static gint find_unused_ia_color (guchar *pixels, gint numpixels, gint *colors) { gint i; gboolean ix_used[256]; gboolean trans_used = FALSE; for (i = 0; i < *colors; i++) { ix_used[i] = FALSE; } for (i = 0; i < numpixels; i++) { /* If alpha is over a threshold, the color index in the * palette is taken. Otherwise, this pixel is transparent. */ if (pixels[i * 2 + 1] > 127) ix_used[pixels[i * 2]] = TRUE; else trans_used = TRUE; } /* If there is no transparency, ignore alpha. */ if (trans_used == FALSE) return -1; for (i = 0; i < *colors; i++) { if (ix_used[i] == FALSE) { 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)++; return ((*colors) - 1); } return -1; } static gboolean ia_has_transparent_pixels (guchar *pixels, gint numpixels) { while (numpixels --) { if (pixels [1] <= 127) return TRUE; pixels += 2; } return FALSE; } static int get_bit_depth_for_palette (int num_palette) { if (num_palette <= 2) return 1; else if (num_palette <= 4) return 2; else if (num_palette <= 16) return 4; else return 8; } /* Spins the color map (palette) putting the transparent color at * index 0 if there is space. If there isn't any space, warn the user * and forget about transparency. Returns TRUE if the colormap has * been changed and FALSE otherwise. */ static gboolean respin_cmap (png_structp pp, png_infop info, guchar *remap, PikaImage *image, GeglBuffer *buffer, int *bit_depth) { static guchar trans[] = { 0 }; guchar *before; guchar *pixels; gint numpixels; gint colors; gint transparent; gint cols, rows; before = pika_image_get_colormap (image, NULL, &colors); /* Make sure there is something in the colormap */ if (colors == 0) { before = g_newa (guchar, 3); memset (before, 0, sizeof (guchar) * 3); colors = 1; } cols = gegl_buffer_get_width (buffer); rows = gegl_buffer_get_height (buffer); numpixels = cols * rows; pixels = (guchar *) g_malloc (numpixels * 2); gegl_buffer_get (buffer, GEGL_RECTANGLE (0, 0, cols, rows), 1.0, NULL, pixels, GEGL_AUTO_ROWSTRIDE, GEGL_ABYSS_NONE); if (ia_has_transparent_pixels (pixels, numpixels)) { transparent = find_unused_ia_color (pixels, numpixels, &colors); if (transparent != -1) { static png_color palette[256] = { {0, 0, 0} }; gint i; /* Set tRNS chunk values for writing later. */ mngg.has_trns = TRUE; mngg.trans = trans; mngg.num_trans = 1; /* Transform all pixels with a value = transparent to * 0 and vice versa to compensate for re-ordering in palette * due to png_set_tRNS(). */ remap[0] = transparent; remap[transparent] = 0; /* Copy from index 0 to index transparent - 1 to index 1 to * transparent of after, then from transparent+1 to colors-1 * unchanged, and finally from index transparent to index 0. */ for (i = 1; i < colors; i++) { palette[i].red = before[3 * remap[i]]; palette[i].green = before[3 * remap[i] + 1]; palette[i].blue = before[3 * remap[i] + 2]; } /* Set PLTE chunk values for writing later. */ mngg.has_plte = TRUE; mngg.palette = palette; mngg.num_palette = colors; *bit_depth = get_bit_depth_for_palette (colors); return TRUE; } else { g_message (_("Couldn't losslessly save transparency, " "saving opacity instead.")); } } mngg.has_plte = TRUE; mngg.palette = (png_colorp) before; mngg.num_palette = colors; *bit_depth = get_bit_depth_for_palette (colors); return FALSE; } static mng_retcode mng_putchunk_plte_wrapper (mng_handle handle, gint numcolors, const guchar *colormap) { mng_palette8 palette; memset (palette, 0, sizeof palette); if (0 < numcolors) memcpy (palette, colormap, numcolors * sizeof palette[0]); return mng_putchunk_plte (handle, numcolors, palette); } static mng_retcode mng_putchunk_trns_wrapper (mng_handle handle, gint n_alphas, const guchar *buffer) { const mng_bool mng_global = TRUE; const mng_bool mng_empty = TRUE; mng_uint8arr alphas; memset (alphas, 0, sizeof alphas); if (buffer && 0 < n_alphas) memcpy (alphas, buffer, n_alphas * sizeof alphas[0]); return mng_putchunk_trns (handle, ! mng_empty, ! mng_global, MNG_COLORTYPE_INDEXED, n_alphas, alphas, 0, 0, 0, 0, 0, alphas); } static gboolean mng_save_image (GFile *file, PikaImage *image, gint n_drawables, PikaDrawable **drawables, GObject *config, GError **error) { gboolean ret = FALSE; gint rows, cols; volatile gint i; time_t t; struct tm *gmt; gint num_layers; PikaLayer **layers; struct mnglib_userdata_t *userdata; mng_handle handle; guint32 mng_ticks_per_second; guint32 mng_simplicity_profile; gboolean config_interlaced; gint config_png_compression; gdouble config_jpeg_quality; gdouble config_jpeg_smoothing; gboolean config_loop; gint config_default_delay; gint config_default_chunks; gint config_default_dispose; gboolean config_bkgd; gboolean config_gama; gboolean config_phys; gboolean config_time; g_object_get (config, "interlaced", &config_interlaced, "png-compression", &config_png_compression, "jpeg-quality", &config_jpeg_quality, "jpeg-smoothing", &config_jpeg_smoothing, "loop", &config_loop, "default-delay", &config_default_delay, "default-chunks", &config_default_chunks, "default-dispose", &config_default_dispose, "bkgd", &config_bkgd, "gama", &config_gama, "phys", &config_phys, "time", &config_time, NULL); layers = pika_image_get_layers (image, &num_layers); if (num_layers < 1) return FALSE; if (num_layers > 1) mng_ticks_per_second = 1000; else mng_ticks_per_second = 0; rows = pika_image_get_height (image); cols = pika_image_get_width (image); mng_simplicity_profile = (MNG_SIMPLICITY_VALID | MNG_SIMPLICITY_SIMPLEFEATURES | MNG_SIMPLICITY_COMPLEXFEATURES); /* JNG and delta-PNG chunks exist */ mng_simplicity_profile |= (MNG_SIMPLICITY_JNG | MNG_SIMPLICITY_DELTAPNG); for (i = 0; i < num_layers; i++) { if (pika_drawable_has_alpha (PIKA_DRAWABLE (layers[i]))) { /* internal transparency exists */ mng_simplicity_profile |= MNG_SIMPLICITY_TRANSPARENCY; /* validity of following flags */ mng_simplicity_profile |= 0x00000040; /* semi-transparency exists */ mng_simplicity_profile |= 0x00000100; /* background transparency should happen */ mng_simplicity_profile |= 0x00000080; break; } } userdata = g_new0 (struct mnglib_userdata_t, 1); userdata->fp = g_fopen (g_file_peek_path (file), "wb"); if (! userdata->fp) { g_set_error (error, G_FILE_ERROR, g_file_error_from_errno (errno), _("Could not open '%s' for writing: %s"), pika_file_get_utf8_name (file), g_strerror (errno)); goto err; } handle = mng_initialize ((mng_ptr) userdata, myalloc, myfree, NULL); if (! handle) { g_warning ("Unable to mng_initialize() in mng_save_image()"); goto err2; } if ((mng_setcb_openstream (handle, myopenstream) != MNG_NOERROR) || (mng_setcb_closestream (handle, myclosestream) != MNG_NOERROR) || (mng_setcb_writedata (handle, mywritedata) != MNG_NOERROR)) { g_warning ("Unable to setup callbacks in mng_save_image()"); goto err3; } if (mng_create (handle) != MNG_NOERROR) { g_warning ("Unable to mng_create() image in mng_save_image()"); goto err3; } if (mng_putchunk_mhdr (handle, cols, rows, mng_ticks_per_second, 1, num_layers, config_default_delay, mng_simplicity_profile) != MNG_NOERROR) { g_warning ("Unable to mng_putchunk_mhdr() in mng_save_image()"); goto err3; } if ((num_layers > 1) && (config_loop)) { gint32 ms = parse_ms_tag_from_layer_name (pika_item_get_name (PIKA_ITEM (layers[0])), config_default_delay); if (mng_putchunk_term (handle, MNG_TERMACTION_REPEAT, MNG_ITERACTION_LASTFRAME, ms, 0x7fffffff) != MNG_NOERROR) { g_warning ("Unable to mng_putchunk_term() in mng_save_image()"); goto err3; } } else { gint32 ms = parse_ms_tag_from_layer_name (pika_item_get_name (PIKA_ITEM (layers[0])), config_default_delay); if (mng_putchunk_term (handle, MNG_TERMACTION_LASTFRAME, MNG_ITERACTION_LASTFRAME, ms, 0x7fffffff) != MNG_NOERROR) { g_warning ("Unable to mng_putchunk_term() in mng_save_image()"); goto err3; } } /* For now, we hardwire a comment */ if (mng_putchunk_text (handle, strlen (MNG_TEXT_TITLE), MNG_TEXT_TITLE, 18, "Created using PIKA") != MNG_NOERROR) { g_warning ("Unable to mng_putchunk_text() in mng_save_image()"); goto err3; } #if 0 /* how do we get this to work? */ if (config_bkgd) { PikaRGB bgcolor; guchar red, green, blue; pika_context_get_background (&bgcolor); pika_rgb_get_uchar (&bgcolor, &red, &green, &blue); if (mng_putchunk_back (handle, red, green, blue, MNG_BACKGROUNDCOLOR_MANDATORY, 0, MNG_BACKGROUNDIMAGE_NOTILE) != MNG_NOERROR) { g_warning ("Unable to mng_putchunk_back() in mng_save_image()"); goto err3; } if (mng_putchunk_bkgd (handle, MNG_FALSE, 2, 0, pika_rgb_luminance_uchar (&bgcolor), red, green, blue) != MNG_NOERROR) { g_warning ("Unable to mng_putchunk_bkgd() in mng_save_image()"); goto err3; } } #endif if (config_gama) { if (mng_putchunk_gama (handle, MNG_FALSE, (1.0 / 2.2 * 100000)) != MNG_NOERROR) { g_warning ("Unable to mng_putchunk_gama() in mng_save_image()"); goto err3; } } #if 0 /* how do we get this to work? */ if (config_phys) { pika_image_get_resolution (image, &xres, &yres); if (mng_putchunk_phyg (handle, MNG_FALSE, (mng_uint32) (xres * 39.37), (mng_uint32) (yres * 39.37), 1) != MNG_NOERROR) { g_warning ("Unable to mng_putchunk_phyg() in mng_save_image()"); goto err3; } if (mng_putchunk_phys (handle, MNG_FALSE, (mng_uint32) (xres * 39.37), (mng_uint32) (yres * 39.37), 1) != MNG_NOERROR) { g_warning ("Unable to mng_putchunk_phys() in mng_save_image()"); goto err3; } } #endif if (config_time) { t = time (NULL); gmt = gmtime (&t); if (mng_putchunk_time (handle, gmt->tm_year + 1900, gmt->tm_mon + 1, gmt->tm_mday, gmt->tm_hour, gmt->tm_min, gmt->tm_sec) != MNG_NOERROR) { g_warning ("Unable to mng_putchunk_time() in mng_save_image()"); goto err3; } } if (pika_image_get_base_type (image) == PIKA_INDEXED) { guchar *palette; gint numcolors; palette = pika_image_get_colormap (image, NULL, &numcolors); if ((numcolors != 0) && (mng_putchunk_plte_wrapper (handle, numcolors, palette) != MNG_NOERROR)) { g_warning ("Unable to mng_putchunk_plte() in mng_save_image()"); goto err3; } } for (i = (num_layers - 1); i >= 0; i--) { PikaImageType layer_drawable_type; GeglBuffer *layer_buffer; gint layer_offset_x, layer_offset_y; gint layer_rows, layer_cols; gchar *layer_name; gint layer_chunks_type; const Babl *layer_format; volatile gint layer_bpp; guint8 __attribute__((unused))layer_mng_colortype; guint8 __attribute__((unused))layer_mng_compression_type; guint8 __attribute__((unused))layer_mng_interlace_type; gboolean layer_has_unique_palette; gchar frame_mode; int frame_delay; GFile *temp_file; png_structp pp; png_infop info; FILE *infile, *outfile; int num_passes; int tile_height; guchar **layer_pixels, *layer_pixel; int pass, j, k, begin, end, num; guchar *fixed; guchar layer_remap[256]; int color_type; int bit_depth; layer_name = pika_item_get_name (PIKA_ITEM (layers[i])); layer_chunks_type = parse_chunks_type_from_layer_name (layer_name, config_default_chunks); layer_drawable_type = pika_drawable_type (PIKA_DRAWABLE (layers[i])); layer_buffer = pika_drawable_get_buffer (PIKA_DRAWABLE (layers[i])); layer_cols = gegl_buffer_get_width (layer_buffer); layer_rows = gegl_buffer_get_height (layer_buffer); pika_drawable_get_offsets (PIKA_DRAWABLE (layers[i]), &layer_offset_x, &layer_offset_y); layer_has_unique_palette = TRUE; for (j = 0; j < 256; j++) layer_remap[j] = j; switch (layer_drawable_type) { case PIKA_RGB_IMAGE: layer_format = babl_format ("R'G'B' u8"); layer_mng_colortype = MNG_COLORTYPE_RGB; break; case PIKA_RGBA_IMAGE: layer_format = babl_format ("R'G'B'A u8"); layer_mng_colortype = MNG_COLORTYPE_RGBA; break; case PIKA_GRAY_IMAGE: layer_format = babl_format ("Y' u8"); layer_mng_colortype = MNG_COLORTYPE_GRAY; break; case PIKA_GRAYA_IMAGE: layer_format = babl_format ("Y'A u8"); layer_mng_colortype = MNG_COLORTYPE_GRAYA; break; case PIKA_INDEXED_IMAGE: layer_format = gegl_buffer_get_format (layer_buffer); layer_mng_colortype = MNG_COLORTYPE_INDEXED; break; case PIKA_INDEXEDA_IMAGE: layer_format = gegl_buffer_get_format (layer_buffer); layer_mng_colortype = MNG_COLORTYPE_INDEXED | MNG_COLORTYPE_GRAYA; break; default: g_warning ("Unsupported PikaImageType in mng_save_image()"); goto err3; } layer_bpp = babl_format_get_bytes_per_pixel (layer_format); /* Delta PNG chunks are not yet supported */ /* if (i == (num_layers - 1)) */ { if (layer_chunks_type == CHUNKS_JNG_D) layer_chunks_type = CHUNKS_JNG; else if (layer_chunks_type == CHUNKS_PNG_D) layer_chunks_type = CHUNKS_PNG; } switch (layer_chunks_type) { case CHUNKS_PNG_D: layer_mng_compression_type = MNG_COMPRESSION_DEFLATE; if (config_interlaced) layer_mng_interlace_type = MNG_INTERLACE_ADAM7; else layer_mng_interlace_type = MNG_INTERLACE_NONE; break; case CHUNKS_JNG_D: layer_mng_compression_type = MNG_COMPRESSION_DEFLATE; if (config_interlaced) layer_mng_interlace_type = MNG_INTERLACE_ADAM7; else layer_mng_interlace_type = MNG_INTERLACE_NONE; break; case CHUNKS_PNG: layer_mng_compression_type = MNG_COMPRESSION_DEFLATE; if (config_interlaced) layer_mng_interlace_type = MNG_INTERLACE_ADAM7; else layer_mng_interlace_type = MNG_INTERLACE_NONE; break; case CHUNKS_JNG: layer_mng_compression_type = MNG_COMPRESSION_BASELINEJPEG; if (config_interlaced) layer_mng_interlace_type = MNG_INTERLACE_PROGRESSIVE; else layer_mng_interlace_type = MNG_INTERLACE_SEQUENTIAL; break; default: g_warning ("Huh? Programmer stupidity error " "with 'layer_chunks_type'"); goto err3; } if ((i == (num_layers - 1)) || (parse_disposal_type_from_layer_name (layer_name, config_default_dispose) != DISPOSE_COMBINE)) frame_mode = MNG_FRAMINGMODE_3; else frame_mode = MNG_FRAMINGMODE_1; frame_delay = parse_ms_tag_from_layer_name (layer_name, config_default_delay); if (mng_putchunk_fram (handle, MNG_FALSE, frame_mode, 0, NULL, MNG_CHANGEDELAY_DEFAULT, MNG_CHANGETIMOUT_NO, MNG_CHANGECLIPPING_DEFAULT, MNG_CHANGESYNCID_NO, frame_delay, 0, 0, layer_offset_x, layer_offset_x + layer_cols, layer_offset_y, layer_offset_y + layer_rows, 0, NULL) != MNG_NOERROR) { g_warning ("Unable to mng_putchunk_fram() in mng_save_image()"); goto err3; } if ((layer_offset_x != 0) || (layer_offset_y != 0)) { if (mng_putchunk_defi (handle, 0, 0, 1, 1, layer_offset_x, layer_offset_y, 1, layer_offset_x, layer_offset_x + layer_cols, layer_offset_y, layer_offset_y + layer_rows) != MNG_NOERROR) { g_warning ("Unable to mng_putchunk_defi() in mng_save_image()"); goto err3; } } if ((temp_file = pika_temp_file ("mng")) == NULL) { g_warning ("pika_temp_file() failed in mng_save_image()"); goto err3; } if ((outfile = g_fopen (g_file_peek_path (temp_file), "wb")) == NULL) { g_set_error (error, G_FILE_ERROR, g_file_error_from_errno (errno), _("Could not open '%s' for writing: %s"), pika_file_get_utf8_name (temp_file), g_strerror (errno)); g_file_delete (temp_file, NULL, NULL); goto err3; } pp = png_create_write_struct (PNG_LIBPNG_VER_STRING, NULL, NULL, NULL); if (! pp) { g_warning ("Unable to png_create_write_struct() in mng_save_image()"); fclose (outfile); g_file_delete (temp_file, NULL, NULL); goto err3; } info = png_create_info_struct (pp); if (! info) { g_warning ("Unable to png_create_info_struct() in mng_save_image()"); png_destroy_write_struct (&pp, NULL); fclose (outfile); g_file_delete (temp_file, NULL, NULL); goto err3; } if (setjmp (png_jmpbuf (pp)) != 0) { g_warning ("HRM saving PNG in mng_save_image()"); png_destroy_write_struct (&pp, &info); fclose (outfile); g_file_delete (temp_file, NULL, NULL); goto err3; } png_init_io (pp, outfile); bit_depth = 8; switch (layer_drawable_type) { case PIKA_RGB_IMAGE: color_type = PNG_COLOR_TYPE_RGB; break; case PIKA_RGBA_IMAGE: color_type = PNG_COLOR_TYPE_RGB_ALPHA; break; case PIKA_GRAY_IMAGE: color_type = PNG_COLOR_TYPE_GRAY; break; case PIKA_GRAYA_IMAGE: color_type = PNG_COLOR_TYPE_GRAY_ALPHA; break; case PIKA_INDEXED_IMAGE: color_type = PNG_COLOR_TYPE_PALETTE; mngg.has_plte = TRUE; mngg.palette = (png_colorp) pika_image_get_colormap (image, NULL, &mngg.num_palette); bit_depth = get_bit_depth_for_palette (mngg.num_palette); break; case PIKA_INDEXEDA_IMAGE: color_type = PNG_COLOR_TYPE_PALETTE; layer_has_unique_palette = respin_cmap (pp, info, layer_remap, image, layer_buffer, &bit_depth); break; default: g_warning ("This can't be!\n"); png_destroy_write_struct (&pp, &info); fclose (outfile); g_file_delete (temp_file, NULL, NULL); goto err3; } /* Note: png_set_IHDR() must be called before any other * png_set_*() functions. */ png_set_IHDR (pp, info, layer_cols, layer_rows, bit_depth, color_type, config_interlaced ? PNG_INTERLACE_ADAM7 : PNG_INTERLACE_NONE, PNG_COMPRESSION_TYPE_BASE, PNG_FILTER_TYPE_BASE); if (mngg.has_trns) { png_set_tRNS (pp, info, mngg.trans, mngg.num_trans, NULL); } if (mngg.has_plte) { png_set_PLTE (pp, info, mngg.palette, mngg.num_palette); } png_set_compression_level (pp, config_png_compression); png_write_info (pp, info); if (config_interlaced) num_passes = png_set_interlace_handling (pp); else num_passes = 1; if ((color_type == PNG_COLOR_TYPE_PALETTE) && (bit_depth < 8)) png_set_packing (pp); tile_height = pika_tile_height (); layer_pixel = g_new (guchar, tile_height * layer_cols * layer_bpp); layer_pixels = g_new (guchar *, tile_height); for (j = 0; j < tile_height; j++) layer_pixels[j] = layer_pixel + (layer_cols * layer_bpp * j); for (pass = 0; pass < num_passes; pass++) { for (begin = 0, end = tile_height; begin < layer_rows; begin += tile_height, end += tile_height) { if (end > layer_rows) end = layer_rows; num = end - begin; gegl_buffer_get (layer_buffer, GEGL_RECTANGLE (0, begin, layer_cols, num), 1.0, layer_format, layer_pixel, GEGL_AUTO_ROWSTRIDE, GEGL_ABYSS_NONE); if (png_get_valid (pp, info, PNG_INFO_tRNS)) { for (j = 0; j < num; j++) { fixed = layer_pixels[j]; for (k = 0; k < layer_cols; k++) fixed[k] = (fixed[k * 2 + 1] > 127) ? layer_remap[fixed[k * 2]] : 0; } } else if (png_get_valid (pp, info, PNG_INFO_PLTE) && (layer_bpp == 2)) { for (j = 0; j < num; j++) { fixed = layer_pixels[j]; for (k = 0; k < layer_cols; k++) fixed[k] = fixed[k * 2]; } } png_write_rows (pp, layer_pixels, num); } } g_object_unref (layer_buffer); png_write_end (pp, info); png_destroy_write_struct (&pp, &info); g_free (layer_pixels); g_free (layer_pixel); fclose (outfile); infile = g_fopen (g_file_peek_path (temp_file), "rb"); if (! infile) { g_set_error (error, G_FILE_ERROR, g_file_error_from_errno (errno), _("Could not open '%s' for reading: %s"), pika_file_get_utf8_name (temp_file), g_strerror (errno)); g_file_delete (temp_file, NULL, NULL); goto err3; } fseek (infile, 8L, SEEK_SET); while (!feof (infile)) { guchar chunksize_chars[4]; gulong chunksize; gchar chunkname[5]; guchar *chunkbuffer; glong chunkwidth; glong chunkheight; gchar chunkbitdepth; gchar chunkcolortype; gchar chunkcompression; gchar chunkfilter; gchar chunkinterlaced; if (fread (chunksize_chars, 1, 4, infile) != 4) break; if (fread (chunkname, 1, 4, infile) != 4) break; chunkname[4] = 0; chunksize = ((chunksize_chars[0] << 24) | (chunksize_chars[1] << 16) | (chunksize_chars[2] << 8) | (chunksize_chars[3])); chunkbuffer = NULL; if (chunksize > 0) { chunkbuffer = g_new (guchar, chunksize); if (fread (chunkbuffer, 1, chunksize, infile) != chunksize) break; } if (strncmp (chunkname, "IHDR", 4) == 0) { chunkwidth = ((chunkbuffer[0] << 24) | (chunkbuffer[1] << 16) | (chunkbuffer[2] << 8) | (chunkbuffer[3])); chunkheight = ((chunkbuffer[4] << 24) | (chunkbuffer[5] << 16) | (chunkbuffer[6] << 8) | (chunkbuffer[7])); chunkbitdepth = chunkbuffer[8]; chunkcolortype = chunkbuffer[9]; chunkcompression = chunkbuffer[10]; chunkfilter = chunkbuffer[11]; chunkinterlaced = chunkbuffer[12]; if (mng_putchunk_ihdr (handle, chunkwidth, chunkheight, chunkbitdepth, chunkcolortype, chunkcompression, chunkfilter, chunkinterlaced) != MNG_NOERROR) { g_warning ("Unable to mng_putchunk_ihdr() " "in mng_save_image()"); fclose (infile); goto err3; } } else if (strncmp (chunkname, "IDAT", 4) == 0) { if (mng_putchunk_idat (handle, chunksize, chunkbuffer) != MNG_NOERROR) { g_warning ("Unable to mng_putchunk_idat() " "in mng_save_image()"); fclose (infile); goto err3; } } else if (strncmp (chunkname, "IEND", 4) == 0) { if (mng_putchunk_iend (handle) != MNG_NOERROR) { g_warning ("Unable to mng_putchunk_iend() " "in mng_save_image()"); fclose (infile); goto err3; } } else if (strncmp (chunkname, "PLTE", 4) == 0) { /* If this frame's palette is the same as the global palette, * write a 0-color palette chunk. */ if (mng_putchunk_plte_wrapper (handle, (layer_has_unique_palette ? (chunksize / 3) : 0), chunkbuffer) != MNG_NOERROR) { g_warning ("Unable to mng_putchunk_plte() " "in mng_save_image()"); fclose (infile); goto err3; } } else if (strncmp (chunkname, "tRNS", 4) == 0) { if (mng_putchunk_trns_wrapper (handle, chunksize, chunkbuffer) != MNG_NOERROR) { g_warning ("Unable to mng_putchunk_trns() " "in mng_save_image()"); fclose (infile); goto err3; } } if (chunksize > 0) g_free (chunkbuffer); /* read 4 bytes after the chunk */ fread (chunkname, 1, 4, infile); } fclose (infile); g_file_delete (temp_file, NULL, NULL); } if (mng_putchunk_mend (handle) != MNG_NOERROR) { g_warning ("Unable to mng_putchunk_mend() in mng_save_image()"); goto err3; } if (mng_write (handle) != MNG_NOERROR) { g_warning ("Unable to mng_write() the image in mng_save_image()"); goto err3; } ret = TRUE; err3: mng_cleanup (&handle); err2: fclose (userdata->fp); err: g_free (userdata); return ret; } /* The interactive dialog. */ static gboolean mng_save_dialog (PikaImage *image, PikaProcedure *procedure, GObject *config) { GtkWidget *dialog; GtkWidget *frame; GtkWidget *hbox; GtkListStore *store; GtkWidget *combo; GtkWidget *label; GtkWidget *scale; gint num_layers; gboolean run; dialog = pika_save_procedure_dialog_new (PIKA_SAVE_PROCEDURE (procedure), PIKA_PROCEDURE_CONFIG (config), image); pika_procedure_dialog_fill_box (PIKA_PROCEDURE_DIALOG (dialog), "options-vbox", "interlaced", "bkgd", "gama", "phys", "time", NULL); pika_procedure_dialog_get_label (PIKA_PROCEDURE_DIALOG (dialog), "options-label", _("MNG Options"), FALSE, FALSE); pika_procedure_dialog_fill_frame (PIKA_PROCEDURE_DIALOG (dialog), "options-frame", "options-label", FALSE, "options-vbox"); g_free (pika_image_get_layers (image, &num_layers)); if (num_layers == 1) store = pika_int_store_new (_("PNG"), CHUNKS_PNG_D, _("JNG"), CHUNKS_JNG_D, NULL); else store = pika_int_store_new (_("PNG + delta PNG"), CHUNKS_PNG_D, _("JNG + delta PNG"), CHUNKS_JNG_D, _("All PNG"), CHUNKS_PNG, _("All JNG"), CHUNKS_JNG, NULL); combo = pika_procedure_dialog_get_int_combo (PIKA_PROCEDURE_DIALOG (dialog), "default-chunks", PIKA_INT_STORE (store)); gtk_widget_set_margin_bottom (combo, 6); store = pika_int_store_new (_("Combine"), DISPOSE_COMBINE, _("Replace"), DISPOSE_REPLACE, NULL); combo = pika_procedure_dialog_get_int_combo (PIKA_PROCEDURE_DIALOG (dialog), "default-dispose", PIKA_INT_STORE (store)); gtk_widget_set_margin_bottom (combo, 6); scale = pika_procedure_dialog_get_scale_entry (PIKA_PROCEDURE_DIALOG (dialog), "png-compression", 1.0); gtk_widget_set_margin_bottom (scale, 6); scale = pika_procedure_dialog_get_scale_entry (PIKA_PROCEDURE_DIALOG (dialog), "jpeg-quality", 1.0); gtk_widget_set_margin_bottom (scale, 6); scale = pika_procedure_dialog_get_scale_entry (PIKA_PROCEDURE_DIALOG (dialog), "jpeg-smoothing", 1.0); gtk_widget_set_margin_bottom (scale, 6); /* MNG Animation Options */ label = pika_procedure_dialog_get_label (PIKA_PROCEDURE_DIALOG (dialog), "milliseconds-label", _("milliseconds"), FALSE, FALSE); gtk_widget_set_margin_start (label, 6); hbox = pika_procedure_dialog_fill_box (PIKA_PROCEDURE_DIALOG (dialog), "animation-box", "default-delay", "milliseconds-label", NULL); gtk_orientable_set_orientation (GTK_ORIENTABLE (hbox), GTK_ORIENTATION_HORIZONTAL); pika_procedure_dialog_fill_box (PIKA_PROCEDURE_DIALOG (dialog), "mng-animation-box", "loop", "animation-box", NULL); pika_procedure_dialog_get_label (PIKA_PROCEDURE_DIALOG (dialog), "mng-animation-label", _("Animated MNG Options"), FALSE, FALSE); frame = pika_procedure_dialog_fill_frame (PIKA_PROCEDURE_DIALOG (dialog), "mng-animation-frame", "mng-animation-label", FALSE, "mng-animation-box"); if (num_layers <= 1) { gtk_widget_set_sensitive (frame, FALSE); pika_help_set_help_data (frame, _("These options are only available when " "the exported image has more than one " "layer. The image you are exporting only has " "one layer."), NULL); } pika_procedure_dialog_fill (PIKA_PROCEDURE_DIALOG (dialog), "options-frame", "default-chunks", "default-dispose", "png-compression", "jpeg-quality", "jpeg-smoothing", "mng-animation-frame", NULL); gtk_widget_show (dialog); run = pika_procedure_dialog_run (PIKA_PROCEDURE_DIALOG (dialog)); gtk_widget_destroy (dialog); return run; }