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 (31857B)


      1 /*
      2  * Copyright (c) 2021-2023 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 <errno.h>
     20 #include <string.h>
     21 #include <stdlib.h>
     22 #include <limits.h>
     23 #include <unistd.h>
     24 #include <X11/Xlib.h>
     25 #include <X11/Xutil.h>
     26 #include <X11/keysym.h>
     27 #include <X11/XF86keysym.h>
     28 #include <X11/cursorfont.h>
     29 #ifndef NODB
     30 #include <X11/extensions/Xdbe.h>
     31 #endif
     32 #include <Imlib2.h>
     33 
     34 /* The number of pixels to check on each side when checking
     35  * if a corner or edge of the selection box was clicked 
     36  * (in order to change the size of the box) */
     37 static int COLLISION_PADDING = 10;
     38 /* The color of the selection box */
     39 static const char *SELECTION_COLOR1 = "#000000";
     40 /* The second selection color - when tab is pressed */
     41 static const char *SELECTION_COLOR2 = "#FFFFFF";
     42 /* The width of the selection line */
     43 static int LINE_WIDTH = 2;
     44 /* When set to 1, the display is redrawn on window resize */
     45 static short RESIZE_REDRAW = 1;
     46 /* When set to 1, the selection is redrawn continually,
     47    not just when the mouse button is released */
     48 static short SELECTION_REDRAW = 1;
     49 /*
     50   The command printed for each image.
     51   %w: Width of cropped area.
     52   %h: Height of cropped area.
     53   %l: Left side of cropped area.
     54   %r: Right side of cropped area.
     55   %t: Top side of cropped area.
     56   %b: Bottom side of cropped area.
     57   %f: Filename of image.
     58 */
     59 static const char *CMD_FORMAT = "croptool_crop %wx%h+%l+%t '%f'";
     60 /* Size of Imlib2 in-memory cache in MiB */
     61 static int CACHE_SIZE = 4;
     62 
     63 extern char *optarg;
     64 extern int optind;
     65 
     66 struct Rect {
     67 	int x0;
     68 	int y0;
     69 	int x1;
     70 	int y1;
     71 };
     72 
     73 struct Point {
     74 	int x;
     75 	int y;
     76 };
     77 
     78 struct Selection {
     79 	struct Rect rect;
     80 	int orig_w;
     81 	int orig_h;
     82 	int scaled_w;
     83 	int scaled_h;
     84 	char valid;
     85 };
     86 
     87 static struct {
     88 	Display *dpy;
     89 	GC gc;
     90 	Window win;
     91 	Visual *vis;
     92 	Drawable drawable;
     93 	#ifndef NODB
     94 	XdbeBackBuffer back_buf;
     95 	int db_enabled;
     96 	#endif
     97 	Colormap cm;
     98 	int screen;
     99 	int depth;
    100 
    101 	struct Selection *selections;
    102 	char **filenames;
    103 	int cur_selection;
    104 	int num_files;
    105 	int window_w;
    106 	int window_h;
    107 	int cursor_x;
    108 	int cursor_y;
    109 	struct Point move_handle;
    110 	char moving;
    111 	char resizing;
    112 	char lock_x;
    113 	char lock_y;
    114 	char dirty;
    115 	XColor col1;
    116 	XColor col2;
    117 	int cur_col;
    118 	Atom wm_delete_msg;
    119 	Imlib_Image cur_image;
    120 	Imlib_Updates updates;
    121 } state;
    122 
    123 static struct {
    124 	Cursor top;
    125 	Cursor bottom;
    126 	Cursor left;
    127 	Cursor right;
    128 	Cursor topleft;
    129 	Cursor topright;
    130 	Cursor bottomleft;
    131 	Cursor bottomright;
    132 	Cursor grab;
    133 } cursors;
    134 
    135 static void usage(void);
    136 static void mainloop(void);
    137 static void setup(int argc, char *argv[]);
    138 static void cleanup(void);
    139 static void sort_coordinates(int *x0, int *y0, int *x1, int *y1);
    140 static void swap(int *a, int *b);
    141 static void redraw(void);
    142 static void print_cmd(const char *filename, int x, int y, int w, int h, int dry_run);
    143 static void print_selection(struct Selection *sel, const char *filename);
    144 static int collide_point(int x, int y, int x_point, int y_point);
    145 static int collide_line(int x, int y, int x0, int y0, int x1, int y1);
    146 static int collide_rect(int x, int y, struct Rect rect);
    147 static void switch_color(void);
    148 static void clear_selection(void);
    149 static void next_picture(int copy_box);
    150 static void last_picture(int copy_box);
    151 static void change_picture(Imlib_Image new_image, int new_selection, int copy_box);
    152 static void get_scaled_size(int orig_w, int orig_h, int *scaled_w, int *scaled_h);
    153 static void set_selection(
    154     struct Selection *sel, int rect_x0, int rect_y0, int rect_x1,
    155     int rect_y1, int orig_w, int orig_h, int scaled_w, int scaled_h
    156 );
    157 static void queue_rectangle_redraw(int x0, int y0, int x1, int y1);
    158 static void set_cursor(struct Rect rect);
    159 static void drag_motion(XEvent event);
    160 static void resize_window(int w, int h);
    161 static void button_release(void);
    162 static void button_press(XEvent event);
    163 static int key_press(XEvent event);
    164 static void queue_update(int x, int y, int w, int h);
    165 static int parse_int(const char *str, int min, int max, int *value);
    166 
    167 static void
    168 usage(void) {
    169 	fprintf(stderr, "USAGE: croptool [-mr] [-f format] "
    170 	    "[-w width] [-c padding] [-p color] [-s color] "
    171 	    "[-z size] file ...\n");
    172 }
    173 
    174 int 
    175 main(int argc, char *argv[]) {
    176 	char c;
    177 
    178 	while ((c = getopt(argc, argv, "f:w:c:mrp:s:z:")) != -1) {
    179 		switch (c) {
    180 		case 'f':
    181 			CMD_FORMAT = optarg;
    182 			break;
    183 		case 'm':
    184 			RESIZE_REDRAW = 0;
    185 			break;
    186 		case 'r':
    187 			SELECTION_REDRAW = 0;
    188 			break;
    189 		case 'p':
    190 			SELECTION_COLOR1 = optarg;
    191 			break;
    192 		case 's':
    193 			SELECTION_COLOR2 = optarg;
    194 			break;
    195 		case 'c':
    196 			if (parse_int(optarg, 1, 99, &COLLISION_PADDING)) {
    197 				fprintf(stderr, "Invalid collision padding.\n");
    198 				exit(1);
    199 			}
    200 			break;
    201 		case 'w':
    202 			if (parse_int(optarg, 1, 99, &LINE_WIDTH)) {
    203 				fprintf(stderr, "Invalid line width.\n");
    204 				exit(1);
    205 			}
    206 			break;
    207 		case 'z':
    208 			if (parse_int(optarg, 0, 1024, &CACHE_SIZE)) {
    209 				fprintf(stderr, "Invalid cache size.\n");
    210 				exit(1);
    211 			}
    212 			break;
    213 		default:
    214 			usage();
    215 			exit(1);
    216 			break;
    217 		}
    218 	}
    219 	/* print warning if command format is invalid */
    220 	print_cmd("", 0, 0, 0, 0, 1);
    221 
    222 	argc -= optind;
    223 	argv += optind;
    224 	if (argc < 1) {
    225 		usage();
    226 		exit(1);
    227 	}
    228 	setup(argc, argv);
    229 
    230 	mainloop();
    231 
    232 	for (int i = 0; i < argc; i++) {
    233 		if (state.selections[i].valid) {
    234 			print_selection(&state.selections[i], state.filenames[i]);
    235 		}
    236 	}
    237 
    238 	cleanup();
    239 
    240 	return 0;
    241 }
    242 
    243 static void
    244 mainloop(void) {
    245 	XEvent event;
    246 	int running = 1;
    247 
    248 	while (running) {
    249 		do {
    250 			XNextEvent(state.dpy, &event);
    251 			switch (event.type) {
    252 			case Expose:
    253 				if (RESIZE_REDRAW)
    254 					queue_update(event.xexpose.x, event.xexpose.y,
    255 					    event.xexpose.width, event.xexpose.height);
    256 				break;
    257 			case ConfigureNotify:
    258 				if (RESIZE_REDRAW)
    259 					resize_window(
    260 					    event.xconfigure.width,
    261 					    event.xconfigure.height
    262 					);
    263 				break;
    264 			case ButtonPress:
    265 				if (event.xbutton.button == Button1)
    266 					button_press(event);
    267 				break;
    268 			case ButtonRelease:
    269 				if (event.xbutton.button == Button1)
    270 					button_release();
    271 				break;
    272 			case MotionNotify:
    273 				drag_motion(event);
    274 				break;
    275 			case KeyPress:
    276 				running = key_press(event);
    277 				break;
    278 			case ClientMessage:
    279 				if ((Atom)event.xclient.data.l[0] == state.wm_delete_msg)
    280 					running = 0;
    281 			default:
    282 				break;
    283 			}
    284 		} while (XPending(state.dpy));
    285 
    286 		redraw();
    287 	}
    288 }
    289 
    290 static void
    291 setup(int argc, char *argv[]) {
    292 	XSetWindowAttributes attrs;
    293 	XGCValues gcv;
    294 
    295 	state.cur_image = NULL;
    296 	state.selections = malloc(argc * sizeof(struct Selection));
    297 	if (!state.selections) {
    298 		fprintf(stderr, "Unable to allocate memory.\n");
    299 		exit(1);
    300 	}
    301 	state.num_files = argc;
    302 	state.filenames = argv;
    303 	state.cur_selection = -1;
    304 	state.moving = 0;
    305 	state.resizing = 0;
    306 	state.lock_x = 0;
    307 	state.lock_y = 0;
    308 	state.window_w = 500;
    309 	state.window_h = 500;
    310 	state.cursor_x = 0;
    311 	state.cursor_y = 0;
    312 	state.cur_col = 1;
    313 
    314 	for (int i = 0; i < argc; i++) {
    315 		state.selections[i].valid = 0;
    316 	}
    317 
    318 	state.dpy = XOpenDisplay(NULL);
    319 	state.screen = DefaultScreen(state.dpy);
    320 	state.vis = DefaultVisual(state.dpy, state.screen);
    321 	state.depth = DefaultDepth(state.dpy, state.screen);
    322 	state.cm = DefaultColormap(state.dpy, state.screen);
    323 
    324 	#ifndef NODB
    325 	state.db_enabled = 0;
    326 	/* based on http://wili.cc/blog/xdbe.html */
    327 	int major, minor;
    328 	if (XdbeQueryExtension(state.dpy, &major, &minor)) {
    329 		int num_screens = 1;
    330 		Drawable screens[] = { DefaultRootWindow(state.dpy) };
    331 		XdbeScreenVisualInfo *info = XdbeGetVisualInfo(
    332 		    state.dpy, screens, &num_screens
    333 		);
    334 		if (!info || num_screens < 1 || info->count < 1) {
    335 			fprintf(stderr,
    336 			    "Warning: No visuals support Xdbe, "
    337 			    "double buffering disabled.\n"
    338 			);
    339 		} else {
    340 			XVisualInfo xvisinfo_templ;
    341 			xvisinfo_templ.visualid = info->visinfo[0].visual;
    342 			xvisinfo_templ.screen = 0;
    343 			xvisinfo_templ.depth = info->visinfo[0].depth;
    344 			int matches;
    345 			XVisualInfo *xvisinfo_match = XGetVisualInfo(
    346 			    state.dpy,
    347 			    VisualIDMask | VisualScreenMask | VisualDepthMask,
    348 			    &xvisinfo_templ, &matches
    349 			);
    350 			if (!xvisinfo_match || matches < 1) {
    351 				fprintf(stderr,
    352 				    "Warning: Couldn't match a Visual with "
    353 				    "double buffering, double buffering disabled.\n"
    354 				);
    355 			} else {
    356 				state.vis = xvisinfo_match->visual;
    357 				state.depth = xvisinfo_match->depth;
    358 				state.db_enabled = 1;
    359 			}
    360 			XFree(xvisinfo_match);
    361 		}
    362 		XdbeFreeVisualInfo(info);
    363 	} else {
    364 		fprintf(stderr, "Warning: No Xdbe support, double buffering disabled.\n");
    365 	}
    366 	#endif
    367 
    368 	memset(&attrs, 0, sizeof(attrs));
    369 	attrs.background_pixel = BlackPixel(state.dpy, state.screen);
    370 	attrs.colormap = state.cm;
    371 	/* this causes the window contents to be kept
    372 	 * when it is resized, leading to less flicker */
    373 	attrs.bit_gravity = NorthWestGravity;
    374 	state.win = XCreateWindow(state.dpy, DefaultRootWindow(state.dpy), 0, 0,
    375 	    state.window_w, state.window_h, 0, state.depth,
    376 	    InputOutput, state.vis, CWBackPixel | CWColormap | CWBitGravity, &attrs);
    377 
    378 	#ifndef NODB
    379 	if (state.db_enabled) {
    380 		state.back_buf = XdbeAllocateBackBufferName(
    381 		    state.dpy, state.win, XdbeCopied
    382 		);
    383 		state.drawable = state.back_buf;
    384 	} else {
    385 		state.drawable = state.win;
    386 	}
    387 	#else
    388 	state.drawable = state.win;
    389 	#endif
    390 
    391 	memset(&gcv, 0, sizeof(gcv));
    392 	gcv.line_width = LINE_WIDTH;
    393 	state.gc = XCreateGC(state.dpy, state.win, GCLineWidth, &gcv);
    394 
    395         if (!XParseColor(state.dpy, state.cm, SELECTION_COLOR1, &state.col1)) {
    396 		fprintf(stderr, "Primary color invalid.\n");
    397 		exit(1);
    398 	}
    399         XAllocColor(state.dpy, state.cm, &state.col1);
    400         if (!XParseColor(state.dpy, state.cm, SELECTION_COLOR2, &state.col2)) {
    401 		fprintf(stderr, "Secondary color invalid.\n");
    402 		exit(1);
    403 	}
    404         XAllocColor(state.dpy, state.cm, &state.col2);
    405 
    406 	XSelectInput(
    407 	    state.dpy, state.win,
    408 	    StructureNotifyMask | KeyPressMask | ButtonPressMask |
    409 	    ButtonReleaseMask | PointerMotionMask | ExposureMask
    410 	);
    411 
    412 	state.wm_delete_msg = XInternAtom(state.dpy, "WM_DELETE_WINDOW", False);
    413 	XSetWMProtocols(state.dpy, state.win, &state.wm_delete_msg, 1);
    414 
    415 	cursors.top = XCreateFontCursor(state.dpy, XC_top_side);
    416 	cursors.bottom = XCreateFontCursor(state.dpy, XC_bottom_side);
    417 	cursors.left = XCreateFontCursor(state.dpy, XC_left_side);
    418 	cursors.right = XCreateFontCursor(state.dpy, XC_right_side);
    419 	cursors.topleft = XCreateFontCursor(state.dpy, XC_top_left_corner);
    420 	cursors.topright = XCreateFontCursor(state.dpy, XC_top_right_corner);
    421 	cursors.bottomleft = XCreateFontCursor(state.dpy, XC_bottom_left_corner);
    422 	cursors.bottomright = XCreateFontCursor(state.dpy, XC_bottom_right_corner);
    423 	cursors.grab = XCreateFontCursor(state.dpy, XC_fleur);
    424 
    425 	/* note: since CACHE_SIZE is <= 1024, this definitely fits in long */
    426 	long cs = (long)CACHE_SIZE * 1024 * 1024;
    427 	if (cs > INT_MAX) {
    428 		fprintf(stderr, "Cache size would cause integer overflow.\n");
    429 		exit(1);
    430 	}
    431 	imlib_set_cache_size((int)cs);
    432 	imlib_set_color_usage(128);
    433 	imlib_context_set_dither(1);
    434 	imlib_context_set_display(state.dpy);
    435 	imlib_context_set_visual(state.vis);
    436 	imlib_context_set_colormap(state.cm);
    437 	imlib_context_set_drawable(state.drawable);
    438 	state.updates = imlib_updates_init();
    439 
    440 	next_picture(0);
    441 	/* Only map window here so the program exits immediately if
    442 	   there are no loadable images, without first opening the
    443 	   window and closing it again immediately */
    444 	XMapWindow(state.dpy, state.win);
    445 	redraw();
    446 }
    447 
    448 static void
    449 cleanup(void) {
    450 	if (state.cur_image) {
    451 		imlib_context_set_image(state.cur_image);
    452 		imlib_free_image();
    453 	}
    454 	free(state.selections);
    455 	XDestroyWindow(state.dpy, state.win);
    456 	XCloseDisplay(state.dpy);
    457 }
    458 
    459 /* TODO: Escape filename properly
    460  * -> But how? Since the format can be set by the user,
    461  * it isn't really clear *what* needs to be escaped. */
    462 static void
    463 print_cmd(const char *filename, int x, int y, int w, int h, int dry_run) {
    464 	short percent = 0;
    465 	const char *c;
    466 	int length = 0;
    467 	int start_index = 0;
    468 	for (c = CMD_FORMAT; *c != '\0'; c++) {
    469 		if (percent)
    470 			start_index++;
    471 		if (*c == '%') {
    472 			if (length) {
    473 				if (!dry_run)
    474 					printf("%.*s", length, CMD_FORMAT + start_index);
    475 				start_index += length;
    476 				length = 0;
    477 			}
    478 			if (percent && !dry_run)
    479 				printf("%%");
    480 			percent++;
    481 			percent %= 2;
    482 			start_index++;
    483 		} else if (percent && *c == 'w') {
    484 			if (!dry_run)
    485 				printf("%d", w);
    486 			percent = 0;
    487 		} else if (percent && *c == 'h') {
    488 			if (!dry_run)
    489 				printf("%d", h);
    490 			percent = 0;
    491 		} else if (percent && *c == 'l') {
    492 			if (!dry_run)
    493 				printf("%d", x);
    494 			percent = 0;
    495 		} else if (percent && *c == 't') {
    496 			if (!dry_run)
    497 				printf("%d", y);
    498 			percent = 0;
    499 		} else if (percent && *c == 'r') {
    500 			if (!dry_run)
    501 				printf("%d", x + w);
    502 			percent = 0;
    503 		} else if (percent && *c == 'b') {
    504 			if (!dry_run)
    505 				printf("%d", y + h);
    506 			percent = 0;
    507 		} else if (percent && *c == 'f') {
    508 			if (!dry_run)
    509 				printf("%s", filename);
    510 			percent = 0;
    511 		} else if (percent) {
    512 			if (dry_run) {
    513 				fprintf(stderr,
    514 				    "Warning: Unknown substitution '%c' "
    515 				    "in format string.\n", *c
    516 				);
    517 			} else {
    518 				printf("%%%c", *c);
    519 			}
    520 			percent = 0;
    521 		} else {
    522 			length++;
    523 		}
    524 	}
    525 	if (!dry_run) {
    526 		if (length)
    527 			printf("%.*s", length, CMD_FORMAT + start_index);
    528 		printf("\n");
    529 	}
    530 }
    531 
    532 /* Parse integer between min and max (inclusive).
    533    Returns 1 on error, 0 otherwise.
    534    The result is stored in *value.
    535    Based on OpenBSD's strtonum. */
    536 static int
    537 parse_int(const char *str, int min, int max, int *value) {
    538 	char *end;
    539 	long l = strtol(str, &end, 10);
    540 	if (min > max)
    541 		return 1;
    542 	if (str == end || *end != '\0') {
    543 		return 1; 
    544 	} else if (l < min || l > max || ((l == LONG_MIN ||
    545 	    l == LONG_MAX) && errno == ERANGE)) {
    546 		return 1;
    547 	}
    548 	*value = (int)l;
    549 
    550 	return 0;
    551 }
    552 
    553 /* queue a part of the image for redrawing */
    554 static void
    555 queue_update(int x, int y, int w, int h) {
    556 	if (state.cur_selection < 0 || !state.selections[state.cur_selection].valid)
    557 		return;
    558 	state.dirty = 1;
    559 	struct Selection *sel = &state.selections[state.cur_selection];
    560 	if (x > sel->scaled_w || y > sel->scaled_h)
    561 		return;
    562 	state.updates = imlib_update_append_rect(
    563 	    state.updates, x, y,
    564 	    w + x > sel->scaled_w ? sel->scaled_w - x : w,
    565 	    h + y > sel->scaled_h ? sel->scaled_h - y : h
    566 	);
    567 }
    568 
    569 static void
    570 redraw(void) {
    571 	Imlib_Image buffer;
    572 	Imlib_Updates current_update;
    573 	if (!state.dirty)
    574 		return;
    575 	if (!state.cur_image || state.cur_selection < 0) {
    576 		/* clear the window completely */
    577 		XSetForeground(state.dpy, state.gc, BlackPixel(state.dpy, state.screen));
    578 		XFillRectangle(
    579 		    state.dpy, state.drawable, state.gc,
    580 		    0, 0, state.window_w, state.window_h
    581 		);
    582 		goto swap_buffers;
    583 	}
    584 
    585 	/* draw the parts of the image that need to be redrawn */
    586 	struct Selection *sel = &state.selections[state.cur_selection];
    587 	state.updates = imlib_updates_merge_for_rendering(
    588 	    state.updates, sel->scaled_w, sel->scaled_h
    589 	);
    590 	for (current_update = state.updates; current_update;
    591 	    current_update = imlib_updates_get_next(current_update)) {
    592 		int up_x, up_y, up_w, up_h;
    593 		imlib_updates_get_coordinates(current_update, &up_x, &up_y, &up_w, &up_h);
    594 		buffer = imlib_create_image(up_w, up_h);
    595 		imlib_context_set_blend(0);
    596 		imlib_context_set_image(buffer);
    597 		imlib_blend_image_onto_image(
    598 		    state.cur_image, 0, 0, 0,
    599 		    sel->orig_w, sel->orig_h,
    600 		    -up_x, -up_y,
    601 		    sel->scaled_w, sel->scaled_h);
    602 		imlib_render_image_on_drawable(up_x, up_y);
    603 		imlib_free_image();
    604 	}
    605 	if (state.updates)
    606 		imlib_updates_free(state.updates);
    607 	state.updates = imlib_updates_init();
    608 
    609 	/* wipe the black area around the image */
    610 	XSetForeground(state.dpy, state.gc, BlackPixel(state.dpy, state.screen));
    611 	XFillRectangle(
    612 	    state.dpy, state.drawable, state.gc,
    613 	    0, sel->scaled_h, sel->scaled_w, state.window_h - sel->scaled_h
    614 	);
    615 	XFillRectangle(
    616 	    state.dpy, state.drawable, state.gc,
    617 	    sel->scaled_w, 0, state.window_w - sel->scaled_w, state.window_h
    618 	);
    619 
    620 	/* draw the rectangle */
    621 	struct Rect rect = sel->rect;
    622 	if (rect.x0 != -200) {
    623 		XColor col = state.cur_col == 1 ? state.col1 : state.col2;
    624 		XSetForeground(state.dpy, state.gc, col.pixel);
    625 		sort_coordinates(&rect.x0, &rect.y0, &rect.x1, &rect.y1);
    626 		XDrawRectangle(
    627 		    state.dpy, state.drawable, state.gc,
    628 		    rect.x0, rect.y0, rect.x1 - rect.x0, rect.y1 - rect.y0
    629 		);
    630 	}
    631 
    632 swap_buffers:
    633 	#ifndef NODB
    634 	if (state.db_enabled) {
    635 		XdbeSwapInfo swap_info;
    636 		swap_info.swap_window = state.win;
    637 		swap_info.swap_action = XdbeCopied;
    638 
    639 		if (!XdbeSwapBuffers(state.dpy, &swap_info, 1))
    640 			fprintf(stderr, "Warning: Unable to swap buffers.\n");
    641 	}
    642 	#endif
    643 	state.dirty = 0;
    644 }
    645 
    646 static void
    647 swap(int *a, int *b) {
    648         int tmp = *a;
    649         *a = *b;
    650         *b = tmp;
    651 }
    652 
    653 /* sort rectangle coordinates into their canonical
    654  * form so *x1 - *x0 >= 0 and *y1 - *y0 >= 0 */
    655 static void
    656 sort_coordinates(int *x0, int *y0, int *x1, int *y1) {
    657         if (*x0 > *x1)
    658                 swap(x0, x1);
    659         if(*y0 > *y1)
    660                 swap(y0, y1);
    661 }
    662 
    663 static void
    664 print_selection(struct Selection *sel, const char *filename) {
    665 	/* The box was never actually used */
    666 	if (sel->rect.x0 == -200)
    667 		return;
    668 	double scale = (double)sel->orig_w / sel->scaled_w;
    669 	int x0 = sel->rect.x0, y0 = sel->rect.y0;
    670 	int x1 = sel->rect.x1, y1 = sel->rect.y1;
    671 	sort_coordinates(&x0, &y0, &x1, &y1);
    672 	x0 = round(x0 * scale);
    673 	y0 = round(y0 * scale);
    674 	x1 = round(x1 * scale);
    675 	y1 = round(y1 * scale);
    676 	/* The box is completely outside of the picture. */
    677 	if (x0 >= sel->orig_w || y0 >= sel->orig_h)
    678 		return;
    679 	/* Cut the bounding box if it goes past the end of the picture. */
    680 	x0 = x0 < 0 ? 0 : x0;
    681 	y0 = y0 < 0 ? 0 : y0;
    682 	x1 = x1 > sel->orig_w ? sel->orig_w : x1;
    683 	y1 = y1 > sel->orig_h ? sel->orig_h : y1;
    684 	print_cmd(filename, x0, y0, x1 - x0, y1 - y0, 0);
    685 }
    686 
    687 static int
    688 collide_point(int x, int y, int x_point, int y_point) {
    689 	return (abs(x - x_point) <= COLLISION_PADDING) &&
    690 		(abs(y - y_point) <= COLLISION_PADDING);
    691 }
    692 
    693 static int
    694 collide_line(int x, int y, int x0, int y0, int x1, int y1) {
    695 	sort_coordinates(&x0, &y0, &x1, &y1);
    696 	/* this expects a valid line */
    697 	if (x0 == x1) {
    698 		return (abs(x - x0) <= COLLISION_PADDING) &&
    699 			(y0 <= y) && (y <= y1);
    700 	} else {
    701 		return (abs(y - y0) <= COLLISION_PADDING) &&
    702 			(x0 <= x) && (x <= x1);
    703 	}
    704 }
    705 
    706 static int
    707 collide_rect(int x, int y, struct Rect rect) {
    708 	int x0 = rect.x0, x1 = rect.x1;
    709 	int y0 = rect.y0, y1 = rect.y1;
    710 	sort_coordinates(&x0, &y0, &x1, &y1);
    711 	return (x0 <= x) && (x <= x1) && (y0 <= y) && (y <= y1);
    712 }
    713 
    714 static void
    715 button_press(XEvent event) {
    716 	if (state.cur_selection < 0 || !state.selections[state.cur_selection].valid)
    717 		return;
    718 	struct Rect *rect = &state.selections[state.cur_selection].rect;
    719 	int x = event.xbutton.x;
    720 	int y = event.xbutton.y;
    721 	int x0 = rect->x0, x1 = rect->x1;
    722 	int y0 = rect->y0, y1 = rect->y1;
    723 	/* erase old rectangle */
    724 	queue_rectangle_redraw(x0, y0, x1, y1);
    725 	if (collide_point(x, y, x0, y0)) {
    726 		rect->x0 = x1;
    727 		rect->y0 = y1;
    728 		rect->x1 = x;
    729 		rect->y1 = y;
    730 	} else if (collide_point(x, y, x1, y1)) {
    731 		rect->x1 = x;
    732 		rect->y1 = y;
    733 	} else if (collide_point(x, y, x0, y1)) {
    734 		rect->x0 = rect->x1;
    735 		rect->x1 = x;
    736 		rect->y1 = y;
    737 	} else if (collide_point(x, y, x1, y0)) {
    738 		rect->y0 = y1;
    739 		rect->x1 = x;
    740 		rect->y1 = y;
    741 	} else if (collide_line(x, y, x0, y0, x1, y0)) {
    742 		state.lock_y = 1;
    743 		swap(&rect->x0, &rect->x1);
    744 		rect->y0 = rect->y1;
    745 		rect->y1 = y;
    746 	} else if (collide_line(x, y, x0, y0, x0, y1)) {
    747 		state.lock_x = 1;
    748 		swap(&rect->y0, &rect->y1);
    749 		rect->x0 = rect->x1;
    750 		rect->x1 = x;
    751 	} else if (collide_line(x, y, x1, y1, x0, y1)) {
    752 		state.lock_y = 1;
    753 		rect->y1 = y;
    754 	} else if (collide_line(x, y, x1, y1, x1, y0)) {
    755 		state.lock_x = 1;
    756 		rect->x1 = x;
    757 	} else if (collide_rect(x, y, *rect)) {
    758 		state.moving = 1;
    759 		state.move_handle.x = x;
    760 		state.move_handle.y = y;
    761 	} else {
    762 		rect->x0 = x;
    763 		rect->y0 = y;
    764 		rect->x1 = x;
    765 		rect->y1 = y;
    766 	}
    767 	state.resizing = 1;
    768 }
    769 
    770 static void
    771 button_release(void) {
    772 	state.moving = 0;
    773 	state.resizing = 0;
    774 	state.lock_x = 0;
    775 	state.lock_y = 0;
    776 	/* redraw everything if automatic redrawing of the rectangle
    777 	   is disabled (so it's redrawn when the mouse is released) */
    778 	if (!SELECTION_REDRAW)
    779 		queue_update(0, 0, state.window_w, state.window_h);
    780 }
    781 
    782 static void
    783 resize_window(int w, int h) {
    784 	int actual_w, actual_h;
    785 	struct Selection *sel;
    786 	state.window_w = w;
    787 	state.window_h = h;
    788 
    789 	if (state.cur_selection < 0 || !state.selections[state.cur_selection].valid)
    790 		return;
    791 	sel = &state.selections[state.cur_selection];
    792 	get_scaled_size(sel->orig_w, sel->orig_h, &actual_w, &actual_h);
    793 	if (actual_w != sel->scaled_w) {
    794 		if (sel->rect.x0 != -200) {
    795 			/* If there is a selection, we need to convert it to
    796 			 * the new scale. This only takes width into account
    797 			 * because the aspect ratio should have been preserved
    798 			 * anyways */
    799 			double scale = (double)actual_w / sel->scaled_w;
    800 			sel->rect.x0 = round(sel->rect.x0 * scale);
    801 			sel->rect.y0 = round(sel->rect.y0 * scale);
    802 			sel->rect.x1 = round(sel->rect.x1 * scale);
    803 			sel->rect.y1 = round(sel->rect.y1 * scale);
    804 		}
    805 		sel->scaled_w = actual_w;
    806 		sel->scaled_h = actual_h;
    807 		queue_update(0, 0, sel->scaled_w, sel->scaled_h);
    808 	}
    809 }
    810 
    811 /* queue the redrawing of a rectangular area on the image -
    812  * this queues four updates, one for each side of the rectangle,
    813  * with the width or height (depending on which side) of the
    814  * rectangle being determined by the configured line width */
    815 static void
    816 queue_rectangle_redraw(int x0, int y0, int x1, int y1) {
    817 	sort_coordinates(&x0, &y0, &x1, &y1);
    818 	queue_update(
    819 	    x0 - LINE_WIDTH > 0 ? x0 - LINE_WIDTH : 0,
    820 	    y0 - LINE_WIDTH > 0 ? y0 - LINE_WIDTH : 0,
    821 	    x1 - x0 + LINE_WIDTH * 2, LINE_WIDTH * 2);
    822 	queue_update(
    823 	    x0 - LINE_WIDTH > 0 ? x0 - LINE_WIDTH : 0,
    824 	    y1 - LINE_WIDTH > 0 ? y1 - LINE_WIDTH : 0,
    825 	    x1 - x0 + LINE_WIDTH * 2, LINE_WIDTH * 2);
    826 	queue_update(
    827 	    x0 - LINE_WIDTH > 0 ? x0 - LINE_WIDTH : 0,
    828 	    y0 - LINE_WIDTH > 0 ? y0 - LINE_WIDTH : 0,
    829 	    LINE_WIDTH * 2, y1 - y0 + LINE_WIDTH * 2);
    830 	queue_update(
    831 	    x1 - LINE_WIDTH > 0 ? x1 - LINE_WIDTH : 0,
    832 	    y0 - LINE_WIDTH > 0 ? y0 - LINE_WIDTH : 0,
    833 	    LINE_WIDTH * 2, y1 - y0 + LINE_WIDTH * 2);
    834 }
    835 
    836 /* set the appropriate cursor based on the
    837  * current mouse position and a cropping rectangle */
    838 static void
    839 set_cursor(struct Rect rect) {
    840 	Cursor c = None;
    841 	sort_coordinates(&rect.x0, &rect.y0, &rect.x1, &rect.y1);
    842 	if (collide_point(
    843 	    state.cursor_x, state.cursor_y,
    844 	    rect.x0, rect.y0)) {
    845 		c = cursors.topleft;
    846 	} else if (collide_point(
    847 	    state.cursor_x, state.cursor_y,
    848 	    rect.x1, rect.y0)) {
    849 		c = cursors.topright;
    850 	} else if (collide_point(
    851 	    state.cursor_x, state.cursor_y,
    852 	    rect.x0, rect.y1)) {
    853 		c = cursors.bottomleft;
    854 	} else if (collide_point(
    855 	    state.cursor_x, state.cursor_y,
    856 	    rect.x1, rect.y1)) {
    857 		c = cursors.bottomright;
    858 	} else if (collide_line(
    859 	    state.cursor_x, state.cursor_y,
    860 	    rect.x0, rect.y0, rect.x1, rect.y0)) {
    861 		c = cursors.top;
    862 	} else if (collide_line(
    863 	    state.cursor_x, state.cursor_y,
    864 	    rect.x1, rect.y1, rect.x0, rect.y1)) {
    865 		c = cursors.bottom;
    866 	} else if (collide_line(
    867 	    state.cursor_x, state.cursor_y,
    868 	    rect.x1, rect.y1, rect.x1, rect.y0)) {
    869 		c = cursors.right;
    870 	} else if (collide_line(
    871 	    state.cursor_x, state.cursor_y,
    872 	    rect.x0, rect.y0, rect.x0, rect.y1)) {
    873 		c = cursors.left;
    874 	} else if (collide_rect(state.cursor_x, state.cursor_y, rect)) {
    875 		c = cursors.grab;
    876 	}
    877 	XDefineCursor(state.dpy, state.win, c);
    878 }
    879 
    880 static void
    881 drag_motion(XEvent event) {
    882 	if (state.cur_selection < 0 || !state.selections[state.cur_selection].valid)
    883 		return;
    884 	struct Selection *sel = &state.selections[state.cur_selection];
    885 	struct Rect *rect = &sel->rect;
    886 
    887 	/* don't allow coordinates to go below 0 */
    888 	if (event.xbutton.x >= 0)
    889 		state.cursor_x = event.xbutton.x;
    890 	else
    891 		state.cursor_x = 0;
    892 	if (event.xbutton.y >= 0)
    893 		state.cursor_y = event.xbutton.y;
    894 	else
    895 		state.cursor_y = 0;
    896 
    897 	int x0 = rect->x0, x1 = rect->x1;
    898 	int y0 = rect->y0, y1 = rect->y1;
    899 	sort_coordinates(&x0, &y0, &x1, &y1);
    900 	/* redraw the old rectangle */
    901 	if (SELECTION_REDRAW && (state.moving || state.resizing))
    902 		queue_rectangle_redraw(x0, y0, x1, y1);
    903 	if (state.moving) {
    904 		int x_delta = state.cursor_x - state.move_handle.x;
    905 		int y_delta = state.cursor_y - state.move_handle.y;
    906 		/* don't allow coordinates to go below 0 */
    907 		int x_realdelta = x0 + x_delta >= 0 ? x_delta : -x0;
    908 		int y_realdelta = y0 + y_delta >= 0 ? y_delta : -y0;
    909 		rect->x0 += x_realdelta;
    910 		rect->x1 += x_realdelta;
    911 		rect->y0 += y_realdelta;
    912 		rect->y1 += y_realdelta;
    913 		state.move_handle.x = state.cursor_x;
    914 		state.move_handle.y = state.cursor_y;
    915 	} else if (state.resizing) {
    916 		if (!state.lock_y)
    917 			rect->x1 = state.cursor_x;
    918 		if (!state.lock_x)
    919 			rect->y1 = state.cursor_y;
    920 	} else {
    921 		set_cursor(*rect);
    922 		return;
    923 	}
    924 	set_cursor(*rect);
    925 
    926 	/* redraw the new rectangle */
    927 	if (SELECTION_REDRAW)
    928 		queue_rectangle_redraw(rect->x0, rect->y0, rect->x1, rect->y1);
    929 }
    930 
    931 static void
    932 set_selection(
    933 	struct Selection *sel, int rect_x0, int rect_y0, int rect_x1,
    934 	int rect_y1, int orig_w, int orig_h, int scaled_w, int scaled_h) {
    935 
    936 	sel->rect.x0 = rect_x0;
    937 	sel->rect.y0 = rect_y0;
    938 	sel->rect.x1 = rect_x1;
    939 	sel->rect.y1 = rect_y1;
    940 	sel->orig_w = orig_w;
    941 	sel->orig_h = orig_h;
    942 	sel->scaled_w = scaled_w;
    943 	sel->scaled_h = scaled_h;
    944 }
    945 
    946 /* get the scaled size of an image based on the current window size */
    947 static void
    948 get_scaled_size(int orig_w, int orig_h, int *scaled_w, int *scaled_h) {
    949 	double scale_w, scale_h;
    950 	scale_w = (double)state.window_w / (double)orig_w;
    951 	scale_h = (double)state.window_h / (double)orig_h;
    952 	if (orig_w <= state.window_w && orig_h <= state.window_h) {
    953 		*scaled_w = orig_w;
    954 		*scaled_h = orig_h;
    955 	} else if (scale_w * orig_h > state.window_h) {
    956 		*scaled_w = (int)(scale_h * orig_w);
    957 		*scaled_h = state.window_h;
    958 	} else {
    959 		*scaled_w = state.window_w;
    960 		*scaled_h = (int)(scale_w * orig_h);
    961 	}
    962 }
    963 
    964 /* change the shown image
    965  * new_selection is the index of the new selection
    966  * copy_box determines whether the cropping rectangle of the current
    967  * selection should be copied (i.e. this is a true value when return
    968  * is pressed) */
    969 static void
    970 change_picture(Imlib_Image new_image, int new_selection, int copy_box) {
    971 	int orig_w, orig_h, actual_w, actual_h;
    972 	/* set window title to filename */
    973 	XSetStandardProperties(
    974 	    state.dpy, state.win,
    975 	    state.filenames[new_selection],
    976 	    NULL, None, NULL, 0, NULL
    977 	);
    978 	if (state.cur_image) {
    979 		imlib_context_set_image(state.cur_image);
    980 		imlib_free_image();
    981 	}
    982 	state.cur_image = new_image;
    983 	imlib_context_set_image(state.cur_image);
    984 	int old_selection = state.cur_selection;
    985 	state.cur_selection = new_selection;
    986 
    987 	orig_w = imlib_image_get_width();
    988 	orig_h = imlib_image_get_height();
    989 	get_scaled_size(orig_w, orig_h, &actual_w, &actual_h);
    990 
    991 	struct Selection *sel = &state.selections[state.cur_selection];
    992 	if (copy_box && old_selection >= 0 && old_selection < state.num_files) {
    993 		struct Selection *old = &state.selections[old_selection];
    994 		set_selection(
    995 		    sel,
    996 		    old->rect.x0, old->rect.y0, old->rect.x1, old->rect.y1,
    997 		    orig_w, orig_h, actual_w, actual_h
    998 		);
    999 	} else if (!sel->valid) {
   1000 		/* Just fill it with -200 so we can check
   1001 		 * later if it has been used yet */
   1002 		set_selection(
   1003 		    sel,
   1004 		    -200, -200, -200, -200,
   1005 		    orig_w, orig_h, actual_w, actual_h
   1006 		);
   1007 	} else if (sel->rect.x0 != -200 && actual_w != sel->scaled_w) {
   1008 		/* If there is a selection, we need to convert it to the
   1009 		 * new scale. This only takes width into account because
   1010 		 * the aspect ratio should have been preserved anyways */
   1011 		double scale = (double)actual_w / sel->scaled_w;
   1012 		sel->rect.x0 = round(sel->rect.x0 * scale);
   1013 		sel->rect.y0 = round(sel->rect.y0 * scale);
   1014 		sel->rect.x1 = round(sel->rect.x1 * scale);
   1015 		sel->rect.y1 = round(sel->rect.y1 * scale);
   1016 	}
   1017 	sel->scaled_w = actual_w;
   1018 	sel->scaled_h = actual_h;
   1019 	sel->valid = 1;
   1020 	queue_update(0, 0, sel->scaled_w, sel->scaled_h);
   1021 
   1022 	/* set the cursor since the cropping rectangle may have changed */
   1023 	set_cursor(sel->rect);
   1024 }
   1025 
   1026 /* show the next image in the argument list - unloadable files are skipped
   1027  * copy_box determines whether the current selection is copied */
   1028 static void
   1029 next_picture(int copy_box) {
   1030 	if (state.cur_selection + 1 >= state.num_files)
   1031 		return;
   1032 	Imlib_Image tmp_image = NULL;
   1033 	int tmp_cur_selection = state.cur_selection;
   1034 	/* loop until we find a loadable file */
   1035 	while (!tmp_image && tmp_cur_selection + 1 < state.num_files) {
   1036 		tmp_cur_selection++;
   1037 		if (!state.filenames[tmp_cur_selection])
   1038 			continue;
   1039 		tmp_image = imlib_load_image_immediately(
   1040 		    state.filenames[tmp_cur_selection]
   1041 		);
   1042 		if (!tmp_image) {
   1043 			fprintf(stderr, "Warning: Unable to load image '%s'.\n",
   1044 			    state.filenames[tmp_cur_selection]);
   1045 			state.filenames[tmp_cur_selection] = NULL;
   1046 		}
   1047 	}
   1048 	/* immediately exit program if no loadable image is found on startup */
   1049 	if (state.cur_selection < 0 && !tmp_image) {
   1050 		fprintf(stderr, "No loadable images found.\n");
   1051 		cleanup();
   1052 		exit(1);
   1053 	}
   1054 	if (!tmp_image)
   1055 		return;
   1056 
   1057 	change_picture(tmp_image, tmp_cur_selection, copy_box);
   1058 }
   1059 
   1060 /* show the previous image in the argument list - unloadable files are skipped
   1061  * copy_box determines whether the current selection is copied */
   1062 static void
   1063 last_picture(int copy_box) {
   1064 	if (state.cur_selection <= 0)
   1065 		return;
   1066 	Imlib_Image tmp_image = NULL;
   1067 	int tmp_cur_selection = state.cur_selection;
   1068 	/* loop until we find a loadable file */
   1069 	while (!tmp_image && tmp_cur_selection > 0) {
   1070 		tmp_cur_selection--;
   1071 		if (!state.filenames[tmp_cur_selection])
   1072 			continue;
   1073 		tmp_image = imlib_load_image_immediately(
   1074 		    state.filenames[tmp_cur_selection]
   1075 		);
   1076 		if (!tmp_image) {
   1077 			fprintf(stderr, "Warning: Unable to load image '%s'.\n",
   1078 			    state.filenames[tmp_cur_selection]);
   1079 			state.filenames[tmp_cur_selection] = NULL;
   1080 		}
   1081 	}
   1082 
   1083 	if (!tmp_image)
   1084 		return;
   1085 
   1086 	change_picture(tmp_image, tmp_cur_selection, copy_box);
   1087 }
   1088 
   1089 static void
   1090 clear_selection(void) {
   1091 	if (state.cur_selection < 0 || !state.selections[state.cur_selection].valid)
   1092 		return;
   1093 	struct Selection *sel = &state.selections[state.cur_selection];
   1094 	sel->rect.x0 = sel->rect.x1 = sel->rect.y0 = sel->rect.y1 = -200;
   1095 	queue_update(0, 0, sel->scaled_w, sel->scaled_h);
   1096 }
   1097 
   1098 static void
   1099 switch_color(void) {
   1100 	if (state.cur_selection < 0 || !state.selections[state.cur_selection].valid)
   1101 		return;
   1102 	state.cur_col = state.cur_col == 1 ? 2 : 1;
   1103 	queue_update(0, 0, state.window_w, state.window_h);
   1104 }
   1105 
   1106 static int
   1107 key_press(XEvent event) {
   1108 	XWindowAttributes attrs;
   1109 	char buf[32];
   1110 	KeySym sym;
   1111 	XLookupString(&event.xkey, buf, sizeof(buf), &sym, NULL);
   1112 	switch (sym) {
   1113 	case XK_Left:
   1114 		last_picture(0);
   1115 		break;
   1116 	case XK_Right:
   1117 		next_picture(0);
   1118 		break;
   1119 	case XK_Return:
   1120 		if (event.xkey.state & ShiftMask)
   1121 			last_picture(1);
   1122 		else
   1123 			next_picture(1);
   1124 		break;
   1125 	case XK_Delete:
   1126 		clear_selection();
   1127 		break;
   1128 	case XK_Tab:
   1129 		switch_color();
   1130 		break;
   1131 	case XK_space:
   1132 		XGetWindowAttributes(state.dpy, state.win, &attrs);
   1133 		resize_window(attrs.width, attrs.height);
   1134 		/* queue update separately so it also redraws when
   1135 		   size didn't change */
   1136 		queue_update(0, 0, state.window_w, state.window_h);
   1137 		break;
   1138 	case XK_q:
   1139 		return 0;
   1140 	default:
   1141 		break;
   1142 	}
   1143 	return 1;
   1144 }