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 }