Compare commits

...

4 Commits

Author SHA1 Message Date
84f8c9436f
Downscale embedded thumbnails within minions
Otherwise the UI would become unresponsive during loading.
2022-06-08 02:51:55 +02:00
a8f7532abd
Employ embedded thumbnail extraction
And store all direct thumbnailer output in the browser's cache--
low-quality thumbnails will always be regenerated, as is desired,
and we'll reload faster on devices where we don't store thumbnails.

This change improves latency at the cost of overall efficiency,
seeing as images with thumbnails will be spent cycles on twice.

Keeping this out-of-process avoids undesired lock-ups.
Moreover, embedded thumbnails can be fairly expensive to decode.
2022-06-08 02:51:54 +02:00
8dfbd0dee2
Add a command line option to extract thumbnails
Only use LibRaw for now, which probably has the most impact
using the least amount of effort.
2022-06-08 02:51:54 +02:00
930744e165
Add flags to the serialization protocol
It still needs no versioning, as it's not really used by anyone.

An alternative method of passing a "low-quality" flag would be
perusing fiv_thumbnail_key_lq from fiv-thumbnail.c, which would
create a circular dependency, unless fiv_io_{de,}serialize*()
were moved to fiv-thumbnail.c.
2022-06-08 02:51:54 +02:00
6 changed files with 220 additions and 89 deletions

View File

@ -439,6 +439,9 @@ entry_add_thumbnail(gpointer data, gpointer user_data)
(intptr_t) cairo_surface_get_user_data(
cached, &fiv_browser_key_mtime_msec) == self->mtime_msec) {
self->thumbnail = cairo_surface_reference(cached);
// TODO(p): If this hit is low-quality, see if a high-quality thumbnail
// hasn't been produced without our knowledge (avoid launching a minion
// unnecessarily; we might also shift the concern there).
} else {
cairo_surface_t *found = fiv_thumbnail_lookup(
self->uri, self->mtime_msec, browser->item_size);
@ -555,18 +558,31 @@ thumbnailer_reprocess_entry(FivBrowser *self, GBytes *output, Entry *entry)
{
g_clear_object(&entry->icon);
g_clear_pointer(&entry->thumbnail, cairo_surface_destroy);
if (!output || !(entry->thumbnail = rescale_thumbnail(
fiv_io_deserialize(output), self->item_height))) {
entry_add_thumbnail(entry, self);
materialize_icon(self, entry);
} else {
// This choice of mtime favours unnecessary thumbnail reloading.
cairo_surface_set_user_data(entry->thumbnail,
&fiv_browser_key_mtime_msec, (void *) (intptr_t) entry->mtime_msec,
NULL);
}
gtk_widget_queue_resize(GTK_WIDGET(self));
guint64 flags = 0;
if (!output || !(entry->thumbnail = rescale_thumbnail(
fiv_io_deserialize(output, &flags), self->item_height))) {
entry_add_thumbnail(entry, self);
materialize_icon(self, entry);
return;
}
if ((flags & FIV_IO_SERIALIZE_LOW_QUALITY)) {
cairo_surface_set_user_data(entry->thumbnail, &fiv_thumbnail_key_lq,
(void *) (intptr_t) 1, NULL);
// TODO(p): Improve complexity; this will iterate the whole linked list.
self->thumbnailers_queue =
g_list_append(self->thumbnailers_queue, entry);
}
// This choice of mtime favours unnecessary thumbnail reloading.
cairo_surface_set_user_data(entry->thumbnail,
&fiv_browser_key_mtime_msec, (void *) (intptr_t) entry->mtime_msec,
NULL);
g_hash_table_insert(self->thumbnail_cache, g_strdup(entry->uri),
cairo_surface_reference(entry->thumbnail));
}
static void
@ -628,11 +644,23 @@ thumbnailer_next(Thumbnailer *t)
self->thumbnailers_queue =
g_list_delete_link(self->thumbnailers_queue, self->thumbnailers_queue);
// Case analysis:
// - We haven't found any thumbnail for the entry at all
// (and it has a symbolic icon as a result):
// we want to fill the void ASAP, so go for embedded thumbnails first.
// - We've found one, but we're not quite happy with it:
// always run the full process for a high-quality wide thumbnail.
// - We can't end up here in any other cases.
const char *argv_faster[] = {PROJECT_NAME, "--extract-thumbnail",
"--thumbnail", fiv_thumbnail_sizes[self->item_size].thumbnail_spec_name,
"--", t->target->uri, NULL};
const char *argv_slower[] = {PROJECT_NAME,
"--thumbnail", fiv_thumbnail_sizes[self->item_size].thumbnail_spec_name,
"--", t->target->uri, NULL};
GError *error = NULL;
t->minion = g_subprocess_new(G_SUBPROCESS_FLAGS_STDOUT_PIPE, &error,
PROJECT_NAME, "--thumbnail",
fiv_thumbnail_sizes[self->item_size].thumbnail_spec_name, "--",
t->target->uri, NULL);
t->minion = g_subprocess_newv(t->target->icon ? argv_faster : argv_slower,
G_SUBPROCESS_FLAGS_STDOUT_PIPE, &error);
if (error) {
g_warning("%s", error->message);
g_error_free(error);

View File

@ -2891,11 +2891,12 @@ fiv_io_open_from_data(
// --- Thumbnail passing utilities ---------------------------------------------
typedef struct {
guint64 user_data;
int width, height, stride, format;
} CairoHeader;
void
fiv_io_serialize_to_stdout(cairo_surface_t *surface)
fiv_io_serialize_to_stdout(cairo_surface_t *surface, guint64 user_data)
{
if (!surface || cairo_surface_get_type(surface) != CAIRO_SURFACE_TYPE_IMAGE)
return;
@ -2907,6 +2908,7 @@ fiv_io_serialize_to_stdout(cairo_surface_t *surface)
#endif
CairoHeader h = {
.user_data = user_data,
.width = cairo_image_surface_get_width(surface),
.height = cairo_image_surface_get_height(surface),
.stride = cairo_image_surface_get_stride(surface),
@ -2921,7 +2923,7 @@ fiv_io_serialize_to_stdout(cairo_surface_t *surface)
}
cairo_surface_t *
fiv_io_deserialize(GBytes *bytes)
fiv_io_deserialize(GBytes *bytes, guint64 *user_data)
{
CairoHeader h = {};
GByteArray *array = g_bytes_unref_to_array(bytes);
@ -2949,6 +2951,7 @@ fiv_io_deserialize(GBytes *bytes)
static cairo_user_data_key_t key;
cairo_surface_set_user_data(
surface, &key, array, (cairo_destroy_func_t) g_byte_array_unref);
*user_data = h.user_data;
return surface;
}

View File

@ -102,8 +102,10 @@ cairo_surface_t *fiv_io_open_png_thumbnail(const char *path, GError **error);
// --- Thumbnail passing utilities ---------------------------------------------
void fiv_io_serialize_to_stdout(cairo_surface_t *surface);
cairo_surface_t *fiv_io_deserialize(GBytes *bytes);
enum { FIV_IO_SERIALIZE_LOW_QUALITY = 1 << 0 };
void fiv_io_serialize_to_stdout(cairo_surface_t *surface, guint64 user_data);
cairo_surface_t *fiv_io_deserialize(GBytes *bytes, guint64 *user_data);
// --- Filesystem --------------------------------------------------------------

View File

@ -30,6 +30,10 @@
#include "fiv-thumbnail.h"
#include "xdg.h"
#ifdef HAVE_LIBRAW
#include <libraw.h>
#endif // HAVE_LIBRAW
#ifndef __linux__
#define st_mtim st_mtimespec
#endif // ! __linux__
@ -104,6 +108,28 @@ fiv_thumbnail_get_root(void)
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
static cairo_surface_t *
render(GFile *target, GBytes *data, gboolean *color_managed, GError **error)
{
FivIoOpenContext ctx = {
.uri = g_file_get_uri(target),
.screen_profile = fiv_io_profile_new_sRGB(),
.screen_dpi = 96,
.first_frame_only = TRUE,
// Only using this array as a redirect.
.warnings = g_ptr_array_new_with_free_func(g_free),
};
cairo_surface_t *surface = fiv_io_open_from_data(
g_bytes_get_data(data, NULL), g_bytes_get_size(data), &ctx, error);
g_free((gchar *) ctx.uri);
g_ptr_array_free(ctx.warnings, TRUE);
if ((*color_managed = !!ctx.screen_profile))
fiv_io_profile_free(ctx.screen_profile);
g_bytes_unref(data);
return surface;
}
// In principle similar to rescale_thumbnail() from fiv-browser.c.
static cairo_surface_t *
adjust_thumbnail(cairo_surface_t *thumbnail, double row_height)
@ -175,6 +201,76 @@ adjust_thumbnail(cairo_surface_t *thumbnail, double row_height)
return scaled;
}
cairo_surface_t *
fiv_thumbnail_extract(GFile *target, FivThumbnailSize max_size, GError **error)
{
const char *path = g_file_peek_path(target);
if (!path) {
set_error(error, "thumbnails will only be extracted from local files");
return NULL;
}
GMappedFile *mf = g_mapped_file_new(path, FALSE, error);
if (!mf)
return NULL;
cairo_surface_t *surface = NULL;
#ifndef HAVE_LIBRAW
// TODO(p): Implement our own thumbnail extractors.
set_error(error, "unsupported file");
#else // HAVE_LIBRAW
libraw_data_t *iprc = libraw_init(
LIBRAW_OPIONS_NO_MEMERR_CALLBACK | LIBRAW_OPIONS_NO_DATAERR_CALLBACK);
if (!iprc) {
set_error(error, "failed to obtain a LibRaw handle");
goto fail;
}
int err = 0;
if ((err = libraw_open_buffer(iprc, (void *) g_mapped_file_get_contents(mf),
g_mapped_file_get_length(mf))) ||
(err = libraw_unpack_thumb(iprc))) {
set_error(error, libraw_strerror(err));
goto fail_libraw;
}
libraw_processed_image_t *image = libraw_dcraw_make_mem_thumb(iprc, &err);
if (!image) {
set_error(error, libraw_strerror(err));
goto fail_libraw;
}
gboolean dummy = FALSE;
switch (image->type) {
case LIBRAW_IMAGE_JPEG:
surface = render(
target, g_bytes_new(image->data, image->data_size), &dummy, error);
break;
case LIBRAW_IMAGE_BITMAP:
// TODO(p): Implement this one as well.
default:
set_error(error, "unsupported embedded thumbnail");
}
libraw_dcraw_clear_mem(image);
fail_libraw:
libraw_close(iprc);
#endif // HAVE_LIBRAW
fail:
g_mapped_file_unref(mf);
if (!surface || max_size < FIV_THUMBNAIL_SIZE_MIN ||
max_size > FIV_THUMBNAIL_SIZE_MAX)
return surface;
cairo_surface_t *result =
adjust_thumbnail(surface, fiv_thumbnail_sizes[max_size].size);
cairo_surface_destroy(surface);
return result;
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
static WebPData
encode_thumbnail(cairo_surface_t *surface)
{
@ -238,30 +334,7 @@ save_thumbnail(cairo_surface_t *thumbnail, const char *path, GString *thum)
}
static cairo_surface_t *
render(GFile *target, GBytes *data, gboolean *color_managed, GError **error)
{
FivIoOpenContext ctx = {
.uri = g_file_get_uri(target),
.screen_profile = fiv_io_profile_new_sRGB(),
.screen_dpi = 96,
.first_frame_only = TRUE,
// Only using this array as a redirect.
.warnings = g_ptr_array_new_with_free_func(g_free),
};
cairo_surface_t *surface = fiv_io_open_from_data(
g_bytes_get_data(data, NULL), g_bytes_get_size(data), &ctx, error);
g_free((gchar *) ctx.uri);
g_ptr_array_free(ctx.warnings, TRUE);
if ((*color_managed = !!ctx.screen_profile))
fiv_io_profile_free(ctx.screen_profile);
g_bytes_unref(data);
return surface;
}
static gboolean
produce_fallback(GFile *target, FivThumbnailSize size,
cairo_surface_t **surface, GError **error)
produce_fallback(GFile *target, FivThumbnailSize size, GError **error)
{
goffset filesize = 0;
GFileInfo *info = g_file_query_info(target,
@ -276,40 +349,39 @@ produce_fallback(GFile *target, FivThumbnailSize size,
// For example, we can employ magic checks.
if (filesize > 10 << 20) {
set_error(error, "oversize, not thumbnailing");
return FALSE;
return NULL;
}
GBytes *data = g_file_load_bytes(target, NULL, NULL, error);
if (!data)
return FALSE;
return NULL;
gboolean color_managed = FALSE;
cairo_surface_t *result = render(target, data, &color_managed, error);
if (!result)
return FALSE;
cairo_surface_t *surface = render(target, data, &color_managed, error);
if (!surface)
return NULL;
if (!*surface)
*surface = adjust_thumbnail(result, fiv_thumbnail_sizes[size].size);
cairo_surface_destroy(result);
return TRUE;
cairo_surface_t *result =
adjust_thumbnail(surface, fiv_thumbnail_sizes[size].size);
cairo_surface_destroy(surface);
return result;
}
gboolean
fiv_thumbnail_produce(GFile *target, FivThumbnailSize max_size,
cairo_surface_t **max_size_surface, GError **error)
cairo_surface_t *
fiv_thumbnail_produce(GFile *target, FivThumbnailSize max_size, GError **error)
{
g_return_val_if_fail(max_size >= FIV_THUMBNAIL_SIZE_MIN &&
max_size <= FIV_THUMBNAIL_SIZE_MAX, FALSE);
const gchar *path = g_file_peek_path(target);
if (!path || !g_file_is_native(target) /* Don't save sftp://. */)
return produce_fallback(target, max_size, max_size_surface, error);
return produce_fallback(target, max_size, error);
// Make the TOCTTOU issue favour unnecessary reloading.
GStatBuf st = {};
if (g_stat(path, &st)) {
set_error(error, g_strerror(errno));
return FALSE;
return NULL;
}
GError *e = NULL;
@ -317,7 +389,7 @@ fiv_thumbnail_produce(GFile *target, FivThumbnailSize max_size,
if (!mf) {
g_debug("%s: %s", path, e->message);
g_error_free(e);
return produce_fallback(target, max_size, max_size_surface, error);
return produce_fallback(target, max_size, error);
}
gsize filesize = g_mapped_file_get_length(mf);
@ -326,7 +398,7 @@ fiv_thumbnail_produce(GFile *target, FivThumbnailSize max_size,
render(target, g_mapped_file_get_bytes(mf), &color_managed, error);
g_mapped_file_unref(mf);
if (!surface)
return FALSE;
return NULL;
// Boilerplate copied from fiv_thumbnail_lookup().
gchar *uri = g_file_get_uri(target);
@ -354,6 +426,7 @@ fiv_thumbnail_produce(GFile *target, FivThumbnailSize max_size,
thum, "%s%c%s%c", THUMB_COLORSPACE, 0, THUMB_COLORSPACE_SRGB, 0);
}
cairo_surface_t *max_size_surface = NULL;
for (int use = max_size; use >= FIV_THUMBNAIL_SIZE_MIN; use--) {
cairo_surface_t *scaled =
adjust_thumbnail(surface, fiv_thumbnail_sizes[use].size);
@ -362,8 +435,8 @@ fiv_thumbnail_produce(GFile *target, FivThumbnailSize max_size,
save_thumbnail(scaled, path, thum);
g_free(path);
if (!*max_size_surface)
*max_size_surface = scaled;
if (!max_size_surface)
max_size_surface = scaled;
else
cairo_surface_destroy(scaled);
}
@ -374,7 +447,7 @@ fiv_thumbnail_produce(GFile *target, FivThumbnailSize max_size,
g_free(sum);
g_free(uri);
cairo_surface_destroy(surface);
return TRUE;
return max_size_surface;
}
static bool

View File

@ -45,20 +45,25 @@ typedef struct _FivThumbnailSizeInfo {
const char *thumbnail_spec_name; ///< thumbnail-spec directory name
} FivThumbnailSizeInfo;
extern FivThumbnailSizeInfo fiv_thumbnail_sizes[FIV_THUMBNAIL_SIZE_COUNT];
enum { FIV_THUMBNAIL_WIDE_COEFFICIENT = 2 };
extern FivThumbnailSizeInfo fiv_thumbnail_sizes[FIV_THUMBNAIL_SIZE_COUNT];
/// If non-NULL, indicates a thumbnail of insufficient quality.
extern cairo_user_data_key_t fiv_thumbnail_key_lq;
/// Returns this user's root thumbnail directory.
gchar *fiv_thumbnail_get_root(void);
/// Attempts to extract any low-quality thumbnail from fast targets.
/// If `max_size` is a valid value, the image will be downscaled as appropriate.
cairo_surface_t *fiv_thumbnail_extract(
GFile *target, FivThumbnailSize max_size, GError **error);
/// Generates wide thumbnails of up to the specified size, saves them in cache.
/// Returns the surface used for the maximum size (if the pointer was NULL).
gboolean fiv_thumbnail_produce(GFile *target, FivThumbnailSize max_size,
cairo_surface_t **max_size_surface, GError **error);
/// Returns the surface used for the maximum size, or an error.
cairo_surface_t *fiv_thumbnail_produce(
GFile *target, FivThumbnailSize max_size, GError **error);
/// Retrieves a thumbnail of the most appropriate quality and resolution
/// for the target file.

66
fiv.c
View File

@ -1767,11 +1767,47 @@ static const char stylesheet[] = "@define-color fiv-tile @content_view_bg; \
} \
.fiv-information label { padding: 0 4px; }";
static void
output_thumbnail(const char *path_arg, gboolean extract, const char *size_arg)
{
if (!path_arg)
exit_fatal("no path given");
FivThumbnailSize size = FIV_THUMBNAIL_SIZE_COUNT;
if (size_arg) {
for (size = 0; size < FIV_THUMBNAIL_SIZE_COUNT; size++) {
if (!strcmp(
fiv_thumbnail_sizes[size].thumbnail_spec_name, size_arg))
break;
}
if (size >= FIV_THUMBNAIL_SIZE_COUNT)
exit_fatal("unknown thumbnail size: %s", size_arg);
}
GError *error = NULL;
GFile *file = g_file_new_for_commandline_arg(path_arg);
cairo_surface_t *surface = NULL;
if (extract && (surface = fiv_thumbnail_extract(file, size, &error)))
fiv_io_serialize_to_stdout(surface, FIV_IO_SERIALIZE_LOW_QUALITY);
else if (size_arg &&
(g_clear_error(&error),
(surface = fiv_thumbnail_produce(file, size, &error))))
fiv_io_serialize_to_stdout(surface, 0);
else
g_assert(error != NULL);
g_object_unref(file);
if (error)
exit_fatal("%s", error->message);
cairo_surface_destroy(surface);
}
int
main(int argc, char *argv[])
{
gboolean show_version = FALSE, show_supported_media_types = FALSE,
invalidate_cache = FALSE, browse = FALSE;
invalidate_cache = FALSE, browse = FALSE, extract_thumbnail = FALSE;
gchar **path_args = NULL, *thumbnail_size = NULL;
const GOptionEntry options[] = {
{G_OPTION_REMAINING, 0, 0, G_OPTION_ARG_FILENAME_ARRAY, &path_args,
@ -1782,9 +1818,12 @@ main(int argc, char *argv[])
{"browse", 0, G_OPTION_FLAG_IN_MAIN,
G_OPTION_ARG_NONE, &browse,
"Start in filesystem browsing mode", NULL},
{"extract-thumbnail", 0, G_OPTION_FLAG_IN_MAIN,
G_OPTION_ARG_NONE, &extract_thumbnail,
"Output any embedded thumbnail (superseding --thumbnail)", 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"},
"Generate thumbnails, up to SIZE, and output that size", "SIZE"},
{"invalidate-cache", 0, G_OPTION_FLAG_IN_MAIN,
G_OPTION_ARG_NONE, &invalidate_cache,
"Invalidate the wide thumbnail cache", NULL},
@ -1820,27 +1859,8 @@ main(int argc, char *argv[])
// 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");
FivThumbnailSize size = 0;
for (; size < FIV_THUMBNAIL_SIZE_COUNT; size++)
if (!strcmp(fiv_thumbnail_sizes[size].thumbnail_spec_name,
thumbnail_size))
break;
if (size >= FIV_THUMBNAIL_SIZE_COUNT)
exit_fatal("unknown thumbnail size: %s", thumbnail_size);
GFile *target = g_file_new_for_commandline_arg(path_arg);
cairo_surface_t *surface = NULL;
if (!fiv_thumbnail_produce(target, size, &surface, &error))
exit_fatal("%s", error->message);
g_object_unref(target);
if (surface) {
fiv_io_serialize_to_stdout(surface);
cairo_surface_destroy(surface);
}
if (extract_thumbnail || thumbnail_size) {
output_thumbnail(path_arg, extract_thumbnail, thumbnail_size);
return 0;
}