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@gmail.com>
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 ::=
45 * "1" (Currently there is only one version)
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 <unistd.h> /* isatty() */
81 #include <sys/types.h> /* size_t, off_t */
82 #include <sys/mman.h> /* mmap(), PROT_READ, MAP_PRIVATE, ... */
83 #include <stdio.h> /* FILE, fopen(), fread(), fwrite(), ... */
84 #include <string.h> /* strlen(), strrchr() */
85 #include <errno.h> /* errno */
86 #include <ctype.h> /* isdigit() */
87 #include <time.h> /* time_t, time() */
90 #include "collection.h"
91 #include "math_floats.h" /* log10f() */
92 #include "large_files.h"
95 #include "thumbnails.h"
96 #include "str_utils.h"
97 #include "files_list.h"
98 #include "next_image.h"
99 #include "gliv-image.h"
102 #include "decompression.h"
103 #include "callbacks.h"
107 /* Max filename length in the progress window. */
108 #define MAX_DISPLAYED_NAME_LENGTH 30
110 extern rt_struct
*rt
;
111 extern options_struct
*options
;
112 extern GlivImage
*current_image
;
114 static gchar
*get_filename(const gchar
* label
, gboolean save
)
116 GtkFileChooserAction action
;
117 const gchar
*stock_button
;
118 GtkFileChooser
*chooser
;
120 gchar
*filename
= NULL
;
123 action
= GTK_FILE_CHOOSER_ACTION_SAVE
;
124 stock_button
= GTK_STOCK_SAVE
;
126 action
= GTK_FILE_CHOOSER_ACTION_OPEN
;
127 stock_button
= GTK_STOCK_OPEN
;
130 chooser
= GTK_FILE_CHOOSER(gtk_file_chooser_dialog_new(label
,
139 response
= run_modal_dialog(GTK_DIALOG(chooser
));
141 if (response
== GTK_RESPONSE_ACCEPT
)
142 filename
= gtk_file_chooser_get_filename(chooser
);
144 gtk_widget_destroy(GTK_WIDGET(chooser
));
148 static gchar
*get_collection_destination(void)
152 filename
= get_filename(_("Choose a file to save the collection"), TRUE
);
153 if (filename
!= NULL
&& g_file_test(filename
, G_FILE_TEST_EXISTS
)) {
154 GtkMessageDialog
*overwrite
=
155 GTK_MESSAGE_DIALOG(gtk_message_dialog_new(NULL
,
157 GTK_MESSAGE_QUESTION
,
159 _("Overwrite \"%s\" ?"),
162 gint response
= run_modal_dialog(GTK_DIALOG(overwrite
));
163 if (response
!= GTK_RESPONSE_YES
) {
168 gtk_widget_destroy(GTK_WIDGET(overwrite
));
174 /*** Progress dialog ***/
176 static GtkLabel
*file_label
;
177 static GtkLabel
*ratio_label
;
178 static GtkProgressBar
*progress
;
179 static GtkWindow
*progress_window
;
180 static GtkLabel
*elapsed_label
, *total_label
, *remaining_label
;
181 static time_t start_time
;
183 static gboolean
set_true(gboolean
* ptr
)
189 static GtkLabel
*add_time_display(GtkTable
* table
, const gchar
* text
,
192 GtkLabel
*name_label
, *time_label
;
194 name_label
= GTK_LABEL(gtk_label_new(text
));
195 gtk_widget_show(GTK_WIDGET(name_label
));
197 time_label
= GTK_LABEL(gtk_label_new(NULL
));
198 gtk_widget_show(GTK_WIDGET(time_label
));
200 gtk_table_attach_defaults(table
, GTK_WIDGET(name_label
),
203 gtk_table_attach_defaults(table
, GTK_WIDGET(time_label
),
209 static void create_progress_dialog(const gchar
* filename
, gpointer cancel_data
,
216 file_label
= GTK_LABEL(gtk_label_new(NULL
));
217 gtk_label_set_ellipsize(file_label
, PANGO_ELLIPSIZE_MIDDLE
);
218 gtk_widget_show(GTK_WIDGET(file_label
));
220 ratio_label
= GTK_LABEL(gtk_label_new(NULL
));
221 gtk_widget_show(GTK_WIDGET(ratio_label
));
224 progress
= GTK_PROGRESS_BAR(gtk_progress_bar_new());
225 gtk_widget_show(GTK_WIDGET(progress
));
227 cancel
= GTK_BUTTON(gtk_button_new_from_stock(GTK_STOCK_CANCEL
));
228 g_signal_connect_swapped(cancel
, "clicked", G_CALLBACK(set_true
),
231 gtk_widget_show(GTK_WIDGET(cancel
));
233 table
= GTK_TABLE(gtk_table_new(7, 2, TRUE
));
234 gtk_table_attach_defaults(table
, GTK_WIDGET(file_label
), 0, 2, 0, 1);
235 gtk_table_attach_defaults(table
, GTK_WIDGET(ratio_label
), 0, 2, 1, 2);
236 gtk_table_attach_defaults(table
, GTK_WIDGET(progress
), 0, 2, 2, 3);
238 elapsed_label
= add_time_display(table
, _("Elapsed time"), 3);
239 remaining_label
= add_time_display(table
, _("Remaining time"), 4);
240 total_label
= add_time_display(table
, _("Total time"), 5);
242 gtk_table_attach_defaults(table
, GTK_WIDGET(cancel
), 1, 2, 6, 7);
244 gtk_widget_show(GTK_WIDGET(table
));
247 title
= g_strdup_printf(_("Saving collection: %s"),
248 filename_to_utf8(filename
));
250 title
= g_strdup_printf(_("Loading collection: %s"),
251 filename_to_utf8(filename
));
253 progress_window
= new_window(title
);
256 g_signal_connect_swapped(progress_window
, "delete-event",
257 G_CALLBACK(set_true
), cancel_data
);
259 gtk_container_add(GTK_CONTAINER(progress_window
), GTK_WIDGET(table
));
260 gtk_widget_show(GTK_WIDGET(progress_window
));
261 start_time
= time(NULL
);
264 static gchar
*time2str(time_t t
)
266 gint hours
= t
/ 3600;
267 gint minutes
= (t
- hours
* 3600) / 60;
268 gint seconds
= t
% 60;
270 return g_strdup_printf("%02d:%02d:%02d", hours
, minutes
, seconds
);
273 static void update_times(time_t elapsed_time
,
274 time_t remaining_time
, time_t total_time
)
278 text
= time2str(elapsed_time
);
279 gtk_label_set_text(elapsed_label
, text
);
282 text
= time2str(remaining_time
);
283 gtk_label_set_text(remaining_label
, text
);
286 text
= time2str(total_time
);
287 gtk_label_set_text(total_label
, text
);
291 static gboolean
need_refresh_progress(gboolean last_time
)
293 static GTimeVal last
= { 0, 0 };
303 if (last
.tv_sec
== 0 && last
.tv_usec
== 0) {
304 g_get_current_time(&last
);
308 g_get_current_time(&now
);
309 usec_diff
= (now
.tv_sec
- last
.tv_sec
) * G_USEC_PER_SEC
+
310 now
.tv_usec
- last
.tv_usec
;
312 if (usec_diff
< G_USEC_PER_SEC
/ 10)
313 /* Lower the redrawing load. */
320 static FILE *get_terminal_output(void)
323 static gboolean print_ok
= FALSE
;
326 if (isatty(fileno(stdout
)))
329 else if (isatty(fileno(stderr
)))
341 static void update_progress_nogui(gint id
, gint count
)
343 FILE *print
= get_terminal_output();
345 if (print
!= NULL
&& need_refresh_progress(id
== count
- 1)) {
346 gint percent
= id
* 100 / count
;
348 fprintf(print
, " %d/%d: %d%%\r", id
+ 1, count
, percent
);
353 static void update_progress(const gchar
* filename
, gint id
, gint count
)
356 time_t current_time
, elapsed_time
, total_time
, remaining_time
;
359 if (progress_window
== NULL
) {
360 update_progress_nogui(id
, count
);
364 if (filename
== NULL
) {
366 gchar
*msg
= _("Inserting files...");
368 need_refresh_progress(TRUE
);
369 gtk_label_set_text(file_label
, msg
);
372 if (need_refresh_progress(FALSE
) == FALSE
)
375 base
= strrchr(filename
, '/');
381 gtk_label_set_text(file_label
, base
);
384 ratio_text
= g_strdup_printf("%d / %d", id
, count
- 1);
385 gtk_label_set_text(ratio_label
, ratio_text
);
389 gtk_progress_bar_set_fraction(progress
, 1.0);
391 gtk_progress_bar_set_fraction(progress
, (gdouble
) id
/ (count
- 1));
393 current_time
= time(NULL
);
394 elapsed_time
= current_time
- start_time
;
398 total_time
= elapsed_time
* (count
- 1) / id
;
400 remaining_time
= total_time
- elapsed_time
;
402 update_times(elapsed_time
, remaining_time
, total_time
);
406 static void destroy_progress_dialog(void)
408 if (progress_window
!= NULL
) {
409 if (GTK_IS_WIDGET(progress_window
))
410 gtk_widget_destroy(GTK_WIDGET(progress_window
));
411 progress_window
= NULL
;
416 * In the serialization and deserialization,
417 * returning FALSE means aborting it.
420 /*** Serialization ***/
426 gchar
*cwd
; /* To avoid relative filenames. */
427 } serialization_data
;
429 /* Attempts to put a string in the serialized file. */
430 #define WRITE(file, str) \
432 gchar * __str__ = (str); \
433 gint len = strlen(__str__) + 1; \
435 if (fwrite(__str__, 1, len, (file)) != len) \
439 /* Attempts to put a number (in string form) in the serialized file. */
440 #define WRITE_NB(file, nb) \
442 gchar *str = g_strdup_printf("%d%c", (nb), '\0'); \
443 gint len = strlen(str) + 1; \
445 if (fwrite(str, 1, len, (file)) != len) { \
453 static gboolean
print_header(serialization_data
* work
)
455 WRITE(work
->file
, "GLiv <COLLECTION>");
456 WRITE(work
->file
, "1"); /* Version */
457 WRITE_NB(work
->file
, work
->count
); /* FileCount */
462 static gboolean
print_footer(FILE * file
)
464 WRITE(file
, "</COLLECTION>");
470 * We will write the value number, then a '\0' and then the image
471 * data. The data should be aligned if we read it via mmap, as
472 * GdkPixbuf uses a rowstride value so that every line beginning are
473 * on a 32 bit boundary.
474 * The alignment is made by padding some '0'.
476 static gboolean
align_next(FILE * file
, gint value
)
478 gint length
, pos
, align
;
480 /* For the (extremly ...) rare case value == 0 */
483 length
= (int) log10f(value
) + 2;
484 pos
= (int) ftell(file
) + length
; /* We don't need 64 bit precision */
485 align
= (4 - pos
% 4) % 4; /* We align on 32 bit boundaries */
488 if (putc('0', file
) < 0)
497 /* Write the MaybeThumb part of an entry. */
498 static gboolean
write_thumb(const gchar
* filename
, FILE * file
)
500 gint height
, rowstride
, len
, bps
;
504 thumb
= get_thumbnail(filename
, &thumb_key
);
510 height
= gdk_pixbuf_get_height(thumb
);
511 rowstride
= gdk_pixbuf_get_rowstride(thumb
);
512 len
= pixels_size(thumb
);
513 bps
= gdk_pixbuf_get_bits_per_sample(thumb
);
516 WRITE_NB(file
, (int) strlen(thumb_key
)); /* HashKeyLength */
517 WRITE(file
, thumb_key
); /* HashKey */
518 WRITE_NB(file
, gdk_pixbuf_get_colorspace(thumb
)); /* GdkColorspace */
519 WRITE_NB(file
, gdk_pixbuf_get_has_alpha(thumb
)); /* AlphaChannel */
520 WRITE_NB(file
, bps
); /* BitsPerSample */
521 WRITE_NB(file
, gdk_pixbuf_get_width(thumb
)); /* ThumbWidth */
522 WRITE_NB(file
, height
); /* ThumbHeight */
523 WRITE_NB(file
, rowstride
); /* RowStride */
525 if (align_next(file
, len
) == FALSE
)
528 WRITE_NB(file
, len
); /* PixelDataLength */
531 return fwrite(gdk_pixbuf_get_pixels(thumb
), 1, len
, file
) == len
;
534 static gboolean
serialize_file(const gchar
* filename
,
535 serialization_data
* work
)
537 FILE *file
= work
->file
;
540 update_progress(filename
, work
->file_id
, work
->count
);
542 WRITE(file
, "<FILE>");
544 path
= get_absolute_filename(filename
);
545 WRITE_NB(file
, (int) strlen(path
)); /* PathLength */
546 WRITE(file
, path
); /* Path */
549 if (write_thumb(filename
, file
) == FALSE
) /* MaybeThumb */
552 WRITE(file
, "</FILE>");
558 static void file_error(const gchar
* filename
)
561 gchar
*message
= g_strconcat(filename
, ": ", g_strerror(errno
), NULL
);
562 DIALOG_MSG("%s", message
);
567 gint
save_collection(FILE * file
, gboolean
* cancel
)
570 serialization_data
*work
;
573 list
= get_list_head();
574 work
= g_new(serialization_data
, 1);
577 work
->count
= get_list_length();
578 work
->cwd
= g_get_current_dir();
580 if (print_header(work
) == FALSE
)
583 while (*cancel
== FALSE
&& list
!= NULL
) {
584 gboolean file_ok
= serialize_file(list
->data
, work
);
585 if (file_ok
== FALSE
)
591 if (*cancel
|| print_footer(work
->file
) == FALSE
)
607 gint
serialize_collection_nogui(const gchar
* filename
)
612 gboolean cancel
= FALSE
;
614 if (get_list_head() == NULL
) {
615 g_printerr(_("No images to put in a collection\n"));
619 if (filename
== NULL
) {
621 if (isatty(fileno(file
))) {
622 g_printerr(_("GLiv won't write a collection to a terminal\n"));
626 /* We remove the file, since we may have mmapped it */
629 file
= fopen(filename
, "w");
636 tty
= get_terminal_output();
638 fprintf(tty
, _("Saving collection: %s"), filename_to_utf8(filename
));
642 res
= save_collection(file
, &cancel
);
645 perror(filename
== NULL
? _("Standard output") : filename
);
647 if (filename
!= NULL
&& fclose(file
) < 0 && res
== 0)
653 gboolean
serialize_collection_gui(void)
658 gboolean cancel
= FALSE
;
660 if (get_list_head() == NULL
)
663 filename
= get_collection_destination();
664 if (filename
== NULL
)
667 /* We remove the file, since we may have mmapped it */
670 file
= fopen(filename
, "w");
672 file_error(filename
);
676 create_progress_dialog(filename
, &cancel
, TRUE
);
678 res
= save_collection(file
, &cancel
);
681 file_error(filename
);
685 } else if (fclose(file
) < 0)
686 file_error(filename
);
688 destroy_progress_dialog();
694 /*** Deserialization ***/
708 static void free_buffer(struct coll_src
*source
, gchar
* buffer
)
714 static gchar
*read_buffer(struct coll_src
*source
, size_t length
,
719 if (source
->is_file
) {
720 data
= g_new(gchar
, length
);
721 if (fread(data
, 1, length
, source
->backend
.file
) != length
) {
726 if (source
->backend
.mem
.ptr
+ length
> source
->backend
.mem
.end
)
729 data
= source
->backend
.mem
.ptr
;
730 source
->backend
.mem
.ptr
+= length
;
733 if (is_string
&& data
[length
- 1] != '\0') {
734 free_buffer(source
, data
);
741 static gint
read_char(struct coll_src
*source
)
744 return fgetc(source
->backend
.file
);
746 if (source
->backend
.mem
.ptr
>= source
->backend
.mem
.end
)
749 return *(source
->backend
.mem
.ptr
++);
752 static GdkPixbufDestroyNotify
destroy_func(struct coll_src
*source
)
754 return (GdkPixbufDestroyNotify
) (source
->is_file
? g_free
: NULL
);
757 /* Checks that file starts with magic. */
758 static gboolean
check_magic(struct coll_src
*source
, const gchar
* magic
)
761 gint size
= strlen(magic
) + 1;
762 gchar
*buffer
= read_buffer(source
, size
, TRUE
);
767 res
= g_str_equal(magic
, buffer
);
768 free_buffer(source
, buffer
);
775 /* Reads a number represented by a string. -1 => ERROR */
776 static gint
read_number(struct coll_src
*source
)
779 gint next_char
= read_char(source
);
781 while (isdigit(next_char
)) {
782 number
= number
* 10 + next_char
- '0';
783 next_char
= read_char(source
);
786 if (next_char
== '\0')
792 #define READ_THUMB_NB(source, var) \
794 var = read_number(source); \
799 static gboolean
read_pixbuf(struct coll_src
*source
, GdkPixbuf
** thumb
)
801 GdkColorspace colorspace
;
803 gint bits_per_sample
, width
, height
, rowstride
;
807 READ_THUMB_NB(source
, colorspace
);
808 READ_THUMB_NB(source
, has_alpha
);
809 if (has_alpha
!= FALSE
&& has_alpha
!= TRUE
)
812 READ_THUMB_NB(source
, bits_per_sample
);
813 READ_THUMB_NB(source
, width
);
814 READ_THUMB_NB(source
, height
);
815 READ_THUMB_NB(source
, rowstride
);
816 READ_THUMB_NB(source
, pixel_length
);
818 pixel
= read_buffer(source
, pixel_length
, FALSE
);
822 *thumb
= gdk_pixbuf_new_from_data(pixel
, colorspace
, has_alpha
,
823 bits_per_sample
, width
, height
, rowstride
,
824 destroy_func(source
), NULL
);
826 if (*thumb
== NULL
) {
827 free_buffer(source
, pixel
);
834 static gboolean
read_thumb(struct coll_src
*source
, gchar
** hash_key
,
837 gint has_thumb
, hash_key_length
;
839 has_thumb
= read_number(source
);
846 hash_key_length
= read_number(source
);
847 if (hash_key_length
<= 0)
851 *hash_key
= read_buffer(source
, hash_key_length
, TRUE
);
852 if (*hash_key
== NULL
)
855 return read_pixbuf(source
, thumb
);
858 static tree_item
*read_entry(struct coll_src
*source
)
862 gchar
*path
, *hash_key
= NULL
;
863 GdkPixbuf
*thumb
= NULL
;
865 if (check_magic(source
, "<FILE>") == FALSE
)
868 len
= read_number(source
);
873 path
= read_buffer(source
, len
, TRUE
);
877 if (read_thumb(source
, &hash_key
, &thumb
) == FALSE
) {
878 free_buffer(source
, path
);
882 if (check_magic(source
, "</FILE>") == FALSE
) {
883 free_buffer(source
, path
);
884 free_buffer(source
, hash_key
);
885 if (hash_key
!= NULL
)
886 g_object_unref(thumb
);
891 item
= g_new(tree_item
, 1);
894 item
->thumb_key
= hash_key
;
900 #define CHECK_MAGIC(source, str) \
902 gboolean res = check_magic(source, str); \
907 static struct coll_src
*build_source(FILE * file
)
910 struct coll_src
*source
= g_new(struct coll_src
, 1);
912 if (fseeko(file
, 0, SEEK_END
) < 0)
915 length
= ftello(file
);
921 source
->backend
.mem
.base
=
922 mmap(NULL
, length
, PROT_READ
, MAP_PRIVATE
, fileno(file
), 0);
923 if (source
->backend
.mem
.base
== MAP_FAILED
) {
928 source
->is_file
= FALSE
;
929 source
->backend
.mem
.ptr
= source
->backend
.mem
.base
;
930 source
->backend
.mem
.end
= source
->backend
.mem
.base
+ length
;
934 fseeko(file
, 0, SEEK_SET
);
936 source
->is_file
= TRUE
;
937 source
->backend
.file
= file
;
943 static void destroy_source(struct coll_src
*source
, gboolean ok
)
945 if (ok
== FALSE
&& source
->is_file
== FALSE
)
946 if (munmap(source
->backend
.mem
.base
,
947 source
->backend
.mem
.end
- source
->backend
.mem
.base
) < 0)
951 gint
load_dot_gliv_from_file(const gchar
* filename
, FILE * file
)
954 tree_item
**items
= NULL
;
955 gchar
**filenames
= NULL
;
956 gboolean ok
= TRUE
, cancel
= FALSE
;
957 gint nb_inserted
= -1;
958 struct coll_src
*source
;
959 gboolean saved_force_load
;
961 source
= build_source(file
);
963 CHECK_MAGIC(source
, "GLiv <COLLECTION>");
964 CHECK_MAGIC(source
, "1");
966 count
= read_number(source
);
971 create_progress_dialog(filename
, &cancel
, FALSE
);
973 items
= g_new(tree_item
*, count
);
974 for (i
= 0; i
< count
; i
++) {
977 item
= read_entry(source
);
982 update_progress(item
->path
, i
, count
);
991 CHECK_MAGIC(source
, "</COLLECTION>");
993 update_progress(NULL
, count
- 1, count
);
996 filenames
= g_new(gchar
*, count
);
998 for (i
= 0; i
< count
; i
++) {
999 if (items
[i
]->thumb_key
!= NULL
)
1000 collection_add_thumbnail(items
[i
]->thumb_key
, items
[i
]->thumb
);
1002 filenames
[i
] = items
[i
]->path
;
1005 /* We don't want to stat() every file in the collection */
1006 saved_force_load
= options
->force
;
1007 options
->force
= TRUE
;
1009 nb_inserted
= insert_after_current(filenames
, count
, TRUE
, FALSE
);
1011 options
->force
= saved_force_load
;
1022 free_buffer(source
, items
[i
]->path
);
1023 if (ok
== FALSE
&& items
[i
]->thumb_key
!= NULL
) {
1024 free_buffer(source
, items
[i
]->thumb_key
);
1025 g_object_unref(items
[i
]->thumb
);
1033 destroy_progress_dialog();
1034 destroy_source(source
, ok
);
1038 gint
load_dot_gliv(const gchar
* filename
)
1045 tty
= get_terminal_output();
1047 fprintf(tty
, _("Loading collection: %s"), filename_to_utf8(filename
));
1051 loader
= get_loader(filename
);
1052 if (loader
== LOADER_DECOMP_DOT_GLIV
)
1053 nb_inserted
= load_compressed_collection(filename
);
1056 file
= fopen(filename
, "r");
1058 file_error(filename
);
1062 nb_inserted
= load_dot_gliv_from_file(filename
, file
);
1066 if (nb_inserted
< 0) {
1068 DIALOG_MSG(_("%s is not a GLiv collection"),
1069 filename_to_utf8(filename
));
1076 gboolean
deserialize_collection(void)
1081 filename
= get_filename(_("Choose a collection to load"), FALSE
);
1082 if (filename
== NULL
)
1085 nb_inserted
= load_dot_gliv(filename
);
1087 if (nb_inserted
<= 0)
1090 new_images(nb_inserted
);