diff options
Diffstat (limited to 'fiv-thumbnail.c')
-rw-r--r-- | fiv-thumbnail.c | 619 |
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); } |