commit b6df034e735fb382f52a5f445551a830d8c5d63e
parent f59a8f3702933fc603571f5ed61c27a4735b1643
Author: Max Schillinger <maxschillinger@web.de>
Date: Mon, 4 Nov 2024 12:05:50 +0100
[dmenu][patch][png_images] Add patch
This patch allows to show (preview) images in dmenu. It depends on the
libspng library.
Diffstat:
2 files changed, 486 insertions(+), 0 deletions(-)
diff --git a/tools.suckless.org/dmenu/patches/png_images/dmenu-png-images-5.3.diff b/tools.suckless.org/dmenu/patches/png_images/dmenu-png-images-5.3.diff
@@ -0,0 +1,448 @@
+From 743d86e56e0c1eb4255a08fe338db03752cc99e7 Mon Sep 17 00:00:00 2001
+From: Max Schillinger <maxschillinger@web.de>
+Date: Fri, 1 Nov 2024 08:58:49 +0100
+Subject: [PATCH] Support PNG images using libspng
+
+---
+ config.mk | 2 +-
+ dmenu.c | 62 +++++++++++++++---
+ drw.c | 189 ++++++++++++++++++++++++++++++++++++++++++++++++++++++
+ drw.h | 5 ++
+ util.c | 6 ++
+ util.h | 1 +
+ 6 files changed, 254 insertions(+), 11 deletions(-)
+
+diff --git a/config.mk b/config.mk
+index 137f7c8..3217090 100644
+--- a/config.mk
++++ b/config.mk
+@@ -21,7 +21,7 @@ FREETYPEINC = /usr/include/freetype2
+
+ # includes and libs
+ INCS = -I$(X11INC) -I$(FREETYPEINC)
+-LIBS = -L$(X11LIB) -lX11 $(XINERAMALIBS) $(FREETYPELIBS)
++LIBS = -L$(X11LIB) -lX11 $(XINERAMALIBS) $(FREETYPELIBS) -lspng
+
+ # flags
+ CPPFLAGS = -D_DEFAULT_SOURCE -D_BSD_SOURCE -D_XOPEN_SOURCE=700 -D_POSIX_C_SOURCE=200809L -DVERSION=\"$(VERSION)\" $(XINERAMAFLAGS)
+diff --git a/dmenu.c b/dmenu.c
+index 804da64..b0e4109 100644
+--- a/dmenu.c
++++ b/dmenu.c
+@@ -38,11 +38,14 @@ static char *embed;
+ static int bh, mw, mh;
+ static int inputw = 0, promptw;
+ static int lrpad; /* sum of left and right padding */
++static int tbpad; /* sum of top and bottom padding for images */
+ static size_t cursor;
+ static struct item *items = NULL;
+ static struct item *matches, *matchend;
+ static struct item *prev, *curr, *next, *sel;
+ static int mon = -1, screen;
++static char *image_prefix = "PNG_IMAGE:";
++static int image_size = -1; /* in pixels */
+
+ static Atom clip, utf8;
+ static Display *dpy;
+@@ -58,12 +61,26 @@ static int (*fstrncmp)(const char *, const char *, size_t) = strncmp;
+ static char *(*fstrstr)(const char *, const char *) = strstr;
+
+ static unsigned int
+-textw_clamp(const char *str, unsigned int n)
++textw_clamp(const char *str, unsigned int n, unsigned int maxw, unsigned int maxh)
+ {
+- unsigned int w = drw_fontset_getwidth_clamp(drw, str, n) + lrpad;
++ unsigned int w;
++ if (startswith(image_prefix, str) &&
++ (w = drw_getimagewidth_clamp(drw, str + strlen(image_prefix), maxw, maxh)))
++ return MIN(w + lrpad, n);
++ w = drw_fontset_getwidth_clamp(drw, str, n) + lrpad;
+ return MIN(w, n);
+ }
+
++static unsigned int
++texth_clamp(const char *str, unsigned int n, unsigned int maxw, unsigned int maxh)
++{
++ unsigned int h;
++ if (startswith(image_prefix, str) &&
++ (h = drw_getimageheight_clamp(drw, str + strlen(image_prefix), maxw, maxh)))
++ return MIN(h + tbpad, n);
++ return MIN(bh, n);
++}
++
+ static void
+ appenditem(struct item *item, struct item **list, struct item **last)
+ {
+@@ -83,15 +100,19 @@ calcoffsets(void)
+ int i, n;
+
+ if (lines > 0)
+- n = lines * bh;
++ n = mh - bh;
+ else
+ n = mw - (promptw + inputw + TEXTW("<") + TEXTW(">"));
+ /* calculate which items will begin the next page and previous page */
+ for (i = 0, next = curr; next; next = next->right)
+- if ((i += (lines > 0) ? bh : textw_clamp(next->text, n)) > n)
++ if ((i += (lines > 0)
++ ? texth_clamp(next->text, n, mw - lrpad, image_size)
++ : textw_clamp(next->text, n, image_size, bh)) > n)
+ break;
+ for (i = 0, prev = curr; prev && prev->left; prev = prev->left)
+- if ((i += (lines > 0) ? bh : textw_clamp(prev->left->text, n)) > n)
++ if ((i += (lines > 0)
++ ? texth_clamp(prev->left->text, n, mw - lrpad, image_size)
++ : textw_clamp(prev->left->text, n, image_size, bh)) > n)
+ break;
+ }
+
+@@ -139,7 +160,18 @@ drawitem(struct item *item, int x, int y, int w)
+ else
+ drw_setscheme(drw, scheme[SchemeNorm]);
+
+- return drw_text(drw, x, y, w, bh, lrpad / 2, item->text, 0);
++ int vertical = lines > 0;
++ if (startswith(image_prefix, item->text)) {
++ char *path = item->text + strlen(image_prefix);
++ unsigned int image_width = vertical ? w - lrpad : image_size;
++ unsigned int image_height = vertical ? image_size : bh;
++ drw_image(drw, &x, &y, &image_width, &image_height,
++ lrpad, vertical ? tbpad : 0, path, vertical);
++ if (image_width && image_height)
++ return vertical ? y : x;
++ }
++ int nextx = drw_text(drw, x, y, w, bh, lrpad / 2, item->text, 0);
++ return vertical ? y + bh : nextx;
+ }
+
+ static void
+@@ -169,8 +201,9 @@ drawmenu(void)
+
+ if (lines > 0) {
+ /* draw vertical list */
++ y = bh;
+ for (item = curr; item != next; item = item->right)
+- drawitem(item, x, y += bh, mw - x);
++ y = drawitem(item, x, y, mw - x);
+ } else if (matches) {
+ /* draw horizontal list */
+ x += inputw;
+@@ -181,7 +214,7 @@ drawmenu(void)
+ }
+ x += w;
+ for (item = curr; item != next; item = item->right)
+- x = drawitem(item, x, 0, textw_clamp(item->text, mw - x - TEXTW(">")));
++ x = drawitem(item, x, 0, textw_clamp(item->text, mw - x - TEXTW(">"), image_size, bh));
+ if (next) {
+ w = TEXTW(">");
+ drw_setscheme(drw, scheme[SchemeNorm]);
+@@ -635,7 +668,10 @@ setup(void)
+ /* calculate menu geometry */
+ bh = drw->fonts->h + 2;
+ lines = MAX(lines, 0);
+- mh = (lines + 1) * bh;
++ /* default values for image_size */
++ if (image_size < 0)
++ image_size = (lines > 0) ? 2 * bh : 8 * bh;
++ mh = bh + ((lines > 0) ? MAX(lines * bh, image_size) : 0);
+ #ifdef XINERAMA
+ i = 0;
+ if (parentwin == root && (info = XineramaQueryScreens(dpy, &n))) {
+@@ -715,7 +751,8 @@ static void
+ usage(void)
+ {
+ die("usage: dmenu [-bfiv] [-l lines] [-p prompt] [-fn font] [-m monitor]\n"
+- " [-nb color] [-nf color] [-sb color] [-sf color] [-w windowid]");
++ " [-nb color] [-nf color] [-sb color] [-sf color] [-w windowid]\n"
++ " [-ip image_prefix] [-is image_size]");
+ }
+
+ int
+@@ -757,6 +794,10 @@ main(int argc, char *argv[])
+ colors[SchemeSel][ColFg] = argv[++i];
+ else if (!strcmp(argv[i], "-w")) /* embedding window id */
+ embed = argv[++i];
++ else if (!strcmp(argv[i], "-ip")) /* image prefix */
++ image_prefix = argv[++i];
++ else if (!strcmp(argv[i], "-is")) /* max. image preview size (height or width) */
++ image_size = atoi(argv[++i]);
+ else
+ usage();
+
+@@ -775,6 +816,7 @@ main(int argc, char *argv[])
+ if (!drw_fontset_create(drw, fonts, LENGTH(fonts)))
+ die("no fonts could be loaded.");
+ lrpad = drw->fonts->h;
++ tbpad = lrpad / 2;
+
+ #ifdef __OpenBSD__
+ if (pledge("stdio rpath", NULL) == -1)
+diff --git a/drw.c b/drw.c
+index c41e6af..20c2125 100644
+--- a/drw.c
++++ b/drw.c
+@@ -4,12 +4,24 @@
+ #include <string.h>
+ #include <X11/Xlib.h>
+ #include <X11/Xft/Xft.h>
++#include <spng.h>
+
+ #include "drw.h"
+ #include "util.h"
+
+ #define UTF_INVALID 0xFFFD
+
++struct image_item {
++ const char *path;
++ int width;
++ int height;
++ char *buf;
++ Pixmap pixmap;
++ struct image_item *next;
++};
++
++static struct image_item *images = NULL;
++
+ static int
+ utf8decode(const char *s_in, long *u, int *err)
+ {
+@@ -382,6 +394,163 @@ no_match:
+ return x + (render ? w : 0);
+ }
+
++static struct image_item *
++load_image(Drw *drw, unsigned int maxw, unsigned int maxh, const char *path)
++{
++ FILE *png;
++ spng_ctx *ctx = NULL;
++ int ret = 0;
++ struct spng_ihdr ihdr;
++ struct spng_plte plte = {0};
++ struct spng_row_info row_info = {0};
++ char *spng_buf;
++ int fmt = SPNG_FMT_RGBA8;
++ int crop_width;
++ int crop_height;
++
++ struct image_item *image = ecalloc(1, sizeof(struct image_item));
++ image->path = path;
++ image->next = images;
++ images = image;
++
++ png = fopen(path, "rb");
++ if (png == NULL) {
++ fprintf(stderr, "error opening input file %s\n", path);
++ return NULL;
++ }
++
++ /* Create a context */
++ ctx = spng_ctx_new(0);
++ if (ctx == NULL) {
++ fprintf(stderr, "%s: spng_ctx_new() failed\n", path);
++ return NULL;
++ }
++
++ /* Ignore and don't calculate chunk CRC's */
++ spng_set_crc_action(ctx, SPNG_CRC_USE, SPNG_CRC_USE);
++
++ /* Set memory usage limits for storing standard and unknown chunks,
++ this is important when reading untrusted files! */
++ size_t limit = 1024 * 1024 * 64;
++ spng_set_chunk_limits(ctx, limit, limit);
++
++ spng_set_png_file(ctx, png);
++
++ ret = spng_get_ihdr(ctx, &ihdr);
++ if (ret) {
++ fprintf(stderr, "%s: spng_get_ihdr() error: %s\n", path, spng_strerror(ret));
++ return NULL;
++ }
++
++ ret = spng_get_plte(ctx, &plte);
++ if (ret && ret != SPNG_ECHUNKAVAIL) {
++ fprintf(stderr, "%s: spng_get_plte() error: %s\n", path, spng_strerror(ret));
++ return NULL;
++ }
++
++ size_t image_size, bytes_per_row; /* size in bytes, not in pixels */
++
++ ret = spng_decoded_image_size(ctx, fmt, &image_size);
++ if (ret)
++ return NULL;
++
++ spng_buf = malloc(image_size);
++ if (!spng_buf)
++ return NULL;
++
++ ret = spng_decode_image(ctx, NULL, 0, fmt, SPNG_DECODE_PROGRESSIVE);
++ if (ret) {
++ fprintf(stderr, "%s: progressive spng_decode_image() error: %s\n",
++ path, spng_strerror(ret));
++ return NULL;
++ }
++
++ /* ihdr.height will always be non-zero if spng_get_ihdr() succeeds */
++ bytes_per_row = image_size / ihdr.height;
++ crop_width = MIN(ihdr.width, maxw);
++ crop_height = MIN(ihdr.height, maxh);
++
++ do {
++ ret = spng_get_row_info(ctx, &row_info);
++ if (ret)
++ break;
++ ret = spng_decode_row(ctx, spng_buf + row_info.row_num * bytes_per_row, bytes_per_row);
++ } while (!ret && row_info.row_num < crop_height);
++
++ if (ret != SPNG_EOI && row_info.row_num < crop_height)
++ fprintf(stderr, "%s: progressive decode error: %s\n", path, spng_strerror(ret));
++
++ image->buf = calloc(ihdr.width * crop_height * 4, sizeof(char));
++ for (int i = 0; i < ihdr.width * crop_height; i++) {
++ /* RGBA to BGRA */
++ image->buf[i*4+2] = spng_buf[i*4+0];
++ image->buf[i*4+1] = spng_buf[i*4+1];
++ image->buf[i*4+0] = spng_buf[i*4+2];
++ image->buf[i*4+3] = spng_buf[i*4+3];
++ }
++ image->width = crop_width;
++ image->height = crop_height;
++
++ XImage *img = XCreateImage(drw->dpy, CopyFromParent, DefaultDepth(drw->dpy, drw->screen),
++ ZPixmap, 0, image->buf, ihdr.width, crop_height, 32, 0);
++ image->pixmap = XCreatePixmap(drw->dpy, drw->root, crop_width, crop_height, 24);
++ XPutImage(drw->dpy, image->pixmap, drw->gc, img, 0, 0, 0, 0, crop_width, crop_height);
++ spng_ctx_free(ctx);
++ fclose(png);
++ return image;
++}
++
++void
++drw_image(Drw *drw, int *x, int *y, unsigned int *w, unsigned int *h,
++ unsigned int lrpad, unsigned int tbpad, const char *path, int vertical)
++{
++ /* *x and *y refer to box position including padding,
++ * *w and *h are the maximum image width and height without padding */
++ struct image_item *image = NULL;
++ int render = *x || *y;
++ int crop_width, crop_height;
++
++ // find path in images
++ for (struct image_item *item = images; item != NULL; item = item->next) {
++ if (!strcmp(item->path, path)) {
++ image = item;
++ if (!image->buf)
++ goto file_error;
++ break;
++ }
++ }
++
++ if (!image && !(image = load_image(drw, *w, *h, path)))
++ goto file_error;
++
++ if (!render) {
++ *w = image->width;
++ *h = image->height;
++ return;
++ }
++
++ crop_width = MIN(image->width, *w);
++ crop_height = MIN(image->height, *h);
++ if (vertical)
++ *h = crop_height;
++ else
++ *w = crop_width;
++
++ XSetForeground(drw->dpy, drw->gc, drw->scheme[ColBg].pixel);
++ XFillRectangle(drw->dpy, drw->drawable, drw->gc, *x, *y, *w + lrpad, *h + tbpad);
++ XCopyArea(drw->dpy, image->pixmap, drw->drawable, drw->gc, 0, 0,
++ crop_width, crop_height, *x + lrpad/2, *y + tbpad/2);
++
++ if (vertical)
++ *y += *h + tbpad;
++ else
++ *x += *w + lrpad;
++ return;
++
++file_error:
++ *w = *h = 0;
++}
++
+ void
+ drw_map(Drw *drw, Window win, int x, int y, unsigned int w, unsigned int h)
+ {
+@@ -424,6 +593,26 @@ drw_font_getexts(Fnt *font, const char *text, unsigned int len, unsigned int *w,
+ *h = font->h;
+ }
+
++unsigned int
++drw_getimagewidth_clamp(Drw *drw, const char *path, unsigned int maxw, unsigned int maxh)
++{
++ int x = 0, y = 0;
++ unsigned int w = maxw, h = maxh;
++ if (drw && path && maxw && maxh)
++ drw_image(drw, &x, &y, &w, &h, 0, 0, path, 0);
++ return MIN(maxw, w);
++}
++
++unsigned int
++drw_getimageheight_clamp(Drw *drw, const char *path, unsigned int maxw, unsigned int maxh)
++{
++ int x = 0, y = 0;
++ unsigned int w = maxw, h = maxh;
++ if (drw && path && maxw && maxh)
++ drw_image(drw, &x, &y, &w, &h, 0, 0, path, 1);
++ return MIN(maxh, h);
++}
++
+ Cur *
+ drw_cur_create(Drw *drw, int shape)
+ {
+diff --git a/drw.h b/drw.h
+index fd7631b..330722d 100644
+--- a/drw.h
++++ b/drw.h
+@@ -38,6 +38,10 @@ unsigned int drw_fontset_getwidth(Drw *drw, const char *text);
+ unsigned int drw_fontset_getwidth_clamp(Drw *drw, const char *text, unsigned int n);
+ void drw_font_getexts(Fnt *font, const char *text, unsigned int len, unsigned int *w, unsigned int *h);
+
++/* Image abstraction */
++unsigned int drw_getimagewidth_clamp(Drw *drw, const char *path, unsigned int maxw, unsigned int maxh);
++unsigned int drw_getimageheight_clamp(Drw *drw, const char *path, unsigned int maxw, unsigned int maxh);
++
+ /* Colorscheme abstraction */
+ void drw_clr_create(Drw *drw, Clr *dest, const char *clrname);
+ Clr *drw_scm_create(Drw *drw, const char *clrnames[], size_t clrcount);
+@@ -53,6 +57,7 @@ void drw_setscheme(Drw *drw, Clr *scm);
+ /* Drawing functions */
+ void drw_rect(Drw *drw, int x, int y, unsigned int w, unsigned int h, int filled, int invert);
+ int drw_text(Drw *drw, int x, int y, unsigned int w, unsigned int h, unsigned int lpad, const char *text, int invert);
++void drw_image(Drw *drw, int *x, int *y, unsigned int *w, unsigned int *h, unsigned int lrpad, unsigned int tbpad, const char *path, int vertical);
+
+ /* Map functions */
+ void drw_map(Drw *drw, Window win, int x, int y, unsigned int w, unsigned int h);
+diff --git a/util.c b/util.c
+index 8e26a51..975735b 100644
+--- a/util.c
++++ b/util.c
+@@ -35,3 +35,9 @@ ecalloc(size_t nmemb, size_t size)
+ die("calloc:");
+ return p;
+ }
++
++int
++startswith(const char* prefix, const char* str)
++{
++ return strncmp(prefix, str, strlen(prefix)) == 0;
++}
+diff --git a/util.h b/util.h
+index c0a50d4..6db39c8 100644
+--- a/util.h
++++ b/util.h
+@@ -7,3 +7,4 @@
+
+ void die(const char *fmt, ...);
+ void *ecalloc(size_t nmemb, size_t size);
++int startswith(const char* prefix, const char* str);
+--
+2.47.0
+
diff --git a/tools.suckless.org/dmenu/patches/png_images/index.md b/tools.suckless.org/dmenu/patches/png_images/index.md
@@ -0,0 +1,38 @@
+png images
+==========
+
+Description
+-----------
+Preview PNG images using [libspng](https://libspng.org/).
+
+Lines like `PNG_IMAGE:/path/to/image.png` will be replaced with a preview of
+the given image file. The prefix (`PNG_IMAGE:`) can be changed via the `-ip`
+flag. An empty prefix string is possible, too.
+
+The image preview is taken from the top left corner of the image. For vertical
+menus, the height is limited to _N_ pixels provided via `-is N` or the height
+of two text lines otherwise. For horizontal menus, the preview height equals
+the bar height. The image width is limited to _N_ pixels provided via `-is N`
+or the height of eight text lines otherwise.
+
+Example
+-------
+
+Select a [greenclip](https://github.com/erebe/greenclip) clipboard entry with
+image previews:
+
+ greenclip print | grep . \
+ | sed -E 's|^(image/png )(.*)|\1/tmp/greenclip/\2.png|' \
+ | ./dmenu -i -fn 'monospace:size=14' -ip 'image/png ' -p clipboard -l 23 -is 120 \
+ | sed -E 's|^(image/png )/tmp/greenclip/(-?[0-9]+)\.png$|\1\2|' \
+ | xargs -r -d'\n' -I '{}' greenclip print '{}'
+
+![dmenu png images screenshot](https://maximilian-schillinger.de/img/screenshot_dmenu_libspng.png)
+
+Download
+--------
+* [dmenu-png-images-5.3.diff](dmenu-png-images-5.3.diff) (2024-11-04) ([mirror @ sr.ht](https://git.sr.ht/~maxgyver83/dmenu/tree/image-support-libspng))
+
+Authors
+-------
+* Max Schillinger - <maxschillinger@web.de>