commit f5defee322437ec0c63a9d2872b7ef10936e9140
parent 022ad164b6516bbb4c5109e45c68248c17ff7115
Author: lumidify <nobody@lumidify.org>
Date: Tue, 14 May 2024 16:55:16 +0200
Add selectool
Diffstat:
M | .gitignore | | | 1 | + |
M | CHANGELOG | | | 6 | ++++++ |
M | LICENSE | | | 2 | +- |
M | Makefile | | | 28 | +++++++++++++++++++++------- |
M | README | | | 12 | +++++++++++- |
M | TODO | | | 4 | ++++ |
A | common.c | | | 332 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
A | common.h | | | 83 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
M | croptool.1 | | | 14 | +++++++++----- |
M | croptool.c | | | 489 | ++++++++++++++++--------------------------------------------------------------- |
A | selectool.1 | | | 140 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
A | selectool.c | | | 423 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
12 files changed, 1131 insertions(+), 403 deletions(-)
diff --git a/.gitignore b/.gitignore
@@ -1,2 +1,3 @@
croptool
croptool_crop
+selectool
diff --git a/CHANGELOG b/CHANGELOG
@@ -1,3 +1,9 @@
+1.2.1 -> 1.3.0-dev
+* IMPORTANT: Change behavior of croptool so it only prints the
+ cropping commands when exited by pressing 'q'
+* Add selectool
+* Fix compilation and linking on some systems
+
1.2.0 -> 1.2.1
* Fix minor style issues
* Fix minor bug in parsing of cropping rectangle in croptool_crop
diff --git a/LICENSE b/LICENSE
@@ -1,4 +1,4 @@
-Copyright (c) 2021-2023 lumidify <nobody@lumidify.org>
+Copyright (c) 2021-2024 lumidify <nobody@lumidify.org>
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
diff --git a/Makefile b/Makefile
@@ -1,15 +1,18 @@
.POSIX:
NAME = croptool
-VERSION = 1.2.1
+VERSION = 1.3.0-dev
PREFIX = /usr/local
MANPREFIX = ${PREFIX}/man
-BIN = ${NAME} croptool_crop
+BIN = ${NAME} croptool_crop selectool
SRC = ${BIN:=.c}
MAN1 = ${BIN:=.1}
-MISCFILES = Makefile README CHANGELOG LICENSE TODO
+MISCFILES = Makefile README CHANGELOG LICENSE TODO common.c common.h
+
+DEBUG = 0
+SANITIZE = 0
# Configuration options:
@@ -20,14 +23,25 @@ DB_LDFLAGS = `pkg-config --libs xext`
#DB_CFLAGS = -DNODB
#DB_LDFLAGS =
+EXTRA_CFLAGS_DEBUG0 =
+EXTRA_CFLAGS_DEBUG1 = -g
+EXTRA_FLAGS_SANITIZE0 =
+EXTRA_FLAGS_SANITIZE1 = -fsanitize=address,undefined
+
# Note: Older systems might need `imlib2-config --cflags` and `imlib2-config --libs` instead of pkg-config.
-CROP_CFLAGS = ${CFLAGS} ${DB_CFLAGS} -Wall -Wextra -D_POSIX_C_SOURCE=200809L `pkg-config --cflags x11 imlib2`
-CROP_LDFLAGS = ${LDFLAGS} ${DB_LDFLAGS} `pkg-config --libs x11 imlib2` -lm
+CROP_CFLAGS = ${CFLAGS} ${DB_CFLAGS} ${EXTRA_FLAGS_SANITIZE${SANITIZE}} ${EXTRA_CFLAGS_DEBUG${DEBUG}} -Wall -Wextra -pedantic -D_POSIX_C_SOURCE=200809L `pkg-config --cflags x11 imlib2`
+CROP_LDFLAGS = ${LDFLAGS} ${DB_LDFLAGS} ${EXTRA_FLAGS_SANITIZE${SANITIZE}} `pkg-config --libs x11 imlib2` -lm
all: ${BIN}
-.c:
- ${CC} -o $@ $< ${CROP_CFLAGS} ${CROP_LDFLAGS}
+croptool: croptool.c common.c common.h
+ ${CC} -o $@ croptool.c common.c ${CROP_CFLAGS} ${CROP_LDFLAGS}
+
+selectool: selectool.c common.c common.h
+ ${CC} -o $@ selectool.c common.c ${CROP_CFLAGS} ${CROP_LDFLAGS}
+
+croptool_crop: croptool_crop.c
+ ${CC} -o $@ croptool_crop.c ${CROP_CFLAGS} ${CROP_LDFLAGS}
install: all
mkdir -p "${DESTDIR}${PREFIX}/bin"
diff --git a/README b/README
@@ -1,4 +1,5 @@
croptool - mass image cropping tool
+selectool - image selection tool
REQUIREMENTS: xlib, imlib2
OPTIONAL: xext (for double-buffering extension)
@@ -6,6 +7,15 @@ OPTIONAL: xext (for double-buffering extension)
croptool is a simple tool to select cropping rectangles
on images and print out a command to crop each image.
+selectool is a simple tool to select images and output
+a command for each selected image. It is mainly meant
+to help quickly delete images that have been recovered
+using programs like photorec or foremost.
+
See Makefile for compile-time options.
-See croptool.1 and croptool_crop.1 for usage information.
+See croptool.1, croptool_crop.1, and selectool.1 for usage information.
+
+Note: I know the names aren't very creative and might
+cause issues if this ever makes its way into any package
+repositories. Let me know if you have any better ideas.
diff --git a/TODO b/TODO
@@ -1,4 +1,8 @@
* Proper path handling (allow paths including "'", etc.)
+ - Option 1: Implement command parsing inside croptool/selectool
+ and call commands directly instead of printing them.
+ - Option 2: Add option 'escape chars' for characters that
+ must be escaped in the filename (kind of hacky).
* Draw pixmap on exposure events instead of doing the
expensive image resizing each time
* Maybe add zooming support
diff --git a/common.c b/common.c
@@ -0,0 +1,332 @@
+/*
+ * Copyright (c) 2021-2024 lumidify <nobody@lumidify.org>
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#include <stdio.h>
+#include <errno.h>
+#include <string.h>
+#include <stdlib.h>
+#include <limits.h>
+
+#include <X11/X.h>
+#include <X11/Xlib.h>
+#include <X11/Xutil.h>
+#ifndef NODB
+#include <X11/extensions/Xdbe.h>
+#endif
+
+#include <Imlib2.h>
+
+#include "common.h"
+
+void
+setup_x(GraphicsContext *ctx, int window_w, int window_h, int line_width, int cache_size) {
+ XSetWindowAttributes attrs;
+ XGCValues gcv;
+
+ ctx->dpy = XOpenDisplay(NULL);
+ ctx->screen = DefaultScreen(ctx->dpy);
+ ctx->vis = DefaultVisual(ctx->dpy, ctx->screen);
+ ctx->depth = DefaultDepth(ctx->dpy, ctx->screen);
+ ctx->cm = DefaultColormap(ctx->dpy, ctx->screen);
+ ctx->dirty = 1;
+
+ #ifndef NODB
+ ctx->db_enabled = 0;
+ /* based on http://wili.cc/blog/xdbe.html */
+ int major, minor;
+ if (XdbeQueryExtension(ctx->dpy, &major, &minor)) {
+ int num_screens = 1;
+ Drawable screens[] = { DefaultRootWindow(ctx->dpy) };
+ XdbeScreenVisualInfo *info = XdbeGetVisualInfo(
+ ctx->dpy, screens, &num_screens
+ );
+ if (!info || num_screens < 1 || info->count < 1) {
+ fprintf(stderr,
+ "Warning: No visuals support Xdbe, "
+ "double buffering disabled.\n"
+ );
+ } else {
+ XVisualInfo xvisinfo_templ;
+ xvisinfo_templ.visualid = info->visinfo[0].visual;
+ xvisinfo_templ.screen = 0;
+ xvisinfo_templ.depth = info->visinfo[0].depth;
+ int matches;
+ XVisualInfo *xvisinfo_match = XGetVisualInfo(
+ ctx->dpy,
+ VisualIDMask | VisualScreenMask | VisualDepthMask,
+ &xvisinfo_templ, &matches
+ );
+ if (!xvisinfo_match || matches < 1) {
+ fprintf(stderr,
+ "Warning: Couldn't match a Visual with "
+ "double buffering, double buffering disabled.\n"
+ );
+ } else {
+ ctx->vis = xvisinfo_match->visual;
+ ctx->depth = xvisinfo_match->depth;
+ ctx->db_enabled = 1;
+ }
+ XFree(xvisinfo_match);
+ }
+ XdbeFreeVisualInfo(info);
+ } else {
+ fprintf(stderr, "Warning: No Xdbe support, double buffering disabled.\n");
+ }
+ #endif
+
+ memset(&attrs, 0, sizeof(attrs));
+ attrs.background_pixel = BlackPixel(ctx->dpy, ctx->screen);
+ attrs.colormap = ctx->cm;
+ /* this causes the window contents to be kept
+ * when it is resized, leading to less flicker */
+ attrs.bit_gravity = NorthWestGravity;
+ ctx->win = XCreateWindow(ctx->dpy, DefaultRootWindow(ctx->dpy), 0, 0,
+ window_w, window_h, 0, ctx->depth,
+ InputOutput, ctx->vis, CWBackPixel | CWColormap | CWBitGravity, &attrs);
+
+ #ifndef NODB
+ if (ctx->db_enabled) {
+ ctx->back_buf = XdbeAllocateBackBufferName(
+ ctx->dpy, ctx->win, XdbeCopied
+ );
+ ctx->drawable = ctx->back_buf;
+ } else {
+ ctx->drawable = ctx->win;
+ }
+ #else
+ ctx->drawable = ctx->win;
+ #endif
+
+ memset(&gcv, 0, sizeof(gcv));
+ gcv.line_width = line_width;
+ ctx->gc = XCreateGC(ctx->dpy, ctx->win, GCLineWidth, &gcv);
+
+ XSelectInput(
+ ctx->dpy, ctx->win,
+ StructureNotifyMask | KeyPressMask | ButtonPressMask |
+ ButtonReleaseMask | PointerMotionMask | ExposureMask
+ );
+
+ ctx->wm_delete_msg = XInternAtom(ctx->dpy, "WM_DELETE_WINDOW", False);
+ XSetWMProtocols(ctx->dpy, ctx->win, &ctx->wm_delete_msg, 1);
+
+ /* note: since cache_size is <= 1024, this definitely fits in long */
+ long cs = (long)cache_size * 1024 * 1024;
+ if (cs > INT_MAX) {
+ fprintf(stderr, "Cache size would cause integer overflow.\n");
+ exit(1);
+ }
+ imlib_set_cache_size((int)cs);
+ imlib_set_color_usage(128);
+ imlib_context_set_dither(1);
+ imlib_context_set_display(ctx->dpy);
+ imlib_context_set_visual(ctx->vis);
+ imlib_context_set_colormap(ctx->cm);
+ imlib_context_set_drawable(ctx->drawable);
+ ctx->updates = imlib_updates_init();
+ ctx->cur_image = NULL;
+}
+
+void
+cleanup_x(GraphicsContext *ctx) {
+ if (ctx->cur_image) {
+ imlib_context_set_image(ctx->cur_image);
+ imlib_free_image();
+ }
+ XDestroyWindow(ctx->dpy, ctx->win);
+ XCloseDisplay(ctx->dpy);
+}
+
+int
+parse_int(const char *str, int min, int max, int *value) {
+ char *end;
+ long l = strtol(str, &end, 10);
+ if (min > max)
+ return 1;
+ if (str == end || *end != '\0') {
+ return 1;
+ } else if (l < min || l > max || ((l == LONG_MIN ||
+ l == LONG_MAX) && errno == ERANGE)) {
+ return 1;
+ }
+ *value = (int)l;
+
+ return 0;
+}
+
+void
+queue_area_update(GraphicsContext *ctx, ImageSize *sz, int x, int y, int w, int h) {
+ if (x > sz->scaled_w || y > sz->scaled_h)
+ return;
+ ctx->updates = imlib_update_append_rect(
+ ctx->updates, x, y,
+ w + x > sz->scaled_w ? sz->scaled_w - x : w,
+ h + y > sz->scaled_h ? sz->scaled_h - y : h
+ );
+ ctx->dirty = 1;
+}
+
+void
+clear_screen(GraphicsContext *ctx) {
+
+ /* clear the window completely */
+ XSetForeground(ctx->dpy, ctx->gc, BlackPixel(ctx->dpy, ctx->screen));
+ XFillRectangle(
+ ctx->dpy, ctx->drawable, ctx->gc,
+ 0, 0, ctx->window_w, ctx->window_h
+ );
+}
+
+void
+draw_image_updates(GraphicsContext *ctx, ImageSize *sz) {
+ Imlib_Image buffer;
+ Imlib_Updates current_update;
+
+ /* draw the parts of the image that need to be redrawn */
+ ctx->updates = imlib_updates_merge_for_rendering(
+ ctx->updates, sz->scaled_w, sz->scaled_h
+ );
+ /* FIXME: check since when imlib_render_image_updates_on_drawable supported, also maybe just render_on_drawable part scaled */
+ for (current_update = ctx->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(
+ ctx->cur_image, 0, 0, 0,
+ sz->orig_w, sz->orig_h,
+ -up_x, -up_y,
+ sz->scaled_w, sz->scaled_h);
+ imlib_render_image_on_drawable(up_x, up_y);
+ imlib_free_image();
+ }
+ if (ctx->updates)
+ imlib_updates_free(ctx->updates);
+ ctx->updates = imlib_updates_init();
+}
+
+void
+wipe_around_image(GraphicsContext *ctx, ImageSize *sz) {
+
+ /* wipe the black area around the image */
+ XSetForeground(ctx->dpy, ctx->gc, BlackPixel(ctx->dpy, ctx->screen));
+ XFillRectangle(
+ ctx->dpy, ctx->drawable, ctx->gc,
+ 0, sz->scaled_h, sz->scaled_w, ctx->window_h - sz->scaled_h
+ );
+ XFillRectangle(
+ ctx->dpy, ctx->drawable, ctx->gc,
+ sz->scaled_w, 0, ctx->window_w - sz->scaled_w, ctx->window_h
+ );
+}
+
+void
+swap_buffers(GraphicsContext *ctx) {
+ #ifndef NODB
+ if (ctx->db_enabled) {
+ XdbeSwapInfo swap_info;
+ swap_info.swap_window = ctx->win;
+ swap_info.swap_action = XdbeCopied;
+
+ if (!XdbeSwapBuffers(ctx->dpy, &swap_info, 1))
+ fprintf(stderr, "Warning: Unable to swap buffers.\n");
+ }
+ #endif
+ ctx->dirty = 0;
+}
+
+/* get the scaled size of an image based on the current window size */
+void
+get_scaled_size(GraphicsContext *ctx, int orig_w, int orig_h, int *scaled_w, int *scaled_h) {
+ double scale_w, scale_h;
+ scale_w = (double)ctx->window_w / (double)orig_w;
+ scale_h = (double)ctx->window_h / (double)orig_h;
+ if (orig_w <= ctx->window_w && orig_h <= ctx->window_h) {
+ *scaled_w = orig_w;
+ *scaled_h = orig_h;
+ } else if (scale_w * orig_h > ctx->window_h) {
+ *scaled_w = (int)(scale_h * orig_w);
+ *scaled_h = ctx->window_h;
+ } else {
+ *scaled_w = ctx->window_w;
+ *scaled_h = (int)(scale_w * orig_h);
+ }
+}
+
+void
+next_picture(int cur_selection, char **filenames, int num_files, int copy_box) {
+ if (cur_selection + 1 >= num_files)
+ return;
+ Imlib_Image tmp_image = NULL;
+ int tmp_cur_selection = cur_selection;
+ /* loop until we find a loadable file */
+ while (!tmp_image && tmp_cur_selection + 1 < num_files) {
+ tmp_cur_selection++;
+ if (!filenames[tmp_cur_selection])
+ continue;
+ tmp_image = imlib_load_image_immediately(
+ filenames[tmp_cur_selection]
+ );
+ if (!tmp_image) {
+ fprintf(
+ stderr, "Warning: Unable to load image '%s'.\n",
+ filenames[tmp_cur_selection]
+ );
+ filenames[tmp_cur_selection] = NULL;
+ }
+ }
+ /* immediately exit program if no loadable image is found on startup */
+ if (cur_selection < 0 && !tmp_image) {
+ fprintf(stderr, "No loadable images found.\n");
+ cleanup();
+ exit(1);
+ }
+ if (!tmp_image)
+ return;
+
+ change_picture(tmp_image, tmp_cur_selection, copy_box);
+}
+
+void
+last_picture(int cur_selection, char **filenames, int copy_box) {
+ if (cur_selection <= 0)
+ return;
+ Imlib_Image tmp_image = NULL;
+ int tmp_cur_selection = cur_selection;
+ /* loop until we find a loadable file */
+ while (!tmp_image && tmp_cur_selection > 0) {
+ tmp_cur_selection--;
+ if (!filenames[tmp_cur_selection])
+ continue;
+ tmp_image = imlib_load_image_immediately(
+ filenames[tmp_cur_selection]
+ );
+ if (!tmp_image) {
+ fprintf(
+ stderr, "Warning: Unable to load image '%s'.\n",
+ filenames[tmp_cur_selection]
+ );
+ filenames[tmp_cur_selection] = NULL;
+ }
+ }
+
+ if (!tmp_image)
+ return;
+
+ change_picture(tmp_image, tmp_cur_selection, copy_box);
+}
diff --git a/common.h b/common.h
@@ -0,0 +1,83 @@
+/*
+ * Copyright (c) 2021-2024 lumidify <nobody@lumidify.org>
+ *
+ * Permission to use, copy, modify, and/or 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.
+ */
+
+#ifndef CROPTOOL_COMMON
+#define CROPTOOL_COMMON
+
+#include <X11/X.h>
+#include <X11/Xlib.h>
+#ifndef NODB
+#include <X11/extensions/Xdbe.h>
+#endif
+#include <Imlib2.h>
+
+typedef struct {
+ int orig_w;
+ int orig_h;
+ int scaled_w;
+ int scaled_h;
+} ImageSize;
+
+typedef struct {
+ Display *dpy;
+ GC gc;
+ Window win;
+ Visual *vis;
+ Drawable drawable;
+ #ifndef NODB
+ XdbeBackBuffer back_buf;
+ int db_enabled;
+ #endif
+ Colormap cm;
+ int screen;
+ int depth;
+
+ int window_w;
+ int window_h;
+ char dirty;
+ Atom wm_delete_msg;
+ Imlib_Image cur_image;
+ Imlib_Updates updates;
+} GraphicsContext;
+
+void setup_x(GraphicsContext *ctx, int window_w, int window_h, int line_height, int cache_size);
+void cleanup_x(GraphicsContext *ctx);
+/* Parse integer between min and max (inclusive).
+ Returns 1 on error, 0 otherwise.
+ The result is stored in *value.
+ Based on OpenBSD's strtonum. */
+int parse_int(const char *str, int min, int max, int *value);
+/* queue a part of the image for redrawing */
+void queue_area_update(GraphicsContext *ctx, ImageSize *sz, int x, int y, int w, int h);
+void clear_screen(GraphicsContext *ctx);
+void draw_image_updates(GraphicsContext *ctx, ImageSize *sz);
+void wipe_around_image(GraphicsContext *ctx, ImageSize *sz);
+void swap_buffers(GraphicsContext *ctx);
+void get_scaled_size(GraphicsContext *ctx, int orig_w, int orig_h, int *scaled_w, int *scaled_h);
+/* show the next image in the argument list - unloadable files are skipped
+ * copy_box determines whether the current selection is copied
+ * (only relevant in croptool, not in selectool) */
+void next_picture(int cur_selection, char **filenames, int num_files, int copy_box);
+/* show the previous image in the argument list - unloadable files are skipped
+ * copy_box determines whether the current selection is copied
+ * (only relevant in croptool, not in selectool) */
+void last_picture(int cur_selection, char **filenames, int copy_box);
+
+/* these are actually defined in croptool.c and selectool.c */
+void cleanup(void);
+void change_picture(Imlib_Image new_image, int new_selection, int copy_box);
+
+#endif /* CROPTOOL_COMMON */
diff --git a/croptool.1 b/croptool.1
@@ -1,4 +1,4 @@
-.Dd August 18, 2023
+.Dd May 14, 2024
.Dt CROPTOOL 1
.Os
.Sh NAME
@@ -113,15 +113,18 @@ though the pixels covered in the original image are different.
Go to the previous image, copying the current cropping rectangle.
The same caveat as above applies.
.It TAB
-Switch the color of the cropping rectangle between the primary and secondary colors.
+Switch the color of the cropping rectangle between the primary and
+secondary colors.
.It DELETE
-Delete the cropping rectangle of the current image.
+Remove the cropping rectangle of the current image.
.It SPACE
Redraw the window.
This is useful when automatic redrawing is disabled with
.Fl m .
.It q
-Exit the program.
+Exit the program, printing the cropping command for any images with a
+cropping rectangle set.
+If the window is closed through some other means, no commands are printed.
.El
.Sh MOUSE ACTIONS
.Bl -tag -width Ds
@@ -168,7 +171,8 @@ filenames containing quotes).
.Sh SEE ALSO
.Xr convert 1 ,
.Xr croptool_crop 1 ,
-.Xr mogrify 1
+.Xr mogrify 1 ,
+.Xr selectool 1
.Sh AUTHORS
.An lumidify Aq Mt nobody@lumidify.org
.Sh BUGS
diff --git a/croptool.c b/croptool.c
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2021-2023 lumidify <nobody@lumidify.org>
+ * Copyright (c) 2021-2024 lumidify <nobody@lumidify.org>
*
* Permission to use, copy, modify, and/or distribute this software for any
* purpose with or without fee is hereby granted, provided that the above
@@ -16,21 +16,19 @@
#include <math.h>
#include <stdio.h>
-#include <errno.h>
-#include <string.h>
#include <stdlib.h>
-#include <limits.h>
#include <unistd.h>
+
+#include <X11/X.h>
#include <X11/Xlib.h>
#include <X11/Xutil.h>
#include <X11/keysym.h>
-#include <X11/XF86keysym.h>
#include <X11/cursorfont.h>
-#ifndef NODB
-#include <X11/extensions/Xdbe.h>
-#endif
+
#include <Imlib2.h>
+#include "common.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) */
@@ -77,47 +75,28 @@ struct Point {
struct Selection {
struct Rect rect;
- int orig_w;
- int orig_h;
- int scaled_w;
- int scaled_h;
+ ImageSize sz;
char valid;
};
static struct {
- Display *dpy;
- GC gc;
- Window win;
- Visual *vis;
- Drawable drawable;
- #ifndef NODB
- XdbeBackBuffer back_buf;
- int db_enabled;
- #endif
- Colormap cm;
- int screen;
- int depth;
+ GraphicsContext ctx;
struct Selection *selections;
char **filenames;
int cur_selection;
int num_files;
- int window_w;
- int window_h;
int cursor_x;
int cursor_y;
struct Point move_handle;
+ XColor col1;
+ XColor col2;
+ int cur_col;
char moving;
char resizing;
char lock_x;
char lock_y;
- char dirty;
- XColor col1;
- XColor col2;
- int cur_col;
- Atom wm_delete_msg;
- Imlib_Image cur_image;
- Imlib_Updates updates;
+ char print_on_exit;
} state;
static struct {
@@ -135,7 +114,6 @@ static struct {
static void usage(void);
static void mainloop(void);
static void setup(int argc, char *argv[]);
-static void cleanup(void);
static void sort_coordinates(int *x0, int *y0, int *x1, int *y1);
static void swap(int *a, int *b);
static void redraw(void);
@@ -146,14 +124,11 @@ 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 switch_color(void);
static void clear_selection(void);
-static void next_picture(int copy_box);
-static void last_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 queue_update(int x, int y, int w, int h);
static void queue_rectangle_redraw(int x0, int y0, int x1, int y1);
static void set_cursor(struct Rect rect);
static void drag_motion(XEvent event);
@@ -161,8 +136,6 @@ static void resize_window(int w, int h);
static void button_release(void);
static void button_press(XEvent event);
static int key_press(XEvent event);
-static void queue_update(int x, int y, int w, int h);
-static int parse_int(const char *str, int min, int max, int *value);
static void
usage(void) {
@@ -229,9 +202,11 @@ main(int argc, char *argv[]) {
mainloop();
- for (int i = 0; i < argc; i++) {
- if (state.selections[i].valid) {
- print_selection(&state.selections[i], state.filenames[i]);
+ if (state.print_on_exit) {
+ for (int i = 0; i < argc; i++) {
+ if (state.selections[i].valid) {
+ print_selection(&state.selections[i], state.filenames[i]);
+ }
}
}
@@ -247,7 +222,7 @@ mainloop(void) {
while (running) {
do {
- XNextEvent(state.dpy, &event);
+ XNextEvent(state.ctx.dpy, &event);
switch (event.type) {
case Expose:
if (RESIZE_REDRAW)
@@ -276,12 +251,12 @@ mainloop(void) {
running = key_press(event);
break;
case ClientMessage:
- if ((Atom)event.xclient.data.l[0] == state.wm_delete_msg)
+ if ((Atom)event.xclient.data.l[0] == state.ctx.wm_delete_msg)
running = 0;
default:
break;
}
- } while (XPending(state.dpy));
+ } while (XPending(state.ctx.dpy));
redraw();
}
@@ -289,10 +264,6 @@ mainloop(void) {
static void
setup(int argc, char *argv[]) {
- XSetWindowAttributes attrs;
- XGCValues gcv;
-
- state.cur_image = NULL;
state.selections = malloc(argc * sizeof(struct Selection));
if (!state.selections) {
fprintf(stderr, "Unable to allocate memory.\n");
@@ -305,155 +276,50 @@ setup(int argc, char *argv[]) {
state.resizing = 0;
state.lock_x = 0;
state.lock_y = 0;
- state.window_w = 500;
- state.window_h = 500;
state.cursor_x = 0;
state.cursor_y = 0;
state.cur_col = 1;
+ state.print_on_exit = 0;
for (int i = 0; i < argc; i++) {
state.selections[i].valid = 0;
}
- 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);
-
- #ifndef NODB
- state.db_enabled = 0;
- /* based on http://wili.cc/blog/xdbe.html */
- int major, minor;
- if (XdbeQueryExtension(state.dpy, &major, &minor)) {
- int num_screens = 1;
- Drawable screens[] = { DefaultRootWindow(state.dpy) };
- XdbeScreenVisualInfo *info = XdbeGetVisualInfo(
- state.dpy, screens, &num_screens
- );
- if (!info || num_screens < 1 || info->count < 1) {
- fprintf(stderr,
- "Warning: No visuals support Xdbe, "
- "double buffering disabled.\n"
- );
- } else {
- XVisualInfo xvisinfo_templ;
- xvisinfo_templ.visualid = info->visinfo[0].visual;
- xvisinfo_templ.screen = 0;
- xvisinfo_templ.depth = info->visinfo[0].depth;
- int matches;
- XVisualInfo *xvisinfo_match = XGetVisualInfo(
- state.dpy,
- VisualIDMask | VisualScreenMask | VisualDepthMask,
- &xvisinfo_templ, &matches
- );
- if (!xvisinfo_match || matches < 1) {
- fprintf(stderr,
- "Warning: Couldn't match a Visual with "
- "double buffering, double buffering disabled.\n"
- );
- } else {
- state.vis = xvisinfo_match->visual;
- state.depth = xvisinfo_match->depth;
- state.db_enabled = 1;
- }
- XFree(xvisinfo_match);
- }
- XdbeFreeVisualInfo(info);
- } else {
- fprintf(stderr, "Warning: No Xdbe support, double buffering disabled.\n");
- }
- #endif
-
- memset(&attrs, 0, sizeof(attrs));
- attrs.background_pixel = BlackPixel(state.dpy, state.screen);
- attrs.colormap = state.cm;
- /* this causes the window contents to be kept
- * when it is resized, leading to less flicker */
- attrs.bit_gravity = NorthWestGravity;
- state.win = XCreateWindow(state.dpy, DefaultRootWindow(state.dpy), 0, 0,
- state.window_w, state.window_h, 0, state.depth,
- InputOutput, state.vis, CWBackPixel | CWColormap | CWBitGravity, &attrs);
+ setup_x(&state.ctx, 500, 500, LINE_WIDTH, CACHE_SIZE);
- #ifndef NODB
- if (state.db_enabled) {
- state.back_buf = XdbeAllocateBackBufferName(
- state.dpy, state.win, XdbeCopied
- );
- state.drawable = state.back_buf;
- } else {
- state.drawable = state.win;
- }
- #else
- state.drawable = state.win;
- #endif
-
- 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)) {
+ if (!XParseColor(state.ctx.dpy, state.ctx.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)) {
+ XAllocColor(state.ctx.dpy, state.ctx.cm, &state.col1);
+ if (!XParseColor(state.ctx.dpy, state.ctx.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
- );
-
- state.wm_delete_msg = XInternAtom(state.dpy, "WM_DELETE_WINDOW", False);
- XSetWMProtocols(state.dpy, state.win, &state.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);
-
- /* note: since CACHE_SIZE is <= 1024, this definitely fits in long */
- long cs = (long)CACHE_SIZE * 1024 * 1024;
- if (cs > INT_MAX) {
- fprintf(stderr, "Cache size would cause integer overflow.\n");
- exit(1);
- }
- imlib_set_cache_size((int)cs);
- 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.drawable);
- state.updates = imlib_updates_init();
-
- next_picture(0);
+ XAllocColor(state.ctx.dpy, state.ctx.cm, &state.col2);
+
+ cursors.top = XCreateFontCursor(state.ctx.dpy, XC_top_side);
+ cursors.bottom = XCreateFontCursor(state.ctx.dpy, XC_bottom_side);
+ cursors.left = XCreateFontCursor(state.ctx.dpy, XC_left_side);
+ cursors.right = XCreateFontCursor(state.ctx.dpy, XC_right_side);
+ cursors.topleft = XCreateFontCursor(state.ctx.dpy, XC_top_left_corner);
+ cursors.topright = XCreateFontCursor(state.ctx.dpy, XC_top_right_corner);
+ cursors.bottomleft = XCreateFontCursor(state.ctx.dpy, XC_bottom_left_corner);
+ cursors.bottomright = XCreateFontCursor(state.ctx.dpy, XC_bottom_right_corner);
+ cursors.grab = XCreateFontCursor(state.ctx.dpy, XC_fleur);
+
+ next_picture(state.cur_selection, state.filenames, state.num_files, 0);
/* Only map window here so the program exits immediately if
there are no loadable images, without first opening the
window and closing it again immediately */
- XMapWindow(state.dpy, state.win);
+ XMapWindow(state.ctx.dpy, state.ctx.win);
redraw();
}
-static void
+void
cleanup(void) {
- 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);
+ cleanup_x(&state.ctx);
}
/* TODO: Escape filename properly
@@ -465,6 +331,7 @@ print_cmd(const char *filename, int x, int y, int w, int h, int dry_run) {
const char *c;
int length = 0;
int start_index = 0;
+ /* FIXME: just use putc instead of this complex printf dance */
for (c = CMD_FORMAT; *c != '\0'; c++) {
if (percent)
start_index++;
@@ -529,118 +396,34 @@ print_cmd(const char *filename, int x, int y, int w, int h, int dry_run) {
}
}
-/* Parse integer between min and max (inclusive).
- Returns 1 on error, 0 otherwise.
- The result is stored in *value.
- Based on OpenBSD's strtonum. */
-static int
-parse_int(const char *str, int min, int max, int *value) {
- char *end;
- long l = strtol(str, &end, 10);
- if (min > max)
- return 1;
- if (str == end || *end != '\0') {
- return 1;
- } else if (l < min || l > max || ((l == LONG_MIN ||
- l == LONG_MAX) && errno == ERANGE)) {
- return 1;
- }
- *value = (int)l;
-
- return 0;
-}
-
-/* queue a part of the image for redrawing */
-static void
-queue_update(int x, int y, int w, int h) {
- if (state.cur_selection < 0 || !state.selections[state.cur_selection].valid)
- return;
- state.dirty = 1;
- 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.dirty)
+ if (!state.ctx.dirty)
+ return;
+ if (!state.ctx.cur_image || state.cur_selection < 0) {
+ clear_screen(&state.ctx);
+ swap_buffers(&state.ctx);
return;
- if (!state.cur_image || state.cur_selection < 0) {
- /* clear the window completely */
- XSetForeground(state.dpy, state.gc, BlackPixel(state.dpy, state.screen));
- XFillRectangle(
- state.dpy, state.drawable, state.gc,
- 0, 0, state.window_w, state.window_h
- );
- goto swap_buffers;
}
/* draw the parts of the image that need to be redrawn */
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,
- sel->orig_w, sel->orig_h,
- -up_x, -up_y,
- sel->scaled_w, sel->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();
+ draw_image_updates(&state.ctx, &sel->sz);
- /* wipe the black area around the image */
- XSetForeground(state.dpy, state.gc, BlackPixel(state.dpy, state.screen));
- XFillRectangle(
- state.dpy, state.drawable, state.gc,
- 0, sel->scaled_h, sel->scaled_w, state.window_h - sel->scaled_h
- );
- XFillRectangle(
- state.dpy, state.drawable, state.gc,
- sel->scaled_w, 0, state.window_w - sel->scaled_w, state.window_h
- );
+ wipe_around_image(&state.ctx, &sel->sz);
/* draw the rectangle */
struct Rect rect = sel->rect;
if (rect.x0 != -200) {
XColor col = state.cur_col == 1 ? state.col1 : state.col2;
- XSetForeground(state.dpy, state.gc, col.pixel);
+ XSetForeground(state.ctx.dpy, state.ctx.gc, col.pixel);
sort_coordinates(&rect.x0, &rect.y0, &rect.x1, &rect.y1);
XDrawRectangle(
- state.dpy, state.drawable, state.gc,
+ state.ctx.dpy, state.ctx.drawable, state.ctx.gc,
rect.x0, rect.y0, rect.x1 - rect.x0, rect.y1 - rect.y0
);
}
-
-swap_buffers:
- #ifndef NODB
- if (state.db_enabled) {
- XdbeSwapInfo swap_info;
- swap_info.swap_window = state.win;
- swap_info.swap_action = XdbeCopied;
-
- if (!XdbeSwapBuffers(state.dpy, &swap_info, 1))
- fprintf(stderr, "Warning: Unable to swap buffers.\n");
- }
- #endif
- state.dirty = 0;
+ swap_buffers(&state.ctx);
}
static void
@@ -665,7 +448,7 @@ print_selection(struct Selection *sel, const char *filename) {
/* The box was never actually used */
if (sel->rect.x0 == -200)
return;
- double scale = (double)sel->orig_w / sel->scaled_w;
+ double scale = (double)sel->sz.orig_w / sel->sz.scaled_w;
int x0 = sel->rect.x0, y0 = sel->rect.y0;
int x1 = sel->rect.x1, y1 = sel->rect.y1;
sort_coordinates(&x0, &y0, &x1, &y1);
@@ -674,13 +457,13 @@ print_selection(struct Selection *sel, const char *filename) {
x1 = round(x1 * scale);
y1 = round(y1 * scale);
/* The box is completely outside of the picture. */
- if (x0 >= sel->orig_w || y0 >= sel->orig_h)
+ if (x0 >= sel->sz.orig_w || y0 >= sel->sz.orig_h)
return;
/* Cut the bounding box if it goes past the end of the picture. */
x0 = x0 < 0 ? 0 : x0;
y0 = y0 < 0 ? 0 : y0;
- x1 = x1 > sel->orig_w ? sel->orig_w : x1;
- y1 = y1 > sel->orig_h ? sel->orig_h : y1;
+ x1 = x1 > sel->sz.orig_w ? sel->sz.orig_w : x1;
+ y1 = y1 > sel->sz.orig_h ? sel->sz.orig_h : y1;
print_cmd(filename, x0, y0, x1 - x0, y1 - y0, 0);
}
@@ -768,6 +551,14 @@ button_press(XEvent event) {
}
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];
+ queue_area_update(&state.ctx, &sel->sz, x, y, w, h);
+}
+
+static void
button_release(void) {
state.moving = 0;
state.resizing = 0;
@@ -776,35 +567,35 @@ button_release(void) {
/* redraw everything if automatic redrawing of the rectangle
is disabled (so it's redrawn when the mouse is released) */
if (!SELECTION_REDRAW)
- queue_update(0, 0, state.window_w, state.window_h);
+ queue_update(0, 0, state.ctx.window_w, state.ctx.window_h);
}
static void
resize_window(int w, int h) {
int actual_w, actual_h;
struct Selection *sel;
- state.window_w = w;
- state.window_h = h;
+ state.ctx.window_w = w;
+ state.ctx.window_h = h;
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) {
+ get_scaled_size(&state.ctx, sel->sz.orig_w, sel->sz.orig_h, &actual_w, &actual_h);
+ if (actual_w != sel->sz.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;
+ double scale = (double)actual_w / sel->sz.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);
+ sel->sz.scaled_w = actual_w;
+ sel->sz.scaled_h = actual_h;
+ queue_update(0, 0, sel->sz.scaled_w, sel->sz.scaled_h);
}
}
@@ -874,7 +665,7 @@ set_cursor(struct Rect rect) {
} else if (collide_rect(state.cursor_x, state.cursor_y, rect)) {
c = cursors.grab;
}
- XDefineCursor(state.dpy, state.win, c);
+ XDefineCursor(state.ctx.dpy, state.ctx.win, c);
}
static void
@@ -937,28 +728,10 @@ set_selection(
sel->rect.y0 = rect_y0;
sel->rect.x1 = rect_x1;
sel->rect.y1 = rect_y1;
- sel->orig_w = orig_w;
- sel->orig_h = orig_h;
- sel->scaled_w = scaled_w;
- sel->scaled_h = scaled_h;
-}
-
-/* get the scaled size of an image based on the current window size */
-static void
-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);
- }
+ sel->sz.orig_w = orig_w;
+ sel->sz.orig_h = orig_h;
+ sel->sz.scaled_w = scaled_w;
+ sel->sz.scaled_h = scaled_h;
}
/* change the shown image
@@ -966,27 +739,27 @@ get_scaled_size(int orig_w, int orig_h, int *scaled_w, int *scaled_h) {
* copy_box determines whether the cropping rectangle of the current
* selection should be copied (i.e. this is a true value when return
* is pressed) */
-static void
+void
change_picture(Imlib_Image new_image, int new_selection, int copy_box) {
int orig_w, orig_h, actual_w, actual_h;
/* set window title to filename */
XSetStandardProperties(
- state.dpy, state.win,
+ state.ctx.dpy, state.ctx.win,
state.filenames[new_selection],
NULL, None, NULL, 0, NULL
);
- if (state.cur_image) {
- imlib_context_set_image(state.cur_image);
+ if (state.ctx.cur_image) {
+ imlib_context_set_image(state.ctx.cur_image);
imlib_free_image();
}
- state.cur_image = new_image;
- imlib_context_set_image(state.cur_image);
+ state.ctx.cur_image = new_image;
+ imlib_context_set_image(state.ctx.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);
+ get_scaled_size(&state.ctx, 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) {
@@ -1004,95 +777,32 @@ change_picture(Imlib_Image new_image, int new_selection, int copy_box) {
-200, -200, -200, -200,
orig_w, orig_h, actual_w, actual_h
);
- } else if (sel->rect.x0 != -200 && actual_w != sel->scaled_w) {
+ } else if (sel->rect.x0 != -200 && actual_w != sel->sz.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 should have been preserved anyways */
- double scale = (double)actual_w / sel->scaled_w;
+ double scale = (double)actual_w / sel->sz.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;
+ sel->sz.scaled_w = actual_w;
+ sel->sz.scaled_h = actual_h;
sel->valid = 1;
- queue_update(0, 0, sel->scaled_w, sel->scaled_h);
+ queue_update(0, 0, sel->sz.scaled_w, sel->sz.scaled_h);
/* set the cursor since the cropping rectangle may have changed */
set_cursor(sel->rect);
}
-/* show the next image in the argument list - unloadable files are skipped
- * copy_box determines whether the current selection is copied */
-static void
-next_picture(int copy_box) {
- if (state.cur_selection + 1 >= state.num_files)
- return;
- Imlib_Image tmp_image = NULL;
- int tmp_cur_selection = state.cur_selection;
- /* loop until we find a loadable file */
- while (!tmp_image && tmp_cur_selection + 1 < state.num_files) {
- tmp_cur_selection++;
- if (!state.filenames[tmp_cur_selection])
- continue;
- tmp_image = imlib_load_image_immediately(
- state.filenames[tmp_cur_selection]
- );
- if (!tmp_image) {
- fprintf(stderr, "Warning: Unable to load image '%s'.\n",
- state.filenames[tmp_cur_selection]);
- state.filenames[tmp_cur_selection] = NULL;
- }
- }
- /* immediately exit program if no loadable image is found on startup */
- if (state.cur_selection < 0 && !tmp_image) {
- fprintf(stderr, "No loadable images found.\n");
- cleanup();
- exit(1);
- }
- if (!tmp_image)
- return;
-
- change_picture(tmp_image, tmp_cur_selection, copy_box);
-}
-
-/* show the previous image in the argument list - unloadable files are skipped
- * copy_box determines whether the current selection is copied */
-static void
-last_picture(int copy_box) {
- if (state.cur_selection <= 0)
- return;
- Imlib_Image tmp_image = NULL;
- int tmp_cur_selection = state.cur_selection;
- /* loop until we find a loadable file */
- while (!tmp_image && tmp_cur_selection > 0) {
- tmp_cur_selection--;
- if (!state.filenames[tmp_cur_selection])
- continue;
- tmp_image = imlib_load_image_immediately(
- state.filenames[tmp_cur_selection]
- );
- if (!tmp_image) {
- fprintf(stderr, "Warning: Unable to load image '%s'.\n",
- state.filenames[tmp_cur_selection]);
- state.filenames[tmp_cur_selection] = NULL;
- }
- }
-
- if (!tmp_image)
- return;
-
- change_picture(tmp_image, tmp_cur_selection, copy_box);
-}
-
static void
clear_selection(void) {
if (state.cur_selection < 0 || !state.selections[state.cur_selection].valid)
return;
struct Selection *sel = &state.selections[state.cur_selection];
sel->rect.x0 = sel->rect.x1 = sel->rect.y0 = sel->rect.y1 = -200;
- queue_update(0, 0, sel->scaled_w, sel->scaled_h);
+ queue_update(0, 0, sel->sz.scaled_w, sel->sz.scaled_h);
}
static void
@@ -1100,7 +810,7 @@ switch_color(void) {
if (state.cur_selection < 0 || !state.selections[state.cur_selection].valid)
return;
state.cur_col = state.cur_col == 1 ? 2 : 1;
- queue_update(0, 0, state.window_w, state.window_h);
+ queue_update(0, 0, state.ctx.window_w, state.ctx.window_h);
}
static int
@@ -1111,16 +821,16 @@ key_press(XEvent event) {
XLookupString(&event.xkey, buf, sizeof(buf), &sym, NULL);
switch (sym) {
case XK_Left:
- last_picture(0);
+ last_picture(state.cur_selection, state.filenames, 0);
break;
case XK_Right:
- next_picture(0);
+ next_picture(state.cur_selection, state.filenames, state.num_files, 0);
break;
case XK_Return:
if (event.xkey.state & ShiftMask)
- last_picture(1);
+ last_picture(state.cur_selection, state.filenames, 1);
else
- next_picture(1);
+ next_picture(state.cur_selection, state.filenames, state.num_files, 1);
break;
case XK_Delete:
clear_selection();
@@ -1129,13 +839,14 @@ key_press(XEvent event) {
switch_color();
break;
case XK_space:
- XGetWindowAttributes(state.dpy, state.win, &attrs);
+ XGetWindowAttributes(state.ctx.dpy, state.ctx.win, &attrs);
resize_window(attrs.width, attrs.height);
/* queue update separately so it also redraws when
size didn't change */
- queue_update(0, 0, state.window_w, state.window_h);
+ queue_update(0, 0, state.ctx.window_w, state.ctx.window_h);
break;
case XK_q:
+ state.print_on_exit = 1;
return 0;
default:
break;
diff --git a/selectool.1 b/selectool.1
@@ -0,0 +1,140 @@
+.Dd May 14, 2024
+.Dt SELECTOOL 1
+.Os
+.Sh NAME
+.Nm selectool
+.Nd image selection tool
+.Sh SYNOPSIS
+.Nm
+.Op Ar -ms
+.Op Ar -f format
+.Op Ar -w width
+.Op Ar -c color
+.Op Ar -z size
+.Ar file ...
+.Sh DESCRIPTION
+.Nm
+shows each of the given images and allows them to be selected or deselected.
+On exit, the given command is printed for each of the files.
+.Sh OPTIONS
+.Bl -tag -width Ds
+.It Fl m
+Disable automatic redrawing when the window is resized (the
+.Fl m
+stands for 'manual').
+This may be useful on older machines that start accelerating global
+warming when the image is redrawn constantly while resizing.
+Note that this also disables exposure events, so the window has to be
+manually redrawn when switching back to it from another window.
+.It Fl s
+Select all images by default.
+.It Fl f Ar format
+Set the format to be used when the commands are output.
+See
+.Sx OUTPUT FORMAT
+for details.
+.It Fl w Ar width
+Set the line width of the cross that is drawn over selected images
+in pixels (valid values: 1-99).
+Default: 5.
+.It Fl c Ar color
+Set the color of the cross that is drawn over selected images.
+Default: #FF0000.
+.It Fl z Ar size
+Set the Imlib2 in-memory cache to
+.Ar size
+MiB (valid values: 0-1024).
+Default: 4.
+.El
+.Sh OUTPUT FORMAT
+The command for each selected image is output using the format given by
+.Fl f ,
+or the default of
+.Ql rm -- '%f' .
+.Pp
+The following substitutions are performed:
+.Bl -tag -width Ds
+.It %%
+Print
+.Ql % .
+.It %f
+Print the filename of the image.
+Warning: This is printed as is, without any escaping.
+.El
+.Pp
+If an unknown substitution is encountered, a warning is printed to
+standard error and the characters are printed verbatim.
+.Sh KEYBINDS
+.Bl -tag -width Ds
+.It ARROW LEFT
+Go to the previous image.
+.It ARROW RIGHT
+Go to the next image.
+.It RETURN
+Deselect the current image and go to the next image.
+.It SHIFT + RETURN
+Deselect the current image and go to the previous image.
+.It d
+Select the current image and go to the next image.
+.It D
+Select the current image and go to the previous image.
+.It t
+Toggle the selection status of the current image.
+.It SPACE
+Redraw the window.
+This is useful when automatic redrawing is disabled with
+.Fl m .
+.It q
+Exit the program, printing the set command for all selected images.
+If the window is closed through some other means, no commands are printed.
+.El
+.Sh EXIT STATUS
+.Ex -std
+.Sh EXAMPLES
+Normal usage to delete selected images:
+.Bd -literal
+$ selectool *.jpg > tmp.sh
+$ sh tmp.sh
+.Ed
+.Pp
+Or, if you're brave:
+.Bd -literal
+$ selectool *.jpg | sh
+.Ed
+.Pp
+The original use case for
+.Nm
+was to quickly delete images that have been recovered using programs like
+.Xr photorec 8
+or
+.Xr foremost 8 .
+When used on a system partition, these programs generally recover a lot of
+images that aren't important, which then need to be sorted manually.
+Other programs that the author used for this task in the past were not ideal
+because they either were much too slow or allowed mistakes to be made too
+easily by deleting images immediately.
+.Pp
+It is also possible to do more advanced things.
+For instance, to move the selected images into a different directory,
+something like this can be done:
+.Bd -literal
+$ selectool -f "mv -- '%f' '/path/to/dir/'" *.jpg | sh
+.Ed
+.Pp
+Note that no great care has been taken to deal with filenames containing
+single or double quotes.
+That is left as an exercise to the reader (hint: just don't have
+filenames containing quotes).
+.Sh SEE ALSO
+.Xr croptool 1 ,
+.Xr rm 1 ,
+.Xr foremost 8 ,
+.Xr photorec 8
+.Sh AUTHORS
+.An lumidify Aq Mt nobody@lumidify.org
+.Sh BUGS
+The filenames are printed without any escaping, so filenames with
+quotes may cause issues depending on the output format.
+.Pp
+Transparent portions of images should probably be shown differently,
+but I'm too lazy to fix that and don't really care at the moment.
diff --git a/selectool.c b/selectool.c
@@ -0,0 +1,423 @@
+/*
+ * Copyright (c) 2024 lumidify <nobody@lumidify.org>
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <unistd.h>
+
+#include <X11/X.h>
+#include <X11/Xlib.h>
+#include <X11/Xutil.h>
+#include <X11/keysym.h>
+
+#include <Imlib2.h>
+
+#include "common.h"
+
+/* Whether to select all images by default */
+static int SELECT_DEFAULT = 0;
+/* The color of the selection box */
+static const char *SELECTION_COLOR = "#FF0000";
+/* The width of the selection line */
+static int LINE_WIDTH = 5;
+/* When set to 1, the display is redrawn on window resize */
+static short RESIZE_REDRAW = 1;
+/*
+ The command printed for each image.
+ %f: Filename of image.
+*/
+static const char *CMD_FORMAT = "rm -- '%f'";
+/* Size of Imlib2 in-memory cache in MiB */
+static int CACHE_SIZE = 4;
+
+extern char *optarg;
+extern int optind;
+
+struct Selection {
+ ImageSize sz;
+ char selected;
+};
+
+static struct {
+ GraphicsContext ctx;
+
+ struct Selection *selections;
+ char **filenames;
+ int cur_selection;
+ int num_files;
+ int cursor_x;
+ int cursor_y;
+ XColor col;
+ char print_on_exit;
+} state;
+
+static void usage(void);
+static void mainloop(void);
+static void setup(int argc, char *argv[]);
+static void redraw(void);
+static void set_selection(char selected);
+static void toggle_selection(void);
+static void resize_window(int w, int h);
+static int key_press(XEvent event);
+static void queue_update(int x, int y, int w, int h);
+static void print_cmd(const char *filename, int dry_run);
+
+static void
+usage(void) {
+ fprintf(stderr, "USAGE: deletetool [-mrs] [-f format] "
+ "[-w width] [-c color] "
+ "[-z size] file ...\n");
+}
+
+int
+main(int argc, char *argv[]) {
+ char c;
+
+ while ((c = getopt(argc, argv, "f:w:c:msz:")) != -1) {
+ switch (c) {
+ case 'f':
+ CMD_FORMAT = optarg;
+ break;
+ case 'm':
+ RESIZE_REDRAW = 0;
+ break;
+ case 'c':
+ SELECTION_COLOR = optarg;
+ break;
+ case 'w':
+ if (parse_int(optarg, 1, 99, &LINE_WIDTH)) {
+ fprintf(stderr, "Invalid line width.\n");
+ exit(1);
+ }
+ break;
+ case 'z':
+ if (parse_int(optarg, 0, 1024, &CACHE_SIZE)) {
+ fprintf(stderr, "Invalid cache size.\n");
+ exit(1);
+ }
+ break;
+ case 's':
+ SELECT_DEFAULT = 1;
+ break;
+ default:
+ usage();
+ exit(1);
+ break;
+ }
+ }
+
+ /* print warning if command format is invalid */
+ print_cmd("", 1);
+
+ argc -= optind;
+ argv += optind;
+ if (argc < 1) {
+ usage();
+ exit(1);
+ }
+ setup(argc, argv);
+
+ mainloop();
+
+ if (state.print_on_exit) {
+ for (int i = 0; i < argc; i++) {
+ if (state.selections[i].selected)
+ print_cmd(state.filenames[i], 0);
+ }
+ }
+
+ cleanup();
+
+ return 0;
+}
+
+static void
+mainloop(void) {
+ XEvent event;
+ int running = 1;
+
+ while (running) {
+ do {
+ XNextEvent(state.ctx.dpy, &event);
+ switch (event.type) {
+ case Expose:
+ if (RESIZE_REDRAW)
+ 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 KeyPress:
+ running = key_press(event);
+ break;
+ case ClientMessage:
+ if ((Atom)event.xclient.data.l[0] == state.ctx.wm_delete_msg)
+ running = 0;
+ default:
+ break;
+ }
+ } while (XPending(state.ctx.dpy));
+
+ redraw();
+ }
+}
+
+static void
+setup(int argc, char *argv[]) {
+ state.selections = malloc(argc * sizeof(struct Selection));
+ if (!state.selections) {
+ fprintf(stderr, "Unable to allocate memory.\n");
+ exit(1);
+ }
+ state.num_files = argc;
+ state.filenames = argv;
+ state.cur_selection = -1;
+ state.print_on_exit = 0;
+
+ for (int i = 0; i < argc; i++) {
+ state.selections[i].selected = SELECT_DEFAULT;
+ }
+
+ setup_x(&state.ctx, 500, 500, LINE_WIDTH, CACHE_SIZE);
+
+ if (!XParseColor(state.ctx.dpy, state.ctx.cm, SELECTION_COLOR, &state.col)) {
+ fprintf(stderr, "Selection color invalid.\n");
+ exit(1);
+ }
+ XAllocColor(state.ctx.dpy, state.ctx.cm, &state.col);
+
+ next_picture(state.cur_selection, state.filenames, state.num_files, 0);
+ /* Only map window here so the program exits immediately if
+ there are no loadable images, without first opening the
+ window and closing it again immediately */
+ XMapWindow(state.ctx.dpy, state.ctx.win);
+ redraw();
+}
+
+void
+cleanup(void) {
+ free(state.selections);
+ cleanup_x(&state.ctx);
+}
+
+/* queue a part of the image for redrawing */
+static void
+queue_update(int x, int y, int w, int h) {
+ if (state.cur_selection < 0)
+ return;
+ struct Selection *sel = &state.selections[state.cur_selection];
+ queue_area_update(&state.ctx, &sel->sz, x, y, w, h);
+}
+
+/* TODO: Escape filename properly
+ * -> But how? Since the format can be set by the user,
+ * it isn't really clear *what* needs to be escaped. */
+static void
+print_cmd(const char *filename, int dry_run) {
+ short percent = 0;
+ const char *c;
+ int length = 0;
+ int start_index = 0;
+ /* FIXME: just use putc instead of this complex printf dance */
+ for (c = CMD_FORMAT; *c != '\0'; c++) {
+ if (percent)
+ start_index++;
+ if (*c == '%') {
+ if (length) {
+ if (!dry_run)
+ printf("%.*s", length, CMD_FORMAT + start_index);
+ start_index += length;
+ length = 0;
+ }
+ if (percent && !dry_run)
+ printf("%%");
+ percent++;
+ percent %= 2;
+ start_index++;
+ } else if (percent && *c == 'f') {
+ if (!dry_run)
+ printf("%s", filename);
+ percent = 0;
+ } else if (percent) {
+ if (dry_run) {
+ fprintf(stderr,
+ "Warning: Unknown substitution '%c' "
+ "in format string.\n", *c
+ );
+ } else {
+ printf("%%%c", *c);
+ }
+ percent = 0;
+ } else {
+ length++;
+ }
+ }
+ if (!dry_run) {
+ if (length)
+ printf("%.*s", length, CMD_FORMAT + start_index);
+ printf("\n");
+ }
+}
+
+static void
+redraw(void) {
+ if (!state.ctx.dirty)
+ return;
+ if (!state.ctx.cur_image || state.cur_selection < 0) {
+ clear_screen(&state.ctx);
+ swap_buffers(&state.ctx);
+ return;
+ }
+
+ /* draw the parts of the image that need to be redrawn */
+ struct Selection *sel = &state.selections[state.cur_selection];
+ draw_image_updates(&state.ctx, &sel->sz);
+
+ wipe_around_image(&state.ctx, &sel->sz);
+
+ /* draw the 'X' */
+ if (sel->selected) {
+ XSetForeground(state.ctx.dpy, state.ctx.gc, state.col.pixel);
+ XDrawLine(
+ state.ctx.dpy, state.ctx.drawable, state.ctx.gc,
+ 0, 0, sel->sz.scaled_w, sel->sz.scaled_h
+ );
+ XDrawLine(
+ state.ctx.dpy, state.ctx.drawable, state.ctx.gc,
+ 0, sel->sz.scaled_h, sel->sz.scaled_w, 0
+ );
+ }
+ swap_buffers(&state.ctx);
+}
+
+static void
+resize_window(int w, int h) {
+ int actual_w, actual_h;
+ struct Selection *sel;
+ state.ctx.window_w = w;
+ state.ctx.window_h = h;
+
+ if (state.cur_selection < 0)
+ return;
+ sel = &state.selections[state.cur_selection];
+ get_scaled_size(&state.ctx, sel->sz.orig_w, sel->sz.orig_h, &actual_w, &actual_h);
+ if (actual_w != sel->sz.scaled_w) {
+ sel->sz.scaled_w = actual_w;
+ sel->sz.scaled_h = actual_h;
+ queue_update(0, 0, sel->sz.scaled_w, sel->sz.scaled_h);
+ }
+}
+
+/* change the shown image
+ * new_selection is the index of the new selection */
+void
+change_picture(Imlib_Image new_image, int new_selection, int copy_box) {
+ (void)copy_box;
+ int orig_w, orig_h, actual_w, actual_h;
+ /* set window title to filename */
+ XSetStandardProperties(
+ state.ctx.dpy, state.ctx.win,
+ state.filenames[new_selection],
+ NULL, None, NULL, 0, NULL
+ );
+ if (state.ctx.cur_image) {
+ imlib_context_set_image(state.ctx.cur_image);
+ imlib_free_image();
+ }
+ state.ctx.cur_image = new_image;
+ imlib_context_set_image(state.ctx.cur_image);
+ state.cur_selection = new_selection;
+
+ orig_w = imlib_image_get_width();
+ orig_h = imlib_image_get_height();
+ get_scaled_size(&state.ctx, orig_w, orig_h, &actual_w, &actual_h);
+
+ struct Selection *sel = &state.selections[state.cur_selection];
+ sel->sz.orig_w = orig_w;
+ sel->sz.orig_h = orig_h;
+ sel->sz.scaled_w = actual_w;
+ sel->sz.scaled_h = actual_h;
+ queue_update(0, 0, sel->sz.scaled_w, sel->sz.scaled_h);
+}
+
+static void
+set_selection(char selected) {
+ if (state.cur_selection < 0)
+ return;
+ struct Selection *sel = &state.selections[state.cur_selection];
+ sel->selected = selected;
+ queue_update(0, 0, sel->sz.scaled_w, sel->sz.scaled_h);
+}
+
+static void
+toggle_selection(void) {
+ if (state.cur_selection < 0)
+ return;
+ struct Selection *sel = &state.selections[state.cur_selection];
+ set_selection(!sel->selected);
+}
+
+static int
+key_press(XEvent event) {
+ XWindowAttributes attrs;
+ char buf[32];
+ KeySym sym;
+ XLookupString(&event.xkey, buf, sizeof(buf), &sym, NULL);
+ switch (sym) {
+ case XK_Left:
+ last_picture(state.cur_selection, state.filenames, 0);
+ break;
+ case XK_Right:
+ next_picture(state.cur_selection, state.filenames, state.num_files, 0);
+ break;
+ case XK_Return:
+ set_selection(0);
+ if (event.xkey.state & ShiftMask)
+ last_picture(state.cur_selection, state.filenames, 0);
+ else
+ next_picture(state.cur_selection, state.filenames, state.num_files, 0);
+ break;
+ case XK_d:
+ set_selection(1);
+ next_picture(state.cur_selection, state.filenames, state.num_files, 0);
+ break;
+ case XK_D:
+ set_selection(1);
+ last_picture(state.cur_selection, state.filenames, 0);
+ break;
+ case XK_space:
+ XGetWindowAttributes(state.ctx.dpy, state.ctx.win, &attrs);
+ resize_window(attrs.width, attrs.height);
+ /* queue update separately so it also redraws when
+ size didn't change */
+ queue_update(0, 0, state.ctx.window_w, state.ctx.window_h);
+ break;
+ case XK_q:
+ state.print_on_exit = 1;
+ return 0;
+ case XK_t:
+ toggle_selection();
+ break;
+ default:
+ break;
+ }
+ return 1;
+}