croptool

Image cropping tool
git clone git://lumidify.org/croptool.git
Log | Files | Refs | README

commit 0aa2c1f79badb244fc0721a35a0b345c9a34c02b
Author: lumidify <nobody@lumidify.org>
Date:   Wed, 15 Apr 2020 19:42:28 +0200

Initial commit

Diffstat:
AMakefile | 19+++++++++++++++++++
AREADME | 45+++++++++++++++++++++++++++++++++++++++++++++
Acroptool.c | 492+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
3 files changed, 556 insertions(+), 0 deletions(-)

diff --git a/Makefile b/Makefile @@ -0,0 +1,19 @@ +CC = cc +PREFIX = /usr/local + +all: croptool + +croptool: croptool.c + ${CC} -pedantic -Wno-deprecated-declarations -Wall -Werror croptool.c -o croptool -std=c99 -g `pkg-config --libs --cflags gtk+-2.0` -lm + +install: all + cp -f croptool ${PREFIX}/bin + chmod 755 ${PREFIX}/bin/croptool + +uninstall: + rm -f ${PREFIX}/bin/croptool + +clean: + rm croptool + +.PHONY: clean install uninstall diff --git a/README b/README @@ -0,0 +1,45 @@ +Requirements: gtk2 (which requires cairo and the other crap anyways) + +This is a small image cropping tool. It was actually written to help +crop large amounts of pictures when digitizing books, but it can be +used for cropping single pictures as well. There are probably many +bugs still. Oh, and the code probably isn't that great. + +Just start it with "croptool <image files>" and a window will pop up. +Initially, no image is shown, so you first have to press enter or +right arrow to go to the first image. When an image is shown, you can +click on it to create a selection box. If you click near the edges or +corners of the box, you can change its size, and if you click anywhere +in the middle, you can move it. Clicking outside creates a new box. +I don't know if all of the collision logic is entirely correct, so +tell me if you notice any problems. + +Three keys are recognized: enter/return, right arrow, and left arrow. +Enter and right arrow both go to the next image, but enter copies the +selection box from the current image and uses it for the next picture, +while right arrow just goes to the next image and only displays a +selection box if it already had one. This is so that lots of pages +of a digitized book can be cropped quickly since the selection box +needs to be tweaked occasionally (since my digitizing equipment, if it +can be called that, isn't exactly very professional). Left arrow +just goes to the last picture. + +Note that resizing the window currently does not resize the images. +It will only take effect if you move to another image. There may be +bugs lurking here as well since the actual cropping box needs to be +scaled according to how much the image was scaled for display. + +When the window is closed, the ImageMagick command (mogrify -crop...) +for cropping each of the pictures that had a selection box defined +is printed (including the image currently being edited). + +Configuration: + +If you want to, you can edit a few things at the top of `bookcrop.c`. +COLLISION_PADDING is the number of pixels to check for collision if +an edge or corner is clicked. +SELECTION_COLOR is the color the selection box is drawn in. +If you want to change the command that is output, you can change +the function `print_cmd`. It just receives the filename, the coordinates +of the top left corner of the cropping box, and the width and height +of the box. diff --git a/croptool.c b/croptool.c @@ -0,0 +1,492 @@ +/* + * Copyright (c) 2020 lumidify <nobody[at]lumidify.org> + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#include <stdio.h> +#include <limits.h> +#include <stdlib.h> +#include <gtk/gtk.h> +#include <cairo/cairo.h> +#include <gdk/gdkkeysyms.h> + +/* The number of pixels to check on each side when checking + * if a corner or edge of the selection box was clicked + * (in order to change the size of the box) */ +static const int COLLISION_PADDING = 10; +/* The color of the selection box */ +static const char *SELECTION_COLOR = "#000"; + +/* Change this if you want a different output format. */ +static void +print_cmd(const char *filename, int x, int y, int w, int h) { + printf("mogrify -crop %dx%d+%d+%d '%s'\n", w, h, x, y, filename); +} + +struct Rect { + int x0; + int y0; + int x1; + int y1; +}; + +struct Point { + int x; + int y; +}; + +struct Selection { + struct Rect rect; + int orig_w; + int orig_h; + int scaled_w; + int scaled_h; +}; + +struct State { + struct Selection **selections; + char **filenames; + int cur_selection; + int num_files; + int window_w; + int window_h; + GdkPixbuf *cur_pixbuf; + struct Point move_handle; + gboolean moving; + gboolean lock_x; + gboolean lock_y; + GdkColor gdk_color; +}; + +static void swap(int *a, int *b); +static void sort_coordinates(int *x0, int *y0, int *x1, int *y1); +static int collide_point(int x, int y, int x_point, int y_point); +static int collide_line(int x, int y, int x0, int y0, int x1, int y1); +static int collide_rect(int x, int y, struct Rect rect); +static void redraw(GtkWidget *area, struct State *state); +static void destroy(GtkWidget *widget, gpointer data); +static gboolean draw_expose(GtkWidget *area, GdkEvent *event, gpointer data); +static gboolean button_press(GtkWidget *area, GdkEventButton *event, gpointer data); +static gboolean button_release(GtkWidget *area, GdkEventButton *event, gpointer data); +static gboolean drag_motion(GtkWidget *area, GdkEventMotion *event, gpointer data); +static gboolean key_press(GtkWidget *area, GdkEventKey *event, gpointer data); +static gboolean configure_event(GtkWidget *area, GdkEvent *event, gpointer data); +static void change_picture(GtkWidget *area, GdkPixbuf *new_pixbuf, int new_selection, + int orig_w, int orig_h, struct State *state, gboolean copy_box); +static void next_picture(GtkWidget *area, struct State *state, gboolean copy_box); +static void last_picture(GtkWidget *area, struct State *state); +static GdkPixbuf *load_pixbuf(char *filename, int w, int h, int *actual_w, int *actual_h); +static void print_selection(struct Selection *sel, const char *filename); + +int main(int argc, char *argv[]) { + GtkWidget *window; + gtk_init(&argc, &argv); + + argc--; + argv++; + if (argc < 1) { + fprintf(stderr, "No file given\n"); + exit(1); + } + + struct State *state = malloc(sizeof(struct State)); + state->cur_pixbuf = NULL; + state->selections = malloc(argc * sizeof(struct Selection *)); + state->num_files = argc; + state->filenames = argv; + state->cur_selection = -1; + state->moving = FALSE; + state->lock_x = FALSE; + state->lock_y = FALSE; + state->window_w = 0; + state->window_h = 0; + for (int i = 0; i < argc; i++) { + state->selections[i] = NULL; + } + + window = gtk_window_new(GTK_WINDOW_TOPLEVEL); + gtk_window_set_title(GTK_WINDOW(window), "croptool"); + gtk_widget_set_size_request(window, 500, 500); + g_signal_connect(G_OBJECT(window), "destroy", G_CALLBACK(destroy), NULL); + + GtkWidget *area = gtk_drawing_area_new(); + GTK_WIDGET_SET_FLAGS(area, GTK_CAN_FOCUS); + gtk_widget_add_events(area, + GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK | + GDK_BUTTON_MOTION_MASK | GDK_KEY_PRESS_MASK | + GDK_POINTER_MOTION_HINT_MASK); + gtk_container_add(GTK_CONTAINER(window), area); + + g_signal_connect(area, "expose-event", G_CALLBACK(draw_expose), state); + g_signal_connect(area, "button-press-event", G_CALLBACK(button_press), state); + g_signal_connect(area, "button-release-event", G_CALLBACK(button_release), state); + g_signal_connect(area, "motion-notify-event", G_CALLBACK(drag_motion), state); + g_signal_connect(window, "configure-event", G_CALLBACK(configure_event), state); + g_signal_connect(window, "key-press-event", G_CALLBACK(key_press), state); + + gtk_widget_show_all(window); + + GdkColormap *cmap = gdk_drawable_get_colormap(area->window); + gdk_colormap_alloc_color(cmap, &state->gdk_color, FALSE, TRUE); + gdk_color_parse(SELECTION_COLOR, &state->gdk_color); + + gtk_main(); + + for (int i = 0; i < argc; i++) { + if (state->selections[i] != NULL) { + print_selection(state->selections[i], argv[i]); + free(state->selections[i]); + } + } + if (state->cur_pixbuf) + g_object_unref(G_OBJECT(state->cur_pixbuf)); + free(state->selections); + free(state); + + return 0; +} + +static void +swap(int *a, int *b) { + int tmp = *a; + *a = *b; + *b = tmp; +} + +static void +sort_coordinates(int *x0, int *y0, int *x1, int *y1) { + if (*x0 > *x1) + swap(x0, x1); + if(*y0 > *y1) + swap(y0, y1); +} + +static void +print_selection(struct Selection *sel, const char *filename) { + /* The box was never actually used */ + if (sel->rect.x0 == -200) + return; + double scale = (double)sel->orig_w / sel->scaled_w; + int x0 = sel->rect.x0, y0 = sel->rect.y0; + int x1 = sel->rect.x1, y1 = sel->rect.y1; + sort_coordinates(&x0, &y0, &x1, &y1); + x0 = (int)(x0 * scale); + y0 = (int)(y0 * scale); + x1 = (int)(x1 * scale); + y1 = (int)(y1 * scale); + /* The box is completely outside of the picture. */ + if (x0 >= sel->orig_w || y0 >= sel->orig_h) + return; + /* Cut the bounding box if it goes past the end of the picture. */ + x0 = x0 < 0 ? 0 : x0; + y0 = y0 < 0 ? 0 : y0; + x1 = x1 > sel->orig_w ? sel->orig_w : x1; + y1 = y1 > sel->orig_h ? sel->orig_h : y1; + print_cmd(filename, x0, y0, x1 - x0, y1 - y0); +} + +static GdkPixbuf * +load_pixbuf(char *filename, int w, int h, int *actual_w, int *actual_h) { + (void)gdk_pixbuf_get_file_info(filename, actual_w, actual_h); + /* *actual_w and *actual_h can be garbage if the file doesn't exist */ + w = w < *actual_w || *actual_w < 0 ? w : *actual_w; + h = h < *actual_h || *actual_h < 0 ? h : *actual_h; + GError *err = NULL; + GdkPixbuf *pix = gdk_pixbuf_new_from_file_at_size(filename, w, h, &err); + if (err != NULL) { + fprintf(stderr, "%s\n", err->message); + g_error_free(err); + return NULL; + } + return pix; +} + +static void +destroy(GtkWidget *widget, gpointer data) { + gtk_main_quit(); +} + +static int +collide_point(int x, int y, int x_point, int y_point) { + return (abs(x - x_point) <= COLLISION_PADDING) && + (abs(y - y_point) <= COLLISION_PADDING); +} + +static int +collide_line(int x, int y, int x0, int y0, int x1, int y1) { + sort_coordinates(&x0, &y0, &x1, &y1); + /* this expects a valid line */ + if (x0 == x1) { + return (abs(x - x0) <= COLLISION_PADDING) && + (y0 <= y) && (y <= y1); + } else { + return (abs(y - y0) <= COLLISION_PADDING) && + (x0 <= x) && (x <= x1); + } +} + +static int +collide_rect(int x, int y, struct Rect rect) { + int x0 = rect.x0, x1 = rect.x1; + int y0 = rect.y0, y1 = rect.y1; + sort_coordinates(&x0, &y0, &x1, &y1); + return (x0 <= x) && (x <= x1) && (y0 <= y) && (y <= y1); +} + +static gboolean +button_press(GtkWidget *area, GdkEventButton *event, gpointer data) { + struct State *state = (struct State *)data; + if (state->cur_selection < 0 || state->selections[state->cur_selection] == NULL) + return FALSE; + struct Rect *rect = &state->selections[state->cur_selection]->rect; + gint x = event->x; + gint y = event->y; + int x0 = rect->x0, x1 = rect->x1; + int y0 = rect->y0, y1 = rect->y1; + if (collide_point(x, y, x0, y0)) { + rect->x0 = x1; + rect->y0 = y1; + rect->x1 = x; + rect->y1 = y; + } else if (collide_point(x, y, x1, y1)) { + rect->x1 = x; + rect->y1 = y; + } else if (collide_point(x, y, x0, y1)) { + rect->x0 = rect->x1; + rect->x1 = x; + rect->y1 = y; + } else if (collide_point(x, y, x1, y0)) { + rect->y0 = y1; + rect->x1 = x; + rect->y1 = y; + } else if (collide_line(x, y, x0, y0, x1, y0)) { + state->lock_y = TRUE; + swap(&rect->x0, &rect->x1); + rect->y0 = rect->y1; + rect->y1 = y; + } else if (collide_line(x, y, x0, y0, x0, y1)) { + state->lock_x = TRUE; + swap(&rect->y0, &rect->y1); + rect->x0 = rect->x1; + rect->x1 = x; + } else if (collide_line(x, y, x1, y1, x0, y1)) { + state->lock_y = TRUE; + rect->y1 = y; + } else if (collide_line(x, y, x1, y1, x1, y0)) { + state->lock_x = TRUE; + rect->x1 = x; + } else if (collide_rect(x, y, *rect)) { + state->moving = TRUE; + state->move_handle.x = x; + state->move_handle.y = y; + } else { + rect->x0 = x; + rect->y0 = y; + rect->x1 = x; + rect->y1 = y; + } + return FALSE; +} + +static gboolean +button_release(GtkWidget *area, GdkEventButton *event, gpointer data) { + struct State *state = (struct State *)data; + state->moving = FALSE; + state->lock_x = FALSE; + state->lock_y = FALSE; + return FALSE; +} + +static void +redraw(GtkWidget *area, struct State *state) { + if (!state->cur_pixbuf) + return; + if (!state->selections[state->cur_selection]) + return; + struct Rect rect = state->selections[state->cur_selection]->rect; + cairo_t *cr; + cr = gdk_cairo_create(area->window); + + gdk_cairo_set_source_pixbuf(cr, state->cur_pixbuf, 0, 0); + cairo_paint(cr); + + gdk_cairo_set_source_color(cr, &state->gdk_color); + cairo_move_to(cr, rect.x0, rect.y0); + cairo_line_to(cr, rect.x1, rect.y0); + cairo_line_to(cr, rect.x1, rect.y1); + cairo_line_to(cr, rect.x0, rect.y1); + cairo_line_to(cr, rect.x0, rect.y0); + cairo_stroke(cr); + cairo_destroy(cr); +} + +static gboolean +configure_event(GtkWidget *area, GdkEvent *event, gpointer data) { + struct State *state = (struct State *)data; + state->window_w = event->configure.width; + state->window_h = event->configure.height; + return FALSE; +} + +static gboolean +draw_expose(GtkWidget *area, GdkEvent *event, gpointer data) { + struct State *state = (struct State *)data; + if (state->cur_selection < 0 || state->selections[state->cur_selection] == NULL) + return FALSE; + redraw(area, state); + return FALSE; +} + +static gboolean +drag_motion(GtkWidget *area, GdkEventMotion *event, gpointer data) { + struct State *state = (struct State *)data; + if (state->cur_selection < 0 || state->selections[state->cur_selection] == NULL) + return FALSE; + struct Rect *rect = &state->selections[state->cur_selection]->rect; + gint x = event->x; + gint y = event->y; + if (state->moving == TRUE) { + int x_delta = x - state->move_handle.x; + int y_delta = y - state->move_handle.y; + rect->x0 += x_delta; + rect->y0 += y_delta; + rect->x1 += x_delta; + rect->y1 += y_delta; + state->move_handle.x = x; + state->move_handle.y = y; + } else { + if (state->lock_y != TRUE) + rect->x1 = x; + if (state->lock_x != TRUE) + rect->y1 = y; + } + + gtk_widget_queue_draw(area); + return FALSE; +} + +static struct Selection * +create_selection( + int rect_x0, int rect_y0, int rect_x1, int rect_y1, + int orig_w, int orig_h, int scaled_w, int scaled_h) { + + struct Selection *sel = malloc(sizeof(struct Selection)); + sel->rect.x0 = rect_x0; + sel->rect.y0 = rect_y0; + sel->rect.x1 = rect_x1; + sel->rect.y1 = rect_y1; + sel->orig_w = orig_w; + sel->orig_h = orig_h; + sel->scaled_w = scaled_w; + sel->scaled_h = scaled_h; + return sel; +} + +static void +change_picture( + GtkWidget *area, GdkPixbuf *new_pixbuf, + int new_selection, int orig_w, int orig_h, + struct State *state, gboolean copy_box) { + + if (state->cur_pixbuf != NULL) { + g_object_unref(G_OBJECT(state->cur_pixbuf)); + state->cur_pixbuf = NULL; + } + state->cur_pixbuf = new_pixbuf; + int old_selection = state->cur_selection; + state->cur_selection = new_selection; + + struct Selection *sel = state->selections[state->cur_selection]; + int actual_w = gdk_pixbuf_get_width(state->cur_pixbuf); + int actual_h = gdk_pixbuf_get_height(state->cur_pixbuf); + if (copy_box == TRUE && old_selection >= 0 && old_selection < state->num_files) { + struct Selection *old = state->selections[old_selection]; + if (sel) + free(sel); + sel = create_selection(old->rect.x0, old->rect.y0, old->rect.x1, old->rect.y1, + orig_w, orig_h, actual_w, actual_h); + } else if (!sel) { + /* Just fill it with -200 so we can check later if it has been used yet */ + sel = create_selection(-200, -200, -200, -200, orig_w, orig_h, actual_w, actual_h); + } else if (sel->rect.x0 != -200) { + /* If there is a selection, we need to convert it to the new scale. + * This only takes width into account because the + * aspect ratio should have been preserved anyways */ + double scale = (double)actual_w / sel->scaled_w; + sel->rect.x0 = (int)(sel->rect.x0 * scale); + sel->rect.y0 = (int)(sel->rect.y0 * scale); + sel->rect.x1 = (int)(sel->rect.x1 * scale); + sel->rect.y1 = (int)(sel->rect.y1 * scale); + sel->scaled_w = actual_w; + sel->scaled_h = actual_h; + } + state->selections[state->cur_selection] = sel; + gtk_widget_queue_draw(area); +} + +static void +next_picture(GtkWidget *area, struct State *state, gboolean copy_box) { + if (state->cur_selection + 1 >= state->num_files) + return; + GdkPixbuf *tmp_pixbuf = NULL; + int tmp_cur_selection = state->cur_selection; + int orig_w, orig_h; + /* loop until we find a loadable file */ + while (tmp_pixbuf == NULL && tmp_cur_selection + 1 < state->num_files) { + tmp_cur_selection++; + tmp_pixbuf = load_pixbuf( + state->filenames[tmp_cur_selection], + state->window_w, state->window_h, &orig_w, &orig_h); + } + if (!tmp_pixbuf) + return; + change_picture(area, tmp_pixbuf, tmp_cur_selection, orig_w, orig_h, state, copy_box); +} + +static void +last_picture(GtkWidget *area, struct State *state) { + if (state->cur_selection <= 0) + return; + GdkPixbuf *tmp_pixbuf = NULL; + int tmp_cur_selection = state->cur_selection; + int orig_w, orig_h; + /* loop until we find a loadable file */ + while (tmp_pixbuf == NULL && tmp_cur_selection > 0) { + tmp_cur_selection--; + tmp_pixbuf = load_pixbuf( + state->filenames[tmp_cur_selection], + state->window_w, state->window_h, &orig_w, &orig_h); + } + + if (!tmp_pixbuf) + return; + change_picture(area, tmp_pixbuf, tmp_cur_selection, orig_w, orig_h, state, FALSE); +} + +static gboolean +key_press(GtkWidget *area, GdkEventKey *event, gpointer data) { + struct State *state = (struct State *)data; + switch (event->keyval) { + case GDK_KEY_Left: + last_picture(area, state); + break; + case GDK_KEY_Right: + next_picture(area, state, FALSE); + break; + case GDK_KEY_Return: + next_picture(area, state, TRUE); + break; + } + return FALSE; +}