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