croptool

Image cropping tool
git clone git://lumidify.org/croptool.git (fast, but not encrypted)
git clone https://lumidify.org/croptool.git (encrypted, but very slow)
git clone git://4kcetb7mo7hj6grozzybxtotsub5bempzo4lirzc3437amof2c2impyd.onion/croptool.git (over tor)
Log | Files | Refs | README | LICENSE

commit f5defee322437ec0c63a9d2872b7ef10936e9140
parent 022ad164b6516bbb4c5109e45c68248c17ff7115
Author: lumidify <nobody@lumidify.org>
Date:   Tue, 14 May 2024 16:55:16 +0200

Add selectool

Diffstat:
M.gitignore | 1+
MCHANGELOG | 6++++++
MLICENSE | 2+-
MMakefile | 28+++++++++++++++++++++-------
MREADME | 12+++++++++++-
MTODO | 4++++
Acommon.c | 332+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acommon.h | 83+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mcroptool.1 | 14+++++++++-----
Mcroptool.c | 489++++++++++++++++---------------------------------------------------------------
Aselectool.1 | 140+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aselectool.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; +}