Commit fe7822f8 authored by Jim Nelson's avatar Jim Nelson

JPEG/EXIF rotations implemented and stored in the photo file itself. Further...

JPEG/EXIF rotations implemented and stored in the photo file itself.  Further work to be done before marking off 
the ticket.
parent 48241be1
......@@ -149,7 +149,8 @@ public class AppWindow : Gtk.Window {
}
collectionPage = new CollectionPage();
photoPage = new PhotoPage();
//photoPage = new PhotoPage();
add_accel_group(uiManager.get_accel_group());
switch_to_collection_page();
}
......@@ -274,7 +275,7 @@ public class AppWindow : Gtk.Window {
photoPage.get_menubar(), photoPage.get_toolbar());
}
private Gtk.ActionGroup oldActionGroup = null;
//private Gtk.ActionGroup oldActionGroup = null;
private void switch_to_page(Gtk.Widget page, Gtk.ActionGroup actionGroup, Gtk.MenuBar menubar,
Gtk.Toolbar toolbar) {
......@@ -283,6 +284,7 @@ public class AppWindow : Gtk.Window {
layout = null;
}
/*
if (oldActionGroup != null) {
remove_accel_group(uiManager.get_accel_group());
uiManager.remove_action_group(oldActionGroup);
......@@ -290,8 +292,8 @@ public class AppWindow : Gtk.Window {
}
uiManager.insert_action_group(actionGroup, 0);
add_accel_group(uiManager.get_accel_group());
oldActionGroup = actionGroup;
*/
// layout the growable collection page with the toolbar beneath
Gtk.VBox pageBox = new Gtk.VBox(false, 0);
......
......@@ -45,6 +45,9 @@ public class CollectionLayout : Gtk.Layout {
}
public void refresh() {
if (thumbnails.size == 0)
return;
// don't bother until layout is of some appreciable size
if (allocation.width <= 1)
return;
......
......@@ -13,7 +13,8 @@ public class CollectionPage : Gtk.ScrolledWindow {
private PhotoTable photoTable = new PhotoTable();
private CollectionLayout layout = new CollectionLayout();
private Gtk.ActionGroup actionGroup = new Gtk.ActionGroup("CollectionActionGroup");
private Gtk.ActionGroup mainActionGroup = new Gtk.ActionGroup("CollectionActionGroup");
private Gtk.ActionGroup contextActionGroup = new Gtk.ActionGroup("CollectionContextActionGroup");
private Gtk.MenuBar menubar = null;
private Gtk.Toolbar toolbar = new Gtk.Toolbar();
private Gtk.HScale slider = null;
......@@ -41,13 +42,19 @@ public class CollectionPage : Gtk.ScrolledWindow {
// TODO: Mark fields for translation
private const Gtk.ActionEntry[] RIGHT_CLICK_ACTIONS = {
{ "Remove", Gtk.STOCK_DELETE, "_Remove", "Delete", "Remove the selected photos from the library", on_remove }
{ "Remove", Gtk.STOCK_DELETE, "_Remove", "Delete", "Remove the selected photos from the library", on_remove },
{ "RotateClockwise", null, "Rotate c_lockwise", "<Ctrl>R", "Rotate the selected photos clockwise", on_rotate_clockwise },
{ "RotateCounterclockwise", null, "Rotate c_ounterclockwise", "<Ctrl><Shift>R", "Rotate the selected photos counterclockwise", on_rotate_counterclockwise },
{ "Mirror", null, "_Mirror", "<Ctrl>M", "Make mirror images of the selected photos", on_mirror }
};
construct {
// set up action group
actionGroup.add_actions(ACTIONS, this);
actionGroup.add_actions(RIGHT_CLICK_ACTIONS, this);
mainActionGroup.add_actions(ACTIONS, this);
AppWindow.get_ui_manager().insert_action_group(mainActionGroup, 0);
contextActionGroup.add_actions(RIGHT_CLICK_ACTIONS, this);
AppWindow.get_ui_manager().insert_action_group(contextActionGroup, 0);
// this page's menu bar
menubar = (Gtk.MenuBar) AppWindow.get_ui_manager().get_widget("/CollectionMenuBar");
......@@ -105,7 +112,7 @@ public class CollectionPage : Gtk.ScrolledWindow {
}
public Gtk.ActionGroup get_action_group() {
return actionGroup;
return mainActionGroup;
}
public void begin_adding() {
......@@ -273,8 +280,13 @@ public class CollectionPage : Gtk.ScrolledWindow {
}
private void on_photos_menu() {
bool selected = (get_selected_count() > 0);
set_item_sensitive("/CollectionMenuBar/PhotosMenu/PhotosIncreaseSize", scale < Thumbnail.MAX_SCALE);
set_item_sensitive("/CollectionMenuBar/PhotosMenu/PhotosDecreaseSize", scale > Thumbnail.MIN_SCALE);
set_item_sensitive("/CollectionMenuBar/PhotosMenu/RotateClockwise", selected);
set_item_sensitive("/CollectionMenuBar/PhotosMenu/RotateCounterclockwise", selected);
set_item_sensitive("/CollectionMenuBar/PhotosMenu/Mirror", selected);
}
private void on_increase_size() {
......@@ -360,11 +372,10 @@ public class CollectionPage : Gtk.ScrolledWindow {
Thumbnail thumbnail = layout.get_thumbnail_at(event.x, event.y);
if (thumbnail != null) {
// this counts as a select
// this counts as a select with all others de-selected
unselect_all();
select(thumbnail);
}
if (get_selected_count() > 0) {
Gtk.Menu contextMenu = (Gtk.Menu) AppWindow.get_ui_manager().get_widget("/CollectionContextMenu");
contextMenu.popup(null, null, null, event.button, event.time);
......@@ -391,6 +402,41 @@ public class CollectionPage : Gtk.ScrolledWindow {
layout.refresh();
}
private delegate Exif.Orientation RotationFunc(Exif.Orientation orientation);
private void do_rotations(string desc, Gee.Collection<Thumbnail> c, RotationFunc func) {
foreach (Thumbnail thumbnail in c) {
Exif.Orientation orientation = thumbnail.get_orientation();
Exif.Orientation rotated = func(orientation);
debug("Rotating %s %s from %s to %s", desc, thumbnail.get_file().get_path(),
orientation.get_description(), rotated.get_description());
thumbnail.set_orientation(rotated);
}
if (c.size > 0) {
schedule_thumbnail_improval();
layout.refresh();
}
}
private void on_rotate_clockwise() {
do_rotations("clockwise", selectedList, (orientation) => {
return orientation.rotate_clockwise();
});
}
private void on_rotate_counterclockwise() {
do_rotations("counterclockwise", selectedList, (orientation) => {
return orientation.rotate_counterclockwise();
});
}
private void on_mirror() {
do_rotations("mirror", selectedList, (orientation) => {
return orientation.flip_left_to_right();
});
}
private double scaleToSlider(int value) {
assert(value >= Thumbnail.MIN_SCALE);
assert(value <= Thumbnail.MAX_SCALE);
......
namespace Exif {
namespace Orientation {
public static const int TOP_LEFT = 1;
public static const int TOP_RIGHT = 2;
public static const int BOTTOM_RIGHT = 3;
public static const int BOTTOM_LEFT = 4;
public static const int LEFT_TOP = 5;
public static const int RIGHT_TOP = 6;
public static const int RIGHT_BOTTOM = 7;
public static const int LEFT_BOTTOM = 8;
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;
}
namespace Jpeg {
public static const uint8 MARKER_PREFIX = 0xFF;
public enum Marker {
SOI = 0xD8,
EOI = 0xD9,
APP0 = 0xE0,
APP1 = 0xE1;
public uint8 get_byte() {
return (uint8) this;
}
}
}
public errordomain ExifError {
FILE_FORMAT
}
extern void free(void *ptr);
public class PhotoExif {
private File file;
private Exif.Data exifData = null;
......@@ -20,14 +212,30 @@ public class PhotoExif {
this.file = file;
}
public int get_orientation() {
public Exif.Orientation get_orientation() {
update();
Exif.Entry orientation = find_entry(Exif.Ifd.ZERO, Exif.Tag.ORIENTATION, Exif.Format.SHORT);
if (orientation == null)
Exif.Entry entry = find_entry(Exif.Ifd.ZERO, Exif.Tag.ORIENTATION, Exif.Format.SHORT);
if (entry == null)
return Exif.Orientation.TOP_LEFT;
return Exif.Convert.get_short(orientation.data, exifData.get_byte_order());
int o = Exif.Convert.get_short(entry.data, exifData.get_byte_order());
assert(o >= Exif.ORIENTATION_MIN);
assert(o <= Exif.ORIENTATION_MAX);
return (Exif.Orientation) o;
}
public void set_orientation(Exif.Orientation orientation) {
update();
Exif.Entry entry = find_first_entry(Exif.Tag.ORIENTATION, Exif.Format.SHORT);
if (entry == null) {
// TODO: Need a fall-back here
error("Unable to set orientation: no entry found");
}
Exif.Convert.set_short(entry.data, exifData.get_byte_order(), orientation);
}
public bool get_dimensions(out Dimensions dim) {
......@@ -64,7 +272,7 @@ public class PhotoExif {
exifData = Exif.Data.new_from_file(file.get_path());
// TODO: Better error handling
assert(exifData != null);
// fix now, all at once
exifData.fix();
}
......@@ -85,5 +293,188 @@ public class PhotoExif {
return entry;
}
private Exif.Entry? find_first_entry(Exif.Tag tag, Exif.Format format) {
assert(exifData != null);
for (int ctr = 0; ctr < (int) Exif.Ifd.COUNT; ctr++) {
Exif.Content content = exifData.ifd[ctr];
assert(content != null);
Exif.Entry entry = content.get_entry(tag);
if (entry == null)
continue;
assert(entry.format == format);
if ((format != Exif.Format.ASCII) && (format != Exif.Format.UNDEFINED))
assert(entry.size == format.get_size());
return entry;
}
return null;
}
public void commit() throws Error {
if (exifData == null)
return;
FileInputStream fins = file.read(null);
Jpeg.Marker marker;
int segmentLength;
// first marker is always SOI
segmentLength = read_marker(fins, out marker);
if ((marker != Jpeg.Marker.SOI) || (segmentLength != 0))
throw new ExifError.FILE_FORMAT("SOI not found in %s".printf(file.get_path()));
// for EXIF, next marker is always APP1
segmentLength = read_marker(fins, out marker);
if (marker != Jpeg.Marker.APP1)
throw new ExifError.FILE_FORMAT("EXIF APP1 not found in %s".printf(file.get_path()));
if (segmentLength <= 0)
throw new ExifError.FILE_FORMAT("EXIF APP1 length of %d".printf(segmentLength));
// flatten exif to buffer
uchar *flattened = null;
int flattenedSize = 0;
exifData.save_data(&flattened, &flattenedSize);
assert(flattened != null);
assert(flattenedSize > 0);
try {
/*
if (flattenedSize == segmentLength) {
// the new EXIF data is exactly the same size as the data in the file, so simply
// overwrite
debug("Writing EXIF in-place of %d bytes to %s", flattenedSize, file.get_path());
// close for reading
fins.close(null);
fins = null;
// open for writing
FileOutputStream fouts = file.replace(null, false, FileCreateFlags.PRIVATE, null);
size_t bytesWritten = 0;
// seeking with a replace() file destroys what's it seeks past (??), so just
// writing since it's all of 6 bytes
// Marker:SOI and Marker:APP1 and length (none change)
write_marker(fouts, Jpeg.Marker.SOI, 0);
write_marker(fouts, Jpeg.Marker.APP1, flattenedSize);
fouts.write_all(flattened, flattenedSize, out bytesWritten, null);
fouts.close(null);
} else */ {
// create a new photo file with the updated EXIF and move it on top of the old one
// skip past APP1
fins.skip(segmentLength, null);
File temp = null;
FileOutputStream fouts = create_temp(file, out temp);
size_t bytesWritten = 0;
debug("Building new file at %s with %d bytes EXIF, overwriting %s", temp.get_path(),
flattenedSize, file.get_path());
// write SOI
write_marker(fouts, Jpeg.Marker.SOI, 0);
// write APP1 with EXIF data
write_marker(fouts, Jpeg.Marker.APP1, flattenedSize);
fouts.write_all(flattened, flattenedSize, out bytesWritten, null);
// copy remainder of file into new file
uint8[] copyBuffer = new uint8[64 * 1024];
for(;;) {
ssize_t bytesRead = fins.read(copyBuffer, copyBuffer.length, null);
if (bytesRead == 0)
break;
assert(bytesRead > 0);
fouts.write_all(copyBuffer, bytesRead, out bytesWritten, null);
}
// close both for move
fouts.close(null);
fins.close(null);
temp.move(file, FileCopyFlags.OVERWRITE, null, null);
}
} finally {
free(flattened);
}
}
private int read_marker(FileInputStream fins, out Jpeg.Marker marker) throws Error {
uint8 byte = 0;
uint16 length = 0;
size_t bytesRead;
fins.read_all(&byte, 1, out bytesRead, null);
if (byte != Jpeg.MARKER_PREFIX)
return -1;
fins.read_all(&byte, 1, out bytesRead, null);
marker = (Jpeg.Marker) byte;
if ((marker == Jpeg.Marker.SOI) || (marker == Jpeg.Marker.EOI)) {
// no length
return 0;
}
fins.read_all(&length, 2, out bytesRead, null);
length = uint16.from_big_endian(length);
if (length < 2) {
debug("Invalid length %Xh at ofs %llXh", length, fins.tell() - 2);
return -1;
}
// account for two length bytes already read
return length - 2;
}
// this writes the marker and a length (if positive)
private void write_marker(FileOutputStream fouts, Jpeg.Marker marker, int length) throws Error {
// this is required to compile
uint8 prefix = Jpeg.MARKER_PREFIX;
uint8 byte = marker.get_byte();
size_t written;
fouts.write_all(&prefix, 1, out written, null);
fouts.write_all(&byte, 1, out written, null);
if (length <= 0)
return;
// +2 to account for length bytes
length += 2;
uint16 host = (uint16) length;
uint16 motorola = (uint16) host.to_big_endian();
fouts.write_all(&motorola, 2, out written, null);
}
private FileOutputStream? create_temp(File original, out File temp) throws Error {
File parent = original.get_parent();
assert(parent != null);
for (int ctr = 0; ctr < int.MAX; ctr++) {
File t = parent.get_child("shotwell.%08X.tmp".printf(ctr));
FileOutputStream fouts = t.create(FileCreateFlags.PRIVATE, null);
if (fouts != null) {
temp = t;
return fouts;
}
}
return null;
}
}
......@@ -2,7 +2,7 @@
TARGET = shotwell
# This takes care of a warning message generated by the use of Math.round in image_util.vala
VALAC_OPTS =--Xcc=-std=c99 --save-temps
VALAC_OPTS =--Xcc=-std=c99
SRC_FILES = \
main.vala \
......
......@@ -73,6 +73,44 @@ public class Thumbnail : Gtk.Alignment {
return photoID;
}
public Exif.Orientation get_orientation() {
return exif.get_orientation();
}
public void set_orientation(Exif.Orientation orientation) {
if (orientation == exif.get_orientation())
return;
exif.set_orientation(orientation);
// rotate dimensions from original dimensions (which doesn't require access to pixbuf)
scaledDim = get_scaled_dimensions(originalDim, scale);
scaledDim = get_rotated_dimensions(scaledDim, orientation);
// rotate image if exposed ... need to rotate everything (the cached thumbnail and the
// scaled image in the widget) to be ready for future events, i.e. resize()
if (cached != null) {
cached = ThumbnailCache.fetch(photoID, scale);
cached = rotate_to_exif(cached, orientation);
Gdk.Pixbuf scaled = cached.scale_simple(scaledDim.width, scaledDim.height, LOW_QUALITY_INTERP);
scaledInterp = LOW_QUALITY_INTERP;
image.set_from_pixbuf(scaled);
}
image.set_size_request(scaledDim.width, scaledDim.height);
// TODO: Write this in the background
try {
exif.commit();
} catch (Error err) {
error("%s", err.message);
}
title.set_text(build_exposed_title());
}
private string build_exposed_title() {
int64 fileSize = 0;
try {
......@@ -88,10 +126,11 @@ public class Thumbnail : Gtk.Alignment {
string datetime = exif.get_datetime();
return "%s\n%s\n%s\n%lld bytes".printf(
return "%s\n%s\n%s\n%s\n%lld bytes".printf(
file.get_basename(),
(datetime != null) ? datetime : "",
(dimFound) ? "%d x %d".printf(dim.width, dim.height) : "",
exif.get_orientation().get_description(),
fileSize);
}
......
......@@ -57,7 +57,7 @@ Dimensions get_scaled_dimensions(Dimensions original, int scale) {
return scaled;
}
Dimensions get_rotated_dimensions(Dimensions dim, int orientation) {
Dimensions get_rotated_dimensions(Dimensions dim, Exif.Orientation orientation) {
int width = dim.width;
int height = dim.height;
......@@ -95,7 +95,7 @@ Gdk.Pixbuf scale_pixbuf(Gdk.Pixbuf pixbuf, int scale, Gdk.InterpType interp) {
return pixbuf.scale_simple(scaled.width, scaled.height, interp);
}
Gdk.Pixbuf rotate_to_exif(Gdk.Pixbuf pixbuf, int orientation) {
Gdk.Pixbuf rotate_to_exif(Gdk.Pixbuf pixbuf, Exif.Orientation orientation) {
switch(orientation) {
case Exif.Orientation.TOP_LEFT: {
// fine just as it is
......@@ -106,8 +106,7 @@ Gdk.Pixbuf rotate_to_exif(Gdk.Pixbuf pixbuf, int orientation) {
} break;
case Exif.Orientation.BOTTOM_RIGHT: {
pixbuf = pixbuf.flip(true);
pixbuf = pixbuf.flip(false);
pixbuf = pixbuf.rotate_simple(Gdk.PixbufRotation.UPSIDEDOWN);
} break;
case Exif.Orientation.BOTTOM_LEFT: {
......@@ -115,8 +114,8 @@ Gdk.Pixbuf rotate_to_exif(Gdk.Pixbuf pixbuf, int orientation) {
} break;
case Exif.Orientation.LEFT_TOP: {
pixbuf = pixbuf.rotate_simple(Gdk.PixbufRotation.CLOCKWISE);
pixbuf = pixbuf.flip(true);
pixbuf = pixbuf.rotate_simple(Gdk.PixbufRotation.COUNTERCLOCKWISE);
pixbuf = pixbuf.flip(false);
} break;
case Exif.Orientation.RIGHT_TOP: {
......@@ -124,8 +123,8 @@ Gdk.Pixbuf rotate_to_exif(Gdk.Pixbuf pixbuf, int orientation) {
} break;
case Exif.Orientation.RIGHT_BOTTOM: {
pixbuf = pixbuf.rotate_simple(Gdk.PixbufRotation.COUNTERCLOCKWISE);
pixbuf = pixbuf.flip(true);
pixbuf = pixbuf.rotate_simple(Gdk.PixbufRotation.CLOCKWISE);
pixbuf = pixbuf.flip(false);
} break;
case Exif.Orientation.LEFT_BOTTOM: {
......
......@@ -50,6 +50,10 @@ namespace Exif {
public static int16 get_sshort(uchar *buffer, ByteOrder byteOrder);
public static uint32 get_long(uchar *buffer, ByteOrder byteOrder);
public static int32 get_slong(uchar *buffer, ByteOrder byteOrder);
public static void set_short(uchar *buffer, ByteOrder byteOrder, uint16 val);
public static void set_sshort(uchar *buffer, ByteOrder byteOrder, int16 val);
public static void set_long(uchar *buffer, ByteOrder byteOrder, uint32 val);
public static void set_slong(uchar *buffer, ByteOrder byteOrder, int32 val);
}
[CCode (cheader_filename="libexif/exif-content.h")]
......@@ -74,6 +78,7 @@ namespace Exif {
public ByteOrder get_byte_order();
public void set_option(DataOption option);
public void unset_option(DataOption option);
public void save_data(uchar **buffer, uint *size);
public Content[Ifd.COUNT] ifd;
public uchar *data;
......
......@@ -13,6 +13,10 @@
<menu name="PhotosMenu" action="Photos">
<menuitem name="PhotosIncreaseSize" action="IncreaseSize" />
<menuitem name="PhotosDecreaseSize" action="DecreaseSize" />
<separator />
<menuitem name="RotateClockwise" action="RotateClockwise" />
<menuitem name="RotateCounterclockwise" action="RotateCounterclockwise" />
<menuitem name="Mirror" action="Mirror" />
</menu>
<menu name="HelpMenu" action="Help">
......@@ -22,6 +26,10 @@