e9f9c2cf62f3fff4e9f11553ca62596ea0a464d8
[geeqie.git] / src / thumb-standard.cc
1 /*
2  * Copyright (C) 2006 John Ellis
3  * Copyright (C) 2008 - 2016 The Geeqie Team
4  *
5  * Author: John Ellis
6  *
7  * This program is free software; you can redistribute it and/or modify
8  * it under the terms of the GNU General Public License as published by
9  * the Free Software Foundation; either version 2 of the License, or
10  * (at your option) any later version.
11  *
12  * This program is distributed in the hope that it will be useful,
13  * but WITHOUT ANY WARRANTY; without even the implied warranty of
14  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15  * GNU General Public License for more details.
16  *
17  * You should have received a copy of the GNU General Public License along
18  * with this program; if not, write to the Free Software Foundation, Inc.,
19  * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
20  */
21
22 #include "main.h"
23 #include "thumb-standard.h"
24
25 #include "cache.h"
26 #include "image-load.h"
27 #include "md5-util.h"
28 #include "pixbuf-util.h"
29 #include "ui-fileops.h"
30 #include "filedata.h"
31 #include "exif.h"
32 #include "metadata.h"
33 #include "color-man.h"
34
35
36 /**
37  * @file
38  *
39  * This thumbnail caching implementation attempts to conform
40  * to the Thumbnail Managing Standard proposed on freedesktop.org
41  * The standard is documented here: \n
42  *   https://www.freedesktop.org/wiki/Specifications/thumbnails/ \n
43  *
44  * This code attempts to conform to version 0.7.0 of the standard.
45  *
46  * Notes:
47  *   > Validation of the thumb's embedded uri is a simple strcmp between our
48  *   > version of the escaped uri and the thumb's escaped uri. But not all uri
49  *   > escape functions escape the same set of chars, comparing the unescaped
50  *   > versions may be more accurate. \n
51  *   > Only Thumb::URI and Thumb::MTime are stored in a thumb at this time.
52  *     Storing the Size, Width, Height should probably be implemented.
53  */
54
55
56 enum {
57         THUMB_SIZE_NORMAL = 128,
58         THUMB_SIZE_LARGE =  256
59 };
60
61 #define THUMB_MARKER_URI    "tEXt::Thumb::URI"
62 #define THUMB_MARKER_MTIME  "tEXt::Thumb::MTime"
63 #define THUMB_MARKER_SIZE   "tEXt::Thumb::Size"
64 #define THUMB_MARKER_WIDTH  "tEXt::Thumb::Image::Width"
65 #define THUMB_MARKER_HEIGHT "tEXt::Thumb::Image::Height"
66 #define THUMB_MARKER_APP    "tEXt::Software"
67
68 /*
69  *-----------------------------------------------------------------------------
70  * thumbnail loader
71  *-----------------------------------------------------------------------------
72  */
73
74
75 static void thumb_loader_std_error_cb(ImageLoader *il, gpointer data);
76 static gint thumb_loader_std_setup(ThumbLoaderStd *tl, FileData *fd);
77
78
79 ThumbLoaderStd *thumb_loader_std_new(gint width, gint height)
80 {
81         ThumbLoaderStd *tl;
82
83         tl = g_new0(ThumbLoaderStd, 1);
84
85         tl->standard_loader = TRUE;
86         tl->requested_width = width;
87         tl->requested_height = height;
88         tl->cache_enable = options->thumbnails.enable_caching;
89
90         return tl;
91 }
92
93 void thumb_loader_std_set_callbacks(ThumbLoaderStd *tl,
94                                     ThumbLoaderStd::Func func_done,
95                                     ThumbLoaderStd::Func func_error,
96                                     ThumbLoaderStd::Func func_progress,
97                                     gpointer data)
98 {
99         if (!tl) return;
100
101         tl->func_done = func_done;
102         tl->func_error = func_error;
103         tl->func_progress = func_progress;
104         tl->data = data;
105 }
106
107 static void thumb_loader_std_reset(ThumbLoaderStd *tl)
108 {
109         image_loader_free(tl->il);
110         tl->il = nullptr;
111
112         file_data_unref(tl->fd);
113         tl->fd = nullptr;
114
115         g_free(tl->thumb_path);
116         tl->thumb_path = nullptr;
117
118         g_free(tl->thumb_uri);
119         tl->thumb_uri = nullptr;
120         tl->local_uri = nullptr;
121
122         tl->thumb_path_local = FALSE;
123
124         tl->cache_hit = FALSE;
125
126         tl->source_mtime = 0;
127         tl->source_size = 0;
128         tl->source_mode = 0;
129
130         tl->progress = 0.0;
131 }
132
133 static gchar *thumb_std_cache_path(const gchar *path, const gchar *uri, gboolean local,
134                                    const gchar *cache_subfolder)
135 {
136         gchar *result = nullptr;
137         gchar *md5_text;
138         gchar *name;
139
140         if (!path || !uri || !cache_subfolder) return nullptr;
141
142         md5_text = md5_get_string(reinterpret_cast<const guchar *>(uri), strlen(uri));
143
144         if (!md5_text) return nullptr;
145
146         name = g_strconcat(md5_text, THUMB_NAME_EXTENSION, NULL);
147
148         if (local)
149                 {
150                 gchar *base = remove_level_from_path(path);
151
152                 result = g_build_filename(base, THUMB_FOLDER_LOCAL, cache_subfolder, name, NULL);
153                 g_free(base);
154                 }
155         else
156                 {
157                 result = g_build_filename(get_thumbnails_standard_cache_dir(),
158                                                                                                         cache_subfolder, name, NULL);
159                 }
160
161         g_free(name);
162         g_free(md5_text);
163
164         return result;
165 }
166
167 static gchar *thumb_loader_std_cache_path(ThumbLoaderStd *tl, gboolean local, GdkPixbuf *pixbuf, gboolean fail)
168 {
169         const gchar *folder;
170         gint w;
171         gint h;
172
173         if (!tl->fd || !tl->thumb_uri) return nullptr;
174
175         if (pixbuf)
176                 {
177                 w = gdk_pixbuf_get_width(pixbuf);
178                 h = gdk_pixbuf_get_height(pixbuf);
179                 }
180         else
181                 {
182                 w = tl->requested_width;
183                 h = tl->requested_height;
184                 }
185
186         if (fail)
187                 {
188                 folder = THUMB_FOLDER_FAIL;
189                 }
190         else if (w > THUMB_SIZE_NORMAL || h > THUMB_SIZE_NORMAL)
191                 {
192                 folder = THUMB_FOLDER_LARGE;
193                 }
194         else
195                 {
196                 folder = THUMB_FOLDER_NORMAL;
197                 }
198
199         return thumb_std_cache_path(tl->fd->path,
200                                     (local) ?  tl->local_uri : tl->thumb_uri,
201                                     local, folder);
202 }
203
204 static gboolean thumb_loader_std_fail_check(ThumbLoaderStd *tl)
205 {
206         gchar *fail_path;
207         gboolean result = FALSE;
208
209         fail_path = thumb_loader_std_cache_path(tl, FALSE, nullptr, TRUE);
210         if (isfile(fail_path))
211                 {
212                 GdkPixbuf *pixbuf;
213
214                 if (tl->cache_retry)
215                         {
216                         pixbuf = nullptr;
217                         }
218                 else
219                         {
220                         gchar *pathl;
221
222                         pathl = path_from_utf8(fail_path);
223                         pixbuf = gdk_pixbuf_new_from_file(pathl, nullptr);
224                         g_free(pathl);
225                         }
226
227                 if (pixbuf)
228                         {
229                         const gchar *mtime_str;
230
231                         mtime_str = gdk_pixbuf_get_option(pixbuf, THUMB_MARKER_MTIME);
232                         if (mtime_str && strtol(mtime_str, nullptr, 10) == tl->source_mtime)
233                                 {
234                                 result = TRUE;
235                                 DEBUG_1("thumb fail valid: %s", tl->fd->path);
236                                 DEBUG_1("           thumb: %s", fail_path);
237                                 }
238
239                         g_object_unref(G_OBJECT(pixbuf));
240                         }
241
242                 if (!result) unlink_file(fail_path);
243                 }
244         g_free(fail_path);
245
246         return result;
247 }
248
249 static gboolean thumb_loader_std_validate(ThumbLoaderStd *tl, GdkPixbuf *pixbuf)
250 {
251         const gchar *valid_uri;
252         const gchar *uri;
253         const gchar *mtime_str;
254         time_t mtime;
255         gint w;
256         gint h;
257
258         if (!pixbuf) return FALSE;
259
260         w = gdk_pixbuf_get_width(pixbuf);
261         h = gdk_pixbuf_get_height(pixbuf);
262
263         if (w != THUMB_SIZE_NORMAL && w != THUMB_SIZE_LARGE &&
264             h != THUMB_SIZE_NORMAL && h != THUMB_SIZE_LARGE) return FALSE;
265
266         valid_uri = (tl->thumb_path_local) ? tl->local_uri : tl->thumb_uri;
267
268         uri = gdk_pixbuf_get_option(pixbuf, THUMB_MARKER_URI);
269         mtime_str = gdk_pixbuf_get_option(pixbuf, THUMB_MARKER_MTIME);
270
271         if (!mtime_str || !uri || !valid_uri) return FALSE;
272         if (strcmp(uri, valid_uri) != 0) return FALSE;
273
274         mtime = strtol(mtime_str, nullptr, 10);
275         if (tl->source_mtime != mtime) return FALSE;
276
277         return TRUE;
278 }
279
280 static void thumb_loader_std_save(ThumbLoaderStd *tl, GdkPixbuf *pixbuf)
281 {
282         gchar *base_path;
283         gchar *tmp_path;
284         gboolean fail;
285
286         if (!tl->cache_enable || tl->cache_hit) return;
287         if (tl->thumb_path) return;
288
289         if (!pixbuf)
290                 {
291                 /* local failures are not stored */
292                 if (tl->cache_local) return;
293
294                 pixbuf = gdk_pixbuf_new(GDK_COLORSPACE_RGB, TRUE, 8, 1, 1);
295                 fail = TRUE;
296                 }
297         else
298                 {
299                 g_object_ref(G_OBJECT(pixbuf));
300                 fail = FALSE;
301                 }
302
303         tl->thumb_path = thumb_loader_std_cache_path(tl, tl->cache_local, pixbuf, fail);
304         if (!tl->thumb_path)
305                 {
306                 g_object_unref(G_OBJECT(pixbuf));
307                 return;
308                 }
309         tl->thumb_path_local = tl->cache_local;
310
311         /* create thumbnail dir if needed */
312         base_path = remove_level_from_path(tl->thumb_path);
313         if (tl->cache_local)
314                 {
315                 if (!isdir(base_path))
316                         {
317                         struct stat st;
318                         gchar *source_base;
319
320                         source_base = remove_level_from_path(tl->fd->path);
321                         if (stat_utf8(source_base, &st))
322                                 {
323                                 recursive_mkdir_if_not_exists(base_path, st.st_mode);
324                                 }
325                         g_free(source_base);
326                         }
327                 }
328         else
329                 {
330                 recursive_mkdir_if_not_exists(base_path, S_IRWXU);
331                 }
332         g_free(base_path);
333
334         DEBUG_1("thumb saving: %s", tl->fd->path);
335         DEBUG_1("       saved: %s", tl->thumb_path);
336
337         /* save thumb, using a temp file then renaming into place */
338         tmp_path = unique_filename(tl->thumb_path, ".tmp", "_", 2);
339         if (tmp_path)
340                 {
341                 const gchar *mark_uri;
342                 gchar *mark_app;
343                 gchar *mark_mtime;
344                 gchar *pathl;
345                 gboolean success;
346
347                 mark_uri = (tl->cache_local) ? tl->local_uri :tl->thumb_uri;
348
349                 mark_app = g_strdup_printf("%s %s", GQ_APPNAME, VERSION);
350                 mark_mtime = g_strdup_printf("%llu", static_cast<unsigned long long>(tl->source_mtime));
351                 pathl = path_from_utf8(tmp_path);
352                 success = gdk_pixbuf_save(pixbuf, pathl, "png", nullptr,
353                                           THUMB_MARKER_URI, mark_uri,
354                                           THUMB_MARKER_MTIME, mark_mtime,
355                                           THUMB_MARKER_APP, mark_app,
356                                           NULL);
357                 if (success)
358                         {
359                         chmod(pathl, (tl->cache_local) ? tl->source_mode : S_IRUSR & S_IWUSR);
360                         success = rename_file(tmp_path, tl->thumb_path);
361                         }
362
363                 g_free(pathl);
364
365                 g_free(mark_mtime);
366                 g_free(mark_app);
367
368                 g_free(tmp_path);
369                 if (!success)
370                         {
371                         DEBUG_1("thumb save failed: %s", tl->fd->path);
372                         DEBUG_1("            thumb: %s", tl->thumb_path);
373                         }
374
375                 }
376
377         g_object_unref(G_OBJECT(pixbuf));
378 }
379
380 static void thumb_loader_std_set_fallback(ThumbLoaderStd *tl)
381 {
382         if (tl->fd->thumb_pixbuf) g_object_unref(tl->fd->thumb_pixbuf);
383         tl->fd->thumb_pixbuf = pixbuf_fallback(tl->fd, tl->requested_width, tl->requested_height);
384 }
385
386
387 void thumb_loader_std_calibrate_pixbuf(FileData *fd, GdkPixbuf *pixbuf) {
388         ColorMan *cm = nullptr;
389         ExifData *exif = nullptr;
390         gint color_profile_from_image = COLOR_PROFILE_NONE;
391         ColorManProfileType input_type = COLOR_PROFILE_MEM;
392         ColorManProfileType screen_type;
393         const gchar *input_file = nullptr;
394         guchar *profile = nullptr;
395         guint profile_len;
396         gint sw;
397         gint sh;
398
399         if (!options->thumbnails.use_color_management)
400                 {
401                 return;
402                 }
403
404         sw = gdk_pixbuf_get_width(pixbuf);
405         sh = gdk_pixbuf_get_height(pixbuf);
406
407         exif = exif_read_fd(fd);
408
409         if (exif)
410                 {
411                 if (g_strcmp0(fd->format_name, "heif") == 0)
412                         {
413                         profile = heif_color_profile(fd, &profile_len);
414                         }
415
416                 if (!profile)
417                         {
418                         profile = exif_get_color_profile(exif, &profile_len);
419                         }
420
421                 if (profile)
422                         {
423                         DEBUG_1("Found embedded color profile");
424                         color_profile_from_image = COLOR_PROFILE_MEM;
425                         }
426                 else
427                         {
428                         gchar *interop_index = exif_get_data_as_text(exif, "Exif.Iop.InteroperabilityIndex");
429
430                         if (interop_index)
431                                 {
432                                 /* Exif 2.21 specification */
433                                 if (!strcmp(interop_index, "R98"))
434                                         {
435                                         color_profile_from_image = COLOR_PROFILE_SRGB;
436                                         DEBUG_1("Found EXIF 2.21 ColorSpace of sRGB");
437                                         }
438                                 else if (!strcmp(interop_index, "R03"))
439                                         {
440                                         color_profile_from_image = COLOR_PROFILE_ADOBERGB;
441                                         DEBUG_1("Found EXIF 2.21 ColorSpace of AdobeRGB");
442                                         }
443                                 g_free(interop_index);
444                                 }
445                         else
446                                 {
447                                 gint cs;
448
449                                 /* ColorSpace == 1 specifies sRGB per EXIF 2.2 */
450                                 if (!exif_get_integer(exif, "Exif.Photo.ColorSpace", &cs)) cs = 0;
451                                 if (cs == 1)
452                                         {
453                                         color_profile_from_image = COLOR_PROFILE_SRGB;
454                                         DEBUG_1("Found EXIF 2.2 ColorSpace of sRGB");
455                                         }
456                                 else if (cs == 2)
457                                         {
458                                         /* non-standard way of specifying AdobeRGB (used by some software) */
459                                         color_profile_from_image = COLOR_PROFILE_ADOBERGB;
460                                         DEBUG_1("Found EXIF 2.2 ColorSpace of AdobeRGB");
461                                         }
462                                 }
463                         }
464
465                 if(color_profile_from_image != COLOR_PROFILE_NONE)
466                         {
467                                 // transform image, we always use sRGB as target for thumbnails
468                                 screen_type = COLOR_PROFILE_SRGB;
469
470                                 if (profile)
471                                         {
472                                         cm = color_man_new_embedded(nullptr, pixbuf,
473                                                                         profile, profile_len,
474                                                                         screen_type, nullptr, nullptr, 0);
475                                         g_free(profile);
476                                         }
477                                 else
478                                         {
479                                         cm = color_man_new(nullptr, pixbuf,
480                                                         input_type, input_file,
481                                                         screen_type, nullptr, nullptr, 0);
482                                         }
483
484                                 if(cm) {
485                                         color_man_correct_region(cm, cm->pixbuf, 0, 0, sw, sh);
486                                         g_free(cm);
487                                 }
488
489                         }
490                 exif_free_fd(fd, exif);
491                 }
492 }
493
494 static GdkPixbuf *thumb_loader_std_finish(ThumbLoaderStd *tl, GdkPixbuf *pixbuf, gboolean shrunk)
495 {
496         GdkPixbuf *pixbuf_thumb = nullptr;
497         GdkPixbuf *result;
498         GdkPixbuf *rotated = nullptr;
499         gint sw;
500         gint sh;
501
502
503         if (!tl->cache_hit && options->image.exif_rotate_enable)
504                 {
505                 if (!tl->fd->exif_orientation)
506                         {
507                         if (g_strcmp0(tl->fd->format_name, "heif") != 0)
508                                 {
509                                 tl->fd->exif_orientation = metadata_read_int(tl->fd, ORIENTATION_KEY, EXIF_ORIENTATION_TOP_LEFT);
510                                 }
511                         else
512                                 {
513                                 tl->fd->exif_orientation = EXIF_ORIENTATION_TOP_LEFT;
514                                 }
515                         }
516
517                 if (tl->fd->exif_orientation != EXIF_ORIENTATION_TOP_LEFT)
518                         {
519                         rotated = pixbuf_apply_orientation(pixbuf, tl->fd->exif_orientation);
520                         pixbuf = rotated;
521                         }
522                 }
523
524         sw = gdk_pixbuf_get_width(pixbuf);
525         sh = gdk_pixbuf_get_height(pixbuf);
526
527         if (tl->cache_enable)
528                 {
529                 if (!tl->cache_hit)
530                         {
531                         gint cache_w;
532                         gint cache_h;
533
534                         if (tl->requested_width > THUMB_SIZE_NORMAL || tl->requested_height > THUMB_SIZE_NORMAL)
535                                 {
536                                 cache_w = cache_h = THUMB_SIZE_LARGE;
537                                 }
538                         else
539                                 {
540                                 cache_w = cache_h = THUMB_SIZE_NORMAL;
541                                 }
542
543                         if (sw > cache_w || sh > cache_h || shrunk)
544                                 {
545                                 gint thumb_w;
546                                 gint thumb_h;
547                                 struct stat st;
548
549                                 if (pixbuf_scale_aspect(cache_w, cache_h, sw, sh,
550                                                                   &thumb_w, &thumb_h))
551                                         {
552                                         pixbuf_thumb = gdk_pixbuf_scale_simple(pixbuf, thumb_w, thumb_h,
553                                                                                static_cast<GdkInterpType>(options->thumbnails.quality));
554                                         }
555                                 else
556                                         {
557                                         pixbuf_thumb = pixbuf;
558                                         g_object_ref(G_OBJECT(pixbuf_thumb));
559                                         }
560
561                                 /* do not save the thumbnail if the source file has changed meanwhile -
562                                    the thumbnail is most probably broken */
563                                 if (stat_utf8(tl->fd->path, &st) &&
564                                     tl->source_mtime == st.st_mtime &&
565                                     tl->source_size == st.st_size)
566                                         {
567                                         thumb_loader_std_save(tl, pixbuf_thumb);
568                                         }
569                                 }
570                         }
571                 else if (tl->cache_hit &&
572                          tl->cache_local && !tl->thumb_path_local)
573                         {
574                         /* A local cache save was requested, but a valid thumb is in $HOME,
575                          * so specifically save as a local thumbnail.
576                          */
577                         g_free(tl->thumb_path);
578                         tl->thumb_path = nullptr;
579
580                         tl->cache_hit = FALSE;
581
582                         DEBUG_1("thumb copied: %s", tl->fd->path);
583
584                         thumb_loader_std_save(tl, pixbuf);
585                         }
586                 }
587
588         if (sw <= tl->requested_width && sh <= tl->requested_height)
589                 {
590                 result = pixbuf;
591                 g_object_ref(result);
592                 }
593         else
594                 {
595                 gint thumb_w;
596                 gint thumb_h;
597
598                 if (pixbuf_thumb)
599                         {
600                         pixbuf = pixbuf_thumb;
601                         sw = gdk_pixbuf_get_width(pixbuf);
602                         sh = gdk_pixbuf_get_height(pixbuf);
603                         }
604
605                 if (pixbuf_scale_aspect(tl->requested_width, tl->requested_height, sw, sh,
606                                                   &thumb_w, &thumb_h))
607                         {
608                         result = gdk_pixbuf_scale_simple(pixbuf, thumb_w, thumb_h,
609                                                          static_cast<GdkInterpType>(options->thumbnails.quality));
610                         }
611                 else
612                         {
613                         result = pixbuf;
614                         g_object_ref(result);
615                         }
616                 }
617
618         // apply color correction, if required
619         thumb_loader_std_calibrate_pixbuf(tl->fd, result);
620
621         if (pixbuf_thumb) g_object_unref(pixbuf_thumb);
622         if (rotated) g_object_unref(rotated);
623
624         return result;
625 }
626
627 static gboolean thumb_loader_std_next_source(ThumbLoaderStd *tl, gboolean remove_broken)
628 {
629         image_loader_free(tl->il);
630         tl->il = nullptr;
631
632         if (tl->thumb_path)
633                 {
634                 if (!tl->thumb_path_local && remove_broken)
635                         {
636                         DEBUG_1("thumb broken, unlinking: %s", tl->thumb_path);
637                         unlink_file(tl->thumb_path);
638                         }
639
640                 g_free(tl->thumb_path);
641                 tl->thumb_path = nullptr;
642
643                 if (!tl->thumb_path_local)
644                         {
645                         tl->thumb_path = thumb_loader_std_cache_path(tl, TRUE, nullptr, FALSE);
646                         if (isfile(tl->thumb_path))
647                                 {
648                                 FileData *fd = file_data_new_no_grouping(tl->thumb_path);
649                                 if (thumb_loader_std_setup(tl, fd))
650                                         {
651                                         file_data_unref(fd);
652                                         tl->thumb_path_local = TRUE;
653                                         return TRUE;
654                                         }
655                                 file_data_unref(fd);
656                                 }
657
658                         g_free(tl->thumb_path);
659                         tl->thumb_path = nullptr;
660                         }
661
662                 if (thumb_loader_std_setup(tl, tl->fd)) return TRUE;
663                 }
664
665         thumb_loader_std_save(tl, nullptr);
666         return FALSE;
667 }
668
669 static void thumb_loader_std_done_cb(ImageLoader *il, gpointer data)
670 {
671         auto tl = static_cast<ThumbLoaderStd *>(data);
672         GdkPixbuf *pixbuf;
673
674         DEBUG_1("thumb image done: %s", tl->fd ? tl->fd->path : "???");
675         DEBUG_1("            from: %s", image_loader_get_fd(tl->il)->path);
676
677         pixbuf = image_loader_get_pixbuf(tl->il);
678         if (!pixbuf)
679                 {
680                 DEBUG_1("...but no pixbuf");
681                 thumb_loader_std_error_cb(il, data);
682                 return;
683                 }
684
685         if (tl->thumb_path && !thumb_loader_std_validate(tl, pixbuf))
686                 {
687                 if (thumb_loader_std_next_source(tl, TRUE)) return;
688
689                 if (tl->func_error) tl->func_error(tl, tl->data);
690                 return;
691                 }
692
693         tl->cache_hit = (tl->thumb_path != nullptr);
694
695         if (tl->fd)
696                 {
697                 if (tl->fd->thumb_pixbuf) g_object_unref(tl->fd->thumb_pixbuf);
698                 tl->fd->thumb_pixbuf = thumb_loader_std_finish(tl, pixbuf, image_loader_get_shrunk(il));
699                 }
700
701         if (tl->func_done) tl->func_done(tl, tl->data);
702 }
703
704 static void thumb_loader_std_error_cb(ImageLoader *il, gpointer data)
705 {
706         auto tl = static_cast<ThumbLoaderStd *>(data);
707
708         /* if at least some of the image is available, go to done */
709         if (image_loader_get_pixbuf(tl->il) != nullptr)
710                 {
711                 thumb_loader_std_done_cb(il, data);
712                 return;
713                 }
714
715         DEBUG_1("thumb image error: %s", tl->fd->path);
716         DEBUG_1("             from: %s", image_loader_get_fd(tl->il)->path);
717
718         if (thumb_loader_std_next_source(tl, TRUE)) return;
719
720         thumb_loader_std_set_fallback(tl);
721
722         if (tl->func_error) tl->func_error(tl, tl->data);
723 }
724
725 static void thumb_loader_std_progress_cb(ImageLoader *, gdouble percent, gpointer data)
726 {
727         auto tl = static_cast<ThumbLoaderStd *>(data);
728
729         tl->progress = percent;
730
731         if (tl->func_progress) tl->func_progress(tl, tl->data);
732 }
733
734 static gboolean thumb_loader_std_setup(ThumbLoaderStd *tl, FileData *fd)
735 {
736         tl->il = image_loader_new(fd);
737         image_loader_set_priority(tl->il, G_PRIORITY_LOW);
738
739         /* this will speed up jpegs by up to 3x in some cases */
740         if (tl->requested_width <= THUMB_SIZE_NORMAL &&
741             tl->requested_height <= THUMB_SIZE_NORMAL)
742                 {
743                 image_loader_set_requested_size(tl->il, THUMB_SIZE_NORMAL, THUMB_SIZE_NORMAL);
744                 }
745         else
746                 {
747                 image_loader_set_requested_size(tl->il, THUMB_SIZE_LARGE, THUMB_SIZE_LARGE);
748                 }
749
750         g_signal_connect(G_OBJECT(tl->il), "error", (GCallback)thumb_loader_std_error_cb, tl);
751         if (tl->func_progress)
752                 {
753                 g_signal_connect(G_OBJECT(tl->il), "percent", (GCallback)thumb_loader_std_progress_cb, tl);
754                 }
755         g_signal_connect(G_OBJECT(tl->il), "done", (GCallback)thumb_loader_std_done_cb, tl);
756
757         if (image_loader_start(tl->il))
758                 {
759                 return TRUE;
760                 }
761
762         image_loader_free(tl->il);
763         tl->il = nullptr;
764         return FALSE;
765 }
766
767 /*
768  * Note: Currently local_cache only specifies where to save a _new_ thumb, if
769  *       a valid existing thumb is found anywhere the local thumb will not be created.
770  */
771 void thumb_loader_std_set_cache(ThumbLoaderStd *tl, gboolean enable_cache, gboolean local, gboolean retry_failed)
772 {
773         if (!tl) return;
774
775         tl->cache_enable = enable_cache;
776         tl->cache_local = local;
777         tl->cache_retry = retry_failed;
778 }
779
780 gboolean thumb_loader_std_start(ThumbLoaderStd *tl, FileData *fd)
781 {
782         static gchar *thumb_cache = nullptr;
783         struct stat st;
784
785         if (!tl || !fd) return FALSE;
786
787         thumb_loader_std_reset(tl);
788
789
790         tl->fd = file_data_ref(fd);
791         if (!stat_utf8(fd->path, &st) || (tl->fd->format_class != FORMAT_CLASS_IMAGE && tl->fd->format_class != FORMAT_CLASS_RAWIMAGE && tl->fd->format_class != FORMAT_CLASS_VIDEO && tl->fd->format_class != FORMAT_CLASS_COLLECTION && tl->fd->format_class != FORMAT_CLASS_DOCUMENT && !options->file_filter.disable))
792                 {
793                 thumb_loader_std_set_fallback(tl);
794                 return FALSE;
795                 }
796         tl->source_mtime = st.st_mtime;
797         tl->source_size = st.st_size;
798         tl->source_mode = st.st_mode;
799
800         if (!thumb_cache)
801                 {
802                 thumb_cache = g_strdup(get_thumbnails_standard_cache_dir());
803                 }
804
805         if (strncmp(tl->fd->path, thumb_cache, strlen(thumb_cache)) != 0)
806                 {
807                 gchar *pathl;
808
809                 pathl = path_from_utf8(fd->path);
810                 tl->thumb_uri = g_filename_to_uri(pathl, nullptr, nullptr);
811                 tl->local_uri = filename_from_path(tl->thumb_uri);
812                 g_free(pathl);
813                 }
814
815         if (tl->cache_enable)
816                 {
817                 gint found;
818
819                 tl->thumb_path = thumb_loader_std_cache_path(tl, FALSE, nullptr, FALSE);
820                 tl->thumb_path_local = FALSE;
821
822                 found = isfile(tl->thumb_path);
823                 if (found)
824                         {
825                         FileData *fd = file_data_new_no_grouping(tl->thumb_path);
826                         if (thumb_loader_std_setup(tl, fd))
827                                 {
828                                 file_data_unref(fd);
829                                 return TRUE;
830                                 }
831                         file_data_unref(fd);
832                         }
833
834                 if (thumb_loader_std_fail_check(tl) ||
835                     !thumb_loader_std_next_source(tl, found))
836                         {
837                         thumb_loader_std_set_fallback(tl);
838                         return FALSE;
839                         }
840                 return TRUE;
841                 }
842
843         if (!thumb_loader_std_setup(tl, tl->fd))
844                 {
845                 thumb_loader_std_save(tl, nullptr);
846                 thumb_loader_std_set_fallback(tl);
847                 return FALSE;
848                 }
849
850         return TRUE;
851 }
852
853 void thumb_loader_std_free(ThumbLoaderStd *tl)
854 {
855         if (!tl) return;
856
857         thumb_loader_std_reset(tl);
858         g_free(tl);
859 }
860
861 GdkPixbuf *thumb_loader_std_get_pixbuf(ThumbLoaderStd *tl)
862 {
863         GdkPixbuf *pixbuf;
864
865         if (tl && tl->fd && tl->fd->thumb_pixbuf)
866                 {
867                 pixbuf = tl->fd->thumb_pixbuf;
868                 g_object_ref(pixbuf);
869                 }
870         else
871                 {
872                 pixbuf = pixbuf_fallback(nullptr, tl->requested_width, tl->requested_height);
873                 }
874
875         return pixbuf;
876 }
877
878
879 struct ThumbValidate
880 {
881         ThumbLoaderStd *tl;
882         gchar *path;
883         gint days;
884
885         void (*func_valid)(const gchar *path, gboolean valid, gpointer data);
886         gpointer data;
887
888         guint idle_id; /* event source id */
889 };
890
891 static void thumb_loader_std_thumb_file_validate_free(ThumbValidate *tv)
892 {
893         thumb_loader_std_free(tv->tl);
894         g_free(tv->path);
895         g_free(tv);
896 }
897
898 void thumb_loader_std_thumb_file_validate_cancel(ThumbLoaderStd *tl)
899 {
900         ThumbValidate *tv;
901
902         if (!tl) return;
903
904         tv = static_cast<ThumbValidate *>(tl->data);
905
906         if (tv->idle_id)
907                 {
908                 g_source_remove(tv->idle_id);
909                 tv->idle_id = 0;
910                 }
911
912         thumb_loader_std_thumb_file_validate_free(tv);
913 }
914
915 static void thumb_loader_std_thumb_file_validate_finish(ThumbValidate *tv, gboolean valid)
916 {
917         if (tv->func_valid) tv->func_valid(tv->path, valid, tv->data);
918
919         thumb_loader_std_thumb_file_validate_free(tv);
920 }
921
922 static void thumb_loader_std_thumb_file_validate_done_cb(ThumbLoaderStd *, gpointer data)
923 {
924         auto tv = static_cast<ThumbValidate *>(data);
925         GdkPixbuf *pixbuf;
926         gboolean valid = FALSE;
927
928         /* get the original thumbnail pixbuf (unrotated, with original options)
929            this is called from image_loader done callback, so tv->tl->il must exist*/
930         pixbuf = image_loader_get_pixbuf(tv->tl->il);
931         if (pixbuf)
932                 {
933                 const gchar *uri;
934                 const gchar *mtime_str;
935
936                 uri = gdk_pixbuf_get_option(pixbuf, THUMB_MARKER_URI);
937                 mtime_str = gdk_pixbuf_get_option(pixbuf, THUMB_MARKER_MTIME);
938                 if (uri && mtime_str)
939                         {
940                         if (strncmp(uri, "file:", strlen("file:")) == 0)
941                                 {
942                                 struct stat st;
943                                 gchar *target;
944
945                                 target = g_filename_from_uri(uri, nullptr, nullptr);
946                                 if (stat(target, &st) == 0 &&
947                                     st.st_mtime == strtol(mtime_str, nullptr, 10))
948                                         {
949                                         valid = TRUE;
950                                         }
951                                 g_free(target);
952                                 }
953                         else
954                                 {
955                                 struct stat st;
956
957                                 DEBUG_1("thumb uri foreign, doing day check: %s", uri);
958
959                                 if (stat_utf8(tv->path, &st))
960                                         {
961                                         time_t now;
962
963                                         now = time(nullptr);
964                                         if (st.st_atime >= now - static_cast<time_t>(tv->days) * 24 * 60 * 60)
965                                                 {
966                                                 valid = TRUE;
967                                                 }
968                                         }
969                                 }
970                         }
971                 else
972                         {
973                         DEBUG_1("invalid image found in std cache: %s", tv->path);
974                         }
975                 }
976
977         thumb_loader_std_thumb_file_validate_finish(tv, valid);
978 }
979
980 static void thumb_loader_std_thumb_file_validate_error_cb(ThumbLoaderStd *, gpointer data)
981 {
982         auto tv = static_cast<ThumbValidate *>(data);
983
984         thumb_loader_std_thumb_file_validate_finish(tv, FALSE);
985 }
986
987 static gboolean thumb_loader_std_thumb_file_validate_idle_cb(gpointer data)
988 {
989         auto tv = static_cast<ThumbValidate *>(data);
990
991         tv->idle_id = 0;
992         thumb_loader_std_thumb_file_validate_finish(tv, FALSE);
993
994         return G_SOURCE_REMOVE;
995 }
996
997 ThumbLoaderStd *thumb_loader_std_thumb_file_validate(const gchar *thumb_path, gint allowed_days,
998                                                      void (*func_valid)(const gchar *path, gboolean valid, gpointer data),
999                                                      gpointer data)
1000 {
1001         ThumbValidate *tv;
1002
1003         tv = g_new0(ThumbValidate, 1);
1004
1005         tv->tl = thumb_loader_std_new(THUMB_SIZE_LARGE, THUMB_SIZE_LARGE);
1006         thumb_loader_std_set_callbacks(tv->tl,
1007                                        thumb_loader_std_thumb_file_validate_done_cb,
1008                                        thumb_loader_std_thumb_file_validate_error_cb,
1009                                        nullptr,
1010                                        tv);
1011         thumb_loader_std_reset(tv->tl);
1012
1013         tv->path = g_strdup(thumb_path);
1014         tv->days = allowed_days;
1015         tv->func_valid = func_valid;
1016         tv->data = data;
1017
1018         FileData *fd = file_data_new_no_grouping(thumb_path);
1019         if (!thumb_loader_std_setup(tv->tl, fd))
1020                 {
1021                 tv->idle_id = g_idle_add(thumb_loader_std_thumb_file_validate_idle_cb, tv);
1022                 }
1023         else
1024                 {
1025                 tv->idle_id = 0;
1026                 }
1027
1028         file_data_unref(fd);
1029         return tv->tl;
1030 }
1031
1032 static void thumb_std_maint_remove_one(const gchar *source, const gchar *uri, gboolean local,
1033                                        const gchar *subfolder)
1034 {
1035         gchar *thumb_path;
1036
1037         thumb_path = thumb_std_cache_path(source,
1038                                           (local) ? filename_from_path(uri) : uri,
1039                                           local, subfolder);
1040         if (isfile(thumb_path))
1041                 {
1042                 DEBUG_1("thumb removing: %s", thumb_path);
1043                 unlink_file(thumb_path);
1044                 }
1045         g_free(thumb_path);
1046 }
1047
1048 /* this also removes local thumbnails (the source is gone so it makes sense) */
1049 void thumb_std_maint_removed(const gchar *source)
1050 {
1051         gchar *uri;
1052         gchar *sourcel;
1053
1054         sourcel = path_from_utf8(source);
1055         uri = g_filename_to_uri(sourcel, nullptr, nullptr);
1056         g_free(sourcel);
1057
1058         /* all this to remove a thumbnail? */
1059
1060         thumb_std_maint_remove_one(source, uri, FALSE, THUMB_FOLDER_NORMAL);
1061         thumb_std_maint_remove_one(source, uri, FALSE, THUMB_FOLDER_LARGE);
1062         thumb_std_maint_remove_one(source, uri, FALSE, THUMB_FOLDER_FAIL);
1063         thumb_std_maint_remove_one(source, uri, TRUE, THUMB_FOLDER_NORMAL);
1064         thumb_std_maint_remove_one(source, uri, TRUE, THUMB_FOLDER_LARGE);
1065
1066         g_free(uri);
1067 }
1068
1069 struct TMaintMove
1070 {
1071         gchar *source;
1072         gchar *dest;
1073
1074         ThumbLoaderStd *tl;
1075         gchar *source_uri;
1076         gchar *thumb_path;
1077
1078         gint pass;
1079 };
1080
1081 static GList *thumb_std_maint_move_list = nullptr;
1082 static GList *thumb_std_maint_move_tail = nullptr;
1083
1084
1085 static void thumb_std_maint_move_step(TMaintMove *tm);
1086 static gboolean thumb_std_maint_move_idle(gpointer data);
1087
1088
1089 static void thumb_std_maint_move_validate_cb(const gchar *, gboolean, gpointer data)
1090 {
1091         auto tm = static_cast<TMaintMove *>(data);
1092         GdkPixbuf *pixbuf;
1093
1094         /* get the original thumbnail pixbuf (unrotated, with original options)
1095            this is called from image_loader done callback, so tm->tl->il must exist*/
1096         pixbuf = image_loader_get_pixbuf(tm->tl->il);
1097         if (pixbuf)
1098                 {
1099                 const gchar *uri;
1100                 const gchar *mtime_str;
1101
1102                 uri = gdk_pixbuf_get_option(pixbuf, THUMB_MARKER_URI);
1103                 mtime_str = gdk_pixbuf_get_option(pixbuf, THUMB_MARKER_MTIME);
1104
1105                 if (uri && mtime_str && strcmp(uri, tm->source_uri) == 0)
1106                         {
1107                         gchar *pathl;
1108
1109                         /* The validation utility abuses ThumbLoader, and we
1110                          * abuse the utility just to load the thumbnail,
1111                          * but the loader needs to look sane for the save to complete.
1112                          */
1113
1114                         tm->tl->cache_enable = TRUE;
1115                         tm->tl->cache_hit = FALSE;
1116                         tm->tl->cache_local = FALSE;
1117                         file_data_unref(tm->tl->fd);
1118                         tm->tl->fd = file_data_new_group(tm->dest);
1119                         tm->tl->source_mtime = strtol(mtime_str, nullptr, 10);
1120
1121                         pathl = path_from_utf8(tm->tl->fd->path);
1122                         g_free(tm->tl->thumb_uri);
1123                         tm->tl->thumb_uri = g_filename_to_uri(pathl, nullptr, nullptr);
1124                         tm->tl->local_uri = filename_from_path(tm->tl->thumb_uri);
1125                         g_free(pathl);
1126
1127                         g_free(tm->tl->thumb_path);
1128                         tm->tl->thumb_path = nullptr;
1129                         tm->tl->thumb_path_local = FALSE;
1130
1131                         DEBUG_1("thumb move attempting save:");
1132
1133                         thumb_loader_std_save(tm->tl, pixbuf);
1134                         }
1135
1136                 DEBUG_1("thumb move unlink: %s", tm->thumb_path);
1137                 unlink_file(tm->thumb_path);
1138                 }
1139
1140         thumb_std_maint_move_step(tm);
1141 }
1142
1143 static void thumb_std_maint_move_step(TMaintMove *tm)
1144 {
1145         const gchar *folder;
1146
1147         tm->pass++;
1148         if (tm->pass > 2)
1149                 {
1150                 g_free(tm->source);
1151                 g_free(tm->dest);
1152                 g_free(tm->source_uri);
1153                 g_free(tm->thumb_path);
1154                 g_free(tm);
1155
1156                 if (thumb_std_maint_move_list)
1157                         {
1158                         g_idle_add_full(G_PRIORITY_LOW, thumb_std_maint_move_idle, nullptr, nullptr);
1159                         }
1160
1161                 return;
1162                 }
1163
1164         folder = (tm->pass == 1) ? THUMB_FOLDER_NORMAL : THUMB_FOLDER_LARGE;
1165
1166         g_free(tm->thumb_path);
1167         tm->thumb_path = thumb_std_cache_path(tm->source, tm->source_uri, FALSE, folder);
1168         tm->tl = thumb_loader_std_thumb_file_validate(tm->thumb_path, 0,
1169                                                       thumb_std_maint_move_validate_cb, tm);
1170 }
1171
1172 static gboolean thumb_std_maint_move_idle(gpointer)
1173 {
1174         TMaintMove *tm;
1175         gchar *pathl;
1176
1177         if (!thumb_std_maint_move_list) return G_SOURCE_REMOVE;
1178
1179         tm = static_cast<TMaintMove *>(thumb_std_maint_move_list->data);
1180
1181         thumb_std_maint_move_list = g_list_remove(thumb_std_maint_move_list, tm);
1182         if (!thumb_std_maint_move_list) thumb_std_maint_move_tail = nullptr;
1183
1184         pathl = path_from_utf8(tm->source);
1185         tm->source_uri = g_filename_to_uri(pathl, nullptr, nullptr);
1186         g_free(pathl);
1187
1188         tm->pass = 0;
1189
1190         thumb_std_maint_move_step(tm);
1191
1192         return G_SOURCE_REMOVE;
1193 }
1194
1195 /* This will schedule a move of the thumbnail for source image to dest when idle.
1196  * We do this so that file renaming or moving speed is not sacrificed by
1197  * moving the thumbnails at the same time because:
1198  *
1199  * This cache design requires the tedious task of loading the png thumbnails and saving them.
1200  *
1201  * The thumbnails are processed when the app is idle. If the app
1202  * exits early well too bad - they can simply be regenerated from scratch.
1203  */
1204 /** @FIXME This does not manage local thumbnails (fixme ?)
1205  */
1206 void thumb_std_maint_moved(const gchar *source, const gchar *dest)
1207 {
1208         TMaintMove *tm;
1209
1210         tm = g_new0(TMaintMove, 1);
1211         tm->source = g_strdup(source);
1212         tm->dest = g_strdup(dest);
1213
1214         if (!thumb_std_maint_move_list)
1215                 {
1216                 g_idle_add_full(G_PRIORITY_LOW, thumb_std_maint_move_idle, nullptr, nullptr);
1217                 }
1218
1219         if (thumb_std_maint_move_tail)
1220                 {
1221                 thumb_std_maint_move_tail = g_list_append(thumb_std_maint_move_tail, tm);
1222                 thumb_std_maint_move_tail = thumb_std_maint_move_tail->next;
1223                 }
1224         else
1225                 {
1226                 thumb_std_maint_move_list = g_list_append(thumb_std_maint_move_list, tm);
1227                 thumb_std_maint_move_tail = thumb_std_maint_move_list;
1228                 }
1229 }
1230 /* vim: set shiftwidth=8 softtabstop=0 cindent cinoptions={1s: */