From 76c2b81b6077702d3bbd9726b698cd8b6547fc5e Mon Sep 17 00:00:00 2001 From: NRK Date: Sun, 8 Jan 2023 10:02:56 +0000 Subject: [PATCH] add support for multi-frame images via imlib2 (#373) this will be a massive change compared to the usual stuff. however the gains will be worth it: * we gain lots of additional animated image support. * and we'll gain _even_ more format support as imlib2 adds them, without needing any change in our code-base. * about ~300 LoC will be purged once we remove our internal gif and webp loader. as for when to remove the internal loaders, a good time might be when debian upgrades their imlib2, currently it seems to be at v1.7.5, which doesn't support animated images. as of now, nsxiv will continue to build with the internal gif/webp loaders (assuming they were enabled in config.mk) if imlib2 version is below 1.8.0 and will print out a deprecation notice. and if imlib2 version supports multi-frame then it will simply ignore the internal loaders and use the imlib2 one. in other words, users shouldn't need to do anything on their side. everything that previously functioned will continue to function regardless of the user's imlib2 version (though they might see the annoying deprecation notice if the imlib2 version doesn't support multi-frame images). known issue: * image loading performance can be noticeably worse in imlib2 versions below 1.9.0 Closes: https://codeberg.org/nsxiv/nsxiv/issues/301 Closes: https://codeberg.org/nsxiv/nsxiv/issues/300 Reviewed-on: https://codeberg.org/nsxiv/nsxiv/pulls/373 Reviewed-by: TAAPArthur --- config.mk | 5 +- etc/woodpecker/clang-tidy-checks | 2 +- image.c | 186 ++++++++++++++++++++++++++++--- 3 files changed, 176 insertions(+), 17 deletions(-) diff --git a/config.mk b/config.mk index 10fd491..b79a63a 100644 --- a/config.mk +++ b/config.mk @@ -14,8 +14,11 @@ HAVE_INOTIFY = $(OPT_DEP_DEFAULT) # optional dependencies, see README for more info HAVE_LIBFONTS = $(OPT_DEP_DEFAULT) -HAVE_LIBGIF = $(OPT_DEP_DEFAULT) HAVE_LIBEXIF = $(OPT_DEP_DEFAULT) + +# unused if imlib2 version is 1.8.0 or higher. +# these options will be removed eventually. +HAVE_LIBGIF = $(OPT_DEP_DEFAULT) HAVE_LIBWEBP = $(OPT_DEP_DEFAULT) # Compiler and linker diff --git a/etc/woodpecker/clang-tidy-checks b/etc/woodpecker/clang-tidy-checks index c243f30..0cc2693 100644 --- a/etc/woodpecker/clang-tidy-checks +++ b/etc/woodpecker/clang-tidy-checks @@ -1,6 +1,6 @@ # checks clang-analyzer-*,clang-diagnostic-*,bugprone-*,performance-*,modernize-* -misc-*,android-cloexec-*,cert-*,llvm-include-order +misc-*,android-cloexec-*,llvm-include-order -readability-*,readability-duplicate-include,readability-misleading-indentation # silence diff --git a/image.c b/image.c index 418eb60..56d409a 100644 --- a/image.c +++ b/image.c @@ -33,12 +33,21 @@ #include #endif -#if HAVE_LIBGIF +#ifdef IMLIB2_VERSION + #if IMLIB2_VERSION >= IMLIB2_VERSION_(1, 8, 0) + #define HAVE_IMLIB2_MULTI_FRAME 1 + #endif +#endif +#ifndef HAVE_IMLIB2_MULTI_FRAME + #define HAVE_IMLIB2_MULTI_FRAME 0 +#endif + +#if HAVE_LIBGIF && !HAVE_IMLIB2_MULTI_FRAME #include enum { DEF_GIF_DELAY = 75 }; #endif -#if HAVE_LIBWEBP +#if HAVE_LIBWEBP && !HAVE_IMLIB2_MULTI_FRAME #include #include #include @@ -139,7 +148,7 @@ void exif_auto_orientate(const fileinfo_t *file) } #endif -#if HAVE_LIBGIF || HAVE_LIBWEBP +#if HAVE_LIBGIF || HAVE_LIBWEBP || HAVE_IMLIB2_MULTI_FRAME static void img_multiframe_context_set(img_t *img) { if (img->multi.cnt > 1) { @@ -156,7 +165,25 @@ static void img_multiframe_context_set(img_t *img) } #endif -#if HAVE_LIBGIF +#if (HAVE_LIBGIF || HAVE_LIBWEBP) && !HAVE_IMLIB2_MULTI_FRAME +static void img_multiframe_deprecation_notice(void) +{ + static bool warned; + if (!warned) { + error(0, 0, "\n" + "################################################################\n" + "# DEPRECATION NOTICE #\n" + "################################################################\n" + "# Internal multi-frame gif and webp loaders are deprecated and #\n" + "# will be removed soon. Please upgrade to Imlib2 v1.8.0 for #\n" + "# multi-frame/animated image support. #\n" + "################################################################"); + warned = true; + } +} +#endif + +#if HAVE_LIBGIF && !HAVE_IMLIB2_MULTI_FRAME static bool img_load_gif(img_t *img, const fileinfo_t *file) { GifFileType *gif; @@ -177,6 +204,8 @@ static bool img_load_gif(img_t *img, const fileinfo_t *file) bool err = false; multi_img_t *m = &img->multi; + img_multiframe_deprecation_notice(); + #if defined(GIFLIB_MAJOR) && GIFLIB_MAJOR >= 5 gif = DGifOpenFileName(file->path, NULL); #else @@ -321,8 +350,7 @@ static bool img_load_gif(img_t *img, const fileinfo_t *file) } #endif /* HAVE_LIBGIF */ - -#if HAVE_LIBWEBP +#if HAVE_LIBWEBP && !HAVE_IMLIB2_MULTI_FRAME static bool img_load_webp(img_t *img, const fileinfo_t *file) { FILE *webp_file; @@ -340,6 +368,8 @@ static bool img_load_webp(img_t *img, const fileinfo_t *file) bool err = false; multi_img_t *m = &img->multi; + img_multiframe_deprecation_notice(); + if ((webp_file = fopen(file->path, "rb")) == NULL) { error(0, errno, "%s: Error opening webp image", file->name); return false; @@ -408,6 +438,118 @@ fail: } #endif /* HAVE_LIBWEBP */ +#if HAVE_IMLIB2_MULTI_FRAME +static void img_area_clear(int x, int y, int w, int h) +{ + assert(x >= 0 && y >= 0); + assert(w > 0 && h > 0); + imlib_image_set_has_alpha(1); + imlib_context_set_blend(0); + imlib_context_set_color(0, 0, 0, 0); + imlib_image_fill_rectangle(x, y, w, h); +} + +static bool img_load_multiframe(img_t *img, const fileinfo_t *file) +{ + unsigned int n, fcnt; + Imlib_Image blank; + Imlib_Frame_Info finfo; + int px, py, pw, ph, pflag; + multi_img_t *m = &img->multi; + + imlib_context_set_image(img->im); + imlib_image_get_frame_info(&finfo); + if ((fcnt = finfo.frame_count) <= 1 || !(finfo.frame_flags & IMLIB_IMAGE_ANIMATED)) + return false; + img->w = finfo.canvas_w; + img->h = finfo.canvas_h; + + if (fcnt > m->cap) { + m->cap = fcnt; + m->frames = erealloc(m->frames, m->cap * sizeof(*m->frames)); + } + + imlib_context_set_dither(0); + imlib_context_set_anti_alias(0); + imlib_context_set_color_modifier(NULL); + imlib_context_set_operation(IMLIB_OP_COPY); + + if ((blank = imlib_create_image(img->w, img->h)) == NULL) { + error(0, 0, "%s: couldn't create image", file->name); + return false; + } + imlib_context_set_image(blank); + img_area_clear(0, 0, img->w, img->h); + + /* + * Imlib2 gives back a "raw frame", we need to blend it on top of the + * previous frame ourselves if necessary to get the fully decoded frame. + */ + pflag = m->length = m->cnt = m->sel = 0; + px = py = pw = ph = 0; + for (n = 1; n <= fcnt; ++n) { + Imlib_Image frame, canvas; + int sx, sy, sw, sh; + bool has_alpha; + + imlib_context_set_image(m->cnt < 1 ? blank : m->frames[m->cnt - 1].im); + if ((canvas = imlib_clone_image()) == NULL || + (frame = imlib_load_image_frame(file->path, n)) == NULL) + { + if (canvas != NULL) { + imlib_context_set_image(canvas); + imlib_free_image(); + } + error(0, 0, "%s: failed to load frame %d", file->name, n); + break; + } + + imlib_context_set_image(frame); + imlib_image_get_frame_info(&finfo); + assert(finfo.frame_count == (int)fcnt); + assert(finfo.canvas_w == img->w && finfo.canvas_h == img->h); + sx = finfo.frame_x; + sy = finfo.frame_y; + sw = finfo.frame_w; + sh = finfo.frame_h; + has_alpha = imlib_image_has_alpha(); + + imlib_context_set_image(canvas); + /* the dispose flags are explained in Imlib2's header */ + if (pflag & IMLIB_FRAME_DISPOSE_CLEAR) { + img_area_clear(px, py, pw, ph); + } else if (pflag & IMLIB_FRAME_DISPOSE_PREV) { + Imlib_Image p = m->cnt < 2 ? blank : m->frames[m->cnt - 2].im; + assert(m->cnt > 0); + img_area_clear(0, 0, img->w, img->h); + imlib_blend_image_onto_image(p, 1, px, py, pw, ph, px, py, pw, ph); + } + pflag = finfo.frame_flags; + if (pflag & (IMLIB_FRAME_DISPOSE_CLEAR | IMLIB_FRAME_DISPOSE_PREV)) { + /* remember these so we can "dispose" them before blending next frame */ + px = sx; + py = sy; + pw = sw; + ph = sh; + } + assert(imlib_context_get_operation() == IMLIB_OP_COPY); + imlib_image_set_has_alpha(has_alpha); + imlib_context_set_blend(!!(finfo.frame_flags & IMLIB_FRAME_BLEND)); + imlib_blend_image_onto_image(frame, has_alpha, 0, 0, sw, sh, sx, sy, sw, sh); + m->frames[m->cnt].im = canvas; + m->frames[m->cnt].delay = finfo.frame_delay; + m->length += m->frames[m->cnt].delay; + m->cnt++; + imlib_context_set_image(frame); + imlib_free_image(); + } + imlib_context_set_image(blank); + imlib_free_image(); + img_multiframe_context_set(img); + return m->cnt > 0; +} +#endif /* HAVE_IMLIB2_MULTI_FRAME */ + Imlib_Image img_open(const fileinfo_t *file) { struct stat st; @@ -415,7 +557,11 @@ Imlib_Image img_open(const fileinfo_t *file) if (access(file->path, R_OK) == 0 && stat(file->path, &st) == 0 && S_ISREG(st.st_mode) && +#if HAVE_IMLIB2_MULTI_FRAME + (im = imlib_load_image_frame(file->path, 1)) != NULL) +#else (im = imlib_load_image_immediately(file->path)) != NULL) +#endif { imlib_context_set_image(im); } @@ -427,6 +573,7 @@ Imlib_Image img_open(const fileinfo_t *file) bool img_load(img_t *img, const fileinfo_t *file) { const char *fmt; + bool animated = false; if ((img->im = img_open(file)) == NULL) return false; @@ -441,12 +588,16 @@ bool img_load(img_t *img, const fileinfo_t *file) exif_auto_orientate(file); #endif +#if HAVE_IMLIB2_MULTI_FRAME + animated = img_load_multiframe(img, file); +#endif + if ((fmt = imlib_image_format()) != NULL) { /* NOLINT: fmt might be unused, not worth fixing */ -#if HAVE_LIBGIF +#if HAVE_LIBGIF && !HAVE_IMLIB2_MULTI_FRAME if (STREQ(fmt, "gif")) img_load_gif(img, file); #endif -#if HAVE_LIBWEBP +#if HAVE_LIBWEBP && !HAVE_IMLIB2_MULTI_FRAME if (STREQ(fmt, "webp")) img_load_webp(img, file); #endif @@ -455,8 +606,13 @@ bool img_load(img_t *img, const fileinfo_t *file) exif_auto_orientate(file); #endif } - img->w = imlib_image_get_width(); - img->h = imlib_image_get_height(); + /* for animated images, we want the _canvas_ width/height, which + * img_load_multiframe() sets already. + */ + if (!animated) { + img->w = imlib_image_get_width(); + img->h = imlib_image_get_height(); + } img->checkpan = true; img->dirty = true; @@ -466,20 +622,18 @@ bool img_load(img_t *img, const fileinfo_t *file) CLEANUP void img_close(img_t *img, bool decache) { unsigned int i; + void (*free_img)(void) = decache ? imlib_free_image_and_decache : imlib_free_image; if (img->multi.cnt > 0) { for (i = 0; i < img->multi.cnt; i++) { imlib_context_set_image(img->multi.frames[i].im); - imlib_free_image(); + free_img(); } img->multi.cnt = 0; img->im = NULL; } else if (img->im != NULL) { imlib_context_set_image(img->im); - if (decache) - imlib_free_image_and_decache(); - else - imlib_free_image(); + free_img(); img->im = NULL; } } @@ -629,6 +783,8 @@ void img_render(img_t *img) imlib_context_set_color(c.red >> 8, c.green >> 8, c.blue >> 8, 0xFF); imlib_image_fill_rectangle(0, 0, dw, dh); } + imlib_context_set_blend(1); + imlib_context_set_operation(IMLIB_OP_COPY); imlib_blend_image_onto_image(img->im, 0, sx, sy, sw, sh, 0, 0, dw, dh); imlib_context_set_color_modifier(NULL); imlib_render_image_on_drawable(dx, dy);