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 (17849B)
---
1 /*
2 * Copyright (c) 2020 lumidify <nobody[at]lumidify.org>
3 *
4 * Permission to use, copy, modify, and 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 <stdio.h>
18 #include <limits.h>
19 #include <stdlib.h>
20 #include <math.h>
21 #include <gtk/gtk.h>
22 #include <cairo/cairo.h>
23 #include <gdk/gdkkeysyms.h>
24
25 /* The number of pixels to check on each side when checking
26 * if a corner or edge of the selection box was clicked
27 * (in order to change the size of the box) */
28 static const int COLLISION_PADDING = 10;
29 /* The color of the selection box */
30 static const char *SELECTION_COLOR1 = "#000";
31 /* The second selection color - when tab is pressed */
32 static const char *SELECTION_COLOR2 = "#fff";
33
34 /* Change this if you want a different output format. */
35 static void
36 print_cmd(const char *filename, int x, int y, int w, int h) {
37 printf("mogrify -crop %dx%d+%d+%d '%s'\n", w, h, x, y, filename);
38 }
39
40 struct Rect {
41 int x0;
42 int y0;
43 int x1;
44 int y1;
45 };
46
47 struct Point {
48 int x;
49 int y;
50 };
51
52 struct Selection {
53 struct Rect rect;
54 int orig_w;
55 int orig_h;
56 int scaled_w;
57 int scaled_h;
58 };
59
60 struct State {
61 struct Selection **selections;
62 char **filenames;
63 int cur_selection;
64 int num_files;
65 int window_w;
66 int window_h;
67 GdkPixbuf *cur_pixbuf;
68 struct Point move_handle;
69 gboolean moving;
70 gboolean resizing;
71 gboolean lock_x;
72 gboolean lock_y;
73 GdkColor col1;
74 GdkColor col2;
75 int cur_col;
76 };
77
78 static void swap(int *a, int *b);
79 static void sort_coordinates(int *x0, int *y0, int *x1, int *y1);
80 static int collide_point(int x, int y, int x_point, int y_point);
81 static int collide_line(int x, int y, int x0, int y0, int x1, int y1);
82 static int collide_rect(int x, int y, struct Rect rect);
83 static void redraw(GtkWidget *area, struct State *state);
84 static void destroy(GtkWidget *widget, gpointer data);
85 static gboolean draw_expose(GtkWidget *area, GdkEvent *event, gpointer data);
86 static gboolean button_press(GtkWidget *area, GdkEventButton *event, gpointer data);
87 static gboolean button_release(GtkWidget *area, GdkEventButton *event, gpointer data);
88 static gboolean drag_motion(GtkWidget *area, GdkEventMotion *event, gpointer data);
89 static gboolean key_press(GtkWidget *area, GdkEventKey *event, gpointer data);
90 static gboolean configure_event(GtkWidget *area, GdkEvent *event, gpointer data);
91 static void change_picture(GtkWidget *area, GdkPixbuf *new_pixbuf, int new_selection,
92 int orig_w, int orig_h, struct State *state, gboolean copy_box);
93 static void next_picture(GtkWidget *area, struct State *state, gboolean copy_box);
94 static void last_picture(GtkWidget *area, struct State *state);
95 static GdkPixbuf *load_pixbuf(char *filename, int w, int h, int *actual_w, int *actual_h);
96 static void print_selection(struct Selection *sel, const char *filename);
97 static void clear_selection(GtkWidget *area, struct State *state);
98 static void resize_manual(GtkWidget *area, struct State *state);
99 static void switch_color(GtkWidget *area, struct State *state);
100
101 int main(int argc, char *argv[]) {
102 GtkWidget *window;
103 gtk_init(&argc, &argv);
104
105 argc--;
106 argv++;
107 if (argc < 1) {
108 fprintf(stderr, "No file given\n");
109 exit(1);
110 }
111
112 struct State *state = malloc(sizeof(struct State));
113 state->cur_pixbuf = NULL;
114 state->selections = malloc(argc * sizeof(struct Selection *));
115 state->num_files = argc;
116 state->filenames = argv;
117 state->cur_selection = -1;
118 state->moving = FALSE;
119 state->resizing = FALSE;
120 state->lock_x = FALSE;
121 state->lock_y = FALSE;
122 state->window_w = 0;
123 state->window_h = 0;
124 state->cur_col = 1;
125 for (int i = 0; i < argc; i++) {
126 state->selections[i] = NULL;
127 }
128
129 window = gtk_window_new(GTK_WINDOW_TOPLEVEL);
130 gtk_window_set_title(GTK_WINDOW(window), "croptool");
131 gtk_window_set_default_size(GTK_WINDOW(window), 500, 500);
132 g_signal_connect(G_OBJECT(window), "destroy", G_CALLBACK(destroy), NULL);
133
134 GtkWidget *area = gtk_drawing_area_new();
135 GTK_WIDGET_SET_FLAGS(area, GTK_CAN_FOCUS);
136 gtk_widget_add_events(area,
137 GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK |
138 GDK_BUTTON_MOTION_MASK | GDK_KEY_PRESS_MASK |
139 GDK_POINTER_MOTION_HINT_MASK | GDK_POINTER_MOTION_MASK);
140 gtk_container_add(GTK_CONTAINER(window), area);
141
142 g_signal_connect(area, "expose-event", G_CALLBACK(draw_expose), state);
143 g_signal_connect(area, "button-press-event", G_CALLBACK(button_press), state);
144 g_signal_connect(area, "button-release-event", G_CALLBACK(button_release), state);
145 g_signal_connect(area, "motion-notify-event", G_CALLBACK(drag_motion), state);
146 g_signal_connect(window, "configure-event", G_CALLBACK(configure_event), state);
147 g_signal_connect(window, "key-press-event", G_CALLBACK(key_press), state);
148
149 gtk_widget_show_all(window);
150
151 GdkColormap *cmap = gdk_drawable_get_colormap(area->window);
152 gdk_colormap_alloc_color(cmap, &state->col1, FALSE, TRUE);
153 gdk_color_parse(SELECTION_COLOR1, &state->col1);
154 gdk_colormap_alloc_color(cmap, &state->col2, FALSE, TRUE);
155 gdk_color_parse(SELECTION_COLOR2, &state->col2);
156 g_object_unref(cmap);
157
158 gtk_main();
159
160 for (int i = 0; i < argc; i++) {
161 if (state->selections[i]) {
162 print_selection(state->selections[i], argv[i]);
163 free(state->selections[i]);
164 }
165 }
166 if (state->cur_pixbuf)
167 g_object_unref(G_OBJECT(state->cur_pixbuf));
168 free(state->selections);
169 free(state);
170
171 return 0;
172 }
173
174 static void
175 swap(int *a, int *b) {
176 int tmp = *a;
177 *a = *b;
178 *b = tmp;
179 }
180
181 static void
182 sort_coordinates(int *x0, int *y0, int *x1, int *y1) {
183 if (*x0 > *x1)
184 swap(x0, x1);
185 if(*y0 > *y1)
186 swap(y0, y1);
187 }
188
189 static void
190 print_selection(struct Selection *sel, const char *filename) {
191 /* The box was never actually used */
192 if (sel->rect.x0 == -200)
193 return;
194 double scale = (double)sel->orig_w / sel->scaled_w;
195 int x0 = sel->rect.x0, y0 = sel->rect.y0;
196 int x1 = sel->rect.x1, y1 = sel->rect.y1;
197 sort_coordinates(&x0, &y0, &x1, &y1);
198 x0 = round(x0 * scale);
199 y0 = round(y0 * scale);
200 x1 = round(x1 * scale);
201 y1 = round(y1 * scale);
202 /* The box is completely outside of the picture. */
203 if (x0 >= sel->orig_w || y0 >= sel->orig_h)
204 return;
205 /* Cut the bounding box if it goes past the end of the picture. */
206 x0 = x0 < 0 ? 0 : x0;
207 y0 = y0 < 0 ? 0 : y0;
208 x1 = x1 > sel->orig_w ? sel->orig_w : x1;
209 y1 = y1 > sel->orig_h ? sel->orig_h : y1;
210 print_cmd(filename, x0, y0, x1 - x0, y1 - y0);
211 }
212
213 static GdkPixbuf *
214 load_pixbuf(char *filename, int w, int h, int *actual_w, int *actual_h) {
215 (void)gdk_pixbuf_get_file_info(filename, actual_w, actual_h);
216 /* *actual_w and *actual_h can be garbage if the file doesn't exist */
217 w = w < *actual_w || *actual_w < 0 ? w : *actual_w;
218 h = h < *actual_h || *actual_h < 0 ? h : *actual_h;
219 GError *err = NULL;
220 GdkPixbuf *pix = gdk_pixbuf_new_from_file_at_size(filename, w, h, &err);
221 if (err) {
222 fprintf(stderr, "%s\n", err->message);
223 g_error_free(err);
224 return NULL;
225 }
226 return pix;
227 }
228
229 static void
230 destroy(GtkWidget *widget, gpointer data) {
231 gtk_main_quit();
232 }
233
234 static int
235 collide_point(int x, int y, int x_point, int y_point) {
236 return (abs(x - x_point) <= COLLISION_PADDING) &&
237 (abs(y - y_point) <= COLLISION_PADDING);
238 }
239
240 static int
241 collide_line(int x, int y, int x0, int y0, int x1, int y1) {
242 sort_coordinates(&x0, &y0, &x1, &y1);
243 /* this expects a valid line */
244 if (x0 == x1) {
245 return (abs(x - x0) <= COLLISION_PADDING) &&
246 (y0 <= y) && (y <= y1);
247 } else {
248 return (abs(y - y0) <= COLLISION_PADDING) &&
249 (x0 <= x) && (x <= x1);
250 }
251 }
252
253 static int
254 collide_rect(int x, int y, struct Rect rect) {
255 int x0 = rect.x0, x1 = rect.x1;
256 int y0 = rect.y0, y1 = rect.y1;
257 sort_coordinates(&x0, &y0, &x1, &y1);
258 return (x0 <= x) && (x <= x1) && (y0 <= y) && (y <= y1);
259 }
260
261 static gboolean
262 button_press(GtkWidget *area, GdkEventButton *event, gpointer data) {
263 struct State *state = (struct State *)data;
264 if (state->cur_selection < 0 || !state->selections[state->cur_selection])
265 return FALSE;
266 struct Rect *rect = &state->selections[state->cur_selection]->rect;
267 gint x = event->x;
268 gint y = event->y;
269 int x0 = rect->x0, x1 = rect->x1;
270 int y0 = rect->y0, y1 = rect->y1;
271 if (collide_point(x, y, x0, y0)) {
272 rect->x0 = x1;
273 rect->y0 = y1;
274 rect->x1 = x;
275 rect->y1 = y;
276 } else if (collide_point(x, y, x1, y1)) {
277 rect->x1 = x;
278 rect->y1 = y;
279 } else if (collide_point(x, y, x0, y1)) {
280 rect->x0 = rect->x1;
281 rect->x1 = x;
282 rect->y1 = y;
283 } else if (collide_point(x, y, x1, y0)) {
284 rect->y0 = y1;
285 rect->x1 = x;
286 rect->y1 = y;
287 } else if (collide_line(x, y, x0, y0, x1, y0)) {
288 state->lock_y = TRUE;
289 swap(&rect->x0, &rect->x1);
290 rect->y0 = rect->y1;
291 rect->y1 = y;
292 } else if (collide_line(x, y, x0, y0, x0, y1)) {
293 state->lock_x = TRUE;
294 swap(&rect->y0, &rect->y1);
295 rect->x0 = rect->x1;
296 rect->x1 = x;
297 } else if (collide_line(x, y, x1, y1, x0, y1)) {
298 state->lock_y = TRUE;
299 rect->y1 = y;
300 } else if (collide_line(x, y, x1, y1, x1, y0)) {
301 state->lock_x = TRUE;
302 rect->x1 = x;
303 } else if (collide_rect(x, y, *rect)) {
304 state->moving = TRUE;
305 state->move_handle.x = x;
306 state->move_handle.y = y;
307 } else {
308 rect->x0 = x;
309 rect->y0 = y;
310 rect->x1 = x;
311 rect->y1 = y;
312 }
313 state->resizing = TRUE;
314 return FALSE;
315 }
316
317 static gboolean
318 button_release(GtkWidget *area, GdkEventButton *event, gpointer data) {
319 struct State *state = (struct State *)data;
320 state->moving = FALSE;
321 state->resizing = FALSE;
322 state->lock_x = FALSE;
323 state->lock_y = FALSE;
324 return FALSE;
325 }
326
327 static void
328 redraw(GtkWidget *area, struct State *state) {
329 if (!state->cur_pixbuf)
330 return;
331 cairo_t *cr;
332 cr = gdk_cairo_create(area->window);
333
334 gdk_cairo_set_source_pixbuf(cr, state->cur_pixbuf, 0, 0);
335 cairo_paint(cr);
336
337 GdkColor col = state->cur_col == 1 ? state->col1 : state->col2;
338 if (state->selections[state->cur_selection]) {
339 struct Rect rect = state->selections[state->cur_selection]->rect;
340 gdk_cairo_set_source_color(cr, &col);
341 cairo_move_to(cr, rect.x0, rect.y0);
342 cairo_line_to(cr, rect.x1, rect.y0);
343 cairo_line_to(cr, rect.x1, rect.y1);
344 cairo_line_to(cr, rect.x0, rect.y1);
345 cairo_line_to(cr, rect.x0, rect.y0);
346 cairo_stroke(cr);
347 }
348
349 cairo_destroy(cr);
350 }
351
352 static gboolean
353 configure_event(GtkWidget *area, GdkEvent *event, gpointer data) {
354 struct State *state = (struct State *)data;
355 state->window_w = event->configure.width;
356 state->window_h = event->configure.height;
357 if (state->cur_selection == -1 && state->window_w > 0 && state->window_h > 0) {
358 next_picture(area, state, FALSE);
359 }
360 return FALSE;
361 }
362
363 static gboolean
364 draw_expose(GtkWidget *area, GdkEvent *event, gpointer data) {
365 struct State *state = (struct State *)data;
366 if (state->cur_selection < 0)
367 return FALSE;
368 redraw(area, state);
369 return FALSE;
370 }
371
372 static gboolean
373 drag_motion(GtkWidget *area, GdkEventMotion *event, gpointer data) {
374 struct State *state = (struct State *)data;
375 if (state->cur_selection < 0 || !state->selections[state->cur_selection])
376 return FALSE;
377 struct Rect *rect = &state->selections[state->cur_selection]->rect;
378 gint x = event->x;
379 gint y = event->y;
380 if (state->moving == TRUE) {
381 int x_delta = x - state->move_handle.x;
382 int y_delta = y - state->move_handle.y;
383 rect->x0 += x_delta;
384 rect->y0 += y_delta;
385 rect->x1 += x_delta;
386 rect->y1 += y_delta;
387 state->move_handle.x = x;
388 state->move_handle.y = y;
389 } else if (state->resizing == TRUE) {
390 if (state->lock_y != TRUE)
391 rect->x1 = x;
392 if (state->lock_x != TRUE)
393 rect->y1 = y;
394 } else {
395 int x0 = rect->x0, x1 = rect->x1;
396 int y0 = rect->y0, y1 = rect->y1;
397 sort_coordinates(&x0, &y0, &x1, &y1);
398 GdkCursor *c = NULL;
399 GdkCursor *old = gdk_window_get_cursor(area->window);
400 if (old)
401 gdk_cursor_unref(old);
402 if (collide_point(x, y, x0, y0)) {
403 c = gdk_cursor_new(GDK_TOP_LEFT_CORNER);
404 } else if (collide_point(x, y, x1, y0)) {
405 c = gdk_cursor_new(GDK_TOP_RIGHT_CORNER);
406 } else if (collide_point(x, y, x0, y1)) {
407 c = gdk_cursor_new(GDK_BOTTOM_LEFT_CORNER);
408 } else if (collide_point(x, y, x1, y1)) {
409 c = gdk_cursor_new(GDK_BOTTOM_RIGHT_CORNER);
410 } else if (collide_line(x, y, x0, y0, x1, y0)) {
411 c = gdk_cursor_new(GDK_TOP_SIDE);
412 } else if (collide_line(x, y, x1, y1, x0, y1)) {
413 c = gdk_cursor_new(GDK_BOTTOM_SIDE);
414 } else if (collide_line(x, y, x1, y1, x1, y0)) {
415 c = gdk_cursor_new(GDK_RIGHT_SIDE);
416 } else if (collide_line(x, y, x0, y0, x0, y1)) {
417 c = gdk_cursor_new(GDK_LEFT_SIDE);
418 } else if (collide_rect(x, y, *rect)) {
419 c = gdk_cursor_new(GDK_FLEUR);
420 }
421 gdk_window_set_cursor(area->window, c);
422 return FALSE;
423 }
424
425 gtk_widget_queue_draw(area);
426 return FALSE;
427 }
428
429 static struct Selection *
430 create_selection(
431 int rect_x0, int rect_y0, int rect_x1, int rect_y1,
432 int orig_w, int orig_h, int scaled_w, int scaled_h) {
433
434 struct Selection *sel = malloc(sizeof(struct Selection));
435 sel->rect.x0 = rect_x0;
436 sel->rect.y0 = rect_y0;
437 sel->rect.x1 = rect_x1;
438 sel->rect.y1 = rect_y1;
439 sel->orig_w = orig_w;
440 sel->orig_h = orig_h;
441 sel->scaled_w = scaled_w;
442 sel->scaled_h = scaled_h;
443 return sel;
444 }
445
446 static void
447 change_picture(
448 GtkWidget *area, GdkPixbuf *new_pixbuf,
449 int new_selection, int orig_w, int orig_h,
450 struct State *state, gboolean copy_box) {
451
452 if (state->cur_pixbuf) {
453 g_object_unref(G_OBJECT(state->cur_pixbuf));
454 state->cur_pixbuf = NULL;
455 }
456 state->cur_pixbuf = new_pixbuf;
457 int old_selection = state->cur_selection;
458 state->cur_selection = new_selection;
459
460 struct Selection *sel = state->selections[state->cur_selection];
461 int actual_w = gdk_pixbuf_get_width(state->cur_pixbuf);
462 int actual_h = gdk_pixbuf_get_height(state->cur_pixbuf);
463 if (copy_box == TRUE && old_selection >= 0 && old_selection < state->num_files) {
464 struct Selection *old = state->selections[old_selection];
465 if (sel)
466 free(sel);
467 sel = create_selection(old->rect.x0, old->rect.y0, old->rect.x1, old->rect.y1,
468 orig_w, orig_h, actual_w, actual_h);
469 } else if (!sel) {
470 /* Just fill it with -200 so we can check later if it has been used yet */
471 sel = create_selection(-200, -200, -200, -200, orig_w, orig_h, actual_w, actual_h);
472 } else if (sel->rect.x0 != -200 && actual_w != sel->scaled_w) {
473 /* If there is a selection, we need to convert it to the new scale.
474 * This only takes width into account because the aspect ratio
475 * should have been preserved anyways */
476 double scale = (double)actual_w / sel->scaled_w;
477 sel->rect.x0 = round(sel->rect.x0 * scale);
478 sel->rect.y0 = round(sel->rect.y0 * scale);
479 sel->rect.x1 = round(sel->rect.x1 * scale);
480 sel->rect.y1 = round(sel->rect.y1 * scale);
481 }
482 sel->scaled_w = actual_w;
483 sel->scaled_h = actual_h;
484 state->selections[state->cur_selection] = sel;
485 gtk_widget_queue_draw(area);
486 }
487
488 static void
489 next_picture(GtkWidget *area, struct State *state, gboolean copy_box) {
490 if (state->cur_selection + 1 >= state->num_files)
491 return;
492 GdkPixbuf *tmp_pixbuf = NULL;
493 int tmp_cur_selection = state->cur_selection;
494 int orig_w, orig_h;
495 /* loop until we find a loadable file */
496 while (!tmp_pixbuf && tmp_cur_selection + 1 < state->num_files) {
497 tmp_cur_selection++;
498 tmp_pixbuf = load_pixbuf(
499 state->filenames[tmp_cur_selection],
500 state->window_w, state->window_h, &orig_w, &orig_h);
501 }
502 if (!tmp_pixbuf)
503 return;
504 change_picture(area, tmp_pixbuf, tmp_cur_selection, orig_w, orig_h, state, copy_box);
505 }
506
507 static void
508 last_picture(GtkWidget *area, struct State *state) {
509 if (state->cur_selection <= 0)
510 return;
511 GdkPixbuf *tmp_pixbuf = NULL;
512 int tmp_cur_selection = state->cur_selection;
513 int orig_w, orig_h;
514 /* loop until we find a loadable file */
515 while (!tmp_pixbuf && tmp_cur_selection > 0) {
516 tmp_cur_selection--;
517 tmp_pixbuf = load_pixbuf(
518 state->filenames[tmp_cur_selection],
519 state->window_w, state->window_h, &orig_w, &orig_h);
520 }
521
522 if (!tmp_pixbuf)
523 return;
524 change_picture(area, tmp_pixbuf, tmp_cur_selection, orig_w, orig_h, state, FALSE);
525 }
526
527 static void
528 clear_selection(GtkWidget *area, struct State *state) {
529 if (state->cur_selection < 0 || !state->selections[state->cur_selection])
530 return;
531 struct Selection *sel = state->selections[state->cur_selection];
532 sel->rect.x0 = sel->rect.x1 = sel->rect.y0 = sel->rect.y1 = -200;
533 gtk_widget_queue_draw(area);
534 }
535
536 static void
537 resize_manual(GtkWidget *area, struct State *state) {
538 if (state->cur_selection < 0 || !state->selections[state->cur_selection])
539 return;
540 int orig_w, orig_h;
541 GdkPixbuf *tmp_pixbuf = load_pixbuf(
542 state->filenames[state->cur_selection],
543 state->window_w, state->window_h, &orig_w, &orig_h);
544 if (!tmp_pixbuf)
545 return;
546 change_picture(area, tmp_pixbuf, state->cur_selection, orig_w, orig_h, state, FALSE);
547 }
548
549 static void
550 switch_color(GtkWidget *area, struct State *state) {
551 if (state->cur_selection < 0 || !state->selections[state->cur_selection])
552 return;
553 state->cur_col = state->cur_col == 1 ? 2 : 1;
554 gtk_widget_queue_draw(area);
555 }
556
557 static gboolean
558 key_press(GtkWidget *area, GdkEventKey *event, gpointer data) {
559 struct State *state = (struct State *)data;
560 switch (event->keyval) {
561 case GDK_KEY_Left:
562 last_picture(area, state);
563 break;
564 case GDK_KEY_Right:
565 next_picture(area, state, FALSE);
566 break;
567 case GDK_KEY_Return:
568 next_picture(area, state, TRUE);
569 break;
570 case GDK_KEY_Delete:
571 clear_selection(area, state);
572 break;
573 case GDK_KEY_space:
574 resize_manual(area, state);
575 break;
576 case GDK_KEY_Tab:
577 switch_color(area, state);
578 break;
579 }
580 return FALSE;
581 }