Show files processed ratio in the serialization progress dialog.
[gliv.git] / src / collection.c
blobb52487b8f01ab1485c5dfdbf2034c48c967bb3f1
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 "rendering.h"
100 #include "matrix.h"
102 /* Max filename length in the progress window. */
103 #define MAX_DISPLAYED_NAME_LENGTH 30
105 extern gliv_image *current_image;
107 /* Let the user choose a filename using a file selection dialog. */
108 static const gchar *get_a_file(const gchar * title)
110 GtkFileSelection *dialog;
111 gint response;
112 const gchar *filename;
114 dialog = GTK_FILE_SELECTION(gtk_file_selection_new(title));
116 gtk_file_selection_set_select_multiple(dialog, TRUE);
117 response = gtk_dialog_run(GTK_DIALOG(dialog));
119 if (response == GTK_RESPONSE_OK)
120 filename = gtk_file_selection_get_filename(dialog);
121 else
122 filename = NULL;
124 gtk_widget_destroy(GTK_WIDGET(dialog));
125 return filename;
128 /*** Progress dialog ***/
130 static GtkLabel *file_label;
131 static GtkLabel *ratio_label;
132 static GtkProgressBar *progress;
133 static GtkWindow *progress_window;
134 static GtkLabel *elapsed_label, *total_label, *remaining_label;
135 static time_t start_time;
137 static gboolean set_true(gboolean * ptr)
139 *ptr = TRUE;
140 return FALSE;
143 static GtkLabel *add_time_display(GtkTable * table, const gchar * text,
144 gint pos)
146 GtkLabel *name_label, *time_label;
148 name_label = GTK_LABEL(gtk_label_new(text));
149 gtk_widget_show(GTK_WIDGET(name_label));
151 time_label = GTK_LABEL(gtk_label_new(NULL));
152 gtk_widget_show(GTK_WIDGET(time_label));
154 gtk_table_attach_defaults(table, GTK_WIDGET(name_label),
155 0, 1, pos, pos + 1);
157 gtk_table_attach_defaults(table, GTK_WIDGET(time_label),
158 1, 2, pos, pos + 1);
160 return time_label;
163 static void create_progress_dialog(const gchar * filename, gboolean serialize,
164 gpointer data)
166 GtkButton *cancel;
167 GtkTable *table;
168 gchar *title;
170 file_label = GTK_LABEL(gtk_label_new(NULL));
171 gtk_widget_show(GTK_WIDGET(file_label));
173 ratio_label = GTK_LABEL(gtk_label_new(NULL));
174 gtk_widget_show(GTK_WIDGET(ratio_label));
177 progress = GTK_PROGRESS_BAR(gtk_progress_bar_new());
178 gtk_widget_show(GTK_WIDGET(progress));
180 cancel = GTK_BUTTON(gtk_button_new_from_stock(GTK_STOCK_CANCEL));
182 if (serialize)
183 g_signal_connect(cancel, "clicked",
184 G_CALLBACK(cancel_using_tree), data);
185 else
186 g_signal_connect_swapped(cancel, "clicked", G_CALLBACK(set_true), data);
188 gtk_widget_show(GTK_WIDGET(cancel));
190 table = GTK_TABLE(gtk_table_new(7, 2, TRUE));
191 gtk_table_attach_defaults(table, GTK_WIDGET(file_label), 0, 2, 0, 1);
192 gtk_table_attach_defaults(table, GTK_WIDGET(ratio_label), 0, 2, 1, 2);
193 gtk_table_attach_defaults(table, GTK_WIDGET(progress), 0, 2, 2, 3);
195 elapsed_label = add_time_display(table, _("Elapsed time"), 3);
196 remaining_label = add_time_display(table, _("Remaining time"), 4);
197 total_label = add_time_display(table, _("Total time"), 5);
199 gtk_table_attach_defaults(table, GTK_WIDGET(cancel), 1, 2, 6, 7);
201 gtk_widget_show(GTK_WIDGET(table));
203 progress_window = GTK_WINDOW(gtk_window_new(GTK_WINDOW_TOPLEVEL));
205 if (serialize)
206 g_signal_connect(progress_window, "delete-event",
207 G_CALLBACK(cancel_using_tree), NULL);
208 else
209 g_signal_connect_swapped(progress_window, "delete-event",
210 G_CALLBACK(set_true), data);
212 if (serialize) {
213 title = g_strdup_printf(_("Saving collection: %s"),
214 filename_to_utf8(filename));
215 gtk_window_set_title(progress_window, title);
216 g_free(title);
217 } else
218 gtk_window_set_title(progress_window, _("Loading GLiv collection"));
220 gtk_container_add(GTK_CONTAINER(progress_window), GTK_WIDGET(table));
221 gtk_widget_show(GTK_WIDGET(progress_window));
222 start_time = time(NULL);
225 static gchar *time2str(time_t t)
227 gint hours = t / 3600;
228 gint minutes = (t - hours * 3600) / 60;
229 gint seconds = t % 60;
231 return g_strdup_printf("%02d:%02d:%02d", hours, minutes, seconds);
234 static void update_times(time_t elapsed_time,
235 time_t remaining_time, time_t total_time)
237 gchar *text;
239 text = time2str(elapsed_time);
240 gtk_label_set_text(elapsed_label, text);
241 g_free(text);
243 text = time2str(remaining_time);
244 gtk_label_set_text(remaining_label, text);
245 g_free(text);
247 text = time2str(total_time);
248 gtk_label_set_text(total_label, text);
249 g_free(text);
252 static void update_progress(const gchar * filename, gint id, gint count)
254 const gchar *base;
255 static GTimeVal last = { 0, 0 };
256 time_t current_time, elapsed_time, total_time, remaining_time;
257 size_t len;
258 gchar *ratio_text;
260 if (filename == NULL) {
261 /* Last update. */
262 gchar *msg = _("Inserting files...");
264 last.tv_sec = last.tv_usec = 0;
265 gtk_label_set_text(file_label, msg);
267 } else {
268 GTimeVal now;
269 g_get_current_time(&now);
271 if (last.tv_sec != 0 || last.tv_usec != 0) {
272 /* Not the first time. */
273 glong usec_diff;
275 g_get_current_time(&now);
276 usec_diff = (now.tv_sec - last.tv_sec) * G_USEC_PER_SEC +
277 now.tv_usec - last.tv_usec;
279 if (usec_diff < G_USEC_PER_SEC / 10)
280 /* Lower the redrawing load. */
281 return;
284 last = now;
286 base = strrchr(filename, '/');
287 if (base == NULL)
288 base = filename;
289 else
290 base++;
292 len = strlen(base);
293 if (len > MAX_DISPLAYED_NAME_LENGTH) {
294 gchar *begin, *text;
295 const gchar *end;
297 begin = g_strndup(base, MAX_DISPLAYED_NAME_LENGTH / 2 - 3);
298 end = base + len - MAX_DISPLAYED_NAME_LENGTH / 2 + 3;
299 text = g_strconcat(begin, " ... ", end, NULL);
300 g_free(begin);
302 gtk_label_set_text(file_label, text);
303 g_free(text);
304 } else
305 gtk_label_set_text(file_label, base);
308 ratio_text = g_strdup_printf("%d / %d", id, count - 1);
309 gtk_label_set_text(ratio_label, ratio_text);
310 g_free(ratio_text);
312 gtk_progress_bar_set_fraction(progress, (gdouble) id / (count - 1));
314 current_time = time(NULL);
315 elapsed_time = current_time - start_time;
316 if (id == 0)
317 total_time = 0;
318 else
319 total_time = elapsed_time * (count - 1) / id;
321 remaining_time = total_time - elapsed_time;
323 update_times(elapsed_time, remaining_time, total_time);
324 process_events();
327 static void destroy_progress_dialog(void)
329 if (progress_window != NULL) {
330 if (GTK_IS_WIDGET(progress_window))
331 gtk_widget_destroy(GTK_WIDGET(progress_window));
332 progress_window = NULL;
337 * In the serialization and deserialization,
338 * returning FALSE means aborting it.
341 /*** Serialization ***/
343 typedef struct {
344 FILE *file;
345 gint file_id;
346 gint count;
347 gchar *cwd; /* To avoid relative filenames. */
348 } serialization_data;
350 /* Attempts to put a string in the serialized file. */
351 #define WRITE(file, str) \
352 do { \
353 gchar * __str__ = (str); \
354 gint len = strlen(__str__) + 1; \
356 if (fwrite(__str__, 1, len, (file)) != len) \
357 return FALSE; \
358 } while (0)
360 /* Attempts to put a number (in string form) in the serialized file. */
361 #define WRITE_NB(file, nb) \
362 do { \
363 gchar *str = g_strdup_printf("%d%c", (nb), '\0'); \
364 gint len = strlen(str) + 1; \
366 if (fwrite(str, 1, len, (file)) != len) { \
367 g_free(str); \
368 return FALSE; \
371 g_free(str); \
372 } while (0)
374 static gboolean print_header(serialization_data * work)
376 WRITE(work->file, "GLiv <COLLECTION>");
377 WRITE(work->file, "1"); /* Version */
378 WRITE_NB(work->file, work->count); /* FileCount */
380 return TRUE;
383 static gboolean print_footer(FILE * file)
385 WRITE(file, "</COLLECTION>");
387 return TRUE;
390 /* Write the MaybeThumb part of an entry. */
391 static gboolean write_thumb(tree_item * item, FILE * file)
393 gint height, rowstride, len;
394 GdkPixbuf *thumb;
396 if (fill_thumbnail(item) == FALSE) {
397 WRITE(file, "0");
398 return TRUE;
401 thumb = item->thumb;
402 height = gdk_pixbuf_get_height(thumb);
403 rowstride = gdk_pixbuf_get_rowstride(thumb);
404 len = height * rowstride;
406 WRITE(file, "1");
407 WRITE_NB(file, strlen(item->thumb_key)); /* HashKeyLength */
408 WRITE(file, item->thumb_key); /* HashKey */
409 WRITE_NB(file, gdk_pixbuf_get_colorspace(thumb)); /* GdkColorspace */
410 WRITE_NB(file, gdk_pixbuf_get_has_alpha(thumb)); /* AlphaChannel */
411 WRITE_NB(file, gdk_pixbuf_get_bits_per_sample(thumb)); /* BitsPerSample */
412 WRITE_NB(file, gdk_pixbuf_get_width(thumb)); /* ThumbWidth */
413 WRITE_NB(file, height); /* ThumbHeight */
414 WRITE_NB(file, rowstride); /* RowStride */
415 WRITE_NB(file, len); /* PixelDataLength */
417 /* PixelData */
418 return fwrite(gdk_pixbuf_get_pixels(thumb), 1, len, file) == len;
421 static gboolean serialize_file(GNode * tree, serialization_data * work)
423 tree_item *item = tree->data;
424 FILE *file = work->file;
425 gchar *path;
427 update_progress(item->path, work->file_id, work->count);
429 WRITE(file, "<FILE>");
431 path = get_absolute_filename(item->path);
432 WRITE_NB(file, strlen(path)); /* PathLength */
433 WRITE(file, path); /* Path */
434 g_free(path);
436 if (write_thumb(item, file) == FALSE) /* MaybeThumb */
437 return FALSE;
439 WRITE(file, "</FILE>");
441 work->file_id++;
442 return TRUE;
445 /* The tree is reversed, so we traverse it in the reverse direction. */
446 static gboolean reverse_traverse_leafs(GNode * tree,
447 GNodeTraverseFunc func, gpointer data)
449 GNode *child;
451 if (G_NODE_IS_LEAF(tree))
452 return func(tree, data);
454 child = g_node_last_child(tree);
455 while (child != NULL) {
456 if (canceled_using_tree())
457 return FALSE;
459 if (reverse_traverse_leafs(child, func, data) == FALSE)
460 return FALSE;
462 child = child->prev;
465 return TRUE;
468 #define FILE_ERROR(filename) \
469 do { \
470 const gchar *message = g_strerror(errno); \
471 message = g_strconcat((filename), ": ", message, NULL); \
472 DIALOG_MSG(message); \
473 } while (0)
475 void serialize_collection(void)
477 FILE *file;
478 GNode *tree;
479 const gchar *filename;
480 serialization_data *work;
481 gboolean res;
483 tree = make_tree();
484 if (tree == NULL)
485 return;
487 filename = get_a_file(_("Choose a file to save the collection"));
488 if (filename == NULL) {
489 end_using_tree();
490 return;
493 file = fopen(filename, "w");
494 if (file == NULL) {
495 FILE_ERROR(filename);
496 end_using_tree();
497 return;
500 create_progress_dialog(filename, TRUE, NULL);
502 work = g_new(serialization_data, 1);
503 work->file = file;
504 work->file_id = 0;
505 work->count = get_list_length();
506 work->cwd = g_get_current_dir();
508 if (print_header(work) == FALSE)
509 goto error;
511 res = reverse_traverse_leafs(tree,
512 (GNodeTraverseFunc) serialize_file, work);
514 if (res == FALSE || work->file_id != work->count)
515 goto error;
517 if (print_footer(work->file) == FALSE)
518 goto error;
520 if (fclose(work->file)) {
521 work->file = NULL;
522 goto error;
524 work->file = NULL;
526 goto ok;
528 error:
529 if (canceled_using_tree() == FALSE || remove(filename) < 0)
530 FILE_ERROR(filename);
533 if (work->file != NULL)
534 fclose(work->file);
536 g_free(work->cwd);
537 g_free(work);
538 end_using_tree();
539 destroy_progress_dialog();
542 /*** Deserialization ***/
544 /* Checks that file starts with magic. */
545 static gboolean check_magic(const gchar * magic, FILE * file)
547 gboolean res;
548 gint size = strlen(magic) + 1;
549 gchar *buffer = g_new(gchar, size);
551 if (fread(buffer, 1, size, file) != size) {
552 g_free(buffer);
553 return FALSE;
556 res = buffer[size - 1] == '\0' && g_str_equal(magic, buffer);
557 g_free(buffer);
559 return res;
562 /* Reads a number represented by a string. -1 => ERROR */
563 static gint read_number(FILE * file)
565 gint number = 0;
566 gint read_char = fgetc(file);
568 while (isdigit(read_char)) {
569 number = number * 10 + read_char - '0';
570 read_char = fgetc(file);
573 if (read_char == '\0')
574 return number;
576 return -1;
579 #define READ_THUMB_NB(file, var) \
580 do { \
581 var = read_number(file); \
582 if (var < 0) \
583 return FALSE; \
584 } while (0)
586 static gboolean read_pixbuf(FILE * file, GdkPixbuf ** thumb)
588 GdkColorspace colorspace;
589 gboolean has_alpha;
590 gint bits_per_sample, width, height, rowstride;
591 gint pixel_length;
592 gchar *pixel;
594 READ_THUMB_NB(file, colorspace);
595 READ_THUMB_NB(file, has_alpha);
596 if (has_alpha != FALSE && has_alpha != TRUE)
597 return FALSE;
599 READ_THUMB_NB(file, bits_per_sample);
600 READ_THUMB_NB(file, width);
601 READ_THUMB_NB(file, height);
602 READ_THUMB_NB(file, rowstride);
603 READ_THUMB_NB(file, pixel_length);
605 pixel = g_new(gchar, pixel_length);
606 if (fread(pixel, 1, pixel_length, file) != pixel_length) {
607 g_free(pixel);
608 return FALSE;
611 *thumb = gdk_pixbuf_new_from_data(pixel, colorspace, has_alpha,
612 bits_per_sample, width, height, rowstride,
613 (GdkPixbufDestroyNotify) g_free, NULL);
615 if (*thumb == NULL) {
616 g_free(pixel);
617 return FALSE;
620 return TRUE;
623 static gboolean read_thumb(FILE * file, gchar ** hash_key, GdkPixbuf ** thumb)
625 gint has_thumb, hash_key_length;
627 has_thumb = read_number(file);
628 if (has_thumb == 0)
629 return TRUE;
631 if (has_thumb != 1)
632 return FALSE;
634 hash_key_length = read_number(file);
635 if (hash_key_length <= 0)
636 return FALSE;
638 hash_key_length++;
639 *hash_key = g_new(gchar, hash_key_length);
641 if (fread(*hash_key, 1, hash_key_length, file) != hash_key_length ||
642 (*hash_key)[hash_key_length - 1] != '\0') {
644 g_free(*hash_key);
645 return FALSE;
648 return read_pixbuf(file, thumb);
651 static tree_item *read_entry(FILE * file)
653 tree_item *item;
654 gint len;
655 gchar *path, *hash_key = NULL;
656 GdkPixbuf *thumb = NULL;
658 if (check_magic("<FILE>", file) == FALSE)
659 return NULL;
661 len = read_number(file);
662 if (len <= 0)
663 return NULL;
665 len++;
666 path = g_new(gchar, len);
667 if (fread(path, 1, len, file) != len || path[len - 1] != '\0') {
668 g_free(path);
669 return NULL;
672 if (read_thumb(file, &hash_key, &thumb) == FALSE) {
673 g_free(path);
674 return NULL;
677 if (check_magic("</FILE>", file) == FALSE) {
678 g_free(path);
679 g_free(hash_key);
680 if (hash_key != NULL)
681 g_object_unref(thumb);
683 return NULL;
686 item = g_new(tree_item, 1);
687 item->name = NULL;
688 item->path = path;
689 item->thumb_key = hash_key;
690 item->thumb = thumb;
692 return item;
695 #define CHECK_MAGIC(file, str) \
696 do { \
697 gboolean res = check_magic((str), (file)); \
698 if (res == FALSE) \
699 goto error; \
700 } while (0)
702 gint load_dot_gliv_from_file(FILE * file, gboolean reverse)
704 gint count, i = 0;
705 tree_item **items = NULL;
706 gchar **filenames = NULL;
707 gboolean ok = TRUE, cancel = FALSE;
708 gint nb_inserted = -1;
710 CHECK_MAGIC(file, "GLiv <COLLECTION>");
711 CHECK_MAGIC(file, "1");
713 count = read_number(file);
714 if (count < 0)
715 goto error;
717 create_progress_dialog(NULL, FALSE, &cancel);
719 items = g_new(tree_item *, count);
720 for (i = 0; i < count; i++) {
721 tree_item *item;
723 item = read_entry(file);
724 if (item == NULL)
725 goto error;
727 items[i] = item;
728 update_progress(item->path, i, count);
730 if (cancel) {
731 i++;
732 nb_inserted = 0;
733 goto error;
737 CHECK_MAGIC(file, "</COLLECTION>");
739 update_progress(NULL, count - 1, count);
740 filenames = g_new(gchar *, count);
741 for (i = 0; i < count; i++) {
742 if (reverse)
743 filenames[i] = items[count - 1 - i]->path;
744 else
745 filenames[i] = items[i]->path;
747 if (items[i]->thumb_key != NULL)
748 collection_add_thumbnail(items[i]->thumb_key, items[i]->thumb);
751 nb_inserted = insert_after_current(filenames, count,
752 FALSE, FALSE, FALSE, FALSE);
754 goto ok;
756 error:
757 ok = FALSE;
760 while (i) {
761 i--;
762 g_free(items[i]->path);
763 if (ok == FALSE) {
764 g_free(items[i]->thumb_key);
765 g_object_unref(items[i]->thumb);
767 g_free(items[i]);
769 g_free(items);
770 g_free(filenames);
771 destroy_progress_dialog();
772 return nb_inserted;
775 gint load_dot_gliv(const gchar * filename, gboolean reverse)
777 FILE *file;
778 gint nb_inserted;
779 loader_t loader;
781 loader = get_loader(filename);
782 if (loader == LOADER_DECOMP_DOT_GLIV)
783 nb_inserted = load_compressed_collection(filename, reverse);
785 else {
786 file = fopen(filename, "r");
787 if (file == NULL) {
788 FILE_ERROR(filename);
789 return 0;
792 nb_inserted = load_dot_gliv_from_file(file, reverse);
793 fclose(file);
796 if (nb_inserted < 0) {
797 DIALOG_MSG(_("%s is not a GLiv collection"),
798 filename_to_utf8(filename));
799 nb_inserted = 0;
802 return nb_inserted;
805 void deserialize_collection(void)
807 const gchar *filename;
809 filename = get_a_file(_("Choose a collection to load"));
810 if (filename == NULL)
811 return;
813 if (load_dot_gliv(filename, FALSE) == 0)
814 return;
816 if (current_image == NULL)
817 load_first_image();
818 else {
819 destroy_next_image();
820 fill_ident(current_image);
821 refresh(REFRESH_STATUS);