prepared infrastructure for delayed metadata writting - refreshing
[geeqie.git] / src / metadata.c
1 /*
2  * Geeqie
3  * (C) 2004 John Ellis
4  * Copyright (C) 2008 The Geeqie Team
5  *
6  * Author: John Ellis, Laurent Monin
7  *
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!
11  */
12
13
14 #include "main.h"
15 #include "metadata.h"
16
17 #include "cache.h"
18 #include "exif.h"
19 #include "filedata.h"
20 #include "misc.h"
21 #include "secure_save.h"
22 #include "ui_fileops.h"
23 #include "ui_misc.h"
24 #include "utilops.h"
25
26 typedef enum {
27         MK_NONE,
28         MK_KEYWORDS,
29         MK_COMMENT
30 } MetadataKey;
31
32
33 gint metadata_write_list(FileData *fd, const gchar *key, GList *values)
34 {
35         if (!fd->modified_xmp)
36                 {
37                 fd->modified_xmp = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, (GDestroyNotify)string_list_free);
38                 }
39         g_hash_table_insert(fd->modified_xmp, g_strdup(key), values);
40         if (fd->exif)
41                 {
42                 exif_update_metadata(fd->exif, key, values);
43                 }
44         return TRUE;
45 }
46         
47 gint metadata_write_string(FileData *fd, const gchar *key, const char *value)
48 {
49         return metadata_write_list(fd, key, g_list_append(NULL, g_strdup(value)));
50 }
51
52
53 /*
54  *-------------------------------------------------------------------
55  * keyword / comment read/write
56  *-------------------------------------------------------------------
57  */
58
59 static gint metadata_file_write(gchar *path, GList *keywords, const gchar *comment)
60 {
61         SecureSaveInfo *ssi;
62
63         ssi = secure_open(path);
64         if (!ssi) return FALSE;
65
66         secure_fprintf(ssi, "#%s comment (%s)\n\n", GQ_APPNAME, VERSION);
67
68         secure_fprintf(ssi, "[keywords]\n");
69         while (keywords && secsave_errno == SS_ERR_NONE)
70                 {
71                 const gchar *word = keywords->data;
72                 keywords = keywords->next;
73
74                 secure_fprintf(ssi, "%s\n", word);
75                 }
76         secure_fputc(ssi, '\n');
77
78         secure_fprintf(ssi, "[comment]\n");
79         secure_fprintf(ssi, "%s\n", (comment) ? comment : "");
80
81         secure_fprintf(ssi, "#end\n");
82
83         return (secure_close(ssi) == 0);
84 }
85
86 static gint metadata_legacy_write(FileData *fd, GList *keywords, const gchar *comment)
87 {
88         gchar *metadata_path;
89         gint success = FALSE;
90
91         /* If an existing metadata file exists, we will try writing to
92          * it's location regardless of the user's preference.
93          */
94         metadata_path = cache_find_location(CACHE_TYPE_METADATA, fd->path);
95         if (metadata_path && !access_file(metadata_path, W_OK))
96                 {
97                 g_free(metadata_path);
98                 metadata_path = NULL;
99                 }
100
101         if (!metadata_path)
102                 {
103                 gchar *metadata_dir;
104                 mode_t mode = 0755;
105
106                 metadata_dir = cache_get_location(CACHE_TYPE_METADATA, fd->path, FALSE, &mode);
107                 if (recursive_mkdir_if_not_exists(metadata_dir, mode))
108                         {
109                         gchar *filename = g_strconcat(fd->name, GQ_CACHE_EXT_METADATA, NULL);
110                         
111                         metadata_path = g_build_filename(metadata_dir, filename, NULL);
112                         g_free(filename);
113                         }
114                 g_free(metadata_dir);
115                 }
116
117         if (metadata_path)
118                 {
119                 gchar *metadata_pathl;
120
121                 DEBUG_1("Saving comment: %s", metadata_path);
122
123                 metadata_pathl = path_from_utf8(metadata_path);
124
125                 success = metadata_file_write(metadata_pathl, keywords, comment);
126
127                 g_free(metadata_pathl);
128                 g_free(metadata_path);
129                 }
130
131         return success;
132 }
133
134 static gint metadata_file_read(gchar *path, GList **keywords, gchar **comment)
135 {
136         FILE *f;
137         gchar s_buf[1024];
138         MetadataKey key = MK_NONE;
139         GList *list = NULL;
140         GString *comment_build = NULL;
141
142         f = fopen(path, "r");
143         if (!f) return FALSE;
144
145         while (fgets(s_buf, sizeof(s_buf), f))
146                 {
147                 gchar *ptr = s_buf;
148
149                 if (*ptr == '#') continue;
150                 if (*ptr == '[' && key != MK_COMMENT)
151                         {
152                         gchar *keystr = ++ptr;
153                         
154                         key = MK_NONE;
155                         while (*ptr != ']' && *ptr != '\n' && *ptr != '\0') ptr++;
156                         
157                         if (*ptr == ']')
158                                 {
159                                 *ptr = '\0';
160                                 if (g_ascii_strcasecmp(keystr, "keywords") == 0)
161                                         key = MK_KEYWORDS;
162                                 else if (g_ascii_strcasecmp(keystr, "comment") == 0)
163                                         key = MK_COMMENT;
164                                 }
165                         continue;
166                         }
167                 
168                 switch(key)
169                         {
170                         case MK_NONE:
171                                 break;
172                         case MK_KEYWORDS:
173                                 {
174                                 while (*ptr != '\n' && *ptr != '\0') ptr++;
175                                 *ptr = '\0';
176                                 if (strlen(s_buf) > 0)
177                                         {
178                                         gchar *kw = utf8_validate_or_convert(s_buf);
179
180                                         list = g_list_prepend(list, kw);
181                                         }
182                                 }
183                                 break;
184                         case MK_COMMENT:
185                                 if (!comment_build) comment_build = g_string_new("");
186                                 g_string_append(comment_build, s_buf);
187                                 break;
188                         }
189                 }
190         
191         fclose(f);
192
193         *keywords = g_list_reverse(list);
194         if (comment_build)
195                 {
196                 if (comment)
197                         {
198                         gint len;
199                         gchar *ptr = comment_build->str;
200
201                         /* strip leading and trailing newlines */
202                         while (*ptr == '\n') ptr++;
203                         len = strlen(ptr);
204                         while (len > 0 && ptr[len - 1] == '\n') len--;
205                         if (ptr[len] == '\n') len++; /* keep the last one */
206                         if (len > 0)
207                                 {
208                                 gchar *text = g_strndup(ptr, len);
209
210                                 *comment = utf8_validate_or_convert(text);
211                                 g_free(text);
212                                 }
213                         }
214                 g_string_free(comment_build, TRUE);
215                 }
216
217         return TRUE;
218 }
219
220 static gint metadata_legacy_delete(FileData *fd)
221 {
222         gchar *metadata_path;
223         gchar *metadata_pathl;
224         gint success = FALSE;
225         if (!fd) return FALSE;
226
227         metadata_path = cache_find_location(CACHE_TYPE_METADATA, fd->path);
228         if (!metadata_path) return FALSE;
229
230         metadata_pathl = path_from_utf8(metadata_path);
231
232         success = !unlink(metadata_pathl);
233
234         g_free(metadata_pathl);
235         g_free(metadata_path);
236
237         return success;
238 }
239
240 static gint metadata_legacy_read(FileData *fd, GList **keywords, gchar **comment)
241 {
242         gchar *metadata_path;
243         gchar *metadata_pathl;
244         gint success = FALSE;
245         if (!fd) return FALSE;
246
247         metadata_path = cache_find_location(CACHE_TYPE_METADATA, fd->path);
248         if (!metadata_path) return FALSE;
249
250         metadata_pathl = path_from_utf8(metadata_path);
251
252         success = metadata_file_read(metadata_pathl, keywords, comment);
253
254         g_free(metadata_pathl);
255         g_free(metadata_path);
256
257         return success;
258 }
259
260 static GList *remove_duplicate_strings_from_list(GList *list)
261 {
262         GList *work = list;
263         GHashTable *hashtable = g_hash_table_new(g_str_hash, g_str_equal);
264         GList *newlist = NULL;
265
266         while (work)
267                 {
268                 gchar *key = work->data;
269
270                 if (g_hash_table_lookup(hashtable, key) == NULL)
271                         {
272                         g_hash_table_insert(hashtable, (gpointer) key, GINT_TO_POINTER(1));
273                         newlist = g_list_prepend(newlist, key);
274                         }
275                 work = work->next;
276                 }
277
278         g_hash_table_destroy(hashtable);
279         g_list_free(list);
280
281         return g_list_reverse(newlist);
282 }
283
284 #define COMMENT_KEY "Xmp.dc.description"
285 #define KEYWORD_KEY "Xmp.dc.subject"
286
287 static gint metadata_xmp_read(FileData *fd, GList **keywords, gchar **comment)
288 {
289         ExifData *exif;
290
291         exif = exif_read_fd(fd);
292         if (!exif) return FALSE;
293
294         if (comment)
295                 {
296                 gchar *text;
297                 ExifItem *item = exif_get_item(exif, COMMENT_KEY);
298
299                 text = exif_item_get_string(item, 0);
300                 *comment = utf8_validate_or_convert(text);
301                 g_free(text);
302                 }
303
304         if (keywords)
305                 {
306                 ExifItem *item;
307                 guint i;
308                 
309                 *keywords = NULL;
310                 item = exif_get_item(exif, KEYWORD_KEY);
311                 for (i = 0; i < exif_item_get_elements(item); i++)
312                         {
313                         gchar *kw = exif_item_get_string(item, i);
314                         gchar *utf8_kw;
315
316                         if (!kw) break;
317
318                         utf8_kw = utf8_validate_or_convert(kw);
319                         *keywords = g_list_append(*keywords, (gpointer) utf8_kw);
320                         g_free(kw);
321                         }
322
323                 /* FIXME:
324                  * Exiv2 handles Iptc keywords as multiple entries with the
325                  * same key, thus exif_get_item returns only the first keyword
326                  * and the only way to get all keywords is to iterate through
327                  * the item list.
328                  */
329                 for (item = exif_get_first_item(exif);
330                      item;
331                      item = exif_get_next_item(exif))
332                         {
333                         guint tag;
334                 
335                         tag = exif_item_get_tag_id(item);
336                         if (tag == 0x0019)
337                                 {
338                                 gchar *tag_name = exif_item_get_tag_name(item);
339
340                                 if (strcmp(tag_name, "Iptc.Application2.Keywords") == 0)
341                                         {
342                                         gchar *kw;
343                                         gchar *utf8_kw;
344
345                                         kw = exif_item_get_data_as_text(item);
346                                         if (!kw) continue;
347
348                                         utf8_kw = utf8_validate_or_convert(kw);
349                                         *keywords = g_list_append(*keywords, (gpointer) utf8_kw);
350                                         g_free(kw);
351                                         }
352                                 g_free(tag_name);
353                                 }
354                         }
355                 }
356
357         exif_free_fd(fd, exif);
358
359         return (comment && *comment) || (keywords && *keywords);
360 }
361
362 static gint metadata_xmp_write(FileData *fd, GList *keywords, const gchar *comment)
363 {
364         gint success = TRUE;
365         gint write_comment = (comment && comment[0]);
366
367         if (write_comment) success = success && metadata_write_string(fd, COMMENT_KEY, comment);
368         
369         if (keywords) success = success && metadata_write_list(fd, KEYWORD_KEY, string_list_copy(keywords));
370
371
372 /* FIXME - actual writting should be triggered in metadata_write_list and should be delayed */
373         success = exif_write_fd(fd);
374
375         return success;
376 }
377
378 gint metadata_write(FileData *fd, GList *keywords, const gchar *comment)
379 {
380         if (!fd) return FALSE;
381
382         if (options->save_metadata_in_image_file &&
383             metadata_xmp_write(fd, keywords, comment))
384                 {
385                 metadata_legacy_delete(fd);
386                 return TRUE;
387                 }
388
389         return metadata_legacy_write(fd, keywords, comment);
390 }
391
392 gint metadata_read(FileData *fd, GList **keywords, gchar **comment)
393 {
394         GList *keywords_xmp = NULL;
395         GList *keywords_legacy = NULL;
396         gchar *comment_xmp = NULL;
397         gchar *comment_legacy = NULL;
398         gint result_xmp, result_legacy;
399
400         if (!fd) return FALSE;
401
402         result_xmp = metadata_xmp_read(fd, &keywords_xmp, &comment_xmp);
403         result_legacy = metadata_legacy_read(fd, &keywords_legacy, &comment_legacy);
404
405         if (!result_xmp && !result_legacy)
406                 {
407                 return FALSE;
408                 }
409
410         if (keywords)
411                 {
412                 if (result_xmp && result_legacy)
413                         *keywords = g_list_concat(keywords_xmp, keywords_legacy);
414                 else
415                         *keywords = result_xmp ? keywords_xmp : keywords_legacy;
416
417                 *keywords = remove_duplicate_strings_from_list(*keywords);
418                 }
419         else
420                 {
421                 if (result_xmp) string_list_free(keywords_xmp);
422                 if (result_legacy) string_list_free(keywords_legacy);
423                 }
424
425
426         if (comment)
427                 {
428                 if (result_xmp && result_legacy && comment_xmp && comment_legacy && *comment_xmp && *comment_legacy)
429                         *comment = g_strdup_printf("%s\n%s", comment_xmp, comment_legacy);
430                 else
431                         *comment = result_xmp ? comment_xmp : comment_legacy;
432                 }
433
434         if (result_xmp && (!comment || *comment != comment_xmp)) g_free(comment_xmp);
435         if (result_legacy && (!comment || *comment != comment_legacy)) g_free(comment_legacy);
436         
437         // return FALSE in the following cases:
438         //  - only looking for a comment and didn't find one
439         //  - only looking for keywords and didn't find any
440         //  - looking for either a comment or keywords, but found nothing
441         if ((!keywords && comment   && !*comment)  ||
442             (!comment  && keywords  && !*keywords) ||
443             ( comment  && !*comment &&   keywords && !*keywords))
444                 return FALSE;
445
446         return TRUE;
447 }
448
449 void metadata_set(FileData *fd, GList *new_keywords, gchar *new_comment, gboolean append)
450 {
451         gchar *comment = NULL;
452         GList *keywords = NULL;
453         GList *keywords_list = NULL;
454
455         metadata_read(fd, &keywords, &comment);
456         
457         if (new_comment)
458                 {
459                 if (append && comment && *comment)
460                         {
461                         gchar *tmp = comment;
462                                 
463                         comment = g_strconcat(tmp, new_comment, NULL);
464                         g_free(tmp);
465                         }
466                 else
467                         {
468                         g_free(comment);
469                         comment = g_strdup(new_comment);
470                         }
471                 }
472         
473         if (new_keywords)
474                 {
475                 if (append && keywords && g_list_length(keywords) > 0)
476                         {
477                         GList *work;
478
479                         work = new_keywords;
480                         while (work)
481                                 {
482                                 gchar *key;
483                                 GList *p;
484
485                                 key = work->data;
486                                 work = work->next;
487
488                                 p = keywords;
489                                 while (p && key)
490                                         {
491                                         gchar *needle = p->data;
492                                         p = p->next;
493
494                                         if (strcmp(needle, key) == 0) key = NULL;
495                                         }
496
497                                 if (key) keywords = g_list_append(keywords, g_strdup(key));
498                                 }
499                         keywords_list = keywords;
500                         }
501                 else
502                         {
503                         keywords_list = new_keywords;
504                         }
505                 }
506         
507         metadata_write(fd, keywords_list, comment);
508
509         string_list_free(keywords);
510         g_free(comment);
511 }
512
513 gboolean find_string_in_list(GList *list, const gchar *string)
514 {
515         while (list)
516                 {
517                 gchar *haystack = list->data;
518
519                 if (haystack && string && strcmp(haystack, string) == 0) return TRUE;
520
521                 list = list->next;
522                 }
523
524         return FALSE;
525 }
526
527 #define KEYWORDS_SEPARATOR(c) ((c) == ',' || (c) == ';' || (c) == '\n' || (c) == '\r' || (c) == '\b')
528
529 GList *string_to_keywords_list(const gchar *text)
530 {
531         GList *list = NULL;
532         const gchar *ptr = text;
533
534         while (*ptr != '\0')
535                 {
536                 const gchar *begin;
537                 gint l = 0;
538
539                 while (KEYWORDS_SEPARATOR(*ptr)) ptr++;
540                 begin = ptr;
541                 while (*ptr != '\0' && !KEYWORDS_SEPARATOR(*ptr))
542                         {
543                         ptr++;
544                         l++;
545                         }
546
547                 /* trim starting and ending whitespaces */
548                 while (l > 0 && g_ascii_isspace(*begin)) begin++, l--;
549                 while (l > 0 && g_ascii_isspace(begin[l-1])) l--;
550
551                 if (l > 0)
552                         {
553                         gchar *keyword = g_strndup(begin, l);
554
555                         /* only add if not already in the list */
556                         if (find_string_in_list(list, keyword) == FALSE)
557                                 list = g_list_append(list, keyword);
558                         else
559                                 g_free(keyword);
560                         }
561                 }
562
563         return list;
564 }
565
566 /* vim: set shiftwidth=8 softtabstop=0 cindent cinoptions={1s: */