diff options
| -rw-r--r-- | README.adoc | 12 | ||||
| -rw-r--r-- | fastiv.c | 33 | ||||
| -rw-r--r-- | fiv-browser.c | 123 | ||||
| -rw-r--r-- | fiv-io.c | 196 | ||||
| -rw-r--r-- | fiv-io.h | 8 | ||||
| -rw-r--r-- | fiv-view.c | 40 | ||||
| -rw-r--r-- | meson.build | 16 | ||||
| -rw-r--r-- | meson_options.txt | 2 | 
8 files changed, 354 insertions, 76 deletions
diff --git a/README.adoc b/README.adoc index f133222..e1689f5 100644 --- a/README.adoc +++ b/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 @@ -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); diff --git a/fiv-browser.c b/fiv-browser.c index 2dfd70e..788197c 100644 --- a/fiv-browser.c +++ b/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);  } @@ -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); @@ -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); @@ -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); diff --git a/meson.build b/meson.build index f3fae8c..ecad76f 100644 --- a/meson.build +++ b/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()) diff --git a/meson_options.txt b/meson_options.txt index e8c6f7d..1ecc87b 100644 --- a/meson_options.txt +++ b/meson_options.txt @@ -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',  | 
