croptool

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

croptool.c (23657B)


      1 /*
      2  * Copyright (c) 2021-2024 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 struct Selection {
     77 	struct Rect rect;
     78 	ImageSize sz;
     79 	char valid;
     80 };
     81 
     82 static struct {
     83 	GraphicsContext ctx;
     84 
     85 	struct Selection *selections;
     86 	char **filenames;
     87 	int cur_selection;
     88 	int num_files;
     89 	int cursor_x;
     90 	int cursor_y;
     91 	struct Point move_handle;
     92 	XColor col1;
     93 	XColor col2;
     94 	int cur_col;
     95 	char moving;
     96 	char resizing;
     97 	char lock_x;
     98 	char lock_y;
     99 	char print_on_exit;
    100 } state;
    101 
    102 static struct {
    103 	Cursor top;
    104 	Cursor bottom;
    105 	Cursor left;
    106 	Cursor right;
    107 	Cursor topleft;
    108 	Cursor topright;
    109 	Cursor bottomleft;
    110 	Cursor bottomright;
    111 	Cursor grab;
    112 } cursors;
    113 
    114 static void usage(void);
    115 static void mainloop(void);
    116 static void setup(int argc, char *argv[]);
    117 static void sort_coordinates(int *x0, int *y0, int *x1, int *y1);
    118 static void swap(int *a, int *b);
    119 static void redraw(void);
    120 static void print_cmd(const char *filename, int x, int y, int w, int h, int dry_run);
    121 static void print_selection(struct Selection *sel, const char *filename);
    122 static int collide_point(int x, int y, int x_point, int y_point);
    123 static int collide_line(int x, int y, int x0, int y0, int x1, int y1);
    124 static int collide_rect(int x, int y, struct Rect rect);
    125 static void switch_color(void);
    126 static void clear_selection(void);
    127 static void set_selection(
    128     struct Selection *sel, int rect_x0, int rect_y0, int rect_x1,
    129     int rect_y1, int orig_w, int orig_h, int scaled_w, int scaled_h
    130 );
    131 static void queue_update(int x, int y, int w, int h);
    132 static void queue_rectangle_redraw(int x0, int y0, int x1, int y1);
    133 static void set_cursor(struct Rect rect);
    134 static void drag_motion(XEvent event);
    135 static void resize_window(int w, int h);
    136 static void button_release(void);
    137 static void button_press(XEvent event);
    138 static int key_press(XEvent event);
    139 
    140 static void
    141 usage(void) {
    142 	fprintf(stderr, "USAGE: croptool [-mr] [-f format] "
    143 	    "[-w width] [-c padding] [-p color] [-s color] "
    144 	    "[-z size] file ...\n");
    145 }
    146 
    147 int 
    148 main(int argc, char *argv[]) {
    149 	char c;
    150 
    151 	while ((c = getopt(argc, argv, "f:w:c:mrp:s:z:")) != -1) {
    152 		switch (c) {
    153 		case 'f':
    154 			CMD_FORMAT = optarg;
    155 			break;
    156 		case 'm':
    157 			RESIZE_REDRAW = 0;
    158 			break;
    159 		case 'r':
    160 			SELECTION_REDRAW = 0;
    161 			break;
    162 		case 'p':
    163 			SELECTION_COLOR1 = optarg;
    164 			break;
    165 		case 's':
    166 			SELECTION_COLOR2 = optarg;
    167 			break;
    168 		case 'c':
    169 			if (parse_int(optarg, 1, 99, &COLLISION_PADDING)) {
    170 				fprintf(stderr, "Invalid collision padding.\n");
    171 				exit(1);
    172 			}
    173 			break;
    174 		case 'w':
    175 			if (parse_int(optarg, 1, 99, &LINE_WIDTH)) {
    176 				fprintf(stderr, "Invalid line width.\n");
    177 				exit(1);
    178 			}
    179 			break;
    180 		case 'z':
    181 			if (parse_int(optarg, 0, 1024, &CACHE_SIZE)) {
    182 				fprintf(stderr, "Invalid cache size.\n");
    183 				exit(1);
    184 			}
    185 			break;
    186 		default:
    187 			usage();
    188 			exit(1);
    189 			break;
    190 		}
    191 	}
    192 	/* print warning if command format is invalid */
    193 	print_cmd("", 0, 0, 0, 0, 1);
    194 
    195 	argc -= optind;
    196 	argv += optind;
    197 	if (argc < 1) {
    198 		usage();
    199 		exit(1);
    200 	}
    201 	setup(argc, argv);
    202 
    203 	mainloop();
    204 
    205 	if (state.print_on_exit) {
    206 		for (int i = 0; i < argc; i++) {
    207 			if (state.selections[i].valid) {
    208 				print_selection(&state.selections[i], state.filenames[i]);
    209 			}
    210 		}
    211 	}
    212 
    213 	cleanup();
    214 
    215 	return 0;
    216 }
    217 
    218 static void
    219 mainloop(void) {
    220 	XEvent event;
    221 	int running = 1;
    222 
    223 	while (running) {
    224 		do {
    225 			XNextEvent(state.ctx.dpy, &event);
    226 			switch (event.type) {
    227 			case Expose:
    228 				if (RESIZE_REDRAW)
    229 					queue_update(event.xexpose.x, event.xexpose.y,
    230 					    event.xexpose.width, event.xexpose.height);
    231 				break;
    232 			case ConfigureNotify:
    233 				if (RESIZE_REDRAW)
    234 					resize_window(
    235 					    event.xconfigure.width,
    236 					    event.xconfigure.height
    237 					);
    238 				break;
    239 			case ButtonPress:
    240 				if (event.xbutton.button == Button1)
    241 					button_press(event);
    242 				break;
    243 			case ButtonRelease:
    244 				if (event.xbutton.button == Button1)
    245 					button_release();
    246 				break;
    247 			case MotionNotify:
    248 				drag_motion(event);
    249 				break;
    250 			case KeyPress:
    251 				running = key_press(event);
    252 				break;
    253 			case ClientMessage:
    254 				if ((Atom)event.xclient.data.l[0] == state.ctx.wm_delete_msg)
    255 					running = 0;
    256 			default:
    257 				break;
    258 			}
    259 		} while (XPending(state.ctx.dpy));
    260 
    261 		redraw();
    262 	}
    263 }
    264 
    265 static void
    266 setup(int argc, char *argv[]) {
    267 	state.selections = malloc(argc * sizeof(struct Selection));
    268 	if (!state.selections) {
    269 		fprintf(stderr, "Unable to allocate memory.\n");
    270 		exit(1);
    271 	}
    272 	state.num_files = argc;
    273 	state.filenames = argv;
    274 	state.cur_selection = -1;
    275 	state.moving = 0;
    276 	state.resizing = 0;
    277 	state.lock_x = 0;
    278 	state.lock_y = 0;
    279 	state.cursor_x = 0;
    280 	state.cursor_y = 0;
    281 	state.cur_col = 1;
    282 	state.print_on_exit = 0;
    283 
    284 	for (int i = 0; i < argc; i++) {
    285 		state.selections[i].valid = 0;
    286 	}
    287 
    288 	setup_x(&state.ctx, 500, 500, LINE_WIDTH, CACHE_SIZE);
    289 
    290         if (!XParseColor(state.ctx.dpy, state.ctx.cm, SELECTION_COLOR1, &state.col1)) {
    291 		fprintf(stderr, "Primary color invalid.\n");
    292 		exit(1);
    293 	}
    294         XAllocColor(state.ctx.dpy, state.ctx.cm, &state.col1);
    295         if (!XParseColor(state.ctx.dpy, state.ctx.cm, SELECTION_COLOR2, &state.col2)) {
    296 		fprintf(stderr, "Secondary color invalid.\n");
    297 		exit(1);
    298 	}
    299         XAllocColor(state.ctx.dpy, state.ctx.cm, &state.col2);
    300 
    301 	cursors.top = XCreateFontCursor(state.ctx.dpy, XC_top_side);
    302 	cursors.bottom = XCreateFontCursor(state.ctx.dpy, XC_bottom_side);
    303 	cursors.left = XCreateFontCursor(state.ctx.dpy, XC_left_side);
    304 	cursors.right = XCreateFontCursor(state.ctx.dpy, XC_right_side);
    305 	cursors.topleft = XCreateFontCursor(state.ctx.dpy, XC_top_left_corner);
    306 	cursors.topright = XCreateFontCursor(state.ctx.dpy, XC_top_right_corner);
    307 	cursors.bottomleft = XCreateFontCursor(state.ctx.dpy, XC_bottom_left_corner);
    308 	cursors.bottomright = XCreateFontCursor(state.ctx.dpy, XC_bottom_right_corner);
    309 	cursors.grab = XCreateFontCursor(state.ctx.dpy, XC_fleur);
    310 
    311 	next_picture(state.cur_selection, state.filenames, state.num_files, 0);
    312 	/* Only map window here so the program exits immediately if
    313 	   there are no loadable images, without first opening the
    314 	   window and closing it again immediately */
    315 	XMapWindow(state.ctx.dpy, state.ctx.win);
    316 	redraw();
    317 }
    318 
    319 void
    320 cleanup(void) {
    321 	free(state.selections);
    322 	cleanup_x(&state.ctx);
    323 }
    324 
    325 /* TODO: Escape filename properly
    326  * -> But how? Since the format can be set by the user,
    327  * it isn't really clear *what* needs to be escaped. */
    328 static void
    329 print_cmd(const char *filename, int x, int y, int w, int h, int dry_run) {
    330 	short percent = 0;
    331 	const char *c;
    332 	int length = 0;
    333 	int start_index = 0;
    334 	/* FIXME: just use putc instead of this complex printf dance */
    335 	for (c = CMD_FORMAT; *c != '\0'; c++) {
    336 		if (percent)
    337 			start_index++;
    338 		if (*c == '%') {
    339 			if (length) {
    340 				if (!dry_run)
    341 					printf("%.*s", length, CMD_FORMAT + start_index);
    342 				start_index += length;
    343 				length = 0;
    344 			}
    345 			if (percent && !dry_run)
    346 				printf("%%");
    347 			percent++;
    348 			percent %= 2;
    349 			start_index++;
    350 		} else if (percent && *c == 'w') {
    351 			if (!dry_run)
    352 				printf("%d", w);
    353 			percent = 0;
    354 		} else if (percent && *c == 'h') {
    355 			if (!dry_run)
    356 				printf("%d", h);
    357 			percent = 0;
    358 		} else if (percent && *c == 'l') {
    359 			if (!dry_run)
    360 				printf("%d", x);
    361 			percent = 0;
    362 		} else if (percent && *c == 't') {
    363 			if (!dry_run)
    364 				printf("%d", y);
    365 			percent = 0;
    366 		} else if (percent && *c == 'r') {
    367 			if (!dry_run)
    368 				printf("%d", x + w);
    369 			percent = 0;
    370 		} else if (percent && *c == 'b') {
    371 			if (!dry_run)
    372 				printf("%d", y + h);
    373 			percent = 0;
    374 		} else if (percent && *c == 'f') {
    375 			if (!dry_run)
    376 				printf("%s", filename);
    377 			percent = 0;
    378 		} else if (percent) {
    379 			if (dry_run) {
    380 				fprintf(stderr,
    381 				    "Warning: Unknown substitution '%c' "
    382 				    "in format string.\n", *c
    383 				);
    384 			} else {
    385 				printf("%%%c", *c);
    386 			}
    387 			percent = 0;
    388 		} else {
    389 			length++;
    390 		}
    391 	}
    392 	if (!dry_run) {
    393 		if (length)
    394 			printf("%.*s", length, CMD_FORMAT + start_index);
    395 		printf("\n");
    396 	}
    397 }
    398 
    399 static void
    400 redraw(void) {
    401 	if (!state.ctx.dirty)
    402 		return;
    403 	if (!state.ctx.cur_image || state.cur_selection < 0) {
    404 		clear_screen(&state.ctx);
    405 		swap_buffers(&state.ctx);
    406 		return;
    407 	}
    408 
    409 	/* draw the parts of the image that need to be redrawn */
    410 	struct Selection *sel = &state.selections[state.cur_selection];
    411 	draw_image_updates(&state.ctx, &sel->sz);
    412 
    413 	wipe_around_image(&state.ctx, &sel->sz);
    414 
    415 	/* draw the rectangle */
    416 	struct Rect rect = sel->rect;
    417 	if (rect.x0 != -200) {
    418 		XColor col = state.cur_col == 1 ? state.col1 : state.col2;
    419 		XSetForeground(state.ctx.dpy, state.ctx.gc, col.pixel);
    420 		sort_coordinates(&rect.x0, &rect.y0, &rect.x1, &rect.y1);
    421 		XDrawRectangle(
    422 		    state.ctx.dpy, state.ctx.drawable, state.ctx.gc,
    423 		    rect.x0, rect.y0, rect.x1 - rect.x0, rect.y1 - rect.y0
    424 		);
    425 	}
    426 	swap_buffers(&state.ctx);
    427 }
    428 
    429 static void
    430 swap(int *a, int *b) {
    431         int tmp = *a;
    432         *a = *b;
    433         *b = tmp;
    434 }
    435 
    436 /* sort rectangle coordinates into their canonical
    437  * form so *x1 - *x0 >= 0 and *y1 - *y0 >= 0 */
    438 static void
    439 sort_coordinates(int *x0, int *y0, int *x1, int *y1) {
    440         if (*x0 > *x1)
    441                 swap(x0, x1);
    442         if(*y0 > *y1)
    443                 swap(y0, y1);
    444 }
    445 
    446 static void
    447 print_selection(struct Selection *sel, const char *filename) {
    448 	/* The box was never actually used */
    449 	if (sel->rect.x0 == -200)
    450 		return;
    451 	double scale = (double)sel->sz.orig_w / sel->sz.scaled_w;
    452 	int x0 = sel->rect.x0, y0 = sel->rect.y0;
    453 	int x1 = sel->rect.x1, y1 = sel->rect.y1;
    454 	sort_coordinates(&x0, &y0, &x1, &y1);
    455 	x0 = round(x0 * scale);
    456 	y0 = round(y0 * scale);
    457 	x1 = round(x1 * scale);
    458 	y1 = round(y1 * scale);
    459 	/* The box is completely outside of the picture. */
    460 	if (x0 >= sel->sz.orig_w || y0 >= sel->sz.orig_h)
    461 		return;
    462 	/* Cut the bounding box if it goes past the end of the picture. */
    463 	x0 = x0 < 0 ? 0 : x0;
    464 	y0 = y0 < 0 ? 0 : y0;
    465 	x1 = x1 > sel->sz.orig_w ? sel->sz.orig_w : x1;
    466 	y1 = y1 > sel->sz.orig_h ? sel->sz.orig_h : y1;
    467 	print_cmd(filename, x0, y0, x1 - x0, y1 - y0, 0);
    468 }
    469 
    470 static int
    471 collide_point(int x, int y, int x_point, int y_point) {
    472 	return (abs(x - x_point) <= COLLISION_PADDING) &&
    473 		(abs(y - y_point) <= COLLISION_PADDING);
    474 }
    475 
    476 static int
    477 collide_line(int x, int y, int x0, int y0, int x1, int y1) {
    478 	sort_coordinates(&x0, &y0, &x1, &y1);
    479 	/* this expects a valid line */
    480 	if (x0 == x1) {
    481 		return (abs(x - x0) <= COLLISION_PADDING) &&
    482 			(y0 <= y) && (y <= y1);
    483 	} else {
    484 		return (abs(y - y0) <= COLLISION_PADDING) &&
    485 			(x0 <= x) && (x <= x1);
    486 	}
    487 }
    488 
    489 static int
    490 collide_rect(int x, int y, struct Rect rect) {
    491 	int x0 = rect.x0, x1 = rect.x1;
    492 	int y0 = rect.y0, y1 = rect.y1;
    493 	sort_coordinates(&x0, &y0, &x1, &y1);
    494 	return (x0 <= x) && (x <= x1) && (y0 <= y) && (y <= y1);
    495 }
    496 
    497 static void
    498 button_press(XEvent event) {
    499 	if (state.cur_selection < 0 || !state.selections[state.cur_selection].valid)
    500 		return;
    501 	struct Rect *rect = &state.selections[state.cur_selection].rect;
    502 	int x = event.xbutton.x;
    503 	int y = event.xbutton.y;
    504 	int x0 = rect->x0, x1 = rect->x1;
    505 	int y0 = rect->y0, y1 = rect->y1;
    506 	/* erase old rectangle */
    507 	queue_rectangle_redraw(x0, y0, x1, y1);
    508 	if (collide_point(x, y, x0, y0)) {
    509 		rect->x0 = x1;
    510 		rect->y0 = y1;
    511 		rect->x1 = x;
    512 		rect->y1 = y;
    513 	} else if (collide_point(x, y, x1, y1)) {
    514 		rect->x1 = x;
    515 		rect->y1 = y;
    516 	} else if (collide_point(x, y, x0, y1)) {
    517 		rect->x0 = rect->x1;
    518 		rect->x1 = x;
    519 		rect->y1 = y;
    520 	} else if (collide_point(x, y, x1, y0)) {
    521 		rect->y0 = y1;
    522 		rect->x1 = x;
    523 		rect->y1 = y;
    524 	} else if (collide_line(x, y, x0, y0, x1, y0)) {
    525 		state.lock_y = 1;
    526 		swap(&rect->x0, &rect->x1);
    527 		rect->y0 = rect->y1;
    528 		rect->y1 = y;
    529 	} else if (collide_line(x, y, x0, y0, x0, y1)) {
    530 		state.lock_x = 1;
    531 		swap(&rect->y0, &rect->y1);
    532 		rect->x0 = rect->x1;
    533 		rect->x1 = x;
    534 	} else if (collide_line(x, y, x1, y1, x0, y1)) {
    535 		state.lock_y = 1;
    536 		rect->y1 = y;
    537 	} else if (collide_line(x, y, x1, y1, x1, y0)) {
    538 		state.lock_x = 1;
    539 		rect->x1 = x;
    540 	} else if (collide_rect(x, y, *rect)) {
    541 		state.moving = 1;
    542 		state.move_handle.x = x;
    543 		state.move_handle.y = y;
    544 	} else {
    545 		rect->x0 = x;
    546 		rect->y0 = y;
    547 		rect->x1 = x;
    548 		rect->y1 = y;
    549 	}
    550 	state.resizing = 1;
    551 }
    552 
    553 static void
    554 queue_update(int x, int y, int w, int h) {
    555 	if (state.cur_selection < 0 || !state.selections[state.cur_selection].valid)
    556 		return;
    557 	struct Selection *sel = &state.selections[state.cur_selection];
    558 	queue_area_update(&state.ctx, &sel->sz, x, y, w, h);
    559 }
    560 
    561 static void
    562 button_release(void) {
    563 	state.moving = 0;
    564 	state.resizing = 0;
    565 	state.lock_x = 0;
    566 	state.lock_y = 0;
    567 	/* redraw everything if automatic redrawing of the rectangle
    568 	   is disabled (so it's redrawn when the mouse is released) */
    569 	if (!SELECTION_REDRAW)
    570 		queue_update(0, 0, state.ctx.window_w, state.ctx.window_h);
    571 }
    572 
    573 static void
    574 resize_window(int w, int h) {
    575 	int actual_w, actual_h;
    576 	struct Selection *sel;
    577 	state.ctx.window_w = w;
    578 	state.ctx.window_h = h;
    579 
    580 	if (state.cur_selection < 0 || !state.selections[state.cur_selection].valid)
    581 		return;
    582 	sel = &state.selections[state.cur_selection];
    583 	get_scaled_size(&state.ctx, sel->sz.orig_w, sel->sz.orig_h, &actual_w, &actual_h);
    584 	if (actual_w != sel->sz.scaled_w) {
    585 		if (sel->rect.x0 != -200) {
    586 			/* If there is a selection, we need to convert it to
    587 			 * the new scale. This only takes width into account
    588 			 * because the aspect ratio should have been preserved
    589 			 * anyways */
    590 			double scale = (double)actual_w / sel->sz.scaled_w;
    591 			sel->rect.x0 = round(sel->rect.x0 * scale);
    592 			sel->rect.y0 = round(sel->rect.y0 * scale);
    593 			sel->rect.x1 = round(sel->rect.x1 * scale);
    594 			sel->rect.y1 = round(sel->rect.y1 * scale);
    595 		}
    596 		sel->sz.scaled_w = actual_w;
    597 		sel->sz.scaled_h = actual_h;
    598 		queue_update(0, 0, sel->sz.scaled_w, sel->sz.scaled_h);
    599 	}
    600 }
    601 
    602 /* queue the redrawing of a rectangular area on the image -
    603  * this queues four updates, one for each side of the rectangle,
    604  * with the width or height (depending on which side) of the
    605  * rectangle being determined by the configured line width */
    606 static void
    607 queue_rectangle_redraw(int x0, int y0, int x1, int y1) {
    608 	sort_coordinates(&x0, &y0, &x1, &y1);
    609 	queue_update(
    610 	    x0 - LINE_WIDTH > 0 ? x0 - LINE_WIDTH : 0,
    611 	    y0 - LINE_WIDTH > 0 ? y0 - LINE_WIDTH : 0,
    612 	    x1 - x0 + LINE_WIDTH * 2, LINE_WIDTH * 2);
    613 	queue_update(
    614 	    x0 - LINE_WIDTH > 0 ? x0 - LINE_WIDTH : 0,
    615 	    y1 - LINE_WIDTH > 0 ? y1 - LINE_WIDTH : 0,
    616 	    x1 - x0 + LINE_WIDTH * 2, LINE_WIDTH * 2);
    617 	queue_update(
    618 	    x0 - LINE_WIDTH > 0 ? x0 - LINE_WIDTH : 0,
    619 	    y0 - LINE_WIDTH > 0 ? y0 - LINE_WIDTH : 0,
    620 	    LINE_WIDTH * 2, y1 - y0 + LINE_WIDTH * 2);
    621 	queue_update(
    622 	    x1 - LINE_WIDTH > 0 ? x1 - LINE_WIDTH : 0,
    623 	    y0 - LINE_WIDTH > 0 ? y0 - LINE_WIDTH : 0,
    624 	    LINE_WIDTH * 2, y1 - y0 + LINE_WIDTH * 2);
    625 }
    626 
    627 /* set the appropriate cursor based on the
    628  * current mouse position and a cropping rectangle */
    629 static void
    630 set_cursor(struct Rect rect) {
    631 	Cursor c = None;
    632 	sort_coordinates(&rect.x0, &rect.y0, &rect.x1, &rect.y1);
    633 	if (collide_point(
    634 	    state.cursor_x, state.cursor_y,
    635 	    rect.x0, rect.y0)) {
    636 		c = cursors.topleft;
    637 	} else if (collide_point(
    638 	    state.cursor_x, state.cursor_y,
    639 	    rect.x1, rect.y0)) {
    640 		c = cursors.topright;
    641 	} else if (collide_point(
    642 	    state.cursor_x, state.cursor_y,
    643 	    rect.x0, rect.y1)) {
    644 		c = cursors.bottomleft;
    645 	} else if (collide_point(
    646 	    state.cursor_x, state.cursor_y,
    647 	    rect.x1, rect.y1)) {
    648 		c = cursors.bottomright;
    649 	} else if (collide_line(
    650 	    state.cursor_x, state.cursor_y,
    651 	    rect.x0, rect.y0, rect.x1, rect.y0)) {
    652 		c = cursors.top;
    653 	} else if (collide_line(
    654 	    state.cursor_x, state.cursor_y,
    655 	    rect.x1, rect.y1, rect.x0, rect.y1)) {
    656 		c = cursors.bottom;
    657 	} else if (collide_line(
    658 	    state.cursor_x, state.cursor_y,
    659 	    rect.x1, rect.y1, rect.x1, rect.y0)) {
    660 		c = cursors.right;
    661 	} else if (collide_line(
    662 	    state.cursor_x, state.cursor_y,
    663 	    rect.x0, rect.y0, rect.x0, rect.y1)) {
    664 		c = cursors.left;
    665 	} else if (collide_rect(state.cursor_x, state.cursor_y, rect)) {
    666 		c = cursors.grab;
    667 	}
    668 	XDefineCursor(state.ctx.dpy, state.ctx.win, c);
    669 }
    670 
    671 static void
    672 drag_motion(XEvent event) {
    673 	if (state.cur_selection < 0 || !state.selections[state.cur_selection].valid)
    674 		return;
    675 	struct Selection *sel = &state.selections[state.cur_selection];
    676 	struct Rect *rect = &sel->rect;
    677 
    678 	/* don't allow coordinates to go below 0 */
    679 	if (event.xbutton.x >= 0)
    680 		state.cursor_x = event.xbutton.x;
    681 	else
    682 		state.cursor_x = 0;
    683 	if (event.xbutton.y >= 0)
    684 		state.cursor_y = event.xbutton.y;
    685 	else
    686 		state.cursor_y = 0;
    687 
    688 	int x0 = rect->x0, x1 = rect->x1;
    689 	int y0 = rect->y0, y1 = rect->y1;
    690 	sort_coordinates(&x0, &y0, &x1, &y1);
    691 	/* redraw the old rectangle */
    692 	if (SELECTION_REDRAW && (state.moving || state.resizing))
    693 		queue_rectangle_redraw(x0, y0, x1, y1);
    694 	if (state.moving) {
    695 		int x_delta = state.cursor_x - state.move_handle.x;
    696 		int y_delta = state.cursor_y - state.move_handle.y;
    697 		/* don't allow coordinates to go below 0 */
    698 		int x_realdelta = x0 + x_delta >= 0 ? x_delta : -x0;
    699 		int y_realdelta = y0 + y_delta >= 0 ? y_delta : -y0;
    700 		rect->x0 += x_realdelta;
    701 		rect->x1 += x_realdelta;
    702 		rect->y0 += y_realdelta;
    703 		rect->y1 += y_realdelta;
    704 		state.move_handle.x = state.cursor_x;
    705 		state.move_handle.y = state.cursor_y;
    706 	} else if (state.resizing) {
    707 		if (!state.lock_y)
    708 			rect->x1 = state.cursor_x;
    709 		if (!state.lock_x)
    710 			rect->y1 = state.cursor_y;
    711 	} else {
    712 		set_cursor(*rect);
    713 		return;
    714 	}
    715 	set_cursor(*rect);
    716 
    717 	/* redraw the new rectangle */
    718 	if (SELECTION_REDRAW)
    719 		queue_rectangle_redraw(rect->x0, rect->y0, rect->x1, rect->y1);
    720 }
    721 
    722 static void
    723 set_selection(
    724 	struct Selection *sel, int rect_x0, int rect_y0, int rect_x1,
    725 	int rect_y1, int orig_w, int orig_h, int scaled_w, int scaled_h) {
    726 
    727 	sel->rect.x0 = rect_x0;
    728 	sel->rect.y0 = rect_y0;
    729 	sel->rect.x1 = rect_x1;
    730 	sel->rect.y1 = rect_y1;
    731 	sel->sz.orig_w = orig_w;
    732 	sel->sz.orig_h = orig_h;
    733 	sel->sz.scaled_w = scaled_w;
    734 	sel->sz.scaled_h = scaled_h;
    735 }
    736 
    737 /* change the shown image
    738  * new_selection is the index of the new selection
    739  * copy_box determines whether the cropping rectangle of the current
    740  * selection should be copied (i.e. this is a true value when return
    741  * is pressed) */
    742 void
    743 change_picture(Imlib_Image new_image, int new_selection, int copy_box) {
    744 	int orig_w, orig_h, actual_w, actual_h;
    745 	/* set window title to filename */
    746 	XSetStandardProperties(
    747 	    state.ctx.dpy, state.ctx.win,
    748 	    state.filenames[new_selection],
    749 	    NULL, None, NULL, 0, NULL
    750 	);
    751 	if (state.ctx.cur_image) {
    752 		imlib_context_set_image(state.ctx.cur_image);
    753 		imlib_free_image();
    754 	}
    755 	state.ctx.cur_image = new_image;
    756 	imlib_context_set_image(state.ctx.cur_image);
    757 	int old_selection = state.cur_selection;
    758 	state.cur_selection = new_selection;
    759 
    760 	orig_w = imlib_image_get_width();
    761 	orig_h = imlib_image_get_height();
    762 	get_scaled_size(&state.ctx, orig_w, orig_h, &actual_w, &actual_h);
    763 
    764 	struct Selection *sel = &state.selections[state.cur_selection];
    765 	if (copy_box && old_selection >= 0 && old_selection < state.num_files) {
    766 		struct Selection *old = &state.selections[old_selection];
    767 		set_selection(
    768 		    sel,
    769 		    old->rect.x0, old->rect.y0, old->rect.x1, old->rect.y1,
    770 		    orig_w, orig_h, actual_w, actual_h
    771 		);
    772 	} else if (!sel->valid) {
    773 		/* Just fill it with -200 so we can check
    774 		 * later if it has been used yet */
    775 		set_selection(
    776 		    sel,
    777 		    -200, -200, -200, -200,
    778 		    orig_w, orig_h, actual_w, actual_h
    779 		);
    780 	} else if (sel->rect.x0 != -200 && actual_w != sel->sz.scaled_w) {
    781 		/* If there is a selection, we need to convert it to the
    782 		 * new scale. This only takes width into account because
    783 		 * the aspect ratio should have been preserved anyways */
    784 		double scale = (double)actual_w / sel->sz.scaled_w;
    785 		sel->rect.x0 = round(sel->rect.x0 * scale);
    786 		sel->rect.y0 = round(sel->rect.y0 * scale);
    787 		sel->rect.x1 = round(sel->rect.x1 * scale);
    788 		sel->rect.y1 = round(sel->rect.y1 * scale);
    789 	}
    790 	sel->sz.scaled_w = actual_w;
    791 	sel->sz.scaled_h = actual_h;
    792 	sel->valid = 1;
    793 	queue_update(0, 0, sel->sz.scaled_w, sel->sz.scaled_h);
    794 
    795 	/* set the cursor since the cropping rectangle may have changed */
    796 	set_cursor(sel->rect);
    797 }
    798 
    799 static void
    800 clear_selection(void) {
    801 	if (state.cur_selection < 0 || !state.selections[state.cur_selection].valid)
    802 		return;
    803 	struct Selection *sel = &state.selections[state.cur_selection];
    804 	sel->rect.x0 = sel->rect.x1 = sel->rect.y0 = sel->rect.y1 = -200;
    805 	queue_update(0, 0, sel->sz.scaled_w, sel->sz.scaled_h);
    806 }
    807 
    808 static void
    809 switch_color(void) {
    810 	if (state.cur_selection < 0 || !state.selections[state.cur_selection].valid)
    811 		return;
    812 	state.cur_col = state.cur_col == 1 ? 2 : 1;
    813 	queue_update(0, 0, state.ctx.window_w, state.ctx.window_h);
    814 }
    815 
    816 static int
    817 key_press(XEvent event) {
    818 	XWindowAttributes attrs;
    819 	char buf[32];
    820 	KeySym sym;
    821 	XLookupString(&event.xkey, buf, sizeof(buf), &sym, NULL);
    822 	switch (sym) {
    823 	case XK_Left:
    824 		last_picture(state.cur_selection, state.filenames, 0);
    825 		break;
    826 	case XK_Right:
    827 		next_picture(state.cur_selection, state.filenames, state.num_files, 0);
    828 		break;
    829 	case XK_Return:
    830 		if (event.xkey.state & ShiftMask)
    831 			last_picture(state.cur_selection, state.filenames, 1);
    832 		else
    833 			next_picture(state.cur_selection, state.filenames, state.num_files, 1);
    834 		break;
    835 	case XK_Delete:
    836 		clear_selection();
    837 		break;
    838 	case XK_Tab:
    839 		switch_color();
    840 		break;
    841 	case XK_space:
    842 		XGetWindowAttributes(state.ctx.dpy, state.ctx.win, &attrs);
    843 		resize_window(attrs.width, attrs.height);
    844 		/* queue update separately so it also redraws when
    845 		   size didn't change */
    846 		queue_update(0, 0, state.ctx.window_w, state.ctx.window_h);
    847 		break;
    848 	case XK_q:
    849 		state.print_on_exit = 1;
    850 		return 0;
    851 	default:
    852 		break;
    853 	}
    854 	return 1;
    855 }