~ubuntu-branches/ubuntu/quantal/shotwell/quantal

« back to all changes in this revision

Viewing changes to src/editing_tools/StraightenTool.vala

  • Committer: Package Import Robot
  • Author(s): Robert Ancell
  • Date: 2012-02-21 13:52:58 UTC
  • mto: This revision was merged to the branch mainline in revision 47.
  • Revision ID: package-import@ubuntu.com-20120221135258-ao9jiib5qicomq7q
Tags: upstream-0.11.92
Import upstream version 0.11.92

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
 
 
2
/* Copyright 2009-2011 Yorba Foundation
 
3
 *
 
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.
 
6
 */
 
7
 
 
8
namespace EditingTools {
 
9
 
 
10
/**
 
11
 * An editing tool that allows one to introduce or remove a Dutch angle from
 
12
 * a photograph.
 
13
 */
 
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;
 
20
    
 
21
    private class StraightenToolWindow : EditingToolWindow {
 
22
        public const int CONTROL_SPACING = 8;
 
23
 
 
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"));
 
30
 
 
31
        /**
 
32
         * Prepare straighten tool's window for use and initialize all its controls.
 
33
         *
 
34
         * @param container The application's main window.
 
35
         */
 
36
        public StraightenToolWindow(Gtk.Window container) {
 
37
            base(container);
 
38
 
 
39
            angle_slider.set_min_slider_size(MIN_SLIDER_SIZE);
 
40
            angle_slider.set_value(0.0);
 
41
            angle_slider.set_draw_value(false);
 
42
 
 
43
            description_label.set_padding(0,0);
 
44
            angle_label.set_padding(0,0);
 
45
 
 
46
            Gtk.HBox slider_layout = new Gtk.HBox(false, CONTROL_SPACING);
 
47
            slider_layout.add(angle_slider);
 
48
 
 
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);
 
53
 
 
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);
 
59
 
 
60
            add(main_layout);
 
61
 
 
62
            reset_button.clicked.connect(on_reset_clicked);
 
63
 
 
64
            set_position(Gtk.WindowPosition.CENTER_ON_PARENT);
 
65
        }
 
66
 
 
67
        private void on_reset_clicked() {
 
68
            angle_slider.set_value(0.0);
 
69
        }
 
70
    }
 
71
 
 
72
    private StraightenToolWindow window;
 
73
 
 
74
    // the incoming image itself.
 
75
    private Cairo.Surface photo_surf;
 
76
    Dimensions image_dims;
 
77
 
 
78
    // temporary surface we'll draw the rotated image into.
 
79
    private Cairo.Surface rotate_surf;
 
80
    private Cairo.Context rotate_ctx;
 
81
 
 
82
    private Dimensions last_viewport;
 
83
    private int view_width;
 
84
    private int view_height;
 
85
    private double photo_angle = 0.0;
 
86
 
 
87
    // should we use a nicer-but-more-expensive filter
 
88
    // when repainting the rotated image?
 
89
    bool use_high_qual = false;
 
90
 
 
91
    private Gdk.Point crop_center;  // original center in image coordinates
 
92
    private int crop_width;
 
93
    private int crop_height;
 
94
    
 
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
 
98
    
 
99
    private double preview_scale;
 
100
 
 
101
    private StraightenTool() {
 
102
    }
 
103
 
 
104
    public static StraightenTool factory() {
 
105
        return new StraightenTool();
 
106
    }
 
107
 
 
108
    /**
 
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).
 
111
     *
 
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.
 
115
     */
 
116
    private void on_ok_clicked() {
 
117
        assert(canvas.get_photo() != null);
 
118
 
 
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();
 
122
 
 
123
        Gdk.Point new_crop_center = rotate_point_arb(rotated_center,
 
124
            image_dims.width, image_dims.height, slider_val);
 
125
 
 
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);            
 
132
 
 
133
        canvas.repaint();
 
134
        deactivate();
 
135
    }
 
136
 
 
137
    private void on_cancel_clicked() {
 
138
        canvas.repaint();
 
139
        deactivate();
 
140
    }
 
141
 
 
142
    private bool on_slider_released(Gdk.EventButton geb) {
 
143
        use_high_qual = true;
 
144
        this.canvas.repaint();
 
145
        return false;
 
146
    }
 
147
 
 
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)
 
152
            return;     // no change
 
153
 
 
154
        last_viewport = viewport;
 
155
            
 
156
        Gdk.Pixbuf low_res_tmp = null;
 
157
        try {
 
158
            low_res_tmp =
 
159
                canvas.get_photo().get_pixbuf_with_options(Scaling.for_viewport(viewport, false),
 
160
                    Photo.Exception.STRAIGHTEN | Photo.Exception.CROP);
 
161
        } catch (Error e) {
 
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);
 
164
        }
 
165
 
 
166
        preview_scale = low_res_tmp.width / (double) image_dims.width;
 
167
 
 
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);
 
173
        ctx.fill();
 
174
        ctx.paint();
 
175
 
 
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);
 
182
    }
 
183
 
 
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);
 
188
        
 
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);
 
192
        
 
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);
 
199
    }
 
200
 
 
201
    /**
 
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.
 
205
     *
 
206
     * @param canvas The PhotoCanvas the tool's output should be painted to.
 
207
     */
 
208
    public override void activate(PhotoCanvas canvas) {
 
209
        base.activate(canvas);
 
210
        this.canvas = canvas;
 
211
        bind_canvas_handlers(this.canvas);
 
212
 
 
213
        image_dims = canvas.get_photo().get_dimensions(
 
214
            Photo.Exception.STRAIGHTEN | Photo.Exception.CROP);
 
215
 
 
216
        Box crop_region;
 
217
        if (!canvas.get_photo().get_crop(out crop_region)) {
 
218
            crop_region.left = 0;
 
219
            crop_region.right = image_dims.width;
 
220
 
 
221
            crop_region.top = 0;
 
222
            crop_region.bottom = image_dims.height;
 
223
        }
 
224
 
 
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);
 
229
 
 
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();
 
235
        
 
236
        adjust_for_rotation();
 
237
 
 
238
        prepare_image();
 
239
 
 
240
        window = new StraightenToolWindow(canvas.get_container());
 
241
        bind_window_handlers();
 
242
 
 
243
        // prepare ths slider for display
 
244
        window.angle_slider.set_value(incoming_angle);
 
245
        photo_angle = incoming_angle;
 
246
 
 
247
        string tmp = "%2.1f°".printf(incoming_angle);
 
248
        window.angle_label.set_text(tmp);
 
249
 
 
250
        window.show_all();
 
251
    }
 
252
 
 
253
    /**
 
254
     * Tears down the tool window and frees resources.
 
255
     */
 
256
    public override void deactivate() {
 
257
        if(window != null) {
 
258
 
 
259
            unbind_window_handlers();
 
260
 
 
261
            window.hide();
 
262
            window = null;
 
263
        }
 
264
 
 
265
        if (canvas != null) {
 
266
            unbind_canvas_handlers(canvas);
 
267
        }
 
268
 
 
269
        base.deactivate();
 
270
    }
 
271
 
 
272
    private void bind_canvas_handlers(PhotoCanvas canvas) {
 
273
        canvas.resized_scaled_pixbuf.connect(on_resized_pixbuf);
 
274
    }
 
275
 
 
276
    private void unbind_canvas_handlers(PhotoCanvas canvas) {
 
277
        canvas.resized_scaled_pixbuf.disconnect(on_resized_pixbuf);
 
278
    }
 
279
 
 
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);
 
285
    }
 
286
 
 
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);
 
292
    }
 
293
 
 
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);
 
298
 
 
299
        use_high_qual = false;
 
300
 
 
301
        adjust_for_rotation();
 
302
        this.canvas.repaint();
 
303
    }
 
304
 
 
305
    /**
 
306
     * @brief Called by the EditingHostPage when a resize event occurs.
 
307
     */
 
308
    private void on_resized_pixbuf(Dimensions old_dim, Gdk.Pixbuf scaled, Gdk.Rectangle scaled_position) {
 
309
        prepare_image();
 
310
    }
 
311
 
 
312
    /**
 
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.
 
315
     */
 
316
    public override EditingToolWindow? get_tool_window() {
 
317
        return window;
 
318
    }
 
319
 
 
320
    /**
 
321
     * Render a smaller, rotated version of the image, with a grid superimposed over it.
 
322
     *
 
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
 
325
     *      it's not used.
 
326
     */
 
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();
 
330
 
 
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);
 
334
 
 
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();
 
340
 
 
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();
 
347
 
 
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();
 
351
    }
 
352
 
 
353
    /**
 
354
     * Copy a rotated version of the source image onto the destination
 
355
     * context.
 
356
     *
 
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.
 
362
     */
 
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);
 
366
 
 
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);
 
372
        dest_ctx.fill();
 
373
 
 
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();
 
378
 
 
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);
 
383
 
 
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);
 
387
        dest_ctx.fill();
 
388
        dest_ctx.paint();
 
389
    }
 
390
 
 
391
    /**
 
392
     * Superimpose a faint grid over the supplied image.
 
393
     *
 
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.
 
397
     */
 
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;
 
401
 
 
402
        int half_height = height / 2;
 
403
        int quarter_height = height / 4;
 
404
 
 
405
        dest_ctx.identity_matrix();
 
406
        dest_ctx.set_source_rgba(1.0, 1.0, 1.0, 1.0);
 
407
 
 
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);
 
411
 
 
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);
 
415
 
 
416
        dest_ctx.set_source_rgba(1.0, 1.0, 1.0, 0.33);
 
417
 
 
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);
 
422
    }
 
423
}
 
424
 
 
425
} // end namespace