Implement trivial wide thumbnail production
Also make libwebp a required dependency.
This commit is contained in:
parent
2f993502fc
commit
336053f24d
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',
|
||||
|
|
Loading…
Reference in New Issue