aboutsummaryrefslogtreecommitdiff
path: root/fiv-thumbnail.c
diff options
context:
space:
mode:
Diffstat (limited to 'fiv-thumbnail.c')
-rw-r--r--fiv-thumbnail.c619
1 files changed, 440 insertions, 179 deletions
diff --git a/fiv-thumbnail.c b/fiv-thumbnail.c
index f95ed70..fffbac7 100644
--- a/fiv-thumbnail.c
+++ b/fiv-thumbnail.c
@@ -1,7 +1,7 @@
//
// fiv-thumbnail.c: thumbnail management
//
-// Copyright (c) 2021 - 2022, Přemysl Eric Janouch <p@janouch.name>
+// Copyright (c) 2021 - 2023, Přemysl Eric Janouch <p@janouch.name>
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted.
@@ -33,6 +33,9 @@
#ifdef HAVE_LIBRAW
#include <libraw.h>
+#if LIBRAW_VERSION >= LIBRAW_MAKE_VERSION(0, 21, 0)
+#define LIBRAW_OPIONS_NO_MEMERR_CALLBACK 0
+#endif
#endif // HAVE_LIBRAW
// TODO(p): Consider merging back with fiv-io.
@@ -97,7 +100,16 @@ mark_thumbnail_lq(cairo_surface_t *surface)
static gchar *
fiv_thumbnail_get_root(void)
{
+#ifdef G_OS_WIN32
+ // We can do better than GLib with FOLDERID_InternetCache,
+ // and we don't want to place .cache directly in the user's home.
+ // TODO(p): Register this thumbnail path using the installer:
+ // https://learn.microsoft.com/en-us/windows/win32/lwef/disk-cleanup
+ gchar *cache_dir =
+ g_build_filename(g_get_user_data_dir(), PROJECT_NAME, NULL);
+#else
gchar *cache_dir = get_xdg_home_dir("XDG_CACHE_HOME", ".cache");
+#endif
gchar *thumbnails_dir = g_build_filename(cache_dir, "thumbnails", NULL);
g_free(cache_dir);
return thumbnails_dir;
@@ -122,35 +134,37 @@ might_be_a_thumbnail(const char *path_or_uri)
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-static cairo_surface_t *
+static FivIoImage *
render(GFile *target, GBytes *data, gboolean *color_managed, GError **error)
{
+ FivIoCmm *cmm = fiv_io_cmm_get_default();
FivIoOpenContext ctx = {
.uri = g_file_get_uri(target),
- .screen_profile = fiv_io_profile_new_sRGB(),
+ // Remember to synchronize changes with adjust_thumbnail().
+ .cmm = cmm,
+ .screen_profile = fiv_io_cmm_get_profile_sRGB(cmm),
.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(
+ FivIoImage *image = 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;
+ return image;
}
// In principle similar to rescale_thumbnail() from fiv-browser.c.
-static cairo_surface_t *
-adjust_thumbnail(cairo_surface_t *thumbnail, double row_height)
+static FivIoImage *
+adjust_thumbnail(FivIoImage *thumbnail, double row_height)
{
// Hardcode orientation.
- FivIoOrientation orientation = (uintptr_t) cairo_surface_get_user_data(
- thumbnail, &fiv_io_key_orientation);
+ FivIoOrientation orientation = thumbnail->orientation;
double w = 0, h = 0;
cairo_matrix_t matrix =
@@ -167,33 +181,46 @@ adjust_thumbnail(cairo_surface_t *thumbnail, double row_height)
}
// Vector images should not have orientation, this should handle them all.
- FivIoRenderClosure *closure =
- cairo_surface_get_user_data(thumbnail, &fiv_io_key_render);
+ FivIoRenderClosure *closure = thumbnail->render;
if (closure && orientation <= FivIoOrientation0) {
+ // Remember to synchronize changes with render().
+ FivIoCmm *cmm = fiv_io_cmm_get_default();
+ FivIoProfile *screen_profile = fiv_io_cmm_get_profile_sRGB(cmm);
// This API doesn't accept non-uniform scaling; prefer a vertical fit.
- cairo_surface_t *scaled = closure->render(closure, scale_y);
+ FivIoImage *scaled =
+ closure->render(closure, cmm, screen_profile, scale_y);
+ if (screen_profile)
+ fiv_io_profile_free(screen_profile);
if (scaled)
return scaled;
}
- // This will be CAIRO_FORMAT_INVALID with non-image surfaces, which is fine.
- cairo_format_t format = cairo_image_surface_get_format(thumbnail);
- if (format != CAIRO_FORMAT_INVALID &&
- orientation <= FivIoOrientation0 && scale_x == 1 && scale_y == 1)
- return cairo_surface_reference(thumbnail);
+ if (orientation <= FivIoOrientation0 && scale_x == 1 && scale_y == 1)
+ return fiv_io_image_ref(thumbnail);
+ cairo_format_t format = thumbnail->format;
int projected_width = round(scale_x * w);
int projected_height = round(scale_y * h);
- cairo_surface_t *scaled = cairo_image_surface_create(
+ FivIoImage *scaled = fiv_io_image_new(
(format == CAIRO_FORMAT_RGB24 || format == CAIRO_FORMAT_RGB30)
? CAIRO_FORMAT_RGB24
: CAIRO_FORMAT_ARGB32,
projected_width, projected_height);
+ if (!scaled) {
+ g_warning("image allocation failure");
+ return fiv_io_image_ref(thumbnail);
+ }
+
+ cairo_surface_t *surface = fiv_io_image_to_surface_noref(scaled);
+ cairo_t *cr = cairo_create(surface);
+ cairo_surface_destroy(surface);
- cairo_t *cr = cairo_create(scaled);
cairo_scale(cr, scale_x, scale_y);
- cairo_set_source_surface(cr, thumbnail, 0, 0);
+ surface = fiv_io_image_to_surface_noref(thumbnail);
+ cairo_set_source_surface(cr, surface, 0, 0);
+ cairo_surface_destroy(surface);
+
cairo_pattern_t *pattern = cairo_get_source(cr);
// CAIRO_FILTER_BEST, for some reason, works bad with CAIRO_FORMAT_RGB30.
cairo_pattern_set_filter(pattern, CAIRO_FILTER_GOOD);
@@ -205,9 +232,7 @@ adjust_thumbnail(cairo_surface_t *thumbnail, double row_height)
// Note that this doesn't get triggered with oversize input surfaces,
// even though nothing will be rendered.
- if (cairo_surface_status(thumbnail) != CAIRO_STATUS_SUCCESS ||
- cairo_surface_status(scaled) != CAIRO_STATUS_SUCCESS ||
- cairo_pattern_status(pattern) != CAIRO_STATUS_SUCCESS ||
+ if (cairo_pattern_status(pattern) != CAIRO_STATUS_SUCCESS ||
cairo_status(cr) != CAIRO_STATUS_SUCCESS)
g_warning("thumbnail scaling failed");
@@ -215,149 +240,325 @@ adjust_thumbnail(cairo_surface_t *thumbnail, double row_height)
return scaled;
}
-static cairo_surface_t *
-orient_thumbnail(cairo_surface_t *surface, FivIoOrientation orientation)
+static FivIoImage *
+orient_thumbnail(FivIoImage *image)
{
- if (!surface || orientation <= FivIoOrientation0)
- return surface;
+ if (image->orientation <= FivIoOrientation0)
+ return image;
double w = 0, h = 0;
cairo_matrix_t matrix =
- fiv_io_orientation_apply(surface, orientation, &w, &h);
- cairo_surface_t *oriented =
- cairo_image_surface_create(CAIRO_FORMAT_RGB24, w, h);
+ fiv_io_orientation_apply(image, image->orientation, &w, &h);
+ FivIoImage *oriented = fiv_io_image_new(image->format, w, h);
+ if (!oriented) {
+ g_warning("image allocation failure");
+ return image;
+ }
- cairo_t *cr = cairo_create(oriented);
+ cairo_surface_t *surface = fiv_io_image_to_surface_noref(oriented);
+ cairo_t *cr = cairo_create(surface);
+ cairo_surface_destroy(surface);
+
+ surface = fiv_io_image_to_surface(image);
cairo_set_source_surface(cr, surface, 0, 0);
+ cairo_surface_destroy(surface);
cairo_pattern_set_matrix(cairo_get_source(cr), &matrix);
cairo_set_operator(cr, CAIRO_OPERATOR_SOURCE);
cairo_paint(cr);
cairo_destroy(cr);
- cairo_surface_destroy(surface);
return oriented;
}
-cairo_surface_t *
-fiv_thumbnail_extract(GFile *target, FivThumbnailSize max_size, GError **error)
+// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+#ifdef HAVE_LIBRAW
+#if LIBRAW_VERSION >= LIBRAW_MAKE_VERSION(0, 21, 0)
+
+static int
+extract_libraw_compare(const void *a, const void *b)
{
- const char *path = g_file_peek_path(target);
- if (!path) {
- set_error(error, "thumbnails will only be extracted from local files");
+ const libraw_thumbnail_item_t **t1 = (const libraw_thumbnail_item_t **) a;
+ const libraw_thumbnail_item_t **t2 = (const libraw_thumbnail_item_t **) b;
+ float p1 = (float) (*t1)->twidth * (*t1)->theight;
+ float p2 = (float) (*t2)->twidth * (*t2)->theight;
+ return (p2 < p1) - (p1 < p2);
+}
+
+static gboolean
+extract_libraw_unpack(libraw_data_t *iprc, int *flip, GError **error)
+{
+ int count = iprc->thumbs_list.thumbcount;
+ if (count <= 0) {
+ set_error(error, "no thumbnails found");
+ return FALSE;
+ }
+
+ // The old libraw_unpack_thumb() goes for the largest thumbnail,
+ // but we currently want the smallest usable thumbnail. Order them.
+ libraw_thumbnail_item_t **sorted = g_malloc_n(count, sizeof *sorted);
+ for (int i = 0; i < count; i++)
+ sorted[i] = &iprc->thumbs_list.thumblist[i];
+ qsort(sorted, count, sizeof *sorted, extract_libraw_compare);
+
+ // With the raw.pixls.us database, zero dimensions occur in two cases:
+ // - when thumbcount should really be 0,
+ // - with the last, huge JPEG thumbnail in CR3 raws.
+ // The maintainer refuses to change anything about it (#589).
+ int i = 0;
+ while (i < count && (!sorted[i]->twidth || !sorted[i]->theight))
+ i++;
+
+ // Ignore thumbnails whose decoding is likely to be a waste of time.
+ // XXX: This primarily targets the TIFF/EP shortcut code,
+ // because decoding a thumbnail will always be /much/ quicker than a render.
+ // TODO(p): Maybe don't mark raw image thumbnails as low-quality
+ // if they're the right aspect ratio, and of sufficiently large size.
+ // The only downsides to camera-provided thumbnails seem to be cropping,
+ // and when they're decoded incorrectly. Also don't trust tflip.
+ float output_pixels = (float) iprc->sizes.iwidth * iprc->sizes.iheight;
+ // Note that the ratio may even be larger than 1, as seen with CR2 files.
+ while (i < count &&
+ (float) sorted[count - 1]->twidth * sorted[count - 1]->theight >
+ output_pixels * 0.75)
+ count--;
+
+ // The smallest size thumbnail is very often forced to be 4:3,
+ // and the remaining space is filled with black, looking quite wrong.
+ // It isn't really possible to strip those borders, because many are JPEGs.
+ //
+ // Another reason to skip thumbnails of mismatching aspect ratios is
+ // to avoid browser items from jumping around when low-quality thumbnails
+ // get replaced with their final versions.
+ //
+ // Note that some of them actually have borders on all four sides
+ // (Nikon/D50/DSC_5155.NEF, Nikon/D70/20170902_0047.NEF,
+ // Nikon/D70s/RAW_NIKON_D70S.NEF), or even on just one side
+ // (Leica/LEICA M MONOCHROM (Typ 246), Leica/M (Typ 240)).
+ // Another interesting possibility is Sony/DSC-HX99/DSC00001.ARW,
+ // where the correct-ratio thumbnail has borders but the main image doesn't.
+ //
+ // The problematic thumbnail is usually, but not always, sized 160x120,
+ // and some of them may actually be fine.
+ float output_ratio = (float) iprc->sizes.iwidth / iprc->sizes.iheight;
+ while (i < count) {
+ // XXX: tflip is less reliable than libraw_dcraw_make_mem_thumb()
+ // and reading out Orientation from the resulting Exif.
+ float ratio = sorted[i]->tflip == 5 || sorted[i]->tflip == 6
+ ? (float) sorted[i]->theight / sorted[i]->twidth
+ : (float) sorted[i]->twidth / sorted[i]->theight;
+ if (fabsf(ratio - output_ratio) < 0.05)
+ break;
+ i++;
+ }
+
+ // Avoid pink-tinted readouts of CR2 IFD2 (#590).
+ //
+ // This thumbnail can also have a black stripe on the left and the top,
+ // which we should remove if using fixed LibRaw > 0.21.1.
+ if (i < count && iprc->idata.maker_index == LIBRAW_CAMERAMAKER_Canon &&
+ sorted[i]->tformat == LIBRAW_INTERNAL_THUMBNAIL_KODAK_THUMB)
+ i++;
+
+ bool found = i != count;
+ if (found)
+ i = sorted[i] - iprc->thumbs_list.thumblist;
+
+ g_free(sorted);
+ if (!found) {
+ set_error(error, "no suitable thumbnails found");
+ return FALSE;
+ }
+
+ int err = 0;
+ if ((err = libraw_unpack_thumb_ex(iprc, i))) {
+ set_error(error, libraw_strerror(err));
+ return FALSE;
+ }
+ *flip = iprc->thumbs_list.thumblist[i].tflip;
+ return TRUE;
+}
+
+#else // LIBRAW_VERSION < LIBRAW_MAKE_VERSION(0, 21, 0)
+
+static gboolean
+extract_libraw_unpack(libraw_data_t *iprc, int *flip, GError **error)
+{
+ int err = 0;
+ if ((err = libraw_unpack_thumb(iprc))) {
+ set_error(error, libraw_strerror(err));
+ return FALSE;
+ }
+
+ // The main image's "flip" often matches up, but sometimes doesn't, e.g.:
+ // - Phase One/H 25/H25_Outdoor_.IIQ
+ // - Phase One/H 25/H25_IT8.7-2_Card.TIF
+ *flip = iprc->sizes.flip;
+ return TRUE;
+}
+
+#endif // LIBRAW_VERSION < LIBRAW_MAKE_VERSION(0, 21, 0)
+
+// LibRaw does a weird permutation here, so follow the documentation,
+// which assumes that mirrored orientations never happen.
+static FivIoOrientation
+extract_libraw_unflip(int flip)
+{
+ switch (flip) {
+ break; case 0:
+ return FivIoOrientation0;
+ break; case 3:
+ return FivIoOrientation180;
+ break; case 5:
+ return FivIoOrientation270;
+ break; case 6:
+ return FivIoOrientation90;
+ break; default:
+ return FivIoOrientationUnknown;
+ }
+}
+
+static FivIoImage *
+extract_libraw_bitmap(libraw_processed_image_t *image, int flip, GError **error)
+{
+ // Anything else is extremely rare.
+ if (image->colors != 3 || image->bits != 8) {
+ set_error(error, "unsupported bitmap thumbnail");
return NULL;
}
- GMappedFile *mf = g_mapped_file_new(path, FALSE, error);
- if (!mf)
+ FivIoImage *I = fiv_io_image_new(
+ CAIRO_FORMAT_RGB24, image->width, image->height);
+ if (!I) {
+ set_error(error, "image allocation failure");
return NULL;
+ }
- // Bitmap thumbnails generally need rotating, e.g.:
- // - Hasselblad/H4D-50/2-9-2017_street_0012.fff
- // - OnePlus/One/IMG_20150729_201116.dng (and more DNGs in general)
- // Though it's apparent LibRaw doesn't adjust the thumbnails to match
- // the main image's "flip" field (it just happens to match up often), e.g.:
- // - Phase One/H 25/H25_Outdoor_.IIQ (correct Orientation in IFD0)
- // - Phase One/H 25/H25_IT8.7-2_Card.TIF (correctly missing in IFD0)
- //
- // JPEG thumbnails generally have the right rotation in their Exif, e.g.:
- // - Canon/EOS-1Ds Mark II/RAW_CANON_1DSM2.CR2
- // - Leica/C (Typ 112)/Leica_-_C_(Typ_112)-_3:2.RWL
- // - Nikon/1 S2/RAW_NIKON_1S2.NEF
- // - Panasonic/DMC-FZ18/RAW_PANASONIC_LUMIX_FZ18.RAW
- // - Panasonic/DMC-FZ70/P1000836.RW2
- // - Samsung/NX200/2013-05-08-194524__sam6589.srw
- // - Sony/DSC-HX95/DSC00018.ARW
- //
- // Some files are problematic and we won't bother with special-casing:
- // - Leaf/Aptus 22/L_003172.mos (JPEG)'s thumbnail wrongly contains
- // Exif Orientation 6, and sizes.flip also contains 6.
- // - Nokia/Lumia 1020/RAW_NOKIA_LUMIA_1020.DNG (bitmap) has wrong color.
- // - Ricoh/GXR/R0017428.DNG (JPEG) seems to be plainly invalid.
- FivIoOrientation orientation = FivIoOrientationUnknown;
- cairo_surface_t *surface = NULL;
-#ifndef HAVE_LIBRAW
- // TODO(p): Implement our own thumbnail extractors.
- set_error(error, "unsupported file");
-#else // HAVE_LIBRAW
+ guint32 *out = (guint32 *) I->data;
+ const unsigned char *in = image->data;
+ for (guint64 i = 0; i < (guint64) image->width * image->height; in += 3)
+ out[i++] = in[0] << 16 | in[1] << 8 | in[2];
+
+ I->orientation = extract_libraw_unflip(flip);
+ return I;
+}
+
+static FivIoImage *
+extract_libraw(GFile *target, GMappedFile *mf, GError **error)
+{
+ FivIoImage *I = NULL;
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;
+ return NULL;
}
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))) {
+ g_mapped_file_get_length(mf)))) {
set_error(error, libraw_strerror(err));
- goto fail_libraw;
+ goto fail;
}
+ if ((err = libraw_adjust_sizes_info_only(iprc))) {
+ set_error(error, libraw_strerror(err));
+ goto fail;
+ }
+
+ int flip = 0;
+ if (!extract_libraw_unpack(iprc, &flip, error))
+ goto fail;
libraw_processed_image_t *image = libraw_dcraw_make_mem_thumb(iprc, &err);
if (!image) {
set_error(error, libraw_strerror(err));
- goto fail_libraw;
+ goto fail;
}
- gboolean dummy = FALSE;
+ // Bitmap thumbnails generally need rotating, e.g.:
+ // - Hasselblad/H4D-50/2-9-2017_street_0012.fff
+ // - OnePlus/One/IMG_20150729_201116.dng (and more DNGs in general)
+ //
+ // JPEG thumbnails generally have the right rotation in their Exif, e.g.:
+ // - Canon/EOS-1Ds Mark II/RAW_CANON_1DSM2.CR2
+ // - Leica/C (Typ 112)/Leica_-_C_(Typ_112)-_3:2.RWL
+ // - Nikon/1 S2/RAW_NIKON_1S2.NEF
+ // - Panasonic/DMC-FZ18/RAW_PANASONIC_LUMIX_FZ18.RAW
+ // - Panasonic/DMC-FZ70/P1000836.RW2
+ // - Samsung/NX200/2013-05-08-194524__sam6589.srw
+ // - Sony/DSC-HX95/DSC00018.ARW
+ // Note that LibRaw inserts its own Exif segment if it doesn't find one,
+ // and this may differ from flip. It may also be wrong, as in:
+ // - Leaf/Aptus 22/L_003172.mos
+ //
+ // Some files are problematic and we won't bother with special-casing:
+ // - Nokia/Lumia 1020/RAW_NOKIA_LUMIA_1020.DNG (bitmap) has wrong color.
+ // - Ricoh/GXR/R0017428.DNG (JPEG) seems to be plainly invalid.
switch (image->type) {
+ gboolean dummy;
case LIBRAW_IMAGE_JPEG:
- surface = render(
+ I = render(
target, g_bytes_new(image->data, image->data_size), &dummy, error);
- orientation = (int) (intptr_t) cairo_surface_get_user_data(
- surface, &fiv_io_key_orientation);
break;
case LIBRAW_IMAGE_BITMAP:
- // Anything else is extremely rare.
- if (image->colors != 3 || image->bits != 8) {
- set_error(error, "unsupported bitmap thumbnail");
- break;
- }
-
- surface = cairo_image_surface_create(
- CAIRO_FORMAT_RGB24, image->width, image->height);
- guint32 *out = (guint32 *) cairo_image_surface_get_data(surface);
- const unsigned char *in = image->data;
- for (guint64 i = 0; i < image->width * image->height; in += 3)
- out[i++] = in[0] << 16 | in[1] << 8 | in[2];
- cairo_surface_mark_dirty(surface);
-
- // LibRaw actually turns an 8 to 5, so follow the documentation.
- switch (iprc->sizes.flip) {
- break; case 3: orientation = FivIoOrientation180;
- break; case 5: orientation = FivIoOrientation270;
- break; case 6: orientation = FivIoOrientation90;
- }
+ I = extract_libraw_bitmap(image, flip, error);
break;
default:
set_error(error, "unsupported embedded thumbnail");
}
libraw_dcraw_clear_mem(image);
-fail_libraw:
+fail:
libraw_close(iprc);
+ return I;
+}
+
#endif // HAVE_LIBRAW
-fail:
+// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+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;
+
+ // In this case, g_mapped_file_get_contents() returns NULL, causing issues.
+ if (!g_mapped_file_get_length(mf)) {
+ set_error(error, "empty file");
+ return NULL;
+ }
+
+ FivIoImage *image = NULL;
+#ifdef HAVE_LIBRAW
+ image = extract_libraw(target, mf, error);
+#else // ! HAVE_LIBRAW
+ // TODO(p): Implement our own thumbnail extractors.
+ set_error(error, "unsupported file");
+#endif // ! HAVE_LIBRAW
g_mapped_file_unref(mf);
- // This hardcodes Exif orientation before adjust_thumbnail() might do so,
- // before the early return below.
- surface = orient_thumbnail(surface, orientation);
- if (!surface || max_size < FIV_THUMBNAIL_SIZE_MIN ||
- max_size > FIV_THUMBNAIL_SIZE_MAX)
- return surface;
+ if (!image)
+ return NULL;
+ if (max_size < FIV_THUMBNAIL_SIZE_MIN || max_size > FIV_THUMBNAIL_SIZE_MAX)
+ return fiv_io_image_to_surface(orient_thumbnail(image));
- cairo_surface_t *result =
- adjust_thumbnail(surface, fiv_thumbnail_sizes[max_size].size);
- cairo_surface_destroy(surface);
- return result;
+ FivIoImage *result =
+ adjust_thumbnail(image, fiv_thumbnail_sizes[max_size].size);
+ fiv_io_image_unref(image);
+ return fiv_io_image_to_surface(result);
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
static WebPData
-encode_thumbnail(cairo_surface_t *surface)
+encode_thumbnail(FivIoImage *image)
{
WebPData bitstream = {};
WebPConfig config = {};
@@ -369,12 +570,12 @@ encode_thumbnail(cairo_surface_t *surface)
if (!WebPValidateConfig(&config))
return bitstream;
- bitstream.bytes = fiv_io_encode_webp(surface, &config, &bitstream.size);
+ bitstream.bytes = fiv_io_encode_webp(image, &config, &bitstream.size);
return bitstream;
}
static void
-save_thumbnail(cairo_surface_t *thumbnail, const char *path, GString *thum)
+save_thumbnail(FivIoImage *thumbnail, const char *path, GString *thum)
{
WebPMux *mux = WebPMuxNew();
WebPData bitstream = encode_thumbnail(thumbnail);
@@ -418,9 +619,33 @@ save_thumbnail(cairo_surface_t *thumbnail, const char *path, GString *thum)
WebPDataClear(&assembled);
}
+cairo_surface_t *
+fiv_thumbnail_produce_for_search(
+ GFile *target, FivThumbnailSize max_size, GError **error)
+{
+ g_return_val_if_fail(max_size >= FIV_THUMBNAIL_SIZE_MIN &&
+ max_size <= FIV_THUMBNAIL_SIZE_MAX, NULL);
+
+ GBytes *data = g_file_load_bytes(target, NULL, NULL, error);
+ if (!data)
+ return NULL;
+
+ gboolean color_managed = FALSE;
+ FivIoImage *image = render(target, data, &color_managed, error);
+ if (!image)
+ return NULL;
+
+ // TODO(p): Might want to keep this a square.
+ FivIoImage *result =
+ adjust_thumbnail(image, fiv_thumbnail_sizes[max_size].size);
+ fiv_io_image_unref(image);
+ return fiv_io_image_to_surface(result);
+}
+
static cairo_surface_t *
produce_fallback(GFile *target, FivThumbnailSize size, GError **error)
{
+ // Note that this comes with a TOCTTOU problem.
goffset filesize = 0;
GFileInfo *info = g_file_query_info(target,
G_FILE_ATTRIBUTE_STANDARD_NAME "," G_FILE_ATTRIBUTE_STANDARD_SIZE,
@@ -442,21 +667,21 @@ produce_fallback(GFile *target, FivThumbnailSize size, GError **error)
return NULL;
gboolean color_managed = FALSE;
- cairo_surface_t *surface = render(target, data, &color_managed, error);
- if (!surface)
+ FivIoImage *image = render(target, data, &color_managed, error);
+ if (!image)
return NULL;
- cairo_surface_t *result =
- adjust_thumbnail(surface, fiv_thumbnail_sizes[size].size);
- cairo_surface_destroy(surface);
- return result;
+ FivIoImage *result =
+ adjust_thumbnail(image, fiv_thumbnail_sizes[size].size);
+ fiv_io_image_unref(image);
+ return fiv_io_image_to_surface(result);
}
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);
+ max_size <= FIV_THUMBNAIL_SIZE_MAX, NULL);
// Don't save thumbnails for FUSE mounts, such as sftp://.
// Moreover, it doesn't make sense to save thumbnails of thumbnails.
@@ -471,6 +696,13 @@ fiv_thumbnail_produce(GFile *target, FivThumbnailSize max_size, GError **error)
return NULL;
}
+ // TODO(p): Use open(O_RDONLY | O_NONBLOCK | _O_BINARY), fstat(),
+ // g_mapped_file_new_from_fd(), and reset the non-blocking flag on the file.
+ if (!S_ISREG(st.st_mode)) {
+ set_error(error, "not a regular file");
+ return NULL;
+ }
+
GError *e = NULL;
GMappedFile *mf = g_mapped_file_new(path, FALSE, &e);
if (!mf) {
@@ -479,12 +711,18 @@ fiv_thumbnail_produce(GFile *target, FivThumbnailSize max_size, GError **error)
return produce_fallback(target, max_size, error);
}
+ // In this case, g_mapped_file_get_bytes() has NULL data, causing issues.
gsize filesize = g_mapped_file_get_length(mf);
+ if (!filesize) {
+ set_error(error, "empty file");
+ return NULL;
+ }
+
gboolean color_managed = FALSE;
- cairo_surface_t *surface =
+ FivIoImage *image =
render(target, g_mapped_file_get_bytes(mf), &color_managed, error);
g_mapped_file_unref(mf);
- if (!surface)
+ if (!image)
return NULL;
// Boilerplate copied from fiv_thumbnail_lookup().
@@ -498,14 +736,12 @@ fiv_thumbnail_produce(GFile *target, FivThumbnailSize max_size, GError **error)
g_string_append_printf(
thum, "%s%c%ld%c", THUMB_MTIME, 0, (long) st.st_mtime, 0);
g_string_append_printf(
- thum, "%s%c%ld%c", THUMB_SIZE, 0, (long) filesize, 0);
+ thum, "%s%c%llu%c", THUMB_SIZE, 0, (unsigned long long) filesize, 0);
- if (cairo_surface_get_type(surface) == CAIRO_SURFACE_TYPE_IMAGE) {
- 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);
- }
+ g_string_append_printf(thum, "%s%c%u%c", THUMB_IMAGE_WIDTH, 0,
+ (unsigned) image->width, 0);
+ g_string_append_printf(thum, "%s%c%u%c", THUMB_IMAGE_HEIGHT, 0,
+ (unsigned) image->height, 0);
// Without a CMM, no conversion is attempted.
if (color_managed) {
@@ -513,19 +749,19 @@ fiv_thumbnail_produce(GFile *target, FivThumbnailSize max_size, GError **error)
thum, "%s%c%s%c", THUMB_COLORSPACE, 0, THUMB_COLORSPACE_SRGB, 0);
}
- cairo_surface_t *max_size_surface = NULL;
+ FivIoImage *max_size_image = NULL;
for (int use = max_size; use >= FIV_THUMBNAIL_SIZE_MIN; use--) {
- cairo_surface_t *scaled =
- adjust_thumbnail(surface, fiv_thumbnail_sizes[use].size);
+ FivIoImage *scaled =
+ adjust_thumbnail(image, 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);
g_free(path);
- if (!max_size_surface)
- max_size_surface = scaled;
+ if (!max_size_image)
+ max_size_image = scaled;
else
- cairo_surface_destroy(scaled);
+ fiv_io_image_unref(scaled);
}
g_string_free(thum, TRUE);
@@ -533,13 +769,20 @@ fiv_thumbnail_produce(GFile *target, FivThumbnailSize max_size, GError **error)
g_free(thumbnails_dir);
g_free(sum);
g_free(uri);
- cairo_surface_destroy(surface);
- return max_size_surface;
+ fiv_io_image_unref(image);
+ return fiv_io_image_to_surface(max_size_image);
}
+// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+typedef struct {
+ const char *uri; ///< Target URI
+ time_t mtime; ///< File modification time
+ guint64 size; ///< File size
+} Stat;
+
static bool
-check_wide_thumbnail_texts(GBytes *thum, const char *target, time_t mtime,
- bool *sRGB)
+check_wide_thumbnail_texts(GBytes *thum, const Stat *st, bool *sRGB)
{
gsize len = 0;
const gchar *s = g_bytes_get_data(thum, &len), *end = s + len;
@@ -553,11 +796,14 @@ check_wide_thumbnail_texts(GBytes *thum, const char *target, time_t mtime,
continue;
} else if (!strcmp(key, THUMB_URI)) {
have_uri = true;
- if (strcmp(target, s))
+ if (strcmp(st->uri, s))
return false;
} else if (!strcmp(key, THUMB_MTIME)) {
have_mtime = true;
- if (atol(s) != mtime)
+ if (atol(s) != st->mtime)
+ return false;
+ } else if (!strcmp(key, THUMB_SIZE)) {
+ if (strtoull(s, NULL, 10) != st->size)
return false;
} else if (!strcmp(key, THUMB_COLORSPACE))
*sRGB = !strcmp(s, THUMB_COLORSPACE_SRGB);
@@ -568,30 +814,29 @@ check_wide_thumbnail_texts(GBytes *thum, const char *target, time_t mtime,
}
static cairo_surface_t *
-read_wide_thumbnail(
- const char *path, const char *uri, time_t mtime, GError **error)
+read_wide_thumbnail(const char *path, const Stat *st, GError **error)
{
gchar *thumbnail_uri = g_filename_to_uri(path, NULL, error);
if (!thumbnail_uri)
return NULL;
- cairo_surface_t *surface =
+ FivIoImage *image =
fiv_io_open(&(FivIoOpenContext){.uri = thumbnail_uri}, error);
g_free(thumbnail_uri);
- if (!surface)
+ if (!image)
return NULL;
bool sRGB = false;
- GBytes *thum = cairo_surface_get_user_data(surface, &fiv_io_key_thum);
- if (!thum) {
+ if (!image->thum) {
g_clear_error(error);
set_error(error, "not a thumbnail");
- } else if (!check_wide_thumbnail_texts(thum, uri, mtime, &sRGB)) {
+ } else if (!check_wide_thumbnail_texts(image->thum, st, &sRGB)) {
g_clear_error(error);
set_error(error, "mismatch");
} else {
// TODO(p): Add a function or a non-valueless define to check
// for CMM presence, then remove this ifdef.
+ cairo_surface_t *surface = fiv_io_image_to_surface(image);
#ifdef HAVE_LCMS2
if (!sRGB)
mark_thumbnail_lq(surface);
@@ -599,24 +844,21 @@ read_wide_thumbnail(
return surface;
}
- cairo_surface_destroy(surface);
+ fiv_io_image_unref(image);
return NULL;
}
-// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-
static cairo_surface_t *
-read_png_thumbnail(
- const char *path, const char *uri, time_t mtime, GError **error)
+read_png_thumbnail(const char *path, const Stat *st, GError **error)
{
- cairo_surface_t *surface = fiv_io_open_png_thumbnail(path, error);
- if (!surface)
+ FivIoImage *image = fiv_io_open_png_thumbnail(path, error);
+ if (!image)
return NULL;
- GHashTable *texts = cairo_surface_get_user_data(surface, &fiv_io_key_text);
+ GHashTable *texts = image->text;
if (!texts) {
set_error(error, "not a thumbnail");
- cairo_surface_destroy(surface);
+ fiv_io_image_unref(image);
return NULL;
}
@@ -624,18 +866,27 @@ read_png_thumbnail(
// but those aren't interesting currently (would be for fast previews).
const char *text_uri = g_hash_table_lookup(texts, THUMB_URI);
const char *text_mtime = g_hash_table_lookup(texts, THUMB_MTIME);
- if (!text_uri || strcmp(text_uri, uri) ||
- !text_mtime || atol(text_mtime) != mtime) {
+ const char *text_size = g_hash_table_lookup(texts, THUMB_SIZE);
+ if (!text_uri || strcmp(text_uri, st->uri) ||
+ !text_mtime || atol(text_mtime) != st->mtime) {
set_error(error, "mismatch or not a thumbnail");
- cairo_surface_destroy(surface);
+ fiv_io_image_unref(image);
+ return NULL;
+ }
+ if (text_size && strtoull(text_size, NULL, 10) != st->size) {
+ set_error(error, "file size mismatch");
+ fiv_io_image_unref(image);
return NULL;
}
- return surface;
+ return fiv_io_image_to_surface(image);
}
+// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
cairo_surface_t *
-fiv_thumbnail_lookup(const char *uri, gint64 mtime_msec, FivThumbnailSize size)
+fiv_thumbnail_lookup(const char *uri, gint64 mtime_msec, guint64 filesize,
+ FivThumbnailSize size)
{
g_return_val_if_fail(size >= FIV_THUMBNAIL_SIZE_MIN &&
size <= FIV_THUMBNAIL_SIZE_MAX, NULL);
@@ -647,6 +898,7 @@ fiv_thumbnail_lookup(const char *uri, gint64 mtime_msec, FivThumbnailSize size)
gchar *sum = g_compute_checksum_for_string(G_CHECKSUM_MD5, uri, -1);
gchar *thumbnails_dir = fiv_thumbnail_get_root();
+ const Stat st = {.uri = uri, .mtime = mtime_msec / 1000, .size = filesize};
// The lookup sequence is: nominal..max, then mirroring back to ..min.
cairo_surface_t *result = NULL;
@@ -659,7 +911,7 @@ fiv_thumbnail_lookup(const char *uri, gint64 mtime_msec, FivThumbnailSize size)
const char *name = fiv_thumbnail_sizes[use].thumbnail_spec_name;
gchar *wide = g_strconcat(thumbnails_dir, G_DIR_SEPARATOR_S "wide-",
name, G_DIR_SEPARATOR_S, sum, ".webp", NULL);
- result = read_wide_thumbnail(wide, uri, mtime_msec / 1000, &error);
+ result = read_wide_thumbnail(wide, &st, &error);
if (error) {
g_debug("%s: %s", wide, error->message);
g_clear_error(&error);
@@ -675,7 +927,7 @@ fiv_thumbnail_lookup(const char *uri, gint64 mtime_msec, FivThumbnailSize size)
gchar *path = g_strconcat(thumbnails_dir, G_DIR_SEPARATOR_S,
name, G_DIR_SEPARATOR_S, sum, ".png", NULL);
- result = read_png_thumbnail(path, uri, mtime_msec / 1000, &error);
+ result = read_png_thumbnail(path, &st, &error);
if (error) {
g_debug("%s: %s", path, error->message);
g_clear_error(&error);
@@ -708,7 +960,7 @@ print_error(GFile *file, GError *error)
}
static gchar *
-identify_wide_thumbnail(GMappedFile *mf, time_t *mtime, GError **error)
+identify_wide_thumbnail(GMappedFile *mf, Stat *st, GError **error)
{
WebPDemuxer *demux = WebPDemux(&(WebPData) {
.bytes = (const uint8_t *) g_mapped_file_get_contents(mf),
@@ -734,7 +986,9 @@ identify_wide_thumbnail(GMappedFile *mf, time_t *mtime, GError **error)
if (!strcmp(key, THUMB_URI) && !uri)
uri = g_strdup(p);
if (!strcmp(key, THUMB_MTIME))
- *mtime = atol(p);
+ st->mtime = atol(p);
+ if (!strcmp(key, THUMB_SIZE))
+ st->size = strtoull(p, NULL, 10);
key = NULL;
} else {
key = p;
@@ -752,16 +1006,17 @@ static void
check_wide_thumbnail(GFile *thumbnail, GError **error)
{
// Not all errors are enough of a reason for us to delete something.
- GError *tolerable = NULL;
+ GError *tolerable_error = NULL;
const char *path = g_file_peek_path(thumbnail);
- GMappedFile *mf = g_mapped_file_new(path, FALSE, &tolerable);
+ GMappedFile *mf = g_mapped_file_new(path, FALSE, &tolerable_error);
if (!mf) {
- print_error(thumbnail, tolerable);
+ print_error(thumbnail, tolerable_error);
return;
}
- time_t target_mtime = 0;
- gchar *target_uri = identify_wide_thumbnail(mf, &target_mtime, error);
+ // Note that we could enforce the presence of the size field in our spec.
+ Stat target_st = {.uri = NULL, .mtime = 0, .size = G_MAXUINT64};
+ gchar *target_uri = identify_wide_thumbnail(mf, &target_st, error);
g_mapped_file_unref(mf);
if (!target_uri)
return;
@@ -783,26 +1038,32 @@ check_wide_thumbnail(GFile *thumbnail, GError **error)
GFile *target = g_file_new_for_uri(target_uri);
g_free(target_uri);
GFileInfo *info = g_file_query_info(target,
- G_FILE_ATTRIBUTE_STANDARD_NAME "," G_FILE_ATTRIBUTE_TIME_MODIFIED,
- G_FILE_QUERY_INFO_NONE, NULL, &tolerable);
+ G_FILE_ATTRIBUTE_STANDARD_NAME ","
+ G_FILE_ATTRIBUTE_STANDARD_SIZE ","
+ G_FILE_ATTRIBUTE_TIME_MODIFIED,
+ G_FILE_QUERY_INFO_NONE, NULL, &tolerable_error);
g_object_unref(target);
- if (g_error_matches(tolerable, G_IO_ERROR, G_IO_ERROR_NOT_FOUND)) {
- g_propagate_error(error, tolerable);
+ if (g_error_matches(tolerable_error, G_IO_ERROR, G_IO_ERROR_NOT_FOUND)) {
+ g_propagate_error(error, tolerable_error);
return;
- } else if (tolerable) {
- print_error(thumbnail, tolerable);
+ } else if (tolerable_error) {
+ print_error(thumbnail, tolerable_error);
return;
}
+ guint64 filesize = g_file_info_get_size(info);
GDateTime *mdatetime = g_file_info_get_modification_date_time(info);
g_object_unref(info);
if (!mdatetime) {
- set_error(&tolerable, "cannot retrieve file modification time");
- print_error(thumbnail, tolerable);
+ set_error(&tolerable_error, "cannot retrieve file modification time");
+ print_error(thumbnail, tolerable_error);
return;
}
- if (g_date_time_to_unix(mdatetime) != target_mtime)
- set_error(error, "mtime mismatch");
+ if (g_date_time_to_unix(mdatetime) != target_st.mtime)
+ set_error(error, "modification time mismatch");
+ else if (target_st.size != G_MAXUINT64 && filesize != target_st.size)
+ set_error(error, "file size mismatch");
+
g_date_time_unref(mdatetime);
}