commit 0aa2c1f79badb244fc0721a35a0b345c9a34c02b
Author: lumidify <nobody@lumidify.org>
Date: Wed, 15 Apr 2020 19:42:28 +0200
Initial commit
Diffstat:
A | Makefile | | | 19 | +++++++++++++++++++ |
A | README | | | 45 | +++++++++++++++++++++++++++++++++++++++++++++ |
A | croptool.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;
+}