diff --git a/fastiv.c b/fastiv.c index b253df1..0a8b426 100644 --- a/fastiv.c +++ b/fastiv.c @@ -31,6 +31,7 @@ #include "fiv-browser.h" #include "fiv-io.h" #include "fiv-sidebar.h" +#include "fiv-thumbnail.h" #include "fiv-view.h" #include "xdg.h" @@ -644,12 +645,12 @@ on_open_location(G_GNUC_UNUSED GtkPlacesSidebar *sidebar, GFile *location, static void on_toolbar_zoom(G_GNUC_UNUSED GtkButton *button, gpointer user_data) { - FivIoThumbnailSize size = FIV_IO_THUMBNAIL_SIZE_COUNT; + FivThumbnailSize size = FIV_THUMBNAIL_SIZE_COUNT; g_object_get(g.browser, "thumbnail-size", &size, NULL); size += (gintptr) user_data; - g_return_if_fail(size >= FIV_IO_THUMBNAIL_SIZE_MIN && - size <= FIV_IO_THUMBNAIL_SIZE_MAX); + g_return_if_fail(size >= FIV_THUMBNAIL_SIZE_MIN && + size <= FIV_THUMBNAIL_SIZE_MAX); g_object_set(g.browser, "thumbnail-size", size, NULL); } @@ -658,10 +659,10 @@ static void on_notify_thumbnail_size( GObject *object, GParamSpec *param_spec, G_GNUC_UNUSED gpointer user_data) { - FivIoThumbnailSize size = 0; + FivThumbnailSize size = 0; g_object_get(object, g_param_spec_get_name(param_spec), &size, NULL); - gtk_widget_set_sensitive(g.plus, size < FIV_IO_THUMBNAIL_SIZE_MAX); - gtk_widget_set_sensitive(g.minus, size > FIV_IO_THUMBNAIL_SIZE_MIN); + gtk_widget_set_sensitive(g.plus, size < FIV_THUMBNAIL_SIZE_MAX); + gtk_widget_set_sensitive(g.minus, size > FIV_THUMBNAIL_SIZE_MIN); } static void @@ -1236,16 +1237,16 @@ main(int argc, char *argv[]) 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, + 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_IO_THUMBNAIL_SIZE_COUNT) + if (size >= FIV_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)) + if (!fiv_thumbnail_produce(target, size, &error)) exit_fatal("%s", error->message); g_object_unref(target); return 0; diff --git a/fiv-browser.c b/fiv-browser.c index 84828d2..0ef212e 100644 --- a/fiv-browser.c +++ b/fiv-browser.c @@ -22,6 +22,7 @@ #include "fiv-browser.h" #include "fiv-io.h" +#include "fiv-thumbnail.h" #include "fiv-view.h" // --- Widget ------------------------------------------------------------------ @@ -43,7 +44,7 @@ struct _FivBrowser { GtkWidget parent_instance; - FivIoThumbnailSize item_size; ///< Thumbnail size + FivThumbnailSize item_size; ///< Thumbnail size int item_height; ///< Thumbnail height in pixels int item_spacing; ///< Space between items in pixels @@ -303,8 +304,8 @@ rescale_thumbnail(cairo_surface_t *thumbnail, double row_height) double scale_x = 1; double scale_y = 1; - if (width > FIV_IO_WIDE_THUMBNAIL_COEFFICIENT * height) { - scale_x = FIV_IO_WIDE_THUMBNAIL_COEFFICIENT * row_height / width; + if (width > FIV_THUMBNAIL_WIDE_COEFFICIENT * height) { + scale_x = FIV_THUMBNAIL_WIDE_COEFFICIENT * row_height / width; scale_y = round(scale_x * height) / height; } else { scale_y = row_height / height; @@ -350,7 +351,7 @@ rescale_thumbnail(cairo_surface_t *thumbnail, double row_height) pixman_image_unref(dest); cairo_surface_set_user_data( - scaled, &fiv_io_key_thumbnail_lq, (void *) (intptr_t) 1, NULL); + scaled, &fiv_thumbnail_key_lq, (void *) (intptr_t) 1, NULL); cairo_surface_destroy(thumbnail); cairo_surface_mark_dirty(scaled); return scaled; @@ -366,8 +367,7 @@ entry_add_thumbnail(gpointer data, gpointer user_data) FivBrowser *browser = FIV_BROWSER(user_data); GFile *file = g_file_new_for_uri(self->uri); self->thumbnail = rescale_thumbnail( - fiv_io_lookup_thumbnail(file, browser->item_size), - browser->item_height); + fiv_thumbnail_lookup(file, browser->item_size), browser->item_height); if (self->thumbnail) goto out; @@ -508,7 +508,7 @@ thumbnailer_next(FivBrowser *self) 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, + fiv_thumbnail_sizes[self->item_size].thumbnail_spec_name, "--", path, NULL); g_free(path); if (error) { @@ -542,7 +542,7 @@ thumbnailer_start(FivBrowser *self) thumbnailer_abort(self); // TODO(p): Leave out all paths containing .cache/thumbnails altogether. - gchar *thumbnails_dir = fiv_io_get_thumbnail_root(); + gchar *thumbnails_dir = fiv_thumbnail_get_root(); GFile *thumbnails = g_file_new_for_path(thumbnails_dir); g_free(thumbnails_dir); GFile *current = g_file_new_for_path(self->path); @@ -558,7 +558,7 @@ thumbnailer_start(FivBrowser *self) if (entry->icon) missing = g_list_prepend(missing, entry); else if (cairo_surface_get_user_data( - entry->thumbnail, &fiv_io_key_thumbnail_lq)) + entry->thumbnail, &fiv_thumbnail_key_lq)) lq = g_list_prepend(lq, entry); } @@ -778,14 +778,14 @@ fiv_browser_get_property( } static void -set_item_size(FivBrowser *self, FivIoThumbnailSize size) +set_item_size(FivBrowser *self, FivThumbnailSize size) { - if (size < FIV_IO_THUMBNAIL_SIZE_MIN || size > FIV_IO_THUMBNAIL_SIZE_MAX) + if (size < FIV_THUMBNAIL_SIZE_MIN || size > FIV_THUMBNAIL_SIZE_MAX) return; if (size != self->item_size) { self->item_size = size; - self->item_height = fiv_io_thumbnail_sizes[self->item_size].size; + self->item_height = fiv_thumbnail_sizes[self->item_size].size; reload_thumbnails(self); g_object_notify_by_pspec( @@ -822,7 +822,7 @@ fiv_browser_get_preferred_width(GtkWidget *widget, gint *minimum, gint *natural) GtkBorder padding = {}; gtk_style_context_get_padding(style, GTK_STATE_FLAG_NORMAL, &padding); *minimum = *natural = - FIV_IO_WIDE_THUMBNAIL_COEFFICIENT * self->item_height + padding.left + + FIV_THUMBNAIL_WIDE_COEFFICIENT * self->item_height + padding.left + 2 * self->item_border_x + padding.right; } @@ -1100,7 +1100,7 @@ fiv_browser_class_init(FivBrowserClass *klass) browser_properties[PROP_THUMBNAIL_SIZE] = g_param_spec_enum( "thumbnail-size", "Thumbnail size", "The thumbnail height to use", - FIV_TYPE_IO_THUMBNAIL_SIZE, FIV_IO_THUMBNAIL_SIZE_NORMAL, + FIV_TYPE_THUMBNAIL_SIZE, FIV_THUMBNAIL_SIZE_NORMAL, G_PARAM_READWRITE); g_object_class_install_properties( object_class, N_PROPERTIES, browser_properties); @@ -1145,7 +1145,7 @@ fiv_browser_init(FivBrowser *self) self->layouted_rows = g_array_new(FALSE, TRUE, sizeof(Row)); g_array_set_clear_func(self->layouted_rows, (GDestroyNotify) row_free); - set_item_size(self, FIV_IO_THUMBNAIL_SIZE_NORMAL); + set_item_size(self, FIV_THUMBNAIL_SIZE_NORMAL); self->selected = -1; self->glow = cairo_image_surface_create(CAIRO_FORMAT_A1, 0, 0); diff --git a/fiv-io.c b/fiv-io.c index 26b6e89..759cb4e 100644 --- a/fiv-io.c +++ b/fiv-io.c @@ -20,9 +20,7 @@ #include #include #include -#include -#include #include #include #include @@ -78,7 +76,6 @@ #include "wuffs-mirror-release-c/release/c/wuffs-v0.3.c" #include "fiv-io.h" -#include "xdg.h" #if CAIRO_VERSION >= 11702 && X11_ACTUALLY_SUPPORTS_RGBA128F_OR_WE_USE_OPENGL #define FIV_CAIRO_RGBA128F @@ -135,6 +132,22 @@ fiv_io_all_supported_media_types(void) return (char **) g_ptr_array_free(types, FALSE); } +int +fiv_io_filecmp(GFile *location1, GFile *location2) +{ + if (g_file_has_prefix(location1, location2)) + return +1; + if (g_file_has_prefix(location2, location1)) + return -1; + + gchar *name1 = g_file_get_parse_name(location1); + gchar *name2 = g_file_get_parse_name(location2); + int result = g_utf8_collate(name1, name2); + g_free(name1); + g_free(name2); + return result; +} + // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - #define FIV_IO_ERROR fiv_io_error_quark() @@ -421,9 +434,6 @@ fiv_io_profile_finalize(cairo_surface_t *image, FivIoProfile target) return image; } -// From libwebp, verified to exactly match [x * a / 255]. -#define PREMULTIPLY8(a, x) (((uint32_t) (x) * (uint32_t) (a) * 32897U) >> 23) - static void fiv_io_premultiply_argb32(cairo_surface_t *surface) { @@ -2297,8 +2307,6 @@ cairo_user_data_key_t fiv_io_key_loops; cairo_user_data_key_t fiv_io_key_page_next; cairo_user_data_key_t fiv_io_key_page_previous; -cairo_user_data_key_t fiv_io_key_thumbnail_lq; - cairo_surface_t * fiv_io_open( const gchar *path, FivIoProfile profile, gboolean enhance, GError **error) @@ -2754,465 +2762,3 @@ fiv_io_save_metadata(cairo_surface_t *page, const gchar *path, GError **error) } return TRUE; } - -// --- Thumbnails -------------------------------------------------------------- - -#ifndef __linux__ -#define st_mtim st_mtimespec -#endif // ! __linux__ - -GType -fiv_io_thumbnail_size_get_type(void) -{ - static gsize guard; - if (g_once_init_enter(&guard)) { -#define XX(name, value, dir) {FIV_IO_THUMBNAIL_SIZE_ ## name, \ - "FIV_IO_THUMBNAIL_SIZE_" #name, #name}, - static const GEnumValue values[] = {FIV_IO_THUMBNAIL_SIZES(XX) {}}; -#undef XX - GType type = g_enum_register_static( - g_intern_static_string("FivIoThumbnailSize"), values); - g_once_init_leave(&guard, type); - } - return guard; -} - -#define XX(name, value, dir) {value, dir}, -FivIoThumbnailSizeInfo - fiv_io_thumbnail_sizes[FIV_IO_THUMBNAIL_SIZE_COUNT] = { - FIV_IO_THUMBNAIL_SIZES(XX)}; -#undef XX - -static void -mark_thumbnail_lq(cairo_surface_t *surface) -{ - cairo_surface_set_user_data( - surface, &fiv_io_key_thumbnail_lq, (void *) (intptr_t) 1, NULL); -} - -gchar * -fiv_io_get_thumbnail_root(void) -{ - gchar *cache_dir = get_xdg_home_dir("XDG_CACHE_HOME", ".cache"); - gchar *thumbnails_dir = g_build_filename(cache_dir, "thumbnails", NULL); - g_free(cache_dir); - return thumbnails_dir; -} - -// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -// In principle similar to rescale_thumbnail() from fiv-browser.c. -static cairo_surface_t * -rescale_thumbnail(cairo_surface_t *thumbnail, double row_height) -{ - cairo_format_t format = cairo_image_surface_get_format(thumbnail); - 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 > FIV_IO_WIDE_THUMBNAIL_COEFFICIENT * height) { - scale_x = FIV_IO_WIDE_THUMBNAIL_COEFFICIENT * 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); - - int projected_width = round(scale_x * width); - int projected_height = round(scale_y * height); - cairo_surface_t *scaled = cairo_image_surface_create( - (format == CAIRO_FORMAT_RGB24 || format == CAIRO_FORMAT_RGB30) - ? CAIRO_FORMAT_RGB24 - : CAIRO_FORMAT_ARGB32, - 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); - mark_thumbnail_lq(scaled); - return scaled; -} - -static WebPData -encode_thumbnail(cairo_surface_t *surface) -{ - WebPData bitstream = {}; - WebPConfig config = {}; - if (!WebPConfigInit(&config) || !WebPConfigLosslessPreset(&config, 6)) - return bitstream; - - config.near_lossless = 95; - config.thread_level = true; - if (!WebPValidateConfig(&config)) - return bitstream; - - bitstream.bytes = fiv_io_encode_webp(surface, &config, &bitstream.size); - return bitstream; -} - -static void -save_thumbnail(cairo_surface_t *thumbnail, const char *path, GString *thum) -{ - WebPMux *mux = WebPMuxNew(); - WebPData bitstream = encode_thumbnail(thumbnail); - gboolean ok = WebPMuxSetImage(mux, &bitstream, true) == WEBP_MUX_OK; - WebPDataClear(&bitstream); - - WebPData data = {.bytes = (const uint8_t *) thum->str, .size = thum->len}; - ok = ok && WebPMuxSetChunk(mux, "THUM", &data, false) == WEBP_MUX_OK; - - WebPData assembled = {}; - WebPDataInit(&assembled); - ok = ok && WebPMuxAssemble(mux, &assembled) == WEBP_MUX_OK; - WebPMuxDelete(mux); - if (!ok) { - g_warning("thumbnail encoding failed"); - return; - } - - GError *e = NULL; - while (!g_file_set_contents( - path, (const gchar *) assembled.bytes, assembled.size, &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. - WebPDataClear(&assembled); -} - -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(); - gsize filesize = g_mapped_file_get_length(mf); - cairo_surface_t *surface = fiv_io_open_from_data( - g_mapped_file_get_contents(mf), filesize, 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 *thumbnails_dir = fiv_io_get_thumbnail_root(); - - GString *thum = g_string_new(""); - g_string_append_printf( - thum, "%s%c%s%c", "Thumb::URI", 0, uri, 0); - g_string_append_printf( - thum, "%s%c%ld%c", "Thumb::Mtime", 0, (long) st.st_mtim.tv_sec, 0); - g_string_append_printf( - thum, "%s%c%ld%c", "Thumb::Size", 0, (long) filesize, 0); - g_string_append_printf(thum, "%s%c%d%c", "Thumb::Image::Width", 0, - cairo_image_surface_get_width(surface), 0); - g_string_append_printf(thum, "%s%c%d%c", "Thumb::Image::Height", 0, - cairo_image_surface_get_height(surface), 0); - - // Without a CMM, no conversion is attempted. - if (sRGB) { - g_string_append_printf( - thum, "%s%c%s%c", "Thumb::ColorSpace", 0, "sRGB", 0); - } - - 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/wide-%s/%s.webp", thumbnails_dir, - fiv_io_thumbnail_sizes[use].thumbnail_spec_name, sum); - save_thumbnail(scaled, path, thum); - cairo_surface_destroy(scaled); - g_free(path); - } - - g_string_free(thum, TRUE); - - g_free(thumbnails_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 fiv_io_key_thum. - (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, - const gchar *target, time_t mtime) -{ - // May contain Thumb::Image::Width Thumb::Image::Height, - // but those aren't interesting currently (would be for fast previews). - bool need_uri = true, need_mtime = true; - for (uint32_t i = 0; i < texts_len; i++) { - struct spng_text *text = texts + i; - if (!strcmp(text->keyword, "Thumb::URI")) { - need_uri = false; - if (strcmp(target, text->text)) - return false; - } - if (!strcmp(text->keyword, "Thumb::MTime")) { - need_mtime = false; - if (atol(text->text) != mtime) - return false; - } - } - return need_uri || need_mtime ? -1 : true; -} - -static int // tri-state -check_spng_thumbnail(spng_ctx *ctx, const gchar *target, time_t mtime, int *err) -{ - uint32_t texts_len = 0; - if ((*err = spng_get_text(ctx, NULL, &texts_len))) - return false; - - int result = false; - struct spng_text *texts = g_malloc0_n(texts_len, sizeof *texts); - if (!(*err = spng_get_text(ctx, texts, &texts_len))) - result = check_spng_thumbnail_texts(texts, texts_len, target, mtime); - g_free(texts); - return result; -} - -static cairo_surface_t * -read_spng_thumbnail( - const gchar *path, const gchar *uri, time_t mtime, GError **error) -{ - FILE *fp; - cairo_surface_t *result = NULL; - if (!(fp = fopen(path, "rb"))) { - set_error(error, g_strerror(errno)); - return NULL; - } - - errno = 0; - spng_ctx *ctx = spng_ctx_new(0); - if (!ctx) { - set_error(error, g_strerror(errno)); - goto fail_init; - } - - int err; - size_t size = 0; - if ((err = spng_set_png_file(ctx, fp)) || - (err = spng_set_image_limits(ctx, INT16_MAX, INT16_MAX)) || - (err = spng_decoded_image_size(ctx, SPNG_FMT_RGBA8, &size))) { - set_error(error, spng_strerror(err)); - goto fail; - } - if (check_spng_thumbnail(ctx, uri, mtime, &err) == false) { - set_error(error, err ? spng_strerror(err) : "mismatch"); - goto fail; - } - - struct spng_ihdr ihdr = {}; - struct spng_trns trns = {}; - spng_get_ihdr(ctx, &ihdr); - bool may_be_translucent = !spng_get_trns(ctx, &trns) || - ihdr.color_type == SPNG_COLOR_TYPE_GRAYSCALE_ALPHA || - ihdr.color_type == SPNG_COLOR_TYPE_TRUECOLOR_ALPHA; - - cairo_surface_t *surface = cairo_image_surface_create( - may_be_translucent ? CAIRO_FORMAT_ARGB32 : CAIRO_FORMAT_RGB24, - ihdr.width, ihdr.height); - - cairo_status_t surface_status = cairo_surface_status(surface); - if (surface_status != CAIRO_STATUS_SUCCESS) { - set_error(error, cairo_status_to_string(surface_status)); - goto fail_data; - } - - uint32_t *data = (uint32_t *) cairo_image_surface_get_data(surface); - g_assert((size_t) cairo_image_surface_get_stride(surface) * - cairo_image_surface_get_height(surface) == size); - - cairo_surface_flush(surface); - if ((err = spng_decode_image(ctx, data, size, SPNG_FMT_RGBA8, - SPNG_DECODE_TRNS | SPNG_DECODE_GAMMA))) { - set_error(error, spng_strerror(err)); - goto fail_data; - } - - // The specification does not say where the required metadata should be, - // it could very well be broken up into two parts. - if (check_spng_thumbnail(ctx, uri, mtime, &err) != true) { - set_error( - error, err ? spng_strerror(err) : "mismatch or not a thumbnail"); - goto fail_data; - } - - // pixman can be mildly abused to do this operation, but it won't be faster. - if (may_be_translucent) { - for (size_t i = size / sizeof *data; i--; ) { - const uint8_t *unit = (const uint8_t *) &data[i]; - uint32_t a = unit[3], - b = PREMULTIPLY8(a, unit[2]), - g = PREMULTIPLY8(a, unit[1]), - r = PREMULTIPLY8(a, unit[0]); - data[i] = a << 24 | r << 16 | g << 8 | b; - } - } else { - for (size_t i = size / sizeof *data; i--; ) { - uint32_t rgba = g_ntohl(data[i]); - data[i] = rgba << 24 | rgba >> 8; - } - } - - cairo_surface_mark_dirty((result = surface)); - -fail_data: - if (!result) - cairo_surface_destroy(surface); -fail: - spng_ctx_free(ctx); -fail_init: - fclose(fp); - return result; -} - -cairo_surface_t * -fiv_io_lookup_thumbnail(GFile *target, FivIoThumbnailSize size) -{ - g_return_val_if_fail(size >= FIV_IO_THUMBNAIL_SIZE_MIN && - size <= FIV_IO_THUMBNAIL_SIZE_MAX, NULL); - - // Local files only, at least for now. - gchar *path = g_file_get_path(target); - if (!path) - return NULL; - - GStatBuf st = {}; - int err = g_stat(path, &st); - g_free(path); - if (err) - return NULL; - - gchar *uri = g_file_get_uri(target); - gchar *sum = g_compute_checksum_for_string(G_CHECKSUM_MD5, uri, -1); - gchar *thumbnails_dir = fiv_io_get_thumbnail_root(); - - // The lookup sequence is: nominal..max, then mirroring back to ..min. - cairo_surface_t *result = NULL; - GError *error = NULL; - for (int i = 0; i < FIV_IO_THUMBNAIL_SIZE_COUNT; i++) { - FivIoThumbnailSize use = size + i; - if (use > FIV_IO_THUMBNAIL_SIZE_MAX) - use = FIV_IO_THUMBNAIL_SIZE_MAX - i; - - const char *name = fiv_io_thumbnail_sizes[use].thumbnail_spec_name; - gchar *wide = - g_strdup_printf("%s/wide-%s/%s.webp", thumbnails_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) { - // Higher up we can't distinguish images smaller than the thumbnail. - // Also, try not to rescale the already rescaled. - if (use != size) - mark_thumbnail_lq(result); - break; - } - - gchar *path = - g_strdup_printf("%s/%s/%s.png", thumbnails_dir, name, sum); - result = read_spng_thumbnail(path, uri, st.st_mtim.tv_sec, &error); - if (error) { - g_debug("%s: %s", path, error->message); - g_clear_error(&error); - } - g_free(path); - if (result) { - // Whatever produced it, we may be able to outclass it. - mark_thumbnail_lq(result); - 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(thumbnails_dir); - g_free(sum); - g_free(uri); - return result; -} - -int -fiv_io_filecmp(GFile *location1, GFile *location2) -{ - if (g_file_has_prefix(location1, location2)) - return +1; - if (g_file_has_prefix(location2, location1)) - return -1; - - gchar *name1 = g_file_get_parse_name(location1); - gchar *name2 = g_file_get_parse_name(location2); - int result = g_utf8_collate(name1, name2); - g_free(name1); - g_free(name2); - return result; -} diff --git a/fiv-io.h b/fiv-io.h index 52fd691..54e2727 100644 --- a/fiv-io.h +++ b/fiv-io.h @@ -31,6 +31,9 @@ FivIoProfile fiv_io_profile_new(const void *data, size_t len); FivIoProfile fiv_io_profile_new_sRGB(void); void fiv_io_profile_free(FivIoProfile self); +// From libwebp, verified to exactly match [x * a / 255]. +#define PREMULTIPLY8(a, x) (((uint32_t) (x) * (uint32_t) (a) * 32897U) >> 23) + // --- Loading ----------------------------------------------------------------- extern const char *fiv_io_supported_media_types[]; @@ -69,9 +72,6 @@ extern cairo_user_data_key_t fiv_io_key_page_next; /// There is no wrap-around. This is a weak pointer. extern cairo_user_data_key_t fiv_io_key_page_previous; -/// If non-NULL, indicates a thumbnail of insufficient quality. -extern cairo_user_data_key_t fiv_io_key_thumbnail_lq; - cairo_surface_t *fiv_io_open( const gchar *path, FivIoProfile profile, gboolean enhance, GError **error); cairo_surface_t *fiv_io_open_from_data(const char *data, size_t len, @@ -113,48 +113,3 @@ FivIoOrientation fiv_io_exif_orientation(const guint8 *exif, gsize len); /// Save metadata attached by this module in Exiv2 format. gboolean fiv_io_save_metadata( cairo_surface_t *page, const gchar *path, GError **error); - -// --- Thumbnails -------------------------------------------------------------- - -// And this is how you avoid glib-mkenums. -typedef enum _FivIoThumbnailSize { -#define FIV_IO_THUMBNAIL_SIZES(XX) \ - XX(SMALL, 128, "normal") \ - XX(NORMAL, 256, "large") \ - XX(LARGE, 512, "x-large") \ - XX(HUGE, 1024, "xx-large") -#define XX(name, value, dir) FIV_IO_THUMBNAIL_SIZE_ ## name, - FIV_IO_THUMBNAIL_SIZES(XX) -#undef XX - FIV_IO_THUMBNAIL_SIZE_COUNT, - - FIV_IO_THUMBNAIL_SIZE_MIN = 0, - FIV_IO_THUMBNAIL_SIZE_MAX = FIV_IO_THUMBNAIL_SIZE_COUNT - 1 -} FivIoThumbnailSize; - -GType fiv_io_thumbnail_size_get_type(void) G_GNUC_CONST; -#define FIV_TYPE_IO_THUMBNAIL_SIZE (fiv_io_thumbnail_size_get_type()) - -typedef struct _FivIoThumbnailSizeInfo { - int size; ///< Nominal size in pixels - const char *thumbnail_spec_name; ///< thumbnail-spec directory name -} FivIoThumbnailSizeInfo; - -extern FivIoThumbnailSizeInfo - fiv_io_thumbnail_sizes[FIV_IO_THUMBNAIL_SIZE_COUNT]; - -enum { - FIV_IO_WIDE_THUMBNAIL_COEFFICIENT = 2 -}; - -/// Returns this user's root thumbnail directory. -gchar *fiv_io_get_thumbnail_root(void); - -/// 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); diff --git a/fiv-thumbnail.c b/fiv-thumbnail.c new file mode 100644 index 0000000..1384213 --- /dev/null +++ b/fiv-thumbnail.c @@ -0,0 +1,494 @@ +// +// fiv-thumbnail.c: thumbnail management +// +// Copyright (c) 2021, Přemysl Eric Janouch +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY +// SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION +// OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN +// CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +// + +#include +#include +#include +#include + +#include +#include + +#include "fiv-io.h" +#include "fiv-thumbnail.h" +#include "xdg.h" + +#ifndef __linux__ +#define st_mtim st_mtimespec +#endif // ! __linux__ + +// TODO(p): Consider merging back with fiv-io. +#define FIV_THUMBNAIL_ERROR fiv_thumbnail_error_quark() + +G_DEFINE_QUARK(fiv-thumbnail-error-quark, fiv_thumbnail_error) + +enum FivThumbnailError { + FIV_THUMBNAIL_ERROR_IO +}; + +static void +set_error(GError **error, const char *message) +{ + g_set_error_literal( + error, FIV_THUMBNAIL_ERROR, FIV_THUMBNAIL_ERROR_IO, message); +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +GType +fiv_thumbnail_size_get_type(void) +{ + static gsize guard; + if (g_once_init_enter(&guard)) { +#define XX(name, value, dir) {FIV_THUMBNAIL_SIZE_ ## name, \ + "FIV_THUMBNAIL_SIZE_" #name, #name}, + static const GEnumValue values[] = {FIV_THUMBNAIL_SIZES(XX) {}}; +#undef XX + GType type = g_enum_register_static( + g_intern_static_string("FivThumbnailSize"), values); + g_once_init_leave(&guard, type); + } + return guard; +} + +#define XX(name, value, dir) {value, dir}, +FivThumbnailSizeInfo + fiv_thumbnail_sizes[FIV_THUMBNAIL_SIZE_COUNT] = { + FIV_THUMBNAIL_SIZES(XX)}; +#undef XX + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +cairo_user_data_key_t fiv_thumbnail_key_lq; + +static void +mark_thumbnail_lq(cairo_surface_t *surface) +{ + cairo_surface_set_user_data( + surface, &fiv_thumbnail_key_lq, (void *) (intptr_t) 1, NULL); +} + +gchar * +fiv_thumbnail_get_root(void) +{ + gchar *cache_dir = get_xdg_home_dir("XDG_CACHE_HOME", ".cache"); + gchar *thumbnails_dir = g_build_filename(cache_dir, "thumbnails", NULL); + g_free(cache_dir); + return thumbnails_dir; +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +// In principle similar to rescale_thumbnail() from fiv-browser.c. +static cairo_surface_t * +rescale_thumbnail(cairo_surface_t *thumbnail, double row_height) +{ + cairo_format_t format = cairo_image_surface_get_format(thumbnail); + 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 > FIV_THUMBNAIL_WIDE_COEFFICIENT * height) { + scale_x = FIV_THUMBNAIL_WIDE_COEFFICIENT * 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); + + int projected_width = round(scale_x * width); + int projected_height = round(scale_y * height); + cairo_surface_t *scaled = cairo_image_surface_create( + (format == CAIRO_FORMAT_RGB24 || format == CAIRO_FORMAT_RGB30) + ? CAIRO_FORMAT_RGB24 + : CAIRO_FORMAT_ARGB32, + 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); + mark_thumbnail_lq(scaled); + return scaled; +} + +static WebPData +encode_thumbnail(cairo_surface_t *surface) +{ + WebPData bitstream = {}; + WebPConfig config = {}; + if (!WebPConfigInit(&config) || !WebPConfigLosslessPreset(&config, 6)) + return bitstream; + + config.near_lossless = 95; + config.thread_level = true; + if (!WebPValidateConfig(&config)) + return bitstream; + + bitstream.bytes = fiv_io_encode_webp(surface, &config, &bitstream.size); + return bitstream; +} + +static void +save_thumbnail(cairo_surface_t *thumbnail, const char *path, GString *thum) +{ + WebPMux *mux = WebPMuxNew(); + WebPData bitstream = encode_thumbnail(thumbnail); + gboolean ok = WebPMuxSetImage(mux, &bitstream, true) == WEBP_MUX_OK; + WebPDataClear(&bitstream); + + WebPData data = {.bytes = (const uint8_t *) thum->str, .size = thum->len}; + ok = ok && WebPMuxSetChunk(mux, "THUM", &data, false) == WEBP_MUX_OK; + + WebPData assembled = {}; + WebPDataInit(&assembled); + ok = ok && WebPMuxAssemble(mux, &assembled) == WEBP_MUX_OK; + WebPMuxDelete(mux); + if (!ok) { + g_warning("thumbnail encoding failed"); + return; + } + + GError *e = NULL; + while (!g_file_set_contents( + path, (const gchar *) assembled.bytes, assembled.size, &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. + WebPDataClear(&assembled); +} + +gboolean +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); + + // 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(); + gsize filesize = g_mapped_file_get_length(mf); + cairo_surface_t *surface = fiv_io_open_from_data( + g_mapped_file_get_contents(mf), filesize, 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_thumbnail_lookup(). + gchar *uri = g_file_get_uri(target); + gchar *sum = g_compute_checksum_for_string(G_CHECKSUM_MD5, uri, -1); + gchar *thumbnails_dir = fiv_thumbnail_get_root(); + + GString *thum = g_string_new(""); + g_string_append_printf( + thum, "%s%c%s%c", "Thumb::URI", 0, uri, 0); + g_string_append_printf( + thum, "%s%c%ld%c", "Thumb::Mtime", 0, (long) st.st_mtim.tv_sec, 0); + g_string_append_printf( + thum, "%s%c%ld%c", "Thumb::Size", 0, (long) filesize, 0); + g_string_append_printf(thum, "%s%c%d%c", "Thumb::Image::Width", 0, + cairo_image_surface_get_width(surface), 0); + g_string_append_printf(thum, "%s%c%d%c", "Thumb::Image::Height", 0, + cairo_image_surface_get_height(surface), 0); + + // Without a CMM, no conversion is attempted. + if (sRGB) { + g_string_append_printf( + thum, "%s%c%s%c", "Thumb::ColorSpace", 0, "sRGB", 0); + } + + for (int use = max_size; use >= FIV_THUMBNAIL_SIZE_MIN; use--) { + cairo_surface_t *scaled = + rescale_thumbnail(surface, fiv_thumbnail_sizes[use].size); + gchar *path = g_strdup_printf("%s/wide-%s/%s.webp", thumbnails_dir, + fiv_thumbnail_sizes[use].thumbnail_spec_name, sum); + save_thumbnail(scaled, path, thum); + cairo_surface_destroy(scaled); + g_free(path); + } + + g_string_free(thum, TRUE); + + g_free(thumbnails_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 fiv_io_key_thum. + (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, + const gchar *target, time_t mtime) +{ + // May contain Thumb::Image::Width Thumb::Image::Height, + // but those aren't interesting currently (would be for fast previews). + bool need_uri = true, need_mtime = true; + for (uint32_t i = 0; i < texts_len; i++) { + struct spng_text *text = texts + i; + if (!strcmp(text->keyword, "Thumb::URI")) { + need_uri = false; + if (strcmp(target, text->text)) + return false; + } + if (!strcmp(text->keyword, "Thumb::MTime")) { + need_mtime = false; + if (atol(text->text) != mtime) + return false; + } + } + return need_uri || need_mtime ? -1 : true; +} + +static int // tri-state +check_spng_thumbnail(spng_ctx *ctx, const gchar *target, time_t mtime, int *err) +{ + uint32_t texts_len = 0; + if ((*err = spng_get_text(ctx, NULL, &texts_len))) + return false; + + int result = false; + struct spng_text *texts = g_malloc0_n(texts_len, sizeof *texts); + if (!(*err = spng_get_text(ctx, texts, &texts_len))) + result = check_spng_thumbnail_texts(texts, texts_len, target, mtime); + g_free(texts); + return result; +} + +static cairo_surface_t * +read_spng_thumbnail( + const gchar *path, const gchar *uri, time_t mtime, GError **error) +{ + FILE *fp; + cairo_surface_t *result = NULL; + if (!(fp = fopen(path, "rb"))) { + set_error(error, g_strerror(errno)); + return NULL; + } + + errno = 0; + spng_ctx *ctx = spng_ctx_new(0); + if (!ctx) { + set_error(error, g_strerror(errno)); + goto fail_init; + } + + int err; + size_t size = 0; + if ((err = spng_set_png_file(ctx, fp)) || + (err = spng_set_image_limits(ctx, INT16_MAX, INT16_MAX)) || + (err = spng_decoded_image_size(ctx, SPNG_FMT_RGBA8, &size))) { + set_error(error, spng_strerror(err)); + goto fail; + } + if (check_spng_thumbnail(ctx, uri, mtime, &err) == false) { + set_error(error, err ? spng_strerror(err) : "mismatch"); + goto fail; + } + + struct spng_ihdr ihdr = {}; + struct spng_trns trns = {}; + spng_get_ihdr(ctx, &ihdr); + bool may_be_translucent = !spng_get_trns(ctx, &trns) || + ihdr.color_type == SPNG_COLOR_TYPE_GRAYSCALE_ALPHA || + ihdr.color_type == SPNG_COLOR_TYPE_TRUECOLOR_ALPHA; + + cairo_surface_t *surface = cairo_image_surface_create( + may_be_translucent ? CAIRO_FORMAT_ARGB32 : CAIRO_FORMAT_RGB24, + ihdr.width, ihdr.height); + + cairo_status_t surface_status = cairo_surface_status(surface); + if (surface_status != CAIRO_STATUS_SUCCESS) { + set_error(error, cairo_status_to_string(surface_status)); + goto fail_data; + } + + uint32_t *data = (uint32_t *) cairo_image_surface_get_data(surface); + g_assert((size_t) cairo_image_surface_get_stride(surface) * + cairo_image_surface_get_height(surface) == size); + + cairo_surface_flush(surface); + if ((err = spng_decode_image(ctx, data, size, SPNG_FMT_RGBA8, + SPNG_DECODE_TRNS | SPNG_DECODE_GAMMA))) { + set_error(error, spng_strerror(err)); + goto fail_data; + } + + // The specification does not say where the required metadata should be, + // it could very well be broken up into two parts. + if (check_spng_thumbnail(ctx, uri, mtime, &err) != true) { + set_error( + error, err ? spng_strerror(err) : "mismatch or not a thumbnail"); + goto fail_data; + } + + // pixman can be mildly abused to do this operation, but it won't be faster. + if (may_be_translucent) { + for (size_t i = size / sizeof *data; i--; ) { + const uint8_t *unit = (const uint8_t *) &data[i]; + uint32_t a = unit[3], + b = PREMULTIPLY8(a, unit[2]), + g = PREMULTIPLY8(a, unit[1]), + r = PREMULTIPLY8(a, unit[0]); + data[i] = a << 24 | r << 16 | g << 8 | b; + } + } else { + for (size_t i = size / sizeof *data; i--; ) { + uint32_t rgba = g_ntohl(data[i]); + data[i] = rgba << 24 | rgba >> 8; + } + } + + cairo_surface_mark_dirty((result = surface)); + +fail_data: + if (!result) + cairo_surface_destroy(surface); +fail: + spng_ctx_free(ctx); +fail_init: + fclose(fp); + return result; +} + +cairo_surface_t * +fiv_thumbnail_lookup(GFile *target, FivThumbnailSize size) +{ + g_return_val_if_fail(size >= FIV_THUMBNAIL_SIZE_MIN && + size <= FIV_THUMBNAIL_SIZE_MAX, NULL); + + // Local files only, at least for now. + gchar *path = g_file_get_path(target); + if (!path) + return NULL; + + GStatBuf st = {}; + int err = g_stat(path, &st); + g_free(path); + if (err) + return NULL; + + gchar *uri = g_file_get_uri(target); + gchar *sum = g_compute_checksum_for_string(G_CHECKSUM_MD5, uri, -1); + gchar *thumbnails_dir = fiv_thumbnail_get_root(); + + // The lookup sequence is: nominal..max, then mirroring back to ..min. + cairo_surface_t *result = NULL; + GError *error = NULL; + for (int i = 0; i < FIV_THUMBNAIL_SIZE_COUNT; i++) { + FivThumbnailSize use = size + i; + if (use > FIV_THUMBNAIL_SIZE_MAX) + use = FIV_THUMBNAIL_SIZE_MAX - i; + + const char *name = fiv_thumbnail_sizes[use].thumbnail_spec_name; + gchar *wide = + g_strdup_printf("%s/wide-%s/%s.webp", thumbnails_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) { + // Higher up we can't distinguish images smaller than the thumbnail. + // Also, try not to rescale the already rescaled. + if (use != size) + mark_thumbnail_lq(result); + break; + } + + gchar *path = + g_strdup_printf("%s/%s/%s.png", thumbnails_dir, name, sum); + result = read_spng_thumbnail(path, uri, st.st_mtim.tv_sec, &error); + if (error) { + g_debug("%s: %s", path, error->message); + g_clear_error(&error); + } + g_free(path); + if (result) { + // Whatever produced it, we may be able to outclass it. + mark_thumbnail_lq(result); + 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(thumbnails_dir); + g_free(sum); + g_free(uri); + return result; +} diff --git a/fiv-thumbnail.h b/fiv-thumbnail.h new file mode 100644 index 0000000..9dcf3f6 --- /dev/null +++ b/fiv-thumbnail.h @@ -0,0 +1,64 @@ +// +// fiv-thumbnail.h: thumbnail management +// +// Copyright (c) 2021, Přemysl Eric Janouch +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY +// SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION +// OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN +// CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +// + +#pragma once + +#include +#include +#include + +// And this is how you avoid glib-mkenums. +typedef enum _FivThumbnailSize { +#define FIV_THUMBNAIL_SIZES(XX) \ + XX(SMALL, 128, "normal") \ + XX(NORMAL, 256, "large") \ + XX(LARGE, 512, "x-large") \ + XX(HUGE, 1024, "xx-large") +#define XX(name, value, dir) FIV_THUMBNAIL_SIZE_ ## name, + FIV_THUMBNAIL_SIZES(XX) +#undef XX + FIV_THUMBNAIL_SIZE_COUNT, + + FIV_THUMBNAIL_SIZE_MIN = 0, + FIV_THUMBNAIL_SIZE_MAX = FIV_THUMBNAIL_SIZE_COUNT - 1 +} FivThumbnailSize; + +GType fiv_thumbnail_size_get_type(void) G_GNUC_CONST; +#define FIV_TYPE_THUMBNAIL_SIZE (fiv_thumbnail_size_get_type()) + +typedef struct _FivThumbnailSizeInfo { + int size; ///< Nominal size in pixels + 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 }; + +/// 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); + +/// Generates wide thumbnails of up to the specified size, saves them in cache. +gboolean fiv_thumbnail_produce( + GFile *target, FivThumbnailSize max_size, GError **error); + +/// Retrieves a thumbnail of the most appropriate quality and resolution +/// for the target file. +cairo_surface_t *fiv_thumbnail_lookup(GFile *target, FivThumbnailSize size); diff --git a/meson.build b/meson.build index ecad76f..50ac6f2 100644 --- a/meson.build +++ b/meson.build @@ -71,7 +71,7 @@ resources = gnome.compile_resources('resources', ) exe = executable('fastiv', 'fastiv.c', 'fiv-view.c', 'fiv-io.c', - 'fiv-browser.c', 'fiv-sidebar.c', 'xdg.c', resources, + 'fiv-browser.c', 'fiv-sidebar.c', 'fiv-thumbnail.c', 'xdg.c', resources, install : true, dependencies : [dependencies])