]> Pileus Git - ~andy/gtk/blobdiff - gdk-pixbuf/io-png.c
Add color management support to gdk_pixbuf_save
[~andy/gtk] / gdk-pixbuf / io-png.c
index 643b16c4087387b485c82b23f2081fade0999458..8b90865855a26f9c6f451ea50b36d34b3c49ec9c 100644 (file)
@@ -1,4 +1,5 @@
-/* GdkPixbuf library - JPEG image loader
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*- */
+/* GdkPixbuf library - PNG image loader
  *
  * Copyright (C) 1999 Mark Crichton
  * Copyright (C) 1999 The Free Software Foundation
@@ -7,32 +8,33 @@
  *          Federico Mena-Quintero <federico@gimp.org>
  *
  * This library is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Library General Public
+ * modify it under the terms of the GNU Lesser General Public
  * License as published by the Free Software Foundation; either
  * version 2 of the License, or (at your option) any later version.
  *
  * This library 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
- * Library General Public License for more details.
+ * Lesser General Public License for more details.
  *
- * You should have received a copy of the GNU Library General Public
+ * You should have received a copy of the GNU Lesser General Public
  * License along with this library; if not, write to the
  * Free Software Foundation, Inc., 59 Temple Place - Suite 330,
  * Boston, MA 02111-1307, USA.
  */
 
-#include <config.h>
+#include "config.h"
 #include <stdio.h>
+#include <stdlib.h>
 #include <png.h>
-#include "gdk-pixbuf.h"
+#include "gdk-pixbuf-private.h"
 #include "gdk-pixbuf-io.h"
 
 \f
 
-static void
+static gboolean
 setup_png_transformations(png_structp png_read_ptr, png_infop png_info_ptr,
-                          gboolean *fatal_error_occurred,
+                          GError **error,
                           png_uint_32* width_p, png_uint_32* height_p,
                           int* color_type_p)
 {
@@ -42,6 +44,17 @@ setup_png_transformations(png_structp png_read_ptr, png_infop png_info_ptr,
         
         /* Get the image info */
 
+        /* Must check bit depth, since png_get_IHDR generates an 
+           FPE on bit_depth 0.
+        */
+        bit_depth = png_get_bit_depth (png_read_ptr, png_info_ptr);
+        if (bit_depth < 1 || bit_depth > 16) {
+                g_set_error_literal (error,
+                                     GDK_PIXBUF_ERROR,
+                                     GDK_PIXBUF_ERROR_CORRUPT_IMAGE,
+                                     _("Bits per channel of PNG image is invalid."));
+                return FALSE;
+        }
         png_get_IHDR (png_read_ptr, png_info_ptr,
                       &width, &height,
                       &bit_depth,
@@ -114,51 +127,158 @@ setup_png_transformations(png_structp png_read_ptr, png_infop png_info_ptr,
         *height_p = height;
         *color_type_p = color_type;
         
-#ifndef G_DISABLE_CHECKS
         /* Check that the new info is what we want */
         
+        if (width == 0 || height == 0) {
+                g_set_error_literal (error,
+                                     GDK_PIXBUF_ERROR,
+                                     GDK_PIXBUF_ERROR_CORRUPT_IMAGE,
+                                     _("Transformed PNG has zero width or height."));
+                return FALSE;
+        }
+
         if (bit_depth != 8) {
-                g_warning("Bits per channel of transformed PNG is %d, not 8.", bit_depth);
-                *fatal_error_occurred = TRUE;
-                return;
+                g_set_error_literal (error,
+                                     GDK_PIXBUF_ERROR,
+                                     GDK_PIXBUF_ERROR_CORRUPT_IMAGE,
+                                     _("Bits per channel of transformed PNG is not 8."));
+                return FALSE;
         }
 
         if ( ! (color_type == PNG_COLOR_TYPE_RGB ||
                 color_type == PNG_COLOR_TYPE_RGB_ALPHA) ) {
-                g_warning("Transformed PNG not RGB or RGBA.");
-                *fatal_error_occurred = TRUE;
-                return;
+                g_set_error_literal (error,
+                                     GDK_PIXBUF_ERROR,
+                                     GDK_PIXBUF_ERROR_CORRUPT_IMAGE,
+                                     _("Transformed PNG not RGB or RGBA."));
+                return FALSE;
         }
 
         channels = png_get_channels(png_read_ptr, png_info_ptr);
         if ( ! (channels == 3 || channels == 4) ) {
-                g_warning("Transformed PNG has %d channels, must be 3 or 4.", channels);
-                *fatal_error_occurred = TRUE;
-                return;
+                g_set_error_literal (error,
+                                     GDK_PIXBUF_ERROR,
+                                     GDK_PIXBUF_ERROR_CORRUPT_IMAGE,
+                                     _("Transformed PNG has unsupported number of channels, must be 3 or 4."));
+                return FALSE;
         }
-#endif
+        return TRUE;
 }
 
-/* Destroy notification function for the libart pixbuf */
 static void
-free_buffer (gpointer user_data, gpointer data)
+png_simple_error_callback(png_structp png_save_ptr,
+                          png_const_charp error_msg)
 {
-       free (data);
+        GError **error;
+        
+        error = png_get_error_ptr(png_save_ptr);
+
+        /* I don't trust libpng to call the error callback only once,
+         * so check for already-set error
+         */
+        if (error && *error == NULL) {
+                g_set_error (error,
+                             GDK_PIXBUF_ERROR,
+                             GDK_PIXBUF_ERROR_FAILED,
+                             _("Fatal error in PNG image file: %s"),
+                             error_msg);
+        }
+
+        longjmp (png_save_ptr->jmpbuf, 1);
+}
+
+static void
+png_simple_warning_callback(png_structp png_save_ptr,
+                            png_const_charp warning_msg)
+{
+        /* Don't print anything; we should not be dumping junk to
+         * stderr, since that may be bad for some apps. If it's
+         * important enough to display, we need to add a GError
+         * **warning return location wherever we have an error return
+         * location.
+         */
+}
+
+static gboolean
+png_text_to_pixbuf_option (png_text   text_ptr,
+                           gchar    **key,
+                           gchar    **value)
+{
+        gboolean is_ascii = TRUE;
+        int i;
+
+        /* Avoid loading iconv if the text is plain ASCII */
+        for (i = 0; i < text_ptr.text_length; i++)
+                if (text_ptr.text[i] & 0x80) {
+                        is_ascii = FALSE;
+                        break;
+                }
+
+        if (is_ascii) {
+                *value = g_strdup (text_ptr.text);
+        } else {
+                *value = g_convert (text_ptr.text, -1,
+                                     "UTF-8", "ISO-8859-1",
+                                     NULL, NULL, NULL);
+        }
+
+        if (*value) {
+                *key = g_strconcat ("tEXt::", text_ptr.key, NULL);
+                return TRUE;
+        } else {
+                g_warning ("Couldn't convert text chunk value to UTF-8.");
+                *key = NULL;
+                return FALSE;
+        }
+}
+
+static png_voidp
+png_malloc_callback (png_structp o, png_size_t size)
+{
+        return g_try_malloc (size);
+}
+
+static void
+png_free_callback (png_structp o, png_voidp x)
+{
+        g_free (x);
 }
 
 /* Shared library entry point */
-GdkPixbuf *
-image_load (FILE *f)
+static GdkPixbuf *
+gdk_pixbuf__png_image_load (FILE *f, GError **error)
 {
+        GdkPixbuf * volatile pixbuf = NULL;
        png_structp png_ptr;
-       png_infop info_ptr, end_info;
-        gboolean failed = FALSE;
-       gint i, ctype, bpp;
+       png_infop info_ptr;
+        png_textp text_ptr;
+       gint i, ctype;
        png_uint_32 w, h;
-       png_bytepp rows;
-       guchar *pixels;
-
-       png_ptr = png_create_read_struct (PNG_LIBPNG_VER_STRING, NULL, NULL, NULL);
+       png_bytepp volatile rows = NULL;
+        gint    num_texts;
+        gchar *key;
+        gchar *value;
+        gchar *icc_profile_base64;
+        const gchar *icc_profile_title;
+        const gchar *icc_profile;
+        guint icc_profile_size;
+        guint32 retval;
+        gint compression_type;
+
+#ifdef PNG_USER_MEM_SUPPORTED
+       png_ptr = png_create_read_struct_2 (PNG_LIBPNG_VER_STRING,
+                                            error,
+                                            png_simple_error_callback,
+                                            png_simple_warning_callback,
+                                            NULL, 
+                                            png_malloc_callback, 
+                                            png_free_callback);
+#else
+       png_ptr = png_create_read_struct (PNG_LIBPNG_VER_STRING,
+                                          error,
+                                          png_simple_error_callback,
+                                          png_simple_warning_callback);
+#endif
        if (!png_ptr)
                return NULL;
 
@@ -168,63 +288,81 @@ image_load (FILE *f)
                return NULL;
        }
 
-       end_info = png_create_info_struct (png_ptr);
-       if (!end_info) {
-               png_destroy_read_struct (&png_ptr, &info_ptr, NULL);
-               return NULL;
-       }
-
        if (setjmp (png_ptr->jmpbuf)) {
-               png_destroy_read_struct (&png_ptr, &info_ptr, &end_info);
+               g_free (rows);
+
+               if (pixbuf)
+                       g_object_unref (pixbuf);
+
+               png_destroy_read_struct (&png_ptr, &info_ptr, NULL);
                return NULL;
        }
 
        png_init_io (png_ptr, f);
        png_read_info (png_ptr, info_ptr);
 
-        setup_png_transformations(png_ptr, info_ptr, &failed, &w, &h, &ctype);
-
-        if (failed) {
-                png_destroy_read_struct (&png_ptr, &info_ptr, &end_info);
+        if (!setup_png_transformations(png_ptr, info_ptr, error, &w, &h, &ctype)) {
+                png_destroy_read_struct (&png_ptr, &info_ptr, NULL);
                 return NULL;
         }
         
-       if (ctype & PNG_COLOR_MASK_ALPHA)
-               bpp = 4;
-       else
-               bpp = 3;
-
-       pixels = malloc (w * h * bpp);
-       if (!pixels) {
-               png_destroy_read_struct (&png_ptr, &info_ptr, &end_info);
+        pixbuf = gdk_pixbuf_new (GDK_COLORSPACE_RGB, ctype & PNG_COLOR_MASK_ALPHA, 8, w, h);
+
+       if (!pixbuf) {
+                if (error && *error == NULL) {
+                        g_set_error_literal (error,
+                                             GDK_PIXBUF_ERROR,
+                                             GDK_PIXBUF_ERROR_INSUFFICIENT_MEMORY,
+                                             _("Insufficient memory to load PNG file"));
+                }
+                
+
+               png_destroy_read_struct (&png_ptr, &info_ptr, NULL);
                return NULL;
        }
 
        rows = g_new (png_bytep, h);
 
        for (i = 0; i < h; i++)
-               rows[i] = pixels + i * w * bpp;
+               rows[i] = pixbuf->pixels + i * pixbuf->rowstride;
 
        png_read_image (png_ptr, rows);
-       png_destroy_read_struct (&png_ptr, &info_ptr, &end_info);
+        png_read_end (png_ptr, info_ptr);
+
+        if (png_get_text (png_ptr, info_ptr, &text_ptr, &num_texts)) {
+                for (i = 0; i < num_texts; i++) {
+                        png_text_to_pixbuf_option (text_ptr[i], &key, &value);
+                        gdk_pixbuf_set_option (pixbuf, key, value);
+                        g_free (key);
+                        g_free (value);
+                }
+        }
+
+#if defined(PNG_cHRM_SUPPORTED)
+        /* Extract embedded ICC profile */
+        retval = png_get_iCCP (png_ptr, info_ptr,
+                               (png_charpp) &icc_profile_title, &compression_type,
+                               (png_charpp) &icc_profile, (png_uint_32*) &icc_profile_size);
+        if (retval != 0) {
+                icc_profile_base64 = g_base64_encode ((const guchar *) icc_profile, icc_profile_size);
+                gdk_pixbuf_set_option (pixbuf, "icc-profile", icc_profile_base64);
+                g_free (icc_profile_base64);
+        }
+#endif
+
        g_free (rows);
+       png_destroy_read_struct (&png_ptr, &info_ptr, NULL);
 
-       if (ctype & PNG_COLOR_MASK_ALPHA)
-               return gdk_pixbuf_new_from_data (pixels, ART_PIX_RGB, TRUE,
-                                                w, h, w * 4,
-                                                free_buffer, NULL);
-       else
-               return gdk_pixbuf_new_from_data (pixels, ART_PIX_RGB, FALSE,
-                                                w, h, w * 3,
-                                                free_buffer, NULL);
+        return pixbuf;
 }
 
-/* These avoid the setjmp()/longjmp() crap in libpng */
+/* I wish these avoided the setjmp()/longjmp() crap in libpng instead
+   just allow you to change the error reporting. */
 static void png_error_callback  (png_structp png_read_ptr,
                                  png_const_charp error_msg);
 
-static void png_warning_callback(png_structp png_read_ptr,
-                                 png_const_charp warning_msg);
+static void png_warning_callback (png_structp png_read_ptr,
+                                  png_const_charp warning_msg);
 
 /* Called at the start of the progressive load */
 static void png_info_callback   (png_structp png_read_ptr,
@@ -247,19 +385,40 @@ struct _LoadContext {
         png_structp png_read_ptr;
         png_infop   png_info_ptr;
 
-        ModulePreparedNotifyFunc notify_func;
+        GdkPixbufModuleSizeFunc size_func;
+        GdkPixbufModulePreparedFunc prepare_func;
+        GdkPixbufModuleUpdatedFunc update_func;
         gpointer notify_user_data;
 
         GdkPixbuf* pixbuf;
+
+        /* row number of first row seen, or -1 if none yet seen */
+
+        gint first_row_seen_in_chunk;
+
+        /* pass number for the first row seen */
+
+        gint first_pass_seen_in_chunk;
+        
+        /* row number of last row seen */
+        gint last_row_seen_in_chunk;
+
+        gint last_pass_seen_in_chunk;
+
+        /* highest row number seen */
+        gint max_row_seen_in_chunk;
         
         guint fatal_error_occurred : 1;
 
+        GError **error;
 };
 
-gpointer
-image_begin_load (ModulePreparedNotifyFunc prepare_func,
-                 ModuleUpdatedNotifyFunc update_func,
-                 gpointer user_data)
+static gpointer
+gdk_pixbuf__png_image_begin_load (GdkPixbufModuleSizeFunc size_func,
+                                  GdkPixbufModulePreparedFunc prepare_func,
+                                 GdkPixbufModuleUpdatedFunc update_func,
+                                 gpointer user_data,
+                                  GError **error)
 {
         LoadContext* lc;
         
@@ -267,20 +426,47 @@ image_begin_load (ModulePreparedNotifyFunc prepare_func,
         
         lc->fatal_error_occurred = FALSE;
 
-        lc->notify_func = prepare_func;
+        lc->size_func = size_func;
+        lc->prepare_func = prepare_func;
+        lc->update_func = update_func;
         lc->notify_user_data = user_data;
 
+        lc->first_row_seen_in_chunk = -1;
+        lc->last_row_seen_in_chunk = -1;
+        lc->first_pass_seen_in_chunk = -1;
+        lc->last_pass_seen_in_chunk = -1;
+        lc->max_row_seen_in_chunk = -1;
+        lc->error = error;
+        
         /* Create the main PNG context struct */
 
+#ifdef PNG_USER_MEM_SUPPORTED
+        lc->png_read_ptr = png_create_read_struct_2 (PNG_LIBPNG_VER_STRING,
+                                                     lc, /* error/warning callback data */
+                                                     png_error_callback,
+                                                     png_warning_callback,
+                                                     NULL,
+                                                     png_malloc_callback,
+                                                     png_free_callback);
+#else
         lc->png_read_ptr = png_create_read_struct(PNG_LIBPNG_VER_STRING,
                                                   lc, /* error/warning callback data */
                                                   png_error_callback,
                                                   png_warning_callback);
-
+#endif
         if (lc->png_read_ptr == NULL) {
                 g_free(lc);
+                /* error callback should have set the error */
                 return NULL;
         }
+        
+       if (setjmp (lc->png_read_ptr->jmpbuf)) {
+               if (lc->png_info_ptr)
+                       png_destroy_read_struct(&lc->png_read_ptr, NULL, NULL);
+                g_free(lc);
+                /* error callback should have set the error */
+                return NULL;
+       }
 
         /* Create the auxiliary context struct */
 
@@ -289,6 +475,7 @@ image_begin_load (ModulePreparedNotifyFunc prepare_func,
         if (lc->png_info_ptr == NULL) {
                 png_destroy_read_struct(&lc->png_read_ptr, NULL, NULL);
                 g_free(lc);
+                /* error callback should have set the error */
                 return NULL;
         }
 
@@ -299,36 +486,111 @@ image_begin_load (ModulePreparedNotifyFunc prepare_func,
                                     png_end_callback);
         
 
+        /* We don't want to keep modifying error after returning here,
+         * it may no longer be valid.
+         */
+        lc->error = NULL;
+        
         return lc;
 }
 
-void
-image_stop_load (gpointer context)
+static gboolean
+gdk_pixbuf__png_image_stop_load (gpointer context, GError **error)
 {
         LoadContext* lc = context;
 
-        g_return_if_fail(lc != NULL);
+        g_return_val_if_fail(lc != NULL, TRUE);
 
-        gdk_pixbuf_unref(lc->pixbuf);
+        /* FIXME this thing needs to report errors if
+         * we have unused image data
+         */
         
-        png_destroy_read_struct(&lc->png_read_ptr, NULL, NULL);
+        if (lc->pixbuf)
+                g_object_unref (lc->pixbuf);
+        
+        png_destroy_read_struct(&lc->png_read_ptr, &lc->png_info_ptr, NULL);
         g_free(lc);
+
+        return TRUE;
 }
 
-gboolean
-image_load_increment(gpointer context, guchar *buf, guint size)
+static gboolean
+gdk_pixbuf__png_image_load_increment(gpointer context,
+                                     const guchar *buf, guint size,
+                                     GError **error)
 {
         LoadContext* lc = context;
 
         g_return_val_if_fail(lc != NULL, FALSE);
 
+        /* reset */
+        lc->first_row_seen_in_chunk = -1;
+        lc->last_row_seen_in_chunk = -1;
+        lc->first_pass_seen_in_chunk = -1;
+        lc->last_pass_seen_in_chunk = -1;
+        lc->max_row_seen_in_chunk = -1;
+        lc->error = error;
+        
         /* Invokes our callbacks as needed */
-        png_process_data(lc->png_read_ptr, lc->png_info_ptr, buf, size);
+       if (setjmp (lc->png_read_ptr->jmpbuf)) {
+                lc->error = NULL;
+               return FALSE;
+       } else {
+               png_process_data(lc->png_read_ptr, lc->png_info_ptr,
+                                 (guchar*) buf, size);
+       }
 
-        if (lc->fatal_error_occurred)
+        if (lc->fatal_error_occurred) {
+                lc->error = NULL;
                 return FALSE;
-        else
+        } else {
+                if (lc->first_row_seen_in_chunk >= 0 && lc->update_func) {
+                        /* We saw at least one row */
+                        gint pass_diff = lc->last_pass_seen_in_chunk - lc->first_pass_seen_in_chunk;
+                        
+                        g_assert(pass_diff >= 0);
+                        
+                        if (pass_diff == 0) {
+                                /* start and end row were in the same pass */
+                                (lc->update_func)(lc->pixbuf, 0,
+                                                  lc->first_row_seen_in_chunk,
+                                                  lc->pixbuf->width,
+                                                  (lc->last_row_seen_in_chunk -
+                                                   lc->first_row_seen_in_chunk) + 1,
+                                                 lc->notify_user_data);
+                        } else if (pass_diff == 1) {
+                                /* We have from the first row seen to
+                                   the end of the image (max row
+                                   seen), then from the top of the
+                                   image to the last row seen */
+                                /* first row to end */
+                                (lc->update_func)(lc->pixbuf, 0,
+                                                  lc->first_row_seen_in_chunk,
+                                                  lc->pixbuf->width,
+                                                  (lc->max_row_seen_in_chunk -
+                                                   lc->first_row_seen_in_chunk) + 1,
+                                                 lc->notify_user_data);
+                                /* top to last row */
+                                (lc->update_func)(lc->pixbuf,
+                                                  0, 0, 
+                                                  lc->pixbuf->width,
+                                                  lc->last_row_seen_in_chunk + 1,
+                                                 lc->notify_user_data);
+                        } else {
+                                /* We made at least one entire pass, so update the
+                                   whole image */
+                                (lc->update_func)(lc->pixbuf,
+                                                  0, 0, 
+                                                  lc->pixbuf->width,
+                                                  lc->max_row_seen_in_chunk + 1,
+                                                 lc->notify_user_data);
+                        }
+                }
+
+                lc->error = NULL;
+                
                 return TRUE;
+        }
 }
 
 /* Called at the start of the progressive load, once we have image info */
@@ -338,22 +600,26 @@ png_info_callback   (png_structp png_read_ptr,
 {
         LoadContext* lc;
         png_uint_32 width, height;
+        png_textp png_text_ptr;
+        int i, num_texts;
         int color_type;
         gboolean have_alpha = FALSE;
-        gboolean failed = FALSE;
-        
+        gchar *icc_profile_base64;
+        const gchar *icc_profile_title;
+        const gchar *icc_profile;
+        guint icc_profile_size;
+        guint32 retval;
+        gint compression_type;
+
         lc = png_get_progressive_ptr(png_read_ptr);
 
         if (lc->fatal_error_occurred)
                 return;
-        
-
-        setup_png_transformations(lc->png_read_ptr,
-                                  lc->png_info_ptr,
-                                  &failed,
-                                  &width, &height, &color_type);
 
-        if (failed) {
+        if (!setup_png_transformations(lc->png_read_ptr,
+                                       lc->png_info_ptr,
+                                       lc->error,
+                                       &width, &height, &color_type)) {
                 lc->fatal_error_occurred = TRUE;
                 return;
         }
@@ -362,19 +628,70 @@ png_info_callback   (png_structp png_read_ptr,
         if (color_type & PNG_COLOR_MASK_ALPHA)
                 have_alpha = TRUE;
         
-        lc->pixbuf = gdk_pixbuf_new(ART_PIX_RGB, have_alpha, 8, width, height);
+        if (lc->size_func) {
+                gint w = width;
+                gint h = height;
+                (* lc->size_func) (&w, &h, lc->notify_user_data);
+                
+                if (w == 0 || h == 0) {
+                        lc->fatal_error_occurred = TRUE;
+                        if (lc->error && *lc->error == NULL) {
+                                g_set_error_literal (lc->error,
+                                                     GDK_PIXBUF_ERROR,
+                                                     GDK_PIXBUF_ERROR_FAILED,
+                                                     _("Transformed PNG has zero width or height."));
+                        }
+                        return;
+                }
+        }
+
+        lc->pixbuf = gdk_pixbuf_new (GDK_COLORSPACE_RGB, have_alpha, 8, width, height);
 
         if (lc->pixbuf == NULL) {
                 /* Failed to allocate memory */
                 lc->fatal_error_occurred = TRUE;
+                if (lc->error && *lc->error == NULL) {
+                        g_set_error (lc->error,
+                                     GDK_PIXBUF_ERROR,
+                                     GDK_PIXBUF_ERROR_INSUFFICIENT_MEMORY,
+                                     _("Insufficient memory to store a %ld by %ld image; try exiting some applications to reduce memory usage"),
+                                     width, height);
+                }
                 return;
         }
+
+        /* Extract text chunks and attach them as pixbuf options */
         
+        if (png_get_text (png_read_ptr, png_info_ptr, &png_text_ptr, &num_texts)) {
+                for (i = 0; i < num_texts; i++) {
+                        gchar *key, *value;
+
+                        if (png_text_to_pixbuf_option (png_text_ptr[i],
+                                                       &key, &value)) {
+                                gdk_pixbuf_set_option (lc->pixbuf, key, value);
+                                g_free (key);
+                                g_free (value);
+                        }
+                }
+        }
+
+#if defined(PNG_cHRM_SUPPORTED)
+        /* Extract embedded ICC profile */
+        retval = png_get_iCCP (png_read_ptr, png_info_ptr,
+                               (png_charpp) &icc_profile_title, &compression_type,
+                               (png_charpp) &icc_profile, (png_uint_32*) &icc_profile_size);
+        if (retval != 0) {
+                icc_profile_base64 = g_base64_encode ((const guchar *) icc_profile, icc_profile_size);
+                gdk_pixbuf_set_option (lc->pixbuf, "icc-profile", icc_profile_base64);
+                g_free (icc_profile_base64);
+        }
+#endif
+
         /* Notify the client that we are ready to go */
 
-        if (lc->notify_func)
-                (* lc->notify_func) (lc->pixbuf, lc->notify_user_data);
-        
+        if (lc->prepare_func)
+                (* lc->prepare_func) (lc->pixbuf, NULL, lc->notify_user_data);
+
         return;
 }
 
@@ -388,13 +705,33 @@ png_row_callback   (png_structp png_read_ptr,
 {
         LoadContext* lc;
         guchar* old_row = NULL;
-        
+
         lc = png_get_progressive_ptr(png_read_ptr);
 
         if (lc->fatal_error_occurred)
                 return;
-                
-        old_row = lc->pixbuf->art_pixbuf->pixels + (row_num * lc->pixbuf->art_pixbuf->rowstride);
+
+        if (row_num >= lc->pixbuf->height) {
+                lc->fatal_error_occurred = TRUE;
+                if (lc->error && *lc->error == NULL) {
+                        g_set_error_literal (lc->error,
+                                             GDK_PIXBUF_ERROR,
+                                             GDK_PIXBUF_ERROR_CORRUPT_IMAGE,
+                                             _("Fatal error reading PNG image file"));
+                }
+                return;
+        }
+
+        if (lc->first_row_seen_in_chunk < 0) {
+                lc->first_row_seen_in_chunk = row_num;
+                lc->first_pass_seen_in_chunk = pass_num;
+        }
+
+        lc->max_row_seen_in_chunk = MAX(lc->max_row_seen_in_chunk, ((gint)row_num));
+        lc->last_row_seen_in_chunk = row_num;
+        lc->last_pass_seen_in_chunk = pass_num;
+        
+        old_row = lc->pixbuf->pixels + (row_num * lc->pixbuf->rowstride);
 
         png_progressive_combine_row(lc->png_read_ptr, old_row, new_row);
 }
@@ -410,11 +747,8 @@ png_end_callback   (png_structp png_read_ptr,
 
         if (lc->fatal_error_occurred)
                 return;
-
-        /* Doesn't do anything for now */
 }
 
-
 static void
 png_error_callback(png_structp png_read_ptr,
                    png_const_charp error_msg)
@@ -424,18 +758,360 @@ png_error_callback(png_structp png_read_ptr,
         lc = png_get_error_ptr(png_read_ptr);
         
         lc->fatal_error_occurred = TRUE;
-        
-        fprintf(stderr, "Fatal error loading PNG: %s\n", error_msg);
+
+        /* I don't trust libpng to call the error callback only once,
+         * so check for already-set error
+         */
+        if (lc->error && *lc->error == NULL) {
+                g_set_error (lc->error,
+                             GDK_PIXBUF_ERROR,
+                             GDK_PIXBUF_ERROR_CORRUPT_IMAGE,
+                             _("Fatal error reading PNG image file: %s"),
+                             error_msg);
+        }
+
+        longjmp (png_read_ptr->jmpbuf, 1);
 }
 
 static void
-png_warning_callback(png_structp png_read_ptr,
-                     png_const_charp warning_msg)
+png_warning_callback (png_structp png_read_ptr,
+                      png_const_charp warning_msg)
 {
         LoadContext* lc;
         
         lc = png_get_error_ptr(png_read_ptr);
-        
-        fprintf(stderr, "Warning loading PNG: %s\n", warning_msg);
+
+        /* Don't print anything; we should not be dumping junk to
+         * stderr, since that may be bad for some apps. If it's
+         * important enough to display, we need to add a GError
+         * **warning return location wherever we have an error return
+         * location.
+         */
 }
 
+
+/* Save */
+
+typedef struct {
+        GdkPixbufSaveFunc save_func;
+        gpointer user_data;
+        GError **error;
+} SaveToFunctionIoPtr;
+
+static void
+png_save_to_callback_write_func (png_structp png_ptr,
+                                 png_bytep   data,
+                                 png_size_t  length)
+{
+        SaveToFunctionIoPtr *ioptr = png_get_io_ptr (png_ptr);
+
+        if (!ioptr->save_func ((gchar *)data, length, ioptr->error, ioptr->user_data)) {
+                /* If save_func has already set an error, which it
+                   should have done, this won't overwrite it. */
+                png_error (png_ptr, "write function failed");
+        }
+}
+
+static void
+png_save_to_callback_flush_func (png_structp png_ptr)
+{
+        ;
+}
+
+static gboolean real_save_png (GdkPixbuf        *pixbuf, 
+                               gchar           **keys,
+                               gchar           **values,
+                               GError          **error,
+                               gboolean          to_callback,
+                               FILE             *f,
+                               GdkPixbufSaveFunc save_func,
+                               gpointer          user_data)
+{
+       png_structp png_ptr = NULL;
+       png_infop info_ptr;
+       png_textp text_ptr = NULL;
+       guchar *ptr;
+       guchar *pixels;
+       int y;
+       int i;
+       png_bytep row_ptr;
+       png_color_8 sig_bit;
+       int w, h, rowstride;
+       int has_alpha;
+       int bpc;
+       int num_keys;
+       int compression = -1;
+       gboolean success = TRUE;
+       guchar *icc_profile = NULL;
+       gsize icc_profile_size = 0;
+       SaveToFunctionIoPtr to_callback_ioptr;
+
+       num_keys = 0;
+
+       if (keys && *keys) {
+               gchar **kiter = keys;
+               gchar **viter = values;
+
+               while (*kiter) {
+                       if (strncmp (*kiter, "tEXt::", 6) == 0) {
+                               gchar  *key = *kiter + 6;
+                               int     len = strlen (key);
+                               if (len <= 1 || len > 79) {
+                                       g_set_error_literal (error,
+                                                            GDK_PIXBUF_ERROR,
+                                                            GDK_PIXBUF_ERROR_BAD_OPTION,
+                                                            _("Keys for PNG text chunks must have at least 1 and at most 79 characters."));
+                                       success = FALSE;
+                                       goto cleanup;
+                               }
+                               for (i = 0; i < len; i++) {
+                                       if ((guchar) key[i] > 127) {
+                                               g_set_error_literal (error,
+                                                                    GDK_PIXBUF_ERROR,
+                                                                    GDK_PIXBUF_ERROR_BAD_OPTION,
+                                                                    _("Keys for PNG text chunks must be ASCII characters."));
+                                               success = FALSE;
+                                               goto cleanup;
+                                       }
+                               }
+                               num_keys++;
+                       } else if (strcmp (*kiter, "icc-profile") == 0) {
+                               /* decode from base64 */
+                               icc_profile = g_base64_decode (*viter, &icc_profile_size);
+                               if (icc_profile_size < 127) {
+                                       /* This is a user-visible error */
+                                       g_set_error (error,
+                                                    GDK_PIXBUF_ERROR,
+                                                    GDK_PIXBUF_ERROR_BAD_OPTION,
+                                                    _("Color profile has invalid length '%d'."),
+                                                    icc_profile_size);
+                                       success = FALSE;
+                                       goto cleanup;
+                               }
+                       } else if (strcmp (*kiter, "compression") == 0) {
+                               char *endptr = NULL;
+                               compression = strtol (*viter, &endptr, 10);
+
+                               if (endptr == *viter) {
+                                       g_set_error (error,
+                                                    GDK_PIXBUF_ERROR,
+                                                    GDK_PIXBUF_ERROR_BAD_OPTION,
+                                                    _("PNG compression level must be a value between 0 and 9; value '%s' could not be parsed."),
+                                                    *viter);
+                                       success = FALSE;
+                                       goto cleanup;
+                               }
+                               if (compression < 0 || compression > 9) {
+                                       /* This is a user-visible error;
+                                        * lets people skip the range-checking
+                                        * in their app.
+                                        */
+                                       g_set_error (error,
+                                                    GDK_PIXBUF_ERROR,
+                                                    GDK_PIXBUF_ERROR_BAD_OPTION,
+                                                    _("PNG compression level must be a value between 0 and 9; value '%d' is not allowed."),
+                                                    compression);
+                                       success = FALSE;
+                                       goto cleanup;
+                               }
+                       } else {
+                               g_warning ("Unrecognized parameter (%s) passed to PNG saver.", *kiter);
+                       }
+
+                       ++kiter;
+                       ++viter;
+               }
+       }
+
+       if (num_keys > 0) {
+               text_ptr = g_new0 (png_text, num_keys);
+               for (i = 0; i < num_keys; i++) {
+                       text_ptr[i].compression = PNG_TEXT_COMPRESSION_NONE;
+                       text_ptr[i].key  = keys[i] + 6;
+                       text_ptr[i].text = g_convert (values[i], -1, 
+                                                     "ISO-8859-1", "UTF-8", 
+                                                     NULL, &text_ptr[i].text_length, 
+                                                     NULL);
+
+#ifdef PNG_iTXt_SUPPORTED 
+                       if (!text_ptr[i].text) {
+                               text_ptr[i].compression = PNG_ITXT_COMPRESSION_NONE;
+                               text_ptr[i].text = g_strdup (values[i]);
+                               text_ptr[i].text_length = 0;
+                               text_ptr[i].itxt_length = strlen (text_ptr[i].text);
+                               text_ptr[i].lang = NULL;
+                               text_ptr[i].lang_key = NULL;
+                       }
+#endif
+
+                       if (!text_ptr[i].text) {
+                               g_set_error (error,
+                                            GDK_PIXBUF_ERROR,
+                                            GDK_PIXBUF_ERROR_BAD_OPTION,
+                                            _("Value for PNG text chunk %s cannot be converted to ISO-8859-1 encoding."), keys[i] + 6);
+                               num_keys = i;
+                               for (i = 0; i < num_keys; i++)
+                                       g_free (text_ptr[i].text);
+                               g_free (text_ptr);
+                               return FALSE;
+                       }
+               }
+       }
+
+       bpc = gdk_pixbuf_get_bits_per_sample (pixbuf);
+       w = gdk_pixbuf_get_width (pixbuf);
+       h = gdk_pixbuf_get_height (pixbuf);
+       rowstride = gdk_pixbuf_get_rowstride (pixbuf);
+       has_alpha = gdk_pixbuf_get_has_alpha (pixbuf);
+       pixels = gdk_pixbuf_get_pixels (pixbuf);
+
+       png_ptr = png_create_write_struct (PNG_LIBPNG_VER_STRING,
+                                          error,
+                                          png_simple_error_callback,
+                                          png_simple_warning_callback);
+       if (png_ptr == NULL) {
+              success = FALSE;
+              goto cleanup;
+       }
+
+       info_ptr = png_create_info_struct (png_ptr);
+       if (info_ptr == NULL) {
+              success = FALSE;
+              goto cleanup;
+       }
+       if (setjmp (png_ptr->jmpbuf)) {
+              success = FALSE;
+              goto cleanup;
+       }
+
+       if (num_keys > 0) {
+               png_set_text (png_ptr, info_ptr, text_ptr, num_keys);
+       }
+
+       if (to_callback) {
+               to_callback_ioptr.save_func = save_func;
+               to_callback_ioptr.user_data = user_data;
+               to_callback_ioptr.error = error;
+               png_set_write_fn (png_ptr, &to_callback_ioptr,
+                                 png_save_to_callback_write_func,
+                                 png_save_to_callback_flush_func);
+       } else {
+               png_init_io (png_ptr, f);
+       }
+
+       if (compression >= 0)
+               png_set_compression_level (png_ptr, compression);
+
+#if defined(PNG_iCCP_SUPPORTED)
+        /* the proper ICC profile title is encoded in the profile */
+        if (icc_profile != NULL) {
+                png_set_iCCP (png_ptr, info_ptr,
+                              "ICC profile", PNG_COMPRESSION_TYPE_BASE,
+                              (gchar*) icc_profile, icc_profile_size);
+        }
+#endif
+
+       if (has_alpha) {
+               png_set_IHDR (png_ptr, info_ptr, w, h, bpc,
+                             PNG_COLOR_TYPE_RGB_ALPHA, PNG_INTERLACE_NONE,
+                             PNG_COMPRESSION_TYPE_BASE, PNG_FILTER_TYPE_BASE);
+       } else {
+               png_set_IHDR (png_ptr, info_ptr, w, h, bpc,
+                             PNG_COLOR_TYPE_RGB, PNG_INTERLACE_NONE,
+                             PNG_COMPRESSION_TYPE_BASE, PNG_FILTER_TYPE_BASE);
+       }
+       sig_bit.red = bpc;
+       sig_bit.green = bpc;
+       sig_bit.blue = bpc;
+       sig_bit.alpha = bpc;
+       png_set_sBIT (png_ptr, info_ptr, &sig_bit);
+       png_write_info (png_ptr, info_ptr);
+       png_set_shift (png_ptr, &sig_bit);
+       png_set_packing (png_ptr);
+
+       ptr = pixels;
+       for (y = 0; y < h; y++) {
+               row_ptr = (png_bytep)ptr;
+               png_write_rows (png_ptr, &row_ptr, 1);
+               ptr += rowstride;
+       }
+
+       png_write_end (png_ptr, info_ptr);
+
+cleanup:
+        if (png_ptr != NULL)
+                png_destroy_write_struct (&png_ptr, &info_ptr);
+
+        g_free (icc_profile);
+
+        if (text_ptr != NULL) {
+                for (i = 0; i < num_keys; i++)
+                        g_free (text_ptr[i].text);
+                g_free (text_ptr);
+        }
+
+       return success;
+}
+
+static gboolean
+gdk_pixbuf__png_image_save (FILE          *f, 
+                            GdkPixbuf     *pixbuf, 
+                            gchar        **keys,
+                            gchar        **values,
+                            GError       **error)
+{
+        return real_save_png (pixbuf, keys, values, error,
+                              FALSE, f, NULL, NULL);
+}
+
+static gboolean
+gdk_pixbuf__png_image_save_to_callback (GdkPixbufSaveFunc   save_func,
+                                        gpointer            user_data,
+                                        GdkPixbuf          *pixbuf, 
+                                        gchar             **keys,
+                                        gchar             **values,
+                                        GError            **error)
+{
+        return real_save_png (pixbuf, keys, values, error,
+                              TRUE, NULL, save_func, user_data);
+}
+
+#ifndef INCLUDE_png
+#define MODULE_ENTRY(function) G_MODULE_EXPORT void function
+#else
+#define MODULE_ENTRY(function) void _gdk_pixbuf__png_ ## function
+#endif
+
+MODULE_ENTRY (fill_vtable) (GdkPixbufModule *module)
+{
+        module->load = gdk_pixbuf__png_image_load;
+        module->begin_load = gdk_pixbuf__png_image_begin_load;
+        module->stop_load = gdk_pixbuf__png_image_stop_load;
+        module->load_increment = gdk_pixbuf__png_image_load_increment;
+        module->save = gdk_pixbuf__png_image_save;
+        module->save_to_callback = gdk_pixbuf__png_image_save_to_callback;
+}
+
+MODULE_ENTRY (fill_info) (GdkPixbufFormat *info)
+{
+        static GdkPixbufModulePattern signature[] = {
+                { "\x89PNG\r\n\x1a\x0a", NULL, 100 },
+                { NULL, NULL, 0 }
+        };
+       static gchar * mime_types[] = {
+               "image/png",
+               NULL
+       };
+       static gchar * extensions[] = {
+               "png",
+               NULL
+       };
+
+       info->name = "png";
+        info->signature = signature;
+       info->description = N_("The PNG image format");
+       info->mime_types = mime_types;
+       info->extensions = extensions;
+       info->flags = GDK_PIXBUF_FORMAT_WRITABLE | GDK_PIXBUF_FORMAT_THREADSAFE;
+       info->license = "LGPL";
+}