Commit 89d8dd31 authored by Jim Nelson's avatar Jim Nelson

#72: Duplicate photo implemented.

parent 8d70f98f
......@@ -86,7 +86,8 @@ SRC_FILES = \
FacebookConnector.vala \
CommandManager.vala \
Commands.vala \
SlideshowPage.vala
SlideshowPage.vala \
LibraryFiles.vala
ifndef LINUX
SRC_FILES += \
......
......@@ -96,8 +96,6 @@ public class ImportManifest {
// it into the system, including database additions and thumbnail creation. It can be monitored by
// multiple observers, but only one ImportReporter can be registered.
public class BatchImport {
public const int IMPORT_DIRECTORY_DEPTH = 3;
private Gee.Iterable<BatchImportJob> jobs;
private string name;
private uint64 total_bytes;
......@@ -140,92 +138,6 @@ public class BatchImport {
this.skip_every = get_test_variable("SHOTWELL_SKIP_EVERY");
}
public static File? create_library_path(string filename, Exif.Data? exif, time_t ts,
out bool collision) {
File dir = AppDirs.get_photos_dir();
time_t timestamp = ts;
// use EXIF exposure timestamp over the supplied one (which probably comes from the file's
// modified time, or is simply now())
if (exif != null && !Exif.get_timestamp(exif, out timestamp)) {
// if no exposure time supplied, use now()
if (ts == 0)
timestamp = time_t();
}
Time tm = Time.local(timestamp);
// build a directory tree inside the library, as deep as IMPORT_DIRECTORY_DEPTH:
// yyyy/mm/dd
dir = dir.get_child("%04u".printf(tm.year + 1900));
dir = dir.get_child("%02u".printf(tm.month + 1));
dir = dir.get_child("%02u".printf(tm.day));
try {
if (!dir.query_exists(null))
dir.make_directory_with_parents(null);
} catch (Error err) {
error("Unable to create photo library directory %s", dir.get_path());
}
// if file doesn't exist, use that and done
File file = dir.get_child(filename);
if (!file.query_exists(null)) {
collision = false;
return file;
}
collision = true;
string name, ext;
disassemble_filename(file.get_basename(), out name, out ext);
// generate a unique filename
for (int ctr = 1; ctr < int.MAX; ctr++) {
string new_name = (ext != null) ? "%s_%d.%s".printf(name, ctr, ext) : "%s_%d".printf(name, ctr);
file = dir.get_child(new_name);
if (!file.query_exists(null))
return file;
}
return null;
}
private static ImportResult copy_file(File src, out File dest) {
PhotoExif exif = new PhotoExif(src);
time_t timestamp = 0;
try {
timestamp = query_file_modified(src);
} catch (Error err) {
critical("Unable to access file modification for %s: %s", src.get_path(), err.message);
}
bool collision;
dest = create_library_path(src.get_basename(), exif.get_exif(), timestamp, out collision);
if (dest == null)
return ImportResult.FILE_ERROR;
debug("Copying %s to %s", src.get_path(), dest.get_path());
try {
src.copy(dest, FileCopyFlags.ALL_METADATA, null, on_copy_progress);
} catch (Error err) {
critical("Unable to copy file %s to %s: %s", src.get_path(), dest.get_path(),
err.message);
return ImportResult.FILE_ERROR;
}
return ImportResult.SUCCESS;
}
private static void on_copy_progress(int64 current, int64 total) {
spin_event_loop();
}
private static int get_test_variable(string name) {
string value = Environment.get_variable(name);
if (value == null || value.length == 0)
......@@ -416,10 +328,15 @@ public class BatchImport {
bool is_in_library_dir = file.has_prefix(AppDirs.get_photos_dir());
if (copy_to_library && !is_in_library_dir) {
File copied;
ImportResult result = copy_file(file, out copied);
if (result != ImportResult.SUCCESS)
return result;
File copied = null;
try {
copied = LibraryFiles.duplicate(file);
} catch (Error err) {
critical("Unable to duplicate file %s: %s", file.get_path(), err.message);
}
if (copied == null)
return ImportResult.FILE_ERROR;
debug("Copied %s into library at %s", file.get_path(), copied.get_path());
......
......@@ -276,6 +276,12 @@ public class CollectionPage : CheckerboardPage {
revert.label = Resources.REVERT_MENU;
revert.tooltip = Resources.REVERT_TOOLTIP;
actions += revert;
Gtk.ActionEntry duplicate = { "Duplicate", null, TRANSLATABLE, "<Ctrl>D", TRANSLATABLE,
on_duplicate_photo };
duplicate.label = Resources.DUPLICATE_PHOTO_MENU;
duplicate.tooltip = Resources.DUPLICATE_PHOTO_TOOLTIP;
actions += duplicate;
Gtk.ActionEntry slideshow = { "Slideshow", Gtk.STOCK_MEDIA_PLAY, TRANSLATABLE, "F5",
TRANSLATABLE, on_slideshow };
......@@ -403,6 +409,7 @@ public class CollectionPage : CheckerboardPage {
bool selected = get_view().get_selected_count() > 0;
bool revert_possible = can_revert_selected();
set_item_sensitive("/CollectionContextMenu/ContextDuplicate", selected);
set_item_sensitive("/CollectionContextMenu/ContextRemove", selected);
set_item_sensitive("/CollectionContextMenu/ContextNewEvent", selected);
set_item_sensitive("/CollectionContextMenu/ContextRotateClockwise", selected);
......@@ -662,6 +669,7 @@ public class CollectionPage : CheckerboardPage {
set_item_sensitive("/CollectionMenuBar/PhotosMenu/Mirror", selected);
set_item_sensitive("/CollectionMenuBar/PhotosMenu/Enhance", selected);
set_item_sensitive("/CollectionMenuBar/PhotosMenu/Revert", selected && revert_possible);
set_item_sensitive("/CollectionMenuBar/PhotosMenu/Duplicate", selected);
set_item_sensitive("/CollectionMenuBar/PhotosMenu/Slideshow", get_view().get_count() > 0);
}
......@@ -783,6 +791,15 @@ public class CollectionPage : CheckerboardPage {
EnhanceMultipleCommand command = new EnhanceMultipleCommand(get_view().get_selected());
get_command_manager().execute(command);
}
private void on_duplicate_photo() {
if (get_view().get_selected_count() == 0)
return;
DuplicateMultiplePhotosCommand command = new DuplicateMultiplePhotosCommand(
get_view().get_selected());
get_command_manager().execute(command);
}
private void on_slideshow() {
if (get_view().get_count() == 0)
......
......@@ -631,3 +631,53 @@ public class MergeEventsCommand : Command {
}
}
public class DuplicateMultiplePhotosCommand : MultipleDataSourceCommand {
private Gee.HashMap<LibraryPhoto, LibraryPhoto> dupes = new Gee.HashMap<LibraryPhoto, LibraryPhoto>();
private int failed = 0;
public DuplicateMultiplePhotosCommand(Gee.Iterable<DataView> iter) {
base (iter, _("Duplicating photos..."), _("Removing duplicated photos..."),
Resources.DUPLICATE_PHOTO_LABEL, Resources.DUPLICATE_PHOTO_TOOLTIP);
}
public override void execute() {
dupes.clear();
failed = 0;
base.execute();
if (failed > 0)
AppWindow.error_message(_("Unable to duplicate %d photos due to file errors.").printf(failed));
}
public override void execute_on_source(DataSource source) {
LibraryPhoto photo = (LibraryPhoto) source;
try {
LibraryPhoto dupe = photo.duplicate();
dupes.set(photo, dupe);
} catch (Error err) {
critical("Unable to duplicate file %s: %s", photo.get_file().get_path(), err.message);
failed++;
}
}
public override void undo() {
base.undo();
// be sure to drop everything that was destroyed
dupes.clear();
failed = 0;
}
public override void undo_on_source(DataSource source) {
LibraryPhoto photo = (LibraryPhoto) source;
LibraryPhoto dupe = dupes.get(photo);
dupe.delete_original_on_destroy();
Marker marker = LibraryPhoto.global.mark(dupe);
LibraryPhoto.global.destroy_marked(marker);
}
}
......@@ -508,6 +508,61 @@ public class PhotoTable : DatabaseTable {
return all;
}
// Create a duplicate of the specified row. A new byte-for-byte duplicate of PhotoID's file
// needs to back this duplicate.
public PhotoID duplicate(PhotoID photo_id, string new_filename) {
// get a copy of the original row, duplicating most (but not all) of it
PhotoRow original = get_row(photo_id);
Sqlite.Statement stmt;
int res = db.prepare_v2("INSERT INTO PhotoTable (filename, width, height, filesize, timestamp, "
+ "exposure_time, orientation, original_orientation, import_id, event_id, transformations, "
+ "md5, thumbnail_md5, exif_md5, time_created) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
-1, out stmt);
assert(res == Sqlite.OK);
res = stmt.bind_text(1, new_filename);
assert(res == Sqlite.OK);
res = stmt.bind_int(2, original.dim.width);
assert(res == Sqlite.OK);
res = stmt.bind_int(3, original.dim.height);
assert(res == Sqlite.OK);
res = stmt.bind_int64(4, original.filesize);
assert(res == Sqlite.OK);
res = stmt.bind_int64(5, original.timestamp);
assert(res == Sqlite.OK);
res = stmt.bind_int64(6, original.exposure_time);
assert(res == Sqlite.OK);
res = stmt.bind_int(7, original.orientation);
assert(res == Sqlite.OK);
res = stmt.bind_int(8, original.original_orientation);
assert(res == Sqlite.OK);
res = stmt.bind_int64(9, original.import_id.id);
assert(res == Sqlite.OK);
res = stmt.bind_int64(10, original.event_id.id);
assert(res == Sqlite.OK);
res = stmt.bind_text(11, unmarshall_all_transformations(original.transformations));
assert(res == Sqlite.OK);
res = stmt.bind_text(12, original.md5);
assert(res == Sqlite.OK);
res = stmt.bind_text(13, original.thumbnail_md5);
assert(res == Sqlite.OK);
res = stmt.bind_text(14, original.exif_md5);
assert(res == Sqlite.OK);
res = stmt.bind_int64(15, now_sec());
assert(res == Sqlite.OK);
res = stmt.step();
if (res != Sqlite.DONE) {
if (res != Sqlite.CONSTRAINT)
fatal("duplicate", res);
return PhotoID();
}
return PhotoID(db.last_insert_rowid());
}
public bool exists(PhotoID photo_id) {
return exists_by_id(photo_id.id);
}
......@@ -1003,6 +1058,12 @@ public class PhotoTable : DatabaseTable {
}
}
public struct ThumbnailCacheRow {
PhotoID photo_id;
Dimensions dim;
int filesize;
}
public class ThumbnailCacheTable : DatabaseTable {
public ThumbnailCacheTable(int scale) {
assert(scale > 0);
......@@ -1084,6 +1145,41 @@ public class ThumbnailCacheTable : DatabaseTable {
fatal("%s add".printf(table_name), res);
}
public ThumbnailCacheRow? get_row(PhotoID photo_id) {
Sqlite.Statement stmt;
int res = db.prepare_v2("SELECT width, height, filesize FROM %s WHERE photo_id=?".printf(table_name),
-1, out stmt);
assert(res == Sqlite.OK);
res = stmt.bind_int64(1, photo_id.id);
assert(res == Sqlite.OK);
res = stmt.step();
if (res != Sqlite.ROW) {
if (res != Sqlite.DONE)
fatal("%s get_row".printf(table_name), res);
return null;
}
ThumbnailCacheRow row = ThumbnailCacheRow();
row.photo_id = photo_id;
row.dim = Dimensions(stmt.column_int(0), stmt.column_int(1));
row.filesize = stmt.column_int(2);
return row;
}
public void duplicate(PhotoID src_id, PhotoID dest_id) {
// copy
ThumbnailCacheRow? row = get_row(src_id);
if (row == null)
error("Unable to duplicate thumbnail cache row %lld", src_id.id);
// paste
add(dest_id, row.filesize, row.dim);
}
public void replace(PhotoID photo_id, int filesize, Dimensions dim) {
Sqlite.Statement stmt;
int res = db.prepare_v2(
......
......@@ -503,7 +503,7 @@ public class PhotoExif {
update();
return null;
return exif;
}
public void set_exif(Exif.Data exif) {
......
......@@ -868,7 +868,7 @@ public class ImportPage : CheckerboardPage {
}
bool collision;
File dest_file = BatchImport.create_library_path(import_file.get_filename(),
File dest_file = LibraryFiles.generate_unique_file(import_file.get_filename(),
import_file.get_exif(), time_t(), out collision);
if (dest_file == null) {
message("Unable to generate local file for %s", import_file.get_filename());
......
/* Copyright 2009 Yorba Foundation
*
* This software is licensed under the GNU Lesser General Public License
* (version 2.1 or later). See the COPYING file in this distribution.
*/
namespace LibraryFiles {
public const int DIRECTORY_DEPTH = 3;
public File? generate_unique_file(string filename, Exif.Data? exif, time_t ts, out bool collision) {
File dir = AppDirs.get_photos_dir();
time_t timestamp = ts;
// use EXIF exposure timestamp over the supplied one (which probably comes from the file's
// modified time, or is simply now())
if (exif != null && !Exif.get_timestamp(exif, out timestamp)) {
// if no exposure time supplied, use now()
if (ts == 0)
timestamp = time_t();
}
Time tm = Time.local(timestamp);
// build a directory tree inside the library, as deep as DIRECTORY_DEPTH:
// yyyy/mm/dd
dir = dir.get_child("%04u".printf(tm.year + 1900));
dir = dir.get_child("%02u".printf(tm.month + 1));
dir = dir.get_child("%02u".printf(tm.day));
try {
if (!dir.query_exists(null))
dir.make_directory_with_parents(null);
} catch (Error err) {
error("Unable to create photo library directory %s", dir.get_path());
}
// if file doesn't exist, use that and done
File file = dir.get_child(filename);
if (!file.query_exists(null)) {
collision = false;
return file;
}
collision = true;
string name, ext;
disassemble_filename(file.get_basename(), out name, out ext);
// generate a unique filename
for (int ctr = 1; ctr < int.MAX; ctr++) {
string new_name = (ext != null) ? "%s_%d.%s".printf(name, ctr, ext) : "%s_%d".printf(name, ctr);
file = dir.get_child(new_name);
if (!file.query_exists(null))
return file;
}
return null;
}
private File duplicate(File src) throws Error {
time_t timestamp = 0;
try {
timestamp = query_file_modified(src);
} catch (Error err) {
critical("Unable to access file modification for %s: %s", src.get_path(), err.message);
}
PhotoExif exif = new PhotoExif(src);
bool collision;
File? dest = generate_unique_file(src.get_basename(), exif.get_exif(), timestamp, out collision);
if (dest == null)
throw new FileError.FAILED("Unable to generate unique pathname for destination");
debug("Copying %s to %s", src.get_path(), dest.get_path());
src.copy(dest, FileCopyFlags.ALL_METADATA, null, on_copy_progress);
return dest;
}
private void on_copy_progress(int64 current, int64 total) {
spin_event_loop();
}
}
......@@ -1797,6 +1797,26 @@ public class LibraryPhoto : TransformablePhoto {
return ThumbnailCache.fetch(get_photo_id(), scale);
}
public LibraryPhoto duplicate() throws Error {
// clone the backing file
File dupe_file = LibraryFiles.duplicate(get_file());
// clone the row in the database so another that relies on this new backing file
PhotoID dupe_id = PhotoTable.get_instance().duplicate(get_photo_id(), dupe_file.get_path());
PhotoRow dupe_row = PhotoTable.get_instance().get_row(dupe_id);
// clone thumbnails
ThumbnailCache.duplicate(get_photo_id(), dupe_id);
// build the DataSource for the duplicate
LibraryPhoto dupe = new LibraryPhoto(dupe_row);
// add it to the SourceCollection; this notifies everyone interested of its presence
global.add(dupe);
return dupe;
}
public void delete_original_on_destroy() {
delete_original = true;
}
......@@ -1853,7 +1873,7 @@ public class LibraryPhoto : TransformablePhoto {
// inside the user's Pictures directory
if (file.has_prefix(AppDirs.get_photos_dir())) {
File parent = file;
for (int depth = 0; depth < BatchImport.IMPORT_DIRECTORY_DEPTH; depth++) {
for (int depth = 0; depth < LibraryFiles.DIRECTORY_DEPTH; depth++) {
parent = parent.get_parent();
if (parent == null)
break;
......
......@@ -145,11 +145,12 @@ private class BasicProperties : Properties {
dimensions = photo_source.get_dimensions();
Exif.Data exif = photo_source.get_exif();
exposure = Exif.get_exposure(exif);
aperture = Exif.get_aperture(exif);
iso = Exif.get_iso(exif);
Exif.Data? exif = photo_source.get_exif();
if (exif != null) {
exposure = Exif.get_exposure(exif);
aperture = Exif.get_aperture(exif);
iso = Exif.get_iso(exif);
}
} else if (source is EventSource) {
EventSource event_source = (EventSource) source;
......
......@@ -116,6 +116,10 @@ along with Shotwell; if not, write to the Free Software Foundation, Inc.,
public const string MERGE_MENU = _("_Merge Events");
public const string MERGE_LABEL = _("Merge");
public const string MERGE_TOOLTIP = _("Merge into a single event");
public const string DUPLICATE_PHOTO_MENU = _("_Duplicate");
public const string DUPLICATE_PHOTO_LABEL = _("Duplicate");
public const string DUPLICATE_PHOTO_TOOLTIP = _("Make a duplicate of the photo");
private Gtk.IconFactory factory = null;
......
......@@ -147,6 +147,14 @@ public class ThumbnailCache : Object {
spin_event_loop();
}
public static void duplicate(PhotoID src_id, PhotoID dest_id) {
big._duplicate(src_id, dest_id);
spin_event_loop();
medium._duplicate(src_id, dest_id);
spin_event_loop();
}
public static void remove(PhotoID photo_id) {
big._remove(photo_id);
spin_event_loop();
......@@ -336,6 +344,25 @@ public class ThumbnailCache : Object {
cache_table.add(photo_id, filesize, Dimensions.for_pixbuf(scaled));
}
private void _duplicate(PhotoID src_id, PhotoID dest_id) {
File src_file = get_cached_file(src_id);
File dest_file = get_cached_file(dest_id);
debug("Duplicating thumbnail for %s [%lld] to %s [%lld]", photo_table.get_name(src_id),
src_id.id, photo_table.get_name(dest_id), dest_id.id);
try {
src_file.copy(dest_file, FileCopyFlags.ALL_METADATA, null, null);
} catch (Error err) {
error("%s", err.message);
}
// Do NOT store in memory cache, for similar reasons as stated in _import().
// duplicate in the database
cache_table.duplicate(src_id, dest_id);
}
private void _replace(PhotoID photo_id, Gdk.Pixbuf original) throws Error {
File file = get_cached_file(photo_id);
......
......@@ -47,6 +47,8 @@
<menuitem name="Revert" action="Revert" />
<menuitem name="Enhance" action="Enhance" />
<separator />
<menuitem name="Duplicate" action="Duplicate" />
<separator />
<menuitem name="Slideshow" action="Slideshow" />
</menu>
......@@ -62,6 +64,7 @@
</menubar>
<popup name="CollectionContextMenu">
<menuitem name="ContextDuplicate" action="Duplicate" />
<menuitem name="ContextRemove" action="Remove" />
<separator />
<menuitem name="ContextRotateClockwise" action="RotateClockwise" />
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment