From 7ce0f2d60e6002f2eb2cfee56adbc0332ab2ff1f Mon Sep 17 00:00:00 2001 From: Daniel Novomesky Date: Thu, 26 Aug 2021 14:24:21 +0200 Subject: [PATCH] Add JPEG XL plug-in --- configure.ac | 28 + meson.build | 13 + meson_options.txt | 1 + plug-ins/common/Makefile.am | 25 + plug-ins/common/file-jpegxl.c | 1132 ++++++++++++++++++++++++++++++++ plug-ins/common/meson.build | 7 + plug-ins/common/plugin-defs.pl | 1 + 7 files changed, 1207 insertions(+) create mode 100644 plug-ins/common/file-jpegxl.c diff --git a/configure.ac b/configure.ac index 7e32eeecd4..3bc7cd02da 100644 --- a/configure.ac +++ b/configure.ac @@ -75,6 +75,7 @@ m4_define([json_glib_required_version], [1.2.6]) m4_define([lcms_required_version], [2.8]) m4_define([libgudev_required_version], [167]) m4_define([libheif_required_version], [1.3.2]) +m4_define([libjxl_required_version], [0.5.0]) m4_define([liblzma_required_version], [5.0.0]) m4_define([libmypaint_required_version], [1.3.0]) m4_define([libpng_required_version], [1.6.25]) @@ -176,6 +177,7 @@ HARFBUZZ_REQUIRED_VERSION=harfbuzz_required_version INTLTOOL_REQUIRED_VERSION=intltool_required_version LCMS_REQUIRED_VERSION=lcms_required_version LIBHEIF_REQUIRED_VERSION=libheif_required_version +LIBJXL_REQUIRED_VERSION=libjxl_required_version LIBLZMA_REQUIRED_VERSION=liblzma_required_version LIBMYPAINT_REQUIRED_VERSION=libmypaint_required_version LIBPNG_REQUIRED_VERSION=libpng_required_version @@ -211,6 +213,7 @@ AC_SUBST(HARFBUZZ_REQUIRED_VERSION) AC_SUBST(INTLTOOL_REQUIRED_VERSION) AC_SUBST(LCMS_REQUIRED_VERSION) AC_SUBST(LIBHEIF_REQUIRED_VERSION) +AC_SUBST(LIBJXL_REQUIRED_VERSION) AC_SUBST(LIBLZMA_REQUIRED_VERSION) AC_SUBST(LIBMYPAINT_REQUIRED_VERSION) AC_SUBST(LIBPNG_REQUIRED_VERSION) @@ -1846,6 +1849,30 @@ if test "x$have_libheif_1_4_0" = xyes; then fi fi +################### +# Check for JPEG XL +################### + +AC_ARG_WITH(jpegxl, [ --without-jpegxl build without JPEG XL support]) + +have_jpegxl=no +if test "x$with_jpegxl" != xno; then + PKG_CHECK_MODULES(JXL, libjxl >= libjxl_required_version, + [ + PKG_CHECK_MODULES(JXL_THREADS, libjxl_threads >= libjxl_required_version, + [have_jpegxl=yes], + [have_jpegxl="no (libjxl_threads not found)"]) + ], + [have_jpegxl="no (libjxl not found)"]) +fi + +if test "x$have_jpegxl" = xyes; then + MIME_TYPES="$MIME_TYPES;image/jxl" + FILE_JPEGXL='file-jpegxl$(EXEEXT)' +fi + +AM_CONDITIONAL(HAVE_JPEGXL, test "x$have_jpegxl" = xyes) +AC_SUBST(FILE_JPEGXL) ######################## # Check for libbacktrace @@ -3213,6 +3240,7 @@ Optional Plug-Ins: Ghostscript: $have_gs Help Browser: $have_webkit JPEG 2000: $have_openjpeg + JPEG XL: $have_jpegxl MNG: $have_libmng OpenEXR: $have_openexr WebP: $have_webp diff --git a/meson.build b/meson.build index 863ad521f3..626e67b46c 100644 --- a/meson.build +++ b/meson.build @@ -802,6 +802,18 @@ if openjpeg.found() MIMEtypes += [ 'image/jp2', 'image/jpeg2000', 'image/jpx', ] endif +jpegxl_minver = '0.5.0' +libjxl = dependency('libjxl', + version: '>='+jpegxl_minver, + required: get_option('jpeg-xl') +) +libjxl_threads = dependency('libjxl_threads', + version: '>='+jpegxl_minver, + required: get_option('jpeg-xl') +) +if libjxl.found() and libjxl_threads.found() + MIMEtypes += 'image/jxl' +endif xmc = dependency('xcursor', required: get_option('xcursor')) if xmc.found() @@ -1774,6 +1786,7 @@ final_message = [ ''' Ghostscript: @0@'''.format(ghostscript.found()), ''' Help Browser: @0@'''.format(webkit.found()), ''' JPEG 2000: @0@'''.format(openjpeg.found()), +''' JPEG XL: @0@'''.format(libjxl.found()), ''' MNG: @0@'''.format(libmng.found()), ''' OpenEXR: @0@'''.format(openexr.found()), ''' WebP: @0@'''.format(webp_found), diff --git a/meson_options.txt b/meson_options.txt index 6e7ae86228..6bb1c69afc 100644 --- a/meson_options.txt +++ b/meson_options.txt @@ -40,6 +40,7 @@ option('ghostscript', type: 'feature', value: 'auto', description: 'Ghosts option('gudev', type: 'feature', value: 'auto', description: 'Gudev support') option('heif', type: 'feature', value: 'auto', description: 'HEIF support') option('jpeg2000', type: 'feature', value: 'auto', description: 'Jpeg-2000 support') +option('jpeg-xl', type: 'feature', value: 'auto', description: 'JPEG XL support') option('mng', type: 'feature', value: 'auto', description: 'Mng support') option('openexr', type: 'feature', value: 'auto', description: 'Openexr support') option('png', type: 'feature', value: 'auto', description: 'PNG support') diff --git a/plug-ins/common/Makefile.am b/plug-ins/common/Makefile.am index 05964bae82..30dc180126 100644 --- a/plug-ins/common/Makefile.am +++ b/plug-ins/common/Makefile.am @@ -79,6 +79,7 @@ file_header_libexecdir = $(gimpplugindir)/plug-ins/file-header file_heif_libexecdir = $(gimpplugindir)/plug-ins/file-heif file_html_table_libexecdir = $(gimpplugindir)/plug-ins/file-html-table file_jp2_load_libexecdir = $(gimpplugindir)/plug-ins/file-jp2-load +file_jpegxl_libexecdir = $(gimpplugindir)/plug-ins/file-jpegxl file_mng_libexecdir = $(gimpplugindir)/plug-ins/file-mng file_pat_libexecdir = $(gimpplugindir)/plug-ins/file-pat file_pcx_libexecdir = $(gimpplugindir)/plug-ins/file-pcx @@ -156,6 +157,7 @@ file_header_libexec_PROGRAMS = file-header file_heif_libexec_PROGRAMS = $(FILE_HEIF) file_html_table_libexec_PROGRAMS = file-html-table file_jp2_load_libexec_PROGRAMS = $(FILE_JP2_LOAD) +file_jpegxl_libexec_PROGRAMS = $(FILE_JPEGXL) file_mng_libexec_PROGRAMS = $(FILE_MNG) file_pat_libexec_PROGRAMS = file-pat file_pcx_libexec_PROGRAMS = file-pcx @@ -204,6 +206,7 @@ EXTRA_PROGRAMS = \ file-aa \ file-heif \ file-jp2-load \ + file-jpegxl \ file-mng \ file-pdf-save \ file-ps \ @@ -821,6 +824,28 @@ file_jp2_load_LDADD = \ $(INTLLIBS) \ $(file_jp2_load_RC) +file_jpegxl_CFLAGS = $(JXL_CFLAGS) + +file_jpegxl_SOURCES = \ + file-jpegxl.c + +file_jpegxl_LDADD = \ + $(libgimpui) \ + $(libgimpwidgets) \ + $(libgimpmodule) \ + $(libgimp) \ + $(libgimpmath) \ + $(libgimpconfig) \ + $(libgimpcolor) \ + $(libgimpbase) \ + $(GTK_LIBS) \ + $(GEGL_LIBS) \ + $(JXL_THREADS_LIBS) \ + $(JXL_LIBS) \ + $(RT_LIBS) \ + $(INTLLIBS) \ + $(file_jpegxl_RC) + file_mng_CFLAGS = $(MNG_CFLAGS) file_mng_SOURCES = \ diff --git a/plug-ins/common/file-jpegxl.c b/plug-ins/common/file-jpegxl.c new file mode 100644 index 0000000000..d87f9ce422 --- /dev/null +++ b/plug-ins/common/file-jpegxl.c @@ -0,0 +1,1132 @@ +/* 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) 2021 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 "libgimp/stdplugins-intl.h" + +#define LOAD_PROC "file-jpegxl-load" +#define SAVE_PROC "file-jpegxl-save" +#define PLUG_IN_BINARY "file-jpegxl" + +typedef struct _JpegXL JpegXL; +typedef struct _JpegXLClass JpegXLClass; + +struct _JpegXL +{ + GimpPlugIn parent_instance; +}; + +struct _JpegXLClass +{ + GimpPlugInClass parent_class; +}; + + +#define JPEGXL_TYPE (jpegxl_get_type ()) +#define JPEGXL (obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), JPEGXL_TYPE, JpegXL)) + +GType jpegxl_get_type (void) G_GNUC_CONST; + +static GList *jpegxl_query_procedures (GimpPlugIn *plug_in); +static GimpProcedure *jpegxl_create_procedure (GimpPlugIn *plug_in, + const gchar *name); + +static GimpValueArray *jpegxl_load (GimpProcedure *procedure, + GimpRunMode run_mode, + GFile *file, + const GimpValueArray *args, + gpointer run_data); +static GimpValueArray *jpegxl_save (GimpProcedure *procedure, + GimpRunMode run_mode, + GimpImage *image, + gint n_drawables, + GimpDrawable **drawables, + GFile *file, + const GimpValueArray *args, + gpointer run_data); + + +G_DEFINE_TYPE (JpegXL, jpegxl, GIMP_TYPE_PLUG_IN) + +GIMP_MAIN (JPEGXL_TYPE) + +static void +jpegxl_class_init (JpegXLClass *klass) +{ + GimpPlugInClass *plug_in_class = GIMP_PLUG_IN_CLASS (klass); + + plug_in_class->query_procedures = jpegxl_query_procedures; + plug_in_class->create_procedure = jpegxl_create_procedure; +} + +static void +jpegxl_init (JpegXL *jpeg_xl) +{ +} + +static GList * +jpegxl_query_procedures (GimpPlugIn *plug_in) +{ + GList *list = NULL; + + list = g_list_append (list, g_strdup (LOAD_PROC)); + list = g_list_append (list, g_strdup (SAVE_PROC)); + + return list; +} + +static GimpProcedure * +jpegxl_create_procedure (GimpPlugIn *plug_in, + const gchar *name) +{ + GimpProcedure *procedure = NULL; + + if (! strcmp (name, LOAD_PROC)) + { + procedure = gimp_load_procedure_new (plug_in, name, + GIMP_PDB_PROC_TYPE_PLUGIN, + jpegxl_load, NULL, NULL); + + gimp_procedure_set_menu_label (procedure, N_("JPEG-XL image")); + + gimp_procedure_set_documentation (procedure, + _("Loads files in the JPEG-XL file format"), + _("Loads files in the JPEG-XL file format"), + name); + gimp_procedure_set_attribution (procedure, + "Daniel Novomesky", + "(C) 2021 Daniel Novomesky", + "2021"); + + gimp_file_procedure_set_mime_types (GIMP_FILE_PROCEDURE (procedure), + "image/jxl"); + gimp_file_procedure_set_extensions (GIMP_FILE_PROCEDURE (procedure), + "jxl"); + gimp_file_procedure_set_magics (GIMP_FILE_PROCEDURE (procedure), + "0,string,\xFF\x0A,3,string,\x0CJXL"); + + } + else if (! strcmp (name, SAVE_PROC)) + { + procedure = gimp_save_procedure_new (plug_in, name, + GIMP_PDB_PROC_TYPE_PLUGIN, + jpegxl_save, NULL, NULL); + + gimp_procedure_set_image_types (procedure, "RGB*, GRAY*"); + + gimp_procedure_set_menu_label (procedure, N_("JPEG-XL image")); + + gimp_procedure_set_documentation (procedure, + _("Saves files in the JPEG-XL file format"), + _("Saves files in the JPEG-XL file format"), + name); + gimp_procedure_set_attribution (procedure, + "Daniel Novomesky", + "(C) 2021 Daniel Novomesky", + "2021"); + + gimp_file_procedure_set_format_name (GIMP_FILE_PROCEDURE (procedure), + "JPEG-XL"); + gimp_file_procedure_set_mime_types (GIMP_FILE_PROCEDURE (procedure), + "image/jxl"); + gimp_file_procedure_set_extensions (GIMP_FILE_PROCEDURE (procedure), + "jxl"); + + GIMP_PROC_ARG_BOOLEAN (procedure, "lossless", + _("L_ossless"), + _("Use lossless compression"), + FALSE, + G_PARAM_READWRITE); + + GIMP_PROC_ARG_DOUBLE (procedure, "compression", + _("Co_mpression/maxError"), + _("Max. butteraugli distance, lower = higher quality. Range: 0 .. 15. 1.0 = visually lossless."), + 0, 15, 1, + G_PARAM_READWRITE); + + GIMP_PROC_AUX_ARG_INT (procedure, "speed", + _("Effort/S_peed"), + _("Encoder effort setting"), + 3, 9, + 7, + G_PARAM_READWRITE); + + GIMP_PROC_ARG_BOOLEAN (procedure, "uses-original-profile", + _("Save ori_ginal profile"), + _("Store ICC profile to exported JXL file"), + FALSE, + G_PARAM_READWRITE); + } + + return procedure; +} + +static GimpImage *load_image (GFile *file, + GimpRunMode runmode, + GError **error) +{ + gchar *filename = g_file_get_path (file); + + 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 result_size; + gpointer picture_buffer; + + GimpImage *image; + GimpLayer *layer; + GeglBuffer *buffer; + + if (!inputFile) + { + g_set_error (error, G_FILE_ERROR, 0, + "Cannot open file for read: %s\n", filename); + g_free (filename); + return NULL; + } + + 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); + g_free (filename); + return NULL; + } + + 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); + + g_free (filename); + return NULL; + } + + 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); + g_free (filename); + return NULL; + } + + decoder = JxlDecoderCreate (NULL); + if (!decoder) + { + g_set_error (error, G_FILE_ERROR, 0, + "ERROR: JxlDecoderCreate failed"); + g_free (memory); + g_free (filename); + return NULL; + } + + runner = JxlThreadParallelRunnerCreate (NULL, gimp_get_num_processors ()); + 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); + g_free (filename); + return NULL; + } + + 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); + g_free (filename); + return NULL; + } + + 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); + g_free (filename); + return NULL; + } + + 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); + g_free (filename); + return NULL; + } + + 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); + g_free (filename); + return NULL; + } + + 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); + g_free (filename); + return NULL; + } + + 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); + g_free (filename); + return NULL; + } + + 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); + g_free (filename); + return NULL; + } + + 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; + pixel_format.data_type = JXL_TYPE_FLOAT; + + 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 = 4 * 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); + g_free (filename); + return NULL; + } + + picture_buffer = g_malloc (result_size); + 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); + g_free (filename); + return NULL; + } + + 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); + g_free (filename); + return NULL; + } + + + if (basicinfo.num_color_channels == 1) /* grayscale */ + { + image = gimp_image_new_with_precision (basicinfo.xsize, basicinfo.ysize, GIMP_GRAY, loadlinear ? GIMP_PRECISION_FLOAT_LINEAR : GIMP_PRECISION_FLOAT_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 ? GIMP_PRECISION_FLOAT_LINEAR : GIMP_PRECISION_FLOAT_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, NULL, 0); + + buffer = gimp_drawable_get_buffer (GIMP_DRAWABLE (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); + } + JxlThreadParallelRunnerDestroy (runner); + JxlDecoderDestroy (decoder); + g_free (memory); + g_free (filename); + return image; +} + +static GimpValueArray * +jpegxl_load (GimpProcedure *procedure, + GimpRunMode run_mode, + GFile *file, + const GimpValueArray *args, + gpointer run_data) +{ + GimpValueArray *return_vals; + GimpImage *image; + GError *error = NULL; + + INIT_I18N (); + gegl_init (NULL, NULL); + + switch (run_mode) + { + case GIMP_RUN_INTERACTIVE: + case GIMP_RUN_WITH_LAST_VALS: + gimp_ui_init (PLUG_IN_BINARY); + break; + + default: + break; + } + + image = load_image (file, run_mode, &error); + + if (! image) + { + return gimp_procedure_new_return_values (procedure, + GIMP_PDB_EXECUTION_ERROR, + error); + } + return_vals = gimp_procedure_new_return_values (procedure, + GIMP_PDB_SUCCESS, + NULL); + + GIMP_VALUES_SET_IMAGE (return_vals, 1, image); + + return return_vals; +} + + +static gboolean save_image (GFile *file, + GimpProcedureConfig *config, + GimpImage *image, + GimpDrawable *drawable, + GError **error) +{ + JxlEncoder *encoder; + void *runner; + JxlEncoderOptions *encoder_options; + JxlPixelFormat pixel_format; + JxlBasicInfo output_info; + JxlColorEncoding color_profile; + JxlEncoderStatus status; + size_t buffer_size; + + GByteArray *compressed; + + gchar *filename; + 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; + + gdouble compression = 1.0; + gboolean lossless = FALSE; + gint speed = 7; + gboolean uses_original_profile = FALSE; + + filename = g_file_get_path (file); + gimp_progress_init_printf ("Exporting '%s'.", filename); + + g_object_get (config, + "lossless", &lossless, + "compression", &compression, + "speed", &speed, + "uses-original-profile", &uses_original_profile, + NULL); + + drawable_type = gimp_drawable_type (drawable); + drawable_width = gimp_drawable_get_width (drawable); + drawable_height = gimp_drawable_get_height (drawable); + + memset (&output_info, 0, sizeof output_info); + + if (uses_original_profile) + { + 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_free (filename); + return FALSE; + } + } + else + { + space = babl_space ("sRGB"); + out_linear = FALSE; + } + + pixel_format.data_type = JXL_TYPE_UINT16; + pixel_format.endianness = JXL_NATIVE_ENDIAN; + pixel_format.align = 0; + + output_info.xsize = drawable_width; + output_info.ysize = drawable_height; + output_info.bits_per_sample = 16; + output_info.exponent_bits_per_sample = 0; + output_info.intensity_target = 255.0f; + 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 (uses_original_profile && out_linear) + { + file_format = babl_format ("YA u16"); + JxlColorEncodingSetToLinearSRGB (&color_profile, JXL_TRUE); + } + else + { + file_format = babl_format ("Y'A u16"); + JxlColorEncodingSetToSRGB (&color_profile, JXL_TRUE); + } + pixel_format.num_channels = 2; + output_info.num_color_channels = 1; + output_info.alpha_bits = 16; + 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 ("Y u16"); + JxlColorEncodingSetToLinearSRGB (&color_profile, JXL_TRUE); + } + else + { + file_format = babl_format ("Y' u16"); + 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: + file_format = babl_format_with_space (out_linear ? "RGBA u16" : "R'G'B'A u16", space); + pixel_format.num_channels = 4; + JxlColorEncodingSetToSRGB (&color_profile, JXL_FALSE); + output_info.num_color_channels = 3; + output_info.alpha_bits = 16; + output_info.alpha_exponent_bits = 0; + output_info.num_extra_channels = 1; + break; + case GIMP_RGB_IMAGE: + file_format = babl_format_with_space (out_linear ? "RGB u16" : "R'G'B' u16", 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); + } + g_free (filename); + return FALSE; + break; + } + + + buffer_size = 2 * 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); + + buffer = gimp_drawable_get_buffer (drawable); + 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); + } + g_free (filename); + return FALSE; + } + + runner = JxlThreadParallelRunnerCreate (NULL, gimp_get_num_processors ()); + 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); + } + g_free (filename); + 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); + } + g_free (filename); + return FALSE; + } + + if (uses_original_profile) + { + 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); + g_free (filename); + } + } + 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); + g_free (filename); + return FALSE; + } + } + + encoder_options = JxlEncoderOptionsCreate (encoder, NULL); + + if (lossless) + { + JxlEncoderOptionsSetDistance (encoder_options, 0); + JxlEncoderOptionsSetLossless (encoder_options, JXL_TRUE); + } + else + { + JxlEncoderOptionsSetDistance (encoder_options, compression); + JxlEncoderOptionsSetLossless (encoder_options, JXL_FALSE); + } + + status = JxlEncoderOptionsSetEffort (encoder_options, speed); + if (status != JXL_ENC_SUCCESS) + { + g_printerr ("JxlEncoderOptionsSetEffort failed to set effort %d", speed); + } + + 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); + g_free (filename); + return FALSE; + } + + 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); + g_free (filename); + 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 (filename, "wb"); + if (!outfile) + { + g_set_error (error, G_FILE_ERROR, 0, + "Could not open '%s' for writing!\n", filename); + g_free (filename); + g_byte_array_free (compressed, TRUE); + return FALSE; + } + + g_free (filename); + 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); + g_free (filename); + return FALSE; +} + +static gboolean +save_dialog (GimpImage *image, + GimpProcedure *procedure, + GObject *config) +{ + GtkWidget *dialog; + GtkListStore *store; + gboolean run; + + dialog = gimp_save_procedure_dialog_new (GIMP_SAVE_PROCEDURE (procedure), + GIMP_PROCEDURE_CONFIG (config), + image); + + gimp_procedure_dialog_get_widget (GIMP_PROCEDURE_DIALOG (dialog), + "lossless", GTK_TYPE_CHECK_BUTTON); + + gimp_procedure_dialog_get_widget (GIMP_PROCEDURE_DIALOG (dialog), + "compression", GIMP_TYPE_SCALE_ENTRY); + + store = gimp_int_store_new (_("falcon (faster)"), 3, + _("cheetah"), 4, + _("hare"), 5, + _("wombat"), 6, + _("squirrel"), 7, + _("kitten"), 8, + _("tortoise (slower)"), 9, + NULL); + + gimp_procedure_dialog_get_int_combo (GIMP_PROCEDURE_DIALOG (dialog), + "speed", GIMP_INT_STORE (store)); + g_object_unref (store); + + gimp_procedure_dialog_get_widget (GIMP_PROCEDURE_DIALOG (dialog), + "uses-original-profile", GTK_TYPE_CHECK_BUTTON); + + gimp_procedure_dialog_fill (GIMP_PROCEDURE_DIALOG (dialog), + "lossless", "compression", + "speed", "uses-original-profile", + NULL); + + run = gimp_procedure_dialog_run (GIMP_PROCEDURE_DIALOG (dialog)); + + gtk_widget_destroy (dialog); + + return run; +} + + +static GimpValueArray * +jpegxl_save (GimpProcedure *procedure, + GimpRunMode run_mode, + GimpImage *image, + gint n_drawables, + GimpDrawable **drawables, + GFile *file, + const GimpValueArray *args, + gpointer run_data) +{ + GimpPDBStatusType status = GIMP_PDB_SUCCESS; + GimpProcedureConfig *config; + GimpExportReturn export = GIMP_EXPORT_CANCEL; + GError *error = NULL; + + INIT_I18N (); + gegl_init (NULL, NULL); + + config = gimp_procedure_create_config (procedure); + gimp_procedure_config_begin_run (config, image, run_mode, args); + + switch (run_mode) + { + case GIMP_RUN_INTERACTIVE: + case GIMP_RUN_WITH_LAST_VALS: + gimp_ui_init (PLUG_IN_BINARY); + + export = gimp_export_image (&image, &n_drawables, &drawables, "JPEG XL", + GIMP_EXPORT_CAN_HANDLE_RGB | + GIMP_EXPORT_CAN_HANDLE_GRAY | + GIMP_EXPORT_CAN_HANDLE_ALPHA); + + if (export == GIMP_EXPORT_CANCEL) + { + return gimp_procedure_new_return_values (procedure, + GIMP_PDB_CANCEL, + NULL); + } + break; + + default: + break; + } + + if (n_drawables < 1) + { + g_set_error (&error, G_FILE_ERROR, 0, + "No drawables to export"); + + return gimp_procedure_new_return_values (procedure, + GIMP_PDB_CALLING_ERROR, + error); + } + + if (run_mode == GIMP_RUN_INTERACTIVE) + { + if (! save_dialog (image, procedure, G_OBJECT (config))) + { + status = GIMP_PDB_CANCEL; + } + } + + if (status == GIMP_PDB_SUCCESS) + { + if (! save_image (file, config, + image, drawables[0], &error)) + { + status = GIMP_PDB_EXECUTION_ERROR; + } + } + + + gimp_procedure_config_end_run (config, status); + g_object_unref (config); + + if (export == GIMP_EXPORT_EXPORT) + { + g_free (drawables); + gimp_image_delete (image); + } + + return gimp_procedure_new_return_values (procedure, status, error); +} diff --git a/plug-ins/common/meson.build b/plug-ins/common/meson.build index 74693a589c..c4b3b5e209 100644 --- a/plug-ins/common/meson.build +++ b/plug-ins/common/meson.build @@ -103,6 +103,13 @@ if openjpeg.found() } endif +if libjxl.found() and libjxl_threads.found() + common_plugins_list += { + 'name': 'file-jpegxl', + 'deps': [ gtk3, gegl, libjxl, libjxl_threads, ], + } +endif + if libmng.found() common_plugins_list += { 'name': 'file-mng', 'deps': [ gtk3, gegl, libmng, libpng, ], diff --git a/plug-ins/common/plugin-defs.pl b/plug-ins/common/plugin-defs.pl index f1999a32df..dd39401b71 100644 --- a/plug-ins/common/plugin-defs.pl +++ b/plug-ins/common/plugin-defs.pl @@ -32,6 +32,7 @@ 'file-heif' => { ui => 1, optional => 1, gegl => 1, libdep => 'GEXIV2:LCMS', libs => 'LIBHEIF_LIBS', cflags => 'LIBHEIF_CFLAGS' }, 'file-html-table' => { ui => 1, gegl => 1 }, 'file-jp2-load' => { ui => 1, optional => 1, gegl => 1, libs => 'OPENJPEG_LIBS', cflags => 'OPENJPEG_CFLAGS' }, + 'file-jpegxl' => { ui => 1, optional => 1, gegl => 1, libdep => 'JXL:JXL_THREADS', cflags => 'JXL_CFLAGS' }, 'file-mng' => { ui => 1, gegl => 1, optional => 1, libs => 'MNG_LIBS', cflags => 'MNG_CFLAGS' }, 'file-pat' => { ui => 1, gegl => 1 }, 'file-pcx' => { ui => 1, gegl => 1 },