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: dfc63d42ee/libjpeg.txt (L1425-L1438)
This commit is contained in:
Jehan 2022-04-17 20:26:41 +02:00
parent 1ccdf467fb
commit f200594d1c
3 changed files with 148 additions and 134 deletions

View File

@ -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;
}
}

View File

@ -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

View File

@ -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), "