plug-in: Initial support for CMYK/A JPEGXL export

Adds option to export JPEG XL images in CMYK/A format.
Key and Alpha data is saved in extra channels, and the
simulation profile is saved as well.
Per the specification developers, the format does not
support 'naive' CMYK conversion, so a profile is required
for export. The option will be disabled if not set.
This commit is contained in:
Alx Sa 2022-12-10 10:04:31 -05:00
parent b04f45f354
commit d1746e464c
1 changed files with 420 additions and 77 deletions

View File

@ -81,6 +81,16 @@ static void create_cmyk_layer (GimpImage *image,
gpointer key_buffer,
gint bit_depth,
gboolean has_alpha);
static void extract_cmyk (GeglBuffer *buffer,
gpointer *cmy_data,
gpointer *key_data,
gpointer *alpha_data,
const Babl *type,
const Babl *space,
gint width,
gint height,
gint bit_depth,
gboolean has_alpha);
G_DEFINE_TYPE (JpegXL, jpegxl, GIMP_TYPE_PLUG_IN)
@ -1158,6 +1168,158 @@ jpegxl_load (GimpProcedure *procedure,
return return_vals;
}
static void
extract_cmyk (GeglBuffer *buffer,
gpointer *cmy_data,
gpointer *key_data,
gpointer *alpha_data,
const Babl *type,
const Babl *space,
gint width,
gint height,
gint bit_depth,
gboolean has_alpha)
{
GeglBuffer *cmy_buffer;
GeglBuffer *key_buffer;
GeglBuffer *alpha_buffer = NULL;
const Babl *format;
const Babl *key_format = NULL;
GeglBufferIterator *iter;
GeglColor *white = gegl_color_new ("white");
if (has_alpha)
{
format = babl_format_new (babl_model ("cmykA"),
type,
babl_component ("cyan"),
babl_component ("magenta"),
babl_component ("yellow"),
babl_component ("key"),
babl_component ("A"),
NULL);
}
else
{
format = babl_format_new (babl_model ("cmyk"),
type,
babl_component ("cyan"),
babl_component ("magenta"),
babl_component ("yellow"),
babl_component ("key"),
NULL);
}
format = babl_format_with_space (babl_format_get_encoding (format), space);
key_format = babl_format_new (babl_model ("Y"),
type,
babl_component ("Y"),
NULL);
key_format = babl_format_with_space (babl_format_get_encoding (key_format),
space);
cmy_buffer = gegl_buffer_new (GEGL_RECTANGLE (0, 0, width, height),
babl_format_n (type, 3));
gegl_buffer_set_color (cmy_buffer, NULL, white);
key_buffer = gegl_buffer_new (GEGL_RECTANGLE (0, 0, width, height),
key_format);
gegl_buffer_set_color (key_buffer, NULL, white);
if (has_alpha)
{
alpha_buffer = gegl_buffer_new (GEGL_RECTANGLE (0, 0, width, height),
babl_format_n (type, 1));
gegl_buffer_set_color (alpha_buffer, NULL, white);
}
g_object_unref (white);
iter = gegl_buffer_iterator_new (buffer,
GEGL_RECTANGLE (0, 0, width, height), 0,
format,
GEGL_BUFFER_READWRITE,
GEGL_ABYSS_NONE, 4);
gegl_buffer_iterator_add (iter, cmy_buffer,
GEGL_RECTANGLE (0, 0, width, height), 0,
babl_format_n (type, 3), GEGL_BUFFER_READWRITE,
GEGL_ABYSS_NONE);
gegl_buffer_iterator_add (iter, key_buffer,
GEGL_RECTANGLE (0, 0, width, height), 0,
key_format, GEGL_BUFFER_READWRITE,
GEGL_ABYSS_NONE);
if (has_alpha)
gegl_buffer_iterator_add (iter, alpha_buffer,
GEGL_RECTANGLE (0, 0, width, height), 0,
babl_format_n (type, 1), GEGL_BUFFER_READWRITE,
GEGL_ABYSS_NONE);
while (gegl_buffer_iterator_next (iter))
{
guchar *pixel = iter->items[0].data;
guchar *cmy = iter->items[1].data;
guchar *k = iter->items[2].data;
guchar *a = NULL;
gint length = iter->length;
if (has_alpha)
a = iter->items[3].data;
while (length--)
{
gint range = 1;
gint i;
if (bit_depth > 8)
range = 2;
for (i = 0; i < (3 * range); i++)
cmy[i] = pixel[i];
for (i = 0; i < range; i++)
k[i] = pixel[i + (3 * range)];
if (has_alpha)
{
for (i = 0; i < range; i++)
a[i] = pixel[i + (4 * range)];
}
pixel += 4 * range;
cmy += 3 * range;
k += range;
if (has_alpha)
{
a += range;
pixel += range;
}
}
}
gegl_buffer_get (cmy_buffer, GEGL_RECTANGLE (0, 0,
width, height), 1.0,
babl_format_n (type, 3), *cmy_data,
GEGL_AUTO_ROWSTRIDE, GEGL_ABYSS_NONE);
gegl_buffer_get (key_buffer, GEGL_RECTANGLE (0, 0,
width, height), 1.0,
key_format, *key_data,
GEGL_AUTO_ROWSTRIDE, GEGL_ABYSS_NONE);
if (has_alpha)
gegl_buffer_get (alpha_buffer, GEGL_RECTANGLE (0, 0,
width, height), 1.0,
babl_format_n (type, 1), *alpha_data,
GEGL_AUTO_ROWSTRIDE, GEGL_ABYSS_NONE);
g_object_unref (cmy_buffer);
g_object_unref (key_buffer);
if (has_alpha)
g_object_unref (alpha_buffer);
}
static gboolean
save_image (GFile *file,
GimpProcedureConfig *config,
@ -1174,6 +1336,8 @@ save_image (GFile *file,
JxlColorEncoding color_profile;
JxlEncoderStatus status;
size_t buffer_size;
size_t total_channel_size = 0;
size_t channel_buffer_size = 0;
GByteArray *compressed;
@ -1184,10 +1348,14 @@ save_image (GFile *file,
gint drawable_width;
gint drawable_height;
gpointer picture_buffer;
gpointer cmy_buffer = NULL;
gpointer key_buffer = NULL;
gpointer alpha_buffer = NULL;
GimpColorProfile *profile = NULL;
const Babl *file_format = NULL;
const Babl *space = NULL;
const Babl *type = NULL;
gboolean out_linear = FALSE;
size_t offset = 0;
@ -1198,6 +1366,7 @@ save_image (GFile *file,
gboolean lossless = FALSE;
gint speed = 7;
gint bit_depth = 8;
gboolean cmyk = FALSE;
gboolean uses_original_profile = FALSE;
gboolean save_exif = FALSE;
gboolean save_xmp = FALSE;
@ -1210,15 +1379,17 @@ save_image (GFile *file,
"compression", &compression,
"speed", &speed,
"save-bit-depth", &bit_depth,
"cmyk", &cmyk,
"uses-original-profile", &uses_original_profile,
"save-exif", &save_exif,
"save-xmp", &save_xmp,
NULL);
if (lossless)
if (lossless || cmyk)
{
/* JPEG XL developers recommend enabling uses_original_profile
* for better lossless compression efficiency. */
* for better lossless compression efficiency.
* Profile must be saved for CMYK export */
uses_original_profile = TRUE;
}
else
@ -1242,7 +1413,19 @@ save_image (GFile *file,
{
output_info.uses_original_profile = JXL_TRUE;
profile = gimp_image_get_effective_color_profile (image);
if (cmyk)
profile = gimp_image_get_simulation_profile (image);
else
profile = gimp_image_get_effective_color_profile (image);
/* CMYK profile is required for export. If not assigned,
* disable CMYK flag and revert to RGB */
if (cmyk && ! profile)
{
cmyk = FALSE;
profile = gimp_image_get_effective_color_profile (image);
}
out_linear = gimp_color_profile_is_linear (profile);
space = gimp_color_profile_get_space (profile,
@ -1283,95 +1466,141 @@ save_image (GFile *file,
output_info.orientation = JXL_ORIENT_IDENTITY;
output_info.animation.tps_numerator = 10;
output_info.animation.tps_denominator = 1;
output_info.num_extra_channels = 0;
switch (drawable_type)
if (cmyk)
{
case GIMP_GRAYA_IMAGE:
if (uses_original_profile && out_linear)
{
file_format = babl_format ( (bit_depth > 8) ? "YA u16" : "YA u8");
JxlColorEncodingSetToLinearSRGB (&color_profile, JXL_TRUE);
}
else
{
file_format = babl_format ( (bit_depth > 8) ? "Y'A u16" : "Y'A u8");
JxlColorEncodingSetToSRGB (&color_profile, JXL_TRUE);
}
pixel_format.num_channels = 2;
output_info.num_color_channels = 1;
output_info.alpha_bits = (bit_depth > 8) ? 16 : 8;
output_info.alpha_exponent_bits = 0;
output_info.num_extra_channels = 1;
uses_original_profile = FALSE;
break;
case GIMP_GRAY_IMAGE:
if (uses_original_profile && out_linear)
{
file_format = babl_format ( (bit_depth > 8) ? "Y u16" : "Y u8");
JxlColorEncodingSetToLinearSRGB (&color_profile, JXL_TRUE);
}
else
{
file_format = babl_format ( (bit_depth > 8) ? "Y' u16" : "Y' u8");
JxlColorEncodingSetToSRGB (&color_profile, JXL_TRUE);
}
pixel_format.num_channels = 1;
output_info.num_color_channels = 1;
output_info.alpha_bits = 0;
uses_original_profile = FALSE;
break;
case GIMP_RGBA_IMAGE:
if (bit_depth > 8)
{
file_format = babl_format_with_space (out_linear ? "RGBA u16" : "R'G'B'A u16", space);
output_info.alpha_bits = 16;
file_format = babl_format_with_space (gimp_drawable_has_alpha (drawable) ?
"cmykA u16" : "cmyk u16", space);
type = babl_type ("u16");
}
else
{
file_format = babl_format_with_space (out_linear ? "RGBA u8" : "R'G'B'A u8", space);
output_info.alpha_bits = 8;
file_format = babl_format_with_space (gimp_drawable_has_alpha (drawable) ?
"cmykA u8" : "cmyk u8", space);
type = babl_type ("u8");
}
pixel_format.num_channels = 4;
JxlColorEncodingSetToSRGB (&color_profile, JXL_FALSE);
output_info.num_color_channels = 3;
output_info.alpha_exponent_bits = 0;
output_info.num_extra_channels = 1;
break;
case GIMP_RGB_IMAGE:
if (bit_depth > 8)
{
file_format = babl_format_with_space (out_linear ? "RGB u16" : "R'G'B' u16", space);
}
else
{
file_format = babl_format_with_space (out_linear ? "RGB u8" : "R'G'B' u8", space);
}
pixel_format.num_channels = 3;
JxlColorEncodingSetToSRGB (&color_profile, JXL_FALSE);
output_info.num_color_channels = 3;
output_info.alpha_bits = 0;
break;
default:
if (profile)
{
g_object_unref (profile);
}
return FALSE;
break;
}
if (gimp_drawable_has_alpha (drawable))
{
output_info.num_extra_channels = 2;
output_info.alpha_bits = 8;
if (bit_depth > 8)
output_info.alpha_bits = 16;
output_info.alpha_exponent_bits = 0;
}
else
{
output_info.num_extra_channels = 1;
output_info.alpha_bits = 0;
}
}
else /* For RGB and grayscale export */
{
switch (drawable_type)
{
case GIMP_GRAYA_IMAGE:
if (uses_original_profile && out_linear)
{
file_format = babl_format ( (bit_depth > 8) ? "YA u16" : "YA u8");
JxlColorEncodingSetToLinearSRGB (&color_profile, JXL_TRUE);
}
else
{
file_format = babl_format ( (bit_depth > 8) ? "Y'A u16" : "Y'A u8");
JxlColorEncodingSetToSRGB (&color_profile, JXL_TRUE);
}
pixel_format.num_channels = 2;
output_info.num_color_channels = 1;
output_info.alpha_bits = (bit_depth > 8) ? 16 : 8;
output_info.alpha_exponent_bits = 0;
output_info.num_extra_channels = 1;
uses_original_profile = FALSE;
break;
case GIMP_GRAY_IMAGE:
if (uses_original_profile && out_linear)
{
file_format = babl_format ( (bit_depth > 8) ? "Y u16" : "Y u8");
JxlColorEncodingSetToLinearSRGB (&color_profile, JXL_TRUE);
}
else
{
file_format = babl_format ( (bit_depth > 8) ? "Y' u16" : "Y' u8");
JxlColorEncodingSetToSRGB (&color_profile, JXL_TRUE);
}
pixel_format.num_channels = 1;
output_info.num_color_channels = 1;
output_info.alpha_bits = 0;
uses_original_profile = FALSE;
break;
case GIMP_RGBA_IMAGE:
if (bit_depth > 8)
{
file_format = babl_format_with_space (out_linear ? "RGBA u16" : "R'G'B'A u16", space);
output_info.alpha_bits = 16;
}
else
{
file_format = babl_format_with_space (out_linear ? "RGBA u8" : "R'G'B'A u8", space);
output_info.alpha_bits = 8;
}
pixel_format.num_channels = 4;
JxlColorEncodingSetToSRGB (&color_profile, JXL_FALSE);
output_info.num_color_channels = 3;
output_info.alpha_exponent_bits = 0;
output_info.num_extra_channels = 1;
break;
case GIMP_RGB_IMAGE:
if (bit_depth > 8)
{
file_format = babl_format_with_space (out_linear ? "RGB u16" : "R'G'B' u16", space);
}
else
{
file_format = babl_format_with_space (out_linear ? "RGB u8" : "R'G'B' u8", space);
}
pixel_format.num_channels = 3;
JxlColorEncodingSetToSRGB (&color_profile, JXL_FALSE);
output_info.num_color_channels = 3;
output_info.alpha_bits = 0;
break;
default:
if (profile)
{
g_object_unref (profile);
}
return FALSE;
break;
}
}
if (bit_depth > 8)
{
buffer_size = 2 * pixel_format.num_channels * (size_t) output_info.xsize * (size_t) output_info.ysize;
total_channel_size =
2 * output_info.num_extra_channels * (size_t) output_info.xsize * (size_t) output_info.ysize;
}
else
{
buffer_size = pixel_format.num_channels * (size_t) output_info.xsize * (size_t) output_info.ysize;
total_channel_size =
output_info.num_extra_channels * (size_t) output_info.xsize * (size_t) output_info.ysize;
}
picture_buffer = g_malloc (buffer_size);
/* The maximum number of channels for JPEG XL is 3.
The image may have more (e.g. CMYKA), so we'll combine them
for the initial GeglBuffer loading */
total_channel_size += buffer_size;
picture_buffer = g_malloc (total_channel_size);
gimp_progress_update (0.3);
@ -1381,6 +1610,24 @@ save_image (GFile *file,
file_format, picture_buffer,
GEGL_AUTO_ROWSTRIDE, GEGL_ABYSS_NONE);
/* Copying K value to buffer */
if (cmyk)
{
channel_buffer_size = (size_t) output_info.xsize * (size_t) output_info.ysize;
if (bit_depth > 8)
channel_buffer_size *= 2;
cmy_buffer = g_malloc (buffer_size);
key_buffer = g_malloc (channel_buffer_size);
if (gimp_drawable_has_alpha (drawable))
alpha_buffer = g_malloc (channel_buffer_size);
extract_cmyk (buffer, &cmy_buffer, &key_buffer, &alpha_buffer,
type, space, output_info.xsize, output_info.ysize,
bit_depth, gimp_drawable_has_alpha (drawable));
}
g_object_unref (buffer);
gimp_progress_update (0.4);
@ -1505,7 +1752,9 @@ save_image (GFile *file,
gimp_progress_update (0.5);
status = JxlEncoderAddImageFrame (encoder_options, &pixel_format, picture_buffer, buffer_size);
status = JxlEncoderAddImageFrame (encoder_options, &pixel_format,
(cmyk) ? cmy_buffer : picture_buffer,
buffer_size);
if (status != JXL_ENC_SUCCESS)
{
g_set_error (error, G_FILE_ERROR, 0,
@ -1516,6 +1765,52 @@ save_image (GFile *file,
return FALSE;
}
if (cmyk)
{
JxlExtraChannelInfo extra;
JxlExtraChannelInfo extra_alpha;
JxlEncoderInitExtraChannelInfo (JXL_CHANNEL_BLACK, &extra);
extra.bits_per_sample = output_info.bits_per_sample;
extra.exponent_bits_per_sample = output_info.exponent_bits_per_sample;
/* Key Channel */
status = JxlEncoderSetExtraChannelInfo (encoder, 0, &extra);
if (status != JXL_ENC_SUCCESS)
g_printerr ("JxlEncoderSetExtraChannelInfo failed");
status = JxlEncoderSetExtraChannelBuffer (encoder_options, &pixel_format,
key_buffer, channel_buffer_size,
0);
if (status != JXL_ENC_SUCCESS)
g_printerr ("JxlEncoderSetExtraChannelBuffer failed");
if (alpha_buffer)
{
/* Alpha Channel */
JxlEncoderInitExtraChannelInfo (JXL_CHANNEL_ALPHA, &extra_alpha);
extra_alpha.bits_per_sample = output_info.bits_per_sample;
extra_alpha.exponent_bits_per_sample = output_info.exponent_bits_per_sample;
status = JxlEncoderSetExtraChannelInfo (encoder, 1, &extra_alpha);
if (status != JXL_ENC_SUCCESS)
g_printerr ("JxlEncoderSetExtraChannelInfo failed");
status = JxlEncoderSetExtraChannelBuffer (encoder_options,
&pixel_format,
alpha_buffer,
channel_buffer_size, 1);
if (status != JXL_ENC_SUCCESS)
g_printerr ("JxlEncoderSetExtraChannelBuffer failed");
g_free (alpha_buffer);
}
g_free (key_buffer);
}
gimp_progress_update (0.65);
if (metadata && (save_exif || save_xmp))
@ -1684,11 +1979,13 @@ save_dialog (GimpImage *image,
GimpProcedure *procedure,
GObject *config)
{
GtkWidget *dialog;
GtkListStore *store;
GtkWidget *compression_scale;
GtkWidget *orig_profile_check;
gboolean run;
GtkWidget *dialog;
GtkListStore *store;
GtkWidget *compression_scale;
GtkWidget *orig_profile_check;
GtkWidget *profile_label;
GimpColorProfile *cmyk_profile = NULL;
gboolean run;
dialog = gimp_save_procedure_dialog_new (GIMP_SAVE_PROCEDURE (procedure),
GIMP_PROCEDURE_CONFIG (config),
@ -1727,6 +2024,51 @@ save_dialog (GimpImage *image,
gimp_procedure_dialog_get_int_combo (GIMP_PROCEDURE_DIALOG (dialog),
"save-bit-depth", GIMP_INT_STORE (store));
/* Profile label */
profile_label = gimp_procedure_dialog_get_label (GIMP_PROCEDURE_DIALOG (dialog),
"profile-label", _("CMYK profile required for export"));
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_image_get_simulation_profile (image);
if (! cmyk_profile)
{
g_object_set (config,
"cmyk", FALSE,
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);
}
/* JPEG XL requires a CMYK profile if exporting as CMYK */
gimp_procedure_dialog_set_sensitive (GIMP_PROCEDURE_DIALOG (dialog),
"cmyk",
cmyk_profile != NULL,
NULL, NULL, FALSE);
orig_profile_check = gimp_procedure_dialog_get_widget (GIMP_PROCEDURE_DIALOG (dialog),
"uses-original-profile",
GTK_TYPE_CHECK_BUTTON);
@ -1739,6 +2081,7 @@ save_dialog (GimpImage *image,
gimp_procedure_dialog_fill (GIMP_PROCEDURE_DIALOG (dialog),
"lossless", "compression",
"speed", "save-bit-depth",
"cmyk-frame",
"uses-original-profile",
"save-exif", "save-xmp",
NULL);