croptool

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

commit c5c5ee818931792a57b3115d89790a4ace8a1d4a
parent b9f502b3551d3ae657214abb591e119b417db772
Author: lumidify <nobody@lumidify.org>
Date:   Fri,  1 Jan 2021 22:18:46 +0100

Add new version based on xlib and imlib2

Diffstat:
A.gitignore | 1+
ALICENSE | 15+++++++++++++++
MMakefile | 35++++++++++++++++++++++++++---------
MREADME | 67+++----------------------------------------------------------------
MTODO | 1-
Acroptool.1 | 106+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mcroptool.c | 890+++++++++++++++++++++++++++++++++++++++++++++++++++----------------------------
CMakefile -> old/Makefile | 0
Aold/README | 70++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Ccroptool.c -> old/croptool.c | 0
10 files changed, 793 insertions(+), 392 deletions(-)

diff --git a/.gitignore b/.gitignore @@ -0,0 +1 @@ +croptool diff --git a/LICENSE b/LICENSE @@ -0,0 +1,15 @@ +/* + * Copyright (c) 2021 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. + */ diff --git a/Makefile b/Makefile @@ -1,19 +1,36 @@ -CC = cc +.POSIX: + +NAME = croptool +VERSION = 1.2-dev + PREFIX = /usr/local +MANPREFIX = ${PREFIX}/man + +BIN = ${NAME} +SRC = ${BIN:=.c} +MAN1 = ${BIN:=.1} + +CFLAGS += `pkg-config --cflags x11` `imlib2-config --cflags` +LDFLAGS += `pkg-config --libs x11` `imlib2-config --libs` -lm -all: croptool +all: ${BIN} -croptool: croptool.c - ${CC} -pedantic -Wno-deprecated-declarations -Wall -Werror croptool.c -o croptool -std=c99 `pkg-config --libs --cflags gtk+-2.0` -lm +${BIN}: + ${CC} ${CFLAGS} ${LDFLAGS} -o $@ ${SRC} install: all - cp -f croptool ${PREFIX}/bin - chmod 755 ${PREFIX}/bin/croptool + mkdir -p "${DESTDIR}${PREFIX}/bin" + cp -f ${BIN} "${DESTDIR}${PREFIX}/bin" + chmod 755 ${PREFIX}/bin/${BIN} + mkdir -p "${DESTDIR}${MANPREFIX}/man1" + cp -f ${MAN1} "${DESTDIR}${MANPREFIX}/man1" + chmod 644 "${DESTDIR}${MANPREFIX}/man1/${MAN1}" uninstall: - rm -f ${PREFIX}/bin/croptool + rm -f "${DESTDIR}${PREFIX}/bin/${BIN} + rm -f "${DESTDIR}${MANPREFIX}/man1/${MAN1}" clean: - rm -f croptool + rm -f ${BIN} -.PHONY: clean install uninstall +.PHONY: all clean install uninstall diff --git a/README b/README @@ -1,66 +1,5 @@ -Requirements: gtk2 (which requires cairo and the other crap anyways) +croptool - mass image cropping tool -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. +REQUIREMENTS: xlib, imlib2 -Note that the whole image is redrawn when changing the selection -because I'm too dumb to change that, so it may occasionally lag -a little bit. It barely lags on my nice laptop with a single-core -Intel Celeron 2.00 Ghz from 2008, though, so that shouldn't be a -huge problem. - -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. - -Several keys are recognized: -* 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 (my digitizing equipment, if it - can be called that, isn't exactly very professional). -* Left arrow just goes to the previous picture. -* Delete removes the selection for the current image (this is then - also not printed out at the end). -* Space bar resizes the image if the window was resized. -* Tab switches the color of the selection box between the two colors - defined at the top of `croptool.c` (SELECTION_COLOR1, SELECTION_COLOR2). - -Note that resizing the window currently does not resize the images. -It will only take effect if you move to another image or press -space bar. A side effect of this is that the image usually is -displayed at the wrong size when the window initially opens in a -tiling window manager because the window is first mapped at the -requested (500x500) size and then resized by the window manager. -Just press space bar if that happens (it hasn't bothered me too -much up til now, and I use dwm). There may be bugs lurking here -as well since the actual cropping box needs to be rescaled 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). If the box -was completely outside of the image, nothing is printed. If only part -of it was outside of the image, it is adjusted so that only the part -inside the image is printed. - -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_COLOR1 and SELECTION_COLOR2 are the two colors for the -selection box that can be switched with tab. -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. +See croptool.1 for more information. diff --git a/TODO b/TODO @@ -1,2 +1 @@ -Allow to change selection color from command line Proper path handling (allow paths including "'", etc.) diff --git a/croptool.1 b/croptool.1 @@ -0,0 +1,106 @@ +.Dd January 1, 2021 +.Dt CROPTOOL 1 +.Os +.Sh NAME +.Nm croptool +.Nd mass image cropping tool +.Sh SYNOPSIS +.Nm +.Op Ar -mr +.Op Ar -f format +.Op Ar -w width +.Op Ar -c padding +.Op Ar -p color +.Op Ar -s color +.Ar file ... +.Sh DESCRIPTION +.Nm +shows each of the given images and allows a cropping rectangle to be drawn. +On exit, the cropping command is printed out for each of the files. If a file +was skipped, nothing is printed out for it. +.Sh OPTIONS +.Bl -tag -width Ds +.It Fl m +Enable manual window redrawing (i.e. disable automatic redrawing) when the window +is resized. This may be useful on older machines that start accelerating global +warming when the image is redrawn constantly while resizing. +.It Fl r +Disable automatic redrawing while the cropping box is being dragged or resized, +for the same reason as +.Fl m . +.It Fl f Ar format +Set the format to be used when the cropping commands are output. +See OUTPUT FORMAT for details. +.It Fl w Ar width +Set the line width of the cropping rectangle. Default: 2. +.It Fl c Ar padding +Set the amount of padding used for collision with the mouse. This determines +how far away the mouse pointer has to be from an edge or corner of the +cropping rectangle to collide with it. Default: 10. +.It Fl p Ar color +Set the primary color for the cropping rectangle. Default: #000000. +.It Fl s Ar color +Set the secondary color for the cropping rectangle. Default: #FFFFFF. +.Sh OUTPUT FORMAT +The cropping commands for each image are output using the format given by +.Fl f +, or the default of +.Ql mogrify -crop %wx%h+%l+%t '%f' . +The following substitutions are performed: +.Bl -tag -width Ds +.It %% +Print +.Ql % . +.It %w +Print the width of the cropping rectangle. +.It %h +Print the height of the cropping rectangle. +.It %l +Print the location of the left side of the cropping rectangle. +.It %r +Print the location of the right side of the cropping rectangle. +.It %t +Print the location of the top side of the cropping rectangle. +.It %b +Print the location of the bottom side of the cropping rectangle. +.It %f +Print the filename of the image. Warning: This is printed out as is, +without any escaping. Yes, this should be fixed. +.El +.Pp +If an unknown substitution is encountered, a warning is printed to +stderr and the characters are printed out verbatim. +.Sh KEYBINDS +.Bl -tag -width Ds +.It ARROW LEFT +Go to the last image. +.It ARROW RIGHT +Go to the next image. +.It RETURN +Go to the next image, copying the current cropping rectangle. +.It TAB +Switch the color of the cropping rectangle between the primary and secondary colors. +.It DELETE +Delete the cropping rectangle of the current image. +.It SPACE +Redraw the window. This is useful when automatic resizing is disabled with +.Fl m . +.Sh MOUSE ACTIONS +.Bl -tag -width Ds +.It LEFT-CLICK +When inside an existing cropping rectangle, drag it around. When on one of the +edges, resize the rectangle, locking it to the that dimension. When on one of +the corners, resize the rectangle regardless of dimension. When outside an +existing cropping rectangle, start a new cropping rectangle. +.Sh EXIT STATUS +.Ex -std +.Sh SEE ALSO +.Xr mogrify 1 +.Sh BUGS +The filenames are printed out without any escaping, so filenames with quotes +may cause issues depending on the output format. +.Pp +Nothing in particular has been done to prevent screen flicker, so there is +flickering when resizing the window or cropping rectangle. +.Sh AUTHORS +.An lumidify Aq Mt nobody@lumidify.org diff --git a/croptool.c b/croptool.c @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020 lumidify <nobody[at]lumidify.org> + * Copyright (c) 2021 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 @@ -14,28 +14,46 @@ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. */ +#include <math.h> #include <stdio.h> -#include <limits.h> +#include <errno.h> +#include <string.h> #include <stdlib.h> -#include <math.h> -#include <gtk/gtk.h> -#include <cairo/cairo.h> -#include <gdk/gdkkeysyms.h> +#include <stdlib.h> +#include <unistd.h> +#include <X11/Xlib.h> +#include <X11/Xutil.h> +#include <X11/keysym.h> +#include <X11/XF86keysym.h> +#include <X11/cursorfont.h> +#include <Imlib2.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; +static int COLLISION_PADDING = 10; /* The color of the selection box */ -static const char *SELECTION_COLOR1 = "#000"; +static char *SELECTION_COLOR1 = "#000000"; /* The second selection color - when tab is pressed */ -static const char *SELECTION_COLOR2 = "#fff"; - -/* 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); -} +static char *SELECTION_COLOR2 = "#FFFFFF"; +/* The width of the selection line */ +static int LINE_WIDTH = 2; +/* When set to 1, the display is redrawn on window resize */ +static short RESIZE_REDRAW = 1; +/* When set to 1, the selection is redrawn continually, + not just when the mouse button is released */ +static short SELECTION_REDRAW = 1; +/* + The command printed for each image. + %w: Width of cropped area. + %h: Height of cropped area. + %l: Left side of cropped area. + %r: Right side of cropped area. + %t: Top side of cropped area. + %b: Bottom side of cropped area. + %f: Filename of image. +*/ +static char *CMD_FORMAT = "mogrify -crop %wx%h+%l+%t '%f'"; struct Rect { int x0; @@ -55,135 +73,397 @@ struct Selection { int orig_h; int scaled_w; int scaled_h; + short valid; }; -struct State { - struct Selection **selections; +static struct { + Display *dpy; + GC gc; + Window win; + Visual *vis; + Colormap cm; + int screen; + int depth; + + 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 resizing; - gboolean lock_x; - gboolean lock_y; - GdkColor col1; - GdkColor col2; + short moving; + short resizing; + short lock_x; + short lock_y; + XColor col1; + XColor col2; int cur_col; -}; + Imlib_Image cur_image; + Imlib_Updates updates; +} state; + +static struct { + Cursor top; + Cursor bottom; + Cursor left; + Cursor right; + Cursor topleft; + Cursor topright; + Cursor bottomleft; + Cursor bottomright; + Cursor grab; +} cursors; -static void swap(int *a, int *b); static void sort_coordinates(int *x0, int *y0, int *x1, int *y1); +static void swap(int *a, int *b); +static void redraw(); +static void print_cmd(const char *filename, int x, int y, int w, int h); +static void print_selection(struct Selection *sel, const char *filename); 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); -static void clear_selection(GtkWidget *area, struct State *state); -static void resize_manual(GtkWidget *area, struct State *state); -static void switch_color(GtkWidget *area, struct State *state); - -int main(int argc, char *argv[]) { - GtkWidget *window; - gtk_init(&argc, &argv); +static void switch_color(void); +static void clear_selection(void); +static void last_picture(void); +static void next_picture(int copy_box); +static void change_picture(Imlib_Image new_image, int new_selection, int copy_box); +static void get_scaled_size(int orig_w, int orig_h, int *scaled_w, int *scaled_h); +static void set_selection( + struct Selection *sel, int rect_x0, int rect_y0, int rect_x1, + int rect_y1, int orig_w, int orig_h, int scaled_w, int scaled_h); +static void drag_motion(XEvent event); +static void resize_window(int w, int h); +static void button_release(XEvent event); +static void button_press(XEvent event); +static void key_press(XEvent event); +static void queue_update(int x, int y, int w, int h); +static int parse_small_positive_int(const char *str, int *value); + +int +main(int argc, char **argv) { + XEvent event; + int running = 1; + Atom wm_delete_msg; + XSetWindowAttributes attrs; + XGCValues gcv; + char c; + + while ((c = getopt(argc, argv, "f:w:c:mrp:s:")) != -1) { + switch (c) { + case 'f': + CMD_FORMAT = optarg; + break; + case 'm': + RESIZE_REDRAW = 0; + break; + case 'r': + SELECTION_REDRAW = 0; + break; + case 'p': + SELECTION_COLOR1 = optarg; + break; + case 's': + SELECTION_COLOR2 = optarg; + break; + case 'c': + if (parse_small_positive_int(optarg, &COLLISION_PADDING)) { + fprintf(stderr, "Invalid collision padding.\n"); + exit(1); + } + break; + case 'w': + if (parse_small_positive_int(optarg, &LINE_WIDTH)) { + fprintf(stderr, "Invalid line width.\n"); + exit(1); + } + break; + default: + fprintf(stderr, "USAGE: croptool [-mr] [-f format] " + "[-w width] [-c padding] [-p color] [-s color] " + "[file ...]\n"); + exit(1); + break; + } + } - argc--; - argv++; + argc -= optind; + argv += optind; 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->resizing = FALSE; - state->lock_x = FALSE; - state->lock_y = FALSE; - state->window_w = 0; - state->window_h = 0; - state->cur_col = 1; + state.cur_image = NULL; + state.selections = malloc(argc * sizeof(struct Selection)); + if (!state.selections) exit(1); + state.num_files = argc; + state.filenames = argv; + state.cur_selection = -1; + state.moving = 0; + state.resizing = 0; + state.lock_x = 0; + state.lock_y = 0; + state.window_w = 500; + state.window_h = 500; + state.cur_col = 1; + for (int i = 0; i < argc; i++) { - state->selections[i] = NULL; + state.selections[i].valid = 0; } - window = gtk_window_new(GTK_WINDOW_TOPLEVEL); - gtk_window_set_title(GTK_WINDOW(window), "croptool"); - gtk_window_set_default_size(GTK_WINDOW(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 | GDK_POINTER_MOTION_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->col1, FALSE, TRUE); - gdk_color_parse(SELECTION_COLOR1, &state->col1); - gdk_colormap_alloc_color(cmap, &state->col2, FALSE, TRUE); - gdk_color_parse(SELECTION_COLOR2, &state->col2); - g_object_unref(cmap); - - gtk_main(); + state.dpy = XOpenDisplay(NULL); + state.screen = DefaultScreen(state.dpy); + state.vis = DefaultVisual(state.dpy, state.screen); + state.depth = DefaultDepth(state.dpy, state.screen); + state.cm = DefaultColormap(state.dpy, state.screen); + + memset(&attrs, 0, sizeof(attrs)); + attrs.background_pixmap = None; + attrs.colormap = state.cm; + state.win = XCreateWindow(state.dpy, DefaultRootWindow(state.dpy), 0, 0, + state.window_w, state.window_h, 0, state.depth, + InputOutput, state.vis, CWBackPixmap | CWColormap, &attrs); + + memset(&gcv, 0, sizeof(gcv)); + gcv.line_width = LINE_WIDTH; + state.gc = XCreateGC(state.dpy, state.win, GCLineWidth, &gcv); + + if (!XParseColor(state.dpy, state.cm, SELECTION_COLOR1, &state.col1)) { + fprintf(stderr, "Primary color invalid.\n"); + exit(1); + } + XAllocColor(state.dpy, state.cm, &state.col1); + if (!XParseColor(state.dpy, state.cm, SELECTION_COLOR2, &state.col2)) { + fprintf(stderr, "Secondary color invalid.\n"); + exit(1); + } + XAllocColor(state.dpy, state.cm, &state.col2); + + XSelectInput(state.dpy, state.win, StructureNotifyMask | KeyPressMask | ButtonPressMask | ButtonReleaseMask | PointerMotionMask | ExposureMask); + XMapWindow(state.dpy, state.win); + + wm_delete_msg = XInternAtom(state.dpy, "WM_DELETE_WINDOW", False); + XSetWMProtocols(state.dpy, state.win, &wm_delete_msg, 1); + + cursors.top = XCreateFontCursor(state.dpy, XC_top_side); + cursors.bottom = XCreateFontCursor(state.dpy, XC_bottom_side); + cursors.left = XCreateFontCursor(state.dpy, XC_left_side); + cursors.right = XCreateFontCursor(state.dpy, XC_right_side); + cursors.topleft = XCreateFontCursor(state.dpy, XC_top_left_corner); + cursors.topright = XCreateFontCursor(state.dpy, XC_top_right_corner); + cursors.bottomleft = XCreateFontCursor(state.dpy, XC_bottom_left_corner); + cursors.bottomright = XCreateFontCursor(state.dpy, XC_bottom_right_corner); + cursors.grab = XCreateFontCursor(state.dpy, XC_fleur); + + imlib_set_cache_size(2048 * 2048); + imlib_set_color_usage(128); + imlib_context_set_dither(1); + imlib_context_set_display(state.dpy); + imlib_context_set_visual(state.vis); + imlib_context_set_colormap(state.cm); + imlib_context_set_drawable(state.win); + state.updates = imlib_updates_init(); + + next_picture(0); + redraw(); + + while (running) { + do { + XNextEvent(state.dpy, &event); + switch (event.type) { + case Expose: + queue_update(event.xexpose.x, event.xexpose.y, + event.xexpose.width, event.xexpose.height); + break; + case ConfigureNotify: + if (RESIZE_REDRAW) + resize_window(event.xconfigure.width, event.xconfigure.height); + break; + case ButtonPress: + if (event.xbutton.button == Button1) + button_press(event); + break; + case ButtonRelease: + if (event.xbutton.button == Button1) + button_release(event); + break; + case MotionNotify: + drag_motion(event); + break; + case KeyPress: + key_press(event); + break; + case ClientMessage: + if (event.xclient.data.l[0] == wm_delete_msg) + running = 0; + default: + break; + } + } while (XPending(state.dpy)); + + redraw(); + } for (int i = 0; i < argc; i++) { - if (state->selections[i]) { - print_selection(state->selections[i], argv[i]); - free(state->selections[i]); + if (state.selections[i].valid) { + print_selection(&state.selections[i], argv[i]); } } - if (state->cur_pixbuf) - g_object_unref(G_OBJECT(state->cur_pixbuf)); - free(state->selections); - free(state); + if (state.cur_image) { + imlib_context_set_image(state.cur_image); + imlib_free_image(); + } + free(state.selections); + XDestroyWindow(state.dpy, state.win); + XCloseDisplay(state.dpy); return 0; } +/* TODO: Allow printing filename without ending */ +/* TODO: Escape filename properly */ +static void +print_cmd(const char *filename, int x, int y, int w, int h) { + short percent = 0; + char *c; + int length = 0; + int start_index = 0; + for (c = CMD_FORMAT; *c != '\0'; c++) { + if (percent) + start_index++; + if (*c == '%') { + if (length) { + printf("%.*s", length, CMD_FORMAT + start_index); + start_index += length; + length = 0; + } + if (percent) + printf("%%"); + percent++; + percent %= 2; + start_index++; + } else if (percent && *c == 'w') { + printf("%d", w); + percent = 0; + } else if (percent && *c == 'h') { + printf("%d", h); + percent = 0; + } else if (percent && *c == 'l') { + printf("%d", x); + percent = 0; + } else if (percent && *c == 't') { + printf("%d", y); + percent = 0; + } else if (percent && *c == 'r') { + printf("%d", x + w); + percent = 0; + } else if (percent && *c == 'b') { + printf("%d", y + h); + percent = 0; + } else if (percent && *c == 'f') { + printf("%s", filename); + percent = 0; + } else if (percent) { + fprintf(stderr, "Warning: Unknown substitution '%c' in format string.\n", *c); + printf("%%%c", *c); + percent = 0; + } else { + length++; + } + } + if (length) + printf("%.*s", length, CMD_FORMAT + start_index); + printf("\n"); +} + +/* Parses integer between 0 and 100 (non-inclusive). + Returns 1 on error, 0 otherwise. + The result is stored in *value. */ +static int +parse_small_positive_int(const char *str, int *value) { + char *end; + long l = strtol(str, &end, 10); + if (str == end || *end != '\0') { + return 1; + } else if (l <= 0 || l >= 100 || ((l == LONG_MIN || + l == LONG_MAX) && errno == ERANGE)) { + return 1; + } + *value = (int)l; + + return 0; +} + +static void +queue_update(int x, int y, int w, int h) { + if (state.cur_selection < 0 || !state.selections[state.cur_selection].valid) + return; + struct Selection *sel = &state.selections[state.cur_selection]; + if (x > sel->scaled_w || y > sel->scaled_h) + return; + state.updates = imlib_update_append_rect( + state.updates, x, y, + w + x > sel->scaled_w ? sel->scaled_w - x : w, + h + y > sel->scaled_h ? sel->scaled_h - y : h); +} + +static void +redraw(void) { + Imlib_Image buffer; + Imlib_Updates current_update; + if (!state.cur_image || state.cur_selection < 0) { + XSetForeground(state.dpy, state.gc, BlackPixel(state.dpy, state.screen)); + XFillRectangle(state.dpy, state.win, state.gc, 0, 0, state.window_w, state.window_h); + return; + } + struct Selection *sel = &state.selections[state.cur_selection]; + state.updates = imlib_updates_merge_for_rendering(state.updates, sel->scaled_w, sel->scaled_h); + for (current_update = state.updates; current_update; + current_update = imlib_updates_get_next(current_update)) { + int up_x, up_y, up_w, up_h; + imlib_updates_get_coordinates(current_update, &up_x, &up_y, &up_w, &up_h); + buffer = imlib_create_image(up_w, up_h); + imlib_context_set_blend(0); + imlib_context_set_image(buffer); + imlib_blend_image_onto_image( + state.cur_image, 0, 0, 0, + state.selections[state.cur_selection].orig_w, + state.selections[state.cur_selection].orig_h, + -up_x, -up_y, + state.selections[state.cur_selection].scaled_w, + state.selections[state.cur_selection].scaled_h); + imlib_render_image_on_drawable(up_x, up_y); + imlib_free_image(); + } + if (state.updates) + imlib_updates_free(state.updates); + state.updates = imlib_updates_init(); + + XSetForeground(state.dpy, state.gc, BlackPixel(state.dpy, state.screen)); + XFillRectangle(state.dpy, state.win, state.gc, 0, sel->scaled_h, sel->scaled_w, state.window_h - sel->scaled_h); + XFillRectangle(state.dpy, state.win, state.gc, sel->scaled_w, 0, state.window_w - sel->scaled_w, state.window_h); + + XColor col = state.cur_col == 1 ? state.col1 : state.col2; + XSetForeground(state.dpy, state.gc, col.pixel); + struct Rect rect = sel->rect; + sort_coordinates(&rect.x0, &rect.y0, &rect.x1, &rect.y1); + XDrawRectangle(state.dpy, state.win, state.gc, rect.x0, rect.y0, rect.x1 - rect.x0, rect.y1 - rect.y0); +} + static void swap(int *a, int *b) { - int tmp = *a; - *a = *b; - *b = tmp; + 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); + if (*x0 > *x1) + swap(x0, x1); + if(*y0 > *y1) + swap(y0, y1); } static void @@ -210,27 +490,6 @@ print_selection(struct Selection *sel, const char *filename) { 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) { - 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) && @@ -258,14 +517,13 @@ collide_rect(int x, int y, struct Rect rect) { 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]) - return FALSE; - struct Rect *rect = &state->selections[state->cur_selection]->rect; - gint x = event->x; - gint y = event->y; +static void +button_press(XEvent event) { + if (state.cur_selection < 0 || !state.selections[state.cur_selection].valid) + return; + struct Rect *rect = &state.selections[state.cur_selection].rect; + int x = event.xbutton.x; + int y = event.xbutton.y; int x0 = rect->x0, x1 = rect->x1; int y0 = rect->y0, y1 = rect->y1; if (collide_point(x, y, x0, y0)) { @@ -285,153 +543,146 @@ button_press(GtkWidget *area, GdkEventButton *event, gpointer data) { rect->x1 = x; rect->y1 = y; } else if (collide_line(x, y, x0, y0, x1, y0)) { - state->lock_y = TRUE; + state.lock_y = 1; 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; + state.lock_x = 1; 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; + state.lock_y = 1; rect->y1 = y; } else if (collide_line(x, y, x1, y1, x1, y0)) { - state->lock_x = TRUE; + state.lock_x = 1; rect->x1 = x; } else if (collide_rect(x, y, *rect)) { - state->moving = TRUE; - state->move_handle.x = x; - state->move_handle.y = y; + state.moving = 1; + state.move_handle.x = x; + state.move_handle.y = y; } else { rect->x0 = x; rect->y0 = y; rect->x1 = x; rect->y1 = y; } - state->resizing = TRUE; - return FALSE; + state.resizing = 1; } -static gboolean -button_release(GtkWidget *area, GdkEventButton *event, gpointer data) { - struct State *state = (struct State *)data; - state->moving = FALSE; - state->resizing = FALSE; - state->lock_x = FALSE; - state->lock_y = FALSE; - return FALSE; +static void +button_release(XEvent event) { + state.moving = 0; + state.resizing = 0; + state.lock_x = 0; + state.lock_y = 0; + if (!SELECTION_REDRAW) + queue_update(0, 0, state.window_w, state.window_h); } static void -redraw(GtkWidget *area, struct State *state) { - if (!state->cur_pixbuf) - return; - cairo_t *cr; - cr = gdk_cairo_create(area->window); - - gdk_cairo_set_source_pixbuf(cr, state->cur_pixbuf, 0, 0); - cairo_paint(cr); - - GdkColor col = state->cur_col == 1 ? state->col1 : state->col2; - if (state->selections[state->cur_selection]) { - struct Rect rect = state->selections[state->cur_selection]->rect; - gdk_cairo_set_source_color(cr, &col); - 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); - } +resize_window(int w, int h) { + int actual_w, actual_h; + struct Selection *sel; + state.window_w = w; + state.window_h = h; - 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; - if (state->cur_selection == -1 && state->window_w > 0 && state->window_h > 0) { - next_picture(area, state, FALSE); + if (state.cur_selection < 0 || !state.selections[state.cur_selection].valid) + return; + sel = &state.selections[state.cur_selection]; + get_scaled_size(sel->orig_w, sel->orig_h, &actual_w, &actual_h); + if (actual_w != sel->scaled_w) { + 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 = round(sel->rect.x0 * scale); + sel->rect.y0 = round(sel->rect.y0 * scale); + sel->rect.x1 = round(sel->rect.x1 * scale); + sel->rect.y1 = round(sel->rect.y1 * scale); + } + sel->scaled_w = actual_w; + sel->scaled_h = actual_h; + queue_update(0, 0, sel->scaled_w, sel->scaled_h); } - return FALSE; -} - -static gboolean -draw_expose(GtkWidget *area, GdkEvent *event, gpointer data) { - struct State *state = (struct State *)data; - if (state->cur_selection < 0) - 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]) - 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; +static void +drag_motion(XEvent event) { + if (state.cur_selection < 0 || !state.selections[state.cur_selection].valid) + return; + struct Rect *rect = &state.selections[state.cur_selection].rect; + int x = event.xbutton.x; + int y = event.xbutton.y; + int x0 = rect->x0, x1 = rect->x1; + int y0 = rect->y0, y1 = rect->y1; + sort_coordinates(&x0, &y0, &x1, &y1); + if (state.moving) { + 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->resizing == TRUE) { - if (state->lock_y != TRUE) + state.move_handle.x = x; + state.move_handle.y = y; + } else if (state.resizing) { + if (!state.lock_y) rect->x1 = x; - if (state->lock_x != TRUE) + if (!state.lock_x) rect->y1 = y; } else { - int x0 = rect->x0, x1 = rect->x1; - int y0 = rect->y0, y1 = rect->y1; - sort_coordinates(&x0, &y0, &x1, &y1); - GdkCursor *c = NULL; - GdkCursor *old = gdk_window_get_cursor(area->window); - if (old) - gdk_cursor_unref(old); + Cursor c = None; if (collide_point(x, y, x0, y0)) { - c = gdk_cursor_new(GDK_TOP_LEFT_CORNER); + c = cursors.topleft; } else if (collide_point(x, y, x1, y0)) { - c = gdk_cursor_new(GDK_TOP_RIGHT_CORNER); + c = cursors.topright; } else if (collide_point(x, y, x0, y1)) { - c = gdk_cursor_new(GDK_BOTTOM_LEFT_CORNER); + c = cursors.bottomleft; } else if (collide_point(x, y, x1, y1)) { - c = gdk_cursor_new(GDK_BOTTOM_RIGHT_CORNER); + c = cursors.bottomright; } else if (collide_line(x, y, x0, y0, x1, y0)) { - c = gdk_cursor_new(GDK_TOP_SIDE); + c = cursors.top; } else if (collide_line(x, y, x1, y1, x0, y1)) { - c = gdk_cursor_new(GDK_BOTTOM_SIDE); + c = cursors.bottom; } else if (collide_line(x, y, x1, y1, x1, y0)) { - c = gdk_cursor_new(GDK_RIGHT_SIDE); + c = cursors.right; } else if (collide_line(x, y, x0, y0, x0, y1)) { - c = gdk_cursor_new(GDK_LEFT_SIDE); + c = cursors.left; } else if (collide_rect(x, y, *rect)) { - c = gdk_cursor_new(GDK_FLEUR); + c = cursors.grab; } - gdk_window_set_cursor(area->window, c); - return FALSE; + XDefineCursor(state.dpy, state.win, c); + return; } - gtk_widget_queue_draw(area); - return FALSE; + if (SELECTION_REDRAW) { + queue_update( + x0 - LINE_WIDTH > 0 ? x0 - LINE_WIDTH : 0, + y0 - LINE_WIDTH > 0 ? y0 - LINE_WIDTH : 0, + x1 - x0 + LINE_WIDTH * 2, LINE_WIDTH * 2); + queue_update( + x0 - LINE_WIDTH > 0 ? x0 - LINE_WIDTH : 0, + y1 - LINE_WIDTH > 0 ? y1 - LINE_WIDTH : 0, + x1 - x0 + LINE_WIDTH * 2, LINE_WIDTH * 2); + queue_update( + x0 - LINE_WIDTH > 0 ? x0 - LINE_WIDTH : 0, + y0 - LINE_WIDTH > 0 ? y0 - LINE_WIDTH : 0, + LINE_WIDTH * 2, y1 - y0 + LINE_WIDTH * 2); + queue_update( + x1 - LINE_WIDTH > 0 ? x1 - LINE_WIDTH : 0, + y0 - LINE_WIDTH > 0 ? y0 - LINE_WIDTH : 0, + LINE_WIDTH * 2, y1 - y0 + LINE_WIDTH * 2); + } } -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) { +static void +set_selection( + struct Selection *sel, 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; @@ -440,35 +691,50 @@ create_selection( 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) { - g_object_unref(G_OBJECT(state->cur_pixbuf)); - state->cur_pixbuf = NULL; +get_scaled_size(int orig_w, int orig_h, int *scaled_w, int *scaled_h) { + double scale_w, scale_h; + scale_w = (double)state.window_w / (double)orig_w; + scale_h = (double)state.window_h / (double)orig_h; + if (orig_w <= state.window_w && orig_h <= state.window_h) { + *scaled_w = orig_w; + *scaled_h = orig_h; + } else if (scale_w * orig_h > state.window_h) { + *scaled_w = (int)(scale_h * orig_w); + *scaled_h = state.window_h; + } else { + *scaled_w = state.window_w; + *scaled_h = (int)(scale_w * orig_h); } - 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, +} + +static void +change_picture(Imlib_Image new_image, int new_selection, int copy_box) { + int orig_w, orig_h, actual_w, actual_h; + XSetStandardProperties(state.dpy, state.win, state.filenames[new_selection], NULL, None, NULL, 0, NULL); + if (state.cur_image) { + imlib_context_set_image(state.cur_image); + imlib_free_image(); + } + state.cur_image = new_image; + imlib_context_set_image(state.cur_image); + int old_selection = state.cur_selection; + state.cur_selection = new_selection; + + orig_w = imlib_image_get_width(); + orig_h = imlib_image_get_height(); + get_scaled_size(orig_w, orig_h, &actual_w, &actual_h); + + struct Selection *sel = &state.selections[state.cur_selection]; + if (copy_box && old_selection >= 0 && old_selection < state.num_files) { + struct Selection *old = &state.selections[old_selection]; + set_selection(sel, old->rect.x0, old->rect.y0, old->rect.x1, old->rect.y1, orig_w, orig_h, actual_w, actual_h); - } else if (!sel) { + } else if (!sel->valid) { /* 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); + set_selection(sel, -200, -200, -200, -200, orig_w, orig_h, actual_w, actual_h); } else if (sel->rect.x0 != -200 && actual_w != sel->scaled_w) { /* If there is a selection, we need to convert it to the new scale. * This only takes width into account because the aspect ratio @@ -481,101 +747,89 @@ change_picture( } sel->scaled_w = actual_w; sel->scaled_h = actual_h; - state->selections[state->cur_selection] = sel; - gtk_widget_queue_draw(area); + sel->valid = 1; + queue_update(0, 0, sel->scaled_w, sel->scaled_h); } static void -next_picture(GtkWidget *area, struct State *state, gboolean copy_box) { - if (state->cur_selection + 1 >= state->num_files) +next_picture(int 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; + Imlib_Image tmp_image = NULL; + int tmp_cur_selection = state.cur_selection; /* loop until we find a loadable file */ - while (!tmp_pixbuf && tmp_cur_selection + 1 < state->num_files) { + while (!tmp_image && 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); + tmp_image = imlib_load_image(state.filenames[tmp_cur_selection]); } - if (!tmp_pixbuf) + if (!tmp_image) return; - change_picture(area, tmp_pixbuf, tmp_cur_selection, orig_w, orig_h, state, copy_box); + + change_picture(tmp_image, tmp_cur_selection, copy_box); } static void -last_picture(GtkWidget *area, struct State *state) { - if (state->cur_selection <= 0) +last_picture(void) { + if (state.cur_selection <= 0) return; - GdkPixbuf *tmp_pixbuf = NULL; - int tmp_cur_selection = state->cur_selection; - int orig_w, orig_h; + Imlib_Image tmp_image = NULL; + int tmp_cur_selection = state.cur_selection; /* loop until we find a loadable file */ - while (!tmp_pixbuf && tmp_cur_selection > 0) { + while (!tmp_image && 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); + tmp_image = imlib_load_image(state.filenames[tmp_cur_selection]); } - if (!tmp_pixbuf) + if (!tmp_image) return; - change_picture(area, tmp_pixbuf, tmp_cur_selection, orig_w, orig_h, state, FALSE); + + change_picture(tmp_image, tmp_cur_selection, 0); } static void -clear_selection(GtkWidget *area, struct State *state) { - if (state->cur_selection < 0 || !state->selections[state->cur_selection]) +clear_selection(void) { + if (state.cur_selection < 0 || !state.selections[state.cur_selection].valid) return; - struct Selection *sel = state->selections[state->cur_selection]; + struct Selection *sel = &state.selections[state.cur_selection]; sel->rect.x0 = sel->rect.x1 = sel->rect.y0 = sel->rect.y1 = -200; - gtk_widget_queue_draw(area); + queue_update(0, 0, sel->scaled_w, sel->scaled_h); } static void -resize_manual(GtkWidget *area, struct State *state) { - if (state->cur_selection < 0 || !state->selections[state->cur_selection]) +switch_color(void) { + if (state.cur_selection < 0 || !state.selections[state.cur_selection].valid) return; - int orig_w, orig_h; - GdkPixbuf *tmp_pixbuf = load_pixbuf( - state->filenames[state->cur_selection], - state->window_w, state->window_h, &orig_w, &orig_h); - if (!tmp_pixbuf) - return; - change_picture(area, tmp_pixbuf, state->cur_selection, orig_w, orig_h, state, FALSE); + state.cur_col = state.cur_col == 1 ? 2 : 1; + queue_update(0, 0, state.window_w, state.window_h); } static void -switch_color(GtkWidget *area, struct State *state) { - if (state->cur_selection < 0 || !state->selections[state->cur_selection]) - return; - state->cur_col = state->cur_col == 1 ? 2 : 1; - gtk_widget_queue_draw(area); -} - -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); +key_press(XEvent event) { + XWindowAttributes attrs; + char buf[64]; + KeySym sym; + XLookupString(&event.xkey, buf, sizeof(buf), &sym, NULL); + switch (sym) { + case XK_Left: + last_picture(); + break; + case XK_Right: + next_picture(0); break; - case GDK_KEY_Right: - next_picture(area, state, FALSE); + case XK_Return: + next_picture(1); break; - case GDK_KEY_Return: - next_picture(area, state, TRUE); + case XK_Delete: + clear_selection(); break; - case GDK_KEY_Delete: - clear_selection(area, state); + case XK_Tab: + switch_color(); break; - case GDK_KEY_space: - resize_manual(area, state); + case XK_space: + XGetWindowAttributes(state.dpy, state.win, &attrs); + resize_window(attrs.width, attrs.height); break; - case GDK_KEY_Tab: - switch_color(area, state); + default: break; } - return FALSE; } diff --git a/Makefile b/old/Makefile diff --git a/old/README b/old/README @@ -0,0 +1,70 @@ +Note (2021-01-01): This is now the old gtk2 version. The new version +is based on xlib and imlib2, but this version should still keep +working. What follows is the original README. + +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. + +Note that the whole image is redrawn when changing the selection +because I'm too dumb to change that, so it may occasionally lag +a little bit. It barely lags on my nice laptop with a single-core +Intel Celeron 2.00 Ghz from 2008, though, so that shouldn't be a +huge problem. + +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. + +Several keys are recognized: +* 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 (my digitizing equipment, if it + can be called that, isn't exactly very professional). +* Left arrow just goes to the previous picture. +* Delete removes the selection for the current image (this is then + also not printed out at the end). +* Space bar resizes the image if the window was resized. +* Tab switches the color of the selection box between the two colors + defined at the top of `croptool.c` (SELECTION_COLOR1, SELECTION_COLOR2). + +Note that resizing the window currently does not resize the images. +It will only take effect if you move to another image or press +space bar. A side effect of this is that the image usually is +displayed at the wrong size when the window initially opens in a +tiling window manager because the window is first mapped at the +requested (500x500) size and then resized by the window manager. +Just press space bar if that happens (it hasn't bothered me too +much up til now, and I use dwm). There may be bugs lurking here +as well since the actual cropping box needs to be rescaled 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). If the box +was completely outside of the image, nothing is printed. If only part +of it was outside of the image, it is adjusted so that only the part +inside the image is printed. + +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_COLOR1 and SELECTION_COLOR2 are the two colors for the +selection box that can be switched with tab. +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/old/croptool.c