/* 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 . */ #include "config.h" #include #include #include #include #include #include #include #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; }