Commit c02170e5 authored by Jim Nelson's avatar Jim Nelson

Major overhaul to improve responsiveness of editing photos and generating new...

Major overhaul to improve responsiveness of editing photos and generating new thumbnails.  All modifications to the photo stored in the database are stored in the photo's original, unrotated coordinate system, which means they do not have to be adjusted whenever the image is rotated.  Some refactoring, including making Orientation a powerhouse of its own.
parent 964e75ad
......@@ -166,6 +166,9 @@ public class AppWindow : Gtk.Window {
}
}
public static void terminate() {
}
public static AppWindow get_instance() {
return instance;
}
......@@ -647,9 +650,9 @@ public class AppWindow : Gtk.Window {
PhotoID[] ids = photo_table.get_photos();
// verify photo table
foreach (PhotoID photoID in ids) {
foreach (PhotoID photo_id in ids) {
PhotoRow row = PhotoRow();
photo_table.get_photo(photoID, out row);
photo_table.get_photo(photo_id, out row);
FileInfo info = null;
try {
......@@ -668,7 +671,7 @@ public class AppWindow : Gtk.Window {
message("Time or filesize changed on %s, reimporting ...", row.file.get_path());
Dimensions dim = Dimensions();
Exif.Orientation orientation = Exif.Orientation.TOP_LEFT;
Orientation orientation = Orientation.TOP_LEFT;
time_t exposure_time = 0;
// TODO: Try to read JFIF metadata too
......@@ -695,12 +698,12 @@ public class AppWindow : Gtk.Window {
error("%s", err.message);
}
if (photo_table.update(photoID, dim, info.get_size(), timestamp.tv_sec, exposure_time,
if (photo_table.update(photo_id, dim, info.get_size(), timestamp.tv_sec, exposure_time,
orientation)) {
ThumbnailCache.import(photoID, original, true);
ThumbnailCache.import(photo_id, original, true);
}
}
// verify event table
EventID[] events = event_table.get_events();
foreach (EventID event_id in events) {
......
......@@ -54,6 +54,13 @@ public struct Box {
return Box(rect.x, rect.y, rect.x + rect.width - 1, rect.y + rect.height - 1);
}
// This ensures a proper box is built from the points supplied, no matter the relationship
// between the two points
public static Box from_points(Gdk.Point corner1, Gdk.Point corner2) {
return Box(int.min(corner1.x, corner2.x), int.min(corner1.y, corner2.y),
int.max(corner1.x, corner2.x), int.max(corner1.y, corner2.y));
}
public int get_width() {
assert(right >= left);
......@@ -74,7 +81,24 @@ public struct Box {
return (left == box.left && top == box.top && right == box.right && bottom == box.bottom);
}
public Box get_scaled(Dimensions orig, Dimensions scaled) {
public Box get_scaled(Dimensions scaled) {
double x_scale = (double) scaled.width / (double) get_width();
double y_scale = (double) scaled.height / (double) get_height();
int l = (int) Math.round(left * x_scale);
int t = (int) Math.round(top * y_scale);
// fix-up to match the scaled dimensions
int r = l + scaled.width - 1;
int b = t + scaled.height - 1;
Box box = Box(l, t, r, b);
assert(box.get_width() == scaled.width || box.get_height() == scaled.height);
return box;
}
public Box get_scaled_proportional(Dimensions orig, Dimensions scaled) {
double x_scale = (double) scaled.width / (double) orig.width;
double y_scale = (double) scaled.height / (double) orig.height;
......@@ -92,6 +116,14 @@ public struct Box {
return Dimensions(get_width(), get_height());
}
public void get_points(out Gdk.Point top_left, out Gdk.Point bottom_right) {
top_left.x = left;
top_left.y = top;
bottom_right.x = right;
bottom_right.y = bottom;
}
public Gdk.Rectangle get_rectangle() {
Gdk.Rectangle rect = Gdk.Rectangle();
rect.x = left;
......
......@@ -125,15 +125,8 @@ public class CollectionLayout : Gtk.Layout {
public CollectionLayout() {
modify_bg(Gtk.StateType.NORMAL, AppWindow.BG_COLOR);
// TODO:
// This is commented out because Vala is generating bogus ccode from it (it's trying to
// unref the Gdk.Color as though it was a Gee.Collection) ... I suspect it has to do with
// the SortedList class. Will probably need to rip out SortedList and sort all lists by
// hand ...
/*
Gdk.Color color = parse_color(LayoutItem.UNSELECTED_COLOR);
message.modify_fg(Gtk.StateType.NORMAL, color);
*/
message.set_single_line_mode(false);
message.set_use_underline(false);
......
......@@ -193,8 +193,12 @@ public class CollectionPage : CheckerboardPage {
}
public void add_photo(Photo photo) {
// search for duplicates
if (get_thumbnail_for_photo(photo) != null)
return;
photo.removed += on_photo_removed;
photo.altered += on_photo_altered;
photo.thumbnail_altered += on_thumbnail_altered;
Thumbnail thumbnail = new Thumbnail(photo, scale);
thumbnail.display_title(display_titles());
......@@ -205,26 +209,14 @@ public class CollectionPage : CheckerboardPage {
private void on_photo_removed(Photo photo) {
debug("%s on_photo_removed", get_name());
Thumbnail found = null;
foreach (LayoutItem item in get_items()) {
Thumbnail thumbnail = (Thumbnail) item;
if (thumbnail.get_photo().equals(photo)) {
found = thumbnail;
break;
}
}
// have to remove outside of iterator
Thumbnail found = get_thumbnail_for_photo(photo);
if (found != null) {
debug("Removing %s from %s", photo.to_string(), get_name());
remove_item(found);
}
}
private void on_photo_altered(Photo photo) {
debug("on_photo_altered");
private void on_thumbnail_altered(Photo photo) {
// the thumbnail is only going to reload a low-quality interp, so schedule improval
schedule_thumbnail_improval();
......@@ -233,6 +225,16 @@ public class CollectionPage : CheckerboardPage {
refresh();
}
private Thumbnail? get_thumbnail_for_photo(Photo photo) {
foreach (LayoutItem item in get_items()) {
Thumbnail thumbnail = (Thumbnail) item;
if (thumbnail.get_photo().equals(photo))
return thumbnail;
}
return null;
}
public int increase_thumb_size() {
if (scale == Thumbnail.MAX_SCALE)
return scale;
......@@ -343,7 +345,7 @@ public class CollectionPage : CheckerboardPage {
foreach (LayoutItem item in get_selected()) {
Photo photo = ((Thumbnail) item).get_photo();
photo.removed -= on_photo_removed;
photo.altered -= on_photo_altered;
photo.thumbnail_altered -= on_thumbnail_altered;
photo.remove();
}
......@@ -354,7 +356,7 @@ public class CollectionPage : CheckerboardPage {
refresh();
}
private void do_rotations(Gee.Iterable<LayoutItem> c, Photo.Rotation rotation) {
private void do_rotations(Gee.Iterable<LayoutItem> c, Rotation rotation) {
bool rotation_performed = false;
foreach (LayoutItem item in c) {
Photo photo = ((Thumbnail) item).get_photo();
......@@ -369,15 +371,15 @@ public class CollectionPage : CheckerboardPage {
}
private void on_rotate_clockwise() {
do_rotations(get_selected(), Photo.Rotation.CLOCKWISE);
do_rotations(get_selected(), Rotation.CLOCKWISE);
}
private void on_rotate_counterclockwise() {
do_rotations(get_selected(), Photo.Rotation.COUNTERCLOCKWISE);
do_rotations(get_selected(), Rotation.COUNTERCLOCKWISE);
}
private void on_mirror() {
do_rotations(get_selected(), Photo.Rotation.MIRROR);
do_rotations(get_selected(), Rotation.MIRROR);
}
private void on_view_menu() {
......
......@@ -30,6 +30,9 @@ public class DatabaseTable : Object {
}
}
public static void terminate() {
}
// TODO: errmsg() is global, and so this will not be accurate in a threaded situation
protected static void fatal(string op, int res) {
error("%s: [%d] %s", op, res, db.errmsg());
......@@ -112,7 +115,7 @@ public class PhotoTable : DatabaseTable {
}
public PhotoID add(File file, Dimensions dim, int64 filesize, long timestamp, time_t exposure_time,
Exif.Orientation orientation, ImportID importID) {
Orientation orientation, ImportID importID) {
Sqlite.Statement stmt;
int res = db.prepare_v2(
"INSERT INTO PhotoTable (filename, width, height, filesize, timestamp, exposure_time, orientation, import_id, event_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)",
......@@ -153,7 +156,7 @@ public class PhotoTable : DatabaseTable {
}
public bool update(PhotoID photoID, Dimensions dim, int64 filesize, long timestamp,
time_t exposure_time, Exif.Orientation orientation) {
time_t exposure_time, Orientation orientation) {
Sqlite.Statement stmt;
int res = db.prepare_v2(
......@@ -202,7 +205,7 @@ public class PhotoTable : DatabaseTable {
return (res == Sqlite.ROW);
}
public bool get_photo(PhotoID photoID, out PhotoRow row) {
Sqlite.Statement stmt;
int res = db.prepare_v2("SELECT filename, width, height, filesize, timestamp, exposure_time, orientation, import_id, event_id FROM PhotoTable WHERE id=?", -1, out stmt);
......@@ -221,13 +224,13 @@ public class PhotoTable : DatabaseTable {
row.filesize = stmt.column_int64(3);
row.timestamp = (long) stmt.column_int64(4);
row.exposure_time = (long) stmt.column_int64(5);
row.orientation = (Exif.Orientation) stmt.column_int(6);
row.orientation = (Orientation) stmt.column_int(6);
row.import_id = ImportID(stmt.column_int64(7));
row.event_id = EventID(stmt.column_int64(8));
return true;
}
public File? get_file(PhotoID photoID) {
Sqlite.Statement stmt;
int res = db.prepare_v2("SELECT filename FROM PhotoTable WHERE id=?", -1, out stmt);
......@@ -358,7 +361,7 @@ public class PhotoTable : DatabaseTable {
return Dimensions(stmt.column_int(0), stmt.column_int(1));
}
public Exif.Orientation get_orientation(PhotoID photo_id) {
public Orientation get_orientation(PhotoID photo_id) {
Sqlite.Statement stmt;
int res = db.prepare_v2("SELECT orientation FROM PhotoTable WHERE id=?", -1, out stmt);
assert(res == Sqlite.OK);
......@@ -372,13 +375,13 @@ public class PhotoTable : DatabaseTable {
fatal("get_orientation", res);
}
return Exif.Orientation.TOP_LEFT;
return Orientation.TOP_LEFT;
}
return (Exif.Orientation) stmt.column_int(0);
return (Orientation) stmt.column_int(0);
}
public bool set_orientation(PhotoID photo_id, Exif.Orientation orientation) {
public bool set_orientation(PhotoID photo_id, Orientation orientation) {
Sqlite.Statement stmt;
int res = db.prepare_v2("UPDATE PhotoTable SET orientation = ? WHERE id = ?", -1, out stmt);
assert(res == Sqlite.OK);
......@@ -590,16 +593,16 @@ public class PhotoTable : DatabaseTable {
}
public class ThumbnailCacheTable : DatabaseTable {
private string tableName;
private string table_name;
public ThumbnailCacheTable(int scale) {
assert(scale > 0);
this.tableName = "Thumb%dTable".printf(scale);
this.table_name = "Thumb%dTable".printf(scale);
Sqlite.Statement stmt;
int res = db.prepare_v2("CREATE TABLE IF NOT EXISTS "
+ tableName
+ table_name
+ "("
+ "id INTEGER PRIMARY KEY, "
+ "photo_id INTEGER UNIQUE, "
......@@ -611,21 +614,21 @@ public class ThumbnailCacheTable : DatabaseTable {
res = stmt.step();
if (res != Sqlite.DONE) {
fatal("create thumbnail cache table", res);
fatal("create %s".printf(table_name), res);
}
}
public bool remove(PhotoID photoID) {
public bool remove(PhotoID photo_id) {
Sqlite.Statement stmt;
int res = db.prepare_v2("DELETE FROM %s WHERE photo_id=?".printf(tableName), -1, out stmt);
int res = db.prepare_v2("DELETE FROM %s WHERE photo_id=?".printf(table_name), -1, out stmt);
assert(res == Sqlite.OK);
res = stmt.bind_int64(1, photoID.id);
res = stmt.bind_int64(1, photo_id.id);
assert(res == Sqlite.OK);
res = stmt.step();
if (res != Sqlite.DONE) {
warning("remove photo", res);
warning("%s remove".printf(table_name), res);
return false;
}
......@@ -633,18 +636,18 @@ public class ThumbnailCacheTable : DatabaseTable {
return true;
}
public bool exists(PhotoID photoID) {
public bool exists(PhotoID photo_id) {
Sqlite.Statement stmt;
int res = db.prepare_v2("SELECT id FROM %s WHERE photo_id=?".printf(tableName), -1, out stmt);
int res = db.prepare_v2("SELECT id FROM %s WHERE photo_id=?".printf(table_name), -1, out stmt);
assert(res == Sqlite.OK);
res = stmt.bind_int64(1, photoID.id);
res = stmt.bind_int64(1, photo_id.id);
assert(res == Sqlite.OK);
res = stmt.step();
if (res != Sqlite.ROW) {
if (res != Sqlite.DONE) {
fatal("exists", res);
fatal("%s exists".printf(table_name), res);
}
return false;
......@@ -653,14 +656,14 @@ public class ThumbnailCacheTable : DatabaseTable {
return true;
}
public void add(PhotoID photoID, int filesize, Dimensions dim) {
public void add(PhotoID photo_id, int filesize, Dimensions dim) {
Sqlite.Statement stmt;
int res = db.prepare_v2(
"INSERT INTO %s (photo_id, filesize, width, height) VALUES (?, ?, ?, ?)".printf(tableName),
"INSERT INTO %s (photo_id, filesize, width, height) VALUES (?, ?, ?, ?)".printf(table_name),
-1, out stmt);
assert(res == Sqlite.OK);
res = stmt.bind_int64(1, photoID.id);
res = stmt.bind_int64(1, photo_id.id);
assert(res == Sqlite.OK);
res = stmt.bind_int(2, filesize);
assert(res == Sqlite.OK);
......@@ -671,23 +674,44 @@ public class ThumbnailCacheTable : DatabaseTable {
res = stmt.step();
if (res != Sqlite.DONE) {
fatal("add", res);
fatal("%s add".printf(table_name), res);
}
}
public Dimensions? get_dimensions(PhotoID photoID) {
public void replace(PhotoID photo_id, int filesize, Dimensions dim) {
Sqlite.Statement stmt;
int res = db.prepare_v2("SELECT width, height FROM %s WHERE photo_id=?".printf(tableName),
int res = db.prepare_v2(
"UPDATE %s SET filesize=?, width=?, height=? WHERE photo_id=?".printf(table_name),
-1, out stmt);
assert(res == Sqlite.OK);
res = stmt.bind_int(1, filesize);
assert(res == Sqlite.OK);
res = stmt.bind_int(2, dim.width);
assert(res == Sqlite.OK);
res = stmt.bind_int(3, dim.height);
assert(res == Sqlite.OK);
res = stmt.bind_int64(4, photo_id.id);
assert(res == Sqlite.OK);
res = stmt.step();
if (res != Sqlite.DONE)
fatal("%s replace".printf(table_name), res);
}
public Dimensions? get_dimensions(PhotoID photo_id) {
Sqlite.Statement stmt;
int res = db.prepare_v2("SELECT width, height FROM %s WHERE photo_id=?".printf(table_name),
-1, out stmt);
assert(res == Sqlite.OK);
res = stmt.bind_int64(1, photoID.id);
res = stmt.bind_int64(1, photo_id.id);
assert(res == Sqlite.OK);
res = stmt.step();
if (res != Sqlite.ROW) {
if(res != Sqlite.DONE) {
fatal("get_dimensions", res);
fatal("%s get_dimensions".printf(table_name), res);
}
return null;
......@@ -698,7 +722,7 @@ public class ThumbnailCacheTable : DatabaseTable {
public int get_filesize(PhotoID photoID) {
Sqlite.Statement stmt;
int res = db.prepare_v2("SELECT filesize FROM %s WHERE photo_id=?".printf(tableName),
int res = db.prepare_v2("SELECT filesize FROM %s WHERE photo_id=?".printf(table_name),
-1, out stmt);
assert(res == Sqlite.OK);
......@@ -708,7 +732,7 @@ public class ThumbnailCacheTable : DatabaseTable {
res = stmt.step();
if (res != Sqlite.ROW) {
if (res != Sqlite.DONE) {
fatal("get_filesize", res);
fatal("%s get_filesize".printf(table_name), res);
}
return -1;
......@@ -943,7 +967,7 @@ public struct PhotoRow {
public int64 filesize;
public long timestamp;
public long exposure_time;
public Exif.Orientation orientation;
public Orientation orientation;
public ImportID import_id;
public EventID event_id;
}
......
......@@ -87,35 +87,6 @@ public struct Dimensions {
return scaled;
}
public Dimensions get_rotated(Exif.Orientation orientation) {
int w = width;
int h = height;
switch(orientation) {
case Exif.Orientation.TOP_LEFT:
case Exif.Orientation.TOP_RIGHT:
case Exif.Orientation.BOTTOM_RIGHT:
case Exif.Orientation.BOTTOM_LEFT: {
// fine just as it is
} break;
case Exif.Orientation.LEFT_TOP:
case Exif.Orientation.RIGHT_TOP:
case Exif.Orientation.RIGHT_BOTTOM:
case Exif.Orientation.LEFT_BOTTOM: {
// swap
w = height;
h = width;
} break;
default: {
error("Unknown orientation: %d", orientation);
} break;
}
return Dimensions(w, h);
}
public Gdk.Rectangle get_scaled_rectangle(Dimensions scale, Gdk.Rectangle rect) {
double x_scale = (double) scale.width / (double) width;
double y_scale = (double) scale.height / (double) height;
......
......@@ -15,7 +15,9 @@ public class DirectoryItem : LayoutItem {
assert(photo_id.is_valid());
Photo photo = Photo.fetch(photo_id);
Gdk.Pixbuf pixbuf = photo.get_scaled_thumbnail(SCALE, INTERP);
Gdk.Pixbuf pixbuf = photo.get_thumbnail(SCALE);
pixbuf = scale_pixbuf(pixbuf, SCALE, INTERP);
image.set_from_pixbuf(pixbuf);
image.set_size_request(pixbuf.get_width(), pixbuf.get_height());
}
......
namespace Exif {
// "Exif"
public static const uint8[] SIGNATURE = { 0x45, 0x78, 0x69, 0x66 };
public enum Orientation {
TOP_LEFT = 1,
TOP_RIGHT = 2,
BOTTOM_RIGHT = 3,
BOTTOM_LEFT = 4,
LEFT_TOP = 5,
RIGHT_TOP = 6,
RIGHT_BOTTOM = 7,
LEFT_BOTTOM = 8;
public string get_description() {
switch (this) {
case TOP_LEFT:
return "top-left";
case TOP_RIGHT:
return "top-right";
case BOTTOM_RIGHT:
return "bottom-right";
case BOTTOM_LEFT:
return "bottom-left";
case LEFT_TOP:
return "left-top";
case RIGHT_TOP:
return "right-top";
case RIGHT_BOTTOM:
return "right-bottom";
case LEFT_BOTTOM:
return "left-bottom";
default:
return "unknown orientation %d".printf((int) this);
}
}
public Orientation rotate_clockwise() {
switch (this) {
case TOP_LEFT:
return RIGHT_TOP;
case TOP_RIGHT:
return RIGHT_BOTTOM;
case BOTTOM_RIGHT:
return LEFT_BOTTOM;
case BOTTOM_LEFT:
return LEFT_TOP;
case LEFT_TOP:
return TOP_RIGHT;
case RIGHT_TOP:
return BOTTOM_RIGHT;
case RIGHT_BOTTOM:
return BOTTOM_LEFT;
case LEFT_BOTTOM:
return TOP_LEFT;
default: {
error("rotate_clockwise: %d", this);
return this;
}
}
}
public Orientation rotate_counterclockwise() {
switch (this) {
case TOP_LEFT:
return LEFT_BOTTOM;
case TOP_RIGHT:
return LEFT_TOP;
case BOTTOM_RIGHT:
return RIGHT_TOP;
case BOTTOM_LEFT:
return RIGHT_BOTTOM;
case LEFT_TOP:
return BOTTOM_LEFT;
case RIGHT_TOP:
return TOP_LEFT;
case RIGHT_BOTTOM:
return TOP_RIGHT;
case LEFT_BOTTOM:
return BOTTOM_RIGHT;
default: {
error("rotate_counterclockwise: %d", this);
return this;
}
}
}
public Orientation flip_top_to_bottom() {
switch (this) {
case TOP_LEFT:
return BOTTOM_LEFT;
case TOP_RIGHT:
return BOTTOM_RIGHT;
case BOTTOM_RIGHT:
return TOP_RIGHT;
case BOTTOM_LEFT:
return TOP_LEFT;
case LEFT_TOP:
return RIGHT_TOP;
case RIGHT_TOP:
return LEFT_TOP;
case RIGHT_BOTTOM:
return LEFT_BOTTOM;
case LEFT_BOTTOM:
return RIGHT_BOTTOM;
default: {
error("flip_top_to_bottom: %d", this);
return this;
}
}
}
public Orientation flip_left_to_right() {
switch (this) {
case TOP_LEFT:
return TOP_RIGHT;
case TOP_RIGHT:
return TOP_LEFT;
case BOTTOM_RIGHT:
return BOTTOM_LEFT;
case BOTTOM_LEFT:
return BOTTOM_RIGHT;
case LEFT_TOP:
return RIGHT_TOP;
case RIGHT_TOP:
return LEFT_TOP;
case RIGHT_BOTTOM:
return LEFT_BOTTOM;
case LEFT_BOTTOM:
return RIGHT_BOTTOM;
default: {
error("flip_left_to_right: %d", this);
return this;
}
}
}
}
public static const int ORIENTATION_MIN = 1;
public static const int ORIENTATION_MAX = 8;
public Exif.Entry? find_first_entry(Data data, Exif.Tag tag, Exif.Format format) {
for (int ctr = 0; ctr < (int) Exif.Ifd.COUNT; ctr++) {
Exif.Content content = data.ifd[ctr];
......@@ -288,21 +106,21 @@ public class PhotoExif {
return false;
}
public Exif.Orientation get_orientation() {
public Orientation get_orientation() {
update();
Exif.Entry entry = find_entry(Exif.Ifd.ZERO, Exif.Tag.ORIENTATION, Exif.Format.SHORT);
if (entry == null)
return Exif.Orientation.TOP_LEFT;
return Orientation.TOP_LEFT;
int o = Exif.Convert.get_short(entry.data, exifData.get_byte_order());
assert(o >= Exif.ORIENTATION_MIN);
assert(o <= Exif.ORIENTATION_MAX);
assert(o >= (int) Orientation.MIN);
assert(o <= (int) Orientation.MAX);
return (Exif.Orientation) o;
return (Orientation) o;
}
public void set_orientation(Exif.Orientation orientation) {
public void set_orientation(Orientation orientation) {
update();
Exif.Entry entry = find_first_entry(Exif.Tag.ORIENTATION, Exif.Format.SHORT);
......
......@@ -6,7 +6,7 @@ class ImportPreview : LayoutItem {
private ImportPage parentPage;
private Exif.Data exif;
private Exif.Orientation orientation = Exif.Orientation.TOP_LEFT;
private Orientation orientation = Orientation.TOP_LEFT;
public ImportPreview(ImportPage parentPage, Gdk.Pixbuf pixbuf, Exif.Data exif, string folder,
string filename) {
......@@ -18,15 +18,15 @@ class ImportPreview : LayoutItem {
Exif.Entry entry = Exif.find_first_entry(exif, Exif.Tag.ORIENTATION, Exif.Format.SHORT);
if (entry != null) {
int o = Exif.Convert.get_short(entry.data, exif.get_byte_order());
assert(o >= Exif.ORIENTATION_MIN);
assert(o <= Exif.ORIENTATION_MAX);
assert(o >= (int) Orientation.MIN);
assert(o <= (int) Orientation.MAX);
orientation = (Exif.Orientation) o;