2
/* Copyright 2009-2011 Yorba Foundation
4
* This software is licensed under the GNU Lesser General Public License
5
* (version 2.1 or later). See the COPYING file in this distribution.
8
namespace EditingTools {
11
* An editing tool that allows one to introduce or remove a Dutch angle from
14
public class StraightenTool : EditingTool {
15
private const double MIN_ANGLE = -15.0;
16
private const double MAX_ANGLE = 15.0;
17
private const double INCREMENT = 0.1;
18
private const int MIN_SLIDER_SIZE = 250;
19
private const int TEMP_PIXBUF_SIZE = 768;
21
private class StraightenToolWindow : EditingToolWindow {
22
public const int CONTROL_SPACING = 8;
24
public Gtk.HScale angle_slider = new Gtk.HScale.with_range(MIN_ANGLE, MAX_ANGLE, INCREMENT);
25
public Gtk.Label angle_label = new Gtk.Label("");
26
public Gtk.Label description_label = new Gtk.Label(_("Angle:"));
27
public Gtk.Button ok_button = new Gtk.Button.from_stock(Gtk.Stock.OK);
28
public Gtk.Button cancel_button = new Gtk.Button.from_stock(Gtk.Stock.CANCEL);
29
public Gtk.Button reset_button = new Gtk.Button.with_mnemonic(_("_Reset"));
32
* Prepare straighten tool's window for use and initialize all its controls.
34
* @param container The application's main window.
36
public StraightenToolWindow(Gtk.Window container) {
39
angle_slider.set_min_slider_size(MIN_SLIDER_SIZE);
40
angle_slider.set_value(0.0);
41
angle_slider.set_draw_value(false);
43
description_label.set_padding(0,0);
44
angle_label.set_padding(0,0);
46
Gtk.HBox slider_layout = new Gtk.HBox(false, CONTROL_SPACING);
47
slider_layout.add(angle_slider);
49
Gtk.HBox button_layout = new Gtk.HBox(false, CONTROL_SPACING);
50
button_layout.add(cancel_button);
51
button_layout.add(reset_button);
52
button_layout.add(ok_button);
54
Gtk.HBox main_layout = new Gtk.HBox(false, 0);
55
main_layout.add(description_label);
56
main_layout.add(slider_layout);
57
main_layout.add(angle_label);
58
main_layout.add(button_layout);
62
reset_button.clicked.connect(on_reset_clicked);
64
set_position(Gtk.WindowPosition.CENTER_ON_PARENT);
67
private void on_reset_clicked() {
68
angle_slider.set_value(0.0);
72
private StraightenToolWindow window;
74
// the incoming image itself.
75
private Cairo.Surface photo_surf;
76
Dimensions image_dims;
78
// temporary surface we'll draw the rotated image into.
79
private Cairo.Surface rotate_surf;
80
private Cairo.Context rotate_ctx;
82
private Dimensions last_viewport;
83
private int view_width;
84
private int view_height;
85
private double photo_angle = 0.0;
87
// should we use a nicer-but-more-expensive filter
88
// when repainting the rotated image?
89
bool use_high_qual = false;
91
private Gdk.Point crop_center; // original center in image coordinates
92
private int crop_width;
93
private int crop_height;
95
// As the crop box rotates, we adjust its center and/or scale it so that it fits in the image.
96
private Gdk.Point rotated_center; // in image coordinates
97
private double rotate_scale; // always <= 1.0: rotation may shrink but not grow box
99
private double preview_scale;
101
private StraightenTool() {
104
public static StraightenTool factory() {
105
return new StraightenTool();
109
* @brief Signal handler for when the 'OK' button has been clicked. Computes where a previously-
110
* set crop region should have rotated to (to match the Photo's straightening angle).
112
* @note After this has been called against a Photo, it will always have a crop region; in the
113
* case of a previously-uncropped Photo, the crop region will be set to the original dimensions
114
* of the photo and centered at the Photo's center.
116
private void on_ok_clicked() {
117
assert(canvas.get_photo() != null);
119
// compute where the crop box should be now and set the image's
120
// current crop to it
121
double slider_val = window.angle_slider.get_value();
123
Gdk.Point new_crop_center = rotate_point_arb(rotated_center,
124
image_dims.width, image_dims.height, slider_val);
126
StraightenCommand command = new StraightenCommand(
127
canvas.get_photo(), slider_val,
128
Box.from_center(new_crop_center,
129
(int) (rotate_scale * crop_width), (int) (rotate_scale * crop_height)),
130
Resources.STRAIGHTEN_LABEL, Resources.STRAIGHTEN_TOOLTIP);
131
AppWindow.get_command_manager().execute(command);
137
private void on_cancel_clicked() {
142
private bool on_slider_released(Gdk.EventButton geb) {
143
use_high_qual = true;
144
this.canvas.repaint();
148
private void prepare_image() {
149
Dimensions canvas_dims = canvas.get_surface_dim();
150
Dimensions viewport = canvas_dims.with_max(TEMP_PIXBUF_SIZE, TEMP_PIXBUF_SIZE);
151
if (viewport == last_viewport)
154
last_viewport = viewport;
156
Gdk.Pixbuf low_res_tmp = null;
159
canvas.get_photo().get_pixbuf_with_options(Scaling.for_viewport(viewport, false),
160
Photo.Exception.STRAIGHTEN | Photo.Exception.CROP);
162
warning("A pixbuf for %s couldn't be fetched.", canvas.get_photo().to_string());
163
low_res_tmp = new Gdk.Pixbuf(Gdk.Colorspace.RGB, false, 8, 1, 1);
166
preview_scale = low_res_tmp.width / (double) image_dims.width;
168
// copy image data from photo into a cairo surface.
169
photo_surf = new Cairo.ImageSurface(Cairo.Format.ARGB32, low_res_tmp.width, low_res_tmp.height);
170
Cairo.Context ctx = new Cairo.Context(photo_surf);
171
Gdk.cairo_set_source_pixbuf(ctx, low_res_tmp, 0, 0);
172
ctx.rectangle(0, 0, low_res_tmp.width, low_res_tmp.height);
176
// prepare rotation surface and context. we paint a rotated,
177
// low-res copy of the image into it, followed by a faint grid.
178
view_width = (int) (crop_width * preview_scale);
179
view_height = (int) (crop_height * preview_scale);
180
rotate_surf = new Cairo.ImageSurface(Cairo.Format.ARGB32, view_width, view_height);
181
rotate_ctx = new Cairo.Context(rotate_surf);
184
// Adjust the rotated crop box so that it fits in the source image.
185
void adjust_for_rotation() {
186
double width, height;
187
compute_arb_rotated_size(crop_width, crop_height, photo_angle, out width, out height);
189
// First compute a scaling factor that will let the rotated box fit in the image.
190
rotate_scale = double.min(image_dims.width / width, image_dims.height / height);
191
rotate_scale = double.min(rotate_scale, 1.0);
193
// Now nudge the box into the image if necessary.
194
rotated_center = crop_center;
195
int radius_x = (int) (rotate_scale * width / 2);
196
int radius_y = (int) (rotate_scale * height / 2);
197
rotated_center.x = rotated_center.x.clamp(radius_x, image_dims.width - radius_x);
198
rotated_center.y = rotated_center.y.clamp(radius_y, image_dims.height - radius_y);
202
* @brief Spawn the tool window, set up the scratch surfaces and prepare the straightening
203
* tool for use. If a valid pixbuf of the incoming Photo can't be loaded for any
204
* reason, the tool will use a 1x1 temporary image instead to avoid crashing.
206
* @param canvas The PhotoCanvas the tool's output should be painted to.
208
public override void activate(PhotoCanvas canvas) {
209
base.activate(canvas);
210
this.canvas = canvas;
211
bind_canvas_handlers(this.canvas);
213
image_dims = canvas.get_photo().get_dimensions(
214
Photo.Exception.STRAIGHTEN | Photo.Exception.CROP);
217
if (!canvas.get_photo().get_crop(out crop_region)) {
218
crop_region.left = 0;
219
crop_region.right = image_dims.width;
222
crop_region.bottom = image_dims.height;
225
// read the photo's current angle and start the tool with the slider set to that value. we
226
// also use this to de-rotate the crop region
227
double incoming_angle = 0.0;
228
canvas.get_photo().get_straighten(out incoming_angle);
230
// Translate the crop center to image coordinates.
231
crop_center = derotate_point_arb(crop_region.get_center(),
232
image_dims.width, image_dims.height, incoming_angle);
233
crop_width = crop_region.get_width();
234
crop_height = crop_region.get_height();
236
adjust_for_rotation();
240
window = new StraightenToolWindow(canvas.get_container());
241
bind_window_handlers();
243
// prepare ths slider for display
244
window.angle_slider.set_value(incoming_angle);
245
photo_angle = incoming_angle;
247
string tmp = "%2.1f°".printf(incoming_angle);
248
window.angle_label.set_text(tmp);
254
* Tears down the tool window and frees resources.
256
public override void deactivate() {
259
unbind_window_handlers();
265
if (canvas != null) {
266
unbind_canvas_handlers(canvas);
272
private void bind_canvas_handlers(PhotoCanvas canvas) {
273
canvas.resized_scaled_pixbuf.connect(on_resized_pixbuf);
276
private void unbind_canvas_handlers(PhotoCanvas canvas) {
277
canvas.resized_scaled_pixbuf.disconnect(on_resized_pixbuf);
280
private void bind_window_handlers() {
281
window.ok_button.clicked.connect(on_ok_clicked);
282
window.cancel_button.clicked.connect(on_cancel_clicked);
283
window.angle_slider.value_changed.connect(on_angle_changed);
284
window.angle_slider.button_release_event.connect(on_slider_released);
287
private void unbind_window_handlers() {
288
window.ok_button.clicked.disconnect(on_ok_clicked);
289
window.cancel_button.clicked.disconnect(on_cancel_clicked);
290
window.angle_slider.value_changed.disconnect(on_angle_changed);
291
window.angle_slider.button_release_event.disconnect(on_slider_released);
294
private void on_angle_changed() {
295
photo_angle = window.angle_slider.get_value();
296
string tmp = "%2.1f°".printf(window.angle_slider.get_value());
297
window.angle_label.set_text(tmp);
299
use_high_qual = false;
301
adjust_for_rotation();
302
this.canvas.repaint();
306
* @brief Called by the EditingHostPage when a resize event occurs.
308
private void on_resized_pixbuf(Dimensions old_dim, Gdk.Pixbuf scaled, Gdk.Rectangle scaled_position) {
313
* Returns a reference to the current StraightenTool instance's tool window;
314
* the PhotoPage uses this to control the tool window's positioning, etc.
316
public override EditingToolWindow? get_tool_window() {
321
* Render a smaller, rotated version of the image, with a grid superimposed over it.
323
* @param ctx The rendering context of a 'scratch' Cairo surface. The tool makes its own
324
* surfaces and contexts so it can have things set up exactly like it wants them, so
327
public override void paint(Cairo.Context ctx) {
328
int w = canvas.get_drawing_window().get_width();
329
int h = canvas.get_drawing_window().get_height();
331
// draw the rotated photo and grid.
332
draw_rotated_source(photo_surf, rotate_ctx, view_width, view_height, photo_angle);
333
draw_superimposed_grid(rotate_ctx, view_width, view_height);
335
// fill region behind the rotation surface with neutral color.
336
canvas.get_default_ctx().identity_matrix();
337
canvas.get_default_ctx().set_source_rgba(0.0, 0.0, 0.0, 1.0);
338
canvas.get_default_ctx().rectangle(0, 0, w, h);
339
canvas.get_default_ctx().fill();
341
// copy the composited result to the main window.
342
canvas.get_default_ctx().translate((w - view_width) / 2.0, (h - view_height) / 2.0);
343
canvas.get_default_ctx().set_source_surface(rotate_surf, 0, 0);
344
canvas.get_default_ctx().rectangle(0, 0, view_width, view_height);
345
canvas.get_default_ctx().fill();
346
canvas.get_default_ctx().paint();
348
// reset the 'modelview' matrix, since when the canvas is not in
349
// 'tool' mode, it 'expects' things to be set up a certain way.
350
canvas.get_default_ctx().identity_matrix();
354
* Copy a rotated version of the source image onto the destination
357
* @param src_surf A Cairo surface containing the source image.
358
* @param dest_ctx The rendering context of the destination image.
359
* @param src_width The width of the image data in src_surf in pixels.
360
* @param src_height The height of the image data in src_surf in pixels.
361
* @param angle The angle the source image should be rotated by, in degrees.
363
private void draw_rotated_source(Cairo.Surface src_surf, Cairo.Context dest_ctx,
364
int src_width, int src_height, double angle) {
365
double angle_internal = degrees_to_radians(angle);
367
// fill area behind rotated image with neutral color to avoid 'ghosting'.
368
// this should be removed after #4612 has been addressed.
369
dest_ctx.identity_matrix();
370
dest_ctx.set_source_rgba(0.0, 0.0, 0.0, 1.0);
371
dest_ctx.rectangle(0, 0, view_width, view_height);
374
// rotate the image, taking into account that the position of the
375
// upper left corner must change depending on rotation amount and direction
376
// and translate so center of preview crop region is now center of rotation
377
dest_ctx.identity_matrix();
379
dest_ctx.translate(view_width / 2, view_height / 2);
380
dest_ctx.scale(1.0 / rotate_scale, 1.0 / rotate_scale);
381
dest_ctx.rotate(angle_internal);
382
dest_ctx.translate(- rotated_center.x * preview_scale, - rotated_center.y * preview_scale);
384
dest_ctx.set_source_surface(src_surf, 0, 0);
385
dest_ctx.get_source().set_filter(use_high_qual ? Cairo.Filter.BEST : Cairo.Filter.NEAREST);
386
dest_ctx.rectangle(0, 0, src_width, src_height);
392
* Superimpose a faint grid over the supplied image.
394
* @param width The total width the grid should be drawn to.
395
* @param height The total height the grid should be drawn to.
396
* @param dest_ctx The rendering context of the destination image.
398
private void draw_superimposed_grid(Cairo.Context dest_ctx, int width, int height) {
399
int half_width = width / 2;
400
int quarter_width = width / 4;
402
int half_height = height / 2;
403
int quarter_height = height / 4;
405
dest_ctx.identity_matrix();
406
dest_ctx.set_source_rgba(1.0, 1.0, 1.0, 1.0);
408
canvas.draw_horizontal_line(dest_ctx, 0, 0, width, false);
409
canvas.draw_horizontal_line(dest_ctx, 0, half_height, width, false);
410
canvas.draw_horizontal_line(dest_ctx, 0, view_height, width, false);
412
canvas.draw_vertical_line(dest_ctx, 0, 0, height, false);
413
canvas.draw_vertical_line(dest_ctx, half_width, 0, height, false);
414
canvas.draw_vertical_line(dest_ctx, width, 0, height, false);
416
dest_ctx.set_source_rgba(1.0, 1.0, 1.0, 0.33);
418
canvas.draw_horizontal_line(dest_ctx, 0, quarter_height, width, false);
419
canvas.draw_horizontal_line(dest_ctx, 0, half_height + quarter_height, width, false);
420
canvas.draw_vertical_line(dest_ctx, quarter_width, 0, height, false);
421
canvas.draw_vertical_line(dest_ctx, half_width + quarter_width, 0, height, false);