From 541dead59a544fbd15390ad35f6412ca6ac64d32 Mon Sep 17 00:00:00 2001 From: Kris Yotam <75515498+krisyotam@users.noreply.github.com> Date: Sun, 21 Jun 2026 04:08:28 -0500 Subject: [PATCH] Add PNG_IMAGES_PATCH for inline image previews Port of suckless.org png_images patch into flexipatch build. Adds -ip (image prefix) and -is (image size) flags. Items prefixed with PNG_IMAGE: render inline PNG thumbnails. Requires libspng. --- config.mk | 5 +- dmenu.c | 121 ++++++++++++++++++++++++++++++-- drw.c | 202 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ drw.h | 7 ++ patches.h | 3 + util.c | 9 +++ util.h | 3 + 7 files changed, 342 insertions(+), 8 deletions(-) diff --git a/config.mk b/config.mk index dc09379f7647c2247e15cd23b3e84923d3466e91..6010bb0ca58fac6d78926886faaad83b365c7890 100644 --- a/config.mk +++ b/config.mk @@ -33,9 +33,12 @@ XRENDER = -lXrender #PANGOINC = `pkg-config --cflags xft pango pangoxft` #PANGOLIB = `pkg-config --libs xft pango pangoxft` +# PNG images patch / PNG_IMAGES_PATCH +SPNGLIBS = -lspng + # includes and libs INCS = -I$(X11INC) -I$(FREETYPEINC) $(PANGOINC) $(BIDIINC) -LIBS = -L$(X11LIB) -lX11 $(XINERAMALIBS) $(FREETYPELIBS) -lm $(XRENDER) $(PANGOLIB) $(BIDILIBS) +LIBS = -L$(X11LIB) -lX11 $(XINERAMALIBS) $(FREETYPELIBS) -lm $(XRENDER) $(PANGOLIB) $(BIDILIBS) $(SPNGLIBS) # flags CPPFLAGS = -D_DEFAULT_SOURCE -D_BSD_SOURCE -D_XOPEN_SOURCE=700 -D_POSIX_C_SOURCE=200809L -DVERSION=\"$(VERSION)\" $(XINERAMAFLAGS) $(EXTRAFLAGS) diff --git a/dmenu.c b/dmenu.c index 885898562fd76e933f428e62860e6b66fc42e851..344d7f5af81dce68c1cccc3f309ffaa032d6e3e6 100644 --- a/dmenu.c +++ b/dmenu.c @@ -126,6 +126,9 @@ static int inputw = 0, promptw; static int passwd = 0; #endif // PASSWORD_PATCH static int lrpad; /* sum of left and right padding */ +#if PNG_IMAGES_PATCH +static int tbpad; /* sum of top and bottom padding for images */ +#endif // PNG_IMAGES_PATCH #if BARPADDING_PATCH static int vp; /* vertical padding for bar */ static int sp; /* side padding for bar */ @@ -138,6 +141,10 @@ static struct item *items = NULL; static struct item *matches, *matchend; static struct item *prev, *curr, *next, *sel; static int mon = -1, screen; +#if PNG_IMAGES_PATCH +static char *image_prefix = "PNG_IMAGE:"; +static int image_size = -1; /* in pixels */ +#endif // PNG_IMAGES_PATCH #if PRINTINDEX_PATCH static int print_index = 0; #endif // PRINTINDEX_PATCH @@ -185,12 +192,34 @@ static Clr *scheme[SchemeLast]; #include "config.h" static unsigned int -textw_clamp(const char *str, unsigned int n) +textw_clamp(const char *str, unsigned int n +#if PNG_IMAGES_PATCH + , unsigned int maxw, unsigned int maxh +#endif // PNG_IMAGES_PATCH +) { - unsigned int w = drw_fontset_getwidth_clamp(drw, str, n) + lrpad; + unsigned int w; + #if PNG_IMAGES_PATCH + if (startswith(image_prefix, str) && + (w = drw_getimagewidth_clamp(drw, str + strlen(image_prefix), maxw, maxh))) + return MIN(w + lrpad, n); + #endif // PNG_IMAGES_PATCH + w = drw_fontset_getwidth_clamp(drw, str, n) + lrpad; return MIN(w, n); } +#if PNG_IMAGES_PATCH +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((unsigned int)bh, n); +} +#endif // PNG_IMAGES_PATCH + static void appenditem(struct item *item, struct item **list, struct item **last); static void calcoffsets(void); static void cleanup(void); @@ -245,14 +274,16 @@ calcoffsets(void) int i, n, rpad = 0; if (lines > 0) { - #if GRID_PATCH + #if PNG_IMAGES_PATCH + n = mh - bh; + #elif GRID_PATCH if (columns) n = lines * columns * bh; else n = lines * bh; #else n = lines * bh; - #endif // GRID_PATCH + #endif // PNG_IMAGES_PATCH / GRID_PATCH } else { #if NUMBERS_PATCH rpad = TEXTW(numbers); @@ -265,10 +296,22 @@ calcoffsets(void) } /* calculate which items will begin the next page and previous page */ for (i = 0, next = curr; next; next = next->right) + #if PNG_IMAGES_PATCH + if ((i += (lines > 0) + ? texth_clamp(next->text, n, mw - lrpad, image_size) + : textw_clamp(next->text, n, image_size, bh)) > n) + #else if ((i += (lines > 0) ? bh : textw_clamp(next->text, n)) > n) + #endif // PNG_IMAGES_PATCH break; for (i = 0, prev = curr; prev && prev->left; prev = prev->left) + #if PNG_IMAGES_PATCH + if ((i += (lines > 0) + ? texth_clamp(prev->left->text, n, mw - lrpad, image_size) + : textw_clamp(prev->left->text, n, image_size, bh)) > n) + #else if ((i += (lines > 0) ? bh : textw_clamp(prev->left->text, n)) > n) + #endif // PNG_IMAGES_PATCH break; } @@ -471,6 +514,23 @@ drawitem(struct item *item, int x, int y, int w) else drw_setscheme(drw, scheme[SchemeNorm]); + #if PNG_IMAGES_PATCH + { + int vertical = lines > 0; + if (startswith(image_prefix, text)) { + char *path = 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) { + r = vertical ? y : x; + goto done_drawitem; + } + } + } + #endif // PNG_IMAGES_PATCH + r = drw_text(drw #if EMOJI_HIGHLIGHT_PATCH , x + ((iscomment == 6) ? temppadding : 0) @@ -502,6 +562,9 @@ drawitem(struct item *item, int x, int y, int w) drawhighlights(item, x, y, w); #endif // EMOJI_HIGHLIGHT_PATCH #endif // HIGHLIGHT_PATCH + #if PNG_IMAGES_PATCH +done_drawitem: + #endif // PNG_IMAGES_PATCH return r; } @@ -717,6 +780,19 @@ drawmenu(void) #else /* draw vertical list */ + #if PNG_IMAGES_PATCH + y = bh; + for (item = curr; item != next; item = item->right) { + #if DYNAMIC_HEIGHT_PATCH + i++; + #endif // DYNAMIC_HEIGHT_PATCH + #if VERTFULL_PATCH + y = drawitem(item, 0, y, mw); + #else + y = drawitem(item, x, y, mw - x); + #endif // VERTFULL_PATCH + } + #else for (item = curr; item != next; item = item->right) { #if DYNAMIC_HEIGHT_PATCH i++; @@ -727,6 +803,7 @@ drawmenu(void) drawitem(item, x, y += bh, mw - x); #endif // VERTFULL_PATCH } + #endif // PNG_IMAGES_PATCH #if DYNAMIC_HEIGHT_PATCH XResizeWindow(dpy, win, mw, (i + 1) * bh); #endif // DYNAMIC_HEIGHT_PATCH @@ -759,9 +836,17 @@ drawmenu(void) stw = TEXTW(">"); #endif // SYMBOLS_PATCH #if TSV_PATCH && !SEPARATOR_PATCH - itw = textw_clamp(item->stext, mw - x - stw - rpad); + itw = textw_clamp(item->stext, mw - x - stw - rpad + #if PNG_IMAGES_PATCH + , image_size, bh + #endif // PNG_IMAGES_PATCH + ); #else - itw = textw_clamp(item->text, mw - x - stw - rpad); + itw = textw_clamp(item->text, mw - x - stw - rpad + #if PNG_IMAGES_PATCH + , image_size, bh + #endif // PNG_IMAGES_PATCH + ); #endif // TSV_PATCH x = drawitem(item, x, 0, itw); } @@ -1769,7 +1854,13 @@ setup(void) bh = MAX(bh,lineheight); /* make a menu line AT LEAST 'lineheight' tall */ #endif // LINE_HEIGHT_PATCH lines = MAX(lines, 0); + #if PNG_IMAGES_PATCH + if (image_size < 0) + image_size = (lines > 0) ? 2 * bh : 8 * bh; + mh = bh + ((lines > 0) ? MAX(lines * bh, image_size) : 0); + #else mh = (lines + 1) * bh; + #endif // PNG_IMAGES_PATCH #if CENTER_PATCH && PANGO_PATCH promptw = (prompt && *prompt) ? TEXTWM(prompt) - lrpad / 4 : 0; #elif CENTER_PATCH @@ -1880,7 +1971,11 @@ setup(void) if (numwidthchecks || minstrlen < curstrlen) { numwidthchecks = MAX(numwidthchecks - 1, 0); minstrlen = MAX(minstrlen, curstrlen); - if ((tmp = textw_clamp(item->text, mw/3)) > inputw) { + if ((tmp = textw_clamp(item->text, mw/3 + #if PNG_IMAGES_PATCH + , image_size, bh + #endif // PNG_IMAGES_PATCH + )) > inputw) { inputw = tmp; if (tmp == mw/3) break; @@ -2085,6 +2180,9 @@ usage(void) #if SEPARATOR_PATCH "\n [-d separator] [-D separator]" #endif // SEPARATOR_PATCH + #if PNG_IMAGES_PATCH + "\n [-ip image_prefix] [-is image_size]" + #endif // PNG_IMAGES_PATCH "\n"); } @@ -2351,6 +2449,12 @@ main(int argc, char *argv[]) insert(text, strlen(text)); } #endif // INITIALTEXT_PATCH + #if PNG_IMAGES_PATCH + 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]); + #endif // PNG_IMAGES_PATCH else { usage(); } @@ -2402,6 +2506,9 @@ main(int argc, char *argv[]) #else lrpad = drw->fonts->h; #endif // PANGO_PATCH + #if PNG_IMAGES_PATCH + tbpad = lrpad / 2; + #endif // PNG_IMAGES_PATCH #if BARPADDING_PATCH sp = sidepad; diff --git a/drw.c b/drw.c index 9cfd13827bde668b482a8c2dc080a675d9517be9..bb4c117015ae5f4381d413b5b5e4a3f9aab5fcb5 100644 --- a/drw.c +++ b/drw.c @@ -6,6 +6,11 @@ #include #include "patches.h" + +#if PNG_IMAGES_PATCH +#include +#endif // PNG_IMAGES_PATCH + #include "drw.h" #include "util.h" @@ -647,6 +652,203 @@ no_match: } #endif // PANGO_PATCH +#if PNG_IMAGES_PATCH +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 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; + } + + ctx = spng_ctx_new(0); + if (ctx == NULL) { + fprintf(stderr, "%s: spng_ctx_new() failed\n", path); + fclose(png); + return NULL; + } + + spng_set_crc_action(ctx, SPNG_CRC_USE, SPNG_CRC_USE); + + 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)); + spng_ctx_free(ctx); + fclose(png); + 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)); + spng_ctx_free(ctx); + fclose(png); + return NULL; + } + + size_t image_size, bytes_per_row; + + ret = spng_decoded_image_size(ctx, fmt, &image_size); + if (ret) { + spng_ctx_free(ctx); + fclose(png); + return NULL; + } + + spng_buf = malloc(image_size); + if (!spng_buf) { + spng_ctx_free(ctx); + fclose(png); + 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)); + free(spng_buf); + spng_ctx_free(ctx); + fclose(png); + return NULL; + } + + 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 < (unsigned int)crop_height); + + if (ret != SPNG_EOI && row_info.row_num < (unsigned int)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 (unsigned int i = 0; i < ihdr.width * (unsigned int)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); + free(spng_buf); + 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) +{ + struct image_item *image = NULL; + int render = *x || *y; + int crop_width, crop_height; + + 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, (int)*w); + crop_height = MIN(image->height, (int)*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; +} + +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); +} +#endif // PNG_IMAGES_PATCH + void drw_map(Drw *drw, Window win, int x, int y, unsigned int w, unsigned int h) { diff --git a/drw.h b/drw.h index 3a92d99019da3c629f2b582b1e356e938b4a31b9..b8e5cef73ba2105d7cf20af29948c5829e1c4620 100644 --- a/drw.h +++ b/drw.h @@ -101,6 +101,13 @@ int drw_text(Drw *drw, int x, int y, unsigned int w, unsigned int h, unsigned in int drw_text(Drw *drw, int x, int y, unsigned int w, unsigned int h, unsigned int lpad, const char *text, int invert); #endif // PANGO_PATCH +#if PNG_IMAGES_PATCH +/* 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); +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); +#endif // PNG_IMAGES_PATCH + /* Map functions */ void drw_map(Drw *drw, Window win, int x, int y, unsigned int w, unsigned int h); diff --git a/patches.h b/patches.h index 0a71d42616190573beb13ddeaca5e6738a2ce442..4f8eca6c62e7ee0f0237e22a6e31ea49977ba23e 100644 --- a/patches.h +++ b/patches.h @@ -179,3 +179,6 @@ /* Position and width options. */ #define XYW_PATCH 0 + +/* PNG image preview support using libspng. */ +#define PNG_IMAGES_PATCH 1 diff --git a/util.c b/util.c index 96b82c980c4ce304d329fc60e582379ab0400993..69131b8b58bc7c3472f3091c1bf8888f0ce12876 100644 --- a/util.c +++ b/util.c @@ -4,6 +4,7 @@ #include #include +#include "patches.h" #include "util.h" void @@ -34,3 +35,11 @@ ecalloc(size_t nmemb, size_t size) die("calloc:"); return p; } + +#if PNG_IMAGES_PATCH +int +startswith(const char *prefix, const char *str) +{ + return strncmp(prefix, str, strlen(prefix)) == 0; +} +#endif // PNG_IMAGES_PATCH diff --git a/util.h b/util.h index 2922d7720e05ed3c9003b39e8dccd3dc17eac601..9eb05fc507151c6adf6df9c7354252072bf0d40a 100644 --- a/util.h +++ b/util.h @@ -11,3 +11,6 @@ void die(const char *fmt, ...); void *ecalloc(size_t nmemb, size_t size); +#if PNG_IMAGES_PATCH +int startswith(const char *prefix, const char *str); +#endif // PNG_IMAGES_PATCH