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 | ++ |
A | LICENSE | | | 21 | +++++++++++++++++++++ |
A | Makefile | | | 45 | +++++++++++++++++++++++++++++++++++++++++++++ |
A | README | | | 41 | +++++++++++++++++++++++++++++++++++++++++ |
A | TODO | | | 25 | +++++++++++++++++++++++++ |
A | arg.h | | | 63 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
A | config.mk | | | 27 | +++++++++++++++++++++++++++ |
A | lel-open | | | 20 | ++++++++++++++++++++ |
A | lel.1 | | | 9 | +++++++++ |
A | lel.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;
+}