[gimp] Add JPEG XL plug-in
- From: Jehan <jehanp src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [gimp] Add JPEG XL plug-in
- Date: Mon, 27 Sep 2021 10:14:52 +0000 (UTC)
commit 7ce0f2d60e6002f2eb2cfee56adbc0332ab2ff1f
Author: Daniel Novomesky <dnovomesky gmail com>
Date: Thu Aug 26 14:24:21 2021 +0200
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(+)
---
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 <https://www.gnu.org/licenses/>.
+ */
+
+#include "config.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"
+
+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 },
[
Date Prev][
Date Next] [
Thread Prev][
Thread Next]
[
Thread Index]
[
Date Index]
[
Author Index]