4 * Copyright (C) 2008 - 2009 The Geeqie Team
6 * Author: John Ellis, Laurent Monin
8 * This software is released under the GNU General Public License (GNU GPL).
9 * Please read the included file COPYING for more information.
10 * This software comes with no warranty of any kind, use at your own risk!
21 #include "secure_save.h"
22 #include "ui_fileops.h"
25 #include "filefilter.h"
34 static const gchar *group_keys[] = {KEYWORD_KEY, COMMENT_KEY, NULL}; /* tags that will be written to all files in a group */
36 static gboolean metadata_write_queue_idle_cb(gpointer data);
37 static gint metadata_legacy_write(FileData *fd);
38 static void metadata_legacy_delete(FileData *fd, const gchar *except);
43 *-------------------------------------------------------------------
45 *-------------------------------------------------------------------
48 static GList *metadata_write_queue = NULL;
49 static gint metadata_write_idle_id = -1;
51 static void metadata_write_queue_add(FileData *fd)
53 if (!g_list_find(metadata_write_queue, fd))
55 metadata_write_queue = g_list_prepend(metadata_write_queue, fd);
58 layout_status_update_write_all();
61 if (metadata_write_idle_id != -1)
63 g_source_remove(metadata_write_idle_id);
64 metadata_write_idle_id = -1;
67 if (options->metadata.confirm_after_timeout)
69 metadata_write_idle_id = g_timeout_add(options->metadata.confirm_timeout * 1000, metadata_write_queue_idle_cb, NULL);
74 gboolean metadata_write_queue_remove(FileData *fd)
76 g_hash_table_destroy(fd->modified_xmp);
77 fd->modified_xmp = NULL;
79 metadata_write_queue = g_list_remove(metadata_write_queue, fd);
81 file_data_increment_version(fd);
82 file_data_send_notification(fd, NOTIFY_TYPE_REREAD);
86 layout_status_update_write_all();
90 gboolean metadata_write_queue_remove_list(GList *list)
98 FileData *fd = work->data;
100 ret = ret && metadata_write_queue_remove(fd);
106 gboolean metadata_write_queue_confirm(FileUtilDoneFunc done_func, gpointer done_data)
109 GList *to_approve = NULL;
111 work = metadata_write_queue;
114 FileData *fd = work->data;
117 if (fd->change) continue; /* another operation in progress, skip this file for now */
119 to_approve = g_list_prepend(to_approve, file_data_ref(fd));
122 file_util_write_metadata(NULL, to_approve, NULL, done_func, done_data);
124 filelist_free(to_approve);
126 return (metadata_write_queue != NULL);
129 static gboolean metadata_write_queue_idle_cb(gpointer data)
131 metadata_write_queue_confirm(NULL, NULL);
132 metadata_write_idle_id = -1;
136 gboolean metadata_write_perform(FileData *fd)
141 g_assert(fd->change);
143 if (fd->change->dest &&
144 strcmp(extension_from_path(fd->change->dest), GQ_CACHE_EXT_METADATA) == 0)
146 success = metadata_legacy_write(fd);
147 if (success) metadata_legacy_delete(fd, fd->change->dest);
151 /* write via exiv2 */
152 /* we can either use cached metadata which have fd->modified_xmp already applied
153 or read metadata from file and apply fd->modified_xmp
154 metadata are read also if the file was modified meanwhile */
155 exif = exif_read_fd(fd);
156 if (!exif) return FALSE;
158 success = (fd->change->dest) ? exif_write_sidecar(exif, fd->change->dest) : exif_write(exif); /* write modified metadata */
159 exif_free_fd(fd, exif);
161 if (fd->change->dest)
162 /* this will create a FileData for the sidecar and link it to the main file
163 (we can't wait until the sidecar is discovered by directory scanning because
164 exif_read_fd is called before that and it would read the main file only and
165 store the metadata in the cache)
166 FIXME: this does not catch new sidecars created by independent external programs
168 file_data_unref(file_data_new_simple(fd->change->dest));
170 if (success) metadata_legacy_delete(fd, fd->change->dest);
174 gint metadata_queue_length(void)
176 return g_list_length(metadata_write_queue);
179 static gboolean metadata_check_key(const gchar *keys[], const gchar *key)
181 const gchar **k = keys;
185 if (strcmp(key, *k) == 0) return TRUE;
191 gboolean metadata_write_list(FileData *fd, const gchar *key, const GList *values)
193 if (!fd->modified_xmp)
195 fd->modified_xmp = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, (GDestroyNotify)string_list_free);
197 g_hash_table_insert(fd->modified_xmp, g_strdup(key), string_list_copy((GList *)values));
200 exif_update_metadata(fd->exif, key, values);
202 metadata_write_queue_add(fd);
203 file_data_increment_version(fd);
204 file_data_send_notification(fd, NOTIFY_TYPE_INTERNAL);
206 if (options->metadata.sync_grouped_files && metadata_check_key(group_keys, key))
208 GList *work = fd->sidecar_files;
212 FileData *sfd = work->data;
215 if (filter_file_class(sfd->extension, FORMAT_CLASS_META)) continue;
217 metadata_write_list(sfd, key, values);
225 gboolean metadata_write_string(FileData *fd, const gchar *key, const char *value)
227 GList *list = g_list_append(NULL, g_strdup(value));
228 gboolean ret = metadata_write_list(fd, key, list);
229 string_list_free(list);
235 *-------------------------------------------------------------------
236 * keyword / comment read/write
237 *-------------------------------------------------------------------
240 static gint metadata_file_write(gchar *path, GHashTable *modified_xmp)
243 GList *keywords = g_hash_table_lookup(modified_xmp, KEYWORD_KEY);
244 GList *comment_l = g_hash_table_lookup(modified_xmp, COMMENT_KEY);
245 gchar *comment = comment_l ? comment_l->data : NULL;
247 ssi = secure_open(path);
248 if (!ssi) return FALSE;
250 secure_fprintf(ssi, "#%s comment (%s)\n\n", GQ_APPNAME, VERSION);
252 secure_fprintf(ssi, "[keywords]\n");
253 while (keywords && secsave_errno == SS_ERR_NONE)
255 const gchar *word = keywords->data;
256 keywords = keywords->next;
258 secure_fprintf(ssi, "%s\n", word);
260 secure_fputc(ssi, '\n');
262 secure_fprintf(ssi, "[comment]\n");
263 secure_fprintf(ssi, "%s\n", (comment) ? comment : "");
265 secure_fprintf(ssi, "#end\n");
267 return (secure_close(ssi) == 0);
270 static gint metadata_legacy_write(FileData *fd)
272 gint success = FALSE;
274 g_assert(fd->change && fd->change->dest);
275 gchar *metadata_pathl;
277 DEBUG_1("Saving comment: %s", fd->change->dest);
279 metadata_pathl = path_from_utf8(fd->change->dest);
281 success = metadata_file_write(metadata_pathl, fd->modified_xmp);
283 g_free(metadata_pathl);
288 static gint metadata_file_read(gchar *path, GList **keywords, gchar **comment)
292 MetadataKey key = MK_NONE;
294 GString *comment_build = NULL;
296 f = fopen(path, "r");
297 if (!f) return FALSE;
299 while (fgets(s_buf, sizeof(s_buf), f))
303 if (*ptr == '#') continue;
304 if (*ptr == '[' && key != MK_COMMENT)
306 gchar *keystr = ++ptr;
309 while (*ptr != ']' && *ptr != '\n' && *ptr != '\0') ptr++;
314 if (g_ascii_strcasecmp(keystr, "keywords") == 0)
316 else if (g_ascii_strcasecmp(keystr, "comment") == 0)
328 while (*ptr != '\n' && *ptr != '\0') ptr++;
330 if (strlen(s_buf) > 0)
332 gchar *kw = utf8_validate_or_convert(s_buf);
334 list = g_list_prepend(list, kw);
339 if (!comment_build) comment_build = g_string_new("");
340 g_string_append(comment_build, s_buf);
349 *keywords = g_list_reverse(list);
353 string_list_free(list);
361 gchar *ptr = comment_build->str;
363 /* strip leading and trailing newlines */
364 while (*ptr == '\n') ptr++;
366 while (len > 0 && ptr[len - 1] == '\n') len--;
367 if (ptr[len] == '\n') len++; /* keep the last one */
370 gchar *text = g_strndup(ptr, len);
372 *comment = utf8_validate_or_convert(text);
376 g_string_free(comment_build, TRUE);
382 static void metadata_legacy_delete(FileData *fd, const gchar *except)
384 gchar *metadata_path;
385 gchar *metadata_pathl;
388 metadata_path = cache_find_location(CACHE_TYPE_METADATA, fd->path);
389 if (metadata_path && (!except || strcmp(metadata_path, except) != 0))
391 metadata_pathl = path_from_utf8(metadata_path);
392 unlink(metadata_pathl);
393 g_free(metadata_pathl);
394 g_free(metadata_path);
396 metadata_path = cache_find_location(CACHE_TYPE_XMP_METADATA, fd->path);
397 if (metadata_path && (!except || strcmp(metadata_path, except) != 0))
399 metadata_pathl = path_from_utf8(metadata_path);
400 unlink(metadata_pathl);
401 g_free(metadata_pathl);
402 g_free(metadata_path);
406 static gint metadata_legacy_read(FileData *fd, GList **keywords, gchar **comment)
408 gchar *metadata_path;
409 gchar *metadata_pathl;
410 gint success = FALSE;
411 if (!fd) return FALSE;
413 metadata_path = cache_find_location(CACHE_TYPE_METADATA, fd->path);
414 if (!metadata_path) return FALSE;
416 metadata_pathl = path_from_utf8(metadata_path);
418 success = metadata_file_read(metadata_pathl, keywords, comment);
420 g_free(metadata_pathl);
421 g_free(metadata_path);
426 static GList *remove_duplicate_strings_from_list(GList *list)
429 GHashTable *hashtable = g_hash_table_new(g_str_hash, g_str_equal);
430 GList *newlist = NULL;
434 gchar *key = work->data;
436 if (g_hash_table_lookup(hashtable, key) == NULL)
438 g_hash_table_insert(hashtable, (gpointer) key, GINT_TO_POINTER(1));
439 newlist = g_list_prepend(newlist, key);
444 g_hash_table_destroy(hashtable);
447 return g_list_reverse(newlist);
450 GList *metadata_read_list(FileData *fd, const gchar *key, MetadataFormat format)
454 if (!fd) return NULL;
456 /* unwritten data overide everything */
457 if (fd->modified_xmp && format == METADATA_PLAIN)
459 list = g_hash_table_lookup(fd->modified_xmp, key);
460 if (list) return string_list_copy(list);
464 Legacy metadata file is the primary source if it exists.
465 Merging the lists does not make much sense, because the existence of
466 legacy metadata file indicates that the other metadata sources are not
467 writable and thus it would not be possible to delete the keywords
468 that comes from the image file.
470 if (strcmp(key, KEYWORD_KEY) == 0)
472 if (metadata_legacy_read(fd, &list, NULL)) return list;
475 if (strcmp(key, COMMENT_KEY) == 0)
477 gchar *comment = NULL;
478 if (metadata_legacy_read(fd, NULL, &comment)) return g_list_append(NULL, comment);
481 exif = exif_read_fd(fd); /* this is cached, thus inexpensive */
482 if (!exif) return NULL;
483 list = exif_get_metadata(exif, key, format);
484 exif_free_fd(fd, exif);
488 gchar *metadata_read_string(FileData *fd, const gchar *key, MetadataFormat format)
490 GList *string_list = metadata_read_list(fd, key, format);
493 gchar *str = string_list->data;
494 string_list->data = NULL;
495 string_list_free(string_list);
501 guint64 metadata_read_int(FileData *fd, const gchar *key, guint64 fallback)
505 gchar *string = metadata_read_string(fd, key, METADATA_PLAIN);
506 if (!string) return fallback;
508 ret = g_ascii_strtoull(string, &endptr, 10);
509 if (string == endptr) ret = fallback;
514 gboolean metadata_append_string(FileData *fd, const gchar *key, const char *value)
516 gchar *str = metadata_read_string(fd, key, METADATA_PLAIN);
520 return metadata_write_string(fd, key, value);
524 gchar *new_string = g_strconcat(str, value, NULL);
525 gboolean ret = metadata_write_string(fd, key, new_string);
532 gboolean metadata_append_list(FileData *fd, const gchar *key, const GList *values)
534 GList *list = metadata_read_list(fd, key, METADATA_PLAIN);
538 return metadata_write_list(fd, key, values);
543 list = g_list_concat(list, string_list_copy(values));
544 list = remove_duplicate_strings_from_list(list);
546 ret = metadata_write_list(fd, key, list);
547 string_list_free(list);
552 gchar *find_string_in_list_utf8nocase(GList *list, const gchar *string)
554 gchar *string_casefold = g_utf8_casefold(string, -1);
558 gchar *haystack = list->data;
563 gchar *haystack_casefold = g_utf8_casefold(haystack, -1);
565 equal = (strcmp(haystack_casefold, string_casefold) == 0);
566 g_free(haystack_casefold);
570 g_free(string_casefold);
578 g_free(string_casefold);
583 #define KEYWORDS_SEPARATOR(c) ((c) == ',' || (c) == ';' || (c) == '\n' || (c) == '\r' || (c) == '\b')
585 GList *string_to_keywords_list(const gchar *text)
588 const gchar *ptr = text;
595 while (KEYWORDS_SEPARATOR(*ptr)) ptr++;
597 while (*ptr != '\0' && !KEYWORDS_SEPARATOR(*ptr))
603 /* trim starting and ending whitespaces */
604 while (l > 0 && g_ascii_isspace(*begin)) begin++, l--;
605 while (l > 0 && g_ascii_isspace(begin[l-1])) l--;
609 gchar *keyword = g_strndup(begin, l);
611 /* only add if not already in the list */
612 if (!find_string_in_list_utf8nocase(list, keyword))
613 list = g_list_append(list, keyword);
627 gboolean meta_data_get_keyword_mark(FileData *fd, gint n, gpointer data)
630 gboolean found = FALSE;
631 keywords = metadata_read_list(fd, KEYWORD_KEY, METADATA_PLAIN);
634 GList *work = keywords;
638 gchar *kw = work->data;
641 if (strcmp(kw, data) == 0)
647 string_list_free(keywords);
652 gboolean meta_data_set_keyword_mark(FileData *fd, gint n, gboolean value, gpointer data)
654 GList *keywords = NULL;
655 gboolean found = FALSE;
656 gboolean changed = FALSE;
658 keywords = metadata_read_list(fd, KEYWORD_KEY, METADATA_PLAIN);
664 gchar *kw = work->data;
666 if (strcmp(kw, data) == 0)
672 keywords = g_list_delete_link(keywords, work);
682 keywords = g_list_append(keywords, g_strdup(data));
685 if (changed) metadata_write_list(fd, KEYWORD_KEY, keywords);
687 string_list_free(keywords);
692 /* vim: set shiftwidth=8 softtabstop=0 cindent cinoptions={1s: */