115
124
public signal void resized_scaled_pixbuf(Dimensions old_dim, Gdk.Pixbuf scaled,
116
125
Gdk.Rectangle scaled_position);
127
public Gdk.Rectangle unscaled_to_raw_rect(Gdk.Rectangle rectangle) {
128
return photo.unscaled_to_raw_rect(rectangle);
118
131
public Gdk.Point active_to_unscaled_point(Gdk.Point active_point) {
119
132
Gdk.Rectangle scaled_position = get_scaled_pixbuf_position();
120
133
Dimensions unscaled_dims = photo.get_dimensions();
259
272
public void paint_pixbuf_area(Gdk.Pixbuf pixbuf, Box source_area) {
260
273
default_ctx.save();
261
274
if (pixbuf.get_has_alpha()) {
262
Gdk.cairo_set_source_color(default_ctx, container.style.black);
275
set_source_color_from_string(default_ctx, "#000");
263
276
default_ctx.rectangle(scaled_position.x + source_area.left,
264
277
scaled_position.y + source_area.top,
265
278
source_area.get_width(), source_area.get_height());
323
public void draw_horizontal_line(Cairo.Context ctx, int x, int y, int width) {
324
x += scaled_position.x;
325
y += scaled_position.y;
337
* Draw a horizontal line into the specified Cairo context at the specified position, taking
338
* into account the scaled position of the image unless directed otherwise.
340
* @param ctx The drawing context of the surface we're drawing to.
341
* @param x The horizontal position to place the line at.
342
* @param y The vertical position to place the line at.
343
* @param width The length of the line.
344
* @param use_scaled_pos Whether to use absolute window positioning or take into account the
345
* position of the scaled image.
347
public void draw_horizontal_line(Cairo.Context ctx, int x, int y, int width, bool use_scaled_pos = true) {
348
if (use_scaled_pos) {
349
x += scaled_position.x;
350
y += scaled_position.y;
327
353
ctx.move_to(x + 0.5, y + 0.5);
328
354
ctx.line_to(x + width - 1, y + 0.5);
332
public void draw_vertical_line(Cairo.Context ctx, int x, int y, int height) {
333
x += scaled_position.x;
334
y += scaled_position.y;
359
* Draw a vertical line into the specified Cairo context at the specified position, taking
360
* into account the scaled position of the image unless directed otherwise.
362
* @param ctx The drawing context of the surface we're drawing to.
363
* @param x The horizontal position to place the line at.
364
* @param y The vertical position to place the line at.
365
* @param width The length of the line.
366
* @param use_scaled_pos Whether to use absolute window positioning or take into account the
367
* position of the scaled image.
369
public void draw_vertical_line(Cairo.Context ctx, int x, int y, int height, bool use_scaled_pos = true) {
370
if (use_scaled_pos) {
371
x += scaled_position.x;
372
y += scaled_position.y;
336
375
ctx.move_to(x + 0.5, y + 0.5);
337
376
ctx.line_to(x + 0.5, y + height - 1);
804
private void on_width_insert_text(string text, int length, void *position) {
805
on_entry_insert_text(crop_tool_window.custom_width_entry, text, length, position);
808
private void on_height_insert_text(string text, int length, void *position) {
809
on_entry_insert_text(crop_tool_window.custom_height_entry, text, length, position);
812
private void on_entry_insert_text(Gtk.Entry sender, string text, int length, void *position) {
843
private void on_width_insert_text(string text, int length, ref int position) {
844
on_entry_insert_text(crop_tool_window.custom_width_entry, text, length, ref position);
847
private void on_height_insert_text(string text, int length, ref int position) {
848
on_entry_insert_text(crop_tool_window.custom_height_entry, text, length, ref position);
851
private void on_entry_insert_text(Gtk.Entry sender, string text, int length, ref int position) {
813
852
if (entry_insert_in_progress)
961
1000
if (user_aspect_ratio == ANY_ASPECT_RATIO)
964
float scaled_width = (float) crop.get_width();
965
float scaled_height = (float) crop.get_height();
966
float scaled_center_x = ((float) crop.left) + (scaled_width / 2.0f);
967
float scaled_center_y = ((float) crop.top) + (scaled_height / 2.0f);
968
float scaled_aspect_ratio = scaled_width / scaled_height;
970
// Crop positioning in the presence of constraint is a three-phase process
972
// PHASE 1: Naively rescale the width and the height of the box so that it has the
973
// user-specified aspect ratio. Even in this initial transformation, the
974
// box's center and minor axis length are preserved. Preserving the center
975
// is especially important since this way the subject that the user has framed
976
// within the crop reticle is preserved.
977
if (scaled_aspect_ratio > 1.0f)
978
scaled_width = scaled_height;
980
scaled_height = scaled_width;
981
scaled_width *= user_aspect_ratio;
983
// PHASE 2: Now that the box has the correct aspect ratio, grow it or shrink it such
984
// that it has the same area that it had prior to constraint. This prevents
985
// the box from growing or shrinking erratically as constraints are set and
1003
// PHASE 1: Scale to the desired aspect ratio, preserving area and center.
987
1004
float old_area = (float) (crop.get_width() * crop.get_height());
988
float new_area = scaled_width * scaled_height;
989
float area_correct_factor = (float) Math.sqrt(old_area / new_area);
990
scaled_width *= area_correct_factor;
991
scaled_height *= area_correct_factor;
993
// PHASE 3: The new crop box may have edges that fall outside of the boundaries of
994
// the photo. Here, we rescale it such that it fits within the boundaries
996
int photo_right_edge = canvas.get_scaled_pixbuf_position().width - 1;
997
int photo_bottom_edge = canvas.get_scaled_pixbuf_position().height - 1;
999
int new_box_left = (int) ((scaled_center_x - (scaled_width / 2.0f)));
1000
int new_box_right = (int) ((scaled_center_x + (scaled_width / 2.0f)));
1001
int new_box_top = (int) ((scaled_center_y - (scaled_height / 2.0f)));
1002
int new_box_bottom = (int) ((scaled_center_y + (scaled_height / 2.0f)));
1004
if(new_box_left < 0) new_box_left = 0;
1005
if(new_box_top < 0) new_box_top = 0;
1006
if(new_box_right > photo_right_edge) new_box_right = photo_right_edge;
1007
if(new_box_bottom > photo_bottom_edge) new_box_bottom = photo_bottom_edge;
1009
Box new_crop_box = Box((int) (new_box_left),
1010
(int) (new_box_top),
1011
(int) (new_box_right),
1012
(int) (new_box_bottom));
1014
return new_crop_box;
1005
crop.adjust_height((int) Math.sqrt(old_area / user_aspect_ratio));
1006
crop.adjust_width((int) Math.sqrt(old_area * user_aspect_ratio));
1008
// PHASE 2: Crop to the image boundary.
1009
Dimensions image_size = get_photo_dimensions();
1011
canvas.get_photo().get_straighten(out angle);
1012
crop = clamp_inside_rotated_image(crop, image_size.width, image_size.height, angle, false);
1014
// PHASE 3: Crop down to the aspect ratio if necessary.
1015
if (crop.get_width() >= crop.get_height() * user_aspect_ratio) // possibly too wide
1016
crop.adjust_width((int) (crop.get_height() * user_aspect_ratio));
1017
else // possibly too tall
1018
crop.adjust_height((int) (crop.get_width() / user_aspect_ratio));
1017
1023
public override void activate(PhotoCanvas canvas) {
1036
1042
// set up the constraint combo box
1037
1043
crop_tool_window.constraint_combo.set_model(constraint_list);
1038
crop_tool_window.constraint_combo.set_active(Config.Facade.get_instance().get_last_crop_menu_choice());
1044
if(!canvas.get_photo().has_crop()) {
1045
crop_tool_window.constraint_combo.set_active(Config.Facade.get_instance().get_last_crop_menu_choice());
1040
1048
// set up the pivot reticle button
1041
1049
update_pivot_button_state();
1042
1050
reticle_orientation = ReticleOrientation.LANDSCAPE;
1080
1088
crop_tool_window.hide();
1082
1090
// was 'custom' the most-recently-chosen menu item?
1083
if (constraints[Config.Facade.get_instance().get_last_crop_menu_choice()].aspect_ratio ==
1084
CUSTOM_ASPECT_RATIO) {
1085
// yes, switch to custom mode, make the entry fields appear.
1086
set_custom_constraint_mode();
1091
if(!canvas.get_photo().has_crop()) {
1092
if (constraints[Config.Facade.get_instance().get_last_crop_menu_choice()].aspect_ratio ==
1093
CUSTOM_ASPECT_RATIO) {
1094
// yes, switch to custom mode, make the entry fields appear.
1095
set_custom_constraint_mode();
1089
1099
// since we no longer just run with the default, but rather
1190
1200
public override Gdk.Pixbuf? get_display_pixbuf(Scaling scaling, Photo photo,
1191
1201
out Dimensions max_dim) throws Error {
1192
// show the uncropped photo for editing, but return null if no crop so the current pixbuf
1194
if (!photo.has_crop()) {
1195
max_dim = Dimensions();
1200
max_dim = photo.get_original_dimensions();
1202
max_dim = photo.get_dimensions(Photo.Exception.CROP);
1202
1204
return photo.get_pixbuf_with_options(scaling, Photo.Exception.CROP);
1205
1207
private void prepare_ctx(Cairo.Context ctx, Dimensions dim) {
1206
1208
wide_black_ctx = new Cairo.Context(ctx.get_target());
1207
Gdk.cairo_set_source_color(wide_black_ctx, fetch_color("#000"));
1209
set_source_color_from_string(wide_black_ctx, "#000");
1208
1210
wide_black_ctx.set_line_width(1);
1210
1212
wide_white_ctx = new Cairo.Context(ctx.get_target());
1211
Gdk.cairo_set_source_color(wide_white_ctx, fetch_color("#FFF"));
1213
set_source_color_from_string(wide_white_ctx, "#FFF");
1212
1214
wide_white_ctx.set_line_width(1);
1214
1216
thin_white_ctx = new Cairo.Context(ctx.get_target());
1215
Gdk.cairo_set_source_color(thin_white_ctx, fetch_color("#FFF"));
1217
set_source_color_from_string(thin_white_ctx, "#FFF");
1216
1218
thin_white_ctx.set_line_width(0.5);
1219
1221
private void on_resized_pixbuf(Dimensions old_dim, Gdk.Pixbuf scaled, Gdk.Rectangle scaled_position) {
1220
1222
Dimensions new_dim = Dimensions.for_pixbuf(scaled);
1221
Dimensions uncropped_dim = canvas.get_photo().get_original_dimensions();
1223
Dimensions uncropped_dim = canvas.get_photo().get_dimensions(Photo.Exception.CROP);
1223
1225
// rescale to full crop
1224
1226
Box crop = scaled_crop.get_scaled_similar(old_dim, uncropped_dim);
1313
1315
// scale screen-coordinate crop to photo's coordinate system
1314
1316
Box crop = scaled_crop.get_scaled_similar(
1315
1317
Dimensions.for_rectangle(canvas.get_scaled_pixbuf_position()),
1316
canvas.get_photo().get_original_dimensions());
1318
canvas.get_photo().get_dimensions(Photo.Exception.CROP));
1318
1320
// crop the current pixbuf and offer it to the editing host
1319
1321
Gdk.Pixbuf cropped = new Gdk.Pixbuf.subpixbuf(canvas.get_scaled_pixbuf(), scaled_crop.left,
1383
private void revert_crop(out int left, out int top, out int right, out int bottom) {
1384
left = scaled_crop.left;
1385
top = scaled_crop.top;
1386
right = scaled_crop.right;
1387
bottom = scaled_crop.bottom;
1390
1385
private int eval_radial_line(double center_x, double center_y, double bounds_x,
1391
1386
double bounds_y, double user_x) {
1392
1387
double decision_slope = (bounds_y - center_y) / (bounds_x - center_x);
1395
1390
return (int) (decision_slope * user_x + decision_intercept);
1393
// Return the dimensions of the uncropped source photo scaled to canvas coordinates.
1394
private Dimensions get_photo_dimensions() {
1395
Dimensions photo_dims = canvas.get_photo().get_dimensions(Photo.Exception.CROP);
1396
Dimensions surface_dims = canvas.get_surface_dim();
1397
double scale_factor = double.min((double) surface_dims.width / photo_dims.width,
1398
(double) surface_dims.height / photo_dims.height);
1399
scale_factor = double.min(scale_factor, 1.0);
1401
photo_dims = canvas.get_photo().get_dimensions(
1402
Photo.Exception.CROP | Photo.Exception.STRAIGHTEN);
1404
return { (int) (photo_dims.width * scale_factor),
1405
(int) (photo_dims.height * scale_factor) };
1398
1408
private bool on_canvas_manipulation(int x, int y) {
1399
1409
Gdk.Rectangle scaled_pos = canvas.get_scaled_pixbuf_position();
1590
1598
// constraint).
1591
1599
int width = right - left + 1;
1592
1600
int height = bottom - top + 1;
1602
Dimensions photo_dims = get_photo_dimensions();
1604
canvas.get_photo().get_straighten(out angle);
1593
1607
if (get_constraint_aspect_ratio() == ANY_ASPECT_RATIO) {
1598
if (right > photo_right_edge)
1599
right = photo_right_edge;
1600
if (bottom > photo_bottom_edge)
1601
bottom = photo_bottom_edge;
1603
1608
width = right - left + 1;
1604
1609
height = bottom - top + 1;
1649
// preliminary crop region has been chosen, now clamp it inside the
1652
new_crop = clamp_inside_rotated_image(
1653
Box(left, top, right, bottom),
1654
photo_dims.width, photo_dims.height, angle,
1655
in_manipulation == BoxLocation.INSIDE);
1644
if ((left < 0) || (top < 0) || (right > photo_right_edge) ||
1645
(bottom > photo_bottom_edge) || (width < CROP_MIN_SIZE) ||
1646
(height < CROP_MIN_SIZE)) {
1647
revert_crop(out left, out top, out right, out bottom);
1658
// one of the constrained modes is active; revert instead of clamping so
1659
// that aspect ratio stays intact
1661
new_crop = Box(left, top, right, bottom);
1662
Box adjusted = clamp_inside_rotated_image(new_crop,
1663
photo_dims.width, photo_dims.height, angle,
1664
in_manipulation == BoxLocation.INSIDE);
1666
if (adjusted != new_crop || width < CROP_MIN_SIZE || height < CROP_MIN_SIZE) {
1667
new_crop = scaled_crop; // revert crop move
1651
Box new_crop = Box(left, top, right, bottom);
1653
1671
if (in_manipulation != BoxLocation.INSIDE)
1654
1672
crop_resized(new_crop);
1707
1725
erase_crop_tool(scaled_crop);
1708
1726
canvas.invalidate_area(scaled_crop);
1710
Box scaled_horizontal;
1711
Box scaled_vertical;
1714
BoxComplements complements = scaled_crop.shifted_complements(new_crop, out scaled_horizontal,
1715
out scaled_vertical, out new_horizontal, out new_vertical);
1717
if (complements == BoxComplements.HORIZONTAL || complements == BoxComplements.BOTH) {
1718
// paint in the horizontal complements appropriately
1719
set_area_alpha(scaled_horizontal, 0.5);
1720
set_area_alpha(new_horizontal, 0.0);
1723
if (complements == BoxComplements.VERTICAL || complements == BoxComplements.BOTH) {
1724
// paint in vertical complements appropriately
1725
set_area_alpha(scaled_vertical, 0.5);
1726
set_area_alpha(new_vertical, 0.0);
1729
if (complements == BoxComplements.NONE) {
1730
// this means the two boxes have no intersection, not that they're equal ... since
1731
// there's no intersection, fill in both new and old with apropriate pixbufs
1732
set_area_alpha(scaled_crop, 0.5);
1733
set_area_alpha(new_crop, 0.0);
1728
set_area_alpha(scaled_crop, 0.5);
1729
set_area_alpha(new_crop, 0.0);
1736
1732
// paint crop in new location
1737
1733
paint_crop_tool(new_crop);
2535
2531
private void prepare_ctx(Cairo.Context ctx, Dimensions dim) {
2536
2532
wider_gray_ctx = new Cairo.Context(ctx.get_target());
2537
Gdk.cairo_set_source_color(wider_gray_ctx, fetch_color("#111"));
2533
set_source_color_from_string(wider_gray_ctx, "#111");
2538
2534
wider_gray_ctx.set_line_width(3);
2540
2536
thin_white_ctx = new Cairo.Context(ctx.get_target());
2541
Gdk.cairo_set_source_color(thin_white_ctx, fetch_color("#FFF"));
2537
set_source_color_from_string(thin_white_ctx, "#FFF");
2542
2538
thin_white_ctx.set_line_width(1);
2566
2562
canvas.user_to_active_rect(bounds_rect_user);
2567
2563
Gdk.Rectangle bounds_rect_unscaled =
2568
2564
canvas.active_to_unscaled_rect(bounds_rect_active);
2565
Gdk.Rectangle bounds_rect_raw =
2566
canvas.unscaled_to_raw_rect(bounds_rect_unscaled);
2570
RedeyeInstance instance_unscaled =
2571
RedeyeInstance.from_bounds_rect(bounds_rect_unscaled);
2568
RedeyeInstance instance_raw =
2569
RedeyeInstance.from_bounds_rect(bounds_rect_raw);
2573
2571
// transform screen coords back to image coords,
2574
2572
// taking into account straightening angle.
2575
int img_w = canvas.get_photo().get_master_dimensions().width;
2576
int img_h = canvas.get_photo().get_master_dimensions().height;
2573
Dimensions dimensions = canvas.get_photo().get_dimensions(
2574
Photo.Exception.STRAIGHTEN | Photo.Exception.CROP);
2578
2576
double theta = 0.0;
2580
2578
canvas.get_photo().get_straighten(out theta);
2582
instance_unscaled.center = rotate_point_arb(instance_unscaled.center, img_w, img_h, theta);
2580
instance_raw.center = derotate_point_arb(instance_raw.center,
2581
dimensions.width, dimensions.height, theta);
2584
RedeyeCommand command = new RedeyeCommand(canvas.get_photo(), instance_unscaled,
2583
RedeyeCommand command = new RedeyeCommand(canvas.get_photo(), instance_raw,
2585
2584
Resources.RED_EYE_LABEL, Resources.RED_EYE_TOOLTIP);
2586
2585
AppWindow.get_command_manager().execute(command);