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:
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