Implement trivial wide thumbnail production
Also make libwebp a required dependency.
This commit is contained in:
		
							
								
								
									
										12
									
								
								README.adoc
									
									
									
									
									
								
							
							
						
						
									
										12
									
								
								README.adoc
									
									
									
									
									
								
							@@ -1,9 +1,9 @@
 | 
			
		||||
fastiv
 | 
			
		||||
======
 | 
			
		||||
 | 
			
		||||
'fastiv' is a fast image viewer, supporting BMP, PNG, GIF, JPEG, and optionally
 | 
			
		||||
raw photos, HEIC, AVIF, WebP, SVG, X11 cursors and TIFF, or whatever gdk-pixbuf
 | 
			
		||||
loads.
 | 
			
		||||
'fastiv' is a fast image viewer, directly supporting BMP, PNG, GIF, JPEG, WebP,
 | 
			
		||||
and optionally raw photos, HEIC, AVIF, SVG, X11 cursors and TIFF,
 | 
			
		||||
or whatever gdk-pixbuf loads.
 | 
			
		||||
 | 
			
		||||
Its development status can be summarized as '`beta`'.
 | 
			
		||||
E.g., certain GIFs animate wrong.
 | 
			
		||||
@@ -25,9 +25,9 @@ Building and Running
 | 
			
		||||
--------------------
 | 
			
		||||
Build dependencies: Meson, pkg-config +
 | 
			
		||||
Runtime dependencies: gtk+-3.0, glib>=2.64, pixman-1, shared-mime-info,
 | 
			
		||||
spng>=0.7.0, libturbojpeg +
 | 
			
		||||
Optional dependencies: lcms2, LibRaw, librsvg-2.0, xcursor, libwebp, libheif,
 | 
			
		||||
libtiff, gdk-pixbuf-2.0, ExifTool
 | 
			
		||||
libturbojpeg, libwebp, spng>=0.7.0 +
 | 
			
		||||
Optional dependencies: lcms2, LibRaw, librsvg-2.0, xcursor, libheif, libtiff,
 | 
			
		||||
gdk-pixbuf-2.0, ExifTool
 | 
			
		||||
 | 
			
		||||
 $ git clone --recursive https://git.janouch.name/p/fastiv.git
 | 
			
		||||
 $ meson builddir
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										33
									
								
								fastiv.c
									
									
									
									
									
								
							
							
						
						
									
										33
									
								
								fastiv.c
									
									
									
									
									
								
							@@ -1191,7 +1191,7 @@ main(int argc, char *argv[])
 | 
			
		||||
{
 | 
			
		||||
	gboolean show_version = FALSE, show_supported_media_types = FALSE,
 | 
			
		||||
		browse = FALSE;
 | 
			
		||||
	gchar **path_args = NULL;
 | 
			
		||||
	gchar **path_args = NULL, *thumbnail_size = NULL;
 | 
			
		||||
	const GOptionEntry options[] = {
 | 
			
		||||
		{G_OPTION_REMAINING, 0, 0, G_OPTION_ARG_FILENAME_ARRAY, &path_args,
 | 
			
		||||
			NULL, "[FILE | DIRECTORY]"},
 | 
			
		||||
@@ -1201,6 +1201,9 @@ main(int argc, char *argv[])
 | 
			
		||||
		{"browse", 0, G_OPTION_FLAG_IN_MAIN,
 | 
			
		||||
			G_OPTION_ARG_NONE, &browse,
 | 
			
		||||
			"Start in filesystem browsing mode", NULL},
 | 
			
		||||
		{"thumbnail", 0, G_OPTION_FLAG_IN_MAIN,
 | 
			
		||||
			G_OPTION_ARG_STRING, &thumbnail_size,
 | 
			
		||||
			"Generate thumbnails for an image, up to the given size", "SIZE"},
 | 
			
		||||
		{"version", 'V', G_OPTION_FLAG_IN_MAIN, G_OPTION_ARG_NONE,
 | 
			
		||||
			&show_version, "Output version information and exit", NULL},
 | 
			
		||||
		{},
 | 
			
		||||
@@ -1224,9 +1227,29 @@ main(int argc, char *argv[])
 | 
			
		||||
	// NOTE: Firefox and Eye of GNOME both interpret multiple arguments
 | 
			
		||||
	// in a special way. This is problematic, because one-element lists
 | 
			
		||||
	// are unrepresentable.
 | 
			
		||||
	// TODO(p): Require a command line switch, load a virtual folder.
 | 
			
		||||
	// We may want or need to create a custom GVfs.
 | 
			
		||||
	// TODO(p): Complain to the user if there's more than one argument.
 | 
			
		||||
	// Best show the help message, if we can figure that out.
 | 
			
		||||
	const gchar *path_arg = path_args ? path_args[0] : NULL;
 | 
			
		||||
	if (thumbnail_size) {
 | 
			
		||||
		if (!path_arg)
 | 
			
		||||
			exit_fatal("no path given");
 | 
			
		||||
 | 
			
		||||
		FivIoThumbnailSize size = 0;
 | 
			
		||||
		for (; size < FIV_IO_THUMBNAIL_SIZE_COUNT; size++)
 | 
			
		||||
			if (!strcmp(fiv_io_thumbnail_sizes[size].thumbnail_spec_name,
 | 
			
		||||
					thumbnail_size))
 | 
			
		||||
				break;
 | 
			
		||||
		if (size >= FIV_IO_THUMBNAIL_SIZE_COUNT)
 | 
			
		||||
			exit_fatal("unknown thumbnail size: %s", thumbnail_size);
 | 
			
		||||
 | 
			
		||||
		GFile *target = g_file_new_for_path(path_arg);
 | 
			
		||||
		if (!fiv_io_produce_thumbnail(target, size, &error))
 | 
			
		||||
			exit_fatal("%s", error->message);
 | 
			
		||||
		g_object_unref(target);
 | 
			
		||||
		return 0;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	gtk_window_set_default_icon_name(PROJECT_NAME);
 | 
			
		||||
	gtk_icon_theme_add_resource_path(
 | 
			
		||||
@@ -1254,7 +1277,6 @@ main(int argc, char *argv[])
 | 
			
		||||
	gtk_box_pack_start(GTK_BOX(g.view_box),
 | 
			
		||||
		gtk_separator_new(GTK_ORIENTATION_VERTICAL), FALSE, FALSE, 0);
 | 
			
		||||
	gtk_box_pack_start(GTK_BOX(g.view_box), view_scroller, TRUE, TRUE, 0);
 | 
			
		||||
	gtk_widget_show_all(g.view_box);
 | 
			
		||||
 | 
			
		||||
	g.browser_scroller = gtk_scrolled_window_new(NULL, NULL);
 | 
			
		||||
	g.browser = g_object_new(FIV_TYPE_BROWSER, NULL);
 | 
			
		||||
@@ -1320,15 +1342,16 @@ main(int argc, char *argv[])
 | 
			
		||||
	g_signal_connect(g.browser_paned, "button-press-event",
 | 
			
		||||
		G_CALLBACK(on_button_press_browser_paned), NULL);
 | 
			
		||||
 | 
			
		||||
	// TODO(p): Can we not do it here separately?
 | 
			
		||||
	gtk_widget_show_all(g.browser_paned);
 | 
			
		||||
 | 
			
		||||
	g.stack = gtk_stack_new();
 | 
			
		||||
	gtk_stack_set_transition_type(
 | 
			
		||||
		GTK_STACK(g.stack), GTK_STACK_TRANSITION_TYPE_NONE);
 | 
			
		||||
	gtk_container_add(GTK_CONTAINER(g.stack), g.view_box);
 | 
			
		||||
	gtk_container_add(GTK_CONTAINER(g.stack), g.browser_paned);
 | 
			
		||||
 | 
			
		||||
	// TODO(p): Can we not do it here separately?
 | 
			
		||||
	gtk_widget_show_all(g.view_box);
 | 
			
		||||
	gtk_widget_show_all(g.browser_paned);
 | 
			
		||||
 | 
			
		||||
	g.window = gtk_window_new(GTK_WINDOW_TOPLEVEL);
 | 
			
		||||
	g_signal_connect(g.window, "destroy",
 | 
			
		||||
		G_CALLBACK(gtk_main_quit), NULL);
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										123
									
								
								fiv-browser.c
									
									
									
									
									
								
							
							
						
						
									
										123
									
								
								fiv-browser.c
									
									
									
									
									
								
							@@ -18,6 +18,8 @@
 | 
			
		||||
#include <math.h>
 | 
			
		||||
#include <pixman.h>
 | 
			
		||||
 | 
			
		||||
#include "config.h"
 | 
			
		||||
 | 
			
		||||
#include "fiv-browser.h"
 | 
			
		||||
#include "fiv-io.h"
 | 
			
		||||
#include "fiv-view.h"
 | 
			
		||||
@@ -50,6 +52,10 @@ struct _FivBrowser {
 | 
			
		||||
	GArray *layouted_rows;              ///< [Row]
 | 
			
		||||
	int selected;
 | 
			
		||||
 | 
			
		||||
	GList *thumbnail_queue;             ///< URIs to thumbnail
 | 
			
		||||
	GSubprocess *thumbnailer;           ///< A slave for the current queue head
 | 
			
		||||
	GCancellable *thumbnail_cancel;     ///< Cancellable handle
 | 
			
		||||
 | 
			
		||||
	GdkCursor *pointer;                 ///< Cached pointer cursor
 | 
			
		||||
	cairo_surface_t *glow;              ///< CAIRO_FORMAT_A8 mask
 | 
			
		||||
	int item_border_x;                  ///< L/R .item margin + border
 | 
			
		||||
@@ -440,6 +446,115 @@ reload_thumbnails(FivBrowser *self)
 | 
			
		||||
	gtk_widget_queue_resize(GTK_WIDGET(self));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// --- Slave management --------------------------------------------------------
 | 
			
		||||
 | 
			
		||||
static void thumbnailer_step(FivBrowser *self);
 | 
			
		||||
 | 
			
		||||
static void
 | 
			
		||||
thumbnailer_process(FivBrowser *self, const gchar *uri)
 | 
			
		||||
{
 | 
			
		||||
	// TODO(p): Consider using Entry pointers directly.
 | 
			
		||||
	Entry *entry = NULL;
 | 
			
		||||
	for (guint i = 0; i < self->entries->len; i++) {
 | 
			
		||||
		Entry *e = &g_array_index(self->entries, Entry, i);
 | 
			
		||||
		if (!g_strcmp0(e->uri, uri)) {
 | 
			
		||||
			entry = e;
 | 
			
		||||
			break;
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	if (!entry) {
 | 
			
		||||
		g_warning("finished thumbnailing an unknown URI");
 | 
			
		||||
		return;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	entry_add_thumbnail(entry, self);
 | 
			
		||||
	materialize_icon(self, entry);
 | 
			
		||||
	gtk_widget_queue_resize(GTK_WIDGET(self));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
static void
 | 
			
		||||
on_thumbnailer_ready(GObject *object, GAsyncResult *res, gpointer user_data)
 | 
			
		||||
{
 | 
			
		||||
	GSubprocess *subprocess = G_SUBPROCESS(object);
 | 
			
		||||
	FivBrowser *self = FIV_BROWSER(user_data);
 | 
			
		||||
	GError *error = NULL;
 | 
			
		||||
	if (!g_subprocess_wait_check_finish(subprocess, res, &error)) {
 | 
			
		||||
		g_warning("%s", error->message);
 | 
			
		||||
		g_error_free(error);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	gboolean succeeded = g_subprocess_get_if_exited(self->thumbnailer) &&
 | 
			
		||||
		g_subprocess_get_exit_status(self->thumbnailer) == EXIT_SUCCESS;
 | 
			
		||||
	g_clear_object(&self->thumbnailer);
 | 
			
		||||
	if (!self->thumbnail_queue) {
 | 
			
		||||
		g_warning("finished thumbnailing an unknown image");
 | 
			
		||||
		return;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	gchar *uri = self->thumbnail_queue->data;
 | 
			
		||||
	self->thumbnail_queue =
 | 
			
		||||
		g_list_delete_link(self->thumbnail_queue, self->thumbnail_queue);
 | 
			
		||||
	if (succeeded)
 | 
			
		||||
		thumbnailer_process(self, uri);
 | 
			
		||||
	g_free(uri);
 | 
			
		||||
 | 
			
		||||
	// TODO(p): Eliminate high recursion depth with non-paths.
 | 
			
		||||
	thumbnailer_step(self);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
static void
 | 
			
		||||
thumbnailer_step(FivBrowser *self)
 | 
			
		||||
{
 | 
			
		||||
	if (!self->thumbnail_queue)
 | 
			
		||||
		return;
 | 
			
		||||
 | 
			
		||||
	GFile *file = g_file_new_for_uri(self->thumbnail_queue->data);
 | 
			
		||||
	gchar *path = g_file_get_path(file);
 | 
			
		||||
	g_object_unref(file);
 | 
			
		||||
 | 
			
		||||
	GError *error = NULL;
 | 
			
		||||
	self->thumbnailer = g_subprocess_new(G_SUBPROCESS_FLAGS_NONE, &error,
 | 
			
		||||
		PROJECT_NAME, "--thumbnail",
 | 
			
		||||
		fiv_io_thumbnail_sizes[self->item_size].thumbnail_spec_name, "--", path,
 | 
			
		||||
		NULL);
 | 
			
		||||
	g_free(path);
 | 
			
		||||
 | 
			
		||||
	if (error) {
 | 
			
		||||
		g_warning("%s", error->message);
 | 
			
		||||
		g_error_free(error);
 | 
			
		||||
		return;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	self->thumbnail_cancel = g_cancellable_new();
 | 
			
		||||
	g_subprocess_wait_check_async(
 | 
			
		||||
		self->thumbnailer, self->thumbnail_cancel, on_thumbnailer_ready, self);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
static void
 | 
			
		||||
thumbnailer_launch(FivBrowser *self)
 | 
			
		||||
{
 | 
			
		||||
	if (self->thumbnailer) {
 | 
			
		||||
		g_cancellable_cancel(self->thumbnail_cancel);
 | 
			
		||||
		g_clear_object(&self->thumbnail_cancel);
 | 
			
		||||
 | 
			
		||||
		// Just let it exit on its own.
 | 
			
		||||
		g_clear_object(&self->thumbnailer);
 | 
			
		||||
		g_list_free_full(self->thumbnail_queue, g_free);
 | 
			
		||||
		self->thumbnail_queue = NULL;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// TODO(p): Also collect rescaled images.
 | 
			
		||||
	GList *missing = NULL, *rescaled = NULL;
 | 
			
		||||
	for (guint i = self->entries->len; i--; ) {
 | 
			
		||||
		Entry *e = &g_array_index(self->entries, Entry, i);
 | 
			
		||||
		if (e->icon)
 | 
			
		||||
			missing = g_list_prepend(missing, g_strdup(e->uri));
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	self->thumbnail_queue = g_list_concat(missing, rescaled);
 | 
			
		||||
	thumbnailer_step(self);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// --- Context menu-------------------------------------------------------------
 | 
			
		||||
 | 
			
		||||
typedef struct _OpenContext {
 | 
			
		||||
@@ -633,6 +748,13 @@ fiv_browser_finalize(GObject *gobject)
 | 
			
		||||
	cairo_surface_destroy(self->glow);
 | 
			
		||||
	g_clear_object(&self->pointer);
 | 
			
		||||
 | 
			
		||||
	g_list_free_full(self->thumbnail_queue, g_free);
 | 
			
		||||
	g_clear_object(&self->thumbnailer);
 | 
			
		||||
	if (self->thumbnail_cancel) {
 | 
			
		||||
		g_cancellable_cancel(self->thumbnail_cancel);
 | 
			
		||||
		g_clear_object(&self->thumbnail_cancel);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	G_OBJECT_CLASS(fiv_browser_parent_class)->finalize(gobject);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -1076,4 +1198,5 @@ fiv_browser_load(
 | 
			
		||||
	g_array_sort(self->entries, entry_compare);
 | 
			
		||||
 | 
			
		||||
	reload_thumbnails(self);
 | 
			
		||||
	thumbnailer_launch(self);
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										196
									
								
								fiv-io.c
									
									
									
									
									
								
							
							
						
						
									
										196
									
								
								fiv-io.c
									
									
									
									
									
								
							@@ -24,6 +24,10 @@
 | 
			
		||||
 | 
			
		||||
#include <spng.h>
 | 
			
		||||
#include <turbojpeg.h>
 | 
			
		||||
#include <webp/decode.h>
 | 
			
		||||
#include <webp/demux.h>
 | 
			
		||||
#include <webp/encode.h>
 | 
			
		||||
#include <webp/mux.h>
 | 
			
		||||
 | 
			
		||||
// Colour management must be handled before RGB conversions.
 | 
			
		||||
#ifdef HAVE_LCMS2
 | 
			
		||||
@@ -48,12 +52,6 @@
 | 
			
		||||
#ifdef HAVE_XCURSOR
 | 
			
		||||
#include <X11/Xcursor/Xcursor.h>
 | 
			
		||||
#endif  // HAVE_XCURSOR
 | 
			
		||||
#ifdef HAVE_LIBWEBP
 | 
			
		||||
#include <webp/decode.h>
 | 
			
		||||
#include <webp/demux.h>
 | 
			
		||||
#include <webp/encode.h>
 | 
			
		||||
#include <webp/mux.h>
 | 
			
		||||
#endif  // HAVE_LIBWEBP
 | 
			
		||||
#ifdef HAVE_LIBHEIF
 | 
			
		||||
#include <libheif/heif.h>
 | 
			
		||||
#endif  // HAVE_LIBHEIF
 | 
			
		||||
@@ -94,6 +92,7 @@ const char *fiv_io_supported_media_types[] = {
 | 
			
		||||
	"image/gif",
 | 
			
		||||
	"image/png",
 | 
			
		||||
	"image/jpeg",
 | 
			
		||||
	"image/webp",
 | 
			
		||||
#ifdef HAVE_LIBRAW
 | 
			
		||||
	"image/x-dcraw",
 | 
			
		||||
#endif  // HAVE_LIBRAW
 | 
			
		||||
@@ -103,9 +102,6 @@ const char *fiv_io_supported_media_types[] = {
 | 
			
		||||
#ifdef HAVE_XCURSOR
 | 
			
		||||
	"image/x-xcursor",
 | 
			
		||||
#endif  // HAVE_XCURSOR
 | 
			
		||||
#ifdef HAVE_LIBWEBP
 | 
			
		||||
	"image/webp",
 | 
			
		||||
#endif  // HAVE_LIBWEBP
 | 
			
		||||
#ifdef HAVE_LIBHEIF
 | 
			
		||||
	"image/heic",
 | 
			
		||||
	"image/heif",
 | 
			
		||||
@@ -1553,7 +1549,6 @@ open_xcursor(const gchar *data, gsize len, GError **error)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#endif  // HAVE_XCURSOR --------------------------------------------------------
 | 
			
		||||
#ifdef HAVE_LIBWEBP  //---------------------------------------------------------
 | 
			
		||||
 | 
			
		||||
static cairo_surface_t *
 | 
			
		||||
load_libwebp_nonanimated(WebPDecoderConfig *config, const WebPData *wd,
 | 
			
		||||
@@ -1763,7 +1758,6 @@ fail:
 | 
			
		||||
	return result;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#endif  // HAVE_LIBWEBP --------------------------------------------------------
 | 
			
		||||
#ifdef HAVE_LIBHEIF  //---------------------------------------------------------
 | 
			
		||||
 | 
			
		||||
static cairo_surface_t *
 | 
			
		||||
@@ -2355,6 +2349,14 @@ fiv_io_open_from_data(const char *data, size_t len, const gchar *path,
 | 
			
		||||
			: open_libjpeg_turbo(data, len, profile, error);
 | 
			
		||||
		break;
 | 
			
		||||
	default:
 | 
			
		||||
		// TODO(p): https://github.com/google/wuffs/commit/4c04ac1
 | 
			
		||||
		if ((surface = open_libwebp(data, len, path, profile, error)))
 | 
			
		||||
			break;
 | 
			
		||||
		if (error) {
 | 
			
		||||
			g_debug("%s", (*error)->message);
 | 
			
		||||
			g_clear_error(error);
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
#ifdef HAVE_LIBRAW  // ---------------------------------------------------------
 | 
			
		||||
		if ((surface = open_libraw(data, len, error)))
 | 
			
		||||
			break;
 | 
			
		||||
@@ -2384,15 +2386,6 @@ fiv_io_open_from_data(const char *data, size_t len, const gchar *path,
 | 
			
		||||
			g_clear_error(error);
 | 
			
		||||
		}
 | 
			
		||||
#endif  // HAVE_XCURSOR --------------------------------------------------------
 | 
			
		||||
#ifdef HAVE_LIBWEBP  //---------------------------------------------------------
 | 
			
		||||
		// TODO(p): https://github.com/google/wuffs/commit/4c04ac1
 | 
			
		||||
		if ((surface = open_libwebp(data, len, path, profile, error)))
 | 
			
		||||
			break;
 | 
			
		||||
		if (error) {
 | 
			
		||||
			g_debug("%s", (*error)->message);
 | 
			
		||||
			g_clear_error(error);
 | 
			
		||||
		}
 | 
			
		||||
#endif  // HAVE_LIBWEBP --------------------------------------------------------
 | 
			
		||||
#ifdef HAVE_LIBHEIF  //---------------------------------------------------------
 | 
			
		||||
		if ((surface = open_libheif(data, len, path, profile, error)))
 | 
			
		||||
			break;
 | 
			
		||||
@@ -2443,7 +2436,6 @@ fiv_io_open_from_data(const char *data, size_t len, const gchar *path,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// --- Export ------------------------------------------------------------------
 | 
			
		||||
#ifdef HAVE_LIBWEBP
 | 
			
		||||
 | 
			
		||||
static WebPData
 | 
			
		||||
encode_lossless_webp(cairo_surface_t *surface)
 | 
			
		||||
@@ -2603,7 +2595,6 @@ fiv_io_save(cairo_surface_t *page, cairo_surface_t *frame, FivIoProfile target,
 | 
			
		||||
	return ok;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#endif  // HAVE_LIBWEBP
 | 
			
		||||
// --- Metadata ----------------------------------------------------------------
 | 
			
		||||
 | 
			
		||||
FivIoOrientation
 | 
			
		||||
@@ -2744,6 +2735,10 @@ fiv_io_save_metadata(cairo_surface_t *page, const gchar *path, GError **error)
 | 
			
		||||
 | 
			
		||||
// --- Thumbnails --------------------------------------------------------------
 | 
			
		||||
 | 
			
		||||
#ifndef __linux__
 | 
			
		||||
#define st_mtim st_mtimespec
 | 
			
		||||
#endif  // ! __linux__
 | 
			
		||||
 | 
			
		||||
GType
 | 
			
		||||
fiv_io_thumbnail_size_get_type(void)
 | 
			
		||||
{
 | 
			
		||||
@@ -2766,11 +2761,143 @@ FivIoThumbnailSizeInfo
 | 
			
		||||
		FIV_IO_THUMBNAIL_SIZES(XX)};
 | 
			
		||||
#undef XX
 | 
			
		||||
 | 
			
		||||
// TODO(p): Put the constant in a header file, share with fiv-browser.c.
 | 
			
		||||
static const double g_wide_thumbnail_factor = 2;
 | 
			
		||||
 | 
			
		||||
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
 | 
			
		||||
 | 
			
		||||
#ifndef __linux__
 | 
			
		||||
#define st_mtim st_mtimespec
 | 
			
		||||
#endif  // ! __linux__
 | 
			
		||||
// In principle similar to rescale_thumbnail() from fiv-browser.c.
 | 
			
		||||
static cairo_surface_t *
 | 
			
		||||
rescale_thumbnail(cairo_surface_t *thumbnail, double row_height)
 | 
			
		||||
{
 | 
			
		||||
	int width = cairo_image_surface_get_width(thumbnail);
 | 
			
		||||
	int height = cairo_image_surface_get_height(thumbnail);
 | 
			
		||||
 | 
			
		||||
	double scale_x = 1;
 | 
			
		||||
	double scale_y = 1;
 | 
			
		||||
	if (width > g_wide_thumbnail_factor * height) {
 | 
			
		||||
		scale_x = g_wide_thumbnail_factor * row_height / width;
 | 
			
		||||
		scale_y = round(scale_x * height) / height;
 | 
			
		||||
	} else {
 | 
			
		||||
		scale_y = row_height / height;
 | 
			
		||||
		scale_x = round(scale_y * width) / width;
 | 
			
		||||
	}
 | 
			
		||||
	if (scale_x == 1 && scale_y == 1)
 | 
			
		||||
		return cairo_surface_reference(thumbnail);
 | 
			
		||||
 | 
			
		||||
	// TODO(p): Don't always include an alpha channel.
 | 
			
		||||
	cairo_format_t cairo_format = CAIRO_FORMAT_ARGB32;
 | 
			
		||||
 | 
			
		||||
	int projected_width = round(scale_x * width);
 | 
			
		||||
	int projected_height = round(scale_y * height);
 | 
			
		||||
	cairo_surface_t *scaled = cairo_image_surface_create(
 | 
			
		||||
		cairo_format, projected_width, projected_height);
 | 
			
		||||
 | 
			
		||||
	cairo_t *cr = cairo_create(scaled);
 | 
			
		||||
	cairo_scale(cr, scale_x, scale_y);
 | 
			
		||||
 | 
			
		||||
	cairo_set_source_surface(cr, thumbnail, 0, 0);
 | 
			
		||||
	cairo_pattern_t *pattern = cairo_get_source(cr);
 | 
			
		||||
	cairo_pattern_set_filter(pattern, CAIRO_FILTER_BEST);
 | 
			
		||||
	cairo_pattern_set_extend(pattern, CAIRO_EXTEND_PAD);
 | 
			
		||||
 | 
			
		||||
	cairo_set_operator(cr, CAIRO_OPERATOR_SOURCE);
 | 
			
		||||
	cairo_paint(cr);
 | 
			
		||||
	cairo_destroy(cr);
 | 
			
		||||
	return scaled;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
gboolean
 | 
			
		||||
fiv_io_produce_thumbnail(GFile *target, FivIoThumbnailSize size, GError **error)
 | 
			
		||||
{
 | 
			
		||||
	g_return_val_if_fail(size >= FIV_IO_THUMBNAIL_SIZE_MIN &&
 | 
			
		||||
		size <= FIV_IO_THUMBNAIL_SIZE_MAX, FALSE);
 | 
			
		||||
 | 
			
		||||
	// Local files only, at least for now.
 | 
			
		||||
	gchar *path = g_file_get_path(target);
 | 
			
		||||
	if (!path)
 | 
			
		||||
		return FALSE;
 | 
			
		||||
 | 
			
		||||
	GMappedFile *mf = g_mapped_file_new(path, FALSE, error);
 | 
			
		||||
	if (!mf) {
 | 
			
		||||
		g_free(path);
 | 
			
		||||
		return FALSE;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	GStatBuf st = {};
 | 
			
		||||
	if (g_stat(path, &st)) {
 | 
			
		||||
		set_error(error, g_strerror(errno));
 | 
			
		||||
		g_free(path);
 | 
			
		||||
		return FALSE;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// TODO(p): Add a flag to avoid loading all pages and frames.
 | 
			
		||||
	FivIoProfile sRGB = fiv_io_profile_new_sRGB();
 | 
			
		||||
	cairo_surface_t *surface =
 | 
			
		||||
		fiv_io_open_from_data(g_mapped_file_get_contents(mf),
 | 
			
		||||
			g_mapped_file_get_length(mf), path, sRGB, FALSE, error);
 | 
			
		||||
 | 
			
		||||
	g_free(path);
 | 
			
		||||
	g_mapped_file_unref(mf);
 | 
			
		||||
	if (sRGB)
 | 
			
		||||
		fiv_io_profile_free(sRGB);
 | 
			
		||||
	if (!surface)
 | 
			
		||||
		return FALSE;
 | 
			
		||||
 | 
			
		||||
	// Boilerplate copied from fiv_io_lookup_thumbnail().
 | 
			
		||||
	gchar *uri = g_file_get_uri(target);
 | 
			
		||||
	gchar *sum = g_compute_checksum_for_string(G_CHECKSUM_MD5, uri, -1);
 | 
			
		||||
	gchar *cache_dir = get_xdg_home_dir("XDG_CACHE_HOME", ".cache");
 | 
			
		||||
 | 
			
		||||
	for (int use = size; use >= FIV_IO_THUMBNAIL_SIZE_MIN; use--) {
 | 
			
		||||
		cairo_surface_t *scaled =
 | 
			
		||||
			rescale_thumbnail(surface, fiv_io_thumbnail_sizes[use].size);
 | 
			
		||||
		gchar *path = g_strdup_printf("%s/thumbnails/wide-%s/%s.webp",
 | 
			
		||||
			cache_dir, fiv_io_thumbnail_sizes[use].thumbnail_spec_name, sum);
 | 
			
		||||
 | 
			
		||||
		GError *e = NULL;
 | 
			
		||||
		while (!fiv_io_save(scaled, scaled, NULL, path, &e)) {
 | 
			
		||||
			bool missing_parents =
 | 
			
		||||
				e->domain == G_FILE_ERROR && e->code == G_FILE_ERROR_NOENT;
 | 
			
		||||
			g_debug("%s: %s", path, e->message);
 | 
			
		||||
			g_clear_error(&e);
 | 
			
		||||
			if (!missing_parents)
 | 
			
		||||
				break;
 | 
			
		||||
 | 
			
		||||
			gchar *dirname = g_path_get_dirname(path);
 | 
			
		||||
			int err = g_mkdir_with_parents(dirname, 0755);
 | 
			
		||||
			if (err)
 | 
			
		||||
				g_debug("%s: %s", dirname, g_strerror(errno));
 | 
			
		||||
 | 
			
		||||
			g_free(dirname);
 | 
			
		||||
			if (err)
 | 
			
		||||
				break;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// It would be possible to create square thumbnails as well,
 | 
			
		||||
		// but it seems like wasted effort.
 | 
			
		||||
		cairo_surface_destroy(scaled);
 | 
			
		||||
		g_free(path);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	g_free(cache_dir);
 | 
			
		||||
	g_free(sum);
 | 
			
		||||
	g_free(uri);
 | 
			
		||||
	cairo_surface_destroy(surface);
 | 
			
		||||
	return TRUE;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
static cairo_surface_t *
 | 
			
		||||
read_wide_thumbnail(
 | 
			
		||||
	const gchar *path, const gchar *uri, time_t mtime, GError **error)
 | 
			
		||||
{
 | 
			
		||||
	// TODO(p): Validate.
 | 
			
		||||
	(void) uri;
 | 
			
		||||
	(void) mtime;
 | 
			
		||||
	return fiv_io_open(path, NULL, FALSE, error);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
 | 
			
		||||
 | 
			
		||||
static int  // tri-state
 | 
			
		||||
check_spng_thumbnail_texts(struct spng_text *texts, uint32_t texts_len,
 | 
			
		||||
@@ -2935,8 +3062,20 @@ fiv_io_lookup_thumbnail(GFile *target, FivIoThumbnailSize size)
 | 
			
		||||
		if (use > FIV_IO_THUMBNAIL_SIZE_MAX)
 | 
			
		||||
			use = FIV_IO_THUMBNAIL_SIZE_MAX - i;
 | 
			
		||||
 | 
			
		||||
		gchar *path = g_strdup_printf("%s/thumbnails/%s/%s.png", cache_dir,
 | 
			
		||||
			fiv_io_thumbnail_sizes[use].thumbnail_spec_name, sum);
 | 
			
		||||
		const char *name = fiv_io_thumbnail_sizes[use].thumbnail_spec_name;
 | 
			
		||||
		gchar *wide = g_strdup_printf(
 | 
			
		||||
			"%s/thumbnails/wide-%s/%s.webp", cache_dir, name, sum);
 | 
			
		||||
		result = read_wide_thumbnail(wide, uri, st.st_mtim.tv_sec, &error);
 | 
			
		||||
		if (error) {
 | 
			
		||||
			g_debug("%s: %s", wide, error->message);
 | 
			
		||||
			g_clear_error(&error);
 | 
			
		||||
		}
 | 
			
		||||
		g_free(wide);
 | 
			
		||||
		if (result)
 | 
			
		||||
			break;
 | 
			
		||||
 | 
			
		||||
		gchar *path =
 | 
			
		||||
			g_strdup_printf("%s/thumbnails/%s/%s.png", cache_dir, name, sum);
 | 
			
		||||
		result = read_spng_thumbnail(path, uri, st.st_mtim.tv_sec, &error);
 | 
			
		||||
		if (error) {
 | 
			
		||||
			g_debug("%s: %s", path, error->message);
 | 
			
		||||
@@ -2947,6 +3086,9 @@ fiv_io_lookup_thumbnail(GFile *target, FivIoThumbnailSize size)
 | 
			
		||||
			break;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// TODO(p): We can definitely extract embedded thumbnails, but it should be
 | 
			
		||||
	// done as a separate stage--the file may be stored on a slow device.
 | 
			
		||||
 | 
			
		||||
	g_free(cache_dir);
 | 
			
		||||
	g_free(sum);
 | 
			
		||||
	g_free(uri);
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										8
									
								
								fiv-io.h
									
									
									
									
									
								
							
							
						
						
									
										8
									
								
								fiv-io.h
									
									
									
									
									
								
							@@ -76,7 +76,7 @@ int fiv_io_filecmp(GFile *f1, GFile *f2);
 | 
			
		||||
 | 
			
		||||
// --- Export ------------------------------------------------------------------
 | 
			
		||||
 | 
			
		||||
/// Requires libwebp.
 | 
			
		||||
/// Saves the page as a lossless WebP still picture or animation.
 | 
			
		||||
/// If no exact frame is specified, this potentially creates an animation.
 | 
			
		||||
gboolean fiv_io_save(cairo_surface_t *page, cairo_surface_t *frame,
 | 
			
		||||
	FivIoProfile target, const gchar *path, GError **error);
 | 
			
		||||
@@ -131,5 +131,11 @@ typedef struct _FivIoThumbnailSizeInfo {
 | 
			
		||||
extern FivIoThumbnailSizeInfo
 | 
			
		||||
	fiv_io_thumbnail_sizes[FIV_IO_THUMBNAIL_SIZE_COUNT];
 | 
			
		||||
 | 
			
		||||
/// Generates wide thumbnails of up to the specified size, saves them in cache.
 | 
			
		||||
gboolean fiv_io_produce_thumbnail(
 | 
			
		||||
	GFile *target, FivIoThumbnailSize size, GError **error);
 | 
			
		||||
 | 
			
		||||
/// Retrieves a thumbnail of the most appropriate quality and resolution
 | 
			
		||||
/// for the target file.
 | 
			
		||||
cairo_surface_t *fiv_io_lookup_thumbnail(
 | 
			
		||||
	GFile *target, FivIoThumbnailSize size);
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										40
									
								
								fiv-view.c
									
									
									
									
									
								
							
							
						
						
									
										40
									
								
								fiv-view.c
									
									
									
									
									
								
							@@ -797,18 +797,7 @@ save_as(FivView *self, cairo_surface_t *frame)
 | 
			
		||||
		"_Cancel", GTK_RESPONSE_CANCEL, "_Save", GTK_RESPONSE_ACCEPT, NULL);
 | 
			
		||||
 | 
			
		||||
	GtkFileChooser *chooser = GTK_FILE_CHOOSER(dialog);
 | 
			
		||||
 | 
			
		||||
	// TODO(p): Consider a hard dependency on libwebp, or clean this up.
 | 
			
		||||
#ifdef HAVE_LIBWEBP
 | 
			
		||||
	// This is the best general format: supports lossless encoding, animations,
 | 
			
		||||
	// alpha channel, and Exif and ICC profile metadata.
 | 
			
		||||
	// PNG is another viable option, but sPNG can't do APNG, Wuffs can't save,
 | 
			
		||||
	// and libpng is a pain in the arse.
 | 
			
		||||
	GtkFileFilter *webp_filter = gtk_file_filter_new();
 | 
			
		||||
	gtk_file_filter_add_mime_type(webp_filter, "image/webp");
 | 
			
		||||
	gtk_file_filter_add_pattern(webp_filter, "*.webp");
 | 
			
		||||
	gtk_file_filter_set_name(webp_filter, "Lossless WebP (*.webp)");
 | 
			
		||||
	gtk_file_chooser_add_filter(chooser, webp_filter);
 | 
			
		||||
	gtk_file_chooser_set_do_overwrite_confirmation(chooser, TRUE);
 | 
			
		||||
 | 
			
		||||
	// Note that GTK+'s save dialog is too stupid to automatically change
 | 
			
		||||
	// the extension when user changes the filter. Presumably,
 | 
			
		||||
@@ -819,14 +808,21 @@ save_as(FivView *self, cairo_surface_t *frame)
 | 
			
		||||
	g_free(basename);
 | 
			
		||||
	gtk_file_chooser_set_current_name(chooser, name);
 | 
			
		||||
	g_free(name);
 | 
			
		||||
#endif  // HAVE_LIBWEBP
 | 
			
		||||
 | 
			
		||||
	gtk_file_chooser_set_do_overwrite_confirmation(chooser, TRUE);
 | 
			
		||||
 | 
			
		||||
	gchar *dirname = g_path_get_dirname(self->path);
 | 
			
		||||
	gtk_file_chooser_set_current_folder(chooser, dirname);
 | 
			
		||||
	g_free(dirname);
 | 
			
		||||
 | 
			
		||||
	// This is the best general format: supports lossless encoding, animations,
 | 
			
		||||
	// alpha channel, and Exif and ICC profile metadata.
 | 
			
		||||
	// PNG is another viable option, but sPNG can't do APNG, Wuffs can't save,
 | 
			
		||||
	// and libpng is a pain in the arse.
 | 
			
		||||
	GtkFileFilter *webp_filter = gtk_file_filter_new();
 | 
			
		||||
	gtk_file_filter_add_mime_type(webp_filter, "image/webp");
 | 
			
		||||
	gtk_file_filter_add_pattern(webp_filter, "*.webp");
 | 
			
		||||
	gtk_file_filter_set_name(webp_filter, "Lossless WebP (*.webp)");
 | 
			
		||||
	gtk_file_chooser_add_filter(chooser, webp_filter);
 | 
			
		||||
 | 
			
		||||
	// The format is supported by Exiv2 and ExifTool.
 | 
			
		||||
	// This is mostly a developer tool.
 | 
			
		||||
	GtkFileFilter *exv_filter = gtk_file_filter_new();
 | 
			
		||||
@@ -835,22 +831,16 @@ save_as(FivView *self, cairo_surface_t *frame)
 | 
			
		||||
	gtk_file_filter_set_name(exv_filter, "Exiv2 metadata (*.exv)");
 | 
			
		||||
	gtk_file_chooser_add_filter(chooser, exv_filter);
 | 
			
		||||
 | 
			
		||||
	GError *error = NULL;
 | 
			
		||||
	switch (gtk_dialog_run(GTK_DIALOG(dialog))) {
 | 
			
		||||
		gchar *path;
 | 
			
		||||
	case GTK_RESPONSE_ACCEPT:
 | 
			
		||||
		path = gtk_file_chooser_get_filename(chooser);
 | 
			
		||||
 | 
			
		||||
		GError *error = NULL;
 | 
			
		||||
#ifdef HAVE_LIBWEBP
 | 
			
		||||
		if (gtk_file_chooser_get_filter(chooser) == webp_filter)
 | 
			
		||||
			fiv_io_save(self->page, frame, target, path, &error);
 | 
			
		||||
		else
 | 
			
		||||
#endif  // HAVE_LIBWEBP
 | 
			
		||||
			fiv_io_save_metadata(self->page, path, &error);
 | 
			
		||||
		if (error)
 | 
			
		||||
		if (!(gtk_file_chooser_get_filter(chooser) == webp_filter
 | 
			
		||||
					? fiv_io_save(self->page, frame, target, path, &error)
 | 
			
		||||
					: fiv_io_save_metadata(self->page, path, &error)))
 | 
			
		||||
			show_error_dialog(window, error);
 | 
			
		||||
		g_free(path);
 | 
			
		||||
 | 
			
		||||
		// Fall-through.
 | 
			
		||||
	default:
 | 
			
		||||
		gtk_widget_destroy(dialog);
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										16
									
								
								meson.build
									
									
									
									
									
								
							
							
						
						
									
										16
									
								
								meson.build
									
									
									
									
									
								
							@@ -14,15 +14,11 @@ if get_option('buildtype').startswith('debug')
 | 
			
		||||
	add_project_link_arguments(flags, language : ['c'])
 | 
			
		||||
endif
 | 
			
		||||
 | 
			
		||||
# TODO(p): Use libraw_r later, when we start parallelizing/preloading.
 | 
			
		||||
lcms2 = dependency('lcms2', required : get_option('lcms2'))
 | 
			
		||||
# Note that only libraw_r is thread-safe, but we'll just run it out-of process.
 | 
			
		||||
libraw = dependency('libraw', required : get_option('libraw'))
 | 
			
		||||
librsvg = dependency('librsvg-2.0', required : get_option('librsvg'))
 | 
			
		||||
xcursor = dependency('xcursor', required : get_option('xcursor'))
 | 
			
		||||
libwebp = dependency('libwebp', required : get_option('libwebp'))
 | 
			
		||||
libwebpdemux = dependency('libwebpdemux', required : get_option('libwebp'))
 | 
			
		||||
libwebpdecoder = dependency('libwebpdecoder', required : get_option('libwebp'))
 | 
			
		||||
libwebpmux = dependency('libwebpmux', required : get_option('libwebp'))
 | 
			
		||||
libheif = dependency('libheif', required : get_option('libheif'))
 | 
			
		||||
libtiff = dependency('libtiff-4', required : get_option('libtiff'))
 | 
			
		||||
gdkpixbuf = dependency('gdk-pixbuf-2.0', required : get_option('gdk-pixbuf'))
 | 
			
		||||
@@ -32,6 +28,11 @@ dependencies = [
 | 
			
		||||
 | 
			
		||||
	dependency('libturbojpeg'),
 | 
			
		||||
	dependency('libjpeg', required : get_option('jpeg-qs')),
 | 
			
		||||
	dependency('libwebp'),
 | 
			
		||||
	dependency('libwebpdemux'),
 | 
			
		||||
	dependency('libwebpdecoder'),
 | 
			
		||||
	dependency('libwebpmux'),
 | 
			
		||||
	# https://github.com/google/wuffs/issues/58
 | 
			
		||||
	dependency('spng', version : '>=0.7.0',
 | 
			
		||||
		default_options: 'default_library=static'),
 | 
			
		||||
 | 
			
		||||
@@ -39,10 +40,6 @@ dependencies = [
 | 
			
		||||
	libraw,
 | 
			
		||||
	librsvg,
 | 
			
		||||
	xcursor,
 | 
			
		||||
	libwebp,
 | 
			
		||||
	libwebpdemux,
 | 
			
		||||
	libwebpdecoder,
 | 
			
		||||
	libwebpmux,
 | 
			
		||||
	libheif,
 | 
			
		||||
	libtiff,
 | 
			
		||||
	gdkpixbuf,
 | 
			
		||||
@@ -58,7 +55,6 @@ conf.set('HAVE_LCMS2', lcms2.found())
 | 
			
		||||
conf.set('HAVE_LIBRAW', libraw.found())
 | 
			
		||||
conf.set('HAVE_LIBRSVG', librsvg.found())
 | 
			
		||||
conf.set('HAVE_XCURSOR', xcursor.found())
 | 
			
		||||
conf.set('HAVE_LIBWEBP', libwebp.found())
 | 
			
		||||
conf.set('HAVE_LIBHEIF', libheif.found())
 | 
			
		||||
conf.set('HAVE_LIBTIFF', libtiff.found())
 | 
			
		||||
conf.set('HAVE_GDKPIXBUF', gdkpixbuf.found())
 | 
			
		||||
 
 | 
			
		||||
@@ -8,8 +8,6 @@ option('librsvg', type : 'feature', value : 'auto',
 | 
			
		||||
	description : 'Build with SVG support, requires librsvg')
 | 
			
		||||
option('xcursor', type : 'feature', value : 'auto',
 | 
			
		||||
	description : 'Build with Xcursor support, requires libXcursor')
 | 
			
		||||
option('libwebp', type : 'feature', value : 'auto',
 | 
			
		||||
	description : 'Build with WEBP support, requires libwebp')
 | 
			
		||||
option('libheif', type : 'feature', value : 'auto',
 | 
			
		||||
	description : 'Build with HEIF/AVIF support, requires libheif')
 | 
			
		||||
option('libtiff', type : 'feature', value : 'auto',
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user