diff options
Diffstat (limited to '')
-rw-r--r-- | plug-ins/common/file-jpegxl.c | 1174 |
1 files changed, 1174 insertions, 0 deletions
diff --git a/plug-ins/common/file-jpegxl.c b/plug-ins/common/file-jpegxl.c new file mode 100644 index 0000000..e85f647 --- /dev/null +++ b/plug-ins/common/file-jpegxl.c @@ -0,0 +1,1174 @@ +/* GIMP - The GNU Image Manipulation Program + * Copyright (C) 1995 Spencer Kimball and Peter Mattis + * + * file-jpegxl - JPEG XL file format plug-in for the GIMP + * Copyright (C) 2022 Daniel Novomesky + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#include "config.h" + +#include <gexiv2/gexiv2.h> +#include <glib/gstdio.h> + +#include <jxl/decode.h> +#include <jxl/encode.h> +#include <jxl/thread_parallel_runner.h> + +#include <libgimp/gimp.h> +#include <libgimp/gimpui.h> + +#include "libgimp/stdplugins-intl.h" + +#define LOAD_PROC "file-jpegxl-load" +#define SAVE_PROC "file-jpegxl-save" +#define PLUG_IN_BINARY "file-jpegxl" + +static void query (void); +static void run (const gchar *name, + gint nparams, + const GimpParam *param, + gint *nreturn_vals, + GimpParam **return_vals); + +GimpPlugInInfo PLUG_IN_INFO = { + NULL, /* init_proc */ + NULL, /* quit_proc */ + query, /* query_proc */ + run, /* run_proc */ +}; + +MAIN () + +static void +query (void) +{ + static const GimpParamDef load_args[] = { + { GIMP_PDB_INT32, "run-mode", "The run mode { RUN-INTERACTIVE (0), RUN-NONINTERACTIVE (1) }"}, + { GIMP_PDB_STRING, "filename", "The name of the file to load" }, + { GIMP_PDB_STRING, "raw-filename", "The name of the file to load" } + }; + static const GimpParamDef load_return_vals[] = { + {GIMP_PDB_IMAGE, "image", "Output image"} + }; + + static const GimpParamDef save_args[] = + { + { GIMP_PDB_INT32, "run-mode", "The run mode { RUN-INTERACTIVE (0), RUN-NONINTERACTIVE (1) }" }, + { GIMP_PDB_IMAGE, "image", "Input image" }, + { GIMP_PDB_DRAWABLE, "drawable", "Drawable to save" }, + { GIMP_PDB_STRING, "filename", "The name of the file to save the image in" }, + { GIMP_PDB_STRING, "raw-filename", "The name of the file to save the image in" } + }; + + gimp_install_procedure (LOAD_PROC, + "Loads files in the JPEG XL file format", + "Loads files in the JPEG XL file format", + "Daniel Novomesky", + "(C) 2022 Daniel Novomesky", + "2022", + N_("JPEG XL image"), + NULL, + GIMP_PLUGIN, + G_N_ELEMENTS (load_args), + G_N_ELEMENTS (load_return_vals), + load_args, load_return_vals); + + gimp_register_file_handler_mime (LOAD_PROC, "image/jxl"); + gimp_register_magic_load_handler (LOAD_PROC, "jxl", "", "0,string,\xFF\x0A,0,string,\\000\\000\\000\x0CJXL\\040\\015\\012\x87\\012"); + gimp_register_file_handler_priority (LOAD_PROC, 100); + + gimp_install_procedure (SAVE_PROC, + "Saves files in the JPEG XL file format", + "Saves files in the JPEG XL file format", + "Daniel Novomesky", + "(C) 2022 Daniel Novomesky", + "2022", + N_("JPEG XL image"), + "RGB*, GRAY*", + GIMP_PLUGIN, + G_N_ELEMENTS (save_args), 0, + save_args, NULL); + + gimp_register_file_handler_mime (SAVE_PROC, "image/jxl"); + gimp_register_save_handler (SAVE_PROC, "jxl", ""); + gimp_register_file_handler_uri (SAVE_PROC); + gimp_register_file_handler_priority (SAVE_PROC, 100); +} + +static gint32 +load_image (const gchar *filename, + GimpRunMode run_mode, + GError **error) +{ + FILE *inputFile = g_fopen (filename, "rb"); + + gsize inputFileSize; + gpointer memory; + + JxlSignature signature; + JxlDecoder *decoder; + void *runner; + JxlBasicInfo basicinfo; + JxlDecoderStatus status; + JxlPixelFormat pixel_format; + JxlColorEncoding color_encoding; + size_t icc_size = 0; + GimpColorProfile *profile = NULL; + gboolean loadlinear = FALSE; + size_t channel_depth; + size_t result_size; + gpointer picture_buffer; + gint32 image = -1; + gint32 layer; + GeglBuffer *buffer; + GimpPrecision precision_linear; + GimpPrecision precision_non_linear; + size_t num_worker_threads = 1; + + if (! inputFile) + { + g_set_error (error, G_FILE_ERROR, 0, "Cannot open file for read: %s\n", filename); + return -1; + } + + fseek (inputFile, 0, SEEK_END); + inputFileSize = ftell (inputFile); + fseek (inputFile, 0, SEEK_SET); + + if (inputFileSize < 1) + { + g_set_error (error, G_FILE_ERROR, 0, "File too small: %s\n", filename); + fclose (inputFile); + return -1; + } + + memory = g_malloc (inputFileSize); + if (fread (memory, 1, inputFileSize, inputFile) != inputFileSize) + { + g_set_error (error, G_FILE_ERROR, 0, "Failed to read %zu bytes: %s\n", + inputFileSize, filename); + fclose (inputFile); + g_free (memory); + return -1; + } + + fclose (inputFile); + + signature = JxlSignatureCheck (memory, inputFileSize); + if (signature != JXL_SIG_CODESTREAM && signature != JXL_SIG_CONTAINER) + { + g_set_error (error, G_FILE_ERROR, 0, + "File %s is probably not in JXL format!\n", filename); + g_free (memory); + return -1; + } + + decoder = JxlDecoderCreate (NULL); + if (! decoder) + { + g_set_error (error, G_FILE_ERROR, 0, "ERROR: JxlDecoderCreate failed"); + g_free (memory); + return -1; + } + + num_worker_threads = g_get_num_processors (); + if (num_worker_threads > 16) + { + num_worker_threads = 16; + } + runner = JxlThreadParallelRunnerCreate (NULL, num_worker_threads); + if (JxlDecoderSetParallelRunner (decoder, JxlThreadParallelRunner, runner) != JXL_DEC_SUCCESS) + { + g_set_error (error, G_FILE_ERROR, 0, "ERROR: JxlDecoderSetParallelRunner failed"); + JxlThreadParallelRunnerDestroy (runner); + JxlDecoderDestroy (decoder); + g_free (memory); + return -1; + } + + if (JxlDecoderSetInput (decoder, memory, inputFileSize) != JXL_DEC_SUCCESS) + { + g_set_error (error, G_FILE_ERROR, 0, "ERROR: JxlDecoderSetInput failed"); + JxlThreadParallelRunnerDestroy (runner); + JxlDecoderDestroy (decoder); + g_free (memory); + return -1; + } + + JxlDecoderCloseInput (decoder); + + if (JxlDecoderSubscribeEvents (decoder, JXL_DEC_BASIC_INFO | JXL_DEC_COLOR_ENCODING | JXL_DEC_FULL_IMAGE) + != JXL_DEC_SUCCESS) + { + g_set_error (error, G_FILE_ERROR, 0, "ERROR: JxlDecoderSubscribeEvents failed"); + JxlThreadParallelRunnerDestroy (runner); + JxlDecoderDestroy (decoder); + g_free (memory); + return -1; + } + + status = JxlDecoderProcessInput (decoder); + if (status == JXL_DEC_ERROR) + { + g_set_error (error, G_FILE_ERROR, 0, "ERROR: JXL decoding failed"); + JxlThreadParallelRunnerDestroy (runner); + JxlDecoderDestroy (decoder); + g_free (memory); + return -1; + } + + if (status == JXL_DEC_NEED_MORE_INPUT) + { + g_set_error (error, G_FILE_ERROR, 0, "ERROR: JXL data incomplete"); + JxlThreadParallelRunnerDestroy (runner); + JxlDecoderDestroy (decoder); + g_free (memory); + return -1; + } + + status = JxlDecoderGetBasicInfo (decoder, &basicinfo); + if (status != JXL_DEC_SUCCESS) + { + g_set_error (error, G_FILE_ERROR, 0, "ERROR: JXL basic info not available"); + JxlThreadParallelRunnerDestroy (runner); + JxlDecoderDestroy (decoder); + g_free (memory); + return -1; + } + + if (basicinfo.xsize == 0 || basicinfo.ysize == 0) + { + g_set_error (error, G_FILE_ERROR, 0, "ERROR: JXL image has zero dimensions"); + JxlThreadParallelRunnerDestroy (runner); + JxlDecoderDestroy (decoder); + g_free (memory); + return -1; + } + + status = JxlDecoderProcessInput (decoder); + if (status != JXL_DEC_COLOR_ENCODING) + { + g_set_error (error, G_FILE_ERROR, 0, + "Unexpected event %d instead of JXL_DEC_COLOR_ENCODING", status); + JxlThreadParallelRunnerDestroy (runner); + JxlDecoderDestroy (decoder); + g_free (memory); + return -1; + } + + if (basicinfo.uses_original_profile == JXL_FALSE) + { + if (basicinfo.num_color_channels == 3) + { + JxlColorEncodingSetToSRGB (&color_encoding, JXL_FALSE); + JxlDecoderSetPreferredColorProfile (decoder, &color_encoding); + } + else if (basicinfo.num_color_channels == 1) + { + JxlColorEncodingSetToSRGB (&color_encoding, JXL_TRUE); + JxlDecoderSetPreferredColorProfile (decoder, &color_encoding); + } + } + + pixel_format.endianness = JXL_NATIVE_ENDIAN; + pixel_format.align = 0; + + if (basicinfo.uses_original_profile == JXL_FALSE || basicinfo.bits_per_sample > 16) + { + pixel_format.data_type = JXL_TYPE_FLOAT; + channel_depth = 4; + precision_linear = GIMP_PRECISION_FLOAT_LINEAR; + precision_non_linear = GIMP_PRECISION_FLOAT_GAMMA; + } + else if (basicinfo.bits_per_sample <= 8) + { + pixel_format.data_type = JXL_TYPE_UINT8; + channel_depth = 1; + precision_linear = GIMP_PRECISION_U8_LINEAR; + precision_non_linear = GIMP_PRECISION_U8_GAMMA; + } + else + { + pixel_format.data_type = JXL_TYPE_UINT16; + channel_depth = 2; + precision_linear = GIMP_PRECISION_U16_LINEAR; + precision_non_linear = GIMP_PRECISION_U16_GAMMA; + } + + if (basicinfo.num_color_channels == 1) /* grayscale */ + { + if (basicinfo.alpha_bits > 0) + { + pixel_format.num_channels = 2; + } + else + { + pixel_format.num_channels = 1; + } + } + else /* RGB */ + { + + if (basicinfo.alpha_bits > 0) /* RGB with alpha */ + { + pixel_format.num_channels = 4; + } + else /* RGB no alpha */ + { + pixel_format.num_channels = 3; + } + } + + result_size = channel_depth * pixel_format.num_channels + * (size_t) basicinfo.xsize * (size_t) basicinfo.ysize; + + if (JxlDecoderGetColorAsEncodedProfile (decoder, &pixel_format, + JXL_COLOR_PROFILE_TARGET_DATA, + &color_encoding) == JXL_DEC_SUCCESS) + { + if (color_encoding.white_point == JXL_WHITE_POINT_D65) + { + switch (color_encoding.transfer_function) + { + case JXL_TRANSFER_FUNCTION_LINEAR: + loadlinear = TRUE; + + switch (color_encoding.color_space) + { + case JXL_COLOR_SPACE_RGB: + profile = gimp_color_profile_new_rgb_srgb_linear (); + break; + case JXL_COLOR_SPACE_GRAY: + profile = gimp_color_profile_new_d65_gray_linear (); + break; + default: + break; + } + break; + case JXL_TRANSFER_FUNCTION_SRGB: + switch (color_encoding.color_space) + { + case JXL_COLOR_SPACE_RGB: + profile = gimp_color_profile_new_rgb_srgb (); + break; + case JXL_COLOR_SPACE_GRAY: + profile = gimp_color_profile_new_d65_gray_srgb_trc (); + break; + default: + break; + } + break; + default: + break; + } + } + } + + if (! profile) + { + if (JxlDecoderGetICCProfileSize (decoder, &pixel_format, + JXL_COLOR_PROFILE_TARGET_DATA, + &icc_size) == JXL_DEC_SUCCESS) + { + if (icc_size > 0) + { + gpointer raw_icc_profile = g_malloc (icc_size); + + if (JxlDecoderGetColorAsICCProfile (decoder, &pixel_format, JXL_COLOR_PROFILE_TARGET_DATA, + raw_icc_profile, icc_size) + == JXL_DEC_SUCCESS) + { + profile = gimp_color_profile_new_from_icc_profile (raw_icc_profile, + icc_size, error); + if (profile) + { + loadlinear = gimp_color_profile_is_linear (profile); + } + else + { + g_printerr ("%s: Failed to read ICC profile: %s\n", + G_STRFUNC, (*error)->message); + g_clear_error (error); + } + } + else + { + g_printerr ("Failed to obtain data from JPEG XL decoder"); + } + + g_free (raw_icc_profile); + } + else + { + g_printerr ("Empty ICC data"); + } + } + else + { + g_message ("no ICC, other color profile"); + } + } + + status = JxlDecoderProcessInput (decoder); + if (status != JXL_DEC_NEED_IMAGE_OUT_BUFFER) + { + g_set_error (error, G_FILE_ERROR, + 0, "Unexpected event %d instead of JXL_DEC_NEED_IMAGE_OUT_BUFFER", status); + if (profile) + { + g_object_unref (profile); + } + JxlThreadParallelRunnerDestroy (runner); + JxlDecoderDestroy (decoder); + g_free (memory); + return -1; + } + + picture_buffer = g_try_malloc (result_size); + if (! picture_buffer) + { + g_set_error (error, G_FILE_ERROR, 0, "Memory could not be allocated."); + if (profile) + { + g_object_unref (profile); + } + JxlThreadParallelRunnerDestroy (runner); + JxlDecoderDestroy (decoder); + g_free (memory); + return -1; + } + + if (JxlDecoderSetImageOutBuffer (decoder, &pixel_format, picture_buffer, result_size) != JXL_DEC_SUCCESS) + { + g_set_error (error, G_FILE_ERROR, 0, "ERROR: JxlDecoderSetImageOutBuffer failed"); + if (profile) + { + g_object_unref (profile); + } + JxlThreadParallelRunnerDestroy (runner); + JxlDecoderDestroy (decoder); + g_free (memory); + return -1; + } + + status = JxlDecoderProcessInput (decoder); + if (status != JXL_DEC_FULL_IMAGE) + { + g_set_error (error, G_FILE_ERROR, 0, + "Unexpected event %d instead of JXL_DEC_FULL_IMAGE", status); + g_free (picture_buffer); + if (profile) + { + g_object_unref (profile); + } + JxlThreadParallelRunnerDestroy (runner); + JxlDecoderDestroy (decoder); + g_free (memory); + return -1; + } + + if (basicinfo.num_color_channels == 1) /* grayscale */ + { + image = gimp_image_new_with_precision (basicinfo.xsize, basicinfo.ysize, GIMP_GRAY, + loadlinear ? precision_linear : precision_non_linear); + + if (profile) + { + if (gimp_color_profile_is_gray (profile)) + { + gimp_image_set_color_profile (image, profile); + } + } + + layer = gimp_layer_new (image, "Background", basicinfo.xsize, basicinfo.ysize, + (basicinfo.alpha_bits > 0) ? GIMP_GRAYA_IMAGE : GIMP_GRAY_IMAGE, + 100, gimp_image_get_default_new_layer_mode (image)); + } + else /* RGB */ + { + image = gimp_image_new_with_precision (basicinfo.xsize, basicinfo.ysize, GIMP_RGB, + loadlinear ? precision_linear : precision_non_linear); + + if (profile) + { + if (gimp_color_profile_is_rgb (profile)) + { + gimp_image_set_color_profile (image, profile); + } + } + + layer = gimp_layer_new (image, "Background", basicinfo.xsize, basicinfo.ysize, + (basicinfo.alpha_bits > 0) ? GIMP_RGBA_IMAGE : GIMP_RGB_IMAGE, + 100, gimp_image_get_default_new_layer_mode (image)); + } + + gimp_image_insert_layer (image, layer, -1, 0); + + buffer = gimp_drawable_get_buffer (layer); + + gegl_buffer_set (buffer, GEGL_RECTANGLE (0, 0, basicinfo.xsize, basicinfo.ysize), + 0, NULL, picture_buffer, GEGL_AUTO_ROWSTRIDE); + + g_object_unref (buffer); + + g_free (picture_buffer); + if (profile) + { + g_object_unref (profile); + } + + if (basicinfo.have_container) + { + JxlDecoderReleaseInput (decoder); + JxlDecoderRewind (decoder); + + if (JxlDecoderSetInput (decoder, memory, inputFileSize) != JXL_DEC_SUCCESS) + { + g_printerr ("%s: JxlDecoderSetInput failed after JxlDecoderRewind\n", G_STRFUNC); + } + else + { + JxlDecoderCloseInput (decoder); + if (JxlDecoderSubscribeEvents (decoder, JXL_DEC_BOX) != JXL_DEC_SUCCESS) + { + g_printerr ("%s: JxlDecoderSubscribeEvents for JXL_DEC_BOX failed\n", G_STRFUNC); + } + else + { + gboolean search_exif = TRUE; + gboolean search_xmp = TRUE; + gboolean success_exif = FALSE; + gboolean success_xmp = FALSE; + JxlBoxType box_type = { 0, 0, 0, 0 }; + GByteArray *exif_box = NULL; + GByteArray *xml_box = NULL; + size_t exif_remains = 0; + size_t xml_remains = 0; + + while (search_exif || search_xmp) + { + status = JxlDecoderProcessInput (decoder); + switch (status) + { + case JXL_DEC_SUCCESS: + if (box_type[0] == 'E' && box_type[1] == 'x' && box_type[2] == 'i' && box_type[3] == 'f' && search_exif) + { + exif_remains = JxlDecoderReleaseBoxBuffer (decoder); + g_byte_array_set_size (exif_box, exif_box->len - exif_remains); + success_exif = TRUE; + } + else if (box_type[0] == 'x' && box_type[1] == 'm' && box_type[2] == 'l' && box_type[3] == ' ' && search_xmp) + { + xml_remains = JxlDecoderReleaseBoxBuffer (decoder); + g_byte_array_set_size (xml_box, xml_box->len - xml_remains); + success_xmp = TRUE; + } + + search_exif = FALSE; + search_xmp = FALSE; + break; + case JXL_DEC_ERROR: + search_exif = FALSE; + search_xmp = FALSE; + g_printerr ("%s: Metadata decoding error\n", G_STRFUNC); + break; + case JXL_DEC_NEED_MORE_INPUT: + search_exif = FALSE; + search_xmp = FALSE; + g_printerr ("%s: JXL metadata are probably incomplete\n", G_STRFUNC); + break; + case JXL_DEC_BOX: + JxlDecoderSetDecompressBoxes (decoder, JXL_TRUE); + + if (box_type[0] == 'E' && box_type[1] == 'x' && box_type[2] == 'i' && box_type[3] == 'f' && search_exif && exif_box) + { + exif_remains = JxlDecoderReleaseBoxBuffer (decoder); + g_byte_array_set_size (exif_box, exif_box->len - exif_remains); + + search_exif = FALSE; + success_exif = TRUE; + } + else if (box_type[0] == 'x' && box_type[1] == 'm' && box_type[2] == 'l' && box_type[3] == ' ' && search_xmp && xml_box) + { + xml_remains = JxlDecoderReleaseBoxBuffer (decoder); + g_byte_array_set_size (xml_box, xml_box->len - xml_remains); + + search_xmp = FALSE; + success_xmp = TRUE; + } + + if (JxlDecoderGetBoxType (decoder, box_type, JXL_TRUE) == JXL_DEC_SUCCESS) + { + if (box_type[0] == 'E' && box_type[1] == 'x' && box_type[2] == 'i' && box_type[3] == 'f' && search_exif) + { + exif_box = g_byte_array_sized_new (4096); + g_byte_array_set_size (exif_box, 4096); + + JxlDecoderSetBoxBuffer (decoder, exif_box->data, exif_box->len); + } + else if (box_type[0] == 'x' && box_type[1] == 'm' && box_type[2] == 'l' && box_type[3] == ' ' && search_xmp) + { + xml_box = g_byte_array_sized_new (4096); + g_byte_array_set_size (xml_box, 4096); + + JxlDecoderSetBoxBuffer (decoder, xml_box->data, xml_box->len); + } + } + else + { + search_exif = FALSE; + search_xmp = FALSE; + g_printerr ("%s: Error in JxlDecoderGetBoxType\n", G_STRFUNC); + } + break; + case JXL_DEC_BOX_NEED_MORE_OUTPUT: + if (box_type[0] == 'E' && box_type[1] == 'x' && box_type[2] == 'i' && box_type[3] == 'f' && search_exif) + { + exif_remains = JxlDecoderReleaseBoxBuffer (decoder); + g_byte_array_set_size (exif_box, exif_box->len + 4096); + JxlDecoderSetBoxBuffer (decoder, exif_box->data + exif_box->len - (4096 + exif_remains), 4096 + exif_remains); + } + else if (box_type[0] == 'x' && box_type[1] == 'm' && box_type[2] == 'l' && box_type[3] == ' ' && search_xmp) + { + xml_remains = JxlDecoderReleaseBoxBuffer (decoder); + g_byte_array_set_size (xml_box, xml_box->len + 4096); + JxlDecoderSetBoxBuffer (decoder, xml_box->data + xml_box->len - (4096 + xml_remains), 4096 + xml_remains); + } + else + { + search_exif = FALSE; + search_xmp = FALSE; + } + break; + default: + break; + } + } + + if (success_exif || success_xmp) + { + GimpMetadata *metadata = gimp_metadata_new (); + + if (success_exif && exif_box) + { + const guint8 tiffHeaderBE[4] = { 'M', 'M', 0, 42 }; + const guint8 tiffHeaderLE[4] = { 'I', 'I', 42, 0 }; + const guint8 *tiffheader = exif_box->data; + glong new_exif_size = exif_box->len; + + while (new_exif_size >= 4) /*Searching for TIFF Header*/ + { + if (tiffheader[0] == tiffHeaderBE[0] && tiffheader[1] == tiffHeaderBE[1] && + tiffheader[2] == tiffHeaderBE[2] && tiffheader[3] == tiffHeaderBE[3]) + { + break; + } + if (tiffheader[0] == tiffHeaderLE[0] && tiffheader[1] == tiffHeaderLE[1] && + tiffheader[2] == tiffHeaderLE[2] && tiffheader[3] == tiffHeaderLE[3]) + { + break; + } + new_exif_size--; + tiffheader++; + } + + if (new_exif_size > 4) /* TIFF header + some data found*/ + { + if (! gexiv2_metadata_open_buf (GEXIV2_METADATA (metadata), tiffheader, new_exif_size, error)) + { + g_printerr ("%s: Failed to set EXIF metadata: %s\n", G_STRFUNC, (*error)->message); + g_clear_error (error); + } + } + else + { + g_printerr ("%s: EXIF metadata not set\n", G_STRFUNC); + } + } + + if (success_xmp && xml_box) + { + if (! gimp_metadata_set_from_xmp (metadata, xml_box->data, xml_box->len, error)) + { + g_printerr ("%s: Failed to set XMP metadata: %s\n", G_STRFUNC, (*error)->message); + g_clear_error (error); + } + } + + gexiv2_metadata_set_orientation (GEXIV2_METADATA (metadata), + GEXIV2_ORIENTATION_NORMAL); + gexiv2_metadata_set_metadata_pixel_width (GEXIV2_METADATA (metadata), + basicinfo.xsize); + gexiv2_metadata_set_metadata_pixel_height (GEXIV2_METADATA (metadata), + basicinfo.ysize); + gimp_image_metadata_load_finish (image, "image/jxl", metadata, + GIMP_METADATA_LOAD_COMMENT | GIMP_METADATA_LOAD_RESOLUTION, + (run_mode == GIMP_RUN_INTERACTIVE)); + } + + if (exif_box) + { + g_byte_array_free (exif_box, TRUE); + } + + if (xml_box) + { + g_byte_array_free (xml_box, TRUE); + } + } + } + } + + JxlThreadParallelRunnerDestroy (runner); + JxlDecoderDestroy (decoder); + g_free (memory); + return image; +} + +static gboolean +save_image (GFile *file, + gint32 image, + gint32 drawable, + GError **error) +{ + JxlEncoder *encoder; + void *runner; + JxlEncoderFrameSettings *encoder_options; + JxlPixelFormat pixel_format; + JxlBasicInfo output_info; + JxlColorEncoding color_profile; + JxlEncoderStatus status; + size_t buffer_size; + + GByteArray *compressed; + + FILE *outfile; + GeglBuffer *buffer; + GimpImageType drawable_type; + + gint drawable_width; + gint drawable_height; + gpointer picture_buffer; + + GimpColorProfile *profile = NULL; + const Babl *file_format = NULL; + const Babl *space = NULL; + gboolean out_linear = FALSE; + + size_t offset = 0; + uint8_t *next_out; + size_t avail_out; + + gboolean save_icc; + size_t num_worker_threads = 1; + + gimp_progress_init_printf (_("Exporting '%s'"), + gimp_file_get_utf8_name (file)); + + drawable_type = gimp_drawable_type (drawable); + buffer = gimp_drawable_get_buffer (drawable); + drawable_width = gegl_buffer_get_width (buffer); + drawable_height = gegl_buffer_get_height (buffer); + + JxlEncoderInitBasicInfo(&output_info); + + output_info.uses_original_profile = JXL_TRUE; + + profile = gimp_image_get_effective_color_profile (image); + out_linear = gimp_color_profile_is_linear (profile); + + space = gimp_color_profile_get_space (profile, + GIMP_COLOR_RENDERING_INTENT_RELATIVE_COLORIMETRIC, + error); + + if (error && *error) + { + g_printerr ("%s: error getting the profile space: %s\n", + G_STRFUNC, (*error)->message); + g_object_unref (buffer); + return FALSE; + } + + pixel_format.data_type = JXL_TYPE_UINT8; + output_info.bits_per_sample = 8; + + pixel_format.endianness = JXL_NATIVE_ENDIAN; + pixel_format.align = 0; + + output_info.xsize = drawable_width; + output_info.ysize = drawable_height; + output_info.exponent_bits_per_sample = 0; + output_info.orientation = JXL_ORIENT_IDENTITY; + output_info.animation.tps_numerator = 10; + output_info.animation.tps_denominator = 1; + + switch (drawable_type) + { + case GIMP_GRAYA_IMAGE: + if (out_linear) + { + file_format = babl_format ( "YA u8"); + JxlColorEncodingSetToLinearSRGB (&color_profile, JXL_TRUE); + } + else + { + file_format = babl_format ("Y'A u8"); + JxlColorEncodingSetToSRGB (&color_profile, JXL_TRUE); + } + pixel_format.num_channels = 2; + output_info.num_color_channels = 1; + output_info.alpha_bits = 8; + output_info.alpha_exponent_bits = 0; + output_info.num_extra_channels = 1; + + save_icc = FALSE; + break; + case GIMP_GRAY_IMAGE: + if (out_linear) + { + file_format = babl_format ("Y u8"); + JxlColorEncodingSetToLinearSRGB (&color_profile, JXL_TRUE); + } + else + { + file_format = babl_format ("Y' u8"); + JxlColorEncodingSetToSRGB (&color_profile, JXL_TRUE); + } + pixel_format.num_channels = 1; + output_info.num_color_channels = 1; + output_info.alpha_bits = 0; + + save_icc = FALSE; + break; + case GIMP_RGBA_IMAGE: + 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; + output_info.num_color_channels = 3; + output_info.alpha_exponent_bits = 0; + output_info.num_extra_channels = 1; + + save_icc = TRUE; + break; + case GIMP_RGB_IMAGE: + file_format = babl_format_with_space (out_linear ? "RGB u8" : "R'G'B' u8", space); + pixel_format.num_channels = 3; + output_info.num_color_channels = 3; + output_info.alpha_bits = 0; + + save_icc = TRUE; + break; + default: + if (profile) + { + g_object_unref (profile); + } + g_object_unref (buffer); + return FALSE; + break; + } + + buffer_size = pixel_format.num_channels * (size_t) output_info.xsize * (size_t) output_info.ysize; + picture_buffer = g_malloc (buffer_size); + + gimp_progress_update (0.3); + + gegl_buffer_get (buffer, GEGL_RECTANGLE (0, 0, + drawable_width, drawable_height), 1.0, + file_format, picture_buffer, + GEGL_AUTO_ROWSTRIDE, GEGL_ABYSS_NONE); + + g_object_unref (buffer); + + gimp_progress_update (0.4); + + encoder = JxlEncoderCreate (NULL); + if (!encoder) + { + g_set_error (error, G_FILE_ERROR, 0, + "Failed to create Jxl encoder"); + g_free (picture_buffer); + if (profile) + { + g_object_unref (profile); + } + return FALSE; + } + + num_worker_threads = g_get_num_processors (); + if (num_worker_threads > 16) + { + num_worker_threads = 16; + } + runner = JxlThreadParallelRunnerCreate (NULL, num_worker_threads); + if (JxlEncoderSetParallelRunner (encoder, JxlThreadParallelRunner, runner) != JXL_ENC_SUCCESS) + { + g_set_error (error, G_FILE_ERROR, 0, + "JxlEncoderSetParallelRunner failed"); + JxlThreadParallelRunnerDestroy (runner); + JxlEncoderDestroy (encoder); + g_free (picture_buffer); + if (profile) + { + g_object_unref (profile); + } + return FALSE; + } + + status = JxlEncoderSetBasicInfo (encoder, &output_info); + if (status != JXL_ENC_SUCCESS) + { + g_set_error (error, G_FILE_ERROR, 0, + "JxlEncoderSetBasicInfo failed!"); + JxlThreadParallelRunnerDestroy (runner); + JxlEncoderDestroy (encoder); + g_free (picture_buffer); + if (profile) + { + g_object_unref (profile); + } + return FALSE; + } + + if (save_icc) + { + const uint8_t *icc_data = NULL; + size_t icc_length = 0; + + icc_data = gimp_color_profile_get_icc_profile (profile, &icc_length); + status = JxlEncoderSetICCProfile (encoder, icc_data, icc_length); + g_object_unref (profile); + profile = NULL; + + if (status != JXL_ENC_SUCCESS) + { + g_set_error (error, G_FILE_ERROR, 0, + "JxlEncoderSetICCProfile failed!"); + JxlThreadParallelRunnerDestroy (runner); + JxlEncoderDestroy (encoder); + g_free (picture_buffer); + return FALSE; + } + } + else + { + if (profile) + { + g_object_unref (profile); + profile = NULL; + } + + status = JxlEncoderSetColorEncoding (encoder, &color_profile); + if (status != JXL_ENC_SUCCESS) + { + g_set_error (error, G_FILE_ERROR, 0, + "JxlEncoderSetColorEncoding failed!"); + JxlThreadParallelRunnerDestroy (runner); + JxlEncoderDestroy (encoder); + g_free (picture_buffer); + return FALSE; + } + } + + encoder_options = JxlEncoderFrameSettingsCreate (encoder, NULL); + JxlEncoderSetFrameDistance (encoder_options, 0); + JxlEncoderSetFrameLossless (encoder_options, JXL_TRUE); + + gimp_progress_update (0.5); + + status = JxlEncoderAddImageFrame (encoder_options, &pixel_format, picture_buffer, buffer_size); + if (status != JXL_ENC_SUCCESS) + { + g_set_error (error, G_FILE_ERROR, 0, + "JxlEncoderAddImageFrame failed!"); + JxlThreadParallelRunnerDestroy (runner); + JxlEncoderDestroy (encoder); + g_free (picture_buffer); + return FALSE; + } + + gimp_progress_update (0.65); + + JxlEncoderCloseInput (encoder); + + gimp_progress_update (0.7); + + compressed = g_byte_array_sized_new (4096); + g_byte_array_set_size (compressed, 4096); + do + { + next_out = compressed->data + offset; + avail_out = compressed->len - offset; + status = JxlEncoderProcessOutput (encoder, &next_out, &avail_out); + + if (status == JXL_ENC_NEED_MORE_OUTPUT) + { + offset = next_out - compressed->data; + g_byte_array_set_size (compressed, compressed->len * 2); + } + else if (status == JXL_ENC_ERROR) + { + g_set_error (error, G_FILE_ERROR, 0, + "JxlEncoderProcessOutput failed!"); + JxlThreadParallelRunnerDestroy (runner); + JxlEncoderDestroy (encoder); + g_free (picture_buffer); + return FALSE; + } + } + while (status != JXL_ENC_SUCCESS); + + JxlThreadParallelRunnerDestroy (runner); + JxlEncoderDestroy (encoder); + + g_free (picture_buffer); + + g_byte_array_set_size (compressed, next_out - compressed->data); + + gimp_progress_update (0.8); + + if (compressed->len > 0) + { + outfile = g_fopen (g_file_peek_path (file), "wb"); + if (!outfile) + { + g_set_error (error, G_FILE_ERROR, 0, + "Could not open '%s' for writing!\n", + g_file_peek_path (file)); + g_byte_array_free (compressed, TRUE); + return FALSE; + } + + fwrite (compressed->data, 1, compressed->len, outfile); + fclose (outfile); + + gimp_progress_update (1.0); + + g_byte_array_free (compressed, TRUE); + return TRUE; + } + + g_set_error (error, G_FILE_ERROR, 0, + "No data to write"); + g_byte_array_free (compressed, TRUE); + return FALSE; +} + +static void +run (const gchar *name, + gint nparams, + const GimpParam *param, + gint *nreturn_vals, + GimpParam **return_vals) +{ + static GimpParam values[6]; + GimpRunMode run_mode; + GimpPDBStatusType status = GIMP_PDB_SUCCESS; + gint32 image_ID; + GError *error = NULL; + + run_mode = param[0].data.d_int32; + + INIT_I18N (); + gegl_init (NULL, NULL); + + *nreturn_vals = 1; + *return_vals = values; + values[0].type = GIMP_PDB_STATUS; + values[0].data.d_status = GIMP_PDB_EXECUTION_ERROR; + + if (strcmp (name, LOAD_PROC) == 0) + { + switch (run_mode) + { + case GIMP_RUN_INTERACTIVE: + case GIMP_RUN_WITH_LAST_VALS: + gimp_ui_init (PLUG_IN_BINARY, FALSE); + break; + default: + break; + } + + image_ID = load_image (param[1].data.d_string, run_mode, &error); + + if (image_ID != -1) + { + *nreturn_vals = 2; + values[1].type = GIMP_PDB_IMAGE; + values[1].data.d_image = image_ID; + } + else + { + status = GIMP_PDB_EXECUTION_ERROR; + } + } + else if (strcmp (name, SAVE_PROC) == 0) + { + gint32 image_ID = param[1].data.d_int32; + gint32 drawable_ID = param[2].data.d_int32; + GimpExportReturn export = GIMP_EXPORT_CANCEL; + + switch (run_mode) + { + case GIMP_RUN_INTERACTIVE: + case GIMP_RUN_WITH_LAST_VALS: + gimp_ui_init (PLUG_IN_BINARY, FALSE); + + export = gimp_export_image (&image_ID, &drawable_ID, "JPEG XL", + GIMP_EXPORT_CAN_HANDLE_RGB | + GIMP_EXPORT_CAN_HANDLE_GRAY | + GIMP_EXPORT_CAN_HANDLE_ALPHA); + + if (export == GIMP_EXPORT_CANCEL) + { + values[0].data.d_status = GIMP_PDB_CANCEL; + return; + } + break; + + default: + break; + } + + if (status == GIMP_PDB_SUCCESS) + { + GFile *file = g_file_new_for_uri (param[3].data.d_string); + + if (!save_image (file, image_ID, drawable_ID, &error)) + { + status = GIMP_PDB_EXECUTION_ERROR; + } + + g_object_unref (file); + } + + if (export == GIMP_EXPORT_EXPORT) + gimp_image_delete (image_ID); + } + else + { + status = GIMP_PDB_CALLING_ERROR; + } + + if (status != GIMP_PDB_SUCCESS && error) + { + *nreturn_vals = 2; + values[1].type = GIMP_PDB_STRING; + values[1].data.d_string = error->message; + } + + values[0].data.d_status = status; +} |