Update french translation for "Options are saved when quitting".
[gliv.git] / src / collection.c
blob642563fa3145df3cbd9b3997a5389b6150471f57
1 /*
2 * This program is free software; you can redistribute it and/or
3 * modify it under the terms of the GNU General Public License
4 * as published by the Free Software Foundation; either version 2
5 * of the License, or (at your option) any later version.
7 * This program is distributed in the hope that it will be useful,
8 * but WITHOUT ANY WARRANTY; without even the implied warranty of
9 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 * GNU General Public License for more details.
12 * You should have received a copy of the GNU General Public License
13 * along with this program; if not, write to the Free Software
14 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
16 * See the COPYING file for license information.
18 * Guillaume Chazarain <guichaz@yahoo.fr>
21 /********************
22 * GLiv collections *
23 ********************/
26 * +--------------------------------+
27 * | The .gliv format specification |
28 * +--------------------------------+
30 * Newlines are here just for clarity. All numbers are represented in a string.
31 * So 123 is actually represented: '1''2''3''\0'
32 * PixelData is a char* so it should be endian independant, testing will tell...
36 * GlivCollectionFile ::=
37 * "GLiv <COLLECTION>"
38 * Version
39 * FileCount
40 * Entry*
41 * "</COLLECTION>"
44 * Version ::=
45 * "1" (Currently there is only one version)
48 * Entry ::=
49 * "<FILE>"
50 * PathLength
51 * Path
52 * MaybeThumb
53 * "</FILE>"
56 * MaybeThumb ::=
57 * "0" | Thumb
59 * Thumb ::=
60 * "1"
61 * HashKeyLength
62 * HashKey
63 * GdkColorspace
64 * AlphaChannel
65 * BitsPerSample
66 * ThumbWidth
67 * ThumbHeight
68 * RowStride
69 * PixelDataLength
70 * PixelData
74 * Path, and HashKey are '0' terminated strings.
75 * FileCount, PathLength, HashKeyLength, GdkColorspace, AlphaChannel,
76 * BitsPerSample, ThumbWidth, ThumbHeight, RowStride and PixelDataLength
77 * are '\0' terminated strings representing decimal numbers.
80 #include <stdio.h> /* FILE, fopen(), fread(), fwrite(), fclose() */
81 #include <string.h> /* strlen(), strrchr() */
82 #include <errno.h> /* errno */
83 #include <ctype.h> /* isdigit() */
84 #include <time.h> /* time_t, time() */
86 #include "gliv.h"
87 #include "collection.h"
88 #include "messages.h"
89 #include "tree.h"
90 #include "thumbnails.h"
91 #include "str_utils.h"
92 #include "files_list.h"
93 #include "next_image.h"
94 #include "gliv-image.h"
95 #include "formats.h"
96 #include "loading.h"
97 #include "decompression.h"
98 #include "callbacks.h"
99 #include "windows.h"
101 /* Max filename length in the progress window. */
102 #define MAX_DISPLAYED_NAME_LENGTH 30
104 extern GlivImage *current_image;
106 /* Let the user choose a filename using a file selection dialog. */
107 static const gchar *get_a_file(const gchar * title)
109 GtkFileSelection *dialog;
110 gint response;
111 const gchar *filename;
113 dialog = GTK_FILE_SELECTION(gtk_file_selection_new(title));
115 gtk_file_selection_set_select_multiple(dialog, TRUE);
116 response = run_modal_dialog(GTK_DIALOG(dialog));
118 if (response == GTK_RESPONSE_OK)
119 filename = gtk_file_selection_get_filename(dialog);
120 else
121 filename = NULL;
123 gtk_widget_destroy(GTK_WIDGET(dialog));
124 return filename;
127 /*** Progress dialog ***/
129 static GtkLabel *file_label;
130 static GtkLabel *ratio_label;
131 static GtkProgressBar *progress;
132 static GtkWindow *progress_window;
133 static GtkLabel *elapsed_label, *total_label, *remaining_label;
134 static time_t start_time;
136 static gboolean set_true(gboolean * ptr)
138 *ptr = TRUE;
139 return FALSE;
142 static GtkLabel *add_time_display(GtkTable * table, const gchar * text,
143 gint pos)
145 GtkLabel *name_label, *time_label;
147 name_label = GTK_LABEL(gtk_label_new(text));
148 gtk_widget_show(GTK_WIDGET(name_label));
150 time_label = GTK_LABEL(gtk_label_new(NULL));
151 gtk_widget_show(GTK_WIDGET(time_label));
153 gtk_table_attach_defaults(table, GTK_WIDGET(name_label),
154 0, 1, pos, pos + 1);
156 gtk_table_attach_defaults(table, GTK_WIDGET(time_label),
157 1, 2, pos, pos + 1);
159 return time_label;
162 static void create_progress_dialog(const gchar * filename, gboolean serialize,
163 gpointer data)
165 GtkButton *cancel;
166 GtkTable *table;
167 gchar *title;
169 file_label = GTK_LABEL(gtk_label_new(NULL));
170 gtk_widget_show(GTK_WIDGET(file_label));
172 ratio_label = GTK_LABEL(gtk_label_new(NULL));
173 gtk_widget_show(GTK_WIDGET(ratio_label));
176 progress = GTK_PROGRESS_BAR(gtk_progress_bar_new());
177 gtk_widget_show(GTK_WIDGET(progress));
179 cancel = GTK_BUTTON(gtk_button_new_from_stock(GTK_STOCK_CANCEL));
181 if (serialize)
182 g_signal_connect(cancel, "clicked",
183 G_CALLBACK(cancel_using_tree), data);
184 else
185 g_signal_connect_swapped(cancel, "clicked", G_CALLBACK(set_true), data);
187 gtk_widget_show(GTK_WIDGET(cancel));
189 table = GTK_TABLE(gtk_table_new(7, 2, TRUE));
190 gtk_table_attach_defaults(table, GTK_WIDGET(file_label), 0, 2, 0, 1);
191 gtk_table_attach_defaults(table, GTK_WIDGET(ratio_label), 0, 2, 1, 2);
192 gtk_table_attach_defaults(table, GTK_WIDGET(progress), 0, 2, 2, 3);
194 elapsed_label = add_time_display(table, _("Elapsed time"), 3);
195 remaining_label = add_time_display(table, _("Remaining time"), 4);
196 total_label = add_time_display(table, _("Total time"), 5);
198 gtk_table_attach_defaults(table, GTK_WIDGET(cancel), 1, 2, 6, 7);
200 gtk_widget_show(GTK_WIDGET(table));
202 progress_window = GTK_WINDOW(gtk_window_new(GTK_WINDOW_TOPLEVEL));
204 if (serialize)
205 g_signal_connect(progress_window, "delete-event",
206 G_CALLBACK(cancel_using_tree), NULL);
207 else
208 g_signal_connect_swapped(progress_window, "delete-event",
209 G_CALLBACK(set_true), data);
211 if (serialize) {
212 title = g_strdup_printf(_("Saving collection: %s"),
213 filename_to_utf8(filename));
214 gtk_window_set_title(progress_window, title);
215 g_free(title);
216 } else
217 gtk_window_set_title(progress_window, _("Loading GLiv collection"));
219 gtk_container_add(GTK_CONTAINER(progress_window), GTK_WIDGET(table));
220 gtk_widget_show(GTK_WIDGET(progress_window));
221 start_time = time(NULL);
224 static gchar *time2str(time_t t)
226 gint hours = t / 3600;
227 gint minutes = (t - hours * 3600) / 60;
228 gint seconds = t % 60;
230 return g_strdup_printf("%02d:%02d:%02d", hours, minutes, seconds);
233 static void update_times(time_t elapsed_time,
234 time_t remaining_time, time_t total_time)
236 gchar *text;
238 text = time2str(elapsed_time);
239 gtk_label_set_text(elapsed_label, text);
240 g_free(text);
242 text = time2str(remaining_time);
243 gtk_label_set_text(remaining_label, text);
244 g_free(text);
246 text = time2str(total_time);
247 gtk_label_set_text(total_label, text);
248 g_free(text);
251 static void update_progress(const gchar * filename, gint id, gint count)
253 const gchar *base;
254 static GTimeVal last = { 0, 0 };
255 time_t current_time, elapsed_time, total_time, remaining_time;
256 size_t len;
257 gchar *ratio_text;
259 if (filename == NULL) {
260 /* Last update. */
261 gchar *msg = _("Inserting files...");
263 last.tv_sec = last.tv_usec = 0;
264 gtk_label_set_text(file_label, msg);
266 } else {
267 GTimeVal now;
268 g_get_current_time(&now);
270 if (last.tv_sec != 0 || last.tv_usec != 0) {
271 /* Not the first time. */
272 glong usec_diff;
274 g_get_current_time(&now);
275 usec_diff = (now.tv_sec - last.tv_sec) * G_USEC_PER_SEC +
276 now.tv_usec - last.tv_usec;
278 if (usec_diff < G_USEC_PER_SEC / 10)
279 /* Lower the redrawing load. */
280 return;
283 last = now;
285 base = strrchr(filename, '/');
286 if (base == NULL)
287 base = filename;
288 else
289 base++;
291 len = strlen(base);
292 if (len > MAX_DISPLAYED_NAME_LENGTH) {
293 gchar *begin, *text;
294 const gchar *end;
296 begin = g_strndup(base, MAX_DISPLAYED_NAME_LENGTH / 2 - 3);
297 end = base + len - MAX_DISPLAYED_NAME_LENGTH / 2 + 3;
298 text = g_strconcat(begin, " ... ", end, NULL);
299 g_free(begin);
301 gtk_label_set_text(file_label, text);
302 g_free(text);
303 } else
304 gtk_label_set_text(file_label, base);
307 ratio_text = g_strdup_printf("%d / %d", id, count - 1);
308 gtk_label_set_text(ratio_label, ratio_text);
309 g_free(ratio_text);
311 if (count == 1)
312 gtk_progress_bar_set_fraction(progress, 1.0);
313 else
314 gtk_progress_bar_set_fraction(progress, (gdouble) id / (count - 1));
316 current_time = time(NULL);
317 elapsed_time = current_time - start_time;
318 if (id == 0)
319 total_time = 0;
320 else
321 total_time = elapsed_time * (count - 1) / id;
323 remaining_time = total_time - elapsed_time;
325 update_times(elapsed_time, remaining_time, total_time);
326 process_events();
329 static void destroy_progress_dialog(void)
331 if (progress_window != NULL) {
332 if (GTK_IS_WIDGET(progress_window))
333 gtk_widget_destroy(GTK_WIDGET(progress_window));
334 progress_window = NULL;
339 * In the serialization and deserialization,
340 * returning FALSE means aborting it.
343 /*** Serialization ***/
345 typedef struct {
346 FILE *file;
347 gint file_id;
348 gint count;
349 gchar *cwd; /* To avoid relative filenames. */
350 } serialization_data;
352 /* Attempts to put a string in the serialized file. */
353 #define WRITE(file, str) \
354 do { \
355 gchar * __str__ = (str); \
356 gint len = strlen(__str__) + 1; \
358 if (fwrite(__str__, 1, len, (file)) != len) \
359 return FALSE; \
360 } while (0)
362 /* Attempts to put a number (in string form) in the serialized file. */
363 #define WRITE_NB(file, nb) \
364 do { \
365 gchar *str = g_strdup_printf("%d%c", (nb), '\0'); \
366 gint len = strlen(str) + 1; \
368 if (fwrite(str, 1, len, (file)) != len) { \
369 g_free(str); \
370 return FALSE; \
373 g_free(str); \
374 } while (0)
376 static gboolean print_header(serialization_data * work)
378 WRITE(work->file, "GLiv <COLLECTION>");
379 WRITE(work->file, "1"); /* Version */
380 WRITE_NB(work->file, work->count); /* FileCount */
382 return TRUE;
385 static gboolean print_footer(FILE * file)
387 WRITE(file, "</COLLECTION>");
389 return TRUE;
392 /* Write the MaybeThumb part of an entry. */
393 static gboolean write_thumb(tree_item * item, FILE * file)
395 gint height, rowstride, len, bps;
396 GdkPixbuf *thumb;
398 if (fill_thumbnail(item) == FALSE) {
399 WRITE(file, "0");
400 return TRUE;
403 thumb = item->thumb;
404 height = gdk_pixbuf_get_height(thumb);
405 rowstride = gdk_pixbuf_get_rowstride(thumb);
406 len = pixels_size(thumb);
407 bps = gdk_pixbuf_get_bits_per_sample(thumb);
409 WRITE(file, "1");
410 WRITE_NB(file, strlen(item->thumb_key)); /* HashKeyLength */
411 WRITE(file, item->thumb_key); /* HashKey */
412 WRITE_NB(file, gdk_pixbuf_get_colorspace(thumb)); /* GdkColorspace */
413 WRITE_NB(file, gdk_pixbuf_get_has_alpha(thumb)); /* AlphaChannel */
414 WRITE_NB(file, bps); /* BitsPerSample */
415 WRITE_NB(file, gdk_pixbuf_get_width(thumb)); /* ThumbWidth */
416 WRITE_NB(file, height); /* ThumbHeight */
417 WRITE_NB(file, rowstride); /* RowStride */
418 WRITE_NB(file, len); /* PixelDataLength */
420 /* PixelData */
421 return fwrite(gdk_pixbuf_get_pixels(thumb), 1, len, file) == len;
424 static gboolean serialize_file(GNode * tree, serialization_data * work)
426 tree_item *item = tree->data;
427 FILE *file = work->file;
428 gchar *path;
430 update_progress(item->path, work->file_id, work->count);
432 WRITE(file, "<FILE>");
434 path = get_absolute_filename(item->path);
435 WRITE_NB(file, strlen(path)); /* PathLength */
436 WRITE(file, path); /* Path */
437 g_free(path);
439 if (write_thumb(item, file) == FALSE) /* MaybeThumb */
440 return FALSE;
442 WRITE(file, "</FILE>");
444 work->file_id++;
445 return TRUE;
448 /* The tree is reversed, so we traverse it in the reverse direction. */
449 static gboolean reverse_traverse_leafs(GNode * tree,
450 GNodeTraverseFunc func, gpointer data)
452 GNode *child;
454 if (G_NODE_IS_LEAF(tree))
455 return func(tree, data);
457 child = g_node_last_child(tree);
458 while (child != NULL) {
459 if (canceled_using_tree())
460 return FALSE;
462 if (reverse_traverse_leafs(child, func, data) == FALSE)
463 return FALSE;
465 child = child->prev;
468 return TRUE;
471 #define FILE_ERROR(filename) \
472 do { \
473 const gchar *message = g_strerror(errno); \
474 message = g_strconcat((filename), ": ", message, NULL); \
475 DIALOG_MSG(message); \
476 } while (0)
478 void serialize_collection(void)
480 FILE *file;
481 GNode *tree;
482 const gchar *filename;
483 serialization_data *work;
484 gboolean res;
486 tree = make_tree();
487 if (tree == NULL)
488 return;
490 filename = get_a_file(_("Choose a file to save the collection"));
491 if (filename == NULL) {
492 end_using_tree();
493 return;
496 file = fopen(filename, "w");
497 if (file == NULL) {
498 FILE_ERROR(filename);
499 end_using_tree();
500 return;
503 create_progress_dialog(filename, TRUE, NULL);
505 work = g_new(serialization_data, 1);
506 work->file = file;
507 work->file_id = 0;
508 work->count = tree_count_files(tree);
509 work->cwd = g_get_current_dir();
511 if (print_header(work) == FALSE)
512 goto error;
514 res = reverse_traverse_leafs(tree,
515 (GNodeTraverseFunc) serialize_file, work);
517 if (res == FALSE || work->file_id != work->count)
518 goto error;
520 if (print_footer(work->file) == FALSE)
521 goto error;
523 if (fclose(work->file)) {
524 perror(filename);
525 work->file = NULL;
526 goto error;
528 work->file = NULL;
530 goto ok;
532 error:
533 if (canceled_using_tree() == FALSE || remove(filename) < 0)
534 FILE_ERROR(filename);
537 if (work->file != NULL)
538 fclose(work->file);
540 g_free(work->cwd);
541 g_free(work);
542 end_using_tree();
543 destroy_progress_dialog();
546 /*** Deserialization ***/
548 /* Checks that file starts with magic. */
549 static gboolean check_magic(const gchar * magic, FILE * file)
551 gboolean res;
552 gint size = strlen(magic) + 1;
553 gchar *buffer = g_new(gchar, size);
555 if (fread(buffer, 1, size, file) != size) {
556 g_free(buffer);
557 return FALSE;
560 res = buffer[size - 1] == '\0' && g_str_equal(magic, buffer);
561 g_free(buffer);
563 return res;
566 /* Reads a number represented by a string. -1 => ERROR */
567 static gint read_number(FILE * file)
569 gint number = 0;
570 gint read_char = fgetc(file);
572 while (isdigit(read_char)) {
573 number = number * 10 + read_char - '0';
574 read_char = fgetc(file);
577 if (read_char == '\0')
578 return number;
580 return -1;
583 #define READ_THUMB_NB(file, var) \
584 do { \
585 var = read_number(file); \
586 if (var < 0) \
587 return FALSE; \
588 } while (0)
590 static gboolean read_pixbuf(FILE * file, GdkPixbuf ** thumb)
592 GdkColorspace colorspace;
593 gboolean has_alpha;
594 gint bits_per_sample, width, height, rowstride;
595 gint pixel_length;
596 guchar *pixel;
598 READ_THUMB_NB(file, colorspace);
599 READ_THUMB_NB(file, has_alpha);
600 if (has_alpha != FALSE && has_alpha != TRUE)
601 return FALSE;
603 READ_THUMB_NB(file, bits_per_sample);
604 READ_THUMB_NB(file, width);
605 READ_THUMB_NB(file, height);
606 READ_THUMB_NB(file, rowstride);
607 READ_THUMB_NB(file, pixel_length);
609 pixel = g_new(guchar, pixel_length);
610 if (fread(pixel, 1, pixel_length, file) != pixel_length) {
611 g_free(pixel);
612 return FALSE;
615 *thumb = gdk_pixbuf_new_from_data(pixel, colorspace, has_alpha,
616 bits_per_sample, width, height, rowstride,
617 (GdkPixbufDestroyNotify) g_free, NULL);
619 if (*thumb == NULL) {
620 g_free(pixel);
621 return FALSE;
624 return TRUE;
627 static gboolean read_thumb(FILE * file, gchar ** hash_key, GdkPixbuf ** thumb)
629 gint has_thumb, hash_key_length;
631 has_thumb = read_number(file);
632 if (has_thumb == 0)
633 return TRUE;
635 if (has_thumb != 1)
636 return FALSE;
638 hash_key_length = read_number(file);
639 if (hash_key_length <= 0)
640 return FALSE;
642 hash_key_length++;
643 *hash_key = g_new(gchar, hash_key_length);
645 if (fread(*hash_key, 1, hash_key_length, file) != hash_key_length ||
646 (*hash_key)[hash_key_length - 1] != '\0') {
648 g_free(*hash_key);
649 return FALSE;
652 return read_pixbuf(file, thumb);
655 static tree_item *read_entry(FILE * file)
657 tree_item *item;
658 gint len;
659 gchar *path, *hash_key = NULL;
660 GdkPixbuf *thumb = NULL;
662 if (check_magic("<FILE>", file) == FALSE)
663 return NULL;
665 len = read_number(file);
666 if (len <= 0)
667 return NULL;
669 len++;
670 path = g_new(gchar, len);
671 if (fread(path, 1, len, file) != len || path[len - 1] != '\0') {
672 g_free(path);
673 return NULL;
676 if (read_thumb(file, &hash_key, &thumb) == FALSE) {
677 g_free(path);
678 return NULL;
681 if (check_magic("</FILE>", file) == FALSE) {
682 g_free(path);
683 g_free(hash_key);
684 if (hash_key != NULL)
685 g_object_unref(thumb);
687 return NULL;
690 item = g_new(tree_item, 1);
691 item->name = NULL;
692 item->path = path;
693 item->thumb_key = hash_key;
694 item->thumb = thumb;
696 return item;
699 #define CHECK_MAGIC(file, str) \
700 do { \
701 gboolean res = check_magic((str), (file)); \
702 if (res == FALSE) \
703 goto error; \
704 } while (0)
706 gint load_dot_gliv_from_file(FILE * file, gboolean in_the_head)
708 gint count, i = 0;
709 tree_item **items = NULL;
710 gchar **filenames = NULL;
711 gboolean ok = TRUE, cancel = FALSE;
712 gint nb_inserted = -1;
714 CHECK_MAGIC(file, "GLiv <COLLECTION>");
715 CHECK_MAGIC(file, "1");
717 count = read_number(file);
718 if (count < 0)
719 goto error;
721 create_progress_dialog(NULL, FALSE, &cancel);
723 items = g_new(tree_item *, count);
724 for (i = 0; i < count; i++) {
725 tree_item *item;
727 item = read_entry(file);
728 if (item == NULL)
729 goto error;
731 items[i] = item;
732 update_progress(item->path, i, count);
734 if (cancel) {
735 i++;
736 nb_inserted = 0;
737 goto error;
741 CHECK_MAGIC(file, "</COLLECTION>");
743 update_progress(NULL, count - 1, count);
744 nb_inserted = 0;
746 if (in_the_head == FALSE)
747 filenames = g_new(gchar *, count);
749 for (i = 0; i < count; i++) {
750 if (items[i]->thumb_key != NULL)
751 collection_add_thumbnail(items[i]->thumb_key, items[i]->thumb);
753 if (in_the_head)
754 nb_inserted += add_file_to_list(items[i]->path);
755 else
756 filenames[i] = items[i]->path;
759 if (in_the_head == FALSE)
760 nb_inserted = insert_after_current(filenames, count,
761 FALSE, FALSE, FALSE, FALSE);
763 goto ok;
765 error:
766 ok = FALSE;
769 while (i) {
770 i--;
771 g_free(items[i]->path);
772 if (ok == FALSE && items[i]->thumb_key != NULL) {
773 g_free(items[i]->thumb_key);
774 g_object_unref(items[i]->thumb);
776 g_free(items[i]);
778 g_free(items);
779 g_free(filenames);
780 destroy_progress_dialog();
781 return nb_inserted;
784 gint load_dot_gliv(const gchar * filename, gboolean in_the_head)
786 FILE *file;
787 gint nb_inserted;
788 loader_t loader;
790 loader = get_loader(filename);
791 if (loader == LOADER_DECOMP_DOT_GLIV)
792 nb_inserted = load_compressed_collection(filename, in_the_head);
794 else {
795 file = fopen(filename, "r");
796 if (file == NULL) {
797 FILE_ERROR(filename);
798 return 0;
801 nb_inserted = load_dot_gliv_from_file(file, in_the_head);
802 fclose(file);
805 if (nb_inserted < 0) {
806 DIALOG_MSG(_("%s is not a GLiv collection"),
807 filename_to_utf8(filename));
808 nb_inserted = 0;
811 return nb_inserted;
814 void deserialize_collection(void)
816 const gchar *filename;
817 gint nb_inserted;
819 filename = get_a_file(_("Choose a collection to load"));
820 if (filename == NULL)
821 return;
823 nb_inserted = load_dot_gliv(filename, FALSE);
824 if (nb_inserted <= 0)
825 return;
827 open_next_image(nb_inserted == 1);