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;
+}