croptool

Image cropping tool
git clone git://lumidify.org/croptool.git (fast, but not encrypted)
git clone https://lumidify.org/git/croptool.git (encrypted, but very slow)
git clone git://4kcetb7mo7hj6grozzybxtotsub5bempzo4lirzc3437amof2c2impyd.onion/croptool.git (over tor)
Log | Files | Refs | README | LICENSE

croptool.c (27730B)


      1 /*
      2  * Copyright (c) 2021-2026 lumidify <nobody@lumidify.org>
      3  *
      4  * Permission to use, copy, modify, and/or distribute this software for any
      5  * purpose with or without fee is hereby granted, provided that the above
      6  * copyright notice and this permission notice appear in all copies.
      7  *
      8  * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
      9  * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
     10  * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
     11  * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
     12  * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
     13  * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
     14  * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
     15  */
     16 
     17 #include <math.h>
     18 #include <stdio.h>
     19 #include <stdlib.h>
     20 #include <unistd.h>
     21 
     22 #include <X11/X.h>
     23 #include <X11/Xlib.h>
     24 #include <X11/Xutil.h>
     25 #include <X11/keysym.h>
     26 #include <X11/cursorfont.h>
     27 
     28 #include <Imlib2.h>
     29 
     30 #include "common.h"
     31 
     32 /* The number of pixels to check on each side when checking
     33  * if a corner or edge of the selection box was clicked 
     34  * (in order to change the size of the box) */
     35 static int COLLISION_PADDING = 10;
     36 /* The color of the selection box */
     37 static const char *SELECTION_COLOR1 = "#000000";
     38 /* The second selection color - when tab is pressed */
     39 static const char *SELECTION_COLOR2 = "#FFFFFF";
     40 /* The width of the selection line */
     41 static int LINE_WIDTH = 2;
     42 /* When set to 1, the display is redrawn on window resize */
     43 static short RESIZE_REDRAW = 1;
     44 /* When set to 1, the selection is redrawn continually,
     45    not just when the mouse button is released */
     46 static short SELECTION_REDRAW = 1;
     47 /*
     48   The command printed for each image.
     49   %w: Width of cropped area.
     50   %h: Height of cropped area.
     51   %l: Left side of cropped area.
     52   %r: Right side of cropped area.
     53   %t: Top side of cropped area.
     54   %b: Bottom side of cropped area.
     55   %f: Filename of image.
     56 */
     57 static const char *CMD_FORMAT = "croptool_crop %wx%h+%l+%t '%f'";
     58 /* Size of Imlib2 in-memory cache in MiB */
     59 static int CACHE_SIZE = 4;
     60 
     61 extern char *optarg;
     62 extern int optind;
     63 
     64 struct Rect {
     65 	int x0;
     66 	int y0;
     67 	int x1;
     68 	int y1;
     69 };
     70 
     71 struct Point {
     72 	int x;
     73 	int y;
     74 };
     75 
     76 /* Note that the ordering of the points in the rectangles is
     77    used to store which side is being dragged when resizing
     78    a selection. This means that the points need to be sorted
     79    using sort_coordinates if they need to be in a canonical
     80    form such that x0 <= x1 and y0 <= y1. */
     81 struct Selection {
     82 	/* scaled_rect can always be calculated from real_rect,
     83 	   but it's convenient to have it stored separately for
     84 	   functions like set_cursor_for_selection that use it a lot */
     85 	struct Rect real_rect;
     86 	struct Rect scaled_rect;
     87 	ImageSize sz;
     88 	char rect_valid;
     89 	/* sz_valid should be implied for the current selection
     90 	   by state.ctx.cur_image being valid, but this makes it
     91 	   a bit more explicit */
     92 	char sz_valid;
     93 };
     94 
     95 static struct {
     96 	GraphicsContext ctx;
     97 
     98 	struct Selection *selections;
     99 	char **filenames;
    100 	int cur_selection;
    101 	int num_files;
    102 	int cursor_x;
    103 	int cursor_y;
    104 	struct Point move_handle;
    105 	XColor col1;
    106 	XColor col2;
    107 	int cur_col;
    108 	struct {
    109 		struct Rect rect;
    110 		char valid;
    111 		char scaled;
    112 	} copied_selection;
    113 	char moving;
    114 	char resizing;
    115 	char lock_x;
    116 	char lock_y;
    117 	char print_on_exit;
    118 } state;
    119 
    120 static struct {
    121 	Cursor top;
    122 	Cursor bottom;
    123 	Cursor left;
    124 	Cursor right;
    125 	Cursor topleft;
    126 	Cursor topright;
    127 	Cursor bottomleft;
    128 	Cursor bottomright;
    129 	Cursor grab;
    130 } cursors;
    131 
    132 static void usage(void);
    133 static void mainloop(void);
    134 static void setup(int argc, char *argv[]);
    135 static void sort_coordinates(int *x0, int *y0, int *x1, int *y1);
    136 static void swap(int *a, int *b);
    137 static void redraw(void);
    138 static void print_cmd(const char *filename, int x, int y, int w, int h, int dry_run);
    139 static void print_selection(struct Selection *sel, const char *filename);
    140 static int collide_point(int x, int y, int x_point, int y_point);
    141 static int collide_line(int x, int y, int x0, int y0, int x1, int y1);
    142 static int collide_rect(int x, int y, struct Rect rect);
    143 static void switch_color(void);
    144 static void scale_selection_rect(double scale, struct Rect rect, struct Rect *rect_ret);
    145 static void set_scaled_selection_rect(struct Selection *sel, struct Rect rect);
    146 static void set_real_selection_rect(struct Selection *sel, struct Rect rect);
    147 static void copy_selection(int scaled);
    148 static void paste_selection(void);
    149 static void select_all(void);
    150 static void clear_selection(void);
    151 static void queue_update(int x, int y, int w, int h);
    152 static void queue_rectangle_redraw(int x0, int y0, int x1, int y1);
    153 static void set_cursor(struct Rect rect);
    154 static void reset_cursor(void);
    155 static void set_cursor_for_selection(struct Selection *sel);
    156 static void drag_motion(XEvent event);
    157 static void resize_window(int w, int h);
    158 static void button_release(void);
    159 static void button_press(XEvent event);
    160 static int key_press(XEvent event);
    161 
    162 static void
    163 usage(void) {
    164 	fprintf(stderr, "USAGE: croptool [-mr] [-f format] "
    165 	    "[-w width] [-c padding] [-p color] [-s color] "
    166 	    "[-z size] file ...\n");
    167 }
    168 
    169 int 
    170 main(int argc, char *argv[]) {
    171 	char c;
    172 
    173 	while ((c = getopt(argc, argv, "f:w:c:mrp:s:z:")) != -1) {
    174 		switch (c) {
    175 		case 'f':
    176 			CMD_FORMAT = optarg;
    177 			break;
    178 		case 'm':
    179 			RESIZE_REDRAW = 0;
    180 			break;
    181 		case 'r':
    182 			SELECTION_REDRAW = 0;
    183 			break;
    184 		case 'p':
    185 			SELECTION_COLOR1 = optarg;
    186 			break;
    187 		case 's':
    188 			SELECTION_COLOR2 = optarg;
    189 			break;
    190 		case 'c':
    191 			if (parse_int(optarg, 1, 99, &COLLISION_PADDING)) {
    192 				fprintf(stderr, "Invalid collision padding.\n");
    193 				exit(1);
    194 			}
    195 			break;
    196 		case 'w':
    197 			if (parse_int(optarg, 1, 99, &LINE_WIDTH)) {
    198 				fprintf(stderr, "Invalid line width.\n");
    199 				exit(1);
    200 			}
    201 			break;
    202 		case 'z':
    203 			if (parse_int(optarg, 0, 1024, &CACHE_SIZE)) {
    204 				fprintf(stderr, "Invalid cache size.\n");
    205 				exit(1);
    206 			}
    207 			break;
    208 		default:
    209 			usage();
    210 			exit(1);
    211 			break;
    212 		}
    213 	}
    214 	/* print warning if command format is invalid */
    215 	print_cmd("", 0, 0, 0, 0, 1);
    216 
    217 	argc -= optind;
    218 	argv += optind;
    219 	if (argc < 1) {
    220 		usage();
    221 		exit(1);
    222 	}
    223 	setup(argc, argv);
    224 
    225 	mainloop();
    226 
    227 	if (state.print_on_exit) {
    228 		for (int i = 0; i < argc; i++) {
    229 			if (state.selections[i].rect_valid) {
    230 				print_selection(&state.selections[i], state.filenames[i]);
    231 			}
    232 		}
    233 	}
    234 
    235 	cleanup();
    236 
    237 	return 0;
    238 }
    239 
    240 static void
    241 mainloop(void) {
    242 	XEvent event;
    243 	int running = 1;
    244 
    245 	while (running) {
    246 		do {
    247 			XNextEvent(state.ctx.dpy, &event);
    248 			switch (event.type) {
    249 			case Expose:
    250 				if (RESIZE_REDRAW)
    251 					queue_update(event.xexpose.x, event.xexpose.y,
    252 					    event.xexpose.width, event.xexpose.height);
    253 				break;
    254 			case ConfigureNotify:
    255 				if (RESIZE_REDRAW)
    256 					resize_window(
    257 					    event.xconfigure.width,
    258 					    event.xconfigure.height
    259 					);
    260 				break;
    261 			case ButtonPress:
    262 				if (event.xbutton.button == Button1)
    263 					button_press(event);
    264 				break;
    265 			case ButtonRelease:
    266 				if (event.xbutton.button == Button1)
    267 					button_release();
    268 				break;
    269 			case MotionNotify:
    270 				drag_motion(event);
    271 				break;
    272 			case KeyPress:
    273 				running = key_press(event);
    274 				break;
    275 			case ClientMessage:
    276 				if ((Atom)event.xclient.data.l[0] == state.ctx.wm_delete_msg)
    277 					running = 0;
    278 			default:
    279 				break;
    280 			}
    281 		} while (XPending(state.ctx.dpy));
    282 
    283 		redraw();
    284 	}
    285 }
    286 
    287 static void
    288 setup(int argc, char *argv[]) {
    289 	state.selections = malloc(argc * sizeof(struct Selection));
    290 	if (!state.selections) {
    291 		fprintf(stderr, "Unable to allocate memory.\n");
    292 		exit(1);
    293 	}
    294 	state.num_files = argc;
    295 	state.filenames = argv;
    296 	state.cur_selection = -1;
    297 	state.moving = 0;
    298 	state.resizing = 0;
    299 	state.lock_x = 0;
    300 	state.lock_y = 0;
    301 	state.cursor_x = 0;
    302 	state.cursor_y = 0;
    303 	state.cur_col = 1;
    304 	state.print_on_exit = 0;
    305 	state.copied_selection.valid = 0;
    306 
    307 	for (int i = 0; i < argc; i++) {
    308 		state.selections[i].rect_valid = 0;
    309 		state.selections[i].sz_valid = 0;
    310 	}
    311 
    312 	setup_x(&state.ctx, 500, 500, LINE_WIDTH, CACHE_SIZE);
    313 
    314         if (!XParseColor(state.ctx.dpy, state.ctx.cm, SELECTION_COLOR1, &state.col1)) {
    315 		fprintf(stderr, "Primary color invalid.\n");
    316 		exit(1);
    317 	}
    318         XAllocColor(state.ctx.dpy, state.ctx.cm, &state.col1);
    319         if (!XParseColor(state.ctx.dpy, state.ctx.cm, SELECTION_COLOR2, &state.col2)) {
    320 		fprintf(stderr, "Secondary color invalid.\n");
    321 		exit(1);
    322 	}
    323         XAllocColor(state.ctx.dpy, state.ctx.cm, &state.col2);
    324 
    325 	cursors.top = XCreateFontCursor(state.ctx.dpy, XC_top_side);
    326 	cursors.bottom = XCreateFontCursor(state.ctx.dpy, XC_bottom_side);
    327 	cursors.left = XCreateFontCursor(state.ctx.dpy, XC_left_side);
    328 	cursors.right = XCreateFontCursor(state.ctx.dpy, XC_right_side);
    329 	cursors.topleft = XCreateFontCursor(state.ctx.dpy, XC_top_left_corner);
    330 	cursors.topright = XCreateFontCursor(state.ctx.dpy, XC_top_right_corner);
    331 	cursors.bottomleft = XCreateFontCursor(state.ctx.dpy, XC_bottom_left_corner);
    332 	cursors.bottomright = XCreateFontCursor(state.ctx.dpy, XC_bottom_right_corner);
    333 	cursors.grab = XCreateFontCursor(state.ctx.dpy, XC_fleur);
    334 
    335 	next_picture(state.cur_selection, state.filenames, state.num_files, 0);
    336 	/* Only map window here so the program exits immediately if
    337 	   there are no loadable images, without first opening the
    338 	   window and closing it again immediately */
    339 	XMapWindow(state.ctx.dpy, state.ctx.win);
    340 	redraw();
    341 }
    342 
    343 void
    344 cleanup(void) {
    345 	free(state.selections);
    346 	cleanup_x(&state.ctx);
    347 }
    348 
    349 /* TODO: Escape filename properly
    350  * -> But how? Since the format can be set by the user,
    351  * it isn't really clear *what* needs to be escaped. */
    352 static void
    353 print_cmd(const char *filename, int x, int y, int w, int h, int dry_run) {
    354 	short percent = 0;
    355 	const char *c;
    356 	int length = 0;
    357 	int start_index = 0;
    358 	/* FIXME: just use putc instead of this complex printf dance */
    359 	for (c = CMD_FORMAT; *c != '\0'; c++) {
    360 		if (percent)
    361 			start_index++;
    362 		if (*c == '%') {
    363 			if (length) {
    364 				if (!dry_run)
    365 					printf("%.*s", length, CMD_FORMAT + start_index);
    366 				start_index += length;
    367 				length = 0;
    368 			}
    369 			if (percent && !dry_run)
    370 				printf("%%");
    371 			percent++;
    372 			percent %= 2;
    373 			start_index++;
    374 		} else if (percent && *c == 'w') {
    375 			if (!dry_run)
    376 				printf("%d", w);
    377 			percent = 0;
    378 		} else if (percent && *c == 'h') {
    379 			if (!dry_run)
    380 				printf("%d", h);
    381 			percent = 0;
    382 		} else if (percent && *c == 'l') {
    383 			if (!dry_run)
    384 				printf("%d", x);
    385 			percent = 0;
    386 		} else if (percent && *c == 't') {
    387 			if (!dry_run)
    388 				printf("%d", y);
    389 			percent = 0;
    390 		} else if (percent && *c == 'r') {
    391 			if (!dry_run)
    392 				printf("%d", x + w);
    393 			percent = 0;
    394 		} else if (percent && *c == 'b') {
    395 			if (!dry_run)
    396 				printf("%d", y + h);
    397 			percent = 0;
    398 		} else if (percent && *c == 'f') {
    399 			if (!dry_run)
    400 				printf("%s", filename);
    401 			percent = 0;
    402 		} else if (percent) {
    403 			if (dry_run) {
    404 				fprintf(stderr,
    405 				    "Warning: Unknown substitution '%c' "
    406 				    "in format string.\n", *c
    407 				);
    408 			} else {
    409 				printf("%%%c", *c);
    410 			}
    411 			percent = 0;
    412 		} else {
    413 			length++;
    414 		}
    415 	}
    416 	if (!dry_run) {
    417 		if (length)
    418 			printf("%.*s", length, CMD_FORMAT + start_index);
    419 		printf("\n");
    420 	}
    421 }
    422 
    423 static void
    424 redraw(void) {
    425 	if (!state.ctx.dirty)
    426 		return;
    427 	if (!state.ctx.cur_image || state.cur_selection < 0) {
    428 		clear_screen(&state.ctx);
    429 		swap_buffers(&state.ctx);
    430 		return;
    431 	}
    432 
    433 	/* draw the parts of the image that need to be redrawn */
    434 	struct Selection *sel = &state.selections[state.cur_selection];
    435 	/* Should never be invalid at this point, but just check anyways */
    436 	if (sel->sz_valid) {
    437 		draw_image_updates(&state.ctx, &sel->sz);
    438 		wipe_around_image(&state.ctx, &sel->sz);
    439 	}
    440 
    441 	/* draw the rectangle */
    442 	if (sel->rect_valid) {
    443 		struct Rect rect = sel->scaled_rect;
    444 		XColor col = state.cur_col == 1 ? state.col1 : state.col2;
    445 		XSetForeground(state.ctx.dpy, state.ctx.gc, col.pixel);
    446 		sort_coordinates(&rect.x0, &rect.y0, &rect.x1, &rect.y1);
    447 		XDrawRectangle(
    448 		    state.ctx.dpy, state.ctx.drawable, state.ctx.gc,
    449 		    rect.x0, rect.y0, rect.x1 - rect.x0, rect.y1 - rect.y0
    450 		);
    451 	}
    452 	swap_buffers(&state.ctx);
    453 }
    454 
    455 static void
    456 swap(int *a, int *b) {
    457         int tmp = *a;
    458         *a = *b;
    459         *b = tmp;
    460 }
    461 
    462 /* sort rectangle coordinates into their canonical
    463  * form so *x1 - *x0 >= 0 and *y1 - *y0 >= 0 */
    464 static void
    465 sort_coordinates(int *x0, int *y0, int *x1, int *y1) {
    466         if (*x0 > *x1)
    467                 swap(x0, x1);
    468         if(*y0 > *y1)
    469                 swap(y0, y1);
    470 }
    471 
    472 static void
    473 print_selection(struct Selection *sel, const char *filename) {
    474 	if (!sel->rect_valid)
    475 		return;
    476 	int x0 = sel->real_rect.x0;
    477 	int y0 = sel->real_rect.y0;
    478 	int x1 = sel->real_rect.x1;
    479 	int y1 = sel->real_rect.y1;
    480 	sort_coordinates(&x0, &y0, &x1, &y1);
    481 	/* The box is completely outside of the picture. */
    482 	if (x0 >= sel->sz.orig_w || y0 >= sel->sz.orig_h)
    483 		return;
    484 	/* Cut the bounding box if it goes past the end of the picture. */
    485 	x0 = x0 < 0 ? 0 : x0;
    486 	y0 = y0 < 0 ? 0 : y0;
    487 	x1 = x1 > sel->sz.orig_w ? sel->sz.orig_w : x1;
    488 	y1 = y1 > sel->sz.orig_h ? sel->sz.orig_h : y1;
    489 	print_cmd(filename, x0, y0, x1 - x0, y1 - y0, 0);
    490 }
    491 
    492 static int
    493 collide_point(int x, int y, int x_point, int y_point) {
    494 	return (abs(x - x_point) <= COLLISION_PADDING) &&
    495 		(abs(y - y_point) <= COLLISION_PADDING);
    496 }
    497 
    498 static int
    499 collide_line(int x, int y, int x0, int y0, int x1, int y1) {
    500 	sort_coordinates(&x0, &y0, &x1, &y1);
    501 	/* this expects a valid line */
    502 	if (x0 == x1) {
    503 		return (abs(x - x0) <= COLLISION_PADDING) &&
    504 			(y0 <= y) && (y <= y1);
    505 	} else {
    506 		return (abs(y - y0) <= COLLISION_PADDING) &&
    507 			(x0 <= x) && (x <= x1);
    508 	}
    509 }
    510 
    511 static int
    512 collide_rect(int x, int y, struct Rect rect) {
    513 	int x0 = rect.x0, x1 = rect.x1;
    514 	int y0 = rect.y0, y1 = rect.y1;
    515 	sort_coordinates(&x0, &y0, &x1, &y1);
    516 	return (x0 <= x) && (x <= x1) && (y0 <= y) && (y <= y1);
    517 }
    518 
    519 static void
    520 button_press(XEvent event) {
    521 	if (state.cur_selection < 0 || !state.selections[state.cur_selection].sz_valid)
    522 		return;
    523 	struct Selection *sel = &state.selections[state.cur_selection];
    524 	struct Rect *rect = &sel->scaled_rect;
    525 	int x = event.xbutton.x;
    526 	int y = event.xbutton.y;
    527 	int x0 = rect->x0, x1 = rect->x1;
    528 	int y0 = rect->y0, y1 = rect->y1;
    529 	/* if old rect was invalid, just set it to something
    530 	   that will definitely not collide with the mouse position */
    531 	if (!sel->rect_valid) {
    532 		x0 = y0 = x1 = y1 = -200;
    533 	} else {
    534 		/* erase old rectangle */
    535 		queue_rectangle_redraw(x0, y0, x1, y1);
    536 	}
    537 	if (collide_point(x, y, x0, y0)) {
    538 		*rect = (struct Rect){x1, y1, x, y};
    539 	} else if (collide_point(x, y, x1, y1)) {
    540 		rect->x1 = x;
    541 		rect->y1 = y;
    542 	} else if (collide_point(x, y, x0, y1)) {
    543 		rect->x0 = rect->x1;
    544 		rect->x1 = x;
    545 		rect->y1 = y;
    546 	} else if (collide_point(x, y, x1, y0)) {
    547 		rect->y0 = y1;
    548 		rect->x1 = x;
    549 		rect->y1 = y;
    550 	} else if (collide_line(x, y, x0, y0, x1, y0)) {
    551 		state.lock_y = 1;
    552 		swap(&rect->x0, &rect->x1);
    553 		rect->y0 = rect->y1;
    554 		rect->y1 = y;
    555 	} else if (collide_line(x, y, x0, y0, x0, y1)) {
    556 		state.lock_x = 1;
    557 		swap(&rect->y0, &rect->y1);
    558 		rect->x0 = rect->x1;
    559 		rect->x1 = x;
    560 	} else if (collide_line(x, y, x1, y1, x0, y1)) {
    561 		state.lock_y = 1;
    562 		rect->y1 = y;
    563 	} else if (collide_line(x, y, x1, y1, x1, y0)) {
    564 		state.lock_x = 1;
    565 		rect->x1 = x;
    566 	} else if (collide_rect(x, y, (struct Rect){x0, y0, x1, y1})) {
    567 		state.moving = 1;
    568 		state.move_handle.x = x;
    569 		state.move_handle.y = y;
    570 	} else {
    571 		*rect = (struct Rect){x, y, x, y};
    572 	}
    573 	state.resizing = 1;
    574 	set_scaled_selection_rect(sel, *rect);
    575 }
    576 
    577 static void
    578 queue_update(int x, int y, int w, int h) {
    579 	if (state.cur_selection < 0 || !state.selections[state.cur_selection].sz_valid)
    580 		return;
    581 	struct Selection *sel = &state.selections[state.cur_selection];
    582 	queue_area_update(&state.ctx, &sel->sz, x, y, w, h);
    583 }
    584 
    585 static void
    586 button_release(void) {
    587 	state.moving = 0;
    588 	state.resizing = 0;
    589 	state.lock_x = 0;
    590 	state.lock_y = 0;
    591 	/* redraw everything if automatic redrawing of the rectangle
    592 	   is disabled (so it's redrawn when the mouse is released) */
    593 	if (!SELECTION_REDRAW)
    594 		queue_update(0, 0, state.ctx.window_w, state.ctx.window_h);
    595 }
    596 
    597 static void
    598 resize_window(int w, int h) {
    599 	int actual_w, actual_h;
    600 	struct Selection *sel;
    601 	state.ctx.window_w = w;
    602 	state.ctx.window_h = h;
    603 
    604 	if (state.cur_selection < 0 || !state.selections[state.cur_selection].sz_valid)
    605 		return;
    606 	sel = &state.selections[state.cur_selection];
    607 	get_scaled_size(&state.ctx, sel->sz.orig_w, sel->sz.orig_h, &actual_w, &actual_h);
    608 	if (actual_w != sel->sz.scaled_w || actual_h != sel->sz.scaled_h) {
    609 		sel->sz.scaled_w = actual_w;
    610 		sel->sz.scaled_h = actual_h;
    611 		if (sel->rect_valid)
    612 			set_real_selection_rect(sel, sel->real_rect);
    613 		queue_update(0, 0, sel->sz.scaled_w, sel->sz.scaled_h);
    614 
    615 	}
    616 }
    617 
    618 /* queue the redrawing of a rectangular area on the image -
    619  * this queues four updates, one for each side of the rectangle,
    620  * with the width or height (depending on which side) of the
    621  * rectangle being determined by the configured line width */
    622 static void
    623 queue_rectangle_redraw(int x0, int y0, int x1, int y1) {
    624 	sort_coordinates(&x0, &y0, &x1, &y1);
    625 	queue_update(
    626 	    x0 - LINE_WIDTH > 0 ? x0 - LINE_WIDTH : 0,
    627 	    y0 - LINE_WIDTH > 0 ? y0 - LINE_WIDTH : 0,
    628 	    x1 - x0 + LINE_WIDTH * 2, LINE_WIDTH * 2);
    629 	queue_update(
    630 	    x0 - LINE_WIDTH > 0 ? x0 - LINE_WIDTH : 0,
    631 	    y1 - LINE_WIDTH > 0 ? y1 - LINE_WIDTH : 0,
    632 	    x1 - x0 + LINE_WIDTH * 2, LINE_WIDTH * 2);
    633 	queue_update(
    634 	    x0 - LINE_WIDTH > 0 ? x0 - LINE_WIDTH : 0,
    635 	    y0 - LINE_WIDTH > 0 ? y0 - LINE_WIDTH : 0,
    636 	    LINE_WIDTH * 2, y1 - y0 + LINE_WIDTH * 2);
    637 	queue_update(
    638 	    x1 - LINE_WIDTH > 0 ? x1 - LINE_WIDTH : 0,
    639 	    y0 - LINE_WIDTH > 0 ? y0 - LINE_WIDTH : 0,
    640 	    LINE_WIDTH * 2, y1 - y0 + LINE_WIDTH * 2);
    641 	/* this has to be set manually here in case the entire
    642 	   rectangle is outside the image area, as that will
    643 	   cause queue_area_update (called by queue_update) to
    644 	   return immediately without setting the flag, but
    645 	   redraw() still needs to know that the area outside
    646 	   the image needs to be redrawn */
    647 	state.ctx.dirty = 1;
    648 }
    649 
    650 /* set the appropriate cursor based on the
    651  * current mouse position and a cropping rectangle */
    652 static void
    653 set_cursor(struct Rect rect) {
    654 	Cursor c = None;
    655 	sort_coordinates(&rect.x0, &rect.y0, &rect.x1, &rect.y1);
    656 	if (collide_point(
    657 	    state.cursor_x, state.cursor_y,
    658 	    rect.x0, rect.y0)) {
    659 		c = cursors.topleft;
    660 	} else if (collide_point(
    661 	    state.cursor_x, state.cursor_y,
    662 	    rect.x1, rect.y0)) {
    663 		c = cursors.topright;
    664 	} else if (collide_point(
    665 	    state.cursor_x, state.cursor_y,
    666 	    rect.x0, rect.y1)) {
    667 		c = cursors.bottomleft;
    668 	} else if (collide_point(
    669 	    state.cursor_x, state.cursor_y,
    670 	    rect.x1, rect.y1)) {
    671 		c = cursors.bottomright;
    672 	} else if (collide_line(
    673 	    state.cursor_x, state.cursor_y,
    674 	    rect.x0, rect.y0, rect.x1, rect.y0)) {
    675 		c = cursors.top;
    676 	} else if (collide_line(
    677 	    state.cursor_x, state.cursor_y,
    678 	    rect.x1, rect.y1, rect.x0, rect.y1)) {
    679 		c = cursors.bottom;
    680 	} else if (collide_line(
    681 	    state.cursor_x, state.cursor_y,
    682 	    rect.x1, rect.y1, rect.x1, rect.y0)) {
    683 		c = cursors.right;
    684 	} else if (collide_line(
    685 	    state.cursor_x, state.cursor_y,
    686 	    rect.x0, rect.y0, rect.x0, rect.y1)) {
    687 		c = cursors.left;
    688 	} else if (collide_rect(state.cursor_x, state.cursor_y, rect)) {
    689 		c = cursors.grab;
    690 	}
    691 	XDefineCursor(state.ctx.dpy, state.ctx.win, c);
    692 }
    693 
    694 static void
    695 reset_cursor(void) {
    696 	XDefineCursor(state.ctx.dpy, state.ctx.win, None);
    697 }
    698 
    699 static void
    700 set_cursor_for_selection(struct Selection *sel) {
    701 	if (sel->rect_valid)
    702 		set_cursor(sel->scaled_rect);
    703 	else
    704 		reset_cursor();
    705 }
    706 
    707 static void
    708 drag_motion(XEvent event) {
    709 	if (state.cur_selection < 0 || !state.selections[state.cur_selection].rect_valid)
    710 		return;
    711 	struct Selection *sel = &state.selections[state.cur_selection];
    712 	struct Rect *rect = &sel->scaled_rect;
    713 
    714 	/* don't allow coordinates to go below 0 */
    715 	if (event.xbutton.x >= 0)
    716 		state.cursor_x = event.xbutton.x;
    717 	else
    718 		state.cursor_x = 0;
    719 	if (event.xbutton.y >= 0)
    720 		state.cursor_y = event.xbutton.y;
    721 	else
    722 		state.cursor_y = 0;
    723 
    724 	int x0 = rect->x0, x1 = rect->x1;
    725 	int y0 = rect->y0, y1 = rect->y1;
    726 	sort_coordinates(&x0, &y0, &x1, &y1);
    727 	/* redraw the old rectangle */
    728 	if (SELECTION_REDRAW && (state.moving || state.resizing))
    729 		queue_rectangle_redraw(x0, y0, x1, y1);
    730 	if (state.moving) {
    731 		int x_delta = state.cursor_x - state.move_handle.x;
    732 		int y_delta = state.cursor_y - state.move_handle.y;
    733 		/* don't allow coordinates to go below 0 */
    734 		int x_realdelta = x0 + x_delta >= 0 ? x_delta : -x0;
    735 		int y_realdelta = y0 + y_delta >= 0 ? y_delta : -y0;
    736 		rect->x0 += x_realdelta;
    737 		rect->x1 += x_realdelta;
    738 		rect->y0 += y_realdelta;
    739 		rect->y1 += y_realdelta;
    740 		state.move_handle.x = state.cursor_x;
    741 		state.move_handle.y = state.cursor_y;
    742 	} else if (state.resizing) {
    743 		if (!state.lock_y)
    744 			rect->x1 = state.cursor_x;
    745 		if (!state.lock_x)
    746 			rect->y1 = state.cursor_y;
    747 	} else {
    748 		set_cursor_for_selection(sel);
    749 		/* return directly to skip the redraw */
    750 		return;
    751 	}
    752 	set_scaled_selection_rect(sel, *rect);
    753 	set_cursor_for_selection(sel);
    754 
    755 	/* redraw the new rectangle */
    756 	if (SELECTION_REDRAW)
    757 		queue_rectangle_redraw(rect->x0, rect->y0, rect->x1, rect->y1);
    758 }
    759 
    760 static void
    761 scale_selection_rect(double scale, struct Rect rect, struct Rect *rect_ret) {
    762 	sort_coordinates(&rect.x0, &rect.y0, &rect.x1, &rect.y1);
    763 	rect.x0 = round(rect.x0 * scale);
    764 	rect.y0 = round(rect.y0 * scale);
    765 	rect.x1 = round(rect.x1 * scale);
    766 	rect.y1 = round(rect.y1 * scale);
    767 	*rect_ret = rect;
    768 }
    769 
    770 /* Only the width is taken into account for the scaling functions since the
    771    aspect ratio should have been preserved anyways. Of course, this could
    772    lead to tiny inaccuracies since the scaled width and height are truncated
    773    to integers. */
    774 static void
    775 set_scaled_selection_rect(struct Selection *sel, struct Rect rect) {
    776 	sel->scaled_rect = rect;
    777 	scale_selection_rect(
    778 		(double)sel->sz.orig_w / sel->sz.scaled_w, rect, &sel->real_rect
    779 	);
    780 	sel->rect_valid = 1;
    781 }
    782 
    783 static void
    784 set_real_selection_rect(struct Selection *sel, struct Rect rect) {
    785 	sel->real_rect = rect;
    786 	scale_selection_rect(
    787 		(double)sel->sz.scaled_w / sel->sz.orig_w, rect, &sel->scaled_rect
    788 	);
    789 	sel->rect_valid = 1;
    790 }
    791 
    792 static void
    793 copy_selection(int scaled) {
    794 	if (state.cur_selection < 0 || !state.selections[state.cur_selection].rect_valid)
    795 		return;
    796 	if (scaled) {
    797 		state.copied_selection.rect = state.selections[state.cur_selection].scaled_rect;
    798 		state.copied_selection.scaled = 1;
    799 	} else {
    800 		state.copied_selection.rect = state.selections[state.cur_selection].real_rect;
    801 		state.copied_selection.scaled = 0;
    802 	}
    803 	state.copied_selection.valid = 1;
    804 }
    805 
    806 static void
    807 paste_selection(void) {
    808 	if (!state.copied_selection.valid || state.cur_selection < 0 ||
    809 	    !state.selections[state.cur_selection].sz_valid) {
    810 		return;
    811 	}
    812 	struct Selection *sel = &state.selections[state.cur_selection];
    813 	/* clear the old rectangle */
    814 	struct Rect *rect = &sel->scaled_rect;
    815 	if (sel->rect_valid)
    816 		queue_rectangle_redraw(rect->x0, rect->y0, rect->x1, rect->y1);
    817 	if (state.copied_selection.scaled)
    818 		set_scaled_selection_rect(sel, state.copied_selection.rect);
    819 	else
    820 		set_real_selection_rect(sel, state.copied_selection.rect);
    821 	queue_rectangle_redraw(rect->x0, rect->y0, rect->x1, rect->y1);
    822 }
    823 
    824 static void
    825 select_all(void) {
    826 	if (state.cur_selection < 0 || !state.selections[state.cur_selection].sz_valid)
    827 		return;
    828 	struct Selection *sel = &state.selections[state.cur_selection];
    829 	struct Rect *rect = &sel->scaled_rect;
    830 	if (sel->rect_valid)
    831 		queue_rectangle_redraw(rect->x0, rect->y0, rect->x1, rect->y1);
    832 	set_real_selection_rect(sel, (struct Rect){0, 0, sel->sz.orig_w, sel->sz.orig_h});
    833 	queue_rectangle_redraw(rect->x0, rect->y0, rect->x1, rect->y1);
    834 }
    835 
    836 /* change the shown image
    837  * new_selection is the index of the new selection
    838  * copy_box determines whether the cropping rectangle of the current
    839  * selection should be copied (i.e. this is a true value when return
    840  * is pressed) */
    841 void
    842 change_picture(Imlib_Image new_image, int new_selection, int copy_box) {
    843 	int orig_w, orig_h, actual_w, actual_h;
    844 	/* set window title to filename */
    845 	XSetStandardProperties(
    846 	    state.ctx.dpy, state.ctx.win,
    847 	    state.filenames[new_selection],
    848 	    NULL, None, NULL, 0, NULL
    849 	);
    850 	if (state.ctx.cur_image) {
    851 		imlib_context_set_image(state.ctx.cur_image);
    852 		imlib_free_image();
    853 	}
    854 	state.ctx.cur_image = new_image;
    855 	imlib_context_set_image(state.ctx.cur_image);
    856 	int old_selection = state.cur_selection;
    857 	state.cur_selection = new_selection;
    858 
    859 	orig_w = imlib_image_get_width();
    860 	orig_h = imlib_image_get_height();
    861 	get_scaled_size(&state.ctx, orig_w, orig_h, &actual_w, &actual_h);
    862 
    863 	struct Selection *sel = &state.selections[state.cur_selection];
    864 	if (sel->sz_valid && (sel->sz.orig_w != orig_w || sel->sz.orig_h != orig_h)) {
    865 		fprintf(
    866 			stderr,
    867 			"WARNING: Image '%s' changed its size since it was previously opened.\n",
    868 			state.filenames[state.cur_selection]
    869 		);
    870 	}
    871 	sel->sz = (ImageSize){orig_w, orig_h, actual_w, actual_h};
    872 	sel->sz_valid = 1;
    873 	if (copy_box && old_selection >= 0 && old_selection < state.num_files) {
    874 		struct Selection *old = &state.selections[old_selection];
    875 		if (old->rect_valid) {
    876 			set_scaled_selection_rect(sel, old->scaled_rect);
    877 			sel->rect_valid = 1;
    878 		} else {
    879 			/* FIXME: an alternative would be to just leave the
    880 			   selection alone instead of clearing it if copy_box
    881 			   is true and the previous selection was not valid */
    882 			sel->rect_valid = 0;
    883 		}
    884 	} else if (sel->rect_valid) {
    885 		/* Always recalculate scaled selection in case the image scale
    886 		   changed (i.e. the window was resized) */
    887 		set_real_selection_rect(sel, sel->real_rect);
    888 	}
    889 	queue_update(0, 0, sel->sz.scaled_w, sel->sz.scaled_h);
    890 
    891 	/* set the cursor since the cropping rectangle may have changed */
    892 	set_cursor_for_selection(sel);
    893 }
    894 
    895 static void
    896 clear_selection(void) {
    897 	if (state.cur_selection < 0 || !state.selections[state.cur_selection].rect_valid)
    898 		return;
    899 	struct Selection *sel = &state.selections[state.cur_selection];
    900 	struct Rect *rect = &sel->scaled_rect;
    901 	if (sel->rect_valid)
    902 		queue_rectangle_redraw(rect->x0, rect->y0, rect->x1, rect->y1);
    903 	sel->rect_valid = 0;
    904 	set_cursor_for_selection(sel);
    905 }
    906 
    907 static void
    908 switch_color(void) {
    909 	if (state.cur_selection < 0 || !state.selections[state.cur_selection].rect_valid)
    910 		return;
    911 	state.cur_col = state.cur_col == 1 ? 2 : 1;
    912 	struct Selection *sel = &state.selections[state.cur_selection];
    913 	struct Rect *rect = &sel->scaled_rect;
    914 	if (sel->rect_valid)
    915 		queue_rectangle_redraw(rect->x0, rect->y0, rect->x1, rect->y1);
    916 }
    917 
    918 static int
    919 key_press(XEvent event) {
    920 	XWindowAttributes attrs;
    921 	char buf[32];
    922 	KeySym sym;
    923 	XLookupString(&event.xkey, buf, sizeof(buf), &sym, NULL);
    924 	switch (sym) {
    925 	case XK_Left:
    926 		last_picture(state.cur_selection, state.filenames, 0);
    927 		break;
    928 	case XK_Right:
    929 		next_picture(state.cur_selection, state.filenames, state.num_files, 0);
    930 		break;
    931 	case XK_Return:
    932 		if (event.xkey.state & ShiftMask)
    933 			last_picture(state.cur_selection, state.filenames, 1);
    934 		else
    935 			next_picture(state.cur_selection, state.filenames, state.num_files, 1);
    936 		break;
    937 	case XK_Delete:
    938 		clear_selection();
    939 		break;
    940 	case XK_Tab:
    941 		switch_color();
    942 		break;
    943 	case XK_space:
    944 		XGetWindowAttributes(state.ctx.dpy, state.ctx.win, &attrs);
    945 		resize_window(attrs.width, attrs.height);
    946 		/* queue update separately so it also redraws when
    947 		   size didn't change */
    948 		queue_update(0, 0, state.ctx.window_w, state.ctx.window_h);
    949 		break;
    950 	case XK_q:
    951 		state.print_on_exit = 1;
    952 		return 0;
    953 	case XK_y:
    954 		copy_selection(1);
    955 		break;
    956 	case XK_p:
    957 		paste_selection();
    958 		break;
    959 	case XK_Y:
    960 		copy_selection(0);
    961 		break;
    962 	case XK_a:
    963 		if (event.xkey.state & ControlMask)
    964 			select_all();
    965 		break;
    966 	default:
    967 		break;
    968 	}
    969 	return 1;
    970 }