/* * fm-thumbnail.c * * Copyright 2010 Hong Jen Yee (PCMan) * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, * MA 02110-1301, USA. */ #ifdef HAVE_CONFIG_H #include #endif /* TODO: * Thunar can directly load embedded thumbnail in jpeg files, we need that, too. * Need to support external thumbnailers. * */ #include "fm-thumbnail.h" #include #include #include /* FIXME: this function prototype seems to be missing in header files of GdkPixbuf. Bug report to them. */ gboolean gdk_pixbuf_set_option(GdkPixbuf *pixbuf, const gchar *key, const gchar *value); /* #define ENABLE_DEBUG */ #ifdef ENABLE_DEBUG #define DEBUG(...) g_debug(__VA_ARGS__) #else #define DEBUG(...) #endif typedef enum { LOAD_NORMAL = 1 << 0, /* need to load normal thumbnail */ LOAD_LARGE = 1 << 1, /* need to load large thumbnail */ GENERATE_NORMAL = 1 << 2, /* need to regenerated normal thumbnail */ GENERATE_LARGE = 1 << 3, /* need to regenerated large thumbnail */ CANCEL = 1 << 4, /* the task is cancelled */ ALLOC_STRINGS = 1 << 5, /* uri, normal_path, and large_path are dynamically allocated and needs to be freed later */ }ThumbnailTaskFlags; typedef struct _ThumbnailTask ThumbnailTask; struct _ThumbnailTask { FmFileInfo* fi; ThumbnailTaskFlags flags; char* uri; char* normal_path; char* large_path; GList* requests; }; #define IS_CANCELLED(task) (task->flags & CANCEL) struct _FmThumbnailRequest { FmFileInfo* fi; ThumbnailTask* task; gboolean size; FmThumbnailReadyCallback callback; gpointer user_data; GdkPixbuf* pix; }; typedef struct _ThumbnailCacheItem ThumbnailCacheItem; struct _ThumbnailCacheItem { guint size; GdkPixbuf* pix; }; typedef struct _ThumbnailCache ThumbnailCache; struct _ThumbnailCache { FmPath* path; GSList* items; }; /* FIXME: use thread pool */ G_LOCK_DEFINE_STATIC(queue); /* load generated thumbnails */ static GQueue loader_queue = G_QUEUE_INIT; static GThread* loader_thread_id = NULL; /* generate thumbnails for files */ static GQueue generator_queue = G_QUEUE_INIT; static GThread* generator_thread_id = NULL; static GCancellable* generator_cancellable = NULL; static ThumbnailTask* cur_loading = NULL; static ThumbnailTask* cur_generating = NULL; /* already loaded thumbnails */ static GQueue ready_queue = G_QUEUE_INIT; /* idle handler to call ready callback */ static guint ready_idle_handler = 0; /* cached thumbnails */ static GHashTable* hash = NULL; static char* thumb_dir = NULL; static ThumbnailTask* find_queued_task(GQueue* queue, FmFileInfo* fi); static gpointer load_thumbnail_thread(gpointer user_data); static gpointer generate_thumbnail_thread(gpointer user_data); static void thumbnail_task_finish(ThumbnailTask* task, GdkPixbuf* normal_pix, GdkPixbuf* large_pix); static void queue_generate(ThumbnailTask* task); static void load_thumbnails(ThumbnailTask* task); static void generate_thumbnails_with_gdk_pixbuf(ThumbnailTask* task); static void generate_thumbnails_with_thumbnailers(ThumbnailTask* task); inline static GdkPixbuf* scale_pix(GdkPixbuf* ori_pix, int size); static void save_thumbnail_to_disk(ThumbnailTask* task, GdkPixbuf* pix, const char* path); static void fm_thumbnail_request_free(FmThumbnailRequest* req) { if(req->pix) g_object_unref(req->pix); g_slice_free(FmThumbnailRequest, req); } inline static void thumbnail_task_free(ThumbnailTask* task) { if(task->requests) { g_list_foreach(task->requests, (GFunc)fm_thumbnail_request_free, NULL); g_list_free(task->requests); } fm_file_info_unref(task->fi); /* if those strings are dynamically allocated, free them. */ if(task->flags & ALLOC_STRINGS) { g_free(task->uri); g_free(task->normal_path); g_free(task->large_path); } g_slice_free(ThumbnailTask, task); } static gboolean on_ready_idle(gpointer user_data) { FmThumbnailRequest* req; G_LOCK(queue); while( req = (FmThumbnailRequest*)g_queue_pop_head(&ready_queue) ) { // GDK_THREADS_ENTER(); req->callback(req, req->user_data); // GDK_THREADS_LEAVE(); fm_thumbnail_request_free(req); } ready_idle_handler = 0; G_UNLOCK(queue); return FALSE; } static gint comp_request(FmThumbnailRequest* a, FmThumbnailRequest* b) { return a->size - b->size; } /* called when cached pixbuf get destroyed */ static void on_pixbuf_destroy(ThumbnailCache* cache, GdkPixbuf* pix) { GSList* l; /* remove it from cache */ DEBUG("remove from cache!"); for(l=cache->items;l;l=l->next) { ThumbnailCacheItem* item = (ThumbnailCacheItem*)l->data; if(item->pix == pix) { cache->items = g_slist_delete_link(cache->items, l); g_slice_free(ThumbnailCacheItem, item); if(!cache->items) { g_hash_table_remove(hash, cache->path); fm_path_unref(cache->path); g_slice_free(ThumbnailCache, cache); } break; } } } inline static void cache_thumbnail_in_hash(FmPath* path, GdkPixbuf* pix, guint size) { ThumbnailCache* cache; ThumbnailCacheItem* item; GSList* l = NULL; cache = (ThumbnailCache*)g_hash_table_lookup(hash, path); if(cache) { for(l=cache->items;l;l=l->next) { item = (ThumbnailCacheItem*)l->data; if(item->size == size) break; } } else { cache = g_slice_new0(ThumbnailCache); cache->path = fm_path_ref(path); g_hash_table_insert(hash, cache->path, cache); } if(!l) /* the item is not in cache->items */ { item = g_slice_new(ThumbnailCacheItem); item->size = size; item->pix = pix; cache->items = g_slist_prepend(cache->items, item); g_object_weak_ref(G_OBJECT(pix), (GWeakNotify)on_pixbuf_destroy, cache); } } /* called with queue lock held */ void thumbnail_task_finish(ThumbnailTask* task, GdkPixbuf* normal_pix, GdkPixbuf* large_pix) { GdkPixbuf* cached_pix = NULL; gint cached_size = 0; GList* l; /* sort the requests by requested size to utilize cached scaled pixbuf */ task->requests = g_list_sort(task->requests, (GCompareFunc)comp_request); for(l=task->requests; l; l=l->next) { FmThumbnailRequest* req = (FmThumbnailRequest*)l->data; /* the thumbnail is ready, queue the request in ready queue. */ /* later, the ready callbacks will be called in idle handler of main thread. */ if(req->size == cached_size) { req->pix = cached_pix ? (GdkPixbuf*)g_object_ref(cached_pix) : NULL; DEBUG("cache hit!"); g_queue_push_tail(&ready_queue, req); if( 0 == ready_idle_handler ) /* schedule an idle handler if there isn't one. */ ready_idle_handler = g_idle_add_full(G_PRIORITY_LOW, on_ready_idle, NULL, NULL); continue; } if(G_LIKELY(req->size <= 128)) /* normal */ { if(normal_pix) req->pix = scale_pix(normal_pix, req->size); else req->pix = NULL; } else /* large */ { if(large_pix) req->pix = scale_pix(large_pix, req->size); else req->pix = NULL; } if(cached_pix) g_object_unref(cached_pix); cached_pix = req->pix ? g_object_ref(req->pix) : NULL; cached_size = req->size; /* cache this in hash table */ if(cached_pix) cache_thumbnail_in_hash(req->fi->path, cached_pix, cached_size); g_queue_push_tail(&ready_queue, req); if( 0 == ready_idle_handler ) /* schedule an idle handler if there isn't one. */ ready_idle_handler = g_idle_add_full(G_PRIORITY_LOW, on_ready_idle, NULL, NULL); } if(cached_pix) g_object_unref(cached_pix); g_list_free(task->requests); task->requests = NULL; thumbnail_task_free(task); } inline static gboolean is_thumbnail_outdated(GdkPixbuf* thumb_pix, const char* path, time_t mtime) { const char* thumb_mtime = gdk_pixbuf_get_option(thumb_pix, "tEXt::Thumb::MTime"); /* out of date, delete it */ if( !thumb_mtime || atol(thumb_mtime) != mtime ) { unlink(path); /* delete the out-dated thumbnail. */ g_object_unref(thumb_pix); return TRUE; } return FALSE; } void load_thumbnails(ThumbnailTask* task) { GList* l; GdkPixbuf* normal_pix = NULL; GdkPixbuf* large_pix = NULL; const char* normal_path = task->normal_path; const char* large_path = task->large_path; if( IS_CANCELLED(task) ) goto _out; DEBUG("loading: %s, %s", task->fi->path->name, normal_path); if(task->flags & LOAD_NORMAL) { normal_pix = gdk_pixbuf_new_from_file(normal_path, NULL); if(!normal_pix || is_thumbnail_outdated(normal_pix, normal_path, task->fi->mtime)) { /* normal_pix is freed in is_thumbnail_outdated() if it's out of date. */ /* generate normal size thumbnail */ task->flags |= GENERATE_NORMAL; normal_pix = NULL; /* DEBUG("need to generate normal thumbnail"); */ } else DEBUG("normal thumbnail loaded: %p", normal_pix); } if( IS_CANCELLED(task) ) goto _out; if(task->flags & LOAD_LARGE) { large_pix = gdk_pixbuf_new_from_file(large_path, NULL); if(!large_pix || is_thumbnail_outdated(large_pix, large_path, task->fi->mtime)) { /* large_pix is freed in is_thumbnail_outdated() if it's out of date. */ /* generate large size thumbnail */ task->flags |= GENERATE_LARGE; large_pix = NULL; } } if( IS_CANCELLED(task) ) goto _out; if(task->flags & (GENERATE_NORMAL|GENERATE_LARGE)) /* need to re-generate some thumbnails */ { GList* generate_reqs = NULL, *l; ThumbnailTask* generate_task; #if 0 /* all requested thumbnails need to be re-generated. */ if( ((task->flags & LOAD_NORMAL|LOAD_LARGE) << 2) == (task->flags & (GENERATE_NORMAL|GENERATE_LARGE)) ) { task->uri = g_strdup(task->uri); task->normal_path = g_strdup(normal_path); task->large_path = g_strdup(large_path); task->flags |= ALLOC_STRINGS; /* push the whole task into generator queue */ queue_generate(task); return; } #endif /* remove all requests which requires re-generating thumbnails from task and gather them in a list */ for(l=task->requests; l; ) { FmThumbnailRequest* req = (FmThumbnailRequest*)l->data; GList* next = l->next; if(req->size <= 128) /* need normal thumbnail */ { if(task->flags & GENERATE_NORMAL) { task->requests = g_list_remove_link(task->requests, l); generate_reqs = g_list_concat(generate_reqs, l); } } else /* need large thumbnail */ { if(task->flags & GENERATE_LARGE) { task->requests = g_list_remove_link(task->requests, l); generate_reqs = g_list_concat(generate_reqs, l); } } l = next; } /* this list contains requests requiring regeration of thumbnails */ if(generate_reqs) { generate_task = g_slice_new0(ThumbnailTask); generate_task->flags = task->flags | ALLOC_STRINGS; generate_task->fi = fm_file_info_ref(task->fi); generate_task->requests = generate_reqs; generate_task->uri = g_strdup(task->uri); generate_task->normal_path = g_strdup(task->normal_path); generate_task->large_path = g_strdup(task->large_path); DEBUG("queue regenerate for :%s", task->fi->path->name); /* queue the re-generation task */ queue_generate(generate_task); } } _out: G_LOCK(queue); /* thumbnails which don't require re-generation should all be loaded at this point. */ if( IS_CANCELLED(task) || !task->requests ) thumbnail_task_free(task); else thumbnail_task_finish(task, normal_pix, large_pix); cur_loading = NULL; /* task is freed in thumbnail_task_finish() */ G_UNLOCK(queue); if(normal_pix) g_object_unref(normal_pix); if(large_pix) g_object_unref(large_pix); return; } gpointer load_thumbnail_thread(gpointer user_data) { ThumbnailTask* task; GChecksum* sum = g_checksum_new(G_CHECKSUM_MD5); gchar* normal_path = g_build_filename(thumb_dir, "normal/00000000000000000000000000000000.png", NULL); gchar* normal_basename = strrchr(normal_path, '/') + 1; gchar* large_path = g_build_filename(thumb_dir, "large/00000000000000000000000000000000.png", NULL); gchar* large_basename = strrchr(large_path, '/') + 1; /* ensure thumbnail directories exists */ g_mkdir_with_parents(normal_path, 0700); g_mkdir_with_parents(large_path, 0700); for(;;) { G_LOCK(queue); task = g_queue_pop_head(&loader_queue); cur_loading = task; if(G_LIKELY(task)) { FmThumbnailRequest* req; char* uri; char* thumb_path; const char* md5; G_UNLOCK(queue); uri = fm_path_to_uri(task->fi->path); /* generate filename for the thumbnail */ g_checksum_update(sum, uri, -1); md5 = g_checksum_get_string(sum); /* md5 sum of the URI */ task->uri = uri; if (task->flags & LOAD_NORMAL) { memcpy( normal_basename, md5, 32 ); task->normal_path = normal_path; } if (task->flags & LOAD_LARGE) { memcpy( large_basename, md5, 32 ); task->large_path = large_path; } load_thumbnails(task); g_checksum_reset(sum); g_free(uri); } else /* no task is left in the loader_queue */ { loader_thread_id = NULL; G_UNLOCK(queue); break; } } g_free(normal_path); g_free(large_path); g_checksum_free(sum); return NULL; } /* should be called with queue locked */ inline static GdkPixbuf* find_thumbnail_in_hash(FmPath* path, guint size) { ThumbnailCache* cache = (ThumbnailCache*)g_hash_table_lookup(hash, path); if(cache) { GSList* l; for(l=cache->items;l;l=l->next) { ThumbnailCacheItem* item = (ThumbnailCacheItem*)l->data; if(item->size == size) return item->pix; } } return NULL; } /* should be called with queue locked */ ThumbnailTask* find_queued_task(GQueue* queue, FmFileInfo* fi) { GList* l; for( l = queue->head; l; l=l->next ) { ThumbnailTask* task = (ThumbnailTask*)l->data; if(G_UNLIKELY(task->fi == fi || fm_path_equal(task->fi->path, fi->path))) return task; } return NULL; } FmThumbnailRequest* fm_thumbnail_request(FmFileInfo* src_file, guint size, FmThumbnailReadyCallback callback, gpointer user_data) { FmThumbnailRequest* req; ThumbnailTask* task; GdkPixbuf* pix; req = g_slice_new(FmThumbnailRequest); req->fi = fm_file_info_ref(src_file); req->size = size; req->callback = callback; req->user_data = user_data; req->pix = NULL; DEBUG("request thumbnail: %s", src_file->path->name); G_LOCK(queue); /* FIXME: find in the cache first to see if thumbnail is already cached */ pix = find_thumbnail_in_hash(src_file->path, size); if(pix) { DEBUG("cache found!"); req->pix = (GdkPixbuf*)g_object_ref(pix); /* call the ready callback in main loader_thread_id from idle handler. */ g_queue_push_tail(&ready_queue, req); if( 0 == ready_idle_handler ) /* schedule an idle handler if there isn't one. */ ready_idle_handler = g_idle_add_full(G_PRIORITY_LOW, on_ready_idle, NULL, NULL); G_UNLOCK(queue); return req; } /* if it's not cached, add it to the loader_queue for loading. */ task = find_queued_task(&loader_queue, src_file); if(!task) { task = g_slice_new0(ThumbnailTask); task->fi = fm_file_info_ref(src_file); g_queue_push_tail(&loader_queue, task); } else { DEBUG("task already in the queue: %p", task); } if(size > 128) task->flags |= LOAD_LARGE; else task->flags |= LOAD_NORMAL; task->requests = g_list_append(task->requests, req); if(!loader_thread_id) loader_thread_id = g_thread_create( load_thumbnail_thread, NULL, FALSE, NULL); G_UNLOCK(queue); return req; } void fm_thumbnail_request_cancel(FmThumbnailRequest* req) { ThumbnailTask* task; GList* l, *l2; G_LOCK(queue); /* if it's in generator queue (most likely) */ if(cur_generating && cur_generating->requests) { /* this is the currently processed item */ if( l2=g_list_find(cur_generating->requests, req) ) { cur_generating->requests = g_list_delete_link(cur_generating->requests, l2); if(!cur_generating->requests) { cur_generating->flags |= CANCEL; g_cancellable_cancel(generator_cancellable); } G_UNLOCK(queue); return; } } for(l=generator_queue.head; l; l=l->next) { task = (ThumbnailTask*)l->data; if(l2 = g_list_find(task->requests, req)) /* found the request */ { task->requests = g_list_delete_link(task->requests, l2); if(!task->requests) /* no one is requesting this thumbnail */ { if(l == generator_queue.head) /* this is the currently processed item */ { task->flags |= CANCEL; if(generator_cancellable) g_cancellable_cancel(generator_cancellable); g_queue_delete_link(&generator_queue, l); } else { g_queue_delete_link(&generator_queue, l); thumbnail_task_free(task); } } G_UNLOCK(queue); return; } } /* not found, try loader queue */ if(cur_loading && cur_loading->requests) { /* this is the currently processed item */ if( l2=g_list_find(cur_loading->requests, req) ) { cur_loading->requests = g_list_delete_link(cur_loading->requests, l2); if(!cur_loading->requests) cur_loading->flags |= CANCEL; G_UNLOCK(queue); return; } } for(l=loader_queue.head; l; l=l->next) { task = (ThumbnailTask*)l->data; if(l2 = g_list_find(task->requests, req)) /* found the request */ { task->requests = g_list_delete_link(task->requests, l2); if(!task->requests) /* no one is requesting this thumbnail */ { g_queue_delete_link(&loader_queue, l); thumbnail_task_free(task); } G_UNLOCK(queue); return; } } /* not found in both loader or generator queue */ /* is it in ready queue? */ l = g_queue_find(&ready_queue, req); if(l) { g_queue_delete_link(&ready_queue, l); fm_thumbnail_request_free(req); /* if there is no item left in ready queue, cancel idle handler */ if(g_queue_is_empty(&ready_queue) && ready_idle_handler) { g_source_remove(ready_idle_handler); ready_idle_handler = 0; } } G_UNLOCK(queue); } GdkPixbuf* fm_thumbnail_request_get_pixbuf(FmThumbnailRequest* req) { return req->pix; } FmFileInfo* fm_thumbnail_request_get_file_info(FmThumbnailRequest* req) { return req->fi; } guint fm_thumbnail_request_get_size(FmThumbnailRequest* req) { return req->size; } void _fm_thumbnail_init() { thumb_dir = g_build_filename(g_get_home_dir(), ".thumbnails", NULL); hash = g_hash_table_new((GHashFunc)fm_path_hash, fm_path_equal); } void _fm_thumbnail_finalize() { g_hash_table_destroy(hash); hash = NULL; /* FIXME: cancel all pending requests... */ g_free(thumb_dir); } gpointer generate_thumbnail_thread(gpointer user_data) { ThumbnailTask* task; generator_cancellable = g_cancellable_new(); for(;;) { G_LOCK(queue); task = g_queue_pop_head(&generator_queue); cur_generating = task; DEBUG("pop task from generator queue: %p", task); if( G_LIKELY(task) ) { G_UNLOCK(queue); if(fm_file_info_is_image(task->fi)) generate_thumbnails_with_gdk_pixbuf(task); else generate_thumbnails_with_thumbnailers(task); if(g_cancellable_is_cancelled(generator_cancellable)) { DEBUG("generation of thumbnail is cancelled!"); g_cancellable_reset(generator_cancellable); } } else { generator_thread_id = NULL; DEBUG("no task is in generator queue, exit generator thread"); g_object_unref(generator_cancellable); generator_cancellable = NULL; G_UNLOCK(queue); return NULL; } } generator_cancellable = NULL; G_UNLOCK(queue); return NULL; } void queue_generate(ThumbnailTask* regenerate_task) { ThumbnailTask* task; G_LOCK(queue); task = find_queued_task(&generator_queue, regenerate_task->fi); if(task) { task->flags |= regenerate_task->flags; task->requests = g_list_concat(task->requests, regenerate_task->requests); regenerate_task->requests = NULL; thumbnail_task_free(regenerate_task); G_UNLOCK(queue); return; } DEBUG("push into generator queue"); g_queue_push_tail(&generator_queue, regenerate_task); if(!generator_thread_id) generator_thread_id = g_thread_create(generate_thumbnail_thread, NULL, FALSE, NULL); G_UNLOCK(queue); } GdkPixbuf* scale_pix(GdkPixbuf* ori_pix, int size) { GdkPixbuf* scaled_pix; /* keep aspect ratio and scale to thumbnail size: 128 or 256 */ int width = gdk_pixbuf_get_width(ori_pix); int height = gdk_pixbuf_get_height(ori_pix); int new_width; int new_height; if(width > height) { gdouble aspect = (gdouble)height / width; new_width = size; new_height = size * aspect; } else if(width < height) { gdouble aspect = (gdouble)width / height; new_height = size; new_width = size * aspect; } else { new_width = new_height = size; } if((new_width == width && new_height == height) || (size > width && size > height )) /* don't scale up */ { /* if size is not changed or original size is smaller, use original size. */ scaled_pix = (GdkPixbuf*)g_object_ref(ori_pix); } else scaled_pix = gdk_pixbuf_scale_simple(ori_pix, new_width, new_height, GDK_INTERP_BILINEAR); return scaled_pix; } void save_thumbnail_to_disk(ThumbnailTask* task, GdkPixbuf* pix, const char* path) { /* save the generated thumbnail to disk */ char* tmpfile = g_strconcat(path, ".XXXXXX", NULL); gint fd; fd = g_mkstemp(tmpfile); /* save to a temp file first */ if(fd != -1) { char mtime_str[100]; g_snprintf( mtime_str, 100, "%lu", task->fi->mtime ); chmod( tmpfile, 0600 ); /* only the owner can read it. */ gdk_pixbuf_save( pix, tmpfile, "png", NULL, "tEXt::Thumb::URI", task->uri, "tEXt::Thumb::MTime", mtime_str, NULL ); close(fd); g_rename(tmpfile, path); g_free(tmpfile); } DEBUG("generator: save to %s", path); } void generate_thumbnails_with_gdk_pixbuf(ThumbnailTask* task) { /* FIXME: only formats supported by GdkPixbuf should be handled this way. */ GFile* gf = fm_path_to_gfile(task->fi->path); GFileInputStream* ins; GdkPixbuf* normal_pix = NULL; GdkPixbuf* large_pix = NULL; DEBUG("generate thumbnail for %s", task->fi->path->name); if( ins = g_file_read(gf, generator_cancellable, NULL) ) { GdkPixbuf* ori_pix; gssize len; ori_pix = gdk_pixbuf_new_from_stream(G_INPUT_STREAM(ins), generator_cancellable, NULL); if(ori_pix) /* if the original image is successfully loaded */ { const char* orientation_str = gdk_pixbuf_get_option(ori_pix, "orientation"); int width = gdk_pixbuf_get_width(ori_pix); int height = gdk_pixbuf_get_height(ori_pix); gboolean need_save; if(task->flags & GENERATE_NORMAL) { /* don't create thumbnails for images which are too small */ if(width <=128 && height <= 128) { normal_pix = (GdkPixbuf*)g_object_ref(ori_pix); need_save = FALSE; } else { normal_pix = scale_pix(ori_pix, 128); need_save = TRUE; } if(orientation_str) { GdkPixbuf* rotated; gdk_pixbuf_set_option(normal_pix, "orientation", orientation_str); rotated = gdk_pixbuf_apply_embedded_orientation(normal_pix); g_object_unref(normal_pix); normal_pix = rotated; } if(need_save) save_thumbnail_to_disk(task, normal_pix, task->normal_path); } if(task->flags & GENERATE_LARGE) { /* don't create thumbnails for images which are too small */ if(width <=256 && height <= 256) { large_pix = (GdkPixbuf*)g_object_ref(ori_pix); need_save = FALSE; } else { large_pix = scale_pix(ori_pix, 256); need_save = TRUE; } if(orientation_str) { GdkPixbuf* rotated; gdk_pixbuf_set_option(large_pix, "orientation", orientation_str); rotated = gdk_pixbuf_apply_embedded_orientation(large_pix); g_object_unref(large_pix); large_pix = rotated; } if(need_save) save_thumbnail_to_disk(task, large_pix, task->large_path); } g_object_unref(ori_pix); } g_input_stream_close(ins, NULL, NULL); } G_LOCK(queue); thumbnail_task_finish(task, normal_pix, large_pix); cur_generating = NULL; G_UNLOCK(queue); if(normal_pix) g_object_unref(normal_pix); if(large_pix) g_object_unref(large_pix); g_object_unref(gf); } void generate_thumbnails_with_thumbnailers(ThumbnailTask* task) { /* TODO: external thumbnailer support */ DEBUG("external thumbnailer is needed for %s", task->fi->disp_name); G_LOCK(queue); thumbnail_task_finish(task, NULL, NULL); cur_generating = NULL; G_UNLOCK(queue); }