From f289f667e78181436c4be5323b7cbfa34b0d81fd Mon Sep 17 00:00:00 2001 From: Richard Hughes Date: Fri, 18 Dec 2009 15:17:13 +0000 Subject: [PATCH] Add color management support to gdk_pixbuf_save This patch adds an icc-profile option to a GdkPixbuf which can be used to read or write an embedded ICC profile. Add PNG support for now, but other image formats are awaiting review. --- demos/Makefile.am | 4 ++ demos/testpixbuf-color.c | 116 +++++++++++++++++++++++++++++++++++++ gdk-pixbuf/gdk-pixbuf-io.c | 15 +++++ gdk-pixbuf/io-png.c | 93 ++++++++++++++++++++++++----- 4 files changed, 215 insertions(+), 13 deletions(-) create mode 100644 demos/testpixbuf-color.c diff --git a/demos/Makefile.am b/demos/Makefile.am index c8c9bbb1c5..81e07f427a 100644 --- a/demos/Makefile.am +++ b/demos/Makefile.am @@ -24,6 +24,7 @@ LDADDS = \ noinst_PROGRAMS = \ testpixbuf-drawable \ testanimation \ + testpixbuf-color \ testpixbuf-save \ testpixbuf-scale \ pixbuf-demo @@ -52,6 +53,7 @@ test-inline-pixbufs.h: $(pixbuf_csource_deps) apple-red.png gnome-foot.png testpixbuf_DEPENDENCIES = $(DEPS) testpixbuf_drawable_DEPENDENCIES = $(DEPS) testpixbuf_save_DEPENDENCIES = $(DEPS) +testpixbuf_color_DEPENDENCIES = $(DEPS) testpixbuf_scale_DEPENDENCIES = $(DEPS) testanimation_DEPENDENCIES = $(DEPS) pixbuf_demo_DEPENDENCIES = $(DEPS) @@ -59,6 +61,7 @@ pixbuf_demo_DEPENDENCIES = $(DEPS) testpixbuf_LDADD = $(LDADDS) testpixbuf_drawable_LDADD = $(LDADDS) testpixbuf_save_LDADD = $(LDADDS) +testpixbuf_color_LDADD = $(LDADDS) testpixbuf_scale_LDADD = $(LDADDS) testanimation_LDADD = $(LDADDS) pixbuf_demo_LDADD = $(LDADDS) @@ -66,6 +69,7 @@ pixbuf_demo_LDADD = $(LDADDS) testpixbuf_SOURCES = testpixbuf.c pixbuf-init.c testpixbuf_drawable_SOURCES = testpixbuf-drawable.c pixbuf-init.c testpixbuf_save_SOURCES = testpixbuf-save.c +testpixbuf_color_SOURCES = testpixbuf-color.c testpixbuf_scale_SOURCES = testpixbuf-scale.c pixbuf-init.c testanimation_SOURCES = testanimation.c pixbuf-init.c pixbuf_demo_SOURCES = pixbuf-demo.c pixbuf-init.c diff --git a/demos/testpixbuf-color.c b/demos/testpixbuf-color.c new file mode 100644 index 0000000000..b87d850a0e --- /dev/null +++ b/demos/testpixbuf-color.c @@ -0,0 +1,116 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*- */ + +#include "config.h" +#include +#include + +#include + +#define ICC_PROFILE "/usr/share/color/icc/bluish.icc" +#define ICC_PROFILE_SIZE 3966 + +static gboolean +save_image_png (const gchar *filename, GdkPixbuf *pixbuf, GError **error) +{ + gchar *contents = NULL; + gchar *contents_encode = NULL; + gsize length; + gboolean ret; + gint len; + + /* get icc file */ + ret = g_file_get_contents (ICC_PROFILE, &contents, &length, error); + if (!ret) + goto out; + contents_encode = g_base64_encode ((const guchar *) contents, length); + ret = gdk_pixbuf_save (pixbuf, filename, "png", error, + "tEXt::Software", "Hello my name is dave", + "icc-profile", contents_encode, + NULL); + len = strlen (contents_encode); + g_debug ("ICC profile was %i bytes", len); +out: + g_free (contents); + g_free (contents_encode); + return ret; +} + +static gboolean +save_image_verify (const gchar *filename, GError **error) +{ + gboolean ret = FALSE; + GdkPixbuf *pixbuf = NULL; + const gchar *option; + gchar *icc_profile = NULL; + gsize len = 0; + + /* load */ + pixbuf = gdk_pixbuf_new_from_file (filename, error); + if (pixbuf == NULL) + goto out; + + /* check values */ + option = gdk_pixbuf_get_option (pixbuf, "icc-profile"); + if (option == NULL) { + *error = g_error_new (1, 0, "no profile set"); + goto out; + } + + /* decode base64 */ + icc_profile = (gchar *) g_base64_decode (option, &len); + if (len != ICC_PROFILE_SIZE) { + *error = g_error_new (1, 0, "profile length invalid, got %i", len); + g_file_set_contents ("error.icc", icc_profile, len, NULL); + goto out; + } + + /* success */ + ret = TRUE; +out: + if (pixbuf != NULL) + g_object_unref (pixbuf); + g_free (icc_profile); + return ret; +} + +int +main (int argc, char **argv) +{ + GdkWindow *root; + GdkPixbuf *pixbuf; + gboolean ret; + gint retval = 1; + GError *error = NULL; + + gtk_init (&argc, &argv); + + gtk_widget_set_default_colormap (gdk_rgb_get_colormap ()); + + root = gdk_get_default_root_window (); + pixbuf = gdk_pixbuf_get_from_drawable (NULL, root, NULL, + 0, 0, 0, 0, 150, 160); + + /* PASS */ + g_debug ("try to save PNG with a profile"); + ret = save_image_png ("icc-profile.png", pixbuf, &error); + if (!ret) { + g_warning ("FAILED: did not save image: %s", error->message); + g_error_free (error); + goto out; + } + + /* PASS */ + g_debug ("try to load PNG and get color attributes"); + ret = save_image_verify ("icc-profile.png", &error); + if (!ret) { + g_warning ("FAILED: did not load image: %s", error->message); + g_error_free (error); + goto out; + } + + /* success */ + retval = 0; + g_debug ("ALL OKAY!"); +out: + return retval; +} diff --git a/gdk-pixbuf/gdk-pixbuf-io.c b/gdk-pixbuf/gdk-pixbuf-io.c index f8ce73d9ab..ed765ed407 100644 --- a/gdk-pixbuf/gdk-pixbuf-io.c +++ b/gdk-pixbuf/gdk-pixbuf-io.c @@ -1902,6 +1902,21 @@ gdk_pixbuf_real_save_to_callback (GdkPixbuf *pixbuf, * be specified using the "compression" parameter; it's value is in an * integer in the range of [0,9]. * + * ICC color profiles can also be embedded into PNG images. + * The "icc-profile" value should be the complete ICC profile encoded + * into base64. + * + * + * gchar *contents; + * gchar *contents_encode; + * gsize length; + * g_file_get_contents ("/home/hughsie/.color/icc/L225W.icm", &contents, &length, NULL); + * contents_encode = g_base64_encode ((const guchar *) contents, length); + * gdk_pixbuf_save (pixbuf, handle, "png", &error, + * "icc-profile", contents_encode, + * NULL); + * + * * TIFF images recognize a "compression" option which acceps an integer value. * Among the codecs are 1 None, 2 Huffman, 5 LZW, 7 JPEG and 8 Deflate, see * the libtiff documentation and tiff.h for all supported codec values. diff --git a/gdk-pixbuf/io-png.c b/gdk-pixbuf/io-png.c index eecdd0fb42..8b90865855 100644 --- a/gdk-pixbuf/io-png.c +++ b/gdk-pixbuf/io-png.c @@ -258,6 +258,12 @@ gdk_pixbuf__png_image_load (FILE *f, GError **error) 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, @@ -332,6 +338,18 @@ gdk_pixbuf__png_image_load (FILE *f, GError **error) } } +#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); @@ -586,7 +604,13 @@ png_info_callback (png_structp png_read_ptr, int i, num_texts; int color_type; gboolean have_alpha = 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) @@ -651,11 +675,23 @@ png_info_callback (png_structp png_read_ptr, } } +#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->prepare_func) (* lc->prepare_func) (lc->pixbuf, NULL, lc->notify_user_data); - + return; } @@ -791,7 +827,7 @@ static gboolean real_save_png (GdkPixbuf *pixbuf, GdkPixbufSaveFunc save_func, gpointer user_data) { - png_structp png_ptr; + png_structp png_ptr = NULL; png_infop info_ptr; png_textp text_ptr = NULL; guchar *ptr; @@ -806,6 +842,8 @@ static gboolean real_save_png (GdkPixbuf *pixbuf, 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; @@ -823,7 +861,8 @@ static gboolean real_save_png (GdkPixbuf *pixbuf, GDK_PIXBUF_ERROR, GDK_PIXBUF_ERROR_BAD_OPTION, _("Keys for PNG text chunks must have at least 1 and at most 79 characters.")); - return FALSE; + success = FALSE; + goto cleanup; } for (i = 0; i < len; i++) { if ((guchar) key[i] > 127) { @@ -831,10 +870,24 @@ static gboolean real_save_png (GdkPixbuf *pixbuf, GDK_PIXBUF_ERROR, GDK_PIXBUF_ERROR_BAD_OPTION, _("Keys for PNG text chunks must be ASCII characters.")); - return FALSE; + 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); @@ -845,7 +898,8 @@ static gboolean real_save_png (GdkPixbuf *pixbuf, GDK_PIXBUF_ERROR_BAD_OPTION, _("PNG compression level must be a value between 0 and 9; value '%s' could not be parsed."), *viter); - return FALSE; + success = FALSE; + goto cleanup; } if (compression < 0 || compression > 9) { /* This is a user-visible error; @@ -857,7 +911,8 @@ static gboolean real_save_png (GdkPixbuf *pixbuf, GDK_PIXBUF_ERROR_BAD_OPTION, _("PNG compression level must be a value between 0 and 9; value '%d' is not allowed."), compression); - return FALSE; + success = FALSE; + goto cleanup; } } else { g_warning ("Unrecognized parameter (%s) passed to PNG saver.", *kiter); @@ -947,6 +1002,15 @@ static gboolean real_save_png (GdkPixbuf *pixbuf, 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, @@ -975,13 +1039,16 @@ static gboolean real_save_png (GdkPixbuf *pixbuf, png_write_end (png_ptr, info_ptr); cleanup: - png_destroy_write_struct (&png_ptr, &info_ptr); + if (png_ptr != NULL) + png_destroy_write_struct (&png_ptr, &info_ptr); - if (num_keys > 0) { - for (i = 0; i < num_keys; i++) - g_free (text_ptr[i].text); - g_free (text_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; }