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 }