diff --git a/README.md b/README.md index 41a67693f8d296780fe652800612accb5a311e9e..e0895a0001873b133cc3812616fb137fedb32d5f 100644 --- a/README.md +++ b/README.md @@ -67,6 +67,7 @@ You need the following to compile GQPE: * `champlain-0.12` * `clutter-gtk-1.0` * `gexiv2` +* `gdk-pixbuf-2.0` * `gtk+-3.18` * `gee-0.8` diff --git a/lib/photograph.vala b/lib/photograph.vala index da652edf8242dbe999f509c7a6f2fb6f78769535..53cb5247b3c4160437705a4b92233daadc6e46e7 100644 --- a/lib/photograph.vala +++ b/lib/photograph.vala @@ -102,6 +102,26 @@ namespace GQPE { */ public string path { owned get { return file.get_path(); } } + /** + * The image width. + */ + public int width { get; private set; } + + /** + * The image height. + */ + public int height { get; private set; } + + /** + * The image file size. + */ + public int size { get; private set; } + + /** + * The image file date and time. + */ + public GLib.DateTime file_datetime { get; private set; } + /** * Wether the photograph has geolocation. */ @@ -120,6 +140,7 @@ namespace GQPE { metadata = new GExiv2.Metadata(); metadata.open_path(file.get_path()); get_metadata(); + get_filedata(); this.notify.connect ((s, p) => modified = true); } @@ -473,5 +494,19 @@ namespace GQPE { d[i] = decimal_to_double(s[i]); return d; } + + /* Gets the file data. */ + private void get_filedata() throws GLib.Error { + var pb = new Gdk.Pixbuf.from_file(path); + width = pb.width; + height = pb.height; + Posix.Stat buf; + if (Posix.stat(path, out buf) != 0) + throw new GLib.Error(GLib.Quark.from_string("gqpe"), 0, + _("Cannot get file status: %s"), path); + size = (int)buf.st_size; + long t = (long)buf.st_mtime; + file_datetime = new GLib.DateTime.from_unix_local(t); + } } } diff --git a/lib/util.vala b/lib/util.vala index 268f7e91577544afa25f07ec5f6f4e8c12c8335b..613dfe24d0e074ac66ce2b66af3e61426e3e5901 100644 --- a/lib/util.vala +++ b/lib/util.vala @@ -22,6 +22,12 @@ namespace GQPE { + public enum ProgressState { + INIT, + ADVANCE, + END; + } + /** * Class for utility functions. */ @@ -37,6 +43,9 @@ namespace GQPE { /* The don't colorize environment variable. */ private const string GQPE_DONT_COLORIZE = "GQPE_DONT_COLORIZE"; + public delegate void ProgressMessage(ProgressState state, + int number); + /** * Returns a colorized message. * @param message the message. @@ -74,7 +83,7 @@ namespace GQPE { GLib.FileQueryInfoFlags.NONE); return info.get_modification_date_time(); } catch (GLib.Error e) { - GLib.warning("There was an error reading from ‘%s’.\n", filename); + GLib.warning(_("Error reading from ‘%s’.\n"), filename); } return new GLib.DateTime.now_local(); } @@ -92,7 +101,7 @@ namespace GQPE { info.set_modification_date_time(time); file.set_attributes_from_info(info, GLib.FileQueryInfoFlags.NONE); } catch (GLib.Error e) { - GLib.warning("There was an error writing to ‘%s’.\n", filename); + GLib.warning(_("Error writing to ‘%s’.\n"), filename); } } @@ -123,7 +132,7 @@ namespace GQPE { t = regex.replace(t, t.length, 0, ""); return t; } catch (GLib.Error e) { - GLib.warning("%s", e.message); + GLib.warning(_("Error normalizing: %s"), e.message); } return ""; } @@ -220,5 +229,120 @@ namespace GQPE { stdout.vprintf(full_format, list); GLib.Process.exit(1); } + + /** + * Loads photographs from an input directory. + * @param input the input directory. + * @return a sorted set of photographs. + */ + public static Gee.SortedSet + load_photos_dir(string input, ProgressMessage messenger) + throws GLib.Error { + messenger(ProgressState.INIT, 0); + int c = 0; + var root = GLib.File.new_for_path(input); + Gee.ArrayQueue queue = new Gee.ArrayQueue(); + queue.offer(root); + var photos = new Gee.TreeSet(); + while (!queue.is_empty) { + var dir = queue.poll(); + var e = dir.enumerate_children(FileAttribute.STANDARD_NAME, 0); + FileInfo file_info; + while ((file_info = e.next_file ()) != null) { + var path = string.join(GLib.Path.DIR_SEPARATOR_S, + dir.get_path(), + file_info.get_name()); + var file = File.new_for_path(path); + if (GLib.FileUtils.test(path, GLib.FileTest.IS_DIR)) { + queue.offer(file); + continue; + } + try { + var photo = new Photograph(file); + photos.add(photo); + messenger(ProgressState.ADVANCE, c++); + } catch (GLib.Error e) { + GLib.warning(_("Error processing %s: %s. Skipping."), + path, e.message); + } + } + } + messenger(ProgressState.END, c); + return photos; + } + + /** + * Loads photographs from an array of filenames. + * @param args the array of filenames. + * @param offset the array offset. + * @return a sorted set of photographs. + */ + public static Gee.SortedSet + load_photos_array(string[] args, int offset, + ProgressMessage messenger) { + messenger(ProgressState.INIT, 0); + int c = 0; + var photos = new Gee.TreeSet(); + for (int i = offset; i < args.length; i++) { + var file = GLib.File.new_for_path(args[i]); + try { + var photo = new Photograph(file); + photos.add(photo); + messenger(ProgressState.ADVANCE, c++); + } catch (GLib.Error e) { + GLib.warning(_("Error processing %s: %s. Skipping."), + args[i], e.message); + } + } + messenger(ProgressState.END, c); + return photos; + } + + public static int64 now() { + return new DateTime.now_utc().to_unix(); + } + + + /** + * Loads the photograph pixbuf. + * @param photograph the photograph. + * @return the photograph pixbuf. + */ + public static Gdk.Pixbuf load_pixbuf(Photograph photograph) + throws GLib.Error { + var path = photograph.path; + var pb = new Gdk.Pixbuf.from_file(path); + switch (photograph.orientation) { + case Orientation.LANDSCAPE: + break; + case Orientation.REVERSE_LANDSCAPE: + pb = pb.rotate_simple(Gdk.PixbufRotation.UPSIDEDOWN); + break; + case Orientation.PORTRAIT: + pb = pb.rotate_simple(Gdk.PixbufRotation.CLOCKWISE); + break; + case Orientation.REVERSE_PORTRAIT: + pb = pb.rotate_simple(Gdk.PixbufRotation.COUNTERCLOCKWISE); + break; + } + return pb; + } + + /** + * Scales a pixbuf by its longest length. + * @param pixbuf the pixbuf. + * @param length the longest length to scale. + * @return the pixbuf scalated. + */ + public static Gdk.Pixbuf scale_pixbuf(Gdk.Pixbuf pixbuf, int length) { + double scale = 1.0; + if (pixbuf.width > pixbuf.height) + scale = ((double)length) / pixbuf.width; + else + scale = ((double)length) / pixbuf.height; + return pixbuf.scale_simple((int)(pixbuf.width * scale), + (int)(pixbuf.height * scale), + Gdk.InterpType.HYPER); + } } } diff --git a/meson.build b/meson.build index 6302ffdb8973f51adaca702a616ae6b978154827..c02324cdb124efc3a598e843c2c943e772ac9344 100644 --- a/meson.build +++ b/meson.build @@ -20,6 +20,7 @@ add_global_arguments('-DGETTEXT_PACKAGE="gqpe"', language: 'c') vapidir = join_paths(meson.current_source_dir(), 'vapi') add_project_arguments(['--vapidir=' + vapidir, '--vapidir=.', + '--pkg=posix', '--pkg=config', '--target-glib=2.38'], language: 'vala') @@ -31,7 +32,10 @@ clutter_gtk = dependency('clutter-gtk-1.0') gee = dependency('gee-0.8') gexiv2 = dependency('gexiv2') gdk = dependency('gdk-3.0') +gdkpixbuf = dependency('gdk-pixbuf-2.0') gtk = dependency('gtk+-3.0') +sqlite3 = dependency('sqlite3') +json = dependency('json-glib-1.0') xsltproc = find_program('xsltproc', required: false) @@ -67,6 +71,7 @@ gqpelib_sources = [ gqpelib_dependencies = [ gee, gexiv2, + gdkpixbuf, math ] @@ -82,6 +87,7 @@ gqpetags_sources = [ gqpetags_dependencies = [ gee, gexiv2, + gdkpixbuf, math ] @@ -97,6 +103,7 @@ gqpecopy_sources = [ gqpecopy_dependencies = [ gee, gexiv2, + gdkpixbuf, math ] @@ -112,6 +119,7 @@ gqpestore_sources = [ gqpestore_dependencies = [ gee, gexiv2, + gdkpixbuf, math ] @@ -127,6 +135,7 @@ gqpeinterpolategps_sources = [ gqpeinterpolategps_dependencies = [ gee, gexiv2, + gdkpixbuf, math ] @@ -135,6 +144,25 @@ executable('gqpe-interpolate-gps', gqpeinterpolategps_sources, include_directories: [ gqpelib_includes ], install: true, link_with: [ gqpelib ]) +gqpeshotwell_sources = [ + 'src/shotwell.vala' +] + +gqpeshotwell_dependencies = [ + gee, + gexiv2, + gdk, + gdkpixbuf, + json, + math, + sqlite3 +] + +executable('gqpe-shotwell', gqpeshotwell_sources, + dependencies: gqpeshotwell_dependencies, + include_directories: [ gqpelib_includes ], + install: true, link_with: [ gqpelib ]) + gresources = gnome.compile_resources( 'gresources', 'data/gqpe.gresource.xml', source_dir: 'data', diff --git a/po/POTFILES b/po/POTFILES index bac33094edd9db45b0188794ce2bad9882d387cd..61a97dcef1139809e6e02c278e26cdf3518582a3 100644 --- a/po/POTFILES +++ b/po/POTFILES @@ -9,9 +9,12 @@ lib/photograph.vala lib/pretty-box.vala lib/tag.vala lib/util.vala +src/application.vala src/application-main.vala src/application-window.vala -src/application.vala +src/copy.vala +src/interpolate-gps.vala src/namespace.vala +src/shotwell.vala src/store.vala src/tags.vala diff --git a/po/es.po b/po/es.po index 115708c9cd80e05395474bd0b16c8c2bfad1394f..c953c677fa255ea5141de2585b6f65f4af702f98 100644 --- a/po/es.po +++ b/po/es.po @@ -8,8 +8,8 @@ msgid "" msgstr "" "Project-Id-Version: gqpe 0.1\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2021-11-28 22:07-0600\n" -"PO-Revision-Date: 2021-11-28 19:55-0600\n" +"POT-Creation-Date: 2021-11-30 13:31-0600\n" +"PO-Revision-Date: 2021-11-30 13:54-0600\n" "Last-Translator: Canek Peláez Valdés \n" "Language-Team: es\n" "Language: es\n" @@ -69,10 +69,303 @@ msgstr "Editor rápido de fotos basado en Gtk+" msgid "Quick editor for photograph tags" msgstr "Editor rápido para etiquetas de fotografía" +#: lib/photograph.vala:506 +#, c-format +msgid "Cannot get file status: %s" +msgstr "No se puede obtener el estado del archivo: %s" + +#: lib/util.vala:86 +#, c-format +msgid "Error reading from ‘%s’.\n" +msgstr "Error leyendo de: ‘%s’.\n" + +#: lib/util.vala:104 +#, c-format +msgid "Error writing to ‘%s’.\n" +msgstr "Error escribendo a: ‘%s’.\n" + +#: lib/util.vala:135 +#, c-format +msgid "Error normalizing: %s" +msgstr "Error normalizando: %s\n" + +#: lib/util.vala:265 lib/util.vala:293 +#, c-format +msgid "Error processing %s: %s. Skipping." +msgstr "Error procesando %s: %s. Saltándolo." + #: src/application.vala:94 msgid "A Gtk+ based quick photo editor" msgstr "Un rápido editor de fotos basado en Gtk+" +#. The option context. +#: src/copy.vala:38 +msgid "INPUT OUTPUT - Copy the image tags." +msgstr "ENTRADA SALIDA - Copiar las etiquetas de imagen." + +#: src/copy.vala:44 +msgid "Do not copy GPS data." +msgstr "No copiar los datos GPS." + +#: src/copy.vala:46 +msgid "Do not copy date and time data." +msgstr "No copiar los datos de la fecha y hora." + +#: src/copy.vala:48 +msgid "Only copy GPS data." +msgstr "Sólo copiar datos GPS." + +#: src/copy.vala:75 src/interpolate-gps.vala:165 src/shotwell.vala:786 +#: src/store.vala:238 src/tags.vala:322 +#, c-format +msgid "Run ‘%s --help’ for a list of options" +msgstr "Ejecute ‘%s --help’ para una lista de opciones" + +#: src/copy.vala:79 +msgid "Exactly one input and one output file needed" +msgstr "Se necesita exactamente un archivo entrada y uno de salida" + +#: src/copy.vala:83 +msgid "You cannot mix -g and -G" +msgstr "No puede mezclar -g y -G" + +#: src/copy.vala:85 +msgid "You cannot mix -g and -T" +msgstr "No puede mezclar -g y -T" + +#: src/copy.vala:94 +#, c-format +msgid "An error ocurred while copying %s:" +msgstr "Ocurrió un error al copiar %s:" + +#: src/interpolate-gps.vala:36 +msgid "INPUT - Interpolate GPS coordinates." +msgstr "ENTRADA - Interpolar coordenadas GPS." + +#: src/interpolate-gps.vala:42 +msgid "Force a range" +msgstr "Forzar un rango" + +#: src/interpolate-gps.vala:44 src/shotwell.vala:183 +msgid "Be verbose" +msgstr "Sé verboso" + +#: src/interpolate-gps.vala:53 src/shotwell.vala:192 src/tags.vala:376 +msgid "Loading photographs…\n" +msgstr "Cargando fotografías…\n" + +#: src/interpolate-gps.vala:56 +#, c-format +msgid "Loaded %d photographs…" +msgstr "Se cargaron %d fotografías…" + +#: src/interpolate-gps.vala:59 src/shotwell.vala:202 +#, c-format +msgid "Loaded %d photographs.\n" +msgstr "Se cargaron %d fotografías.\n" + +#: src/interpolate-gps.vala:103 src/tags.vala:277 +#, c-format +msgid "Updating %s…\n" +msgstr "Actualizando %s…\n" + +#: src/interpolate-gps.vala:149 +#, c-format +msgid "%d photographs exported\n" +msgstr "% fotografías exportadas.\n" + +#: src/interpolate-gps.vala:151 src/shotwell.vala:673 +#, c-format +msgid "Error while exporting: %s" +msgstr "Error al exportar: %s" + +#: src/interpolate-gps.vala:169 src/shotwell.vala:800 +msgid "Missing files or directory" +msgstr "Faltan archivos o un directorio" + +#: src/interpolate-gps.vala:172 src/shotwell.vala:804 +#, c-format +msgid "%s is not a directory" +msgstr "%s no es un directorio" + +#: src/shotwell.vala:173 +msgid "[DIRNAME] [FILENAME…] - Export to Shotwell." +msgstr "[DIRECTORIO] [ARCHIVO…] - Exportar a Shotwell." + +#: src/shotwell.vala:179 +msgid "Use a file list instead of a directory" +msgstr "Usar una lista de archivos en lugar de un directorio" + +#: src/shotwell.vala:181 +msgid "Use a Galería JSON to set the primary photos" +msgstr "Usar un JSON de Galería para definir las fotos primarias" + +#: src/shotwell.vala:196 +#, c-format +msgid "Loaded %d photographs…%s" +msgstr "Se cargaron %d fotografías…%s" + +#: src/shotwell.vala:212 src/shotwell.vala:224 src/shotwell.vala:248 +#: src/shotwell.vala:265 src/shotwell.vala:291 src/shotwell.vala:311 +#: src/shotwell.vala:328 src/shotwell.vala:347 src/shotwell.vala:361 +#: src/shotwell.vala:397 src/shotwell.vala:497 src/shotwell.vala:513 +#: src/shotwell.vala:534 src/shotwell.vala:557 src/shotwell.vala:693 +#, c-format +msgid "SQL error: %d, %s\n" +msgstr "Error SQL: %d, %s\n" + +#: src/shotwell.vala:275 src/shotwell.vala:336 src/shotwell.vala:436 +#, c-format +msgid "Error inserting: %d, %s\n" +msgstr "Error insertando: %d, %s\n" + +#: src/shotwell.vala:378 +#, c-format +msgid "Cannot read file: %s" +msgstr "No puedo leer archivo: %s" + +#: src/shotwell.vala:389 +#, c-format +msgid "Error getting photo id for: %s\n" +msgstr "Error obteniendo el identificador de la foto: %s\n" + +#: src/shotwell.vala:470 +#, c-format +msgid "Generating GNOME thumb: %s\n" +msgstr "Generando miniatura de GNOME: %s\n" + +#: src/shotwell.vala:475 +#, c-format +msgid "Generating Shotwell 128px thumb: %s\n" +msgstr "Generando miniatura 128px de Shotwell: %s\n" + +#: src/shotwell.vala:481 +#, c-format +msgid "Generating Shotwell 360px thumb: %s\n" +msgstr "Generando miniatura 360px de Shotwell: %s\n" + +#: src/shotwell.vala:571 +#, c-format +msgid "Exporting: %s\n" +msgstr "Exportando: %s\n" + +#: src/shotwell.vala:573 +#, c-format +msgid "Already in database: %s, skipping.\n" +msgstr "Ya está en la base de datos: %s, saltándolo.\n" + +#: src/shotwell.vala:578 +#, c-format +msgid "Missing title: %s, skipping.\n" +msgstr "Título faltante: %s, saltándolo." + +#: src/shotwell.vala:582 +#, c-format +msgid "Missing album: %s, skipping.\n" +msgstr "Álbum faltante: %s, saltándolo." + +#: src/shotwell.vala:587 +#, c-format +msgid "Error getting event for: %s, skipping.\n" +msgstr "Error obteniendo evento para: %s, saltándolo.\n" + +#: src/shotwell.vala:593 +#, c-format +msgid "Error getting tag for: %s, skipping.\n" +msgstr "Error obteniendo etiqueta para: %s, saltándolo." + +#: src/shotwell.vala:599 +#, c-format +msgid "Error inserting photograph: %s, skipping.\n" +msgstr "Error insertando fotografía: %s, saltándolo." + +#: src/shotwell.vala:604 +#, c-format +msgid "Error making thumbnail for: %s, skipping.\n" +msgstr "Error haciedo miniatura para: %s, saltándolo." + +#: src/shotwell.vala:609 +#, c-format +msgid "Error updating event for: %s, skipping.\n" +msgstr "Error actualizando evento para: %s, saltándolo.\n" + +#: src/shotwell.vala:614 +#, c-format +msgid "Error updating tag for: %s, skipping.\n" +msgstr "Error actualizando etiqueta para: %s, saltándolo.\n" + +#: src/shotwell.vala:630 +#, c-format +msgid "%d photographs exported…%s" +msgstr "%d fotografías exportadas…%s" + +#: src/shotwell.vala:644 +#, c-format +msgid "Cannot open database: %s\n" +msgstr "No se puede abrir la base de datos: %s\n" + +#: src/shotwell.vala:655 src/shotwell.vala:749 +#, c-format +msgid "Cannot begin transaction: %s\n" +msgstr "No se puede iniciar transacción: %s\n" + +#: src/shotwell.vala:664 +#, c-format +msgid "Exporting %d photographs…\n" +msgstr "Exportando %d fotografías…\n" + +#: src/shotwell.vala:666 +#, c-format +msgid "%d photographs exported.\n" +msgstr "%d fotografías exportadas.\n" + +#: src/shotwell.vala:669 src/shotwell.vala:762 +#, c-format +msgid "Cannot rollback transaction: %s\n" +msgstr "No se pudo deshacer la transación: %s\n" + +#: src/shotwell.vala:676 src/shotwell.vala:770 +#, c-format +msgid "Cannot commit transaction: %s\n" +msgstr "No se pudo realizar la transacción: %s\n" + +#: src/shotwell.vala:706 +#, c-format +msgid "Updating cover for %s…\n" +msgstr "Actualizando portada para %s…\n" + +#: src/shotwell.vala:737 +#, c-format +msgid "%d covers set.%s" +msgstr "%d portadas ajustadas.%s" + +#: src/shotwell.vala:754 +msgid "Setting covers…\n" +msgstr "Reajustando portadas…\n" + +#: src/shotwell.vala:759 +#, c-format +msgid "%d covers set.\n" +msgstr "%d portadas ajustadas.\n" + +#: src/shotwell.vala:766 +#, c-format +msgid "Error while setting primary photos: %s" +msgstr "Error al reajustar fotos primarias: %s" + +#: src/shotwell.vala:790 +msgid "You cannot mix -c and -f" +msgstr "No puede mezclar -c y -f" + +#: src/shotwell.vala:794 +msgid "The -c option only needs one file" +msgstr "La opción -c sólo necesita un archivo" + +#: src/shotwell.vala:808 +#, c-format +msgid "%s is not a file" +msgstr "%s no es un archivo" + #: src/store.vala:51 msgid "INPUTDIR OUTPUTDIR - Store images to a normalized location." msgstr "ENTRADA SALIDA - Almacena imágenes a una dirección normalizada." @@ -123,11 +416,6 @@ msgstr "Hubo un error al procesar %s: %s." msgid "Skipping.\n" msgstr "Omitiendo.\n" -#: src/store.vala:238 src/tags.vala:322 -#, c-format -msgid "Run ‘%s --help’ for a list of options" -msgstr "Ejecute ‘%s --help’ para una lista de opciones" - #: src/store.vala:243 msgid "Exactly one output and one input directory needed" msgstr "Se necesitan exactamente un directorio entrada y de salida" @@ -143,8 +431,8 @@ msgid "There was an error while storing: %s\n" msgstr "Ocurrió un error al almacenar: %s\n" #: src/tags.vala:67 -msgid "[FILENAME…] - Edit and show the image tags." -msgstr "[ARCHIVO…] - Edita y muestra las etiquetas de imagen." +msgid "{FILENAME…} - Edit and show the image tags." +msgstr "{ARCHIVO…} - Edita y muestra las etiquetas de imagen." #: src/tags.vala:72 msgid "" @@ -297,11 +585,6 @@ msgstr "Versión GPS" msgid "GPS datum" msgstr "Dato GPS" -#: src/tags.vala:277 -#, c-format -msgid "Updating %s…\n" -msgstr "Actualizando %s…\n" - #: src/tags.vala:281 #, c-format msgid "%s updated.\n" @@ -353,10 +636,6 @@ msgstr "La latitud sólo se definirá para imáneges con información GPS" msgid "Longitude will only be set on images with GPS data" msgstr "La longitud sólo se definirá para imágenes con información GPS" -#: src/tags.vala:376 -msgid "Loading photographs…\n" -msgstr "Cargando fotografías…\n" - #: src/tags.vala:382 #, c-format msgid "Loaded %d photographs… %s" diff --git a/po/gqpe.pot b/po/gqpe.pot index 16e5db6062bf7b9640e604b10989e341d3e26000..9fb48e4eb502039748d6ebb1368708cf2f271e13 100644 --- a/po/gqpe.pot +++ b/po/gqpe.pot @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: gqpe\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2021-11-28 22:07-0600\n" +"POT-Creation-Date: 2021-11-30 13:31-0600\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -69,10 +69,303 @@ msgstr "" msgid "Quick editor for photograph tags" msgstr "" +#: lib/photograph.vala:506 +#, c-format +msgid "Cannot get file status: %s" +msgstr "" + +#: lib/util.vala:86 +#, c-format +msgid "Error reading from ‘%s’.\n" +msgstr "" + +#: lib/util.vala:104 +#, c-format +msgid "Error writing to ‘%s’.\n" +msgstr "" + +#: lib/util.vala:135 +#, c-format +msgid "Error normalizing: %s" +msgstr "" + +#: lib/util.vala:265 lib/util.vala:293 +#, c-format +msgid "Error processing %s: %s. Skipping." +msgstr "" + #: src/application.vala:94 msgid "A Gtk+ based quick photo editor" msgstr "" +#. The option context. +#: src/copy.vala:38 +msgid "INPUT OUTPUT - Copy the image tags." +msgstr "" + +#: src/copy.vala:44 +msgid "Do not copy GPS data." +msgstr "" + +#: src/copy.vala:46 +msgid "Do not copy date and time data." +msgstr "" + +#: src/copy.vala:48 +msgid "Only copy GPS data." +msgstr "" + +#: src/copy.vala:75 src/interpolate-gps.vala:165 src/shotwell.vala:786 +#: src/store.vala:238 src/tags.vala:322 +#, c-format +msgid "Run ‘%s --help’ for a list of options" +msgstr "" + +#: src/copy.vala:79 +msgid "Exactly one input and one output file needed" +msgstr "" + +#: src/copy.vala:83 +msgid "You cannot mix -g and -G" +msgstr "" + +#: src/copy.vala:85 +msgid "You cannot mix -g and -T" +msgstr "" + +#: src/copy.vala:94 +#, c-format +msgid "An error ocurred while copying %s:" +msgstr "" + +#: src/interpolate-gps.vala:36 +msgid "INPUT - Interpolate GPS coordinates." +msgstr "" + +#: src/interpolate-gps.vala:42 +msgid "Force a range" +msgstr "" + +#: src/interpolate-gps.vala:44 src/shotwell.vala:183 +msgid "Be verbose" +msgstr "" + +#: src/interpolate-gps.vala:53 src/shotwell.vala:192 src/tags.vala:376 +msgid "Loading photographs…\n" +msgstr "" + +#: src/interpolate-gps.vala:56 +#, c-format +msgid "Loaded %d photographs…" +msgstr "" + +#: src/interpolate-gps.vala:59 src/shotwell.vala:202 +#, c-format +msgid "Loaded %d photographs.\n" +msgstr "" + +#: src/interpolate-gps.vala:103 src/tags.vala:277 +#, c-format +msgid "Updating %s…\n" +msgstr "" + +#: src/interpolate-gps.vala:149 +#, c-format +msgid "%d photographs exported\n" +msgstr "" + +#: src/interpolate-gps.vala:151 src/shotwell.vala:673 +#, c-format +msgid "Error while exporting: %s" +msgstr "" + +#: src/interpolate-gps.vala:169 src/shotwell.vala:800 +msgid "Missing files or directory" +msgstr "" + +#: src/interpolate-gps.vala:172 src/shotwell.vala:804 +#, c-format +msgid "%s is not a directory" +msgstr "" + +#: src/shotwell.vala:173 +msgid "[DIRNAME] [FILENAME…] - Export to Shotwell." +msgstr "" + +#: src/shotwell.vala:179 +msgid "Use a file list instead of a directory" +msgstr "" + +#: src/shotwell.vala:181 +msgid "Use a Galería JSON to set the primary photos" +msgstr "" + +#: src/shotwell.vala:196 +#, c-format +msgid "Loaded %d photographs…%s" +msgstr "" + +#: src/shotwell.vala:212 src/shotwell.vala:224 src/shotwell.vala:248 +#: src/shotwell.vala:265 src/shotwell.vala:291 src/shotwell.vala:311 +#: src/shotwell.vala:328 src/shotwell.vala:347 src/shotwell.vala:361 +#: src/shotwell.vala:397 src/shotwell.vala:497 src/shotwell.vala:513 +#: src/shotwell.vala:534 src/shotwell.vala:557 src/shotwell.vala:693 +#, c-format +msgid "SQL error: %d, %s\n" +msgstr "" + +#: src/shotwell.vala:275 src/shotwell.vala:336 src/shotwell.vala:436 +#, c-format +msgid "Error inserting: %d, %s\n" +msgstr "" + +#: src/shotwell.vala:378 +#, c-format +msgid "Cannot read file: %s" +msgstr "" + +#: src/shotwell.vala:389 +#, c-format +msgid "Error getting photo id for: %s\n" +msgstr "" + +#: src/shotwell.vala:470 +#, c-format +msgid "Generating GNOME thumb: %s\n" +msgstr "" + +#: src/shotwell.vala:475 +#, c-format +msgid "Generating Shotwell 128px thumb: %s\n" +msgstr "" + +#: src/shotwell.vala:481 +#, c-format +msgid "Generating Shotwell 360px thumb: %s\n" +msgstr "" + +#: src/shotwell.vala:571 +#, c-format +msgid "Exporting: %s\n" +msgstr "" + +#: src/shotwell.vala:573 +#, c-format +msgid "Already in database: %s, skipping.\n" +msgstr "" + +#: src/shotwell.vala:578 +#, c-format +msgid "Missing title: %s, skipping.\n" +msgstr "" + +#: src/shotwell.vala:582 +#, c-format +msgid "Missing album: %s, skipping.\n" +msgstr "" + +#: src/shotwell.vala:587 +#, c-format +msgid "Error getting event for: %s, skipping.\n" +msgstr "" + +#: src/shotwell.vala:593 +#, c-format +msgid "Error getting tag for: %s, skipping.\n" +msgstr "" + +#: src/shotwell.vala:599 +#, c-format +msgid "Error inserting photograph: %s, skipping.\n" +msgstr "" + +#: src/shotwell.vala:604 +#, c-format +msgid "Error making thumbnail for: %s, skipping.\n" +msgstr "" + +#: src/shotwell.vala:609 +#, c-format +msgid "Error updating event for: %s, skipping.\n" +msgstr "" + +#: src/shotwell.vala:614 +#, c-format +msgid "Error updating tag for: %s, skipping.\n" +msgstr "" + +#: src/shotwell.vala:630 +#, c-format +msgid "%d photographs exported…%s" +msgstr "" + +#: src/shotwell.vala:644 +#, c-format +msgid "Cannot open database: %s\n" +msgstr "" + +#: src/shotwell.vala:655 src/shotwell.vala:749 +#, c-format +msgid "Cannot begin transaction: %s\n" +msgstr "" + +#: src/shotwell.vala:664 +#, c-format +msgid "Exporting %d photographs…\n" +msgstr "" + +#: src/shotwell.vala:666 +#, c-format +msgid "%d photographs exported.\n" +msgstr "" + +#: src/shotwell.vala:669 src/shotwell.vala:762 +#, c-format +msgid "Cannot rollback transaction: %s\n" +msgstr "" + +#: src/shotwell.vala:676 src/shotwell.vala:770 +#, c-format +msgid "Cannot commit transaction: %s\n" +msgstr "" + +#: src/shotwell.vala:706 +#, c-format +msgid "Updating cover for %s…\n" +msgstr "" + +#: src/shotwell.vala:737 +#, c-format +msgid "%d covers set.%s" +msgstr "" + +#: src/shotwell.vala:754 +msgid "Setting covers…\n" +msgstr "" + +#: src/shotwell.vala:759 +#, c-format +msgid "%d covers set.\n" +msgstr "" + +#: src/shotwell.vala:766 +#, c-format +msgid "Error while setting primary photos: %s" +msgstr "" + +#: src/shotwell.vala:790 +msgid "You cannot mix -c and -f" +msgstr "" + +#: src/shotwell.vala:794 +msgid "The -c option only needs one file" +msgstr "" + +#: src/shotwell.vala:808 +#, c-format +msgid "%s is not a file" +msgstr "" + #: src/store.vala:51 msgid "INPUTDIR OUTPUTDIR - Store images to a normalized location." msgstr "" @@ -123,11 +416,6 @@ msgstr "" msgid "Skipping.\n" msgstr "" -#: src/store.vala:238 src/tags.vala:322 -#, c-format -msgid "Run ‘%s --help’ for a list of options" -msgstr "" - #: src/store.vala:243 msgid "Exactly one output and one input directory needed" msgstr "" @@ -143,7 +431,7 @@ msgid "There was an error while storing: %s\n" msgstr "" #: src/tags.vala:67 -msgid "[FILENAME…] - Edit and show the image tags." +msgid "{FILENAME…} - Edit and show the image tags." msgstr "" #: src/tags.vala:72 @@ -278,11 +566,6 @@ msgstr "" msgid "GPS datum" msgstr "" -#: src/tags.vala:277 -#, c-format -msgid "Updating %s…\n" -msgstr "" - #: src/tags.vala:281 #, c-format msgid "%s updated.\n" @@ -334,10 +617,6 @@ msgstr "" msgid "Longitude will only be set on images with GPS data" msgstr "" -#: src/tags.vala:376 -msgid "Loading photographs…\n" -msgstr "" - #: src/tags.vala:382 #, c-format msgid "Loaded %d photographs… %s" diff --git a/src/application-window.vala b/src/application-window.vala index 2a914bb59e915de9c7a21aa543697ed02fc5e674..3c6588ab3c7556b3f43cf93f47074c3ef94b7aec 100644 --- a/src/application-window.vala +++ b/src/application-window.vala @@ -384,7 +384,7 @@ namespace GQPE { var photo = pmap[path]; if (!pixbufs.has_key(path)) { try { - load_pixbuf(photo); + pixbufs[path] = Util.load_pixbuf(photo); } catch (GLib.Error e) { GLib.warning("Could not load '%s': %s", path, e.message); } @@ -478,7 +478,7 @@ namespace GQPE { var path = photograph.path; if (!pixbufs.has_key(path)) { try { - load_pixbuf(photograph); + pixbufs[path] = Util.load_pixbuf(photograph); } catch (GLib.Error e) { GLib.warning("Could not load '%s': %s", path, e.message); disable_ui(Item.PICTURE); @@ -592,33 +592,5 @@ namespace GQPE { entry.secondary_icon_activatable = false; } } - - /* Loads the pixbuf. */ - private void load_pixbuf(Photograph photograph) - throws GLib.Error { - var path = photograph.path; - var pb = new Gdk.Pixbuf.from_file(path); - switch (photograph.orientation) { - case Orientation.LANDSCAPE: - break; - case Orientation.REVERSE_LANDSCAPE: - pb = pb.rotate_simple(Gdk.PixbufRotation.UPSIDEDOWN); - break; - case Orientation.PORTRAIT: - pb = pb.rotate_simple(Gdk.PixbufRotation.CLOCKWISE); - break; - case Orientation.REVERSE_PORTRAIT: - pb = pb.rotate_simple(Gdk.PixbufRotation.COUNTERCLOCKWISE); - break; - } - double scale = 1.0; - if (pb.width > pb.height) - scale = ((double)MAX_LENGTH) / pb.width; - else - scale = ((double)MAX_LENGTH) / pb.height; - pixbufs[path] = pb.scale_simple((int)(pb.width * scale), - (int)(pb.height * scale), - Gdk.InterpType.HYPER); - } } } diff --git a/src/interpolate-gps.vala b/src/interpolate-gps.vala index ade80f91059fc407a92289f37c55badfb9317cfc..a08ad07be973fcb479894d73b8a2794999aa0c62 100644 --- a/src/interpolate-gps.vala +++ b/src/interpolate-gps.vala @@ -28,8 +28,6 @@ namespace GQPE { /* Wheter to be verbose. */ private static bool verbose; - /* The input directory. */ - private static string input; /* The photographs. */ private static Photograph[] photographs; @@ -49,44 +47,18 @@ namespace GQPE { return options; } - /*Loads the photos from the input directory. */ - private static void load_photos() throws GLib.Error { - stdout.printf(_("Loading photographs…\n")); - int c = 0; - var root = GLib.File.new_for_path(input); - Gee.ArrayQueue queue = new Gee.ArrayQueue(); - queue.offer(root); - var photos = new Gee.TreeSet(); - while (!queue.is_empty) { - var dir = queue.poll(); - var e = dir.enumerate_children(FileAttribute.STANDARD_NAME, 0); - FileInfo file_info; - while ((file_info = e.next_file ()) != null) { - var path = string.join(GLib.Path.DIR_SEPARATOR_S, - dir.get_path(), - file_info.get_name()); - var file = File.new_for_path(path); - if (GLib.FileUtils.test(path, GLib.FileTest.IS_DIR)) { - queue.offer(file); - continue; - } - try { - var photo = new Photograph(file); - photos.add(photo); - stderr.printf(_("Loaded %d photographs… "), - c++, "\r\b"); - } catch (GLib.Error e) { - var m = _("There was an error processing %s: %s. "); - stderr.printf(m, path, e.message); - stderr.printf(_("Skipping.\n")); - } - } + private static void progress(ProgressState state, int number) { + switch (state) { + case INIT: + stdout.printf(_("Loading photographs…\n")); + break; + case ADVANCE: + stderr.printf(_("Loaded %d photographs…"), number, "\r\b"); + break; + case END: + stdout.printf(_("Loaded %d photographs.\n"), number); + break; } - int i = 0; - photographs = new Photograph[photos.size]; - foreach (var photo in photos) - photographs[i++] = photo; - stdout.printf(_("Loaded %d photographs… \n"), c++); } /* Recursively interpolates the coordinates for a range. */ @@ -139,7 +111,6 @@ namespace GQPE { /* Interpolates a directory of photos. */ private static int interpolate_photos() throws GLib.Error { - load_photos(); int j = -1; bool left = false, middle = false; int c = 0; @@ -165,6 +136,22 @@ namespace GQPE { return c; } + private static void do_interpolating(string[] args) { + try { + var photos = (args.length == 2) ? + Util.load_photos_dir(args[1], (s, n) => progress(s, n)) : + Util.load_photos_array(args, 1, (s, n) => progress(s, n)); + photographs = new Photograph[photos.size]; + int i = 0; + foreach (var photo in photos) + photographs[i++] = photo; + int c = interpolate_photos(); + stderr.printf(_("%d photographs exported\n"), c); + } catch (GLib.Error e) { + Util.error(_("Error while exporting: %s"), e.message); + } + } + public static int main(string[] args) { GLib.Intl.setlocale(LocaleCategory.ALL, ""); verbose = false; @@ -178,47 +165,12 @@ namespace GQPE { Util.error(_("Run ‘%s --help’ for a list of options"), args[0]); } - if (args.length < 2) { + if (args.length < 2) Util.error(_("Missing files or directory")); - } else if (args.length == 2) { - input = args[1]; - if (!GLib.FileUtils.test(input, GLib.FileTest.IS_DIR)) - Util.error(_("%s is not a directory"), input); - try { - int c = interpolate_photos(); - stderr.printf(_("%d photographs updated\n"), c); - } catch (GLib.Error e) { - Util.error(_("There was an error while interpolating: %s")); - } - } else { - photographs = new Photograph[args.length-1]; - int c = 0; - for (int i = 1; i < args.length; i++) { - var file = GLib.File.new_for_path(args[i]); - try { - photographs[i-1] = new Photograph(file); - stderr.printf(_("Loaded %d photographs… %s"), - c++, "\r\b"); - } catch (GLib.Error e) { - var m = _("There was an error processing %s: %s. "); - stderr.printf(m, args[i], e.message); - stderr.printf(_("Skipping.\n")); - } - } - stderr.printf(_("Loaded %d photosgraphs… \n"), c); - int n = photographs.length; - if (!photographs[0].has_geolocation || - !photographs[n-1].has_geolocation) - Util.error( - _("First and last photograph must have GPS data")); - try { - c = interpolate_range(0, n-1); - } catch (GLib.Error e) { - Util.error( - _("There was an error while interpolating: %s")); - } - stderr.printf(_("%d photographs updated\n"), c); - } + else if (args.length == 2) + if (!GLib.FileUtils.test(args[1], GLib.FileTest.IS_DIR)) + Util.error(_("%s is not a directory"), args[1]); + do_interpolating(args); return 0; } diff --git a/src/shotwell.vala b/src/shotwell.vala new file mode 100644 index 0000000000000000000000000000000000000000..63dcd3ef481379e71a32859ee6be13c74e8138bb --- /dev/null +++ b/src/shotwell.vala @@ -0,0 +1,816 @@ +/* + * This file is part of gqpe. + * + * Copyright © 2013-2021 Canek Peláez Valdés + * + * This program is free software: you can redistribute it and/or modify it under + * the terms of the GNU General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later + * version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + * details. + * + * You should have received a copy of the GNU General Public License along with + * this program. If not, see . + */ +namespace GQPE { + + /** + * Export photos to Shotwell. + */ + public class Shotwell { + + private enum PhotoColumn { + ID, + FILENAME, + WIDTH, + HEIGHT, + FILESIZE, + TIMESTAMP, + EXPOSURE_TIME, + ORIENTATION, + ORIGINAL_ORIENTATION, + IMPORT_ID, + EVENT_ID, + TRANSFORMATIONS, + MD5, + THUMBNAIL_MD5, + EXIF_MD5, + TIME_CREATED, + FLAGS, + RATING, + FILE_FORMAT, + TITLE, + BACKLINKS, + TIME_REIMPORTED, + EDITABLE_ID, + METADATA_DIRTY, + DEVELOPER, + DEVELOP_SHOTWELL_ID, + DEVELOP_CAMERA_ID, + DEVELOP_EMBEDDED_ID, + COMMENT; + + public int arg() { + return ((int)this) + 1; + } + } + + private enum EventColumn { + ID, + NAME, + PRIMARY_PHOTO_ID, + TIME_CREATED, + PRIMARY_SOURCE_ID, + COMMENT; + + public int arg() { + return ((int)this) + 1; + } + } + + private enum TagColumn { + ID, + NAME, + PHOTO_ID_LIST, + TIME_CREATED; + + public int arg() { + return ((int)this) + 1; + } + } + + private class Event : GLib.Object { + public int id { get; private set; } + private int year; + private int month; + private string name; + public string key { get; private set; } + + public Event(int id, int year, int month, string name) { + this.id = id; + this.year = year; + this.month = month; + this.name = name; + key = Event.event_key(year, month, name); + } + + public Event.from_photo(int id, Photograph photo) { + this.id = id; + this.year = photo.datetime.get_year(); + this.month = photo.datetime.get_month(); + this.name = photo.album; + key = Event.event_key(year, month, name); + } + + public static string event_key(int year, int month, string name) { + return "%04d/%02d/%s".printf(year, month, name); + } + + public static string event_key_from_photo(Photograph photo) { + return "%04d/%02d/%s".printf(photo.datetime.get_year(), + photo.datetime.get_month(), + photo.album); + } + } + + private const string DEVELOPER = "SHOTWELL"; + private const string SELECT_PHOTO_PATH_QUERY = + "SELECT * FROM PhotoTable WHERE filename = ?;"; + private const string SELECT_PHOTO_PATH_LIKE_QUERY = + "SELECT * FROM PhotoTable WHERE filename LIKE ?;"; + private const string SELECT_PHOTO_EVENT_QUERY = + "SELECT * FROM PhotoTable WHERE event_id = ?;"; + private const string SELECT_MAX_PHOTO_ID_QUERY = + "SELECT MAX(id) FROM PhotoTable;"; + private const string SELECT_EVENT_NAME_QUERY = + "SELECT * FROM EventTable WHERE name = ?;"; + private const string SELECT_EVENT_ID_QUERY = + "SELECT * FROM EventTable WHERE id = ?;"; + private const string SELECT_MAX_EVENT_ID_QUERY = + "SELECT MAX(id) FROM EventTable;"; + private const string SELECT_TAG_NAME_QUERY = + "SELECT * FROM TagTable WHERE name = ?;"; + private const string SELECT_TAG_ID_QUERY = + "SELECT * FROM TagTable WHERE ID = ?;"; + private const string SELECT_MAX_TAG_ID_QUERY = + "SELECT MAX(id) FROM TagTable;"; + private const string SELECT_TAG_PHOTOS_QUERY = + "SELECT photo_id_list FROM TagTable WHERE id = ?;"; + private const string INSERT_PHOTO_QUERY = + "INSERT INTO PhotoTable VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?);"; + private const string INSERT_EVENT_QUERY = + "INSERT INTO EventTable VALUES (?, ?, ?, ?, ?, ?);"; + private const string INSERT_TAG_QUERY = + "INSERT INTO TagTable VALUES (?, ?, ?, ?);"; + private const string UPDATE_EVENT_QUERY = + "UPDATE EventTable SET primary_source_id = ? WHERE id = ?"; + private const string UPDATE_TAG_QUERY = + "UPDATE TagTable SET photo_id_list = ? WHERE id = ?"; + + private const int GTHUMB_LENGTH = 256; + private const int T128_LENGTH = 128; + private const int T360_LENGTH = 360; + + /* Wheter to use a list of files. */ + private static bool files; + /* The covers JSON file name. */ + private static string covers; + /* Wheter to be verbose. */ + private static bool verbose; + /* The database connection. */ + private static Sqlite.Database db; + + private static int64 export_id; + + private static Gee.TreeMap events; + + /* The option context. */ + private const string CONTEXT = + _("[DIRNAME] [FILENAME…] - Export to Shotwell."); + + /* Returns the options. */ + private static GLib.OptionEntry[] get_options() { + GLib.OptionEntry[] options = { + { "files", 'f', 0, GLib.OptionArg.NONE, &files, + _("Use a file list instead of a directory"), null }, + { "covers", 'c', 0, GLib.OptionArg.STRING, &covers, + _("Use a Galería JSON to set the primary photos"), null }, + { "verbose", 'v', 0, GLib.OptionArg.NONE, &verbose, + _("Be verbose"), null }, + { null } + }; + return options; + } + + private static void progress(ProgressState state, int number) { + switch (state) { + case INIT: + stdout.printf(_("Loading photographs…\n")); + break; + case ADVANCE: + if (!verbose) { + stdout.printf(_("Loaded %d photographs…%s"), + number, "\r\b"); + stdout.flush(); + } + break; + case END: + stdout.printf(_("Loaded %d photographs.\n"), number); + break; + } + } + + private static bool photo_in_db(Photograph photo) { + int rc; + Sqlite.Statement stmt; + rc = db.prepare_v2(SELECT_PHOTO_PATH_QUERY, -1, out stmt, null); + if (rc != Sqlite.OK) { + stderr.printf(_("SQL error: %d, %s\n"), rc, db.errmsg()); + return true; + } + stmt.bind_text(1, photo.path); + return stmt.step() == Sqlite.ROW; + } + + private static bool photo_in_event(Photograph photo, int event_id) { + int rc; + Sqlite.Statement stmt; + rc = db.prepare_v2(SELECT_PHOTO_EVENT_QUERY, -1, out stmt, null); + if (rc != Sqlite.OK) { + stderr.printf(_("SQL error: %d, %s\n"), rc, db.errmsg()); + return false; + } + stmt.bind_int(1, event_id); + if (stmt.step() == Sqlite.ROW) { + var path = stmt.column_text(PhotoColumn.FILENAME); + try { + var file = GLib.File.new_for_path(path); + var p = new Photograph(file); + if (photo.datetime.get_year() == p.datetime.get_year() && + photo.datetime.get_month() == p.datetime.get_month()) + return true; + } catch (GLib.Error e) { + return false; + } + } + return false; + } + + private static int get_new_event_id() { + int rc; + Sqlite.Statement stmt; + rc = db.prepare_v2(SELECT_MAX_EVENT_ID_QUERY, -1, out stmt, null); + if (rc != Sqlite.OK) { + stderr.printf(_("SQL error: %d, %s\n"), rc, db.errmsg()); + return -1; + } + if (stmt.step() == Sqlite.ROW) + return stmt.column_int(0)+1; + /* Empty table. */ + return 1; + } + + private static int create_new_event(Photograph photo) { + int event_id = get_new_event_id(); + if (event_id < 0) + return -1; + int rc; + Sqlite.Statement stmt; + rc = db.prepare_v2(INSERT_EVENT_QUERY, -1, out stmt, null); + if (rc != Sqlite.OK) { + stderr.printf(_("SQL error: %d, %s\n"), rc, db.errmsg()); + return -1; + } + stmt.bind_int(EventColumn.ID.arg(), event_id); + stmt.bind_text(EventColumn.NAME.arg(), photo.album); + stmt.bind_null(EventColumn.PRIMARY_PHOTO_ID.arg()); + stmt.bind_int64(EventColumn.TIME_CREATED.arg(), Util.now()); + stmt.bind_null(EventColumn.PRIMARY_SOURCE_ID.arg()); + stmt.bind_null(EventColumn.COMMENT.arg()); + if ((rc = stmt.step()) != Sqlite.DONE) { + stderr.printf(_("Error inserting: %d, %s\n"), rc, db.errmsg()); + return -1; + } + var _event = new Event.from_photo(event_id, photo); + events[_event.key] = _event; + return event_id; + } + + private static int get_event_by_name(Photograph photo) { + var key = Event.event_key_from_photo(photo); + if (events.has_key(key)) + return events[key].id; + int rc; + Sqlite.Statement stmt; + rc = db.prepare_v2(SELECT_EVENT_NAME_QUERY, -1, out stmt, null); + if (rc != Sqlite.OK) { + stderr.printf(_("SQL error: %d, %s\n"), rc, db.errmsg()); + return -1; + } + stmt.bind_text(1, photo.album); + if (stmt.step() == Sqlite.ROW) { + int event_id = stmt.column_int(EventColumn.ID); + if (photo_in_event(photo, event_id)) { + var _event = new Event.from_photo(event_id, photo); + events[_event.key] = _event; + return event_id; + } + } + return create_new_event(photo); + } + + private static int get_new_tag_id() { + int rc; + Sqlite.Statement stmt; + rc = db.prepare_v2(SELECT_MAX_TAG_ID_QUERY, -1, out stmt, null); + if (rc != Sqlite.OK) { + stderr.printf(_("SQL error: %d, %s\n"), rc, db.errmsg()); + return -1; + } + if (stmt.step() == Sqlite.ROW) + return stmt.column_int(0)+1; + /* Empty table. */ + return 1; + } + + private static int create_new_tag(Photograph photo) { + int tag_id = get_new_tag_id(); + if (tag_id < 0) + return -1; + int rc; + Sqlite.Statement stmt; + rc = db.prepare_v2(INSERT_TAG_QUERY, -1, out stmt, null); + if (rc != Sqlite.OK) { + stderr.printf(_("SQL error: %d, %s\n"), rc, db.errmsg()); + return -1; + } + stmt.bind_int(TagColumn.ID.arg(), tag_id); + stmt.bind_text(TagColumn.NAME.arg(), photo.album); + stmt.bind_null(TagColumn.PHOTO_ID_LIST.arg()); + stmt.bind_int64(TagColumn.TIME_CREATED.arg(), Util.now()); + if ((rc = stmt.step()) != Sqlite.DONE) { + stderr.printf(_("Error inserting: %d, %s\n"), rc, db.errmsg()); + return -1; + } + return tag_id; + } + + private static int get_tag_by_name(Photograph photo) { + int rc; + Sqlite.Statement stmt; + rc = db.prepare_v2(SELECT_TAG_NAME_QUERY, -1, out stmt, null); + if (rc != Sqlite.OK) { + stderr.printf(_("SQL error: %d, %s\n"), rc, db.errmsg()); + return -1; + } + stmt.bind_text(1, photo.album); + if (stmt.step() == Sqlite.ROW) + return stmt.column_int(TagColumn.ID); + return create_new_tag(photo); + } + + private static int get_new_photo_id() { + int rc; + Sqlite.Statement stmt; + rc = db.prepare_v2(SELECT_MAX_PHOTO_ID_QUERY, -1, out stmt, null); + if (rc != Sqlite.OK) { + stderr.printf(_("SQL error: %d, %s\n"), rc, db.errmsg()); + return -1; + } + if (stmt.step() == Sqlite.ROW) + return stmt.column_int(0) + 1; + /* Empty table. */ + return 1; + } + + private static string get_photo_md5(Photograph photo) + throws GLib.Error { + var data = new uint8[photo.size]; + var file = photo.file.read(); + size_t size; + file.read_all(data, out size); + if (photo.size != size) + throw new GLib.Error(GLib.Quark.from_string("gqpe"), 0, + _("Cannot read file: %s"), photo.path); + var md5 = GLib.Checksum.compute_for_data(GLib.ChecksumType.MD5, + data); + return md5; + } + + private static int insert_photo(Photograph photo, + int event_id, + int tag_id) throws GLib.Error { + int photo_id = get_new_photo_id(); + if (photo_id < 0) { + stderr.printf(_("Error getting photo id for: %s\n"), + photo.path); + return -1; + } + int rc; + Sqlite.Statement stmt; + rc = db.prepare_v2(INSERT_PHOTO_QUERY, -1, out stmt, null); + if (rc != Sqlite.OK) { + stderr.printf(_("SQL error: %d, %s\n"), rc, db.errmsg()); + return -1; + } + stmt.bind_int(PhotoColumn.ID.arg(), photo_id); + stmt.bind_text(PhotoColumn.FILENAME.arg(), photo.path); + stmt.bind_int(PhotoColumn.WIDTH.arg(), photo.width); + stmt.bind_int(PhotoColumn.HEIGHT.arg(), photo.height); + stmt.bind_int(PhotoColumn.FILESIZE.arg(), photo.size); + stmt.bind_int64(PhotoColumn.TIMESTAMP.arg(), + photo.file_datetime.to_unix()); + stmt.bind_int64(PhotoColumn.EXPOSURE_TIME.arg(), + photo.file_datetime.to_unix()); + stmt.bind_int(PhotoColumn.ORIENTATION.arg(), photo.orientation); + stmt.bind_int(PhotoColumn.ORIGINAL_ORIENTATION.arg(), + photo.orientation); + stmt.bind_int64(PhotoColumn.IMPORT_ID.arg(), export_id); + stmt.bind_int(PhotoColumn.EVENT_ID.arg(), event_id); + stmt.bind_null(PhotoColumn.TRANSFORMATIONS.arg()); + stmt.bind_text(PhotoColumn.MD5.arg(), get_photo_md5(photo)); + stmt.bind_null(PhotoColumn.THUMBNAIL_MD5.arg()); + stmt.bind_null(PhotoColumn.EXIF_MD5.arg()); + stmt.bind_int64(PhotoColumn.TIME_CREATED.arg(), Util.now()); + stmt.bind_int(PhotoColumn.FLAGS.arg(), 0); + stmt.bind_int(PhotoColumn.RATING.arg(), 0); + stmt.bind_int(PhotoColumn.FILE_FORMAT.arg(), 0); + stmt.bind_text(PhotoColumn.TITLE.arg(), photo.title); + stmt.bind_null(PhotoColumn.BACKLINKS.arg()); + stmt.bind_null(PhotoColumn.TIME_REIMPORTED.arg()); + stmt.bind_int(PhotoColumn.EDITABLE_ID.arg(), -1); + stmt.bind_int(PhotoColumn.METADATA_DIRTY.arg(), 0); + stmt.bind_text(PhotoColumn.DEVELOPER.arg(), DEVELOPER); + stmt.bind_int(PhotoColumn.DEVELOP_SHOTWELL_ID.arg(), -1); + stmt.bind_int(PhotoColumn.DEVELOP_CAMERA_ID.arg(), -1); + stmt.bind_int(PhotoColumn.DEVELOP_EMBEDDED_ID.arg(), -1); + if (photo.comment != null && photo.comment != "") + stmt.bind_text(PhotoColumn.COMMENT.arg(), photo.comment); + else + stmt.bind_null(PhotoColumn.COMMENT.arg()); + if ((rc = stmt.step()) != Sqlite.DONE) { + stderr.printf(_("Error inserting: %d, %s\n"), rc, db.errmsg()); + return -1; + } + return photo_id; + } + + private static bool + do_create_thumbnail(Gdk.Pixbuf pb, string path, + string format, int length) throws GLib.Error { + var t = Util.scale_pixbuf(pb, length); + var r = t.save(path, format); + GLib.FileUtils.chmod(path, 6*(8*8) + 0*8 + 0); + return r; + } + + private static bool create_thumbnail(int photo_id, Photograph photo) + throws GLib.Error { + var md5 = GLib.ChecksumType.MD5; + var uri = photo.file.get_uri(); + var png = GLib.Checksum.compute_for_string(md5, uri) + ".png"; + var g_path = string.join(GLib.Path.DIR_SEPARATOR_S, + GLib.Environment.get_user_cache_dir(), + "thumbnails", "large", png); + var jpg = source_id(photo_id) + ".jpg"; + var t128_path = string.join(GLib.Path.DIR_SEPARATOR_S, + GLib.Environment.get_user_cache_dir(), + "shotwell", "thumbs", "thumbs128", jpg); + var t360_path = string.join(GLib.Path.DIR_SEPARATOR_S, + GLib.Environment.get_user_cache_dir(), + "shotwell", "thumbs", "thumbs360", jpg); + bool r = true; + var pb = Util.load_pixbuf(photo); + if (!FileUtils.test(g_path, FileTest.EXISTS)) { + if (verbose) + stderr.printf(_("Generating GNOME thumb: %s\n"), g_path); + r &= do_create_thumbnail(pb, g_path, "png", GTHUMB_LENGTH); + } + if (!FileUtils.test(t128_path, FileTest.EXISTS)) { + if (verbose) + stderr.printf(_("Generating Shotwell 128px thumb: %s\n"), + t128_path); + r &= do_create_thumbnail(pb, t128_path, "jpeg", T128_LENGTH); + } + if (!FileUtils.test(t360_path, FileTest.EXISTS)) { + if (verbose) + stderr.printf(_("Generating Shotwell 360px thumb: %s\n"), + t360_path); + r &= do_create_thumbnail(pb, t360_path, "jpeg", T360_LENGTH); + } + return r; + } + + private static string source_id(int photo_id) { + return "thumb%016x".printf(photo_id); + } + + private static bool event_has_primary_photo(int event_id) { + int rc; + Sqlite.Statement stmt; + rc = db.prepare_v2(SELECT_EVENT_ID_QUERY, -1, out stmt, null); + if (rc != Sqlite.OK) { + stderr.printf(_("SQL error: %d, %s\n"), rc, db.errmsg()); + return true; + } + stmt.bind_int(1, event_id); + if (stmt.step() == Sqlite.ROW) { + var ps = stmt.column_text(EventColumn.PRIMARY_SOURCE_ID); + return ps != null; + } + return false; + } + + private static bool do_update_event(int event_id, int photo_id) { + int rc; + Sqlite.Statement stmt; + rc = db.prepare_v2(UPDATE_EVENT_QUERY, -1, out stmt, null); + if (rc != Sqlite.OK) { + stderr.printf(_("SQL error: %d, %s\n"), rc, db.errmsg()); + return false; + } + var sid = source_id(photo_id); + stmt.bind_text(1, sid); + stmt.bind_int(2, event_id); + return stmt.step() == Sqlite.DONE; + } + + private static bool update_event(int event_id, int photo_id) { + if (event_has_primary_photo(event_id)) + return true; + return do_update_event(event_id, photo_id); + } + + private static Gee.SortedSet get_tag_photos(int tag_id) { + var tag_photos = new Gee.TreeSet(); + int rc; + Sqlite.Statement stmt; + rc = db.prepare_v2(SELECT_TAG_PHOTOS_QUERY, -1, out stmt, null); + if (rc != Sqlite.OK) { + stderr.printf(_("SQL error: %d, %s\n"), rc, db.errmsg()); + return tag_photos; + } + stmt.bind_int(1, tag_id); + if (stmt.step() == Sqlite.ROW) { + var ps = stmt.column_text(0); + if (ps == null) + return tag_photos; + var ids = ps.split(","); + foreach (string id in ids) { + if (id != "") + tag_photos.add(id); + } + } + return tag_photos; + } + + private static bool update_tag(int tag_id, int photo_id) { + var tag_photos = get_tag_photos(tag_id); + int rc; + Sqlite.Statement stmt; + rc = db.prepare_v2(UPDATE_TAG_QUERY, -1, out stmt, null); + if (rc != Sqlite.OK) { + stderr.printf(_("SQL error: %d, %s\n"), rc, db.errmsg()); + return false; + } + tag_photos.add(source_id(photo_id)); + var ids = ""; + foreach (string id in tag_photos) + ids += (id + ","); + stmt.bind_text(1, ids); + stmt.bind_int(2, tag_id); + return stmt.step() == Sqlite.DONE; + } + + private static bool export_photo(Photograph photo) throws GLib.Error { + if (verbose) + stdout.printf(_("Exporting: %s\n"), photo.path); + if (photo_in_db(photo)) { + stderr.printf(_("Already in database: %s, skipping.\n"), + photo.path); + return false; + } + if (photo.title == null || photo.title == "") { + stderr.printf(_("Missing title: %s, skipping.\n"), photo.path); + return false; + } + if (photo.album == null || photo.album == "") { + stderr.printf(_("Missing album: %s, skipping.\n"), photo.path); + return false; + } + int event_id = get_event_by_name(photo); + if (event_id < 0) { + stderr.printf(_("Error getting event for: %s, skipping.\n"), + photo.path); + return false; + } + int tag_id = get_tag_by_name(photo); + if (tag_id < 0) { + stderr.printf(_("Error getting tag for: %s, skipping.\n"), + photo.path); + return false; + } + int photo_id = insert_photo(photo, event_id, tag_id); + if (photo_id < 0) { + stderr.printf(_("Error inserting photograph: %s, skipping.\n"), + photo.path); + return false; + } + if (!create_thumbnail(photo_id, photo)) { + stderr.printf(_("Error making thumbnail for: %s, skipping.\n"), + photo.path); + return false; + } + if (!update_event(event_id, photo_id)) { + stderr.printf(_("Error updating event for: %s, skipping.\n"), + photo.path); + return false; + } + if (!update_tag(tag_id, photo_id)) { + stderr.printf(_("Error updating tag for: %s, skipping.\n"), + photo.path); + return false; + } + return true; + } + + /* Exports the photos. */ + private static int + export_photos(Gee.SortedSet photos) throws GLib.Error { + int c = 0; + export_id = Util.now(); + foreach (var photo in photos) { + if (export_photo(photo)) { + c++; + if (!verbose) { + stdout.printf(_("%d photographs exported…%s"), + c, "\r\b"); + stdout.flush(); + } + } + } + return c; + } + + private static bool open_database() { + var path = string.join(GLib.Path.DIR_SEPARATOR_S, + GLib.Environment.get_user_data_dir(), + "shotwell", "data", "photo.db"); + if (Sqlite.Database.open(path, out db) != Sqlite.OK) { + stderr.printf(_("Cannot open database: %s\n"), db.errmsg()); + db = null; + return false; + } + return true; + } + + private static void do_exporting(string[] args) { + if (!open_database()) + return; + if (db.exec("BEGIN TRANSACTION") != Sqlite.OK) { + stderr.printf(_("Cannot begin transaction: %s\n"), db.errmsg()); + db = null; + return; + } + try { + events = new Gee.TreeMap(); + var photos = !files ? + Util.load_photos_dir(args[1], progress) : + Util.load_photos_array(args, 1, progress); + stdout.printf(_("Exporting %d photographs…\n"), photos.size); + int c = export_photos(photos); + stdout.printf(_("%d photographs exported.\n"), c); + } catch (GLib.Error e) { + if (db.exec("ROLLBACK") != Sqlite.OK) { + stderr.printf(_("Cannot rollback transaction: %s\n"), + db.errmsg()); + } + db = null; + Util.error(_("Error while exporting: %s"), e.message); + } + if (db.exec("COMMIT") != Sqlite.OK) + stderr.printf(_("Cannot commit transaction: %s\n"), + db.errmsg()); + db = null; + } + + private static int search_cover(string year, string month, + string _event, string hl) + throws GLib.Error { + int y = int.parse(year); + int m = int.parse(month); + var s_path = string.join(GLib.Path.DIR_SEPARATOR_S, + year, month, _event, hl); + int rc; + Sqlite.Statement stmt; + rc = db.prepare_v2(SELECT_PHOTO_PATH_LIKE_QUERY, + -1, out stmt, null); + if (rc != Sqlite.OK) { + stderr.printf(_("SQL error: %d, %s\n"), rc, db.errmsg()); + return 0; + } + stmt.bind_text(1, "%" + s_path + "%"); + while (stmt.step() == Sqlite.ROW) { + var photo_id = stmt.column_int(PhotoColumn.ID); + var event_id = stmt.column_int(PhotoColumn.EVENT_ID); + var filename = stmt.column_text(PhotoColumn.FILENAME); + var file = GLib.File.new_for_path(filename); + var photo = new Photograph(file); + if (y == photo.datetime.get_year() && + m == photo.datetime.get_month()) { + if (verbose) + stdout.printf(_("Updating cover for %s…\n"), s_path); + if (do_update_event(event_id, photo_id)) + return 1; + return 0; + } + } + return 0; + } + + private static int parse_covers(Json.Object root) throws GLib.Error { + int c = 0; + var years = root.get_members(); + foreach (string year in years) { + var y = root.get_object_member(year); + if (!y.has_member("months")) + continue; + var ms = y.get_object_member("months"); + var months = ms.get_members(); + foreach (string month in months) { + var m = ms.get_object_member(month); + if (!m.has_member("events")) + continue; + var es = m.get_object_member("events"); + var events = es.get_members(); + foreach (var _event in events) { + var e = es.get_object_member(_event); + if (!e.has_member("highlight")) + continue; + var hl = e.get_string_member("highlight"); + c += search_cover(year, month, _event, hl); + if (!verbose) + stdout.printf(_("%d covers set.%s"), c, "\r\b"); + stdout.flush(); + } + } + } + return c; + } + + private static void do_covers() { + if (!open_database()) + return; + if (db.exec("BEGIN TRANSACTION") != Sqlite.OK) { + stderr.printf(_("Cannot begin transaction: %s\n"), db.errmsg()); + db = null; + return; + } + try { + stdout.printf(_("Setting covers…\n")); + var parser = new Json.Parser(); + parser.load_from_file(covers); + var root = parser.get_root().get_object(); + int c = parse_covers(root); + stdout.printf(_("%d covers set.\n"), c); + } catch (GLib.Error e) { + if (db.exec("ROLLBACK") != Sqlite.OK) { + stderr.printf(_("Cannot rollback transaction: %s\n"), + db.errmsg()); + } + db = null; + Util.error(_("Error while setting primary photos: %s"), + e.message); + } + if (db.exec("COMMIT") != Sqlite.OK) + stderr.printf(_("Cannot commit transaction: %s\n"), + db.errmsg()); + db = null; + } + + public static int main(string[] args) { + GLib.Intl.setlocale(LocaleCategory.ALL, ""); + files = verbose = false; + covers = null; + try { + var opt = new GLib.OptionContext(CONTEXT); + opt.set_help_enabled(true); + opt.add_main_entries(get_options(), null); + opt.parse(ref args); + } catch (GLib.Error e) { + stderr.printf("%s\n", e.message); + Util.error(_("Run ‘%s --help’ for a list of options"), args[0]); + } + + if (files && covers != null) + Util.error(_("You cannot mix -c and -f")); + + if (covers != null) { + if (args.length > 1) + Util.error(_("The -c option only needs one file")); + do_covers(); + return 0; + } + + if (args.length < 2) { + Util.error(_("Missing files or directory")); + } else if (!files) { + if (!GLib.FileUtils.test(args[1], GLib.FileTest.IS_DIR) || + args.length != 2) + Util.error(_("%s is not a directory"), args[1]); + } else if (files) { + for (int i = 1; i < args.length; i++) { + if (!GLib.FileUtils.test(args[i], GLib.FileTest.IS_REGULAR)) + Util.error(_("%s is not a file"), args[i]); + } + } + do_exporting(args); + + return 0; + } + } +} diff --git a/src/tags.vala b/src/tags.vala index dda053bc7ca4b39a18de8cdeb81c434f4b400c8b..e90dc6dec7752db445d5e11b5f408dddd6376862 100644 --- a/src/tags.vala +++ b/src/tags.vala @@ -64,7 +64,7 @@ namespace GQPE { /* The option context. */ private const string CONTEXT = - _("[FILENAME…] - Edit and show the image tags."); + _("{FILENAME…} - Edit and show the image tags."); /* Returns option context. */ private static string get_description() {