lel

farbfeld image viewer
git clone git://git.2f30.org/lel
Log | Files | Refs | README | LICENSE

commit 0bcb47ab3451309490d3f4a856b755ea43ff282e
Author: Hiltjo Posthuma <hiltjo@codemadness.org>
Date:   Fri,  1 Aug 2014 22:33:45 +0000

initial repo

Signed-off-by: Hiltjo Posthuma <hiltjo@codemadness.org>

Diffstat:
A.gitignore | 2++
ALICENSE | 21+++++++++++++++++++++
AMakefile | 45+++++++++++++++++++++++++++++++++++++++++++++
AREADME | 41+++++++++++++++++++++++++++++++++++++++++
ATODO | 25+++++++++++++++++++++++++
Aarg.h | 63+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aconfig.mk | 27+++++++++++++++++++++++++++
Alel-open | 20++++++++++++++++++++
Alel.1 | 9+++++++++
Alel.c | 509+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
10 files changed, 762 insertions(+), 0 deletions(-)

diff --git a/.gitignore b/.gitignore @@ -0,0 +1,2 @@ +lel +*.o diff --git a/LICENSE b/LICENSE @@ -0,0 +1,21 @@ +MIT/X Consortium License + +(c) 2014 Hiltjo Posthuma <hiltjo@codemadness.org> + +Permission is hereby granted, free of charge, to any person obtaining a +copy of this software and associated documentation files (the "Software"), +to deal in the Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, sublicense, +and/or sell copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. diff --git a/Makefile b/Makefile @@ -0,0 +1,45 @@ +include config.mk + +NAME = lel +SRC = lel.c +OBJ = ${SRC:.c=.o} + +all: lel + +options: + @echo ${NAME} build options: + @echo "CFLAGS = ${CFLAGS}" + @echo "LDFLAGS = ${LDFLAGS}" + @echo "CC = ${CC}" + +.c.o: + ${CC} -c ${CFLAGS} $< + +${OBJ}: config.mk + +lel: lel.o + ${CC} -o $@ lel.o ${LDFLAGS} + +clean: + rm -f lel ${OBJ} + +install: all + @echo installing executable file to ${DESTDIR}${PREFIX}/bin + @mkdir -p ${DESTDIR}${PREFIX}/bin + @cp -f lel ${DESTDIR}${PREFIX}/bin + @cp -f lel-open ${DESTDIR}${PREFIX}/bin + @chmod 755 ${DESTDIR}${PREFIX}/bin/lel + @chmod 755 ${DESTDIR}${PREFIX}/bin/lel-open + @echo installing manual pages to ${DESTDIR}${MANPREFIX}/man1 + @mkdir -p ${DESTDIR}${MANPREFIX}/man1 + @cp -f lel.1 ${DESTDIR}${MANPREFIX}/man1 + @chmod 644 ${DESTDIR}${MANPREFIX}/man1/lel.1 + +uninstall: + @echo removing executable file from ${DESTDIR}${PREFIX}/bin + @rm -f ${DESTDIR}${PREFIX}/bin/lel + @rm -f ${DESTDIR}${PREFIX}/bin/lel-open + @echo removing manual pages from ${DESTDIR}${MANPREFIX}/man1 + @rm -f ${DESTDIR}${MANPREFIX}/man1/lel.1 + +.PHONY: all options clean dist install uninstall diff --git a/README b/README @@ -0,0 +1,41 @@ +lel - simple X11 image viewer +============================= + +lel is a simple X11 image viewer. It reads image data (in the "imagefile" +format from stdout) and displays it in a X11 window. + + +Features +-------- + +- view modes: + - original image size. + - stretch image to window, keep aspect ratio. + - stretch image to window, don't keep aspect ratio. +- keybinds +- zooming +- panning + + +Dependencies +------------ + +- libX11 + + +Optional (run-time) dependencies +-------------------------------- +- imagefile: http://git.2f30.org/imagefile/ + + +Compile: +-------- + +$ make +# make install + + +Known issues: +------------- + +See TODO file. diff --git a/TODO b/TODO @@ -0,0 +1,25 @@ +[ ] bugs: + [ ] alpha mask with png2if | lel is always black? + [ ] resizing slow (might be a dwm issue). +[ ] use XSHM again? +[ ] enter key or some other key should print the current filename to stdout. +[ ] for pictures which use an alpha mask use a checked pattern, similar to: + http://www.modejong.com/blog/Media/Ghost_TransparentBG_400x300.jpg +[ ] support multiple filenames as arguments, use hotkeys to switch image. +[ ] zooming + [ ] improve performance (cull only the visible part?), use XGetImage() ? + [ ] improve zoomfact "steps". +[ ] mouse support. + [ ] mouse move panning. + [x] mouse scroll zooming. +[ ] scale + [ ] use XPutPixel for byte-order safety ? +[ ] improve README. +[ ] write man page. + [ ] lel.1 + [ ] lel-open.1 +[?] improve key lookup code. +[?] project name change. +[?] add support for more color depths (< 24). +[?] add command mode or shell support for viewing + images in a directory (output key to stdout might be enough?). diff --git a/arg.h b/arg.h @@ -0,0 +1,63 @@ +/* + * Copy me if you can. + * by 20h + */ + +#ifndef ARG_H__ +#define ARG_H__ + +extern char *argv0; + +/* use main(int argc, char *argv[]) */ +#define ARGBEGIN for (argv0 = *argv, argv++, argc--;\ + argv[0] && argv[0][1]\ + && argv[0][0] == '-';\ + argc--, argv++) {\ + char argc_;\ + char **argv_;\ + int brk_;\ + if (argv[0][1] == '-' && argv[0][2] == '\0') {\ + argv++;\ + argc--;\ + break;\ + }\ + for (brk_ = 0, argv[0]++, argv_ = argv;\ + argv[0][0] && !brk_;\ + argv[0]++) {\ + if (argv_ != argv)\ + break;\ + argc_ = argv[0][0];\ + switch (argc_) + +/* Handles obsolete -NUM syntax */ +#define ARGNUM case '0':\ + case '1':\ + case '2':\ + case '3':\ + case '4':\ + case '5':\ + case '6':\ + case '7':\ + case '8':\ + case '9' + +#define ARGEND }\ + } + +#define ARGC() argc_ + +#define ARGNUMF(base) (brk_ = 1, estrtol(argv[0], (base))) + +#define EARGF(x) ((argv[0][1] == '\0' && argv[1] == NULL)?\ + ((x), abort(), (char *)0) :\ + (brk_ = 1, (argv[0][1] != '\0')?\ + (&argv[0][1]) :\ + (argc--, argv++, argv[0]))) + +#define ARGF() ((argv[0][1] == '\0' && argv[1] == NULL)?\ + (char *)0 :\ + (brk_ = 1, (argv[0][1] != '\0')?\ + (&argv[0][1]) :\ + (argc--, argv++, argv[0]))) + +#endif diff --git a/config.mk b/config.mk @@ -0,0 +1,27 @@ +VERSION = 0.1 + +# customize below to fit your system + +# paths +PREFIX = /usr/local +MANPREFIX = ${PREFIX}/share/man + +# includes and libs +INCS = +LIBS = -lc -lX11 + +# debug +CFLAGS = -O0 -g -std=c99 -Wall -pedantic -DVERSION=\"${VERSION}\" +LDFLAGS = ${LIBS} + +# optimized +#CFLAGS = -O2 -std=c99 -DVERSION=\"${VERSION}\" +#LDFLAGS = -s ${LIBS} + +# tcc +#CC = tcc +#CFLAGS = -DVERSION=\"${VERSION}\" +#LDFLAGS = -s ${LIBS} + +# compiler and linker +CC = cc diff --git a/lel-open b/lel-open @@ -0,0 +1,20 @@ +#!/bin/sh +filename="$1" +shift +ext=$(basename "$filename" | grep -o '\..[^\.]*$') + +if test x"$ext" = x".jpg"; then + convert="jpg2if" +elif test x"$ext" = x".png"; then + convert="png2if" +elif test x"$ext" = x".gif"; then + convert="gif2if" +elif test x"$ext" = x".if"; then + convert="cat" +else + echo "unknown extension \"$ext\"" >&2 +fi + +if test x"$convert" != x""; then + "$convert" < "$filename" | lel -t "$filename" "$@" +fi diff --git a/lel.1 b/lel.1 @@ -0,0 +1,9 @@ +.TH LEL 1 lel\-0.1 +.SH NAME +lel \- simple X11 image viewer +.SH SYNOPSIS +.B lel +.SH DESCRIPTION +View an "imagefile" image. +.SH BUGS +Please report them! diff --git a/lel.c b/lel.c @@ -0,0 +1,509 @@ +#include <unistd.h> +#include <stdarg.h> +#include <stdint.h> +#include <stdlib.h> +#include <stdio.h> +#include <string.h> +#include <errno.h> +#include <signal.h> +#include <time.h> +#include <arpa/inet.h> + +#include <X11/Xlib.h> +#include <X11/Xutil.h> +#include <X11/keysym.h> + +#include "arg.h" +char *argv0; + +#define APP_NAME "lel" +#define HEADER_FORMAT "imagefile########" + +/* Image status flags. */ +enum { NONE = 0, LOADED = 1, SCALED = 2, DRAWN = 4 }; +/* View mode. */ +enum { ASPECT = 0, FULL_ASPECT, FULL_STRETCH }; + +static int viewmode = ASPECT; +static char *wintitle = APP_NAME; +static XImage *ximg = NULL; +static Drawable xpix = 0; +static Display *dpy = NULL; +static Window win; +static GC gc; +static int screen, xfd; +static int running = 1; +static int imgstate = NONE; +static int imgwidth, imgheight; +static uint8_t *imgbuf; +static int winx, winy, winwidth = 320, winheight = 240; +static int panxoffset = 0, panyoffset = 0; +static float zoomfact = 1.0, zoominc = 0.25; + +void +die(const char *fmt, ...) +{ + va_list ap; + + va_start(ap, fmt); + vfprintf(stderr, fmt, ap); + va_end(ap); + + if(fmt[0] && fmt[strlen(fmt)-1] == ':') { + fputc(' ', stderr); + perror(NULL); + } + exit(EXIT_FAILURE); +} + +void +usage(void) +{ + die("%s", APP_NAME " " VERSION " - (c) 2014 " APP_NAME " engineers\n\n" + "usage: " APP_NAME "[OPTIONS...] [FILE]\n" + " -a Full window, keep aspect ratio\n" + " -f Full window, stretch (no aspect)\n" + " -w <w> Window width\n" + " -h <h> Window height\n" + " -x <x> Window x position\n" + " -y <y> Window y position\n" + " -t <title> Use title\n" + " -v Print version and exit\n"); +} + +int +if_open(FILE *f) +{ + uint8_t hdr[17]; + + if (fread(hdr, 1, strlen(HEADER_FORMAT), f) != strlen(HEADER_FORMAT)) + return -1; + + if(memcmp(hdr, "imagefile", 9)) + return -1; + + imgwidth = ntohl((hdr[9] << 0) | (hdr[10] << 8) | (hdr[11] << 16) | (hdr[12] << 24)); + imgheight = ntohl((hdr[13] << 0) | (hdr[14] << 8) | (hdr[15] << 16) | (hdr[16] << 24)); + if(imgwidth <= 0 || imgheight <= 0) + return -1; + + return 0; +} + +int +if_read(FILE *f) +{ + int i, j, off, row_len; + uint8_t *row; + + row_len = imgwidth * strlen("RGBA"); + if(!(row = malloc(row_len))) + return 1; + + for(off = 0, i = 0; i < imgheight; ++i) { + if(fread(row, 1, (size_t)row_len, f) != (size_t)row_len) { + free(row); + die("unexpected EOF or row-skew at %d\n", i); + } + for(j = 0; j < row_len; j += 4, off += 4) { + imgbuf[off] = row[j]; + imgbuf[off + 1] = row[j + 1]; + imgbuf[off + 2] = row[j + 2]; + imgbuf[off + 3] = row[j + 3]; + } + } + free(row); + + imgstate |= LOADED; + + return 0; +} + +/* NOTE: will be removed later, for debugging alpha mask */ +#if 0 +void +normalsize(char *newbuf) +{ + unsigned int x, y, soff = 0, doff = 0; + + for(y = 0; y < imgheight; y++) { + for(x = 0; x < imgwidth; x++, soff += 4, doff += 4) { + newbuf[doff+0] = imgbuf[soff+2]; + newbuf[doff+1] = imgbuf[soff+1]; + newbuf[doff+2] = imgbuf[soff+0]; + newbuf[doff+3] = imgbuf[soff+3]; + } + } +} +#endif + +/* scales imgbuf data to newbuf (ximg->data), nearest neighbour. */ +void +scale(unsigned int width, unsigned int height, unsigned int bytesperline, + char *newbuf) +{ + unsigned char *ibuf; + unsigned int jdy, dx, bufx, x, y; + + jdy = bytesperline / 4 - width; + dx = (imgwidth << 10) / width; + for(y = 0; y < height; y++) { + bufx = imgwidth / width; + ibuf = &imgbuf[y * imgheight / height * imgwidth * 4]; + + for(x = 0; x < width; x++) { + *newbuf++ = (ibuf[(bufx >> 10)*4+2]); + *newbuf++ = (ibuf[(bufx >> 10)*4+1]); + *newbuf++ = (ibuf[(bufx >> 10)*4+0]); + newbuf++; + bufx += dx; + } + newbuf += jdy; + } +} + +void +ximage(unsigned int newwidth, unsigned int newheight) +{ + int depth; + + /* destroy previous image */ + if(ximg) { + XDestroyImage(ximg); + ximg = NULL; + } + depth = DefaultDepth(dpy, screen); + if(depth >= 24) { + if(xpix) + XFreePixmap(dpy, xpix); + xpix = XCreatePixmap(dpy, win, winwidth, winheight, depth); + ximg = XCreateImage(dpy, CopyFromParent, depth, ZPixmap, 0, + NULL, newwidth, newheight, 32, 0); + ximg->data = malloc(ximg->bytes_per_line * ximg->height); + scale(ximg->width, ximg->height, ximg->bytes_per_line, ximg->data); + XInitImage(ximg); + } else { + die("This program does not yet support display depths < 24.\n"); + } +} + +void +scaleview(void) +{ + switch(viewmode) { + case FULL_STRETCH: + ximage(winwidth, winheight); + break; + case FULL_ASPECT: + if(winwidth * imgheight > winheight * imgwidth) + ximage(imgwidth * winheight / imgheight, winheight); + else + ximage(winwidth, imgheight * winwidth / imgwidth); + break; + case ASPECT: + default: + ximage(imgwidth * zoomfact, imgheight * zoomfact); + break; + } + imgstate |= SCALED; +} + +void +draw(void) +{ + int xoffset = 0, yoffset = 0; + + if(viewmode != FULL_STRETCH) { + /* center vertical, horizontal */ + xoffset = (winwidth - ximg->width) / 2; + yoffset = (winheight - ximg->height) / 2; + /* pan offset */ + xoffset -= panxoffset; + yoffset -= panyoffset; + } + XSetForeground(dpy, gc, BlackPixel(dpy, 0)); + XFillRectangle(dpy, xpix, gc, 0, 0, winwidth, winheight); + XPutImage(dpy, xpix, gc, ximg, 0, 0, xoffset, yoffset, ximg->width, ximg->height); + XCopyArea(dpy, xpix, win, gc, 0, 0, winwidth, winheight, 0, 0); + + XFlush(dpy); + imgstate |= DRAWN; +} + +void +update(void) +{ + if(!(imgstate & LOADED)) + return; + if(!(imgstate & SCALED)) + scaleview(); + if(!(imgstate & DRAWN)) + draw(); +} + +void +setview(int mode) +{ + if(viewmode == mode) + return; + viewmode = mode; + imgstate &= ~(DRAWN | SCALED); + update(); +} + +void +pan(int x, int y) +{ + panxoffset -= x; + panyoffset -= y; + imgstate &= ~(DRAWN | SCALED); + update(); +} + +void +inczoom(float f) +{ + if((zoomfact + f) <= 0) + return; + zoomfact += f; + imgstate &= ~(DRAWN | SCALED); + update(); +} + +void +zoom(float f) +{ + if(f == zoomfact) + return; + zoomfact = f; + imgstate &= ~(DRAWN | SCALED); + update(); +} + +void +buttonpress(XEvent *ev) +{ + switch(ev->xbutton.button) { + case Button4: + inczoom(zoominc); + break; + case Button5: + inczoom(-zoominc); + break; + } +} + +void +keypress(XEvent *ev) +{ + KeySym key; + + key = XLookupKeysym(&ev->xkey, 0); + switch(key) { + case XK_Escape: + case XK_q: + running = 0; + break; + case XK_Left: + case XK_h: + pan(winwidth / 20, 0); + break; + case XK_Down: + case XK_j: + pan(0, -(winheight / 20)); + break; + case XK_Up: + case XK_k: + pan(0, winheight / 20); + break; + case XK_Right: + case XK_l: + pan(-(winwidth / 20), 0); + break; + case XK_a: + setview(FULL_ASPECT); + break; + case XK_o: + setview(ASPECT); + break; + case XK_f: + setview(FULL_STRETCH); + break; + case XK_KP_Add: + case XK_equal: + case XK_plus: + inczoom(zoominc); + break; + case XK_KP_Subtract: + case XK_underscore: + case XK_minus: + inczoom(-zoominc); + break; + case XK_3: + zoom(4.0); + break; + case XK_2: + zoom(2.0); + break; + case XK_1: + zoom(1.0); + break; + case XK_0: + zoom(1.0); + setview(ASPECT); /* fallthrough */ + case XK_r: + panxoffset = 0; + panyoffset = 0; + imgstate &= ~(DRAWN | SCALED); + update(); + break; + } +} + +void +handleevent(XEvent *ev) +{ + XWindowAttributes attr; + + switch(ev->type) { + case MapNotify: + if (!winwidth || !winheight) { + XGetWindowAttributes(ev->xmap.display, ev->xmap.window, &attr); + winwidth = attr.width; + winheight = attr.height; + } + break; + case ConfigureNotify: + if(winwidth != ev->xconfigure.width || winheight != ev->xconfigure.height) { + winwidth = ev->xconfigure.width; + winheight = ev->xconfigure.height; + imgstate &= ~(SCALED); + } + break; + case Expose: + imgstate &= ~(DRAWN); + update(); + break; + case KeyPress: + keypress(ev); + break; + case ButtonPress: + buttonpress(ev); + break; + } +} + +void +setup(void) +{ + XClassHint class = { APP_NAME, APP_NAME }; + + if(!(dpy = XOpenDisplay(NULL))) + die("Can't open X display.\n"); + xfd = ConnectionNumber(dpy); + screen = DefaultScreen(dpy); + + win = XCreateWindow(dpy, DefaultRootWindow(dpy), winx, winy, winwidth, winheight, 0, + DefaultDepth(dpy, screen), InputOutput, + CopyFromParent, 0, NULL); + gc = XCreateGC(dpy, win, 0, NULL); + + XStoreName(dpy, win, wintitle); + XSelectInput(dpy, win, StructureNotifyMask | ExposureMask | KeyPressMask | + ButtonPressMask); + XMapRaised(dpy, win); + XSetWMProperties(dpy, win, NULL, NULL, NULL, 0, NULL, NULL, &class); + XFlush(dpy); +} + +void +run(void) +{ + XEvent ev; + + while(running && !XNextEvent(dpy, &ev)) { + handleevent(&ev); + } +} + +int +main(int argc, char *argv[]) { + char *filename = ""; + FILE *fp = NULL; + int tflag = 0; + int wflag = 0; + int hflag = 0; + + ARGBEGIN { + case 'a': + viewmode = FULL_ASPECT; + break; + case 'f': + viewmode = FULL_STRETCH; + break; + case 'h': + hflag = 1; + if(!(winheight = atoi(EARGF(usage())))) + usage(); + break; + case 't': + wintitle = EARGF(usage()); + tflag = 1; + break; + case 'w': + wflag = 1; + if(!(winwidth = atoi(EARGF(usage())))) + usage(); + break; + case 'x': + winx = atoi(EARGF(usage())); + break; + case 'y': + winy = atoi(EARGF(usage())); + break; + default: + usage(); + break; + } ARGEND; + + if(argc >= 1) { + filename = argv[0]; + if(!(fp = fopen(filename, "rb"))) { + die("can't read %s:", filename); + return EXIT_FAILURE; + } + } else { + filename = "<stdin>"; + fp = stdin; + } + if(!tflag) + wintitle = filename; + + if(if_open(fp)) + die("can't open image (invalid format?)\n"); + if(!(imgbuf = malloc((imgwidth) * (imgheight) * 4))) + die("can't malloc\n"); + if_read(fp); + + if(!wflag) + winwidth = imgwidth; + if(!hflag) + winheight = imgheight; + + setup(); + run(); + + if(fp && fp != stdin) + fclose(fp); + + free(imgbuf); + + if(ximg) + XDestroyImage(ximg); + if(xpix) + XFreePixmap(dpy, xpix); + if(dpy) + XCloseDisplay(dpy); + + return EXIT_SUCCESS; +}