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