[gimp] plug-ins: support CMYK import/export for JPEG.



commit f200594d1c8cdd88b93beda41023dba2630298a1
Author: Jehan <jehan girinstud io>
Date:   Sun Apr 17 20:26:41 2022 +0200

    plug-ins: support CMYK import/export for JPEG.
    
    We already had import support through littleCMS. We now use fully
    babl/GEGL which makes our code more straightforward and identical,
    whichever the input format.
    
    The export support is totally new. It comes with a checkbox to propose
    selecting CMYK export and a label displaying the CMYK profile which will
    be used.
    
    Now this whole implementation has a few drawbacks so far, but it will be
    a good first sample for future CMYK-related improvements to come:
    
    * The export profile I am using is what we call the "simulation
      profile" from the GimpColorConfig. This corresponds to the default
      "Soft-proofing" profile as set in Preferences. In particular, this is
      not the actual soft-proofing profile for this image which might have
      been changed through the View menu because this information is
      currently and unfortunately unavailable to plug-ins. It is not the
      "Preferred CMYK Profile" either, as set in Preferences.
      TODOS:
      - We really need to straighten the soft-proof profile core concept by
        storing it in the image and making it visible to plug-in.
      - Another interesting improvement could be to create a
        GimpColorProfile procedure argument which would be mapped to a color
        profile chooser widget, allowing people to choose profiles in
        plug-ins. For an export plug-in in particular, it could allow to
        select a profile different from the soft-proof one at export time.
    * When we export, if no profile is choosen, babl will use a naive
      profile. It would be nice to store this naive profile into the JPEG if
      the "Save color profile" option is checked (same as we store a generic
      sRGB profile when no RGB profile is set).
    * When we import, we just import the image as sRGB. Since CMYK gamuts
      are not necessarily within sRGB (some part of the spectrum is usually
      well within, but other well outside), other than the basic conversion
      accuracy issue, we may lose colors. It would be much nicer to be able
      to select an output RGB profile. Optionally if we could create a RGB
      color space which is made to contain the whole input CMYK profile
      color space, without explicit choice step, it would be nice too.
    * I am using babl's "cmyk" format, not the expected "CMYK" format.
      "cmyk" is meant to be an inverted CMYK where 0.0 is full ink coverage
      and 1.0 none. Nevertheless when loading the resulting JPEG in other
      software (editors or viewers alike), the normal CMYK would always
      display inverted colors and the inverted cmyk would look fine.
      Finally I found a docs from libjpeg-turbo library, explaining that
      Photoshop was wrongly inverting CMYK color data while it should not.
      This text dates back from 1994, looking at the commit date which
      introduced this paragraph. In the 28 years since then, could this
      color inversion have become the de-facto standard for JPEG because one
      of the main editor would just output all its JPEG files this way?
      See: 
https://github.com/libjpeg-turbo/libjpeg-turbo/blob/dfc63d42ee3d1ae8eacb921e89e64ac57861dff6/libjpeg.txt#L1425-L1438

 plug-ins/file-jpeg/jpeg-load.c | 153 +++++++++--------------------------------
 plug-ins/file-jpeg/jpeg-save.c | 123 +++++++++++++++++++++++++++++----
 plug-ins/file-jpeg/jpeg.c      |   6 ++
 3 files changed, 148 insertions(+), 134 deletions(-)
---
diff --git a/plug-ins/file-jpeg/jpeg-load.c b/plug-ins/file-jpeg/jpeg-load.c
index e7b4503122..2d227ee0cc 100644
--- a/plug-ins/file-jpeg/jpeg-load.c
+++ b/plug-ins/file-jpeg/jpeg-load.c
@@ -46,11 +46,6 @@ static gboolean  jpeg_load_resolution       (GimpImage *image,
 
 static void      jpeg_load_sanitize_comment (gchar    *comment);
 
-static gpointer  jpeg_load_cmyk_transform   (guint8   *profile_data,
-                                             gsize     profile_len);
-static void      jpeg_load_cmyk_to_rgb      (guchar   *buf,
-                                             glong     pixels,
-                                             gpointer  transform);
 
 GimpImage * volatile  preview_image;
 GimpLayer *           preview_layer;
@@ -72,12 +67,14 @@ load_image (GFile        *file,
   guchar           **rowbuf;
   GimpImageBaseType  image_type;
   GimpImageType      layer_type;
-  GeglBuffer        *buffer = NULL;
+  GeglBuffer        *buffer       = NULL;
   const Babl        *format;
-  const gchar       *layer_name = NULL;
+  const Babl        *space;
+  const gchar       *encoding;
+  const gchar       *layer_name   = NULL;
+  GimpColorProfile  *cmyk_profile = NULL;
   gint               tile_height;
   gint               i;
-  cmsHTRANSFORM      cmyk_transform = NULL;
 
   /* We set up the normal JPEG error routines. */
   cinfo.err = jpeg_std_error (&jerr.pub);
@@ -298,17 +295,20 @@ load_image (GFile        *file,
       /* Step 5.3: check for an embedded ICC profile in APP2 markers */
       jpeg_icc_read_profile (&cinfo, &icc_data, &icc_length);
 
-      if (cinfo.out_color_space == JCS_CMYK)
-        {
-          cmyk_transform = jpeg_load_cmyk_transform (icc_data, icc_length);
-        }
-      else if (icc_data) /* don't attach the profile if we are transforming */
+      if (icc_data)
         {
           GimpColorProfile *profile;
 
           profile = gimp_color_profile_new_from_icc_profile (icc_data,
                                                              icc_length,
                                                              NULL);
+          if (cinfo.out_color_space == JCS_CMYK)
+            {
+              /* don't attach the profile if we are transforming */
+              cmyk_profile = profile;
+              profile = NULL;
+            }
+
           if (profile)
             {
               gimp_image_set_color_profile (image, profile);
@@ -342,9 +342,25 @@ load_image (GFile        *file,
 
   buffer = gimp_drawable_get_buffer (GIMP_DRAWABLE (layer));
 
-  format = babl_format_with_space (image_type == GIMP_RGB ?
-                                   "R'G'B' u8" : "Y' u8",
-                                   gimp_drawable_get_format (GIMP_DRAWABLE (layer)));
+  if (cinfo.out_color_space == JCS_CMYK)
+    {
+      encoding = "cmyk u8";
+      if (cmyk_profile)
+        space = gimp_color_profile_get_space (cmyk_profile,
+                                              GIMP_COLOR_RENDERING_INTENT_RELATIVE_COLORIMETRIC,
+                                              error);
+      else
+        space = NULL;
+    }
+  else
+    {
+      if (image_type == GIMP_RGB)
+        encoding = "R'G'B' u8";
+      else
+        encoding = "Y' u8";
+      space = gimp_drawable_get_format (GIMP_DRAWABLE (layer));
+    }
+  format = babl_format_with_space (encoding, space);
 
   while (cinfo.output_scanline < cinfo.output_height)
     {
@@ -371,10 +387,6 @@ load_image (GFile        *file,
       for (i = 0; i < scanlines; i++)
         jpeg_read_scanlines (&cinfo, (JSAMPARRAY) &rowbuf[i], 1);
 
-      if (cinfo.out_color_space == JCS_CMYK)
-        jpeg_load_cmyk_to_rgb (buf, cinfo.output_width * scanlines,
-                               cmyk_transform);
-
     set_buffer:
       gegl_buffer_set (buffer,
                        GEGL_RECTANGLE (0, start, cinfo.output_width, scanlines),
@@ -403,9 +415,7 @@ load_image (GFile        *file,
 
  finish:
 
-  if (cmyk_transform)
-    cmsDeleteTransform (cmyk_transform);
-
+  g_clear_object (&cmyk_profile);
   /* Step 8: Release JPEG decompression object */
 
   /* This is an important step since it will release a good deal of memory. */
@@ -617,100 +627,3 @@ load_thumbnail_image (GFile         *file,
 
   return image;
 }
-
-static gpointer
-jpeg_load_cmyk_transform (guint8 *profile_data,
-                          gsize   profile_len)
-{
-  GimpColorConfig  *config       = gimp_get_color_configuration ();
-  GimpColorProfile *cmyk_profile = NULL;
-  GimpColorProfile *rgb_profile  = NULL;
-  cmsHPROFILE       cmyk_lcms;
-  cmsHPROFILE       rgb_lcms;
-  cmsUInt32Number   flags        = 0;
-  cmsHTRANSFORM     transform;
-
-  /*  try to load the embedded CMYK profile  */
-  if (profile_data)
-    {
-      cmyk_profile = gimp_color_profile_new_from_icc_profile (profile_data,
-                                                              profile_len,
-                                                              NULL);
-
-      if (cmyk_profile && ! gimp_color_profile_is_cmyk (cmyk_profile))
-        {
-          g_object_unref (cmyk_profile);
-          cmyk_profile = NULL;
-        }
-    }
-
-  /*  if that fails, try to load the CMYK profile configured in the prefs  */
-  if (! cmyk_profile)
-    cmyk_profile = gimp_color_config_get_cmyk_color_profile (config, NULL);
-
-  /*  bail out if we can't load any CMYK profile  */
-  if (! cmyk_profile)
-    {
-      g_object_unref (config);
-      return NULL;
-    }
-
-  /*  always convert to sRGB  */
-  rgb_profile = gimp_color_profile_new_rgb_srgb ();
-
-  cmyk_lcms = gimp_color_profile_get_lcms_profile (cmyk_profile);
-  rgb_lcms  = gimp_color_profile_get_lcms_profile (rgb_profile);
-
-  if (gimp_color_config_get_display_intent (config) ==
-      GIMP_COLOR_RENDERING_INTENT_RELATIVE_COLORIMETRIC)
-    {
-      flags |= cmsFLAGS_BLACKPOINTCOMPENSATION;
-    }
-
-  transform = cmsCreateTransform (cmyk_lcms, TYPE_CMYK_8_REV,
-                                  rgb_lcms,  TYPE_RGB_8,
-                                  gimp_color_config_get_display_intent (config),
-                                  flags);
-
-  g_object_unref (cmyk_profile);
-  g_object_unref (rgb_profile);
-
-  g_object_unref (config);
-
-  return transform;
-}
-
-
-static void
-jpeg_load_cmyk_to_rgb (guchar   *buf,
-                       glong     pixels,
-                       gpointer  transform)
-{
-  const guchar *src  = buf;
-  guchar       *dest = buf;
-
-  if (transform)
-    {
-      cmsDoTransform (transform, buf, buf, pixels);
-      return;
-    }
-
-  /* NOTE: The following code assumes inverted CMYK values, even when an
-     APP14 marker doesn't exist. This is the behavior of recent versions
-     of PhotoShop as well. */
-
-  while (pixels--)
-    {
-      guint c = src[0];
-      guint m = src[1];
-      guint y = src[2];
-      guint k = src[3];
-
-      dest[0] = (c * k) / 255;
-      dest[1] = (m * k) / 255;
-      dest[2] = (y * k) / 255;
-
-      src  += 4;
-      dest += 3;
-    }
-}
diff --git a/plug-ins/file-jpeg/jpeg-save.c b/plug-ins/file-jpeg/jpeg-save.c
index 9f2a53a514..185cf2592d 100644
--- a/plug-ins/file-jpeg/jpeg-save.c
+++ b/plug-ins/file-jpeg/jpeg-save.c
@@ -213,7 +213,9 @@ save_image (GFile                *file,
   FILE             * volatile outfile;
   guchar           *data;
   guchar           *src;
-  GimpColorProfile *profile = NULL;
+  GimpColorConfig  *color_config = gimp_get_color_configuration ();
+  GimpColorProfile *profile      = NULL;
+  GimpColorProfile *cmyk_profile = NULL;
 
   gboolean         has_alpha;
   gboolean         out_linear = FALSE;
@@ -224,6 +226,7 @@ save_image (GFile                *file,
   gdouble          smoothing;
   gboolean         optimize;
   gboolean         progressive;
+  gboolean         cmyk;
   gint             subsmp;
   gboolean         baseline;
   gint             restart;
@@ -241,6 +244,7 @@ save_image (GFile                *file,
                 "smoothing",                 &smoothing,
                 "optimize",                  &optimize,
                 "progressive",               &progressive,
+                "cmyk",                      &cmyk,
                 "sub-sampling",              &subsmp,
                 "baseline",                  &baseline,
                 "restart",                   &restart,
@@ -426,6 +430,43 @@ save_image (GFile                *file,
       return FALSE;
     }
 
+  if (cmyk)
+    {
+      if (save_profile)
+        {
+          GError *err = NULL;
+
+          cmyk_profile = gimp_color_config_get_simulation_color_profile (color_config, &err);
+          if (! cmyk_profile && err)
+            g_printerr ("%s: no soft-proof profile: %s\n", G_STRFUNC, err->message);
+
+          if (cmyk_profile && ! gimp_color_profile_is_cmyk (cmyk_profile))
+            g_clear_object (&cmyk_profile);
+
+          g_clear_error (&err);
+        }
+
+      /* As far as I know, without access to JPEG specifications, we
+       * should encode as proper "CMYK" encoding scheme. But every other
+       * program where I test this JPEG, the color are inverted, so I
+       * use the "cmyk" encoding where 0.0 is full ink coverage vs. 1.0
+       * being no ink.
+       * libjpeg-turbo says that Photoshop is wrongly inverting the data
+       * in JPEG files in a 1994 commit! We might imagine that since
+       * then it became the de-facto encoding?
+       * See: 
https://github.com/libjpeg-turbo/libjpeg-turbo/blob/dfc63d42ee3d1ae8eacb921e89e64ac57861dff6/libjpeg.txt#L1425-L1438
+       */
+      encoding = "cmyk u8";
+
+      if (cmyk_profile)
+        space = gimp_color_profile_get_space (cmyk_profile,
+                                              GIMP_COLOR_RENDERING_INTENT_RELATIVE_COLORIMETRIC,
+                                              error);
+      else
+        /* The NULL space will fallback to a naive CMYK conversion. */
+        space = NULL;
+    }
+
   format = babl_format_with_space (encoding, space);
 
   /* Step 3: set parameters for compression */
@@ -437,15 +478,27 @@ save_image (GFile                *file,
   cinfo.image_width  = gegl_buffer_get_width (buffer);
   cinfo.image_height = gegl_buffer_get_height (buffer);
   /* colorspace of input image */
-  cinfo.in_color_space = (drawable_type == GIMP_RGB_IMAGE ||
-                          drawable_type == GIMP_RGBA_IMAGE)
-    ? JCS_RGB : JCS_GRAYSCALE;
+  if (cmyk)
+    {
+      cinfo.input_components = 4;
+      cinfo.in_color_space   = JCS_CMYK;
+      cinfo.jpeg_color_space = JCS_CMYK;
+    }
+  else
+    {
+      cinfo.in_color_space = (drawable_type == GIMP_RGB_IMAGE ||
+                              drawable_type == GIMP_RGBA_IMAGE)
+        ? JCS_RGB : JCS_GRAYSCALE;
+    }
   /* Now use the library's routine to set default compression parameters.
    * (You must set at least cinfo.in_color_space before calling this,
    * since the defaults depend on the source color space.)
    */
   jpeg_set_defaults (&cinfo);
 
+  if (cmyk_profile)
+    jpeg_set_colorspace (&cinfo, JCS_CMYK);
+
   jpeg_set_quality (&cinfo, quality, baseline);
 
   if (use_orig_quality && orig_num_quant_tables > 0)
@@ -598,16 +651,23 @@ save_image (GFile                *file,
     }
 
   /* Step 4.2: store the color profile */
-  if (save_profile)
+  if (save_profile &&
+      /* XXX Only case when we don't save a profile even though the
+       * option was requested is if we store as CMYK without setting a
+       * profile. It would actually be better to generate a profile
+       * corresponding to the "naive" CMYK space we use in such case.
+       * But it doesn't look like babl can do this yet.
+       */
+      (! cmyk || cmyk_profile != NULL))
     {
       const guint8 *icc_data;
       gsize         icc_length;
 
-      icc_data = gimp_color_profile_get_icc_profile (profile, &icc_length);
+      icc_data = gimp_color_profile_get_icc_profile (cmyk_profile ? cmyk_profile : profile, &icc_length);
       jpeg_icc_write_profile (&cinfo, icc_data, icc_length);
-
-      g_object_unref (profile);
     }
+  g_clear_object (&profile);
+  g_clear_object (&cmyk_profile);
 
   /* Step 5: while (scan lines remain to be written) */
   /*           jpeg_write_scanlines(...); */
@@ -780,12 +840,15 @@ save_dialog (GimpProcedure       *procedure,
              GimpProcedureConfig *config,
              GimpDrawable        *drawable)
 {
-  GtkWidget    *dialog;
-  GtkWidget    *widget;
-  GtkListStore *store;
-  gint          orig_quality;
-  gint          restart;
-  gboolean      run;
+  GtkWidget        *dialog;
+  GtkWidget        *widget;
+  GtkWidget        *profile_label;
+  GtkListStore     *store;
+  GimpColorConfig  *color_config = gimp_get_color_configuration ();
+  GimpColorProfile *cmyk_profile = NULL;
+  gint              orig_quality;
+  gint              restart;
+  gboolean          run;
 
   g_object_get (config,
                 "original-quality", &orig_quality,
@@ -824,6 +887,37 @@ save_dialog (GimpProcedure       *procedure,
                            _("Enable preview to obtain the file size."), NULL);
 
 
+  /* Profile label. */
+  profile_label = gimp_procedure_dialog_get_label (GIMP_PROCEDURE_DIALOG (dialog),
+                                                   "profile-label", _("No soft-proofing profile"));
+  gtk_label_set_xalign (GTK_LABEL (profile_label), 0.0);
+  gtk_label_set_ellipsize (GTK_LABEL (profile_label), PANGO_ELLIPSIZE_END);
+  gimp_label_set_attributes (GTK_LABEL (profile_label),
+                             PANGO_ATTR_STYLE, PANGO_STYLE_ITALIC,
+                             -1);
+  gimp_help_set_help_data (profile_label,
+                           _("Name of the color profile used for CMYK export."), NULL);
+  gimp_procedure_dialog_fill_frame (GIMP_PROCEDURE_DIALOG (dialog),
+                                    "cmyk-frame", "cmyk", FALSE,
+                                    "profile-label");
+  cmyk_profile = gimp_color_config_get_simulation_color_profile (color_config, NULL);
+  if (cmyk_profile)
+    {
+      if (gimp_color_profile_is_cmyk (cmyk_profile))
+        {
+          gchar *label_text;
+
+          label_text = g_strdup_printf (_("Profile: %s"),
+                                        gimp_color_profile_get_label (cmyk_profile));
+          gtk_label_set_text (GTK_LABEL (profile_label), label_text);
+          gimp_label_set_attributes (GTK_LABEL (profile_label),
+                                     PANGO_ATTR_STYLE, PANGO_STYLE_NORMAL,
+                                     -1);
+          g_free (label_text);
+        }
+      g_object_unref (cmyk_profile);
+    }
+
 #ifdef C_ARITH_CODING_SUPPORTED
   gimp_procedure_dialog_fill_frame (GIMP_PROCEDURE_DIALOG (dialog),
                                     "arithmetic-frame", "use-arithmetic-coding", TRUE,
@@ -895,6 +989,7 @@ save_dialog (GimpProcedure       *procedure,
                                   "advanced-options",
                                   "smoothing",
                                   "progressive",
+                                  "cmyk-frame",
 #ifdef C_ARITH_CODING_SUPPORTED
                                   "arithmetic-frame",
 #else
diff --git a/plug-ins/file-jpeg/jpeg.c b/plug-ins/file-jpeg/jpeg.c
index 49711b56e9..8ed0adebbc 100644
--- a/plug-ins/file-jpeg/jpeg.c
+++ b/plug-ins/file-jpeg/jpeg.c
@@ -221,6 +221,12 @@ jpeg_create_procedure (GimpPlugIn  *plug_in,
                              TRUE,
                              G_PARAM_READWRITE);
 
+      GIMP_PROC_ARG_BOOLEAN (procedure, "cmyk",
+                             "Export as _CMYK",
+                             "Create a CMYK JPEG image using the soft-proofing color profile",
+                             FALSE,
+                             G_PARAM_READWRITE);
+
       GIMP_PROC_ARG_INT (procedure, "sub-sampling",
                          _("Su_bsampling"),
                          "Sub-sampling type { 0 == 4:2:0 (chroma quartered), "


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]