URI: 
       tInitial commit - croptool - Image cropping tool
  HTML git clone git://lumidify.org/croptool.git
   DIR Log
   DIR Files
   DIR Refs
   DIR README
       ---
   DIR commit 0aa2c1f79badb244fc0721a35a0b345c9a34c02b
  HTML 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(-)
       ---
   DIR diff --git a/Makefile b/Makefile
       t@@ -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
   DIR diff --git a/README b/README
       t@@ -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.
   DIR diff --git a/croptool.c b/croptool.c
       t@@ -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;
       +}