/* 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 * * Portable Network Graphics (PNG) plug-in * * Copyright 1997-1998 Michael Sweet (mike@easysw.com) and * Daniel Skarda (0rfelyus@atrey.karlin.mff.cuni.cz). * and 1999-2000 Nick Lamb (njl195@zepler.org.uk) * * 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 . */ #include "config.h" #include #include #include #include "lcms2.h" #include #include #include #include "libpika/stdplugins-intl.h" #define LOAD_PROC "file-png-load" #define SAVE_PROC "file-png-save" #define PLUG_IN_BINARY "file-png" #define PLUG_IN_ROLE "pika-file-png" #define PLUG_IN_VERSION "1.3.4 - 03 September 2002" #define SCALE_WIDTH 125 #define DEFAULT_GAMMA 2.20 typedef enum _PngExportformat { PNG_FORMAT_AUTO = 0, PNG_FORMAT_RGB8, PNG_FORMAT_GRAY8, PNG_FORMAT_RGBA8, PNG_FORMAT_GRAYA8, PNG_FORMAT_RGB16, PNG_FORMAT_GRAY16, PNG_FORMAT_RGBA16, PNG_FORMAT_GRAYA16 } PngExportFormat; static GSList *safe_to_copy_chunks; typedef struct _Png Png; typedef struct _PngClass PngClass; struct _Png { PikaPlugIn parent_instance; }; struct _PngClass { PikaPlugInClass parent_class; }; #define PNG_TYPE (png_get_type ()) #define PNG(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), PNG_TYPE, Png)) GType png_get_type (void) G_GNUC_CONST; static GList * png_query_procedures (PikaPlugIn *plug_in); static PikaProcedure * png_create_procedure (PikaPlugIn *plug_in, const gchar *name); static PikaValueArray * png_load (PikaProcedure *procedure, PikaRunMode run_mode, GFile *file, PikaMetadata *metadata, PikaMetadataLoadFlags *flags, PikaProcedureConfig *config, gpointer run_data); static PikaValueArray * png_save (PikaProcedure *procedure, PikaRunMode run_mode, PikaImage *image, gint n_drawables, PikaDrawable **drawables, GFile *file, PikaMetadata *metadata, PikaProcedureConfig *config, gpointer run_data); static PikaImage * load_image (GFile *file, gboolean report_progress, gboolean *resolution_loaded, gboolean *profile_loaded, GError **error); static gboolean save_image (GFile *file, PikaImage *image, PikaDrawable *drawable, PikaImage *orig_image, GObject *config, gint *bits_per_sample, gboolean report_progress, GError **error); static int respin_cmap (png_structp pp, png_infop info, guchar *remap, PikaImage *image, PikaDrawable *drawable); static gboolean save_dialog (PikaImage *image, PikaProcedure *procedure, GObject *config, gboolean alpha); static gboolean offsets_dialog (gint offset_x, gint offset_y); static gboolean ia_has_transparent_pixels (GeglBuffer *buffer); static gint find_unused_ia_color (GeglBuffer *buffer, gint *colors); static gint read_unknown_chunk (png_structp png_ptr, png_unknown_chunkp chunk); G_DEFINE_TYPE (Png, png, PIKA_TYPE_PLUG_IN) PIKA_MAIN (PNG_TYPE) DEFINE_STD_SET_I18N static void png_class_init (PngClass *klass) { PikaPlugInClass *plug_in_class = PIKA_PLUG_IN_CLASS (klass); plug_in_class->query_procedures = png_query_procedures; plug_in_class->create_procedure = png_create_procedure; plug_in_class->set_i18n = STD_SET_I18N; } static void png_init (Png *png) { } static GList * png_query_procedures (PikaPlugIn *plug_in) { GList *list = NULL; list = g_list_append (list, g_strdup (LOAD_PROC)); list = g_list_append (list, g_strdup (SAVE_PROC)); return list; } static PikaProcedure * png_create_procedure (PikaPlugIn *plug_in, const gchar *name) { PikaProcedure *procedure = NULL; if (! strcmp (name, LOAD_PROC)) { procedure = pika_load_procedure_new (plug_in, name, PIKA_PDB_PROC_TYPE_PLUGIN, png_load, NULL, NULL); pika_procedure_set_menu_label (procedure, _("PNG image")); pika_procedure_set_documentation (procedure, "Loads files in PNG file format", "This plug-in loads Portable Network " "Graphics (PNG) files.", name); pika_procedure_set_attribution (procedure, "Michael Sweet , " "Daniel Skarda <0rfelyus@atrey.karlin.mff.cuni.cz>", "Michael Sweet , " "Daniel Skarda <0rfelyus@atrey.karlin.mff.cuni.cz>, " "Nick Lamb ", PLUG_IN_VERSION); pika_file_procedure_set_mime_types (PIKA_FILE_PROCEDURE (procedure), "image/png"); pika_file_procedure_set_extensions (PIKA_FILE_PROCEDURE (procedure), "png"); pika_file_procedure_set_magics (PIKA_FILE_PROCEDURE (procedure), "0,string,\211PNG\r\n\032\n"); } else if (! strcmp (name, SAVE_PROC)) { procedure = pika_save_procedure_new (plug_in, name, PIKA_PDB_PROC_TYPE_PLUGIN, TRUE, png_save, NULL, NULL); pika_procedure_set_image_types (procedure, "*"); pika_procedure_set_menu_label (procedure, _("PNG image")); pika_procedure_set_documentation (procedure, "Exports files in PNG file format", "This plug-in exports Portable Network " "Graphics (PNG) files.", name); pika_procedure_set_attribution (procedure, "Michael Sweet , " "Daniel Skarda <0rfelyus@atrey.karlin.mff.cuni.cz>", "Michael Sweet , " "Daniel Skarda <0rfelyus@atrey.karlin.mff.cuni.cz>, " "Nick Lamb ", PLUG_IN_VERSION); pika_file_procedure_set_format_name (PIKA_FILE_PROCEDURE (procedure), _("PNG")); pika_file_procedure_set_mime_types (PIKA_FILE_PROCEDURE (procedure), "image/png"); pika_file_procedure_set_extensions (PIKA_FILE_PROCEDURE (procedure), "png"); PIKA_PROC_ARG_BOOLEAN (procedure, "interlaced", _("_Interlacing (Adam7)"), _("Use Adam7 interlacing"), FALSE, G_PARAM_READWRITE); PIKA_PROC_ARG_INT (procedure, "compression", _("Co_mpression level"), _("Deflate Compression factor (0..9)"), 0, 9, 9, G_PARAM_READWRITE); PIKA_PROC_ARG_BOOLEAN (procedure, "bkgd", _("Save _background color"), _("Write bKGD chunk (PNG metadata)"), TRUE, G_PARAM_READWRITE); PIKA_PROC_ARG_BOOLEAN (procedure, "offs", _("Save layer o_ffset"), _("Write oFFs chunk (PNG metadata)"), FALSE, G_PARAM_READWRITE); PIKA_PROC_ARG_BOOLEAN (procedure, "phys", _("Save resol_ution"), _("Write pHYs chunk (PNG metadata)"), TRUE, G_PARAM_READWRITE); PIKA_PROC_ARG_BOOLEAN (procedure, "time", _("Save creation _time"), _("Write tIME chunk (PNG metadata)"), TRUE, G_PARAM_READWRITE); PIKA_PROC_ARG_BOOLEAN (procedure, "save-transparent", _("Save color _values from transparent pixels"), _("Preserve color of completely transparent pixels"), FALSE, G_PARAM_READWRITE); PIKA_PROC_ARG_BOOLEAN (procedure, "optimize-palette", _("_Optimize for smallest possible palette size"), _("When checked, save as 1, 2, 4, or 8-bit depending" " on number of colors used. When unchecked, always" " save as 8-bit"), FALSE, G_PARAM_READWRITE); PIKA_PROC_AUX_ARG_CHOICE (procedure, "format", _("_Pixel format"), _("PNG export format"), pika_choice_new_with_values ("auto", PNG_FORMAT_AUTO, _("Automatic"), NULL, "rgb8", PNG_FORMAT_RGB8, _("8 bpc RGB"), NULL, "gray8", PNG_FORMAT_GRAY8, _("8 bpc GRAY"), NULL, "rgba8", PNG_FORMAT_RGBA8, _("8 bpc RGBA"), NULL, "graya8", PNG_FORMAT_GRAYA8, _("8 bpc GRAYA"), NULL, "rgb16", PNG_FORMAT_RGB16, _("16 bpc RGB"), NULL, "gray16", PNG_FORMAT_GRAY16, _("16 bpc GRAY"), NULL, "rgba16", PNG_FORMAT_RGBA16, _("16 bpc RGBA"), NULL, "graya16", PNG_FORMAT_GRAYA16, _("16 bpc GRAYA"), NULL, NULL), "auto", G_PARAM_READWRITE); pika_save_procedure_set_support_exif (PIKA_SAVE_PROCEDURE (procedure), TRUE); pika_save_procedure_set_support_iptc (PIKA_SAVE_PROCEDURE (procedure), TRUE); pika_save_procedure_set_support_xmp (PIKA_SAVE_PROCEDURE (procedure), TRUE); #if defined(PNG_iCCP_SUPPORTED) pika_save_procedure_set_support_profile (PIKA_SAVE_PROCEDURE (procedure), TRUE); #endif pika_save_procedure_set_support_thumbnail (PIKA_SAVE_PROCEDURE (procedure), TRUE); pika_save_procedure_set_support_comment (PIKA_SAVE_PROCEDURE (procedure), TRUE); } return procedure; } static PikaValueArray * png_load (PikaProcedure *procedure, PikaRunMode run_mode, GFile *file, PikaMetadata *metadata, PikaMetadataLoadFlags *flags, PikaProcedureConfig *config, gpointer run_data) { PikaValueArray *return_vals; gboolean report_progress = FALSE; gboolean resolution_loaded = FALSE; gboolean profile_loaded = FALSE; PikaImage *image; GError *error = NULL; gegl_init (NULL, NULL); if (run_mode != PIKA_RUN_NONINTERACTIVE) { pika_ui_init (PLUG_IN_BINARY); report_progress = TRUE; } image = load_image (file, report_progress, &resolution_loaded, &profile_loaded, &error); if (! image) return pika_procedure_new_return_values (procedure, PIKA_PDB_EXECUTION_ERROR, error); if (resolution_loaded) *flags &= ~PIKA_METADATA_LOAD_RESOLUTION; if (profile_loaded) *flags &= ~PIKA_METADATA_LOAD_COLORSPACE; return_vals = pika_procedure_new_return_values (procedure, PIKA_PDB_SUCCESS, NULL); PIKA_VALUES_SET_IMAGE (return_vals, 1, image); return return_vals; } static PikaValueArray * png_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_CANCEL; PikaImage *orig_image; gboolean alpha; GError *error = NULL; gegl_init (NULL, NULL); orig_image = image; switch (run_mode) { case PIKA_RUN_INTERACTIVE: case PIKA_RUN_WITH_LAST_VALS: pika_ui_init (PLUG_IN_BINARY); export = pika_export_image (&image, &n_drawables, &drawables, "PNG", PIKA_EXPORT_CAN_HANDLE_RGB | PIKA_EXPORT_CAN_HANDLE_GRAY | PIKA_EXPORT_CAN_HANDLE_INDEXED | PIKA_EXPORT_CAN_HANDLE_ALPHA); if (export == PIKA_EXPORT_CANCEL) return pika_procedure_new_return_values (procedure, PIKA_PDB_CANCEL, NULL); break; default: break; } if (n_drawables != 1) { /* PNG images have no layer concept. Export image should have a * single drawable selected. */ g_set_error (&error, G_FILE_ERROR, 0, _("PNG format does not support multiple layers.")); return pika_procedure_new_return_values (procedure, PIKA_PDB_CALLING_ERROR, error); } alpha = pika_drawable_has_alpha (drawables[0]); /* If the image has no transparency, then there is usually no need * to save a bKGD chunk. For more information, see: * http://bugzilla.gnome.org/show_bug.cgi?id=92395 */ if (! alpha) g_object_set (config, "bkgd", FALSE, NULL); if (run_mode == PIKA_RUN_INTERACTIVE) { if (! save_dialog (orig_image, procedure, G_OBJECT (config), alpha)) status = PIKA_PDB_CANCEL; } if (status == PIKA_PDB_SUCCESS) { gint bits_per_sample; if (save_image (file, image, drawables[0], orig_image, G_OBJECT (config), &bits_per_sample, run_mode != PIKA_RUN_NONINTERACTIVE, &error)) { if (metadata) pika_metadata_set_bits_per_sample (metadata, bits_per_sample); } else { 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); } struct read_error_data { guchar *pixel; /* Pixel data */ GeglBuffer *buffer; /* GEGL buffer for layer */ const Babl *file_format; guint32 width; /* png_infop->width */ guint32 height; /* png_infop->height */ gint bpp; /* Bytes per pixel */ gint tile_height; /* Height of tile in PIKA */ gint begin; /* Beginning tile row */ gint end; /* Ending tile row */ gint num; /* Number of rows to load */ }; static void on_read_error (png_structp png_ptr, png_const_charp error_msg) { struct read_error_data *error_data = png_get_error_ptr (png_ptr); gint begin; gint end; gint num; g_printerr (_("Error loading PNG file: %s\n"), error_msg); /* Flush the current half-read row of tiles */ gegl_buffer_set (error_data->buffer, GEGL_RECTANGLE (0, error_data->begin, error_data->width, error_data->num), 0, error_data->file_format, error_data->pixel, GEGL_AUTO_ROWSTRIDE); begin = error_data->begin + error_data->tile_height; if (begin < error_data->height) { end = MIN (error_data->end + error_data->tile_height, error_data->height); num = end - begin; gegl_buffer_clear (error_data->buffer, GEGL_RECTANGLE (0, begin, error_data->width, num)); } g_object_unref (error_data->buffer); longjmp (png_jmpbuf (png_ptr), 1); } 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; } static PikaColorProfile * load_color_profile (png_structp pp, png_infop info, gchar **profile_name) { PikaColorProfile *profile = NULL; #if defined(PNG_iCCP_SUPPORTED) png_uint_32 proflen; png_charp profname; png_bytep prof; int profcomp; if (png_get_iCCP (pp, info, &profname, &profcomp, &prof, &proflen)) { profile = pika_color_profile_new_from_icc_profile ((guint8 *) prof, proflen, NULL); if (profile && profname) { *profile_name = g_convert (profname, strlen (profname), "ISO-8859-1", "UTF-8", NULL, NULL, NULL); } } #endif return profile; } /* Copied from src/cmsvirt.c in Little-CMS. */ static cmsToneCurve * Build_sRGBGamma (cmsContext ContextID) { cmsFloat64Number Parameters[5]; Parameters[0] = 2.4; Parameters[1] = 1. / 1.055; Parameters[2] = 0.055 / 1.055; Parameters[3] = 1. / 12.92; Parameters[4] = 0.04045; return cmsBuildParametricToneCurve (ContextID, 4, Parameters); } /* * 'load_image()' - Load a PNG image into a new image window. */ static PikaImage * load_image (GFile *file, gboolean report_progress, gboolean *resolution_loaded, gboolean *profile_loaded, GError **error) { gint i; /* Looping var */ gint trns; /* Transparency present */ gint bpp; /* Bytes per pixel */ gint width; /* image width */ gint height; /* image height */ gint num_passes; /* Number of interlace passes in file */ gint pass; /* Current pass in file */ gint tile_height; /* Height of tile in PIKA */ gint begin; /* Beginning tile row */ gint end; /* Ending tile row */ gint num; /* Number of rows to load */ PikaImageBaseType image_type; /* Type of image */ PikaPrecision image_precision; /* Precision of image */ PikaImageType layer_type; /* Type of drawable/layer */ PikaColorProfile *profile = NULL; /* Color profile */ gchar *profile_name = NULL; /* Profile's name */ FILE *fp; /* File pointer */ volatile PikaImage *image = NULL; /* Image -- protected for setjmp() */ PikaLayer *layer; /* Layer */ GeglBuffer *buffer; /* GEGL buffer for layer */ const Babl *file_format; /* BABL format for layer */ png_structp pp; /* PNG read pointer */ png_infop info; /* PNG info pointers */ png_voidp user_chunkp; /* PNG unknown chunk pointer */ guchar **pixels; /* Pixel rows */ guchar *pixel; /* Pixel data */ guchar alpha[256]; /* Index -> Alpha */ png_textp text; gint num_texts; struct read_error_data error_data; safe_to_copy_chunks = NULL; pp = png_create_read_struct (PNG_LIBPNG_VER_STRING, NULL, NULL, NULL); if (! pp) { /* this could happen if the compile time and run-time libpng versions do not match. */ g_set_error (error, G_FILE_ERROR, 0, _("Error creating PNG read struct while loading '%s'."), pika_file_get_utf8_name (file)); return NULL; } info = png_create_info_struct (pp); if (! info) { g_set_error (error, G_FILE_ERROR, 0, _("Error while reading '%s'. Could not create PNG header info structure."), pika_file_get_utf8_name (file)); return NULL; } if (setjmp (png_jmpbuf (pp))) { g_set_error (error, G_FILE_ERROR, G_FILE_ERROR_FAILED, _("Error while reading '%s'. File corrupted?"), pika_file_get_utf8_name (file)); return (PikaImage *) image; } #ifdef PNG_BENIGN_ERRORS_SUPPORTED /* Change some libpng errors to warnings (e.g. bug 721135) */ png_set_benign_errors (pp, TRUE); /* bug 765850 */ png_set_option (pp, PNG_SKIP_sRGB_CHECK_PROFILE, PNG_OPTION_ON); #endif /* * Open the file and initialize the PNG read "engine"... */ if (report_progress) pika_progress_init_printf (_("Opening '%s'"), pika_file_get_utf8_name (file)); fp = g_fopen (g_file_peek_path (file), "rb"); if (fp == NULL) { 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 (file), g_strerror (errno)); return NULL; } png_init_io (pp, fp); png_set_compression_buffer_size (pp, 512); /* Set up callback to save "safe to copy" chunks */ png_set_keep_unknown_chunks (pp, PNG_HANDLE_CHUNK_IF_SAFE, NULL, 0); user_chunkp = png_get_user_chunk_ptr (pp); png_set_read_user_chunk_fn (pp, user_chunkp, read_unknown_chunk); /* * Get the image info */ png_read_info (pp, info); if (G_BYTE_ORDER == G_LITTLE_ENDIAN) png_set_swap (pp); /* * Get the iCCP (color profile) chunk, if any. */ profile = load_color_profile (pp, info, &profile_name); if (! profile && ! png_get_valid (pp, info, PNG_INFO_sRGB) && (png_get_valid (pp, info, PNG_INFO_gAMA) || png_get_valid (pp, info, PNG_INFO_cHRM))) { /* This is kind of a special case for PNG. If an image has no * profile, and the sRGB chunk is not set, and either gAMA or cHRM * (or ideally both) are set, then we generate a profile from * these data on import. See #3265. */ cmsToneCurve *gamma_curve[3]; cmsCIExyY whitepoint; cmsCIExyYTRIPLE primaries; cmsHPROFILE cms_profile = NULL; gdouble gamma = 1.0 / DEFAULT_GAMMA; if (png_get_valid (pp, info, PNG_INFO_gAMA) && png_get_gAMA (pp, info, &gamma) == PNG_INFO_gAMA) { gamma_curve[0] = gamma_curve[1] = gamma_curve[2] = cmsBuildGamma (NULL, 1.0 / gamma); } else { /* Use the sRGB gamma curve. */ gamma_curve[0] = gamma_curve[1] = gamma_curve[2] = Build_sRGBGamma (NULL); } if (png_get_valid (pp, info, PNG_INFO_cHRM) && png_get_cHRM (pp, info, &whitepoint.x, &whitepoint.y, &primaries.Red.x, &primaries.Red.y, &primaries.Green.x, &primaries.Green.y, &primaries.Blue.x, &primaries.Blue.y) == PNG_INFO_cHRM) { whitepoint.Y = primaries.Red.Y = primaries.Green.Y = primaries.Blue.Y = 1.0; } else { /* Rec709 primaries and D65 whitepoint as copied from * cmsCreate_sRGBProfileTHR() in Little-CMS. */ cmsCIExyY d65_whitepoint = { 0.3127, 0.3290, 1.0 }; cmsCIExyYTRIPLE rec709_primaries = { {0.6400, 0.3300, 1.0}, {0.3000, 0.6000, 1.0}, {0.1500, 0.0600, 1.0} }; memcpy (&whitepoint, &d65_whitepoint, sizeof whitepoint); memcpy (&primaries, &rec709_primaries, sizeof primaries); } if (png_get_color_type (pp, info) == PNG_COLOR_TYPE_GRAY || png_get_color_type (pp, info) == PNG_COLOR_TYPE_GRAY_ALPHA) cms_profile = cmsCreateGrayProfile (&whitepoint, gamma_curve[0]); else /* RGB, RGB with Alpha and Indexed. */ cms_profile = cmsCreateRGBProfile (&whitepoint, &primaries, gamma_curve); cmsFreeToneCurve (gamma_curve[0]); g_warn_if_fail (cms_profile != NULL); if (cms_profile != NULL) { /* Customize the profile description to show it is generated * from PNG metadata. */ gchar *profile_desc; cmsMLU *description_mlu; cmsContext context_id = cmsGetProfileContextID (cms_profile); /* Note that I am not trying to localize these strings on purpose * because cmsMLUsetASCII() expects ASCII. Maybe we should move to * using cmsMLUsetWide() if we want the generated profile * descriptions to be localized. XXX */ if ((png_get_valid (pp, info, PNG_INFO_gAMA) && png_get_valid (pp, info, PNG_INFO_cHRM))) profile_desc = g_strdup_printf ("Generated %s profile from PNG's gAMA (gamma %.4f) and cHRM chunks", (png_get_color_type (pp, info) == PNG_COLOR_TYPE_GRAY || png_get_color_type (pp, info) == PNG_COLOR_TYPE_GRAY_ALPHA) ? "grayscale" : "RGB", 1.0 / gamma); else if (png_get_valid (pp, info, PNG_INFO_gAMA)) profile_desc = g_strdup_printf ("Generated %s profile from PNG's gAMA chunk (gamma %.4f)", (png_get_color_type (pp, info) == PNG_COLOR_TYPE_GRAY || png_get_color_type (pp, info) == PNG_COLOR_TYPE_GRAY_ALPHA) ? "grayscale" : "RGB", 1.0 / gamma); else profile_desc = g_strdup_printf ("Generated %s profile from PNG's cHRM chunk", (png_get_color_type (pp, info) == PNG_COLOR_TYPE_GRAY || png_get_color_type (pp, info) == PNG_COLOR_TYPE_GRAY_ALPHA) ? "grayscale" : "RGB"); description_mlu = cmsMLUalloc (context_id, 1); cmsMLUsetASCII (description_mlu, "en", "US", profile_desc); cmsWriteTag (cms_profile, cmsSigProfileDescriptionTag, description_mlu); profile = pika_color_profile_new_from_lcms_profile (cms_profile, NULL); g_free (profile_desc); cmsMLUfree (description_mlu); cmsCloseProfile (cms_profile); } } if (profile) *profile_loaded = TRUE; /* * Get image precision and color model. * Note that we always import PNG as non-linear. The data might be * actually linear because of a linear profile, or because of a gAMA * chunk with 1.0 value (which we convert to a profile above). But * then we'll just set the right profile and that's it. Other than * this, PNG doesn't have (that I can see in the spec) any kind of * flag saying that data is linear, bypassing the profile's TRC so * there is basically no reason to explicitly set a linear precision. */ if (png_get_bit_depth (pp, info) == 16) image_precision = PIKA_PRECISION_U16_NON_LINEAR; else image_precision = PIKA_PRECISION_U8_NON_LINEAR; if (png_get_bit_depth (pp, info) < 8) { if (png_get_color_type (pp, info) == PNG_COLOR_TYPE_GRAY) png_set_expand (pp); if (png_get_color_type (pp, info) == PNG_COLOR_TYPE_PALETTE) png_set_packing (pp); } /* * Expand G+tRNS to GA, RGB+tRNS to RGBA */ if (png_get_color_type (pp, info) != PNG_COLOR_TYPE_PALETTE && png_get_valid (pp, info, PNG_INFO_tRNS)) png_set_expand (pp); /* * Turn on interlace handling... libpng returns just 1 (ie single pass) * if the image is not interlaced */ num_passes = png_set_interlace_handling (pp); /* * Special handling for INDEXED + tRNS (transparency palette) */ if (png_get_valid (pp, info, PNG_INFO_tRNS) && png_get_color_type (pp, info) == PNG_COLOR_TYPE_PALETTE) { guchar *alpha_ptr; png_get_tRNS (pp, info, &alpha_ptr, &num, NULL); /* Copy the existing alpha values from the tRNS chunk */ for (i = 0; i < num; ++i) alpha[i] = alpha_ptr[i]; /* And set any others to fully opaque (255) */ for (i = num; i < 256; ++i) alpha[i] = 255; trns = 1; } else { trns = 0; } /* * Update the info structures after the transformations take effect */ png_read_update_info (pp, info); switch (png_get_color_type (pp, info)) { case PNG_COLOR_TYPE_RGB: image_type = PIKA_RGB; layer_type = PIKA_RGB_IMAGE; break; case PNG_COLOR_TYPE_RGB_ALPHA: image_type = PIKA_RGB; layer_type = PIKA_RGBA_IMAGE; break; case PNG_COLOR_TYPE_GRAY: image_type = PIKA_GRAY; layer_type = PIKA_GRAY_IMAGE; break; case PNG_COLOR_TYPE_GRAY_ALPHA: image_type = PIKA_GRAY; layer_type = PIKA_GRAYA_IMAGE; break; case PNG_COLOR_TYPE_PALETTE: image_type = PIKA_INDEXED; layer_type = PIKA_INDEXED_IMAGE; break; default: g_set_error (error, G_FILE_ERROR, 0, _("Unknown color model in PNG file '%s'."), pika_file_get_utf8_name (file)); return NULL; } width = png_get_image_width (pp, info); height = png_get_image_height (pp, info); image = pika_image_new_with_precision (width, height, image_type, image_precision); if (! image) { g_set_error (error, G_FILE_ERROR, 0, _("Could not create new image for '%s': %s"), pika_file_get_utf8_name (file), pika_pdb_get_last_error (pika_get_pdb ())); return NULL; } /* * Attach the color profile, if any */ if (profile) { pika_image_set_color_profile ((PikaImage *) image, profile); g_object_unref (profile); if (profile_name) { PikaParasite *parasite; parasite = pika_parasite_new ("icc-profile-name", PIKA_PARASITE_PERSISTENT | PIKA_PARASITE_UNDOABLE, strlen (profile_name), profile_name); pika_image_attach_parasite ((PikaImage *) image, parasite); pika_parasite_free (parasite); g_free (profile_name); } } /* * Create the "background" layer to hold the image... */ layer = pika_layer_new ((PikaImage *) image, _("Background"), width, height, layer_type, 100, pika_image_get_default_new_layer_mode ((PikaImage *) image)); pika_image_insert_layer ((PikaImage *) image, layer, NULL, 0); file_format = pika_drawable_get_format (PIKA_DRAWABLE (layer)); /* * Find out everything we can about the image resolution * This is only practical with the new 1.0 APIs, I'm afraid * due to a bug in libpng-1.0.6, see png-implement for details */ if (png_get_valid (pp, info, PNG_INFO_oFFs)) { gint offset_x = png_get_x_offset_pixels (pp, info); gint offset_y = png_get_y_offset_pixels (pp, info); if (offset_x != 0 || offset_y != 0) { if (! report_progress) { pika_layer_set_offsets (layer, offset_x, offset_y); } else if (offsets_dialog (offset_x, offset_y)) { pika_layer_set_offsets (layer, offset_x, offset_y); if (abs (offset_x) > width || abs (offset_y) > height) { g_message (_("The PNG file specifies an offset that caused " "the layer to be positioned outside the image.")); } } } } if (png_get_valid (pp, info, PNG_INFO_pHYs)) { png_uint_32 xres; png_uint_32 yres; gint unit_type; if (png_get_pHYs (pp, info, &xres, &yres, &unit_type) && xres > 0 && yres > 0) { switch (unit_type) { case PNG_RESOLUTION_UNKNOWN: { gdouble image_xres, image_yres; pika_image_get_resolution ((PikaImage *) image, &image_xres, &image_yres); if (xres > yres) image_xres = image_yres * (gdouble) xres / (gdouble) yres; else image_yres = image_xres * (gdouble) yres / (gdouble) xres; pika_image_set_resolution ((PikaImage *) image, image_xres, image_yres); *resolution_loaded = TRUE; } break; case PNG_RESOLUTION_METER: pika_image_set_resolution ((PikaImage *) image, (gdouble) xres * 0.0254, (gdouble) yres * 0.0254); pika_image_set_unit ((PikaImage *) image, PIKA_UNIT_MM); *resolution_loaded = TRUE; break; default: break; } } } /* * Load the colormap as necessary... */ if (png_get_color_type (pp, info) & PNG_COLOR_MASK_PALETTE) { png_colorp palette; int num_palette; png_get_PLTE (pp, info, &palette, &num_palette); pika_image_set_colormap ((PikaImage *) image, (guchar *) palette, num_palette); } bpp = babl_format_get_bytes_per_pixel (file_format); buffer = pika_drawable_get_buffer (PIKA_DRAWABLE (layer)); /* * Temporary buffer... */ tile_height = pika_tile_height (); pixel = g_new0 (guchar, tile_height * width * bpp); pixels = g_new (guchar *, tile_height); for (i = 0; i < tile_height; i++) pixels[i] = pixel + width * bpp * i; /* Install our own error handler to handle incomplete PNG files better */ error_data.buffer = buffer; error_data.pixel = pixel; error_data.file_format = file_format; error_data.tile_height = tile_height; error_data.width = width; error_data.height = height; error_data.bpp = bpp; png_set_error_fn (pp, &error_data, on_read_error, NULL); for (pass = 0; pass < num_passes; pass++) { /* * This works if you are only reading one row at a time... */ for (begin = 0; begin < height; begin += tile_height) { end = MIN (begin + tile_height, height); num = end - begin; if (pass != 0) /* to handle interlaced PiNGs */ gegl_buffer_get (buffer, GEGL_RECTANGLE (0, begin, width, num), 1.0, file_format, pixel, GEGL_AUTO_ROWSTRIDE, GEGL_ABYSS_NONE); error_data.begin = begin; error_data.end = end; error_data.num = num; png_read_rows (pp, pixels, NULL, num); gegl_buffer_set (buffer, GEGL_RECTANGLE (0, begin, width, num), 0, file_format, pixel, GEGL_AUTO_ROWSTRIDE); if (report_progress) pika_progress_update (((gdouble) pass + (gdouble) end / (gdouble) height) / (gdouble) num_passes); } } png_read_end (pp, info); /* Switch back to default error handler */ png_set_error_fn (pp, NULL, NULL, NULL); if (png_get_text (pp, info, &text, &num_texts)) { gchar *comment = NULL; for (i = 0; i < num_texts && !comment; i++, text++) { if (text->key == NULL || strcmp (text->key, "Comment")) continue; if (text->text_length > 0) /* tEXt */ { comment = g_convert (text->text, -1, "UTF-8", "ISO-8859-1", NULL, NULL, NULL); } else if (g_utf8_validate (text->text, -1, NULL)) { /* iTXt */ comment = g_strdup (text->text); } } if (comment && *comment) { PikaParasite *parasite; parasite = pika_parasite_new ("pika-comment", PIKA_PARASITE_PERSISTENT, strlen (comment) + 1, comment); pika_image_attach_parasite ((PikaImage *) image, parasite); pika_parasite_free (parasite); } g_free (comment); } /* * Done with the file... */ png_destroy_read_struct (&pp, &info, NULL); g_free (pixel); g_free (pixels); g_object_unref (buffer); free (pp); free (info); fclose (fp); if (trns) { GeglBufferIterator *iter; gint n_components; pika_layer_add_alpha (layer); buffer = pika_drawable_get_buffer (PIKA_DRAWABLE (layer)); file_format = gegl_buffer_get_format (buffer); iter = gegl_buffer_iterator_new (buffer, NULL, 0, file_format, GEGL_ACCESS_READWRITE, GEGL_ABYSS_NONE, 1); n_components = babl_format_get_n_components (file_format); g_warn_if_fail (n_components == 2); while (gegl_buffer_iterator_next (iter)) { guchar *data = iter->items[0].data; gint length = iter->length; while (length--) { data[1] = alpha[data[0]]; data += n_components; } } g_object_unref (buffer); } /* If any safe-to-copy chunks were saved, * store them in the image as parasite */ if (safe_to_copy_chunks) { GSList *iter; for (iter = safe_to_copy_chunks; iter; iter = iter->next) { PikaParasite *parasite = iter->data; pika_image_attach_parasite ((PikaImage *) image, parasite); pika_parasite_free (parasite); } g_slist_free (safe_to_copy_chunks); } return (PikaImage *) image; } /* * 'offsets_dialog ()' - Asks the user about offsets when loading. */ static gboolean offsets_dialog (gint offset_x, gint offset_y) { GtkWidget *dialog; GtkWidget *hbox; GtkWidget *image; GtkWidget *label; gchar *message; gboolean run; pika_ui_init (PLUG_IN_BINARY); dialog = pika_dialog_new (_("Apply PNG Offset"), PLUG_IN_ROLE, NULL, 0, pika_standard_help_func, LOAD_PROC, _("Ignore PNG offset"), GTK_RESPONSE_NO, _("Apply PNG offset to layer"), GTK_RESPONSE_YES, NULL); gtk_dialog_set_default_response (GTK_DIALOG (dialog), GTK_RESPONSE_YES); pika_dialog_set_alternative_button_order (GTK_DIALOG (dialog), GTK_RESPONSE_YES, GTK_RESPONSE_NO, -1); pika_window_set_transient (GTK_WINDOW (dialog)); gtk_window_set_resizable (GTK_WINDOW (dialog), FALSE); hbox = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 12); gtk_container_set_border_width (GTK_CONTAINER (hbox), 12); gtk_box_pack_start (GTK_BOX (gtk_dialog_get_content_area (GTK_DIALOG (dialog))), hbox, FALSE, FALSE, 0); gtk_widget_show (hbox); image = gtk_image_new_from_icon_name (PIKA_ICON_DIALOG_QUESTION, GTK_ICON_SIZE_DIALOG); gtk_widget_set_valign (image, GTK_ALIGN_START); gtk_box_pack_start (GTK_BOX (hbox), image, FALSE, FALSE, 0); gtk_widget_show (image); message = g_strdup_printf (_("The PNG image you are importing specifies an " "offset of %d, %d. Do you want to apply " "this offset to the layer?"), offset_x, offset_y); label = gtk_label_new (message); gtk_label_set_yalign (GTK_LABEL (label), 0.0); gtk_label_set_line_wrap (GTK_LABEL (label), TRUE); gtk_box_pack_start (GTK_BOX (hbox), label, TRUE, TRUE, 0); gtk_widget_show (label); gtk_widget_show (dialog); run = (pika_dialog_run (PIKA_DIALOG (dialog)) == GTK_RESPONSE_YES); gtk_widget_destroy (dialog); return run; } /* * 'save_image ()' - Export the specified image to a PNG file. */ typedef struct { gboolean has_trns; png_bytep trans; int num_trans; gboolean has_plte; png_colorp palette; int num_palette; } PngGlobals; static PngGlobals pngg; static gboolean save_image (GFile *file, PikaImage *image, PikaDrawable *drawable, PikaImage *orig_image, GObject *config, gint *bits_per_sample, gboolean report_progress, GError **error) { gint i, k; /* Looping vars */ gint bpp = 0; /* Bytes per pixel */ gint type; /* Type of drawable/layer */ gint num_passes; /* Number of interlace passes in file */ gint pass; /* Current pass in file */ gint tile_height; /* Height of tile in PIKA */ gint width; /* image width */ gint height; /* image height */ gint begin; /* Beginning tile row */ gint end; /* Ending tile row */ gint num; /* Number of rows to load */ FILE *fp; /* File pointer */ PikaColorProfile *profile = NULL; /* Color profile */ gchar **parasites; /* Safe-to-copy chunks */ gboolean out_linear; /* Save linear RGB */ GeglBuffer *buffer; /* GEGL buffer for layer */ const Babl *file_format = NULL; /* BABL format of file */ const gchar *encoding; const Babl *space; png_structp pp; /* PNG read pointer */ png_infop info; /* PNG info pointer */ gint offx, offy; /* Drawable offsets from origin */ guchar **pixels; /* Pixel rows */ guchar *fixed; /* Fixed-up pixel data */ guchar *pixel; /* Pixel data */ gdouble xres, yres; /* PIKA resolution (dpi) */ png_color_16 background; /* Background color */ png_time mod_time; /* Modification time (ie NOW) */ time_t cutime; /* Time since epoch */ struct tm *gmt; /* GMT broken down */ gint color_type; /* PNG color type */ gint bit_depth; /* Default to bit depth 16 */ guchar remap[256]; /* Re-mapping for the palette */ png_textp text = NULL; gboolean save_interlaced; gboolean save_bkgd; gboolean save_offs; gboolean save_phys; gboolean save_time; gboolean save_comment; gchar *comment; gboolean save_transp_pixels; gboolean optimize_palette; gint compression_level; PngExportFormat export_format; gboolean save_profile; #if !defined(PNG_iCCP_SUPPORTED) g_object_set (config, "save-color-profile", FALSE, NULL); #endif g_object_get (config, "interlaced", &save_interlaced, "bkgd", &save_bkgd, "offs", &save_offs, "phys", &save_phys, "time", &save_time, "save-comment", &save_comment, "pika-comment", &comment, "save-transparent", &save_transp_pixels, "optimize-palette", &optimize_palette, "compression", &compression_level, "save-color-profile", &save_profile, NULL); export_format = pika_procedure_config_get_choice_id (PIKA_PROCEDURE_CONFIG (config), "format"); out_linear = FALSE; space = pika_drawable_get_format (drawable); #if defined(PNG_iCCP_SUPPORTED) /* If no profile is written: export as sRGB. * If manually assigned profile written: follow its TRC. * If default profile written: * - when export as auto or 16-bit: follow the storage TRC. * - when export from 8-bit storage: follow the storage TRC. * - when converting high bit depth to 8-bit: export as sRGB. */ if (save_profile) { profile = pika_image_get_color_profile (orig_image); if (profile || export_format == PNG_FORMAT_AUTO || export_format == PNG_FORMAT_RGB16 || export_format == PNG_FORMAT_RGBA16 || export_format == PNG_FORMAT_GRAY16 || export_format == PNG_FORMAT_GRAYA16 || pika_image_get_precision (image) == PIKA_PRECISION_U8_LINEAR || pika_image_get_precision (image) == PIKA_PRECISION_U8_NON_LINEAR || pika_image_get_precision (image) == PIKA_PRECISION_U8_PERCEPTUAL) { if (! profile) profile = pika_image_get_effective_color_profile (orig_image); out_linear = (pika_color_profile_is_linear (profile)); } else { /* When converting higher bit depth work image into 8-bit, * with no manually assigned profile, make sure the result is * sRGB. */ profile = pika_image_get_effective_color_profile (orig_image); if (pika_color_profile_is_linear (profile)) { PikaColorProfile *saved_profile; saved_profile = pika_color_profile_new_srgb_trc_from_color_profile (profile); g_object_unref (profile); profile = saved_profile; } } space = pika_color_profile_get_space (profile, PIKA_COLOR_RENDERING_INTENT_RELATIVE_COLORIMETRIC, error); if (error && *error) { /* XXX: the profile space should normally be the same one as * the drawable's so let's continue with it. We were mostly * getting the profile space to be complete. Still let's * display the error to standard error channel because if the * space could not be extracted, there is a problem somewhere! */ g_printerr ("%s: error getting the profile space: %s", G_STRFUNC, (*error)->message); g_clear_error (error); space = pika_drawable_get_format (drawable); } } #endif /* We save as 8-bit PNG only if: * (1) Work image is 8-bit linear with linear profile to be saved. * (2) Work image is 8-bit non-linear or perceptual with or without * profile. */ bit_depth = 16; switch (pika_image_get_precision (image)) { case PIKA_PRECISION_U8_LINEAR: if (out_linear) bit_depth = 8; break; case PIKA_PRECISION_U8_NON_LINEAR: case PIKA_PRECISION_U8_PERCEPTUAL: if (! out_linear) bit_depth = 8; break; default: break; } pp = png_create_write_struct (PNG_LIBPNG_VER_STRING, NULL, NULL, NULL); if (!pp) { /* this could happen if the compile time and run-time libpng * versions do not match. */ g_set_error (error, G_FILE_ERROR, 0, _("Error creating PNG write struct while exporting '%s'."), pika_file_get_utf8_name (file)); return FALSE; } info = png_create_info_struct (pp); if (! info) { g_set_error (error, G_FILE_ERROR, 0, _("Error while exporting '%s'. Could not create PNG header info structure."), pika_file_get_utf8_name (file)); return FALSE; } if (setjmp (png_jmpbuf (pp))) { g_set_error (error, G_FILE_ERROR, 0, _("Error while exporting '%s'. Could not export image."), pika_file_get_utf8_name (file)); return FALSE; } #ifdef PNG_BENIGN_ERRORS_SUPPORTED /* Change some libpng errors to warnings (e.g. bug 721135) */ png_set_benign_errors (pp, TRUE); /* bug 765850 */ png_set_option (pp, PNG_SKIP_sRGB_CHECK_PROFILE, PNG_OPTION_ON); #endif /* * Open the file and initialize the PNG write "engine"... */ if (report_progress) pika_progress_init_printf (_("Exporting '%s'"), pika_file_get_utf8_name (file)); fp = g_fopen (g_file_peek_path (file), "wb"); if (! 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)); return FALSE; } png_init_io (pp, fp); /* * Get the buffer for the current image... */ buffer = pika_drawable_get_buffer (drawable); width = gegl_buffer_get_width (buffer); height = gegl_buffer_get_height (buffer); type = pika_drawable_type (drawable); /* * Initialise remap[] */ for (i = 0; i < 256; i++) remap[i] = i; if (export_format == PNG_FORMAT_AUTO) { /* * Set color type and remember bytes per pixel count */ switch (type) { case PIKA_RGB_IMAGE: color_type = PNG_COLOR_TYPE_RGB; if (bit_depth == 8) { if (out_linear) encoding = "RGB u8"; else encoding = "R'G'B' u8"; } else { if (out_linear) encoding = "RGB u16"; else encoding = "R'G'B' u16"; } break; case PIKA_RGBA_IMAGE: color_type = PNG_COLOR_TYPE_RGB_ALPHA; if (bit_depth == 8) { if (out_linear) encoding = "RGBA u8"; else encoding = "R'G'B'A u8"; } else { if (out_linear) encoding = "RGBA u16"; else encoding = "R'G'B'A u16"; } break; case PIKA_GRAY_IMAGE: color_type = PNG_COLOR_TYPE_GRAY; if (bit_depth == 8) { if (out_linear) encoding = "Y u8"; else encoding = "Y' u8"; } else { if (out_linear) encoding = "Y u16"; else encoding = "Y' u16"; } break; case PIKA_GRAYA_IMAGE: color_type = PNG_COLOR_TYPE_GRAY_ALPHA; if (bit_depth == 8) { if (out_linear) encoding = "YA u8"; else encoding = "Y'A u8"; } else { if (out_linear) encoding = "YA u16"; else encoding = "Y'A u16"; } break; case PIKA_INDEXED_IMAGE: color_type = PNG_COLOR_TYPE_PALETTE; file_format = pika_drawable_get_format (drawable); pngg.has_plte = TRUE; pngg.palette = (png_colorp) pika_image_get_colormap (image, NULL, &pngg.num_palette); if (optimize_palette) bit_depth = get_bit_depth_for_palette (pngg.num_palette); break; case PIKA_INDEXEDA_IMAGE: color_type = PNG_COLOR_TYPE_PALETTE; file_format = pika_drawable_get_format (drawable); /* fix up transparency */ if (optimize_palette) bit_depth = respin_cmap (pp, info, remap, image, drawable); else respin_cmap (pp, info, remap, image, drawable); break; default: g_set_error (error, G_FILE_ERROR, 0, "Image type can't be exported as PNG"); return FALSE; } } else { switch (export_format) { case PNG_FORMAT_RGB8: color_type = PNG_COLOR_TYPE_RGB; if (out_linear) encoding = "RGB u8"; else encoding = "R'G'B' u8"; bit_depth = 8; break; case PNG_FORMAT_GRAY8: color_type = PNG_COLOR_TYPE_GRAY; if (out_linear) encoding = "Y u8"; else encoding = "Y' u8"; bit_depth = 8; break; case PNG_FORMAT_RGBA8: color_type = PNG_COLOR_TYPE_RGB_ALPHA; if (out_linear) encoding = "RGBA u8"; else encoding = "R'G'B'A u8"; bit_depth = 8; break; case PNG_FORMAT_GRAYA8: color_type = PNG_COLOR_TYPE_GRAY_ALPHA; if (out_linear) encoding = "YA u8"; else encoding = "Y'A u8"; bit_depth = 8; break; case PNG_FORMAT_RGB16: color_type = PNG_COLOR_TYPE_RGB; if (out_linear) encoding = "RGB u16"; else encoding = "R'G'B' u16"; bit_depth = 16; break; case PNG_FORMAT_GRAY16: color_type = PNG_COLOR_TYPE_GRAY; if (out_linear) encoding = "Y u16"; else encoding = "Y' u16"; bit_depth = 16; break; case PNG_FORMAT_RGBA16: color_type = PNG_COLOR_TYPE_RGB_ALPHA; if (out_linear) encoding = "RGBA u16"; else encoding = "R'G'B'A u16"; bit_depth = 16; break; case PNG_FORMAT_GRAYA16: color_type = PNG_COLOR_TYPE_GRAY_ALPHA; if (out_linear) encoding = "YA u16"; else encoding = "Y'A u16"; bit_depth = 16; break; case PNG_FORMAT_AUTO: g_return_val_if_reached (FALSE); } } if (! file_format) file_format = babl_format_with_space (encoding, space); bpp = babl_format_get_bytes_per_pixel (file_format); /* Note: png_set_IHDR() must be called before any other png_set_*() functions. */ png_set_IHDR (pp, info, width, height, bit_depth, color_type, save_interlaced ? PNG_INTERLACE_ADAM7 : PNG_INTERLACE_NONE, PNG_COMPRESSION_TYPE_BASE, PNG_FILTER_TYPE_BASE); if (pngg.has_trns) png_set_tRNS (pp, info, pngg.trans, pngg.num_trans, NULL); if (pngg.has_plte) png_set_PLTE (pp, info, pngg.palette, pngg.num_palette); /* Set the compression level */ png_set_compression_level (pp, compression_level); /* All this stuff is optional extras, if the user is aiming for smallest possible file size she can turn them all off */ if (save_bkgd) { PikaRGB color; guchar red, green, blue; pika_context_get_background (&color); pika_rgb_get_uchar (&color, &red, &green, &blue); background.index = 0; background.red = red; background.green = green; background.blue = blue; background.gray = pika_rgb_luminance_uchar (&color); png_set_bKGD (pp, info, &background); } if (save_offs) { pika_drawable_get_offsets (drawable, &offx, &offy); if (offx != 0 || offy != 0) png_set_oFFs (pp, info, offx, offy, PNG_OFFSET_PIXEL); } if (save_phys) { pika_image_get_resolution (orig_image, &xres, &yres); png_set_pHYs (pp, info, RINT (xres / 0.0254), RINT (yres / 0.0254), PNG_RESOLUTION_METER); } if (save_time) { cutime = time (NULL); /* time right NOW */ gmt = gmtime (&cutime); mod_time.year = gmt->tm_year + 1900; mod_time.month = gmt->tm_mon + 1; mod_time.day = gmt->tm_mday; mod_time.hour = gmt->tm_hour; mod_time.minute = gmt->tm_min; mod_time.second = gmt->tm_sec; png_set_tIME (pp, info, &mod_time); } #if defined(PNG_iCCP_SUPPORTED) if (save_profile) { PikaParasite *parasite; gchar *profile_name = NULL; const guint8 *icc_data; gsize icc_length; icc_data = pika_color_profile_get_icc_profile (profile, &icc_length); parasite = pika_image_get_parasite (orig_image, "icc-profile-name"); if (parasite) { gchar *parasite_data; guint32 parasite_size; parasite_data = (gchar *) pika_parasite_get_data (parasite, ¶site_size); profile_name = g_convert (parasite_data, parasite_size, "UTF-8", "ISO-8859-1", NULL, NULL, NULL); } png_set_iCCP (pp, info, profile_name ? profile_name : "ICC profile", 0, icc_data, icc_length); g_free (profile_name); g_object_unref (profile); } else #endif { /* Be more specific by writing into the file that the image is in * sRGB color space. */ PikaColorConfig *config = pika_get_color_configuration (); int srgb_intent; switch (pika_color_config_get_display_intent (config)) { case PIKA_COLOR_RENDERING_INTENT_PERCEPTUAL: srgb_intent = PNG_sRGB_INTENT_PERCEPTUAL; break; case PIKA_COLOR_RENDERING_INTENT_RELATIVE_COLORIMETRIC: srgb_intent = PNG_sRGB_INTENT_RELATIVE; break; case PIKA_COLOR_RENDERING_INTENT_SATURATION: srgb_intent = PNG_sRGB_INTENT_SATURATION; break; case PIKA_COLOR_RENDERING_INTENT_ABSOLUTE_COLORIMETRIC: srgb_intent = PNG_sRGB_INTENT_ABSOLUTE; break; } png_set_sRGB_gAMA_and_cHRM (pp, info, srgb_intent); g_object_unref (config); } #ifdef PNG_zTXt_SUPPORTED /* Small texts are not worth compressing and will be even bigger if compressed. Empirical length limit of a text being worth compressing. */ #define COMPRESSION_WORTHY_LENGTH 200 #endif if (save_comment && comment && strlen (comment)) { gsize text_length = 0; text = g_new0 (png_text, 1); text[0].key = "Comment"; #ifdef PNG_iTXt_SUPPORTED text[0].text = g_convert (comment, -1, "ISO-8859-1", "UTF-8", NULL, &text_length, NULL); if (text[0].text == NULL || strlen (text[0].text) == 0) { /* We can't convert to ISO-8859-1 without loss. * Save the comment as iTXt (UTF-8). */ g_free (text[0].text); text[0].text = g_strdup (comment); text[0].itxt_length = strlen (text[0].text); #ifdef PNG_zTXt_SUPPORTED text[0].compression = strlen (text[0].text) > COMPRESSION_WORTHY_LENGTH ? PNG_ITXT_COMPRESSION_zTXt : PNG_ITXT_COMPRESSION_NONE; #else text[0].compression = PNG_ITXT_COMPRESSION_NONE; #endif /* PNG_zTXt_SUPPORTED */ } else /* The comment is ISO-8859-1 compatible, so we use tEXt even * if there is iTXt support for compatibility to more png * reading programs. */ #endif /* PNG_iTXt_SUPPORTED */ { #ifndef PNG_iTXt_SUPPORTED /* No iTXt support, so we are forced to use tEXt * (ISO-8859-1). A broken comment is better than no comment * at all, so the conversion does not fail on unknown * character. They are simply ignored. */ text[0].text = g_convert_with_fallback (comment, -1, "ISO-8859-1", "UTF-8", "", NULL, &text_length, NULL); #endif #ifdef PNG_zTXt_SUPPORTED text[0].compression = strlen (text[0].text) > COMPRESSION_WORTHY_LENGTH ? PNG_TEXT_COMPRESSION_zTXt : PNG_TEXT_COMPRESSION_NONE; #else text[0].compression = PNG_TEXT_COMPRESSION_NONE; #endif /* PNG_zTXt_SUPPORTED */ text[0].text_length = text_length; } if (! text[0].text || strlen (text[0].text) == 0) { g_free (text[0].text); g_free (text); text = NULL; } } g_free (comment); #ifdef PNG_zTXt_SUPPORTED #undef COMPRESSION_WORTHY_LENGTH #endif if (text) png_set_text (pp, info, text, 1); png_write_info (pp, info); if (G_BYTE_ORDER == G_LITTLE_ENDIAN) png_set_swap (pp); /* Write any safe-to-copy chunks saved from import */ parasites = pika_image_get_parasite_list (image); if (parasites) { gint count; count = g_strv_length (parasites); for (gint i = 0; i < count; i++) { if (strncmp (parasites[i], "png", 3) == 0) { PikaParasite *parasite; parasite = pika_image_get_parasite (image, parasites[i]); if (parasite) { gchar buf[1024]; gchar *chunk_name; g_strlcpy (buf, parasites[i], sizeof (buf)); chunk_name = strchr (buf, '/'); chunk_name++; if (chunk_name) { png_byte name[4]; const guint8 *data; guint32 len; for (gint j = 0; j < 4; j++) name[j] = chunk_name[j]; data = (const guint8 *) pika_parasite_get_data (parasite, &len); png_write_chunk (pp, name, data, len); } pika_parasite_free (parasite); } } } } g_strfreev (parasites); /* * Turn on interlace handling... */ if (save_interlaced) num_passes = png_set_interlace_handling (pp); else num_passes = 1; /* * Convert unpacked pixels to packed if necessary */ if (color_type == PNG_COLOR_TYPE_PALETTE && bit_depth < 8) png_set_packing (pp); /* * Allocate memory for "tile_height" rows and export the image... */ tile_height = pika_tile_height (); pixel = g_new (guchar, tile_height * width * bpp); pixels = g_new (guchar *, tile_height); for (i = 0; i < tile_height; i++) pixels[i] = pixel + width * bpp * i; for (pass = 0; pass < num_passes; pass++) { /* This works if you are only writing one row at a time... */ for (begin = 0, end = tile_height; begin < height; begin += tile_height, end += tile_height) { if (end > height) end = height; num = end - begin; gegl_buffer_get (buffer, GEGL_RECTANGLE (0, begin, width, num), 1.0, file_format, pixel, GEGL_AUTO_ROWSTRIDE, GEGL_ABYSS_NONE); /* If we are with a RGBA image and have to pre-multiply the alpha channel */ if (bpp == 4 && ! save_transp_pixels) { for (i = 0; i < num; ++i) { fixed = pixels[i]; for (k = 0; k < width; ++k) { if (!fixed[3]) fixed[0] = fixed[1] = fixed[2] = 0; fixed += bpp; } } } if (bpp == 8 && ! save_transp_pixels) { for (i = 0; i < num; ++i) { fixed = pixels[i]; for (k = 0; k < width; ++k) { if (!fixed[6] && !fixed[7]) fixed[0] = fixed[1] = fixed[2] = fixed[3] = fixed[4] = fixed[5] = 0; fixed += bpp; } } } /* If we're dealing with a paletted image with * transparency set, write out the remapped palette */ if (png_get_valid (pp, info, PNG_INFO_tRNS)) { guchar inverse_remap[256]; for (i = 0; i < 256; i++) inverse_remap[ remap[i] ] = i; for (i = 0; i < num; ++i) { fixed = pixels[i]; for (k = 0; k < width; ++k) { fixed[k] = (fixed[k*2+1] > 127) ? inverse_remap[ fixed[k*2] ] : 0; } } } /* Otherwise if we have a paletted image and transparency * couldn't be set, we ignore the alpha channel */ else if (png_get_valid (pp, info, PNG_INFO_PLTE) && bpp == 2) { for (i = 0; i < num; ++i) { fixed = pixels[i]; for (k = 0; k < width; ++k) { fixed[k] = fixed[k * 2]; } } } png_write_rows (pp, pixels, num); if (report_progress) pika_progress_update (((double) pass + (double) end / (double) height) / (double) num_passes); } } if (report_progress) pika_progress_update (1.0); png_write_end (pp, info); png_destroy_write_struct (&pp, &info); g_free (pixel); g_free (pixels); /* * Done with the file... */ if (text) { g_free (text[0].text); g_free (text); } free (pp); free (info); fclose (fp); *bits_per_sample = bit_depth; return TRUE; } static gboolean ia_has_transparent_pixels (GeglBuffer *buffer) { GeglBufferIterator *iter; const Babl *format; gint n_components; format = gegl_buffer_get_format (buffer); iter = gegl_buffer_iterator_new (buffer, NULL, 0, format, GEGL_ACCESS_READ, GEGL_ABYSS_NONE, 1); n_components = babl_format_get_n_components (format); g_return_val_if_fail (n_components == 2, FALSE); while (gegl_buffer_iterator_next (iter)) { const guchar *data = iter->items[0].data; gint length = iter->length; while (length--) { if (data[1] <= 127) { gegl_buffer_iterator_stop (iter); return TRUE; } data += n_components; } } return FALSE; } /* 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 * gif.c */ static gint find_unused_ia_color (GeglBuffer *buffer, gint *colors) { GeglBufferIterator *iter; const Babl *format; gint n_components; gboolean ix_used[256]; gboolean trans_used = FALSE; gint i; for (i = 0; i < *colors; i++) ix_used[i] = FALSE; format = gegl_buffer_get_format (buffer); iter = gegl_buffer_iterator_new (buffer, NULL, 0, format, GEGL_ACCESS_READ, GEGL_ABYSS_NONE, 1); n_components = babl_format_get_n_components (format); g_return_val_if_fail (n_components == 2, FALSE); while (gegl_buffer_iterator_next (iter)) { const guchar *data = iter->items[0].data; gint length = iter->length; while (length--) { if (data[1] > 127) ix_used[data[0]] = TRUE; else trans_used = TRUE; data += n_components; } } /* If there is no transparency, ignore alpha. */ if (trans_used == FALSE) return -1; /* If there is still some room at the end of the palette, increment * the number of colors in the image and assign a transparent pixel * there. */ if ((*colors) < 256) { (*colors)++; return (*colors) - 1; } for (i = 0; i < *colors; i++) { if (ix_used[i] == FALSE) return i; } return -1; } static int respin_cmap (png_structp pp, png_infop info, guchar *remap, PikaImage *image, PikaDrawable *drawable) { static guchar trans[] = { 0 }; GeglBuffer *buffer; gint colors; guchar *before; before = pika_image_get_colormap (image, NULL, &colors); buffer = pika_drawable_get_buffer (drawable); /* Make sure there is something in the colormap. */ if (colors == 0) { before = g_newa (guchar, 3); memset (before, 0, sizeof (guchar) * 3); colors = 1; } /* Try to find an entry which isn't actually used in the image, for * a transparency index. */ if (ia_has_transparent_pixels (buffer)) { gint transparent = find_unused_ia_color (buffer, &colors); if (transparent != -1) /* we have a winner for a transparent * index - do like gif2png and swap * index 0 and index transparent */ { static png_color palette[256]; gint i; /* Set tRNS chunk values for writing later. */ pngg.has_trns = TRUE; pngg.trans = trans; pngg.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; for (i = 1; i <= transparent; i++) remap[i] = i - 1; /* 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 = 0; 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. */ pngg.has_plte = TRUE; pngg.palette = palette; pngg.num_palette = colors; } else { /* Inform the user that we couldn't losslessly save the * transparency & just use the full palette */ g_message (_("Couldn't losslessly save transparency, " "saving opacity instead.")); /* Set PLTE chunk values for writing later. */ pngg.has_plte = TRUE; pngg.palette = (png_colorp) before; pngg.num_palette = colors; } } else { /* Set PLTE chunk values for writing later. */ pngg.has_plte = TRUE; pngg.palette = (png_colorp) before; pngg.num_palette = colors; } g_object_unref (buffer); return get_bit_depth_for_palette (colors); } static gint read_unknown_chunk (png_structp png_ptr, png_unknown_chunkp chunk) { /* Chunks with a lowercase letter in the 4th byte * are safe to copy */ if (g_ascii_islower (chunk->name[3])) { PikaParasite *parasite; gchar pname[255]; g_snprintf (pname, sizeof (pname), "png/%s", chunk->name); if ((parasite = pika_parasite_new (pname, PIKA_PARASITE_PERSISTENT, chunk->size, chunk->data))) safe_to_copy_chunks = g_slist_prepend (safe_to_copy_chunks, parasite); } return 0; } static gboolean save_dialog (PikaImage *image, PikaProcedure *procedure, GObject *config, gboolean alpha) { GtkWidget *dialog; gboolean run; gboolean indexed; indexed = (pika_image_get_base_type (image) == PIKA_INDEXED); dialog = pika_save_procedure_dialog_new (PIKA_SAVE_PROCEDURE (procedure), PIKA_PROCEDURE_CONFIG (config), image); pika_procedure_dialog_get_widget (PIKA_PROCEDURE_DIALOG (dialog), "compression", PIKA_TYPE_SPIN_SCALE); pika_procedure_dialog_set_sensitive (PIKA_PROCEDURE_DIALOG (dialog), "save-transparent", alpha, NULL, NULL, FALSE); pika_procedure_dialog_set_sensitive (PIKA_PROCEDURE_DIALOG (dialog), "optimize-palette", indexed, NULL, NULL, FALSE); pika_save_procedure_dialog_add_metadata (PIKA_SAVE_PROCEDURE_DIALOG (dialog), "bkgd"); pika_save_procedure_dialog_add_metadata (PIKA_SAVE_PROCEDURE_DIALOG (dialog), "offs"); pika_save_procedure_dialog_add_metadata (PIKA_SAVE_PROCEDURE_DIALOG (dialog), "phys"); pika_save_procedure_dialog_add_metadata (PIKA_SAVE_PROCEDURE_DIALOG (dialog), "time"); pika_procedure_dialog_fill (PIKA_PROCEDURE_DIALOG (dialog), "format", "compression", "interlaced", "save-transparent", "optimize-palette", NULL); run = pika_procedure_dialog_run (PIKA_PROCEDURE_DIALOG (dialog)); gtk_widget_destroy (dialog); return run; }