commit 4a162390c92941c9432efc5af31ad81f2de2d974
parent de1ffb4e697a3e88ed1768577805177f853ba480
Author: lumidify <nobody@lumidify.org>
Date: Sun, 12 Jul 2026 17:08:30 +0200
Implement copying of selections and select-all
This also reworks a lot of the code so that the real cropping rectangle
is stored, not just the visual rectangle displayed over the scaled image.
Diffstat:
7 files changed, 293 insertions(+), 112 deletions(-)
diff --git a/LICENSE b/LICENSE
@@ -1,4 +1,4 @@
-Copyright (c) 2021-2024 lumidify <nobody@lumidify.org>
+Copyright (c) 2021-2026 lumidify <nobody@lumidify.org>
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
diff --git a/README b/README
@@ -17,6 +17,15 @@ See Makefile for compile-time options.
See croptool.1, croptool_crop.1, and selectool.1 for usage information.
+Note that croptool uses croptool_crop for the actual cropping
+by default in order to avoid depending on other programs like
+ImageMagick. However, croptool_crop is an extremely basic
+cropping program that does not support any features like
+specifying the JPEG compression level and also strips all
+EXIF tags from images. If any of these features are required,
+it is probably a good idea to use ImageMagick for the cropping
+instead (see croptool.1 for some examples).
+
Note: I know the names aren't very creative and might
cause issues if this ever makes its way into any package
repositories. Let me know if you have any better ideas.
diff --git a/TODO b/TODO
@@ -7,9 +7,12 @@
expensive image resizing each time
* Maybe add zooming support
* Maybe optionally show rectangle coordinates on screen
-* Allow to copy/paste selection
* Key for allowing/disallowing rectangle out of image bounds
* Read selections from file - to modify again later
* Better navigation - jump to beginning, etc.
* Some sort of 'multiselectool' that makes it easy to sort
images into different directories.
+* Preserve EXIF data (and other metadata) in croptool_crop.
+* Allow setting the JPEG compression level in croptool_crop
+ (there does not appear to be an option for this in Imlib2,
+ as far as I could tell).
diff --git a/croptool.1 b/croptool.1
@@ -1,4 +1,4 @@
-.Dd May 14, 2024
+.Dd July 12, 2026
.Dt CROPTOOL 1
.Os
.Sh NAME
@@ -17,8 +17,10 @@
.Sh DESCRIPTION
.Nm
shows each of the given images and allows a cropping rectangle to be drawn.
-On exit, the cropping command is printed for each of the files.
-If a file was skipped, nothing is printed for it.
+When the keyboard command
+.Sq q
+is used to exit, the cropping command is printed for each of the images.
+If an image was skipped, nothing is printed for it.
.Sh OPTIONS
.Bl -tag -width Ds
.It Fl m
@@ -96,7 +98,7 @@ image, even if the original cropping rectangle was located partially
outside.
If the cropping rectangle was located entirely outside of the image,
no command is printed for it.
-.Sh KEYBINDS
+.Sh KEYBOARD COMMANDS
.Bl -tag -width Ds
.It ARROW LEFT
Go to the previous image.
@@ -104,7 +106,7 @@ Go to the previous image.
Go to the next image.
.It RETURN
Go to the next image, copying the current cropping rectangle.
-Note that this copies the visual rectangle, not the scaled rectangle
+Note that this copies the visual rectangle, not the rectangle
that is printed for the cropping command.
In other words, when switching to an image that is a different size and
thus scaled differently, the displayed rectangle will stay the same even
@@ -121,6 +123,21 @@ Remove the cropping rectangle of the current image.
Redraw the window.
This is useful when automatic redrawing is disabled with
.Fl m .
+.It CTRL + a
+Select the entire image, i.e. make the cropping rectangle cover the entire
+image.
+.It y
+Copy the current visual rectangle, to be pasted later on another image.
+.It Y
+Copy the current real rectangle, to be pasted later on another image.
+This copies the real cropping rectangle for the original, non-scaled image
+(i.e. the rectangle that is printed for the cropping command).
+Thus, if it is pasted later on an image that has a different size and is
+scaled differently, the visual rectangle over the scaled image might look
+different, but the pixels covered in the original image are the same
+(unless the rectangle goes outside the bounds of the image, of course).
+.It p
+Paste the rectangle that was previously copied.
.It q
Exit the program, printing the cropping command for any images with a
cropping rectangle set.
@@ -164,6 +181,34 @@ can be used:
$ croptool -f 'convert -crop %wx%h+%l+%t "%f" "$(basename "%f" .jpg).png"' *.jpg | sh
.Ed
.Pp
+With
+.Xr convert 1
+or
+.Xr mogrify 1 ,
+it is also possible to specify the compression level for JPEG images:
+.Bd -literal
+$ croptool -f 'mogrify -quality 90 -crop %wx%h+%l+%t "%f"' *.jpg | sh
+.Ed
+.Pp
+Using
+.Xr convert 1
+or
+.Xr mogrify 1
+has the additional benefit of preserving the EXIF tags embedded in an image,
+which is a feature
+.Xr croptool_crop 1
+currently lacks.
+.Pp
+For ImageMagick 7,
+.Sq convert
+and
+.Sq mogrify
+need to be replaced with
+.Sq magick convert
+and
+.Sq magick mogrify ,
+respectively.
+.Pp
Note that no great care has been taken to deal with filenames containing
single or double quotes.
That is left as an exercise to the reader (hint: just don't have
@@ -171,6 +216,7 @@ filenames containing quotes).
.Sh SEE ALSO
.Xr convert 1 ,
.Xr croptool_crop 1 ,
+.Xr ImageMagick 1 ,
.Xr mogrify 1 ,
.Xr selectool 1
.Sh AUTHORS
@@ -181,8 +227,3 @@ quotes may cause issues depending on the output format.
.Pp
Transparent portions of images should probably be shown differently,
but I'm too lazy to fix that and don't really care at the moment.
-.Pp
-Since the coordinates of the cropping rectangle are stored as integers,
-they will become skewed while resizing.
-If this becomes a real problem, though, you're probably doing something
-wrong anyways.
diff --git a/croptool.c b/croptool.c
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2021-2024 lumidify <nobody@lumidify.org>
+ * Copyright (c) 2021-2026 lumidify <nobody@lumidify.org>
*
* Permission to use, copy, modify, and/or distribute this software for any
* purpose with or without fee is hereby granted, provided that the above
@@ -73,10 +73,23 @@ struct Point {
int y;
};
+/* Note that the ordering of the points in the rectangles is
+ used to store which side is being dragged when resizing
+ a selection. This means that the points need to be sorted
+ using sort_coordinates if they need to be in a canonical
+ form such that x0 <= x1 and y0 <= y1. */
struct Selection {
- struct Rect rect;
+ /* scaled_rect can always be calculated from real_rect,
+ but it's convenient to have it stored separately for
+ functions like set_cursor_for_selection that use it a lot */
+ struct Rect real_rect;
+ struct Rect scaled_rect;
ImageSize sz;
- char valid;
+ char rect_valid;
+ /* sz_valid should be implied for the current selection
+ by state.ctx.cur_image being valid, but this makes it
+ a bit more explicit */
+ char sz_valid;
};
static struct {
@@ -92,6 +105,11 @@ static struct {
XColor col1;
XColor col2;
int cur_col;
+ struct {
+ struct Rect rect;
+ char valid;
+ char scaled;
+ } copied_selection;
char moving;
char resizing;
char lock_x;
@@ -123,14 +141,18 @@ static int collide_point(int x, int y, int x_point, int y_point);
static int collide_line(int x, int y, int x0, int y0, int x1, int y1);
static int collide_rect(int x, int y, struct Rect rect);
static void switch_color(void);
+static void scale_selection_rect(double scale, struct Rect rect, struct Rect *rect_ret);
+static void set_scaled_selection_rect(struct Selection *sel, struct Rect rect);
+static void set_real_selection_rect(struct Selection *sel, struct Rect rect);
+static void copy_selection(int scaled);
+static void paste_selection(void);
+static void select_all(void);
static void clear_selection(void);
-static void set_selection(
- struct Selection *sel, int rect_x0, int rect_y0, int rect_x1,
- int rect_y1, int orig_w, int orig_h, int scaled_w, int scaled_h
-);
static void queue_update(int x, int y, int w, int h);
static void queue_rectangle_redraw(int x0, int y0, int x1, int y1);
static void set_cursor(struct Rect rect);
+static void reset_cursor(void);
+static void set_cursor_for_selection(struct Selection *sel);
static void drag_motion(XEvent event);
static void resize_window(int w, int h);
static void button_release(void);
@@ -204,7 +226,7 @@ main(int argc, char *argv[]) {
if (state.print_on_exit) {
for (int i = 0; i < argc; i++) {
- if (state.selections[i].valid) {
+ if (state.selections[i].rect_valid) {
print_selection(&state.selections[i], state.filenames[i]);
}
}
@@ -280,9 +302,11 @@ setup(int argc, char *argv[]) {
state.cursor_y = 0;
state.cur_col = 1;
state.print_on_exit = 0;
+ state.copied_selection.valid = 0;
for (int i = 0; i < argc; i++) {
- state.selections[i].valid = 0;
+ state.selections[i].rect_valid = 0;
+ state.selections[i].sz_valid = 0;
}
setup_x(&state.ctx, 500, 500, LINE_WIDTH, CACHE_SIZE);
@@ -408,13 +432,15 @@ redraw(void) {
/* draw the parts of the image that need to be redrawn */
struct Selection *sel = &state.selections[state.cur_selection];
- draw_image_updates(&state.ctx, &sel->sz);
-
- wipe_around_image(&state.ctx, &sel->sz);
+ /* Should never be invalid at this point, but just check anyways */
+ if (sel->sz_valid) {
+ draw_image_updates(&state.ctx, &sel->sz);
+ wipe_around_image(&state.ctx, &sel->sz);
+ }
/* draw the rectangle */
- struct Rect rect = sel->rect;
- if (rect.x0 != -200) {
+ if (sel->rect_valid) {
+ struct Rect rect = sel->scaled_rect;
XColor col = state.cur_col == 1 ? state.col1 : state.col2;
XSetForeground(state.ctx.dpy, state.ctx.gc, col.pixel);
sort_coordinates(&rect.x0, &rect.y0, &rect.x1, &rect.y1);
@@ -445,17 +471,13 @@ sort_coordinates(int *x0, int *y0, int *x1, int *y1) {
static void
print_selection(struct Selection *sel, const char *filename) {
- /* The box was never actually used */
- if (sel->rect.x0 == -200)
+ if (!sel->rect_valid)
return;
- double scale = (double)sel->sz.orig_w / sel->sz.scaled_w;
- int x0 = sel->rect.x0, y0 = sel->rect.y0;
- int x1 = sel->rect.x1, y1 = sel->rect.y1;
+ int x0 = sel->real_rect.x0;
+ int y0 = sel->real_rect.y0;
+ int x1 = sel->real_rect.x1;
+ int y1 = sel->real_rect.y1;
sort_coordinates(&x0, &y0, &x1, &y1);
- x0 = round(x0 * scale);
- y0 = round(y0 * scale);
- x1 = round(x1 * scale);
- y1 = round(y1 * scale);
/* The box is completely outside of the picture. */
if (x0 >= sel->sz.orig_w || y0 >= sel->sz.orig_h)
return;
@@ -496,20 +518,24 @@ collide_rect(int x, int y, struct Rect rect) {
static void
button_press(XEvent event) {
- if (state.cur_selection < 0 || !state.selections[state.cur_selection].valid)
+ if (state.cur_selection < 0 || !state.selections[state.cur_selection].sz_valid)
return;
- struct Rect *rect = &state.selections[state.cur_selection].rect;
+ struct Selection *sel = &state.selections[state.cur_selection];
+ struct Rect *rect = &sel->scaled_rect;
int x = event.xbutton.x;
int y = event.xbutton.y;
int x0 = rect->x0, x1 = rect->x1;
int y0 = rect->y0, y1 = rect->y1;
- /* erase old rectangle */
- queue_rectangle_redraw(x0, y0, x1, y1);
+ /* if old rect was invalid, just set it to something
+ that will definitely not collide with the mouse position */
+ if (!sel->rect_valid) {
+ x0 = y0 = x1 = y1 = -200;
+ } else {
+ /* erase old rectangle */
+ queue_rectangle_redraw(x0, y0, x1, y1);
+ }
if (collide_point(x, y, x0, y0)) {
- rect->x0 = x1;
- rect->y0 = y1;
- rect->x1 = x;
- rect->y1 = y;
+ *rect = (struct Rect){x1, y1, x, y};
} else if (collide_point(x, y, x1, y1)) {
rect->x1 = x;
rect->y1 = y;
@@ -537,22 +563,20 @@ button_press(XEvent event) {
} else if (collide_line(x, y, x1, y1, x1, y0)) {
state.lock_x = 1;
rect->x1 = x;
- } else if (collide_rect(x, y, *rect)) {
+ } else if (collide_rect(x, y, (struct Rect){x0, y0, x1, y1})) {
state.moving = 1;
state.move_handle.x = x;
state.move_handle.y = y;
} else {
- rect->x0 = x;
- rect->y0 = y;
- rect->x1 = x;
- rect->y1 = y;
+ *rect = (struct Rect){x, y, x, y};
}
state.resizing = 1;
+ set_scaled_selection_rect(sel, *rect);
}
static void
queue_update(int x, int y, int w, int h) {
- if (state.cur_selection < 0 || !state.selections[state.cur_selection].valid)
+ if (state.cur_selection < 0 || !state.selections[state.cur_selection].sz_valid)
return;
struct Selection *sel = &state.selections[state.cur_selection];
queue_area_update(&state.ctx, &sel->sz, x, y, w, h);
@@ -577,25 +601,17 @@ resize_window(int w, int h) {
state.ctx.window_w = w;
state.ctx.window_h = h;
- if (state.cur_selection < 0 || !state.selections[state.cur_selection].valid)
+ if (state.cur_selection < 0 || !state.selections[state.cur_selection].sz_valid)
return;
sel = &state.selections[state.cur_selection];
get_scaled_size(&state.ctx, sel->sz.orig_w, sel->sz.orig_h, &actual_w, &actual_h);
- if (actual_w != sel->sz.scaled_w) {
- if (sel->rect.x0 != -200) {
- /* If there is a selection, we need to convert it to
- * the new scale. This only takes width into account
- * because the aspect ratio should have been preserved
- * anyways */
- double scale = (double)actual_w / sel->sz.scaled_w;
- sel->rect.x0 = round(sel->rect.x0 * scale);
- sel->rect.y0 = round(sel->rect.y0 * scale);
- sel->rect.x1 = round(sel->rect.x1 * scale);
- sel->rect.y1 = round(sel->rect.y1 * scale);
- }
+ if (actual_w != sel->sz.scaled_w || actual_h != sel->sz.scaled_h) {
sel->sz.scaled_w = actual_w;
sel->sz.scaled_h = actual_h;
+ if (sel->rect_valid)
+ set_real_selection_rect(sel, sel->real_rect);
queue_update(0, 0, sel->sz.scaled_w, sel->sz.scaled_h);
+
}
}
@@ -622,6 +638,13 @@ queue_rectangle_redraw(int x0, int y0, int x1, int y1) {
x1 - LINE_WIDTH > 0 ? x1 - LINE_WIDTH : 0,
y0 - LINE_WIDTH > 0 ? y0 - LINE_WIDTH : 0,
LINE_WIDTH * 2, y1 - y0 + LINE_WIDTH * 2);
+ /* this has to be set manually here in case the entire
+ rectangle is outside the image area, as that will
+ cause queue_area_update (called by queue_update) to
+ return immediately without setting the flag, but
+ redraw() still needs to know that the area outside
+ the image needs to be redrawn */
+ state.ctx.dirty = 1;
}
/* set the appropriate cursor based on the
@@ -669,11 +692,24 @@ set_cursor(struct Rect rect) {
}
static void
+reset_cursor(void) {
+ XDefineCursor(state.ctx.dpy, state.ctx.win, None);
+}
+
+static void
+set_cursor_for_selection(struct Selection *sel) {
+ if (sel->rect_valid)
+ set_cursor(sel->scaled_rect);
+ else
+ reset_cursor();
+}
+
+static void
drag_motion(XEvent event) {
- if (state.cur_selection < 0 || !state.selections[state.cur_selection].valid)
+ if (state.cur_selection < 0 || !state.selections[state.cur_selection].rect_valid)
return;
struct Selection *sel = &state.selections[state.cur_selection];
- struct Rect *rect = &sel->rect;
+ struct Rect *rect = &sel->scaled_rect;
/* don't allow coordinates to go below 0 */
if (event.xbutton.x >= 0)
@@ -709,10 +745,12 @@ drag_motion(XEvent event) {
if (!state.lock_x)
rect->y1 = state.cursor_y;
} else {
- set_cursor(*rect);
+ set_cursor_for_selection(sel);
+ /* return directly to skip the redraw */
return;
}
- set_cursor(*rect);
+ set_scaled_selection_rect(sel, *rect);
+ set_cursor_for_selection(sel);
/* redraw the new rectangle */
if (SELECTION_REDRAW)
@@ -720,18 +758,79 @@ drag_motion(XEvent event) {
}
static void
-set_selection(
- struct Selection *sel, int rect_x0, int rect_y0, int rect_x1,
- int rect_y1, int orig_w, int orig_h, int scaled_w, int scaled_h) {
+scale_selection_rect(double scale, struct Rect rect, struct Rect *rect_ret) {
+ sort_coordinates(&rect.x0, &rect.y0, &rect.x1, &rect.y1);
+ rect.x0 = round(rect.x0 * scale);
+ rect.y0 = round(rect.y0 * scale);
+ rect.x1 = round(rect.x1 * scale);
+ rect.y1 = round(rect.y1 * scale);
+ *rect_ret = rect;
+}
+
+/* Only the width is taken into account for the scaling functions since the
+ aspect ratio should have been preserved anyways. Of course, this could
+ lead to tiny inaccuracies since the scaled width and height are truncated
+ to integers. */
+static void
+set_scaled_selection_rect(struct Selection *sel, struct Rect rect) {
+ sel->scaled_rect = rect;
+ scale_selection_rect(
+ (double)sel->sz.orig_w / sel->sz.scaled_w, rect, &sel->real_rect
+ );
+ sel->rect_valid = 1;
+}
+
+static void
+set_real_selection_rect(struct Selection *sel, struct Rect rect) {
+ sel->real_rect = rect;
+ scale_selection_rect(
+ (double)sel->sz.scaled_w / sel->sz.orig_w, rect, &sel->scaled_rect
+ );
+ sel->rect_valid = 1;
+}
+
+static void
+copy_selection(int scaled) {
+ if (state.cur_selection < 0 || !state.selections[state.cur_selection].rect_valid)
+ return;
+ if (scaled) {
+ state.copied_selection.rect = state.selections[state.cur_selection].scaled_rect;
+ state.copied_selection.scaled = 1;
+ } else {
+ state.copied_selection.rect = state.selections[state.cur_selection].real_rect;
+ state.copied_selection.scaled = 0;
+ }
+ state.copied_selection.valid = 1;
+}
- sel->rect.x0 = rect_x0;
- sel->rect.y0 = rect_y0;
- sel->rect.x1 = rect_x1;
- sel->rect.y1 = rect_y1;
- sel->sz.orig_w = orig_w;
- sel->sz.orig_h = orig_h;
- sel->sz.scaled_w = scaled_w;
- sel->sz.scaled_h = scaled_h;
+static void
+paste_selection(void) {
+ if (!state.copied_selection.valid || state.cur_selection < 0 ||
+ !state.selections[state.cur_selection].sz_valid) {
+ return;
+ }
+ struct Selection *sel = &state.selections[state.cur_selection];
+ /* clear the old rectangle */
+ struct Rect *rect = &sel->scaled_rect;
+ if (sel->rect_valid)
+ queue_rectangle_redraw(rect->x0, rect->y0, rect->x1, rect->y1);
+ if (state.copied_selection.scaled)
+ set_scaled_selection_rect(sel, state.copied_selection.rect);
+ else
+ set_real_selection_rect(sel, state.copied_selection.rect);
+ queue_rectangle_redraw(rect->x0, rect->y0, rect->x1, rect->y1);
+}
+
+static void
+select_all(void) {
+ if (state.cur_selection < 0 || !state.selections[state.cur_selection].sz_valid)
+ return;
+ struct Selection *sel = &state.selections[state.cur_selection];
+ struct Rect *rect = &sel->scaled_rect;
+ if (sel->rect_valid)
+ queue_rectangle_redraw(rect->x0, rect->y0, rect->x1, rect->y1);
+ set_real_selection_rect(sel, (struct Rect){0, 0, sel->sz.orig_w, sel->sz.orig_h});
+ queue_rectangle_redraw(rect->x0, rect->y0, rect->x1, rect->y1);
}
/* change the shown image
@@ -762,55 +861,58 @@ change_picture(Imlib_Image new_image, int new_selection, int copy_box) {
get_scaled_size(&state.ctx, orig_w, orig_h, &actual_w, &actual_h);
struct Selection *sel = &state.selections[state.cur_selection];
+ if (sel->sz_valid && (sel->sz.orig_w != orig_w || sel->sz.orig_h != orig_h)) {
+ fprintf(
+ stderr,
+ "WARNING: Image '%s' changed its size since it was previously opened.\n",
+ state.filenames[state.cur_selection]
+ );
+ }
+ sel->sz = (ImageSize){orig_w, orig_h, actual_w, actual_h};
+ sel->sz_valid = 1;
if (copy_box && old_selection >= 0 && old_selection < state.num_files) {
struct Selection *old = &state.selections[old_selection];
- set_selection(
- sel,
- old->rect.x0, old->rect.y0, old->rect.x1, old->rect.y1,
- orig_w, orig_h, actual_w, actual_h
- );
- } else if (!sel->valid) {
- /* Just fill it with -200 so we can check
- * later if it has been used yet */
- set_selection(
- sel,
- -200, -200, -200, -200,
- orig_w, orig_h, actual_w, actual_h
- );
- } else if (sel->rect.x0 != -200 && actual_w != sel->sz.scaled_w) {
- /* If there is a selection, we need to convert it to the
- * new scale. This only takes width into account because
- * the aspect ratio should have been preserved anyways */
- double scale = (double)actual_w / sel->sz.scaled_w;
- sel->rect.x0 = round(sel->rect.x0 * scale);
- sel->rect.y0 = round(sel->rect.y0 * scale);
- sel->rect.x1 = round(sel->rect.x1 * scale);
- sel->rect.y1 = round(sel->rect.y1 * scale);
+ if (old->rect_valid) {
+ set_scaled_selection_rect(sel, old->scaled_rect);
+ sel->rect_valid = 1;
+ } else {
+ /* FIXME: an alternative would be to just leave the
+ selection alone instead of clearing it if copy_box
+ is true and the previous selection was not valid */
+ sel->rect_valid = 0;
+ }
+ } else if (sel->rect_valid) {
+ /* Always recalculate scaled selection in case the image scale
+ changed (i.e. the window was resized) */
+ set_real_selection_rect(sel, sel->real_rect);
}
- sel->sz.scaled_w = actual_w;
- sel->sz.scaled_h = actual_h;
- sel->valid = 1;
queue_update(0, 0, sel->sz.scaled_w, sel->sz.scaled_h);
/* set the cursor since the cropping rectangle may have changed */
- set_cursor(sel->rect);
+ set_cursor_for_selection(sel);
}
static void
clear_selection(void) {
- if (state.cur_selection < 0 || !state.selections[state.cur_selection].valid)
+ if (state.cur_selection < 0 || !state.selections[state.cur_selection].rect_valid)
return;
struct Selection *sel = &state.selections[state.cur_selection];
- sel->rect.x0 = sel->rect.x1 = sel->rect.y0 = sel->rect.y1 = -200;
- queue_update(0, 0, sel->sz.scaled_w, sel->sz.scaled_h);
+ struct Rect *rect = &sel->scaled_rect;
+ if (sel->rect_valid)
+ queue_rectangle_redraw(rect->x0, rect->y0, rect->x1, rect->y1);
+ sel->rect_valid = 0;
+ set_cursor_for_selection(sel);
}
static void
switch_color(void) {
- if (state.cur_selection < 0 || !state.selections[state.cur_selection].valid)
+ if (state.cur_selection < 0 || !state.selections[state.cur_selection].rect_valid)
return;
state.cur_col = state.cur_col == 1 ? 2 : 1;
- queue_update(0, 0, state.ctx.window_w, state.ctx.window_h);
+ struct Selection *sel = &state.selections[state.cur_selection];
+ struct Rect *rect = &sel->scaled_rect;
+ if (sel->rect_valid)
+ queue_rectangle_redraw(rect->x0, rect->y0, rect->x1, rect->y1);
}
static int
@@ -848,6 +950,19 @@ key_press(XEvent event) {
case XK_q:
state.print_on_exit = 1;
return 0;
+ case XK_y:
+ copy_selection(1);
+ break;
+ case XK_p:
+ paste_selection();
+ break;
+ case XK_Y:
+ copy_selection(0);
+ break;
+ case XK_a:
+ if (event.xkey.state & ControlMask)
+ select_all();
+ break;
default:
break;
}
diff --git a/croptool_crop.1 b/croptool_crop.1
@@ -1,4 +1,4 @@
-.Dd September 21, 2021
+.Dd July 12, 2026
.Dt CROPTOOL_CROP 1
.Os
.Sh NAME
@@ -28,11 +28,22 @@ is given, the cropped image is written to this file instead
of modifying the source image in-place.
This program is meant primarily as a companion to
.Xr croptool 1 .
+.Pp
+Note that metadata such as EXIF data is currently not preserved.
+If preserving such data is necessary, consider using the commands
+provided by
+.Xr ImageMagick 1
+instead.
+The same goes for cases where it is necessary to specify options
+such as the JPEG compression level that
+.Nm
+does not support.
.Sh EXIT STATUS
.Ex -std
.Sh SEE ALSO
.Xr convert 1 ,
.Xr croptool 1 ,
+.Xr ImageMagick 1 ,
.Xr mogrify 1
.Sh AUTHORS
.An lumidify Aq Mt nobody@lumidify.org
diff --git a/selectool.1 b/selectool.1
@@ -1,4 +1,4 @@
-.Dd May 14, 2024
+.Dd July 12, 2026
.Dt SELECTOOL 1
.Os
.Sh NAME
@@ -15,7 +15,9 @@
.Sh DESCRIPTION
.Nm
shows each of the given images and allows them to be selected or deselected.
-On exit, the given command is printed for each of the files.
+When the keyboard command
+.Sq q
+is used to exit, the given command is printed for each of the selected images.
.Sh OPTIONS
.Bl -tag -width Ds
.It Fl m
@@ -64,7 +66,7 @@ Warning: This is printed as is, without any escaping.
.Pp
If an unknown substitution is encountered, a warning is printed to
standard error and the characters are printed verbatim.
-.Sh KEYBINDS
+.Sh KEYBOARD COMMANDS
.Bl -tag -width Ds
.It ARROW LEFT
Go to the previous image.