diff --git a/AUTHORS b/AUTHORS index c1ce951..fefde3b 100644 --- a/AUTHORS +++ b/AUTHORS @@ -7,7 +7,7 @@ git log. For a complete list, you could try: git log --pretty="format:%an <%ae>" | sort -f | uniq -Per 2024-01-08, this yields the following (sans duplicates): +Per 2024-01-23, this yields the following (sans duplicates): alkahest begasus @@ -35,3 +35,4 @@ Sudhakar Verma Tim Gates Wu Zhenyu Øyvind Kolås +oupson diff --git a/configure.ac b/configure.ac index 89f623f..3e85d7a 100644 --- a/configure.ac +++ b/configure.ac @@ -156,6 +156,18 @@ AS_IF([test "$with_tools" != no], [ missing_debs="$missing_debs libwebp-dev" with_webp=no)]) AS_IF([test "$with_webp" != no], [AC_DEFINE([HAVE_WEBP], [1], [Define if we have WebP support.])]) + + dnl libjxl (optional) + AC_ARG_WITH(jxl, + [AS_HELP_STRING([--without-jxl], [don't build JXL loader [default=on]])], + , + with_jxl=yes) + AS_IF([test "$with_jxl" != no], [ + PKG_CHECK_MODULES(JXL, [libjxl libjxl_threads],, + missing_rpms="$missing_rpms libjxl-devel" + missing_debs="$missing_debs libjxl-dev" + with_jxl=no)]) + AS_IF([test "$with_jxl" != no], [AC_DEFINE([HAVE_JXL], [1], [Define if we have JXL support.])]) ]) AM_CONDITIONAL([WANT_TOOLS], [test "$with_tools" != no]) @@ -164,6 +176,7 @@ AM_CONDITIONAL([HAVE_SVG], [test "$with_tools" != no -a "$with_svg" != no]) AM_CONDITIONAL([HAVE_TIFF], [test "$with_tools" != no -a "$with_tiff" != no]) AM_CONDITIONAL([HAVE_WEBP], [test "$with_tools" != no -a "$with_webp" != no]) AM_CONDITIONAL([HAVE_AVIF], [test "$with_tools" != no -a "$with_avif" != no]) +AM_CONDITIONAL([HAVE_JXL], [test "$with_tools" != no -a "$with_jxl" != no]) # Used by gtk-doc's fixxref. GLIB_PREFIX="`$PKG_CONFIG --variable=prefix glib-2.0`" @@ -492,6 +505,7 @@ colorize_vars=" with_svg with_tiff with_webp + with_jxl with_avif " @@ -556,6 +570,7 @@ echo >&AS_MESSAGE_FD "With QOI loader ............. $pyes (internal)" echo >&AS_MESSAGE_FD "With SVG loader ............. $pwith_svg" echo >&AS_MESSAGE_FD "With TIFF loader ............ $pwith_tiff" echo >&AS_MESSAGE_FD "With WebP loader ............ $pwith_webp" +echo >&AS_MESSAGE_FD "With JXL loader ............. $pwith_jxl" echo >&AS_MESSAGE_FD "With XWD loader ............. $pyes (internal)" fi diff --git a/tools/chafa/Makefile.am b/tools/chafa/Makefile.am index ca69601..1672da7 100644 --- a/tools/chafa/Makefile.am +++ b/tools/chafa/Makefile.am @@ -57,17 +57,23 @@ chafa_SOURCES += \ webp-loader.h endif +if HAVE_JXL +chafa_SOURCES += \ + jxl-loader.c \ + jxl-loader.h +endif + # We can pass -rpath so the binary knows where to find libchafa.so when # installed outside /usr (e.g. the default /usr/local). This affects Ubuntu. # Resolved by running ldconfig. See Github issue #32. # # This is disabled by default. -chafa_CFLAGS = $(CHAFA_CFLAGS) $(GLIB_CFLAGS) $(JPEG_CFLAGS) $(SVG_CFLAGS) $(TIFF_CFLAGS) $(WEBP_CFLAGS) $(AVIF_CFLAGS) $(FREETYPE_CFLAGS) +chafa_CFLAGS = $(CHAFA_CFLAGS) $(GLIB_CFLAGS) $(JPEG_CFLAGS) $(SVG_CFLAGS) $(TIFF_CFLAGS) $(WEBP_CFLAGS) $(JXL_CFLAGS) $(AVIF_CFLAGS) $(FREETYPE_CFLAGS) if ENABLE_RPATH chafa_LDFLAGS = $(CHAFA_LDFLAGS) -rpath $(libdir) endif -chafa_LDADD = $(GLIB_LIBS) $(JPEG_LIBS) $(SVG_LIBS) $(TIFF_LIBS) $(WEBP_LIBS) $(AVIF_LIBS) $(FREETYPE_LIBS) $(top_builddir)/chafa/libchafa.la $(top_builddir)/libnsgif/libnsgif.la $(top_builddir)/lodepng/liblodepng.la $(WIN32_LDADD) +chafa_LDADD = $(GLIB_LIBS) $(JPEG_LIBS) $(SVG_LIBS) $(TIFF_LIBS) $(WEBP_LIBS) $(JXL_LIBS) $(AVIF_LIBS) $(FREETYPE_LIBS) $(top_builddir)/chafa/libchafa.la $(top_builddir)/libnsgif/libnsgif.la $(top_builddir)/lodepng/liblodepng.la $(WIN32_LDADD) # On Microsoft Windows, we compile a resource file with windres and link it in. # This enables UTF-8 support in filenames, environment variables, etc. diff --git a/tools/chafa/jxl-loader.c b/tools/chafa/jxl-loader.c new file mode 100644 index 0000000..a907b50 --- /dev/null +++ b/tools/chafa/jxl-loader.c @@ -0,0 +1,300 @@ +/* -*- Mode: C; tab-width: 4; ent su: nil; c-basic-offset: 4 -*- */ + +/* Copyright (C) 2024 oupson + * + * This file is part of Chafa, a program that shows pictures on text terminals. + * + * Chafa is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Chafa 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 Lesser General Public License + * along with Chafa. If not, see . */ + +#include "chafa.h" +#include "file-mapping.h" +#include "glib.h" +#include +#include +#include +#include + +#include "jxl-loader.h" + +#define BUFFER_SIZE (1024) + +GList *jxl_get_frames (JxlDecoder *dec, JxlParallelRunner *runner, FileMapping *mapping); + +void jxl_cleanup_frame_list (GList *frame_list); + +struct JxlLoader +{ + GList *frames; + int frame_count; + int index; +}; + +typedef struct JxlFrame +{ + uint8_t *buffer; + int width; + int height; + gboolean have_alpha; + gboolean is_premul; + int frame_duration; +} JxlFrame; + +static JxlLoader * +jxl_loader_new (void) +{ + return g_new0 (JxlLoader, 1); +} + +GList * +jxl_get_frames (JxlDecoder *dec, JxlParallelRunner *runner, FileMapping *mapping) +{ + if (JXL_DEC_SUCCESS + != JxlDecoderSetParallelRunner (dec, JxlResizableParallelRunner, runner)) + { + return NULL; + } + + if (JXL_DEC_SUCCESS + != JxlDecoderSubscribeEvents (dec, JXL_DEC_BASIC_INFO | JXL_DEC_FULL_IMAGE + | JXL_DEC_FRAME)) + { + return NULL; + } + + JxlPixelFormat format = { 4, JXL_TYPE_UINT8, JXL_NATIVE_ENDIAN, 0 }; + + JxlDecoderStatus decode_status = JXL_DEC_NEED_MORE_INPUT; + uint8_t buffer [BUFFER_SIZE]; + + gpointer image_buffer = NULL; + GList *frame_list = NULL; + JxlBasicInfo info; + JxlFrameHeader frame_header; + + int read = 0; + + while (JXL_DEC_ERROR != decode_status) + { + if (JXL_DEC_NEED_MORE_INPUT == decode_status) + { + JxlDecoderReleaseInput (dec); + const gsize nbr = file_mapping_read (mapping, buffer, read, BUFFER_SIZE); + read += nbr; + if (nbr > 0) + { + if (JXL_DEC_SUCCESS != JxlDecoderSetInput (dec, buffer, nbr)) + { + break; + } + } + else + { + break; + } + } + else if (JXL_DEC_BASIC_INFO == decode_status) + { + if (JXL_DEC_SUCCESS != JxlDecoderGetBasicInfo (dec, &info)) + { + break; + } + if (!info.alpha_bits) + { + format.num_channels = 3; + } + image_buffer = g_malloc (info.xsize * info.ysize * format.num_channels); + } + else if (JXL_DEC_NEED_IMAGE_OUT_BUFFER == decode_status) + { + if (JXL_DEC_SUCCESS + != JxlDecoderSetImageOutBuffer (dec, &format, image_buffer, + info.xsize * info.ysize + * format.num_channels)) + { + break; + } + } + else if (JXL_DEC_FRAME == decode_status) + { + if (JXL_DEC_SUCCESS != JxlDecoderGetFrameHeader (dec, &frame_header)) + { + break; + } + } + else if (JXL_DEC_FULL_IMAGE == decode_status) + { + const uint32_t num + = info.animation.tps_numerator == 0 ? 1 : info.animation.tps_numerator; + JxlFrame *frame = g_new (JxlFrame, 1); + frame->buffer = image_buffer; + frame->width = info.xsize; + frame->height = info.ysize; + frame->have_alpha = format.num_channels == 4; + frame->is_premul = info.alpha_premultiplied; + frame->frame_duration + = frame_header.duration * 1000 * info.animation.tps_denominator / num; + frame_list = g_list_prepend (frame_list, frame); + + image_buffer = g_malloc (info.xsize * info.ysize * format.num_channels); + } + else if (JXL_DEC_SUCCESS == decode_status) + { + frame_list = g_list_reverse (frame_list); + return frame_list; + } + + decode_status = JxlDecoderProcessInput (dec); + } + + // Decoding failed + if (image_buffer != NULL) + { + g_free (image_buffer); + } + + if (frame_list != NULL) + { + jxl_cleanup_frame_list (frame_list); + g_list_free (frame_list); + } + + return NULL; +} + +JxlLoader * +jxl_loader_new_from_mapping (FileMapping *mapping) +{ + JxlLoader *loader = NULL; + gboolean success = FALSE; + + g_return_val_if_fail (mapping != NULL, NULL); + + if (!file_mapping_has_magic (mapping, 0, "\xFF\x0A", 2) + && !file_mapping_has_magic ( + mapping, 0, "\x00\x00\x00\x0C\x4A\x58\x4C\x20\x0D\x0A\x87\x0A", 12)) + { + return NULL; + } + + loader = jxl_loader_new (); + + JxlDecoder *decoder = JxlDecoderCreate (NULL); + JxlParallelRunner *runner = JxlResizableParallelRunnerCreate (NULL); + + GList *frames = jxl_get_frames (decoder, runner, mapping); + + JxlDecoderDestroy (decoder); + JxlResizableParallelRunnerDestroy (runner); + + success = frames != NULL; + + if (success) + { + loader->frames = frames; + loader->frame_count = g_list_length (frames); + loader->index = 0; + } + else + { + if (loader) + { + g_free (loader); + loader = NULL; + } + } + + return loader; +}; + +void +jxl_cleanup_frame_list (GList *frame_list) +{ + GList *frame = frame_list; + + while (frame != NULL) + { + if (frame->data != NULL) + { + JxlFrame *jxl_frame = frame->data; + g_free (jxl_frame->buffer); + g_free (jxl_frame); + frame->data = NULL; + } + frame = frame->next; + } +} + +void +jxl_loader_destroy (JxlLoader *loader) +{ + GList *list = loader->frames; + jxl_cleanup_frame_list (list); + g_list_free (loader->frames); + g_free (loader); +} + +gboolean +jxl_loader_get_is_animation (JxlLoader *loader) +{ + return loader->frame_count > 1; +} + +gconstpointer +jxl_loader_get_frame_data (JxlLoader *loader, ChafaPixelType *pixel_type_out, + gint *width_out, gint *height_out, gint *rowstride_out) +{ + g_return_val_if_fail (loader != NULL, NULL); + const JxlFrame *frame = g_list_nth_data (loader->frames, loader->index); + + int num_channels; + if (frame->have_alpha) + { + num_channels = 4; + *pixel_type_out = frame->is_premul ? CHAFA_PIXEL_RGBA8_PREMULTIPLIED : + CHAFA_PIXEL_RGBA8_UNASSOCIATED; + } + else + { + num_channels = 3; + *pixel_type_out = CHAFA_PIXEL_RGB8; + } + + *width_out = frame->width; + *height_out = frame->height; + *rowstride_out = frame->width * num_channels; + + return frame->buffer; +} + +gint +jxl_loader_get_frame_delay (JxlLoader *loader) +{ + g_return_val_if_fail (loader != NULL, 0); + return ((JxlFrame *) g_list_nth_data (loader->frames, loader->index))->frame_duration; +} + +void +jxl_loader_goto_first_frame (JxlLoader *loader) +{ + g_return_if_fail (loader != NULL); + loader->index = 0; +} + +gboolean +jxl_loader_goto_next_frame (JxlLoader *loader) +{ + g_return_val_if_fail (loader != NULL, FALSE); + loader->index = loader->index + 1; + return loader->index < loader->frame_count; +} diff --git a/tools/chafa/jxl-loader.h b/tools/chafa/jxl-loader.h new file mode 100644 index 0000000..4c4eae8 --- /dev/null +++ b/tools/chafa/jxl-loader.h @@ -0,0 +1,45 @@ +/* -*- Mode: C; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ + +/* Copyright (C) 2024 oupson + * + * This file is part of Chafa, a program that shows pictures on text terminals. + * + * Chafa is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Chafa 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 Lesser General Public License + * along with Chafa. If not, see . */ + +#ifndef __JXL_LOADER_H__ +#define __JXL_LOADER_H__ + +#include "file-mapping.h" +#include + +G_BEGIN_DECLS + +typedef struct JxlLoader JxlLoader; + +JxlLoader *jxl_loader_new_from_mapping (FileMapping *mapping); +void jxl_loader_destroy (JxlLoader *loader); + +gboolean jxl_loader_get_is_animation (JxlLoader *loader); + +gconstpointer jxl_loader_get_frame_data (JxlLoader *loader, + ChafaPixelType *pixel_type_out, gint *width_out, + gint *height_out, gint *rowstride_out); +gint jxl_loader_get_frame_delay (JxlLoader *loader); + +void jxl_loader_goto_first_frame (JxlLoader *loader); +gboolean jxl_loader_goto_next_frame (JxlLoader *loader); + +G_END_DECLS + +#endif /* __JXL_LOADER_H__ */ \ No newline at end of file diff --git a/tools/chafa/media-loader.c b/tools/chafa/media-loader.c index 277c561..a36d652 100644 --- a/tools/chafa/media-loader.c +++ b/tools/chafa/media-loader.c @@ -40,6 +40,7 @@ #include "tiff-loader.h" #include "webp-loader.h" #include "avif-loader.h" +#include "jxl-loader.h" typedef enum { @@ -52,6 +53,7 @@ typedef enum LOADER_TYPE_WEBP, LOADER_TYPE_AVIF, LOADER_TYPE_SVG, + LOADER_TYPE_JXL, LOADER_TYPE_LAST } @@ -189,6 +191,20 @@ loader_vtable [LOADER_TYPE_LAST] = (gint (*) (gpointer)) avif_loader_get_frame_delay }, #endif +#ifdef HAVE_JXL + [LOADER_TYPE_JXL] = + { + "JXL", + (gpointer (*)(gpointer)) jxl_loader_new_from_mapping, + (gpointer (*)(gconstpointer)) NULL, + (void (*)(gpointer)) jxl_loader_destroy, + (gboolean (*)(gpointer)) jxl_loader_get_is_animation, + (void (*)(gpointer)) jxl_loader_goto_first_frame, + (gboolean (*)(gpointer)) jxl_loader_goto_next_frame, + (gconstpointer (*) (gpointer, gpointer, gpointer, gpointer, gpointer)) jxl_loader_get_frame_data, + (gint (*) (gpointer)) jxl_loader_get_frame_delay + }, +#endif }; struct MediaLoader