Commit fd3c1db8 authored by Lucas Beeler's avatar Lucas Beeler

Implemented red-eye removal feature.

parent 1c495ed4
......@@ -626,6 +626,9 @@ public class PhotoTable : DatabaseTable {
if (!keyfile.load_from_data(trans, trans.length, KeyFileFlags.NONE))
return null;
if (!keyfile.has_group(object))
return null;
string[] keys = keyfile.get_keys(object);
if (keys == null || keys.length == 0)
return null;
......
......@@ -5,7 +5,7 @@
*/
public abstract class EditingToolWindow : Gtk.Window {
private static const int FRAME_BORDER = 6;
private const int FRAME_BORDER = 6;
private Gtk.Window container;
private Gtk.Frame layout_frame = new Gtk.Frame(null);
......@@ -103,6 +103,72 @@ public abstract class PhotoCanvas {
public signal void resized_scaled_pixbuf(Dimensions old_dim, Gdk.Pixbuf scaled, Gdk.Rectangle scaled_position);
public Gdk.Point active_to_unscaled_point(Gdk.Point active_point) {
Gdk.Rectangle scaled_position = get_scaled_pixbuf_position();
Dimensions unscaled_dims = photo.get_dimensions();
double scale_factor_x = ((double) unscaled_dims.width) /
((double) scaled_position.width);
double scale_factor_y = ((double) unscaled_dims.height) /
((double) scaled_position.height);
Gdk.Point result = {0};
result.x = (int)(((double) active_point.x) * scale_factor_x + 0.5);
result.y = (int)(((double) active_point.y) * scale_factor_y + 0.5);
return result;
}
public Gdk.Rectangle active_to_unscaled_rect(Gdk.Rectangle active_rect) {
Gdk.Point upper_left = {0};
Gdk.Point lower_right = {0};
upper_left.x = active_rect.x;
upper_left.y = active_rect.y;
lower_right.x = upper_left.x + active_rect.width;
lower_right.y = upper_left.y + active_rect.height;
upper_left = active_to_unscaled_point(upper_left);
lower_right = active_to_unscaled_point(lower_right);
Gdk.Rectangle unscaled_rect = {0};
unscaled_rect.x = upper_left.x;
unscaled_rect.y = upper_left.y;
unscaled_rect.width = lower_right.x - upper_left.x;
unscaled_rect.height = lower_right.y - upper_left.y;
return unscaled_rect;
}
public Gdk.Point user_to_active_point(Gdk.Point user_point) {
Gdk.Rectangle active_offsets = get_scaled_pixbuf_position();
Gdk.Point result = {0};
result.x = user_point.x - active_offsets.x;
result.y = user_point.y - active_offsets.y;
return result;
}
public Gdk.Rectangle user_to_active_rect(Gdk.Rectangle user_rect) {
Gdk.Point upper_left = {0};
Gdk.Point lower_right = {0};
upper_left.x = user_rect.x;
upper_left.y = user_rect.y;
lower_right.x = upper_left.x + user_rect.width;
lower_right.y = upper_left.y + user_rect.height;
upper_left = user_to_active_point(upper_left);
lower_right = user_to_active_point(lower_right);
Gdk.Rectangle active_rect = {0};
active_rect.x = upper_left.x;
active_rect.y = upper_left.y;
active_rect.width = lower_right.x - upper_left.x;
active_rect.height = lower_right.y - upper_left.y;
return active_rect;
}
public Photo get_photo() {
return photo;
}
......@@ -200,6 +266,21 @@ public abstract class PhotoCanvas {
width, 1,
Gdk.RgbDither.NORMAL, 0, 0);
}
public void draw_circle(Gdk.GC gc, int active_center_x, int active_center_y,
int radius) {
int center_x = active_center_x + get_scaled_pixbuf_position().x;
int center_y = active_center_y + get_scaled_pixbuf_position().y;
Gdk.Rectangle bounds = { 0 };
bounds.x = center_x - radius;
bounds.y = center_y - radius;
bounds.width = 2 * radius;
bounds.height = bounds.width;
Gdk.draw_arc(get_drawable(), gc, false, bounds.x, bounds.y,
bounds.width, bounds.height, 0, (360 * 64));
}
public void erase_vertical_line(int x, int y, int height) {
drawable.draw_pixbuf(default_gc, scaled,
......@@ -307,20 +388,20 @@ public abstract class EditingTool {
}
public class CropTool : EditingTool {
private static const double CROP_INIT_X_PCT = 0.15;
private static const double CROP_INIT_Y_PCT = 0.15;
private const double CROP_INIT_X_PCT = 0.15;
private const double CROP_INIT_Y_PCT = 0.15;
private static const int CROP_MIN_WIDTH = 100;
private static const int CROP_MIN_HEIGHT = 100;
private const int CROP_MIN_WIDTH = 100;
private const int CROP_MIN_HEIGHT = 100;
private static const float CROP_EXTERIOR_SATURATION = 0.00f;
private static const int CROP_EXTERIOR_RED_SHIFT = -32;
private static const int CROP_EXTERIOR_GREEN_SHIFT = -32;
private static const int CROP_EXTERIOR_BLUE_SHIFT = -32;
private static const int CROP_EXTERIOR_ALPHA_SHIFT = 0;
private const float CROP_EXTERIOR_SATURATION = 0.00f;
private const int CROP_EXTERIOR_RED_SHIFT = -32;
private const int CROP_EXTERIOR_GREEN_SHIFT = -32;
private const int CROP_EXTERIOR_BLUE_SHIFT = -32;
private const int CROP_EXTERIOR_ALPHA_SHIFT = 0;
private class CropToolWindow : EditingToolWindow {
private static const int CONTROL_SPACING = 8;
private const int CONTROL_SPACING = 8;
public Gtk.Button apply_button = new Gtk.Button.from_stock(Gtk.STOCK_APPLY);
public Gtk.Button cancel_button = new Gtk.Button.from_stock(Gtk.STOCK_CANCEL);
......@@ -883,3 +964,272 @@ public class CropTool : EditingTool {
}
}
public struct RedeyeInstance {
public const int MIN_RADIUS = 4;
public const int MAX_RADIUS = 32;
public const int DEFAULT_RADIUS = 10;
public Gdk.Point center;
public int radius;
RedeyeInstance() {
Gdk.Point default_center = Gdk.Point();
center = default_center;
radius = DEFAULT_RADIUS;
}
public static Gdk.Rectangle to_bounds_rect(RedeyeInstance inst) {
Gdk.Rectangle result = {0};
result.x = inst.center.x - inst.radius;
result.y = inst.center.y - inst.radius;
result.width = 2 * inst.radius;
result.height = result.width;
return result;
}
public static RedeyeInstance from_bounds_rect(Gdk.Rectangle rect) {
Gdk.Rectangle in_rect = rect;
RedeyeInstance result = RedeyeInstance();
result.radius = (in_rect.width + in_rect.height) / 4;
result.center.x = in_rect.x + result.radius;
result.center.y = in_rect.y + result.radius;
return result;
}
}
public class RedeyeTool : EditingTool {
private class RedeyeToolWindow : EditingToolWindow {
private const int CONTROL_SPACING = 8;
private Gtk.Label slider_label = new Gtk.Label.with_mnemonic("Size:");
public Gtk.Button apply_button =
new Gtk.Button.from_stock(Gtk.STOCK_APPLY);
public Gtk.Button close_button =
new Gtk.Button.from_stock(Gtk.STOCK_CLOSE);
public Gtk.HScale slider = new Gtk.HScale.with_range(
RedeyeInstance.MIN_RADIUS, RedeyeInstance.MAX_RADIUS, 1.0);
public RedeyeToolWindow(Gtk.Window container) {
base(container);
slider.set_size_request(80, -1);
slider.set_draw_value(false);
close_button.set_tooltip_text("Close the red-eye tool");
close_button.set_image_position(Gtk.PositionType.LEFT);
apply_button.set_tooltip_text("Remove any red-eye effects in the selected region");
apply_button.set_image_position(Gtk.PositionType.LEFT);
Gtk.HBox layout = new Gtk.HBox(false, CONTROL_SPACING);
layout.add(slider_label);
layout.add(slider);
layout.add(close_button);
layout.add(apply_button);
add(layout);
}
}
private Gdk.GC thin_white_gc = null;
private Gdk.GC wider_gray_gc = null;
private RedeyeToolWindow redeye_tool_window = null;
private RedeyeInstance user_interaction_instance;
private bool is_reticle_move_in_progress = false;
private Gdk.Point reticle_move_mouse_start_point;
private Gdk.Point reticle_move_anchor;
private Gdk.Cursor cached_arrow_cursor;
private Gdk.Cursor cached_grab_cursor;
private Gdk.Rectangle old_scaled_pixbuf_position;
private RedeyeInstance new_interaction_instance(PhotoCanvas canvas) {
Gdk.Rectangle photo_bounds = canvas.get_scaled_pixbuf_position();
Gdk.Point photo_center = {0};
photo_center.x = photo_bounds.x + (photo_bounds.width / 2);
photo_center.y = photo_bounds.y + (photo_bounds.height / 2);
RedeyeInstance result = RedeyeInstance();
result.center.x = photo_center.x;
result.center.y = photo_center.y;
result.radius = RedeyeInstance.DEFAULT_RADIUS;
return result;
}
private void prepare_gc(Gdk.GC default_gc, Gdk.Drawable drawable) {
Gdk.GCValues gc_values = Gdk.GCValues();
gc_values.function = Gdk.Function.COPY;
gc_values.fill = Gdk.Fill.SOLID;
gc_values.line_style = Gdk.LineStyle.SOLID;
gc_values.cap_style = Gdk.CapStyle.BUTT;
gc_values.join_style = Gdk.JoinStyle.MITER;
Gdk.GCValuesMask mask =
Gdk.GCValuesMask.FOREGROUND
| Gdk.GCValuesMask.FUNCTION
| Gdk.GCValuesMask.FILL
| Gdk.GCValuesMask.LINE_WIDTH
| Gdk.GCValuesMask.LINE_STYLE
| Gdk.GCValuesMask.CAP_STYLE
| Gdk.GCValuesMask.JOIN_STYLE;
gc_values.foreground = fetch_color("#222", drawable);
gc_values.line_width = 1;
wider_gray_gc = new Gdk.GC.with_values(drawable, gc_values, mask);
gc_values.foreground = fetch_color("#FFF", drawable);
gc_values.line_width = 1;
thin_white_gc = new Gdk.GC.with_values(drawable, gc_values, mask);
}
private void draw_redeye_instance(RedeyeInstance inst) {
canvas.draw_circle(wider_gray_gc, inst.center.x, inst.center.y,
inst.radius - 1);
canvas.draw_circle(thin_white_gc, inst.center.x, inst.center.y,
inst.radius - 2);
}
private bool on_size_slider_adjust(Gtk.ScrollType type) {
user_interaction_instance.radius =
(int) redeye_tool_window.slider.get_value();
canvas.repaint();
return false;
}
private void on_apply() {
Gdk.Rectangle bounds_rect_user =
RedeyeInstance.to_bounds_rect(user_interaction_instance);
Gdk.Rectangle bounds_rect_active =
canvas.user_to_active_rect(bounds_rect_user);
Gdk.Rectangle bounds_rect_unscaled =
canvas.active_to_unscaled_rect(bounds_rect_active);
RedeyeInstance instance_unscaled =
RedeyeInstance.from_bounds_rect(bounds_rect_unscaled);
canvas.get_photo().add_redeye_instance(instance_unscaled);
notify_apply();
}
private void on_canvas_resize() {
Gdk.Rectangle scaled_pixbuf_position =
canvas.get_scaled_pixbuf_position();
user_interaction_instance.center.x -= old_scaled_pixbuf_position.x;
user_interaction_instance.center.y -= old_scaled_pixbuf_position.y;
double scale_factor = ((double) scaled_pixbuf_position.width) /
((double) old_scaled_pixbuf_position.width);
user_interaction_instance.center.x =
(int)(((double) user_interaction_instance.center.x) *
scale_factor + 0.5);
user_interaction_instance.center.y =
(int)(((double) user_interaction_instance.center.y) *
scale_factor + 0.5);
user_interaction_instance.center.x += scaled_pixbuf_position.x;
user_interaction_instance.center.y += scaled_pixbuf_position.y;
old_scaled_pixbuf_position = scaled_pixbuf_position;
}
public override void activate(PhotoCanvas canvas) {
user_interaction_instance = new_interaction_instance(canvas);
canvas.new_drawable += prepare_gc;
prepare_gc(canvas.get_default_gc(), canvas.get_drawable());
canvas.resized_scaled_pixbuf += on_canvas_resize;
old_scaled_pixbuf_position = canvas.get_scaled_pixbuf_position();
redeye_tool_window = new RedeyeToolWindow(canvas.get_container());
redeye_tool_window.slider.set_value(user_interaction_instance.radius);
redeye_tool_window.slider.change_value += on_size_slider_adjust;
redeye_tool_window.apply_button.clicked += on_apply;
redeye_tool_window.close_button.clicked += notify_cancel;
redeye_tool_window.show_all();
cached_arrow_cursor = new Gdk.Cursor(Gdk.CursorType.ARROW);
cached_grab_cursor = new Gdk.Cursor(Gdk.CursorType.FLEUR);
base.activate(canvas);
}
public override void deactivate() {
if (redeye_tool_window != null) {
redeye_tool_window.hide();
redeye_tool_window = null;
}
base.deactivate();
}
public override EditingToolWindow? get_tool_window() {
return redeye_tool_window;
}
public override void paint(Gdk.GC gc, Gdk.Drawable drawable) {
canvas.paint_pixbuf(canvas.get_scaled_pixbuf());
/* user_interaction_instance has its radius in user coords, and
draw_redeye_instance expects active region coords */
RedeyeInstance active_inst = user_interaction_instance;
active_inst.center =
canvas.user_to_active_point(user_interaction_instance.center);
draw_redeye_instance(active_inst);
}
public override void on_left_click(int x, int y) {
Gdk.Rectangle bounds_rect =
RedeyeInstance.to_bounds_rect(user_interaction_instance);
if (coord_in_rectangle(x, y, bounds_rect)) {
is_reticle_move_in_progress = true;
reticle_move_mouse_start_point.x = x;
reticle_move_mouse_start_point.y = y;
reticle_move_anchor = user_interaction_instance.center;
}
}
public override void on_left_released(int x, int y) {
is_reticle_move_in_progress = false;
}
public override void on_motion(int x, int y, Gdk.ModifierType mask) {
if (is_reticle_move_in_progress) {
int delta_x = x - reticle_move_mouse_start_point.x;
int delta_y = y - reticle_move_mouse_start_point.y;
user_interaction_instance.center.x = reticle_move_anchor.x +
delta_x;
user_interaction_instance.center.y = reticle_move_anchor.y +
delta_y;
canvas.repaint();
} else {
Gdk.Rectangle bounds =
RedeyeInstance.to_bounds_rect(user_interaction_instance);
if (coord_in_rectangle(x, y, bounds)) {
canvas.get_drawing_window().set_cursor(cached_grab_cursor);
} else {
canvas.get_drawing_window().set_cursor(cached_arrow_cursor);
}
}
}
}
......@@ -16,12 +16,13 @@ public enum ImportResult {
}
public class Photo : Object {
public static const int EXCEPTION_NONE = 0;
public static const int EXCEPTION_ORIENTATION = 1 << 0;
public static const int EXCEPTION_CROP = 1 << 1;
public static const Jpeg.Quality EXPORT_JPEG_QUALITY = Jpeg.Quality.HIGH;
public static const Gdk.InterpType EXPORT_INTERP = Gdk.InterpType.BILINEAR;
public const int EXCEPTION_NONE = 0;
public const int EXCEPTION_ORIENTATION = 1 << 0;
public const int EXCEPTION_CROP = 1 << 1;
public const int EXCEPTION_REDEYE = 1 << 2;
public const Jpeg.Quality EXPORT_JPEG_QUALITY = Jpeg.Quality.HIGH;
public const Gdk.InterpType EXPORT_INTERP = Gdk.InterpType.BILINEAR;
private static Gee.HashMap<int64?, Photo> photo_map = null;
private static PhotoTable photo_table = null;
......@@ -229,8 +230,17 @@ public class Photo : Object {
altered = true;
}
if (altered)
if (altered) {
// REDEYE: if photo was altered, clear the pixbuf cache. This is
// necessary because the redeye transformation, unlike rotate/crop,
// actually modifies the pixel data in the pixbuf, so we need to
// re-load the original pixel data from its source file when redeye
// is cleared
cached_raw = null;
photo_altered();
}
}
public void rotate(Rotation rotation) {
......@@ -308,6 +318,10 @@ public class Photo : Object {
return true;
}
private Orientation get_orientation() {
return photo_table.get_orientation(photo_id);
}
// Sets the crop using the raw photo's unrotated coordinate system
private bool set_raw_crop(Box crop) {
KeyValueMap map = new KeyValueMap("crop");
......@@ -343,7 +357,159 @@ public class Photo : Object {
return res;
}
public bool add_redeye_instance(RedeyeInstance inst_unscaled) {
Gdk.Rectangle bounds_rect_unscaled =
RedeyeInstance.to_bounds_rect(inst_unscaled);
Gdk.Rectangle bounds_rect_raw =
unscaled_to_raw_rect(bounds_rect_unscaled);
RedeyeInstance inst = RedeyeInstance.from_bounds_rect(bounds_rect_raw);
KeyValueMap map = photo_table.get_transformation(photo_id, "redeye");
if (map == null) {
map = new KeyValueMap("redeye");
map.set_int("num_points", 0);
}
int num_points = map.get_int("num_points", -1);
assert(num_points >= 0);
num_points++;
string radius_key = "radius%d".printf(num_points - 1);
string center_key = "center%d".printf(num_points - 1);
map.set_int(radius_key, inst.radius);
map.set_point(center_key, inst.center);
map.set_int("num_points", num_points);
bool res = photo_table.set_transformation(photo_id, map);
if (res)
photo_altered();
return res;
}
private RedeyeInstance[] get_all_redeye() {
KeyValueMap map = photo_table.get_transformation(photo_id, "redeye");
if (map != null) {
int num_points = map.get_int("num_points", -1);
assert(num_points > 0);
RedeyeInstance[] res = new RedeyeInstance[num_points];
Gdk.Point default_point = {0};
default_point.x = -1;
default_point.y = -1;
for (int i = 0; i < num_points; i++) {
string center_key = "center%d".printf(i);
string radius_key = "radius%d".printf(i);
res[i].center = map.get_point(center_key, default_point);
assert(res[i].center.x != default_point.x);
assert(res[i].center.y != default_point.y);
res[i].radius = map.get_int(radius_key, -1);
assert(res[i].radius != -1);
}
return res;
}
return new RedeyeInstance[0];
}
private Gdk.Pixbuf do_redeye(owned Gdk.Pixbuf pixbuf,
owned RedeyeInstance inst) {
/* we remove redeye within a circular region called the "effect
extent." the effect extent is inscribed within its "bounding
rectangle." */
/* for each scanline in the top half-circle of the effect extent,
compute the number of pixels by which the effect extent is inset
from the edges of its bounding rectangle. note that we only have
to do this for the first quadrant because the second quadrant's
insets can be derived by symmetry */
double r = (double) inst.radius;
int[] x_insets_first_quadrant = new int[inst.radius + 1];
int i = 0;
for (double y = r; y >= 0.0; y -= 1.0) {
double theta = Math.asin(y / r);
int x = (int)((r * Math.cos(theta)) + 0.5);
x_insets_first_quadrant[i] = inst.radius - x;
i++;
}
int x_bounds_min = inst.center.x - inst.radius;
int x_bounds_max = inst.center.x + inst.radius;
int ymin = inst.center.y - inst.radius;
ymin = (ymin < 0) ? 0 : ymin;
int ymax = inst.center.y;
ymax = (ymax > (pixbuf.height - 1)) ? (pixbuf.height - 1) : ymax;
/* iterate over all the pixels in the top half-circle of the effect
extent from top to bottom */
int inset_index = 0;
for (int y_it = ymin; y_it <= ymax; y_it++) {
int xmin = x_bounds_min + x_insets_first_quadrant[inset_index];
xmin = (xmin < 0) ? 0 : xmin;
int xmax = x_bounds_max - x_insets_first_quadrant[inset_index];
xmax = (xmax > (pixbuf.width - 1)) ? (pixbuf.width - 1) : xmax;
for (int x_it = xmin; x_it <= xmax; x_it++) {
red_reduce_pixel(pixbuf, x_it, y_it);
}
inset_index++;
}
/* iterate over all the pixels in the top half-circle of the effect
extent from top to bottom */
ymin = inst.center.y;
ymax = inst.center.y + inst.radius;
inset_index = x_insets_first_quadrant.length - 1;
for (int y_it = ymin; y_it <= ymax; y_it++) {
int xmin = x_bounds_min + x_insets_first_quadrant[inset_index];
xmin = (xmin < 0) ? 0 : xmin;
int xmax = x_bounds_max - x_insets_first_quadrant[inset_index];
xmax = (xmax > (pixbuf.width - 1)) ? (pixbuf.width - 1) : xmax;
for (int x_it = xmin; x_it <= xmax; x_it++) {
red_reduce_pixel(pixbuf, x_it, y_it);
}
inset_index--;
}
return pixbuf;
}
private Gdk.Pixbuf red_reduce_pixel(owned Gdk.Pixbuf pixbuf, int x, int y) {
int px_start_byte_offset = (y * pixbuf.get_rowstride()) +
(x * pixbuf.get_n_channels());
unowned uchar[] pixel_data = pixbuf.get_pixels();
/* The pupil of the human eye has no pigment, so we expect all
color channels to be of about equal intensity. This means that at
any point within the effects region, the value of the red channel
should be about the same as the values of the green and blue
channels. So set the value of the red channel to be the mean of the
values of the red and blue channels. This preserves achromatic
intensity across all channels while eliminating any extraneous flare
affecting the red channel only (i.e. the red-eye effect). */
uchar g = pixel_data[px_start_byte_offset + 1];
uchar b = pixel_data[px_start_byte_offset + 2];
uchar r = (g + b) / 2;
pixel_data[px_start_byte_offset] = r;
return pixbuf;
}
// Retrieves a full-sized pixbuf for the Photo with all modifications, except those specified
public Gdk.Pixbuf get_pixbuf(int exceptions = EXCEPTION_NONE) throws Error {
Gdk.Pixbuf pixbuf = null;
......@@ -365,7 +531,15 @@ public class Photo : Object {
//
// Image modification pipeline
//
// redeye reduction
if ((exceptions & EXCEPTION_REDEYE) == 0) {
RedeyeInstance[] redeye_instances = get_all_redeye();
for (int i = 0; i < redeye_instances.length; i++) {
pixbuf = do_redeye(pixbuf, redeye_instances[i]);
}
}
// crop
if ((exceptions & EXCEPTION_CROP) == 0) {
Box crop;
......@@ -632,5 +806,62 @@ public class Photo : Object {
photo_altered();
}
}
private Gdk.Point unscaled_to_raw_point(Gdk.Point unscaled_point) {
Orientation unscaled_orientation = get_orientation();
Dimensions unscaled_dims =
unscaled_orientation.rotate_dimensions(get_dimensions());
int unscaled_x_offset_raw = 0;
int unscaled_y_offset_raw = 0;
Box crop_box = {0};
bool is_cropped = get_raw_crop(out crop_box);
if (is_cropped) {
unscaled_x_offset_raw = crop_box.left;
unscaled_y_offset_raw = crop_box.top;
}
Gdk.Point derotated_point =
unscaled_orientation.derotate_point(unscaled_dims,
unscaled_point);
derotated_point.x += unscaled_x_offset_raw;
derotated_point.y += unscaled_y_offset_raw;
return derotated_point;
}
private Gdk.Rectangle unscaled_to_raw_rect(Gdk.Rectangle unscaled_rect) {
Gdk.Point upper_left = {0};
Gdk.Point lower_right = {0};
upper_left.x = unscaled_rect.x;
upper_left.y = unscaled_rect.y;
lower_right.x = upper_left.x + unscaled_rect.width;
lower_right.y = upper_left.y + unscaled_rect.height;
upper_left = unscaled_to_raw_point(upper_left);
lower_right = unscaled_to_raw_point(lower_right);
if (upper_left.x > lower_right.x) {
int temp = upper_left.x;
upper_left.x = lower_right.x;
lower_right.x = temp;
}
if (upper_left.y > lower_right.y) {
int temp = upper_left.y;
upper_left.y = lower_right.y;
lower_right.y = temp;
}
Gdk.Rectangle raw_rect = {0};
raw_rect.x = upper_left.x;
raw_rect.y = upper_left.y;
raw_rect.width = lower_right.x - upper_left.x;
raw_rect.height = lower_right.y - upper_left.y;
return raw_rect;
}
}
......@@ -5,7 +5,7 @@
*/
public class PhotoPage : SinglePhotoPage {
public static const int TOOL_WINDOW_SEPARATOR = 8;
public const int TOOL_WINDOW_SEPARATOR = 8;
private class PhotoPageCanvas : PhotoCanvas {
private PhotoPage photo_page;
......@@ -30,6 +30,7 @@ public class PhotoPage : SinglePhotoPage {
private Gtk.Toolbar toolbar = new Gtk.Toolbar();
private Gtk.ToolButton rotate_button = null;
private Gtk.ToggleToolButton crop_button = null;
private Gtk.ToggleToolButton redeye_button = null;