placeholder; does not yet build
diff --git a/src/cmd/page/filter.c b/src/cmd/page/filter.c
new file mode 100644
index 0000000..07c3df2
--- /dev/null
+++ b/src/cmd/page/filter.c
@@ -0,0 +1,107 @@
+#include <u.h>
+#include <libc.h>
+#include <draw.h>
+#include <event.h>
+#include <bio.h>
+#include "page.h"
+
+Document*
+initfilt(Biobuf *b, int argc, char **argv, uchar *buf, int nbuf, char *type, char *cmd, int docopy)
+{
+	int ofd;
+	int p[2];
+	char xbuf[8192];
+	int n;
+
+	if(argc > 1) {
+		fprint(2, "can only view one %s file at a time\n", type);
+		return nil;
+	}
+
+	fprint(2, "converting from %s to postscript...\n", type);
+
+	if(docopy){
+		if(pipe(p) < 0){
+			fprint(2, "pipe fails: %r\n");
+			exits("Epipe");
+		}
+	}else{
+		p[0] = open("/dev/null", ORDWR);
+		p[1] = open("/dev/null", ORDWR);
+	}
+
+	ofd = opentemp("/tmp/pagecvtXXXXXXXXX");
+	switch(fork()){
+	case -1:
+		fprint(2, "fork fails: %r\n");
+		exits("Efork");
+	default:
+		close(p[1]);
+		if(docopy){
+			write(p[0], buf, nbuf);
+			if(b)
+				while((n = Bread(b, xbuf, sizeof xbuf)) > 0)
+					write(p[0], xbuf, n);
+			else
+				while((n = read(stdinfd, xbuf, sizeof xbuf)) > 0)
+					write(p[0], xbuf, n);
+		}
+		close(p[0]);
+		waitpid();
+		break;
+	case 0:
+		close(p[0]);
+		dup(p[1], 0);
+		dup(ofd, 1);
+		/* stderr shines through */
+		execl("/bin/rc", "rc", "-c", cmd, nil);
+		break;
+	}
+
+	if(b)
+		Bterm(b);
+	seek(ofd, 0, 0);
+	b = emalloc(sizeof(Biobuf));
+	Binit(b, ofd, OREAD);
+
+	return initps(b, argc, argv, nil, 0);
+}
+
+Document*
+initdvi(Biobuf *b, int argc, char **argv, uchar *buf, int nbuf)
+{
+	int fd;
+	char *name;
+	char cmd[256];
+	char fdbuf[20];
+
+	/*
+	 * Stupid DVIPS won't take standard input.
+	 */
+	if(b == nil){	/* standard input; spool to disk (ouch) */
+		fd = spooltodisk(buf, nbuf, &name);
+		sprint(fdbuf, "/fd/%d", fd);
+		b = Bopen(fdbuf, OREAD);
+		if(b == nil){
+			fprint(2, "cannot open disk spool file\n");
+			wexits("Bopen temp");
+		}
+		argv = &name;
+		argc = 1;
+	}
+
+	snprint(cmd, sizeof cmd, "dvips -Pps -r0 -q1 -f1 '%s'", argv[0]);
+	return initfilt(b, argc, argv, buf, nbuf, "dvi", cmd, 0);
+}
+
+Document*
+inittroff(Biobuf *b, int argc, char **argv, uchar *buf, int nbuf)
+{
+	return initfilt(b, argc, argv, buf, nbuf, "troff", "lp -dstdout", 1);
+}
+
+Document*
+initmsdoc(Biobuf *b, int argc, char **argv, uchar *buf, int nbuf)
+{
+	return initfilt(b, argc, argv, buf, nbuf, "microsoft office", "doc2ps", 1);
+}
diff --git a/src/cmd/page/gfx.c b/src/cmd/page/gfx.c
new file mode 100644
index 0000000..72254de
--- /dev/null
+++ b/src/cmd/page/gfx.c
@@ -0,0 +1,331 @@
+/*
+ * graphics file reading for page
+ */
+
+#include <u.h>
+#include <libc.h>
+#include <draw.h>
+#include <event.h>
+#include <bio.h>
+#include "page.h"
+
+typedef struct Convert	Convert;
+typedef struct GfxInfo	GfxInfo;
+typedef struct Graphic	Graphic;
+
+struct Convert {
+	char *name;
+	char *cmd;
+	char *truecmd;	/* cmd for true color */
+};
+
+struct GfxInfo {
+	Graphic *g;
+};
+
+struct Graphic {
+	int type;
+	char *name;
+	uchar *buf;	/* if stdin */
+	int nbuf;
+};
+
+enum {
+	Ipic,
+	Itiff,
+	Ijpeg,
+	Igif,
+	Iinferno,
+	Ifax,
+	Icvt2pic,
+	Iplan9bm,
+	Iccittg4,
+	Ippm,
+	Ipng,
+	Iyuv,
+	Ibmp,
+};
+
+/*
+ * N.B. These commands need to read stdin if %a is replaced
+ * with an empty string.
+ */
+Convert cvt[] = {
+[Ipic]		{ "plan9",	"fb/3to1 rgbv %a |fb/pcp -tplan9" },
+[Itiff]		{ "tiff",	"fb/tiff2pic %a | fb/3to1 rgbv | fb/pcp -tplan9" },
+[Iplan9bm]	{ "plan9bm",	nil },
+[Ijpeg]		{ "jpeg",	"jpg -9 %a", "jpg -t9 %a" },
+[Igif]		{ "gif",	"gif -9 %a", "gif -t9 %a" },
+[Iinferno]	{ "inferno",	nil },
+[Ifax]		{ "fax",	"aux/g3p9bit -g %a" },
+[Icvt2pic]	{ "unknown",	"fb/cvt2pic %a |fb/3to1 rgbv" },
+[Ippm]		{ "ppm",	"ppm -9 %a", "ppm -t9 %a" },
+/* ``temporary'' hack for hobby */
+[Iccittg4]	{ "ccitt-g4",	"cat %a|rx nslocum /usr/lib/ocr/bin/bcp -M|fb/pcp -tcompressed -l0" },
+[Ipng]		{ "png",	"png -9 %a", "png -t9 %a" },
+[Iyuv]		{ "yuv",	"yuv -9 %a", "yuv -t9 %a"  },
+[Ibmp]		{ "bmp",	"bmp -9 %a", "bmp -t9 %a"  },
+};
+
+static Image*	convert(Graphic*);
+static Image*	gfxdrawpage(Document *d, int page);
+static char*	gfxpagename(Document*, int);
+static int	spawnrc(char*, uchar*, int);
+static int	addpage(Document*, char*);
+static int	rmpage(Document*, int);
+static int	genaddpage(Document*, char*, uchar*, int);
+
+static char*
+gfxpagename(Document *doc, int page)
+{
+	GfxInfo *gfx = doc->extra;
+	return gfx->g[page].name;
+}
+
+static Image*
+gfxdrawpage(Document *doc, int page)
+{
+	GfxInfo *gfx = doc->extra;
+	return convert(gfx->g+page);
+}
+
+Document*
+initgfx(Biobuf *b, int argc, char **argv, uchar *buf, int nbuf)
+{
+	GfxInfo *gfx;
+	Document *doc;
+	int i;
+
+	USED(b);
+	doc = emalloc(sizeof(*doc));
+	gfx = emalloc(sizeof(*gfx));
+	gfx->g = nil;
+	
+	doc->npage = 0;
+	doc->drawpage = gfxdrawpage;
+	doc->pagename = gfxpagename;
+	doc->addpage = addpage;
+	doc->rmpage = rmpage;
+	doc->extra = gfx;
+	doc->fwdonly = 0;
+
+	fprint(2, "reading through graphics...\n");
+	if(argc==0 && buf)
+		genaddpage(doc, nil, buf, nbuf);
+	else{
+		for(i=0; i<argc; i++)
+			if(addpage(doc, argv[i]) < 0)
+				fprint(2, "warning: not including %s: %r\n", argv[i]);
+	}
+
+	return doc;
+}
+
+static int
+genaddpage(Document *doc, char *name, uchar *buf, int nbuf)
+{
+	Graphic *g;
+	GfxInfo *gfx;
+	Biobuf *b;
+	uchar xbuf[32];
+	int i, l;
+
+	l = 0;
+	gfx = doc->extra;
+
+	assert((name == nil) ^ (buf == nil));
+	assert(name != nil || doc->npage == 0);
+
+	for(i=0; i<doc->npage; i++)
+		if(strcmp(gfx->g[i].name, name) == 0)
+			return i;
+
+	if(name){
+		l = strlen(name);
+		if((b = Bopen(name, OREAD)) == nil) {
+			werrstr("Bopen: %r");
+			return -1;
+		}
+
+		if(Bread(b, xbuf, sizeof xbuf) != sizeof xbuf) {
+			werrstr("short read: %r");
+			return -1;
+		}
+		Bterm(b);
+		buf = xbuf;
+		nbuf = sizeof xbuf;
+	}
+
+
+	gfx->g = erealloc(gfx->g, (doc->npage+1)*(sizeof(*gfx->g)));
+	g = &gfx->g[doc->npage];
+
+	memset(g, 0, sizeof *g);
+	if(memcmp(buf, "GIF", 3) == 0)
+		g->type = Igif;
+	else if(memcmp(buf, "\111\111\052\000", 4) == 0) 
+		g->type = Itiff;
+	else if(memcmp(buf, "\115\115\000\052", 4) == 0)
+		g->type = Itiff;
+	else if(memcmp(buf, "\377\330\377", 3) == 0)
+		g->type = Ijpeg;
+	else if(memcmp(buf, "\211PNG\r\n\032\n", 3) == 0)
+		g->type = Ipng;
+	else if(memcmp(buf, "compressed\n", 11) == 0)
+		g->type = Iinferno;
+	else if(memcmp(buf, "\0PC Research, Inc", 17) == 0)
+		g->type = Ifax;
+	else if(memcmp(buf, "TYPE=ccitt-g31", 14) == 0)
+		g->type = Ifax;
+	else if(memcmp(buf, "II*", 3) == 0)
+		g->type = Ifax;
+	else if(memcmp(buf, "TYPE=ccitt-g4", 13) == 0)
+		g->type = Iccittg4;
+	else if(memcmp(buf, "TYPE=", 5) == 0)
+		g->type = Ipic;
+	else if(buf[0] == 'P' && '0' <= buf[1] && buf[1] <= '9')
+		g->type = Ippm;
+	else if(memcmp(buf, "BM", 2) == 0)
+		g->type = Ibmp;
+	else if(memcmp(buf, "          ", 10) == 0 &&
+		'0' <= buf[10] && buf[10] <= '9' &&
+		buf[11] == ' ')
+		g->type = Iplan9bm;
+	else if(strtochan((char*)buf) != 0)
+		g->type = Iplan9bm;
+	else if (l > 4 && strcmp(name + l -4, ".yuv") == 0)
+		g->type = Iyuv;
+	else
+		g->type = Icvt2pic;
+
+	if(name)
+		g->name = estrdup(name);
+	else{
+		g->name = estrdup("stdin");	/* so it can be freed */
+		g->buf = buf;
+		g->nbuf = nbuf;
+	}
+
+	if(chatty) fprint(2, "classified \"%s\" as \"%s\"\n", g->name, cvt[g->type].name);
+	return doc->npage++;
+}
+
+static int 
+addpage(Document *doc, char *name)
+{
+	return genaddpage(doc, name, nil, 0);
+}
+
+static int
+rmpage(Document *doc, int n)
+{
+	int i;
+	GfxInfo *gfx;
+
+	if(n < 0 || n >= doc->npage)
+		return -1;
+
+	gfx = doc->extra;
+	doc->npage--;
+	free(gfx->g[n].name);
+
+	for(i=n; i<doc->npage; i++)
+		gfx->g[i] = gfx->g[i+1];
+
+	if(n < doc->npage)
+		return n;
+	if(n == 0)
+		return 0;
+	return n-1;
+}
+
+
+static Image*
+convert(Graphic *g)
+{
+	int fd;
+	Convert c;
+	char *cmd;
+	char *name, buf[1000];
+	Image *im;
+	int rcspawned = 0;
+	Waitmsg *w;
+
+	c = cvt[g->type];
+	if(c.cmd == nil) {
+		if(chatty) fprint(2, "no conversion for bitmap \"%s\"...\n", g->name);
+		if(g->buf == nil){	/* not stdin */
+			fd = open(g->name, OREAD);
+			if(fd < 0) {
+				fprint(2, "cannot open file: %r\n");
+				wexits("open");
+			}
+		}else
+			fd = stdinpipe(g->buf, g->nbuf);	
+	} else {
+		cmd = c.cmd;
+		if(truecolor && c.truecmd)
+			cmd = c.truecmd;
+
+		if(g->buf != nil)	/* is stdin */
+			name = "";
+		else
+			name = g->name;
+		if(strlen(cmd)+strlen(name) > sizeof buf) {
+			fprint(2, "command too long\n");
+			wexits("convert");
+		}
+		snprint(buf, sizeof buf, cmd, name);
+		if(chatty) fprint(2, "using \"%s\" to convert \"%s\"...\n", buf, g->name);
+		fd = spawnrc(buf, g->buf, g->nbuf);
+		rcspawned++;
+		if(fd < 0) {
+			fprint(2, "cannot spawn converter: %r\n");
+			wexits("convert");
+		}	
+	}
+
+	im = readimage(display, fd, 0);
+	if(im == nil) {
+		fprint(2, "warning: couldn't read image: %r\n");
+	}
+	close(fd);
+
+	/* for some reason rx doesn't work well with wait */
+	/* for some reason 3to1 exits on success with a non-null status of |3to1 */
+	if(rcspawned && g->type != Iccittg4) {
+		if((w=wait())!=nil && w->msg[0] && !strstr(w->msg, "3to1"))
+			fprint(2, "slave wait error: %s\n", w->msg);
+		free(w);
+	}
+	return im;
+}
+
+static int
+spawnrc(char *cmd, uchar *stdinbuf, int nstdinbuf)
+{
+	int pfd[2];
+	int pid;
+
+	if(chatty) fprint(2, "spawning(%s)...", cmd);
+
+	if(pipe(pfd) < 0)
+		return -1;
+	if((pid = fork()) < 0)
+		return -1;
+
+	if(pid == 0) {
+		close(pfd[1]);
+		if(stdinbuf)
+			dup(stdinpipe(stdinbuf, nstdinbuf), 0);
+		else
+			dup(open("/dev/null", OREAD), 0);
+		dup(pfd[0], 1);
+		//dup(pfd[0], 2);
+		execl("/bin/rc", "rc", "-c", cmd, nil);
+		wexits("exec");
+	}
+	close(pfd[0]);
+	return pfd[1];
+}
+
diff --git a/src/cmd/page/gs.c b/src/cmd/page/gs.c
new file mode 100644
index 0000000..524701e
--- /dev/null
+++ b/src/cmd/page/gs.c
@@ -0,0 +1,342 @@
+/*
+ * gs interface for page.
+ * ps.c and pdf.c both use these routines.
+ * a caveat: if you run more than one gs, only the last 
+ * one gets killed by killgs 
+ */
+#include <u.h>
+#include <libc.h>
+#include <draw.h>
+#include <event.h>
+#include <bio.h>
+#include "page.h"
+
+static int gspid;	/* globals for atexit */
+static int gsfd;
+static void	killgs(void);
+
+static void
+killgs(void)
+{
+	char tmpfile[100];
+
+	close(gsfd);
+	postnote(PNGROUP, getpid(), "die");
+
+	/*
+	 * from ghostscript's use.txt:
+	 * ``Ghostscript currently doesn't do a very good job of deleting temporary
+	 * files when it exits; you may have to delete them manually from time to
+	 * time.''
+	 */
+	sprint(tmpfile, "/tmp/gs_%.5da", (gspid+300000)%100000);
+	if(chatty) fprint(2, "remove %s...\n", tmpfile);
+	remove(tmpfile);
+	sleep(100);
+	postnote(PNPROC, gspid, "die yankee pig dog");
+}
+
+int
+spawnwriter(GSInfo *g, Biobuf *b)
+{
+	char buf[4096];
+	int n;
+	int fd;
+
+	switch(fork()){
+	case -1:	return -1;
+	case 0:	break;
+	default:	return 0;
+	}
+
+	Bseek(b, 0, 0);
+	fd = g->gsfd;
+	while((n = Bread(b, buf, sizeof buf)) > 0)
+		write(fd, buf, n);
+	fprint(fd, "(/fd/3) (w) file dup (THIS IS NOT AN INFERNO BITMAP\\n) writestring flushfile\n");
+	_exits(0);
+	return -1;
+}
+
+int
+spawnreader(int fd)
+{
+	int n, pfd[2];
+	char buf[1024];
+
+	if(pipe(pfd)<0)
+		return -1;
+	switch(fork()){
+	case -1:
+		return -1;
+	case 0:
+		break;
+	default:
+		close(pfd[0]);
+		return pfd[1];
+	}
+
+	close(pfd[1]);
+	switch(fork()){
+	case -1:
+		wexits("fork failed");
+	case 0:
+		while((n=read(fd, buf, sizeof buf)) > 0) {
+			write(1, buf, n);
+			write(pfd[0], buf, n);
+		}
+		break;
+	default:
+		while((n=read(pfd[0], buf, sizeof buf)) > 0) {
+			write(1, buf, n);
+			write(fd, buf, n);
+		}
+		break;
+	}
+	postnote(PNGROUP, getpid(), "i'm die-ing");
+	_exits(0);
+	return -1;
+}
+
+void
+spawnmonitor(int fd)
+{
+	char buf[4096];
+	char *xbuf;
+	int n;
+	int out;
+	int first;
+
+	switch(rfork(RFFDG|RFNOTEG|RFPROC)){
+	case -1:
+	default:
+		return;
+
+	case 0:
+		break;
+	}
+
+	out = open("/dev/cons", OWRITE);
+	if(out < 0)
+		out = 2;
+
+	xbuf = buf;	/* for ease of acid */
+	first = 1;
+	while((n = read(fd, xbuf, sizeof buf)) > 0){
+		if(first){
+			first = 0;
+			fprint(2, "Ghostscript Error:\n");
+		}
+		write(out, xbuf, n);
+		alarm(500);
+	}
+	_exits(0);
+}
+
+int 
+spawngs(GSInfo *g)
+{
+	char *args[16];
+	char tb[32], gb[32];
+	int i, nargs;
+	int devnull;
+	int stdinout[2];
+	int dataout[2];
+	int errout[2];
+
+	/*
+	 * spawn gs
+	 *
+ 	 * gs's standard input is fed from stdinout.
+	 * gs output written to fd-2 (i.e. output we generate intentionally) is fed to stdinout.
+	 * gs output written to fd 1 (i.e. ouptut gs generates on error) is fed to errout.
+	 * gs data output is written to fd 3, which is dataout.
+	 */
+	if(pipe(stdinout) < 0 || pipe(dataout)<0 || pipe(errout)<0)
+		return -1;
+
+	nargs = 0;
+	args[nargs++] = "gs";
+	args[nargs++] = "-dNOPAUSE";
+	args[nargs++] = "-dSAFER";
+	args[nargs++] = "-sDEVICE=plan9";
+	args[nargs++] = "-sOutputFile=/fd/3";
+	args[nargs++] = "-dQUIET";
+	args[nargs++] = "-r100";
+	sprint(tb, "-dTextAlphaBits=%d", textbits);
+	sprint(gb, "-dGraphicsAlphaBits=%d", gfxbits);
+	if(textbits)
+		args[nargs++] = tb;
+	if(gfxbits)
+		args[nargs++] = gb;
+	args[nargs++] = "-";
+	args[nargs] = nil;
+
+	gspid = fork();
+	if(gspid == 0) {
+		close(stdinout[1]);
+		close(dataout[1]);
+		close(errout[1]);
+
+		/*
+		 * Horrible problem: we want to dup fd's 0-4 below,
+		 * but some of the source fd's might have those small numbers.
+		 * So we need to reallocate those.  In order to not step on
+		 * anything else, we'll dup the fd's to higher ones using
+		 * dup(x, -1), but we need to use up the lower ones first.
+		 */
+		while((devnull = open("/dev/null", ORDWR)) < 5)
+			;
+
+		stdinout[0] = dup(stdinout[0], -1);
+		errout[0] = dup(errout[0], -1);
+		dataout[0] = dup(dataout[0], -1);
+
+		dup(stdinout[0], 0);
+		dup(errout[0], 1);
+		dup(devnull, 2);	/* never anything useful */
+		dup(dataout[0], 3);
+		dup(stdinout[0], 4);
+		for(i=5; i<20; i++)
+			close(i);
+		exec("/bin/gs", args);
+		wexits("exec");
+	}
+	close(stdinout[0]);
+	close(errout[0]);
+	close(dataout[0]);
+	atexit(killgs);
+
+	if(teegs)
+		stdinout[1] = spawnreader(stdinout[1]);
+
+	gsfd = g->gsfd = stdinout[1];
+	g->gsdfd = dataout[1];
+	g->gspid = gspid;
+
+	spawnmonitor(errout[1]);
+	Binit(&g->gsrd, g->gsfd, OREAD);
+
+	gscmd(g, "/PAGEOUT (/fd/4) (w) file def\n");
+	gscmd(g, "/PAGE== { PAGEOUT exch write==only PAGEOUT (\\n) writestring PAGEOUT flushfile } def\n");
+	waitgs(g);
+
+	return 0;
+}
+
+int
+gscmd(GSInfo *gs, char *fmt, ...)
+{
+	char buf[1024];
+	int n;
+
+	va_list v;
+	va_start(v, fmt);
+	n = vseprint(buf, buf+sizeof buf, fmt, v) - buf;
+	if(n <= 0)
+		return n;
+
+	if(chatty) {
+		fprint(2, "cmd: ");
+		write(2, buf, n);
+	}
+
+	if(write(gs->gsfd, buf, n) != 0)
+		return -1;
+
+	return n;
+}
+
+/*
+ * set the dimensions of the bitmap we expect to get back from GS.
+ */
+void
+setdim(GSInfo *gs, Rectangle bbox, int ppi, int landscape)
+{
+	Rectangle pbox;
+
+	if(chatty)
+		fprint(2, "setdim: bbox=%R\n", bbox);
+
+	if(ppi)
+		gs->ppi = ppi;
+
+	gscmd(gs, "mark\n");
+	if(ppi)
+		gscmd(gs, "/HWResolution [%d %d]\n", ppi, ppi);
+
+	if(!Dx(bbox))
+		bbox = Rect(0, 0, 612, 792);	/* 8½×11 */
+
+	switch(landscape){
+	case 0:
+		pbox = bbox;
+		break;
+	case 1:
+		pbox = Rect(bbox.min.y, bbox.min.x, bbox.max.y, bbox.max.x);
+		break;
+	}
+	gscmd(gs, "/PageSize [%d %d]\n", Dx(pbox), Dy(pbox));
+	gscmd(gs, "/Margins [%d %d]\n", -pbox.min.x, -pbox.min.y);
+	gscmd(gs, "currentdevice putdeviceprops pop\n");
+	gscmd(gs, "/#copies 1 store\n");
+
+	if(!eqpt(bbox.min, ZP))
+		gscmd(gs, "%d %d translate\n", -bbox.min.x, -bbox.min.y);
+
+	switch(landscape){
+	case 0:
+		break;
+	case 1:
+		gscmd(gs, "%d 0 translate\n", Dy(bbox));
+		gscmd(gs, "90 rotate\n");
+		break;
+	}
+
+	waitgs(gs);
+}
+
+void
+waitgs(GSInfo *gs)
+{
+	/* we figure out that gs is done by telling it to
+	 * print something and waiting until it does.
+	 */
+	char *p;
+	Biobuf *b = &gs->gsrd;
+	uchar buf[1024];
+	int n;
+
+//	gscmd(gs, "(\\n**bstack\\n) print flush\n");
+//	gscmd(gs, "stack flush\n");
+//	gscmd(gs, "(**estack\\n) print flush\n");
+	gscmd(gs, "(\\n//GO.SYSIN DD\\n) PAGE==\n");
+
+	alarm(300*1000);
+	for(;;) {
+		p = Brdline(b, '\n');
+		if(p == nil) {
+			n = Bbuffered(b);
+			if(n <= 0)
+				break;
+			if(n > sizeof buf)
+				n = sizeof buf;
+			Bread(b, buf, n);
+			continue;
+		}
+		p[Blinelen(b)-1] = 0;
+		if(chatty) fprint(2, "p: ");
+		if(chatty) write(2, p, Blinelen(b)-1);
+		if(chatty) fprint(2, "\n");
+		if(strstr(p, "Error:")) {
+			alarm(0);
+			fprint(2, "ghostscript error: %s\n", p);
+			wexits("gs error");
+		}
+
+		if(strstr(p, "//GO.SYSIN DD")) {
+			break;
+		}
+	}
+	alarm(0);
+}
diff --git a/src/cmd/page/mkfile b/src/cmd/page/mkfile
new file mode 100644
index 0000000..e8dbf52
--- /dev/null
+++ b/src/cmd/page/mkfile
@@ -0,0 +1,23 @@
+<$PLAN9/src/mkhdr
+
+TARG=page
+
+HFILES=page.h
+OFILES=\
+	filter.$O\
+	gfx.$O\
+	gs.$O\
+	page.$O\
+	pdf.$O\
+	ps.$O\
+	rotate.$O\
+	util.$O\
+	view.$O\
+
+<$PLAN9/src//mkone
+
+pdfprolog.c: pdfprolog.ps
+	cat pdfprolog.ps | sed 's/.*/"&\\n"/g' >pdfprolog.c
+
+pdf.$O: pdfprolog.c
+
diff --git a/src/cmd/page/nrotate.c b/src/cmd/page/nrotate.c
new file mode 100644
index 0000000..2225ec3
--- /dev/null
+++ b/src/cmd/page/nrotate.c
@@ -0,0 +1,277 @@
+/*
+ * Rotate an image 180° in O(log Dx + log Dy)
+ * draw calls, using an extra buffer the same size
+ * as the image.
+ *
+ * The basic concept is that you can invert an array by
+ * inverting the top half, inverting the bottom half, and
+ * then swapping them.
+ * 
+ * This is usually overkill, but it speeds up slow remote
+ * connections quite a bit.
+ */
+
+#include <u.h>
+#include <libc.h>
+#include <bio.h>
+#include <draw.h>
+#include <event.h>
+#include "page.h"
+
+int ndraw = 0;
+
+enum {
+	Xaxis,
+	Yaxis,
+};
+
+static void reverse(Image*, Image*, int);
+static void shuffle(Image*, Image*, int, int, Image*, int, int);
+static void writefile(char *name, Image *im, int gran);
+static void halvemaskdim(Image*);
+static void swapranges(Image*, Image*, int, int, int, int);
+
+/*
+ * Rotate the image 180° by reflecting first
+ * along the X axis, and then along the Y axis.
+ */
+void
+rot180(Image *img)
+{
+	Image *tmp;
+
+	tmp = xallocimage(display, img->r, img->chan, 0, DNofill);
+	if(tmp == nil)
+		return;
+
+	reverse(img, tmp, Xaxis);
+	reverse(img, tmp, Yaxis);
+
+	freeimage(tmp);
+}
+
+Image *mtmp;
+
+static void
+reverse(Image *img, Image *tmp, int axis)
+{
+	Image *mask;
+	Rectangle r;
+	int i, d;
+
+	/*
+	 * We start by swapping large chunks at a time.
+	 * The chunk size should be the largest power of
+	 * two that fits in the dimension.
+	 */
+	d = axis==Xaxis ? Dx(img) : Dy(img);
+	for(i = 1; i*2 <= d; i *= 2)
+		;
+
+	r = axis==Xaxis ? Rect(0,0, i,100) : Rect(0,0, 100,i);
+	mask = xallocimage(display, r, GREY1, 1, DTransparent);
+	mtmp = xallocimage(display, r, GREY1, 1, DTransparent);
+
+	/*
+	 * Now color the bottom (or left) half of the mask opaque.
+	 */
+	if(axis==Xaxis)
+		r.max.x /= 2;
+	else
+		r.max.y /= 2;
+
+	draw(mask, r, display->opaque, nil, ZP);
+	writefile("mask", mask, i);
+
+	/*
+	 * Shuffle will recur, shuffling the pieces as necessary
+	 * and making the mask a finer and finer grating.
+	 */
+	shuffle(img, tmp, axis, d, mask, i, 0);
+
+	freeimage(mask);
+}
+
+/*
+ * Shuffle the image by swapping pieces of size maskdim.
+ */
+static void
+shuffle(Image *img, Image *tmp, int axis, int imgdim, Image *mask, int maskdim)
+{
+	int slop;
+
+	if(maskdim == 0)
+		return;
+
+	/*
+	 * Figure out how much will be left over that needs to be
+	 * shifted specially to the bottom.
+	 */
+	slop = imgdim % maskdim;
+
+	/*
+	 * Swap adjacent grating lines as per mask.
+	 */
+	swapadjacent(img, tmp, axis, imgdim - slop, mask, maskdim);
+
+	/*
+	 * Calculate the mask with gratings half as wide and recur.
+	 */
+	halvemaskdim(mask, maskdim, axis);
+	writefile("mask", mask, maskdim/2);
+
+	shuffle(img, tmp, axis, imgdim, mask, maskdim/2);
+
+	/*
+	 * Move the slop down to the bottom of the image.
+	 */
+	swapranges(img, tmp, 0, imgdim-slop, imgdim, axis);
+	moveup(im, tmp, lastnn, nn, n, axis);
+}
+
+/*
+ * Halve the grating period in the mask.
+ * The grating currently looks like 
+ * ####____####____####____####____
+ * where #### is opacity.
+ *
+ * We want
+ * ##__##__##__##__##__##__##__##__
+ * which is achieved by shifting the mask
+ * and drawing on itself through itself.
+ * Draw doesn't actually allow this, so 
+ * we have to copy it first.
+ *
+ *     ####____####____####____####____ (dst)
+ * +   ____####____####____####____#### (src)
+ * in  __####____####____####____####__ (mask)
+ * ===========================================
+ *     ##__##__##__##__##__##__##__##__
+ */
+static void
+halvemaskdim(Image *m, int maskdim, int axis)
+{
+	Point δ;
+
+	δ = axis==Xaxis ? Pt(maskdim,0) : Pt(0,maskdim);
+	draw(mtmp, mtmp->r, mask, nil, mask->r.min);
+	gendraw(mask, mask->r, mtmp, δ, mtmp, divpt(δ,2));
+	writefile("mask", mask, maskdim/2);
+}
+
+/*
+ * Swap the regions [a,b] and [b,c]
+ */
+static void
+swapranges(Image *img, Image *tmp, int a, int b, int c, int axis)
+{
+	Rectangle r;
+	Point δ;
+
+	if(a == b || b == c)
+		return;
+
+	writefile("swap", img, 0);
+	draw(tmp, tmp->r, im, nil, im->r.min);
+
+	/* [a,a+(c-b)] gets [b,c] */
+	r = img->r;
+	if(axis==Xaxis){
+		δ = Pt(1,0);
+		r.min.x = img->r.min.x + a;
+		r.max.x = img->r.min.x + a + (c-b);
+	}else{
+		δ = Pt(0,1);
+		r.min.y = img->r.min.y + a;
+		r.max.y = img->r.min.y + a + (c-b);
+	}
+	draw(img, r, tmp, nil, addpt(tmp->r.min, mulpt(δ, b)));
+
+	/* [a+(c-b), c] gets [a,b] */
+	r = img->r;
+	if(axis==Xaxis){
+		r.min.x = img->r.min.x + a + (c-b);
+		r.max.x = img->r.min.x + c;
+	}else{
+		r.min.y = img->r.min.y + a + (c-b);
+		r.max.y = img->r.min.y + c;
+	}
+	draw(img, r, tmp, nil, addpt(tmp->r.min, mulpt(δ, a)));
+	writefile("swap", img, 1);
+}
+
+/*
+ * Swap adjacent regions as specified by the grating.
+ * We do this by copying the image through the mask twice,
+ * once aligned with the grading and once 180° out of phase.
+ */
+static void
+swapadjacent(Image *img, Image *tmp, int axis, int imgdim, Image *mask, int maskdim)
+{
+	Point δ;
+	Rectangle r0, r1;
+
+	δ = axis==Xaxis ? Pt(1,0) : Pt(0,1);
+
+	r0 = img->r;
+	r1 = img->r;
+	switch(axis){
+	case Xaxis:
+		r0.max.x = imgdim;
+		r1.min.x = imgdim;
+		break;
+	case Yaxis:
+		r0.max.y = imgdim;
+		r1.min.y = imgdim;
+	}
+
+	/*
+	 * r0 is the lower rectangle, while r1 is the upper one.
+	 */
+	draw(tmp, tmp->r, img, nil, 
+}
+
+void
+interlace(Image *im, Image *tmp, int axis, int n, Image *mask, int gran)
+{
+	Point p0, p1;
+	Rectangle r0, r1;
+
+	r0 = im->r;
+	r1 = im->r;
+	switch(axis) {
+	case Xaxis:
+		r0.max.x = n;
+		r1.min.x = n;
+		p0 = (Point){gran, 0};
+		p1 = (Point){-gran, 0};
+		break;
+	case Yaxis:
+		r0.max.y = n;
+		r1.min.y = n;
+		p0 = (Point){0, gran};
+		p1 = (Point){0, -gran};
+		break;
+	}
+
+	draw(tmp, im->r, im, display->black, im->r.min);
+	gendraw(im, r0, tmp, p0, mask, mask->r.min);
+	gendraw(im, r0, tmp, p1, mask, p1);
+}
+
+
+static void
+writefile(char *name, Image *im, int gran)
+{
+	static int c = 100;
+	int fd;
+	char buf[200];
+
+	snprint(buf, sizeof buf, "%d%s%d", c++, name, gran);
+	fd = create(buf, OWRITE, 0666);
+	if(fd < 0)
+		return;	
+	writeimage(fd, im, 0);
+	close(fd);
+}
+
diff --git a/src/cmd/page/page.c b/src/cmd/page/page.c
new file mode 100644
index 0000000..3669ebf
--- /dev/null
+++ b/src/cmd/page/page.c
@@ -0,0 +1,236 @@
+#include <u.h>
+#include <libc.h>
+#include <draw.h>
+#include <event.h>
+#include <bio.h>
+#include "page.h"
+
+int resizing;
+int mknewwindow;
+int doabort;
+int chatty;
+int reverse = -1;
+int goodps = 1;
+int ppi = 100;
+int teegs = 0;
+int truetoboundingbox;
+int textbits=4, gfxbits=4;
+int wctlfd = -1;
+int stdinfd;
+int truecolor;
+int imagemode;
+int notewatcher;
+int notegp;
+
+int
+watcher(void *v, char *x)
+{
+	USED(v);
+
+	if(strcmp(x, "die") != 0)
+		postnote(PNGROUP, notegp, x);
+	_exits(0);
+	return 0;
+}
+
+int
+bell(void *u, char *x)
+{
+	if(x && strcmp(x, "hangup") == 0)
+		_exits(0);
+
+	if(x && strstr(x, "die") == nil)
+		fprint(2, "postnote %d: %s\n", getpid(), x);
+
+	/* alarms come from the gs monitor */
+	if(x && strstr(x, "alarm")){
+		postnote(PNGROUP, getpid(), "die (gs error)");
+		postnote(PNPROC, notewatcher, "die (gs error)");
+	}
+
+	/* function mentions u so that it's in the stack trace */
+	if((u == nil || u != x) && doabort)
+		abort();
+
+/*	fprint(2, "exiting %d\n", getpid()); */
+	wexits("note");
+	return 0;
+}
+
+static int
+afmt(Fmt *fmt)
+{
+	char *s;
+
+	s = va_arg(fmt->args, char*);
+	if(s == nil || s[0] == '\0')
+		return fmtstrcpy(fmt, "");
+	else
+		return fmtprint(fmt, "%#q", s);
+}
+
+void
+usage(void)
+{
+	fprint(2, "usage: page [-biRrw] [-p ppi] file...\n");
+	exits("usage");
+}
+
+void
+main(int argc, char **argv)
+{
+	Document *doc;
+	Biobuf *b;
+	enum { Ninput = 16 };
+	uchar buf[Ninput+1];
+	int readstdin;
+
+	ARGBEGIN{
+	/* "temporary" debugging options */
+	case 'P':
+		goodps = 0;
+		break;
+	case 'v':
+		chatty++;
+		break;
+	case 'V':
+		teegs++;
+		break;
+	case 'a':
+		doabort++;
+		break;
+	case 'T':
+		textbits = atoi(EARGF(usage()));
+		gfxbits = atoi(EARGF(usage()));
+		break;
+
+	/* real options */
+	case 'R':
+		resizing = 1;
+		break;
+	case 'r':
+		reverse = 1;
+		break;
+	case 'p':
+		ppi = atoi(EARGF(usage()));
+		break;
+	case 'b':
+		truetoboundingbox = 1;
+		break;
+	case 'w':
+		mknewwindow = 1;
+		resizing = 1;
+		break;
+	case 'i':
+		imagemode = 1;
+		break;
+	default:
+		usage();
+	}ARGEND;
+
+	notegp = getpid();
+
+	switch(notewatcher = fork()){
+	case -1:
+		sysfatal("fork\n");
+		exits(0);
+	default:
+		break;
+	case 0:
+		atnotify(watcher, 1);
+		for(;;)
+			sleep(1000);
+		_exits(0);
+	}
+
+	rfork(RFNOTEG);
+	atnotify(bell, 1);
+
+	readstdin = 0;
+	if(imagemode == 0 && argc == 0){
+		readstdin = 1;
+		stdinfd = dup(0, -1);
+		close(0);
+		open("/dev/cons", OREAD);
+	}
+
+	quotefmtinstall();
+	fmtinstall('a', afmt);
+
+	fmtinstall('R', Rfmt);
+	fmtinstall('P', Pfmt);
+
+	if(readstdin){
+		b = nil;
+		if(readn(stdinfd, buf, Ninput) != Ninput){
+			fprint(2, "page: short read reading %s\n", argv[0]);
+			wexits("read");
+		}
+	}else if(argc != 0){
+		if(!(b = Bopen(argv[0], OREAD))) {
+			fprint(2, "page: cannot open \"%s\"\n", argv[0]);
+			wexits("open");
+		}	
+
+		if(Bread(b, buf, Ninput) != Ninput) {
+			fprint(2, "page: short read reading %s\n", argv[0]);
+			wexits("read");
+		}
+	}else
+		b = nil;
+
+	buf[Ninput] = '\0';
+	if(imagemode)
+		doc = initgfx(nil, 0, nil, nil, 0);
+	else if(strncmp((char*)buf, "%PDF-", 5) == 0)
+		doc = initpdf(b, argc, argv, buf, Ninput);
+	else if(strncmp((char*)buf, "\x04%!", 2) == 0)
+		doc = initps(b, argc, argv, buf, Ninput);
+	else if(buf[0] == '\x1B' && strstr((char*)buf, "@PJL"))
+		doc = initps(b, argc, argv, buf, Ninput);
+	else if(strncmp((char*)buf, "%!", 2) == 0)
+		doc = initps(b, argc, argv, buf, Ninput);
+	else if(strcmp((char*)buf, "\xF7\x02\x01\x83\x92\xC0\x1C;") == 0)
+		doc = initdvi(b, argc, argv, buf, Ninput);
+	else if(strncmp((char*)buf, "\xD0\xCF\x11\xE0\xA1\xB1\x1A\xE1", 8) == 0)
+		doc = initmsdoc(b, argc, argv, buf, Ninput);
+	else if(strncmp((char*)buf, "x T ", 4) == 0)
+		doc = inittroff(b, argc, argv, buf, Ninput);
+	else {
+		if(ppi != 100) {
+			fprint(2, "page: you can't specify -p with graphic files\n");
+			wexits("-p and graphics");
+		}
+		doc = initgfx(b, argc, argv, buf, Ninput);
+	}
+
+	if(doc == nil) {
+		fprint(2, "page: error reading file: %r\n");
+		wexits("document init");
+	}
+
+	if(doc->npage < 1 && !imagemode) {
+		fprint(2, "page: no pages found?\n");
+		wexits("pagecount");
+	}
+
+	if(reverse == -1) /* neither cmdline nor ps reader set it */
+		reverse = 0;
+
+	if(initdraw(0, 0, "page") < 0){
+		fprint(2, "page: initdraw failed: %r\n");
+		wexits("initdraw");
+	}
+	truecolor = screen->depth > 8;
+	viewer(doc);
+	wexits(0);
+}
+
+void
+wexits(char *s)
+{
+	if(s && *s && strcmp(s, "note") != 0 && mknewwindow)
+		sleep(10*1000);
+	postnote(PNPROC, notewatcher, "die");
+	exits(s);
+}
diff --git a/src/cmd/page/page.h b/src/cmd/page/page.h
new file mode 100644
index 0000000..aa19ff7
--- /dev/null
+++ b/src/cmd/page/page.h
@@ -0,0 +1,84 @@
+#include <cursor.h>
+
+typedef struct Document Document;
+
+struct Document {
+	char *docname;
+	int npage;
+	int fwdonly;
+	char* (*pagename)(Document*, int);
+	Image* (*drawpage)(Document*, int);
+	int	(*addpage)(Document*, char*);
+	int	(*rmpage)(Document*, int);
+	Biobuf *b;
+	void *extra;
+};
+
+void *emalloc(int);
+void *erealloc(void*, int);
+char *estrdup(char*);
+int spawncmd(char*, char **, int, int, int);
+
+int spooltodisk(uchar*, int, char**);
+int stdinpipe(uchar*, int);
+Document *initps(Biobuf*, int, char**, uchar*, int);
+Document *initpdf(Biobuf*, int, char**, uchar*, int);
+Document *initgfx(Biobuf*, int, char**, uchar*, int);
+Document *inittroff(Biobuf*, int, char**, uchar*, int);
+Document *initdvi(Biobuf*, int, char**, uchar*, int);
+Document *initmsdoc(Biobuf*, int, char**, uchar*, int);
+
+void viewer(Document*);
+extern Cursor reading;
+extern int chatty;
+extern int goodps;
+extern int textbits, gfxbits;
+extern int reverse;
+extern int clean;
+extern int ppi;
+extern int teegs;
+extern int truetoboundingbox;
+extern int wctlfd;
+extern int resizing;
+extern int mknewwindow;
+
+void rot180(Image*);
+Image *rot90(Image*);
+Image *resample(Image*, Image*);
+
+/* ghostscript interface shared by ps, pdf */
+typedef struct GSInfo	GSInfo;
+struct GSInfo {
+	int gsfd;
+	Biobuf gsrd;
+	int gspid;
+	int gsdfd;
+	int ppi;
+};
+void	waitgs(GSInfo*);
+int	gscmd(GSInfo*, char*, ...);
+int	spawngs(GSInfo*);
+void	setdim(GSInfo*, Rectangle, int, int);
+int	spawnwriter(GSInfo*, Biobuf*);
+Rectangle	screenrect(void);
+void	newwin(void);
+void	zerox(void);
+Rectangle winrect(void);
+void	resize(int, int);
+int	max(int, int);
+int	min(int, int);
+void	wexits(char*);
+Image*	xallocimage(Display*, Rectangle, ulong, int, ulong);
+int	bell(void*, char*);
+int	opentemp(char *template);
+
+extern int stdinfd;
+extern int truecolor;
+
+/* BUG BUG BUG BUG BUG: cannot use new draw operations in drawterm,
+ * or in vncs, and there is a bug in the kernel for copying images
+ * from cpu memory -> video memory (memmove is not being used).
+ * until all that is settled, ignore the draw operators.
+ */
+#define drawop(a,b,c,d,e,f) draw(a,b,c,d,e)
+#define gendrawop(a,b,c,d,e,f,g) gendraw(a,b,c,d,e,f)
diff --git a/src/cmd/page/pdf.c b/src/cmd/page/pdf.c
new file mode 100644
index 0000000..44615a2
--- /dev/null
+++ b/src/cmd/page/pdf.c
@@ -0,0 +1,155 @@
+/*
+ * pdf.c
+ * 
+ * pdf file support for page
+ */
+
+#include <u.h>
+#include <libc.h>
+#include <draw.h>
+#include <event.h>
+#include <bio.h>
+#include "page.h"
+
+typedef struct PDFInfo	PDFInfo;
+struct PDFInfo {
+	GSInfo gs;
+	Rectangle *pagebbox;
+};
+
+static Image*	pdfdrawpage(Document *d, int page);
+static char*	pdfpagename(Document*, int);
+
+char *pdfprolog = 
+#include "pdfprolog.c"
+	;
+
+Rectangle
+pdfbbox(GSInfo *gs)
+{
+	char *p;
+	char *f[4];
+	Rectangle r;
+	
+	r = Rect(0,0,0,0);
+	waitgs(gs);
+	gscmd(gs, "/CropBox knownoget {} {[0 0 0 0]} ifelse PAGE==\n");
+	p = Brdline(&gs->gsrd, '\n');
+	p[Blinelen(&gs->gsrd)-1] ='\0';
+	if(p[0] != '[')
+		return r;
+	if(tokenize(p+1, f, 4) != 4)
+		return r;
+	r = Rect(atoi(f[0]), atoi(f[1]), atoi(f[2]), atoi(f[3]));
+	waitgs(gs);
+	return r;
+}
+
+Document*
+initpdf(Biobuf *b, int argc, char **argv, uchar *buf, int nbuf)
+{
+	Document *d;
+	PDFInfo *pdf;
+	char *p;
+	char *fn;
+	char fdbuf[20];
+	int fd;
+	int i, npage;
+	Rectangle bbox;
+
+	if(argc > 1) {
+		fprint(2, "can only view one pdf file at a time\n");
+		return nil;
+	}
+
+	fprint(2, "reading through pdf...\n");
+	if(b == nil){	/* standard input; spool to disk (ouch) */
+		fd = spooltodisk(buf, nbuf, &fn);
+		sprint(fdbuf, "/fd/%d", fd);
+		b = Bopen(fdbuf, OREAD);
+		if(b == nil){
+			fprint(2, "cannot open disk spool file\n");
+			wexits("Bopen temp");
+		}
+	}else
+		fn = argv[0];
+
+	/* sanity check */
+	Bseek(b, 0, 0);
+	if(!(p = Brdline(b, '\n')) && !(p = Brdline(b, '\r'))) {
+		fprint(2, "cannot find end of first line\n");
+		wexits("initps");
+	}
+	if(strncmp(p, "%PDF-", 5) != 0) {
+		werrstr("not pdf");
+		return nil;
+	}
+
+	/* setup structures so one free suffices */
+	p = emalloc(sizeof(*d) + sizeof(*pdf));
+	d = (Document*) p;
+	p += sizeof(*d);
+	pdf = (PDFInfo*) p;
+
+	d->extra = pdf;
+	d->b = b;
+	d->drawpage = pdfdrawpage;
+	d->pagename = pdfpagename;
+	d->fwdonly = 0;
+
+	if(spawngs(&pdf->gs) < 0)
+		return nil;
+
+	gscmd(&pdf->gs, "%s", pdfprolog);
+	waitgs(&pdf->gs);
+
+	setdim(&pdf->gs, Rect(0,0,0,0), ppi, 0);
+	gscmd(&pdf->gs, "(%s) (r) file pdfopen begin\n", fn);
+	gscmd(&pdf->gs, "pdfpagecount PAGE==\n");
+	p = Brdline(&pdf->gs.gsrd, '\n');
+	npage = atoi(p);
+	if(npage < 1) {
+		fprint(2, "no pages?\n");
+		return nil;
+	}
+	d->npage = npage;
+	d->docname = argv[0];
+
+	gscmd(&pdf->gs, "Trailer\n");
+	bbox = pdfbbox(&pdf->gs);
+
+	pdf->pagebbox = emalloc(sizeof(Rectangle)*npage);
+	for(i=0; i<npage; i++) {
+		gscmd(&pdf->gs, "%d pdfgetpage\n", i+1);
+		pdf->pagebbox[i] = pdfbbox(&pdf->gs);
+		if(Dx(pdf->pagebbox[i]) <= 0)
+			pdf->pagebbox[i] = bbox;
+	}
+
+	return d;
+}
+
+static Image*
+pdfdrawpage(Document *doc, int page)
+{
+	PDFInfo *pdf = doc->extra;
+	Image *im;
+
+	gscmd(&pdf->gs, "%d DoPDFPage\n", page+1);
+	im = readimage(display, pdf->gs.gsdfd, 0);
+	if(im == nil) {
+		fprint(2, "fatal: readimage error %r\n");
+		wexits("readimage");
+	}
+	waitgs(&pdf->gs);
+	return im;
+}
+
+static char*
+pdfpagename(Document *d, int page)
+{
+	static char str[15];
+	USED(d);
+	sprint(str, "p %d", page+1);
+	return str;
+}
diff --git a/src/cmd/page/pdfprolog.c b/src/cmd/page/pdfprolog.c
new file mode 100644
index 0000000..8493e6d
--- /dev/null
+++ b/src/cmd/page/pdfprolog.c
@@ -0,0 +1,29 @@
+"/Page null def\n"
+"/Page# 0 def\n"
+"/PDFSave null def\n"
+"/DSCPageCount 0 def\n"
+"/DoPDFPage {dup /Page# exch store pdfgetpage mypdfshowpage } def\n"
+"\n"
+"/pdfshowpage_mysetpage {	% <pagedict> pdfshowpage_mysetpage <pagedict>\n"
+"  dup /CropBox pget {\n"
+"      boxrect\n"
+"      2 array astore /PageSize exch 4 2 roll\n"
+"      neg exch neg exch 2 array astore /PageOffset exch\n"
+"      << 5 1 roll >> setpagedevice\n"
+"  } if\n"
+"} bind def\n"
+"\n"
+"/mypdfshowpage		% <pagedict> pdfshowpage -\n"
+" { dup /Page exch store\n"
+"   pdfshowpage_init \n"
+"   pdfshowpage_setpage \n"
+"   pdfshowpage_mysetpage\n"
+"   save /PDFSave exch store\n"
+"   (before exec) VMDEBUG\n"
+"     pdfshowpage_finish\n"
+"   (after exec) VMDEBUG\n"
+"   PDFSave restore\n"
+" } bind def\n"
+"\n"
+"GS_PDF_ProcSet begin\n"
+"pdfdict begin\n"
diff --git a/src/cmd/page/ps.c b/src/cmd/page/ps.c
new file mode 100644
index 0000000..46ad5cd
--- /dev/null
+++ b/src/cmd/page/ps.c
@@ -0,0 +1,450 @@
+/*
+ * ps.c
+ * 
+ * provide postscript file reading support for page
+ */
+
+#include <u.h>
+#include <libc.h>
+#include <draw.h>
+#include <event.h>
+#include <bio.h>
+#include <ctype.h>
+#include "page.h"
+
+typedef struct PSInfo	PSInfo;
+typedef struct Page	Page;
+	
+struct Page {
+	char *name;
+	int offset;			/* offset of page beginning within file */
+};
+
+struct PSInfo {
+	GSInfo gs;
+	Rectangle bbox;	/* default bounding box */
+	Page *page;
+	int npage;
+	int clueless;	/* don't know where page boundaries are */
+	long psoff;	/* location of %! in file */
+	char ctm[256];
+};
+
+static int	pswritepage(Document *d, int fd, int page);
+static Image*	psdrawpage(Document *d, int page);
+static char*	pspagename(Document*, int);
+
+#define R(r) (r).min.x, (r).min.y, (r).max.x, (r).max.y
+Rectangle
+rdbbox(char *p)
+{
+	Rectangle r;
+	int a;
+	char *f[4];
+	while(*p == ':' || *p == ' ' || *p == '\t')
+		p++;
+	if(tokenize(p, f, 4) != 4)
+		return Rect(0,0,0,0);
+	r = Rect(atoi(f[0]), atoi(f[1]), atoi(f[2]), atoi(f[3]));
+	r = canonrect(r);
+	if(Dx(r) <= 0 || Dy(r) <= 0)
+		return Rect(0,0,0,0);
+
+	if(truetoboundingbox)
+		return r;
+
+	/* initdraw not called yet, can't use %R */
+	if(chatty) fprint(2, "[%d %d %d %d] -> ", R(r));
+	/*
+	 * attempt to sniff out A4, 8½×11, others
+	 * A4 is 596×842
+	 * 8½×11 is 612×792
+	 */
+
+	a = Dx(r)*Dy(r);
+	if(a < 300*300){	/* really small, probably supposed to be */
+		/* empty */
+	} else if(Dx(r) <= 596 && r.max.x <= 596 && Dy(r) > 792 && Dy(r) <= 842 && r.max.y <= 842)	/* A4 */
+		r = Rect(0, 0, 596, 842);
+	else {	/* cast up to 8½×11 */
+		if(Dx(r) <= 612 && r.max.x <= 612){
+			r.min.x = 0;
+			r.max.x = 612;
+		}
+		if(Dy(r) <= 792 && r.max.y <= 792){
+			r.min.y = 0;
+			r.max.y = 792;
+		}
+	}
+	if(chatty) fprint(2, "[%d %d %d %d]\n", R(r));
+	return r;
+}
+
+#define RECT(X) X.min.x, X.min.y, X.max.x, X.max.y
+
+int
+prefix(char *x, char *y)
+{
+	return strncmp(x, y, strlen(y)) == 0;
+}
+
+/*
+ * document ps is really being printed as n-up pages.
+ * we need to treat every n pages as 1.
+ */
+void
+repaginate(PSInfo *ps, int n)
+{
+	int i, np, onp;
+	Page *page;
+
+	page = ps->page;
+	onp = ps->npage;
+	np = (ps->npage+n-1)/n;
+
+	if(chatty) {
+		for(i=0; i<=onp+1; i++)
+			print("page %d: %d\n", i, page[i].offset);
+	}
+
+	for(i=0; i<np; i++)
+		page[i] = page[n*i];
+
+	/* trailer */
+	page[np] = page[onp];
+
+	/* EOF */
+	page[np+1] = page[onp+1];
+
+	ps->npage = np;
+
+	if(chatty) {
+		for(i=0; i<=np+1; i++)
+			print("page %d: %d\n", i, page[i].offset);
+	}
+
+}
+
+Document*
+initps(Biobuf *b, int argc, char **argv, uchar *buf, int nbuf)
+{
+	Document *d;
+	PSInfo *ps;
+	char *p;
+	char *q, *r;
+	char eol;
+	char *nargv[1];
+	char fdbuf[20];
+	char tmp[32];
+	int fd;
+	int i;
+	int incomments;
+	int cantranslate;
+	int trailer=0;
+	int nesting=0;
+	int dumb=0;
+	int landscape=0;
+	long psoff;
+	long npage, mpage;
+	Page *page;
+	Rectangle bbox = Rect(0,0,0,0);
+
+	if(argc > 1) {
+		fprint(2, "can only view one ps file at a time\n");
+		return nil;
+	}
+
+	fprint(2, "reading through postscript...\n");
+	if(b == nil){	/* standard input; spool to disk (ouch) */
+		fd = spooltodisk(buf, nbuf, nil);
+		sprint(fdbuf, "/fd/%d", fd);
+		b = Bopen(fdbuf, OREAD);
+		if(b == nil){
+			fprint(2, "cannot open disk spool file\n");
+			wexits("Bopen temp");
+		}
+		nargv[0] = fdbuf;
+		argv = nargv;
+	}
+
+	/* find %!, perhaps after PCL nonsense */
+	Bseek(b, 0, 0);
+	psoff = 0;
+	eol = 0;
+	for(i=0; i<16; i++){
+		psoff = Boffset(b);
+		if(!(p = Brdline(b, eol='\n')) && !(p = Brdline(b, eol='\r'))) {
+			fprint(2, "cannot find end of first line\n");
+			wexits("initps");
+		}
+		if(p[0]=='\x1B')
+			p++, psoff++;
+		if(p[0] == '%' && p[1] == '!')
+			break;
+	}
+	if(i == 16){
+		werrstr("not ps");
+		return nil;
+	}
+
+	/* page counting */
+	npage = 0;
+	mpage = 16;
+	page = emalloc(mpage*sizeof(*page));
+	memset(page, 0, mpage*sizeof(*page));
+
+	cantranslate = goodps;
+	incomments = 1;
+Keepreading:
+	while(p = Brdline(b, eol)) {
+		if(p[0] == '%')
+			if(chatty) fprint(2, "ps %.*s\n", utfnlen(p, Blinelen(b)-1), p);
+		if(npage == mpage) {
+			mpage *= 2;
+			page = erealloc(page, mpage*sizeof(*page));
+			memset(&page[npage], 0, npage*sizeof(*page));
+		}
+
+		if(p[0] != '%' || p[1] != '%')
+			continue;
+
+		if(prefix(p, "%%BeginDocument")) {
+			nesting++;
+			continue;
+		}
+		if(nesting > 0 && prefix(p, "%%EndDocument")) {
+			nesting--;
+			continue;
+		}
+		if(nesting)
+			continue;
+
+		if(prefix(p, "%%EndComment")) {
+			incomments = 0;
+			continue;
+		}
+		if(reverse == -1 && prefix(p, "%%PageOrder")) {
+			/* glean whether we should reverse the viewing order */
+			p[Blinelen(b)-1] = 0;
+			if(strstr(p, "Ascend"))
+				reverse = 0;
+			else if(strstr(p, "Descend"))
+				reverse = 1;
+			else if(strstr(p, "Special"))
+				dumb = 1;
+			p[Blinelen(b)-1] = '\n';
+			continue;
+		} else if(prefix(p, "%%Trailer")) {
+			incomments = 1;
+			page[npage].offset = Boffset(b)-Blinelen(b);
+			trailer = 1;
+			continue;
+		} else if(incomments && prefix(p, "%%Orientation")) {
+			if(strstr(p, "Landscape"))
+				landscape = 1;
+		} else if(incomments && Dx(bbox)==0 && prefix(p, q="%%BoundingBox")) {
+			bbox = rdbbox(p+strlen(q)+1);
+			if(chatty)
+				/* can't use %R because haven't initdraw() */
+				fprint(2, "document bbox [%d %d %d %d]\n",
+					RECT(bbox));
+			continue;
+		}
+
+		/*
+		 * If they use the initgraphics command, we can't play our translation tricks.
+		 */
+		p[Blinelen(b)-1] = 0;
+		if((q=strstr(p, "initgraphics")) && ((r=strchr(p, '%'))==nil || r > q))
+			cantranslate = 0;
+		p[Blinelen(b)-1] = eol;
+
+		if(!prefix(p, "%%Page:"))
+			continue;
+
+		/* 
+		 * figure out of the %%Page: line contains a page number
+		 * or some other page description to use in the menu bar.
+		 * 
+		 * lines look like %%Page: x y or %%Page: x
+		 * we prefer just x, and will generate our
+		 * own if necessary.
+		 */
+		p[Blinelen(b)-1] = 0;
+		if(chatty) fprint(2, "page %s\n", p);
+		r = p+7;
+		while(*r == ' ' || *r == '\t')
+			r++;
+		q = r;
+		while(*q && *q != ' ' && *q != '\t')
+			q++;
+		free(page[npage].name);
+		if(*r) {
+			if(*r == '"' && *q == '"')
+				r++, q--;
+			if(*q)
+				*q = 0;
+			page[npage].name = estrdup(r);
+			*q = 'x';
+		} else {
+			snprint(tmp, sizeof tmp, "p %ld", npage+1);
+			page[npage].name = estrdup(tmp);
+		}
+
+		/*
+		 * store the offset info for later viewing
+		 */
+		trailer = 0;
+		p[Blinelen(b)-1] = eol;
+		page[npage++].offset = Boffset(b)-Blinelen(b);
+	}
+	if(Blinelen(b) > 0){
+		fprint(2, "page: linelen %d\n", Blinelen(b));
+		Bseek(b, Blinelen(b), 1);
+		goto Keepreading;
+	}
+
+	if(Dx(bbox) == 0 || Dy(bbox) == 0)
+		bbox = Rect(0,0,612,792);	/* 8½×11 */
+	/*
+	 * if we didn't find any pages, assume the document
+	 * is one big page
+	 */
+	if(npage == 0) {
+		dumb = 1;
+		if(chatty) fprint(2, "don't know where pages are\n");
+		reverse = 0;
+		goodps = 0;
+		trailer = 0;
+		page[npage].name = "p 1";
+		page[npage++].offset = 0;
+	}
+
+	if(npage+2 > mpage) {
+		mpage += 2;
+		page = erealloc(page, mpage*sizeof(*page));
+		memset(&page[mpage-2], 0, 2*sizeof(*page));
+	}
+
+	if(!trailer)
+		page[npage].offset = Boffset(b);
+
+	Bseek(b, 0, 2); /* EOF */
+	page[npage+1].offset = Boffset(b);
+
+	d = emalloc(sizeof(*d));
+	ps = emalloc(sizeof(*ps));
+	ps->page = page;
+	ps->npage = npage;
+	ps->bbox = bbox;
+	ps->psoff = psoff;
+
+	d->extra = ps;
+	d->npage = ps->npage;
+	d->b = b;
+	d->drawpage = psdrawpage;
+	d->pagename = pspagename;
+
+	d->fwdonly = ps->clueless = dumb;
+	d->docname = argv[0];
+
+	if(spawngs(&ps->gs) < 0)
+		return nil;
+
+	if(!cantranslate)
+		bbox.min = ZP;
+	setdim(&ps->gs, bbox, ppi, landscape);
+
+	if(goodps){
+		/*
+		 * We want to only send the page (i.e. not header and trailer) information
+	 	 * for each page, so initialize the device by sending the header now.
+		 */
+		pswritepage(d, ps->gs.gsfd, -1);
+		waitgs(&ps->gs);
+	}
+
+	if(dumb) {
+		fprint(ps->gs.gsfd, "(%s) run\n", argv[0]);
+		fprint(ps->gs.gsfd, "(/fd/3) (w) file dup (THIS IS NOT A PLAN9 BITMAP 01234567890123456789012345678901234567890123456789\\n) writestring flushfile\n");
+	}
+
+	ps->bbox = bbox;
+
+	return d;
+}
+
+static int
+pswritepage(Document *d, int fd, int page)
+{
+	Biobuf *b = d->b;
+	PSInfo *ps = d->extra;
+	int t, n, i;
+	long begin, end;
+	char buf[8192];
+
+	if(page == -1)
+		begin = ps->psoff;
+	else
+		begin = ps->page[page].offset;
+
+	end = ps->page[page+1].offset;
+
+	if(chatty) {
+		fprint(2, "writepage(%d)... from #%ld to #%ld...\n",
+			page, begin, end);
+	}
+	Bseek(b, begin, 0);
+
+	t = end-begin;
+	n = sizeof(buf);
+	if(n > t) n = t;
+	while(t > 0 && (i=Bread(b, buf, n)) > 0) {
+		if(write(fd, buf, i) != i)
+			return -1;
+		t -= i;
+		if(n > t)
+			n = t;
+	}
+	return end-begin;
+}
+
+static Image*
+psdrawpage(Document *d, int page)
+{
+	PSInfo *ps = d->extra;
+	Image *im;
+
+	if(ps->clueless)
+		return readimage(display, ps->gs.gsdfd, 0);
+
+	waitgs(&ps->gs);
+
+	if(goodps)
+		pswritepage(d, ps->gs.gsfd, page);
+	else {
+		pswritepage(d, ps->gs.gsfd, -1);
+		pswritepage(d, ps->gs.gsfd, page);
+		pswritepage(d, ps->gs.gsfd, d->npage);
+	}
+	/*
+	 * If last line terminator is \r, gs will read ahead to check for \n
+	 * so send one to avoid deadlock.
+	 */
+	write(ps->gs.gsfd, "\n", 1);
+	im = readimage(display, ps->gs.gsdfd, 0);
+	if(im == nil) {
+		fprint(2, "fatal: readimage error %r\n");
+		wexits("readimage");
+	}
+	waitgs(&ps->gs);
+
+	return im;
+}
+
+static char*
+pspagename(Document *d, int page)
+{
+	PSInfo *ps = (PSInfo *) d->extra;
+	return ps->page[page].name;
+}
diff --git a/src/cmd/page/rotate.c b/src/cmd/page/rotate.c
new file mode 100644
index 0000000..b295263
--- /dev/null
+++ b/src/cmd/page/rotate.c
@@ -0,0 +1,474 @@
+/*
+ * rotate an image 180° in O(log Dx + log Dy) /dev/draw writes,
+ * using an extra buffer same size as the image.
+ * 
+ * the basic concept is that you can invert an array by inverting
+ * the top half, inverting the bottom half, and then swapping them.
+ * the code does this slightly backwards to ensure O(log n) runtime.
+ * (If you do it wrong, you can get O(log² n) runtime.)
+ * 
+ * This is usually overkill, but it speeds up slow remote
+ * connections quite a bit.
+ */
+
+#include <u.h>
+#include <libc.h>
+#include <bio.h>
+#include <draw.h>
+#include <event.h>
+#include "page.h"
+
+int ndraw = 0;
+enum {
+	Xaxis = 0,
+	Yaxis = 1,
+};
+
+Image *mtmp;
+
+void
+writefile(char *name, Image *im, int gran)
+{
+	static int c = 100;
+	int fd;
+	char buf[200];
+
+	snprint(buf, sizeof buf, "%d%s%d", c++, name, gran);
+	fd = create(buf, OWRITE, 0666);
+	if(fd < 0)
+		return;	
+	writeimage(fd, im, 0);
+	close(fd);
+}
+
+void
+moveup(Image *im, Image *tmp, int a, int b, int c, int axis)
+{
+	Rectangle range;
+	Rectangle dr0, dr1;
+	Point p0, p1;
+
+	if(a == b || b == c)
+		return;
+
+	drawop(tmp, tmp->r, im, nil, im->r.min, S);
+
+	switch(axis){
+	case Xaxis:
+		range = Rect(a, im->r.min.y,  c, im->r.max.y);
+		dr0 = range;
+		dr0.max.x = dr0.min.x+(c-b);
+		p0 = Pt(b, im->r.min.y);
+
+		dr1 = range;
+		dr1.min.x = dr1.max.x-(b-a);
+		p1 = Pt(a, im->r.min.y);
+		break;
+	case Yaxis:
+		range = Rect(im->r.min.x, a,  im->r.max.x, c);
+		dr0 = range;
+		dr0.max.y = dr0.min.y+(c-b);
+		p0 = Pt(im->r.min.x, b);
+
+		dr1 = range;
+		dr1.min.y = dr1.max.y-(b-a);
+		p1 = Pt(im->r.min.x, a);
+		break;
+	}
+	drawop(im, dr0, tmp, nil, p0, S);
+	drawop(im, dr1, tmp, nil, p1, S);
+}
+
+void
+interlace(Image *im, Image *tmp, int axis, int n, Image *mask, int gran)
+{
+	Point p0, p1;
+	Rectangle r0, r1;
+
+	r0 = im->r;
+	r1 = im->r;
+	switch(axis) {
+	case Xaxis:
+		r0.max.x = n;
+		r1.min.x = n;
+		p0 = (Point){gran, 0};
+		p1 = (Point){-gran, 0};
+		break;
+	case Yaxis:
+		r0.max.y = n;
+		r1.min.y = n;
+		p0 = (Point){0, gran};
+		p1 = (Point){0, -gran};
+		break;
+	}
+
+	drawop(tmp, im->r, im, display->opaque, im->r.min, S);
+	gendrawop(im, r0, tmp, p0, mask, mask->r.min, S);
+	gendrawop(im, r0, tmp, p1, mask, p1, S);
+}
+
+/*
+ * Halve the grating period in the mask.
+ * The grating currently looks like 
+ * ####____####____####____####____
+ * where #### is opacity.
+ *
+ * We want
+ * ##__##__##__##__##__##__##__##__
+ * which is achieved by shifting the mask
+ * and drawing on itself through itself.
+ * Draw doesn't actually allow this, so 
+ * we have to copy it first.
+ *
+ *     ####____####____####____####____ (dst)
+ * +   ____####____####____####____#### (src)
+ * in  __####____####____####____####__ (mask)
+ * ===========================================
+ *     ##__##__##__##__##__##__##__##__
+ */
+int
+nextmask(Image *mask, int axis, int maskdim)
+{
+	Point delta;
+
+	delta = axis==Xaxis ? Pt(maskdim,0) : Pt(0,maskdim);
+	drawop(mtmp, mtmp->r, mask, nil, mask->r.min, S);
+	gendrawop(mask, mask->r, mtmp, delta, mtmp, divpt(delta,-2), S);
+//	writefile("mask", mask, maskdim/2);
+	return maskdim/2;
+}
+
+void
+shuffle(Image *im, Image *tmp, int axis, int n, Image *mask, int gran,
+	int lastnn)
+{
+	int nn, left;
+
+	if(gran == 0)
+		return;
+	left = n%(2*gran);
+	nn = n - left;
+
+	interlace(im, tmp, axis, nn, mask, gran);
+//	writefile("interlace", im, gran);
+	
+	gran = nextmask(mask, axis, gran);
+	shuffle(im, tmp, axis, n, mask, gran, nn);
+//	writefile("shuffle", im, gran);
+	moveup(im, tmp, lastnn, nn, n, axis);
+//	writefile("move", im, gran);
+}
+
+void
+rot180(Image *im)
+{
+	Image *tmp, *tmp0;
+	Image *mask;
+	Rectangle rmask;
+	int gran;
+
+	if(chantodepth(im->chan) < 8){
+		/* this speeds things up dramatically; draw is too slow on sub-byte pixel sizes */
+		tmp0 = xallocimage(display, im->r, CMAP8, 0, DNofill);
+		drawop(tmp0, tmp0->r, im, nil, im->r.min, S);
+	}else
+		tmp0 = im;
+
+	tmp = xallocimage(display, tmp0->r, tmp0->chan, 0, DNofill);
+	if(tmp == nil){
+		if(tmp0 != im)
+			freeimage(tmp0);
+		return;
+	}
+	for(gran=1; gran<Dx(im->r); gran *= 2)
+		;
+	gran /= 4;
+
+	rmask.min = ZP;
+	rmask.max = (Point){2*gran, 100};
+
+	mask = xallocimage(display, rmask, GREY1, 1, DTransparent);
+	mtmp = xallocimage(display, rmask, GREY1, 1, DTransparent);
+	if(mask == nil || mtmp == nil) {
+		fprint(2, "out of memory during rot180: %r\n");
+		wexits("memory");
+	}
+	rmask.max.x = gran;
+	drawop(mask, rmask, display->opaque, nil, ZP, S);
+//	writefile("mask", mask, gran);
+	shuffle(im, tmp, Xaxis, Dx(im->r), mask, gran, 0);
+	freeimage(mask);
+	freeimage(mtmp);
+
+	for(gran=1; gran<Dy(im->r); gran *= 2)
+		;
+	gran /= 4;
+	rmask.max = (Point){100, 2*gran};
+	mask = xallocimage(display, rmask, GREY1, 1, DTransparent);
+	mtmp = xallocimage(display, rmask, GREY1, 1, DTransparent);
+	if(mask == nil || mtmp == nil) {
+		fprint(2, "out of memory during rot180: %r\n");
+		wexits("memory");
+	}
+	rmask.max.y = gran;
+	drawop(mask, rmask, display->opaque, nil, ZP, S);
+	shuffle(im, tmp, Yaxis, Dy(im->r), mask, gran, 0);
+	freeimage(mask);
+	freeimage(mtmp);
+	freeimage(tmp);
+	if(tmp0 != im)
+		freeimage(tmp0);
+}
+
+/* rotates an image 90 degrees clockwise */
+Image *
+rot90(Image *im)
+{
+	Image *tmp;
+	int i, j, dx, dy;
+
+	dx = Dx(im->r);
+	dy = Dy(im->r);
+	tmp = xallocimage(display, Rect(0, 0, dy, dx), im->chan, 0, DCyan);
+	if(tmp == nil) {
+		fprint(2, "out of memory during rot90: %r\n");
+		wexits("memory");
+	}
+
+	for(j = 0; j < dx; j++) {
+		for(i = 0; i < dy; i++) {
+			drawop(tmp, Rect(i, j, i+1, j+1), im, nil, Pt(j, dy-(i+1)), S);
+		}
+	}
+	freeimage(im);
+
+	return(tmp);
+}
+
+/* from resample.c -- resize from → to using interpolation */
+
+
+#define K2 7	/* from -.7 to +.7 inclusive, meaning .2 into each adjacent pixel */
+#define NK (2*K2+1)
+double K[NK];
+
+double
+fac(int L)
+{
+	int i, f;
+
+	f = 1;
+	for(i=L; i>1; --i)
+		f *= i;
+	return f;
+}
+
+/* 
+ * i0(x) is the modified Bessel function, Σ (x/2)^2L / (L!)²
+ * There are faster ways to calculate this, but we precompute
+ * into a table so let's keep it simple.
+ */
+double
+i0(double x)
+{
+	double v;
+	int L;
+
+	v = 1.0;
+	for(L=1; L<10; L++)
+		v += pow(x/2., 2*L)/pow(fac(L), 2);
+	return v;
+}
+
+double
+kaiser(double x, double tau, double alpha)
+{
+	if(fabs(x) > tau)
+		return 0.;
+	return i0(alpha*sqrt(1-(x*x/(tau*tau))))/i0(alpha);
+}
+
+void
+resamplex(uchar *in, int off, int d, int inx, uchar *out, int outx)
+{
+	int i, x, k;
+	double X, xx, v, rat;
+
+
+	rat = (double)inx/(double)outx;
+	for(x=0; x<outx; x++){
+		if(inx == outx){
+			/* don't resample if size unchanged */
+			out[off+x*d] = in[off+x*d];
+			continue;
+		}
+		v = 0.0;
+		X = x*rat;
+		for(k=-K2; k<=K2; k++){
+			xx = X + rat*k/10.;
+			i = xx;
+			if(i < 0)
+				i = 0;
+			if(i >= inx)
+				i = inx-1;
+			v += in[off+i*d] * K[K2+k];
+		}
+		out[off+x*d] = v;
+	}
+}
+
+void
+resampley(uchar **in, int off, int iny, uchar **out, int outy)
+{
+	int y, i, k;
+	double Y, yy, v, rat;
+
+	rat = (double)iny/(double)outy;
+	for(y=0; y<outy; y++){
+		if(iny == outy){
+			/* don't resample if size unchanged */
+			out[y][off] = in[y][off];
+			continue;
+		}
+		v = 0.0;
+		Y = y*rat;
+		for(k=-K2; k<=K2; k++){
+			yy = Y + rat*k/10.;
+			i = yy;
+			if(i < 0)
+				i = 0;
+			if(i >= iny)
+				i = iny-1;
+			v += in[i][off] * K[K2+k];
+		}
+		out[y][off] = v;
+	}
+
+}
+
+Image*
+resample(Image *from, Image *to)
+{
+	int i, j, bpl, nchan;
+	uchar **oscan, **nscan;
+	char tmp[20];
+	int xsize, ysize;
+	double v;
+	Image *t1, *t2;
+	ulong tchan;
+
+	for(i=-K2; i<=K2; i++){
+		K[K2+i] = kaiser(i/10., K2/10., 4.);
+	}
+
+	/* normalize */
+	v = 0.0;
+	for(i=0; i<NK; i++)
+		v += K[i];
+	for(i=0; i<NK; i++)
+		K[i] /= v;
+
+	switch(from->chan){
+	case GREY8:
+	case RGB24:
+	case RGBA32:
+	case ARGB32:
+	case XRGB32:
+		break;
+
+	case CMAP8:
+	case RGB15:
+	case RGB16:
+		tchan = RGB24;
+		goto Convert;
+
+	case GREY1:
+	case GREY2:
+	case GREY4:
+		tchan = GREY8;
+	Convert:
+		/* use library to convert to byte-per-chan form, then convert back */
+		t1 = xallocimage(display, Rect(0, 0, Dx(from->r), Dy(from->r)), tchan, 0, DNofill);
+		if(t1 == nil) {
+			fprint(2, "out of memory for temp image 1 in resample: %r\n");
+			wexits("memory");
+		}
+		drawop(t1, t1->r, from, nil, ZP, S);
+		t2 = xallocimage(display, to->r, tchan, 0, DNofill);
+		if(t2 == nil) {
+			fprint(2, "out of memory temp image 2 in resample: %r\n");
+			wexits("memory");
+		}
+		resample(t1, t2);
+		drawop(to, to->r, t2, nil, ZP, S);
+		freeimage(t1);
+		freeimage(t2);
+		return to;
+
+	default:
+		sysfatal("can't handle channel type %s", chantostr(tmp, from->chan));
+	}
+
+	xsize = Dx(to->r);
+	ysize = Dy(to->r);
+	oscan = malloc(Dy(from->r)*sizeof(uchar*));
+	nscan = malloc(max(ysize, Dy(from->r))*sizeof(uchar*));
+	if(oscan == nil || nscan == nil)
+		sysfatal("can't allocate: %r");
+
+	/* unload original image into scan lines */
+	bpl = bytesperline(from->r, from->depth);
+	for(i=0; i<Dy(from->r); i++){
+		oscan[i] = malloc(bpl);
+		if(oscan[i] == nil)
+			sysfatal("can't allocate: %r");
+		j = unloadimage(from, Rect(from->r.min.x, from->r.min.y+i, from->r.max.x, from->r.min.y+i+1), oscan[i], bpl);
+		if(j != bpl)
+			sysfatal("unloadimage");
+	}
+
+	/* allocate scan lines for destination. we do y first, so need at least Dy(from->r) lines */
+	bpl = bytesperline(Rect(0, 0, xsize, Dy(from->r)), from->depth);
+	for(i=0; i<max(ysize, Dy(from->r)); i++){
+		nscan[i] = malloc(bpl);
+		if(nscan[i] == nil)
+			sysfatal("can't allocate: %r");
+	}
+
+	/* resample in X */
+	nchan = from->depth/8;
+	for(i=0; i<Dy(from->r); i++){
+		for(j=0; j<nchan; j++){
+			if(j==0 && from->chan==XRGB32)
+				continue;
+			resamplex(oscan[i], j, nchan, Dx(from->r), nscan[i], xsize);
+		}
+		free(oscan[i]);
+		oscan[i] = nscan[i];
+		nscan[i] = malloc(bpl);
+		if(nscan[i] == nil)
+			sysfatal("can't allocate: %r");
+	}
+
+	/* resample in Y */
+	for(i=0; i<xsize; i++)
+		for(j=0; j<nchan; j++)
+			resampley(oscan, nchan*i+j, Dy(from->r), nscan, ysize);
+
+	/* pack data into destination */
+	bpl = bytesperline(to->r, from->depth);
+	for(i=0; i<ysize; i++){
+		j = loadimage(to, Rect(0, i, xsize, i+1), nscan[i], bpl);
+		if(j != bpl)
+			sysfatal("loadimage: %r");
+	}
+
+	for(i=0; i<Dy(from->r); i++){
+		free(oscan[i]);
+		free(nscan[i]);
+	}
+	free(oscan);
+	free(nscan);
+
+	return to;
+}
diff --git a/src/cmd/page/util.c b/src/cmd/page/util.c
new file mode 100644
index 0000000..22832ba
--- /dev/null
+++ b/src/cmd/page/util.c
@@ -0,0 +1,131 @@
+#include <u.h>
+#include <libc.h>
+#include <draw.h>
+#include <event.h>
+#include <bio.h>
+#include "page.h"
+
+void*
+emalloc(int sz)
+{
+	void *v;
+	v = malloc(sz);
+	if(v == nil) {
+		fprint(2, "out of memory allocating %d\n", sz);
+		wexits("mem");
+	}
+	memset(v, 0, sz);
+	return v;
+}
+
+void*
+erealloc(void *v, int sz)
+{
+	v = realloc(v, sz);
+	if(v == nil) {
+		fprint(2, "out of memory allocating %d\n", sz);
+		wexits("mem");
+	}
+	return v;
+}
+
+char*
+estrdup(char *s)
+{
+	char *t;
+	if((t = strdup(s)) == nil) {
+		fprint(2, "out of memory in strdup(%.10s)\n", s);
+		wexits("mem");
+	}
+	return t;
+}
+
+int
+opentemp(char *template)
+{
+	int fd, i;
+	char *p;
+
+	p = estrdup(template);
+	fd = -1;
+	for(i=0; i<10; i++){
+		mktemp(p);
+		if(access(p, 0) < 0 && (fd=create(p, ORDWR|ORCLOSE, 0400)) >= 0)
+			break;
+		strcpy(p, template);
+	}
+	if(fd < 0){
+		fprint(2, "couldn't make temporary file\n");
+		wexits("Ecreat");
+	}
+	strcpy(template, p);
+	free(p);
+
+	return fd;
+}
+
+/*
+ * spool standard input to /tmp.
+ * we've already read the initial in bytes into ibuf.
+ */
+int
+spooltodisk(uchar *ibuf, int in, char **name)
+{
+	uchar buf[8192];
+	int fd, n;
+	char temp[40];
+
+	strcpy(temp, "/tmp/pagespoolXXXXXXXXX");
+	fd = opentemp(temp);
+	if(name)
+		*name = estrdup(temp);
+
+	if(write(fd, ibuf, in) != in){
+		fprint(2, "error writing temporary file\n");
+		wexits("write temp");
+	}
+
+	while((n = read(stdinfd, buf, sizeof buf)) > 0){
+		if(write(fd, buf, n) != n){
+			fprint(2, "error writing temporary file\n");
+			wexits("write temp0");
+		}
+	}
+	seek(fd, 0, 0);
+	return fd;
+}
+
+/*
+ * spool standard input into a pipe.
+ * we've already ready the first in bytes into ibuf
+ */
+int
+stdinpipe(uchar *ibuf, int in)
+{
+	uchar buf[8192];
+	int n;
+	int p[2];
+	if(pipe(p) < 0){
+		fprint(2, "pipe fails: %r\n");	
+		wexits("pipe");
+	}
+
+	switch(rfork(RFPROC|RFFDG)){
+	case -1:
+		fprint(2, "fork fails: %r\n");
+		wexits("fork");
+	default:
+		close(p[1]);
+		return p[0];
+	case 0:
+		break;
+	}
+
+	close(p[0]);
+	write(p[1], ibuf, in);
+	while((n = read(stdinfd, buf, sizeof buf)) > 0)
+		write(p[1], buf, n);
+
+	_exits(0);
+	return -1;	/* not reached */
+}
diff --git a/src/cmd/page/view.c b/src/cmd/page/view.c
new file mode 100644
index 0000000..92aedeb
--- /dev/null
+++ b/src/cmd/page/view.c
@@ -0,0 +1,1022 @@
+/*
+ * the actual viewer that handles screen stuff
+ */
+
+#include <u.h>
+#include <libc.h>
+#include <draw.h>
+#include <cursor.h>
+#include <event.h>
+#include <bio.h>
+#include <plumb.h>
+#include <ctype.h>
+#include <keyboard.h>
+#include "page.h"
+
+Document *doc;
+Image *im;
+int page;
+int upside = 0;
+int showbottom = 0;		/* on the next showpage, move the image so the bottom is visible. */
+
+Rectangle ulrange;	/* the upper left corner of the image must be in this rectangle */
+Point ul;			/* the upper left corner of the image is at this point on the screen */
+
+Point pclip(Point, Rectangle);
+Rectangle mkrange(Rectangle screenr, Rectangle imr);
+void redraw(Image*);
+
+Cursor reading={
+	{-1, -1},
+	{0xff, 0x80, 0xff, 0x80, 0xff, 0x00, 0xfe, 0x00, 
+	 0xff, 0x00, 0xff, 0x80, 0xff, 0xc0, 0xef, 0xe0, 
+	 0xc7, 0xf0, 0x03, 0xf0, 0x01, 0xe0, 0x00, 0xc0, 
+	 0x03, 0xff, 0x03, 0xff, 0x03, 0xff, 0x03, 0xff, },
+	{0x00, 0x00, 0x7f, 0x00, 0x7e, 0x00, 0x7c, 0x00, 
+	 0x7e, 0x00, 0x7f, 0x00, 0x6f, 0x80, 0x47, 0xc0, 
+	 0x03, 0xe0, 0x01, 0xf0, 0x00, 0xe0, 0x00, 0x40, 
+	 0x00, 0x00, 0x01, 0xb6, 0x01, 0xb6, 0x00, 0x00, }
+};
+
+Cursor query = {
+	{-7,-7},
+	{0x0f, 0xf0, 0x1f, 0xf8, 0x3f, 0xfc, 0x7f, 0xfe, 
+	 0x7c, 0x7e, 0x78, 0x7e, 0x00, 0xfc, 0x01, 0xf8, 
+	 0x03, 0xf0, 0x07, 0xe0, 0x07, 0xc0, 0x07, 0xc0, 
+	 0x07, 0xc0, 0x07, 0xc0, 0x07, 0xc0, 0x07, 0xc0, },
+	{0x00, 0x00, 0x0f, 0xf0, 0x1f, 0xf8, 0x3c, 0x3c, 
+	 0x38, 0x1c, 0x00, 0x3c, 0x00, 0x78, 0x00, 0xf0, 
+	 0x01, 0xe0, 0x03, 0xc0, 0x03, 0x80, 0x03, 0x80, 
+	 0x00, 0x00, 0x03, 0x80, 0x03, 0x80, 0x00, 0x00, }
+};
+
+enum {
+	Left = 1,
+	Middle = 2,
+	Right = 4,
+
+	RMenu = 3,
+};
+
+void
+unhide(void)
+{
+	static int wctl = -1;
+
+	if(wctl < 0)
+		wctl = open("/dev/wctl", OWRITE);
+	if(wctl < 0)
+		return;
+
+	write(wctl, "unhide", 6);
+}
+
+int 
+max(int a, int b)
+{
+	return a > b ? a : b;
+}
+
+int 
+min(int a, int b)
+{
+	return a < b ? a : b;
+}
+
+
+char*
+menugen(int n)
+{
+	static char menustr[32];
+	char *p;
+	int len;
+
+	if(n == doc->npage)
+		return "exit";
+	if(n > doc->npage)
+		return nil;
+
+	if(reverse)
+		n = doc->npage-1-n;
+
+	p = doc->pagename(doc, n);
+	len = (sizeof menustr)-2;
+
+	if(strlen(p) > len && strrchr(p, '/'))
+		p = strrchr(p, '/')+1;
+	if(strlen(p) > len)
+		p = p+strlen(p)-len;
+
+	strcpy(menustr+1, p);
+	if(page == n)
+		menustr[0] = '>';
+	else
+		menustr[0] = ' ';
+	return menustr;
+}
+
+void
+showpage(int page, Menu *m)
+{
+	Image *tmp;
+
+	if(doc->fwdonly)
+		m->lasthit = 0;	/* this page */
+	else
+		m->lasthit = reverse ? doc->npage-1-page : page;
+	
+	esetcursor(&reading);
+	freeimage(im);
+	if((page < 0 || page >= doc->npage) && !doc->fwdonly){
+		im = nil;
+		return;
+	}
+	im = doc->drawpage(doc, page);
+	if(im == nil) {
+		if(doc->fwdonly)	/* this is how we know we're out of pages */
+			wexits(0);
+
+		im = xallocimage(display, Rect(0,0,50,50), GREY1, 1, DBlack);
+		if(im == nil) {
+			fprint(2, "out of memory: %r\n");
+			wexits("memory");
+		}
+		string(im, ZP, display->white, ZP, display->defaultfont, "?");
+	}else if(resizing){
+		resize(Dx(im->r), Dy(im->r));
+	}
+	if(im->r.min.x > 0 || im->r.min.y > 0) {
+		tmp = xallocimage(display, Rect(0, 0, Dx(im->r), Dy(im->r)), im->chan, 0, DNofill);
+		if(tmp == nil) {
+			fprint(2, "out of memory during showpage: %r\n");
+			wexits("memory");
+		}
+		drawop(tmp, tmp->r, im, nil, im->r.min, S);
+		freeimage(im);
+		im = tmp;
+	}
+
+	if(upside)
+		rot180(im);
+
+	esetcursor(nil);
+	if(showbottom){
+		ul.y = screen->r.max.y - Dy(im->r);
+		showbottom = 0;
+	}
+
+	redraw(screen);
+	flushimage(display, 1);
+}
+
+char*
+writebitmap(void)
+{
+	char basename[64];
+	char name[64+30];
+	static char result[200];
+	char *p, *q;
+	int fd;
+
+	if(im == nil)
+		return "no image";
+
+	memset(basename, 0, sizeof basename);
+	if(doc->docname)
+		strncpy(basename, doc->docname, sizeof(basename)-1);
+	else if((p = menugen(page)) && p[0] != '\0')
+		strncpy(basename, p+1, sizeof(basename)-1);
+
+	if(basename[0]) {
+		if(q = strrchr(basename, '/'))
+			q++;
+		else
+			q = basename;
+		if(p = strchr(q, '.'))
+			*p = 0;
+		
+		memset(name, 0, sizeof name);
+		snprint(name, sizeof(name)-1, "%s.%d.bit", q, page+1);
+		if(access(name, 0) >= 0) {
+			strcat(name, "XXXX");
+			mktemp(name);
+		}
+		if(access(name, 0) >= 0)
+			return "couldn't think of a name for bitmap";
+	} else {
+		strcpy(name, "bitXXXX");
+		mktemp(name);
+		if(access(name, 0) >= 0) 
+			return "couldn't think of a name for bitmap";
+	}
+
+	if((fd = create(name, OWRITE, 0666)) < 0) {
+		snprint(result, sizeof result, "cannot create %s: %r", name);
+		return result;
+	}
+
+	if(writeimage(fd, im, 0) < 0) {
+		snprint(result, sizeof result, "cannot writeimage: %r");
+		close(fd);
+		return result;
+	}
+	close(fd);
+
+	snprint(result, sizeof result, "wrote %s", name);
+	return result;
+}
+
+static void translate(Point);
+
+static int
+showdata(Plumbmsg *msg)
+{
+	char *s;
+
+	s = plumblookup(msg->attr, "action");
+	return s && strcmp(s, "showdata")==0;
+}
+
+/* correspond to entries in miditems[] below,
+ * changing one means you need to change
+ */
+enum{
+	Restore = 0,
+	Zin,
+	Fit,
+	Rot,
+	Upside,
+	Empty1,
+	Next,
+	Prev,
+	Zerox,
+	Empty2,
+	Reverse,
+	Del,
+	Write,
+	Empty3,
+	Exit,
+};
+ 
+void
+viewer(Document *dd)
+{
+	int i, fd, n, oldpage;
+	int nxt;
+	Menu menu, midmenu;
+	Mouse m;
+	Event e;
+	Point dxy, oxy, xy0;
+	Rectangle r;
+	Image *tmp;
+	static char *fwditems[] = { "this page", "next page", "exit", 0 };
+ 	static char *miditems[] = {
+ 		"orig size",
+ 		"zoom in",
+ 		"fit window",
+ 		"rotate 90",
+ 		"upside down",
+ 		"",
+ 		"next",
+ 		"prev",
+		"zerox",
+ 		"", 
+ 		"reverse",
+ 		"discard",
+ 		"write",
+ 		"", 
+ 		"quit", 
+ 		0 
+ 	};
+	char *s;
+	enum { Eplumb = 4 };
+	Plumbmsg *pm;
+
+	doc = dd;    /* save global for menuhit */
+	ul = screen->r.min;
+	einit(Emouse|Ekeyboard);
+	if(doc->addpage != nil)
+		eplumb(Eplumb, "image");
+
+	esetcursor(&reading);
+	r.min = ZP;
+
+	/*
+	 * im is a global pointer to the current image.
+	 * eventually, i think we will have a layer between
+	 * the display routines and the ps/pdf/whatever routines
+	 * to perhaps cache and handle images of different
+	 * sizes, etc.
+	 */
+	im = 0;
+	page = reverse ? doc->npage-1 : 0;
+
+	if(doc->fwdonly) {
+		menu.item = fwditems;
+		menu.gen = 0;
+		menu.lasthit = 0;
+	} else {
+		menu.item = 0;
+		menu.gen = menugen;
+		menu.lasthit = 0;
+	}
+
+	midmenu.item = miditems;
+	midmenu.gen = 0;
+	midmenu.lasthit = Next;
+
+	showpage(page, &menu);
+	esetcursor(nil);
+
+	nxt = 0;
+	for(;;) {
+		/*
+		 * throughout, if doc->fwdonly is set, we restrict the functionality
+		 * a fair amount.  we don't care about doc->npage anymore, and
+		 * all that can be done is select the next page.
+		 */
+		switch(eread(Emouse|Ekeyboard|Eplumb, &e)){
+		case Ekeyboard:
+			if(e.kbdc <= 0xFF && isdigit(e.kbdc)) {
+				nxt = nxt*10+e.kbdc-'0';
+				break;
+			} else if(e.kbdc != '\n')
+				nxt = 0;
+			switch(e.kbdc) {
+			case 'r':	/* reverse page order */
+				if(doc->fwdonly)
+					break;
+				reverse = !reverse;
+				menu.lasthit = doc->npage-1-menu.lasthit;
+
+				/*
+				 * the theory is that if we are reversing the
+				 * document order and are on the first or last
+				 * page then we're just starting and really want
+		 	 	 * to view the other end.  maybe the if
+				 * should be dropped and this should happen always.
+				 */
+				if(page == 0 || page == doc->npage-1) {
+					page = doc->npage-1-page;
+					showpage(page, &menu);
+				}
+				break;
+			case 'w':	/* write bitmap of current screen */
+				esetcursor(&reading);
+				s = writebitmap();
+				if(s)
+					string(screen, addpt(screen->r.min, Pt(5,5)), display->black, ZP,
+						display->defaultfont, s);
+				esetcursor(nil);
+				flushimage(display, 1);
+				break;
+			case 'd':	/* remove image from working set */
+				if(doc->rmpage && page < doc->npage) {
+					if(doc->rmpage(doc, page) >= 0) {
+						if(doc->npage < 0)
+							wexits(0);
+						if(page >= doc->npage)
+							page = doc->npage-1;
+						showpage(page, &menu);
+					}
+				}
+				break;
+			case 'q':
+			case 0x04: /* ctrl-d */
+				wexits(0);
+			case 'u':
+				if(im==nil)
+					break;
+				esetcursor(&reading);
+				rot180(im);
+				esetcursor(nil);
+				upside = !upside;
+				redraw(screen);
+				flushimage(display, 1);
+				break;
+			case '-':
+			case '\b':
+			case Kleft:
+				if(page > 0 && !doc->fwdonly) {
+					--page;
+					showpage(page, &menu);
+				}
+				break;
+			case '\n':
+				if(nxt) {
+					nxt--;
+					if(nxt >= 0 && nxt < doc->npage && !doc->fwdonly)
+						showpage(page=nxt, &menu);
+					nxt = 0;
+					break;
+				}
+				goto Gotonext;
+			case Kright:
+			case ' ':
+			Gotonext:
+				if(doc->npage && ++page >= doc->npage && !doc->fwdonly)
+					wexits(0);
+				showpage(page, &menu);
+				break;
+
+			/*
+			 * The upper y coordinate of the image is at ul.y in screen->r.
+			 * Panning up means moving the upper left corner down.  If the
+			 * upper left corner is currently visible, we need to go back a page.
+			 */
+			case Kup:
+				if(screen->r.min.y <= ul.y && ul.y < screen->r.max.y){
+					if(page > 0 && !doc->fwdonly){
+						--page;
+						showbottom = 1;
+						showpage(page, &menu);
+					}
+				} else {
+					i = Dy(screen->r)/2;
+					if(i > 10)
+						i -= 10;
+					if(i+ul.y > screen->r.min.y)
+						i = screen->r.min.y - ul.y;
+					translate(Pt(0, i));
+				}
+				break;
+
+			/*
+			 * If the lower y coordinate is on the screen, we go to the next page.
+			 * The lower y coordinate is at ul.y + Dy(im->r).
+			 */
+			case Kdown:
+				i = ul.y + Dy(im->r);
+				if(screen->r.min.y <= i && i <= screen->r.max.y){
+					ul.y = screen->r.min.y;
+					goto Gotonext;
+				} else {
+					i = -Dy(screen->r)/2;
+					if(i < -10)
+						i += 10;
+					if(i+ul.y+Dy(im->r) <= screen->r.max.y)
+						i = screen->r.max.y - Dy(im->r) - ul.y - 1;
+					translate(Pt(0, i));
+				}
+				break;
+			default:
+				esetcursor(&query);
+				sleep(1000);
+				esetcursor(nil);
+				break;	
+			}
+			break;
+
+		case Emouse:
+			m = e.mouse;
+			switch(m.buttons){
+			case Left:
+				oxy = m.xy;
+				xy0 = oxy;
+				do {
+					dxy = subpt(m.xy, oxy);
+					oxy = m.xy;	
+					translate(dxy);
+					m = emouse();
+				} while(m.buttons == Left);
+				if(m.buttons) {
+					dxy = subpt(xy0, oxy);
+					translate(dxy);
+				}
+				break;
+	
+			case Middle:
+				if(doc->npage == 0)
+					break;
+
+				n = emenuhit(Middle, &m, &midmenu);
+				if(n == -1)
+					break;
+				switch(n){
+				case Next: 	/* next */
+					if(reverse)
+						page--;
+					else
+						page++;
+					if(page < 0) {
+						if(reverse) return;
+						else page = 0;
+					}
+
+					if((page >= doc->npage) && !doc->fwdonly)
+						return;
+	
+					showpage(page, &menu);
+					nxt = 0;
+					break;
+				case Prev:	/* prev */
+					if(reverse)
+						page++;
+					else
+						page--;
+					if(page < 0) {
+						if(reverse) return;
+						else page = 0;
+					}
+
+					if((page >= doc->npage) && !doc->fwdonly && !reverse)
+						return;
+	
+					showpage(page, &menu);
+					nxt = 0;
+					break;
+				case Zerox:	/* prev */
+					zerox();
+					break;
+				case Zin:	/* zoom in */
+					{
+						double delta;
+						Rectangle r;
+
+						r = egetrect(Middle, &m);
+						if((rectclip(&r, rectaddpt(im->r, ul)) == 0) ||
+							Dx(r) == 0 || Dy(r) == 0)
+							break;
+						/* use the smaller side to expand */
+						if(Dx(r) < Dy(r))
+							delta = (double)Dx(im->r)/(double)Dx(r);
+						else
+							delta = (double)Dy(im->r)/(double)Dy(r);
+
+						esetcursor(&reading);
+						tmp = xallocimage(display, 
+								Rect(0, 0, (int)((double)Dx(im->r)*delta), (int)((double)Dy(im->r)*delta)), 
+								im->chan, 0, DBlack);
+						if(tmp == nil) {
+							fprint(2, "out of memory during zoom: %r\n");
+							wexits("memory");
+						}
+						resample(im, tmp);
+						freeimage(im);
+						im = tmp;
+						esetcursor(nil);
+						ul = screen->r.min;
+						redraw(screen);
+						flushimage(display, 1);
+						break;
+					}
+				case Fit:	/* fit */
+					{
+						double delta;
+						Rectangle r;
+						
+						delta = (double)Dx(screen->r)/(double)Dx(im->r);
+						if((double)Dy(im->r)*delta > Dy(screen->r))
+							delta = (double)Dy(screen->r)/(double)Dy(im->r);
+
+						r = Rect(0, 0, (int)((double)Dx(im->r)*delta), (int)((double)Dy(im->r)*delta));
+						esetcursor(&reading);
+						tmp = xallocimage(display, r, im->chan, 0, DBlack);
+						if(tmp == nil) {
+							fprint(2, "out of memory during fit: %r\n");
+							wexits("memory");
+						}
+						resample(im, tmp);
+						freeimage(im);
+						im = tmp;
+						esetcursor(nil);
+						ul = screen->r.min;
+						redraw(screen);
+						flushimage(display, 1);
+						break;
+					}
+				case Rot:	/* rotate 90 */
+					esetcursor(&reading);
+					im = rot90(im);
+					esetcursor(nil);
+					redraw(screen);
+					flushimage(display, 1);
+					break;
+				case Upside: 	/* upside-down */
+					if(im==nil)
+						break;
+					esetcursor(&reading);
+					rot180(im);
+					esetcursor(nil);
+					upside = !upside;
+					redraw(screen);
+					flushimage(display, 1);
+					break;
+				case Restore:	/* restore */
+					showpage(page, &menu);
+					break;
+				case Reverse:	/* reverse */
+					if(doc->fwdonly)
+						break;
+					reverse = !reverse;
+					menu.lasthit = doc->npage-1-menu.lasthit;
+	
+					if(page == 0 || page == doc->npage-1) {
+						page = doc->npage-1-page;
+						showpage(page, &menu);
+					}
+					break;
+				case Write: /* write */
+					esetcursor(&reading);
+					s = writebitmap();
+					if(s)
+						string(screen, addpt(screen->r.min, Pt(5,5)), display->black, ZP,
+							display->defaultfont, s);
+					esetcursor(nil);
+					flushimage(display, 1);
+					break;
+				case Del: /* delete */
+					if(doc->rmpage && page < doc->npage) {
+						if(doc->rmpage(doc, page) >= 0) {
+							if(doc->npage < 0)
+								wexits(0);
+							if(page >= doc->npage)
+								page = doc->npage-1;
+							showpage(page, &menu);
+						}
+					}
+					break;
+				case Exit:	/* exit */
+					return;
+				case Empty1:
+				case Empty2:
+				case Empty3:
+					break;
+
+				}; 
+
+	
+	
+			case Right:
+				if(doc->npage == 0)
+					break;
+
+				oldpage = page;
+				n = emenuhit(RMenu, &m, &menu);
+				if(n == -1)
+					break;
+	
+				if(doc->fwdonly) {
+					switch(n){
+					case 0:	/* this page */
+						break;
+					case 1:	/* next page */
+						showpage(++page, &menu);
+						break;
+					case 2:	/* exit */
+						return;
+					}
+					break;
+				}
+	
+				if(n == doc->npage)
+					return;
+				else
+					page = reverse ? doc->npage-1-n : n;
+	
+				if(oldpage != page)
+					showpage(page, &menu);
+				nxt = 0;
+				break;
+			}
+			break;
+
+		case Eplumb:
+			pm = e.v;
+			if(pm->ndata <= 0){
+				plumbfree(pm);
+				break;
+			}
+			if(showdata(pm)) {
+				s = estrdup("/tmp/pageplumbXXXXXXX");
+				fd = opentemp(s);
+				write(fd, pm->data, pm->ndata);
+				/* lose fd reference on purpose; the file is open ORCLOSE */
+			} else if(pm->data[0] == '/') {
+				s = estrdup(pm->data);
+			} else {
+				s = emalloc(strlen(pm->wdir)+1+pm->ndata+1);
+				sprint(s, "%s/%s", pm->wdir, pm->data);
+				cleanname(s);
+			}
+			if((i = doc->addpage(doc, s)) >= 0) {
+				page = i;
+				unhide();
+				showpage(page, &menu);
+			}
+			free(s);
+			plumbfree(pm);
+			break;
+		}
+	}
+}
+
+Image *gray;
+
+/*
+ * A draw operation that touches only the area contained in bot but not in top.
+ * mp and sp get aligned with bot.min.
+ */
+static void
+gendrawdiff(Image *dst, Rectangle bot, Rectangle top, 
+	Image *src, Point sp, Image *mask, Point mp, int op)
+{
+	Rectangle r;
+	Point origin;
+	Point delta;
+
+	USED(op);
+
+	if(Dx(bot)*Dy(bot) == 0)
+		return;
+
+	/* no points in bot - top */
+	if(rectinrect(bot, top))
+		return;
+
+	/* bot - top ≡ bot */
+	if(Dx(top)*Dy(top)==0 || rectXrect(bot, top)==0){
+		gendrawop(dst, bot, src, sp, mask, mp, op);
+		return;
+	}
+
+	origin = bot.min;
+	/* split bot into rectangles that don't intersect top */
+	/* left side */
+	if(bot.min.x < top.min.x){
+		r = Rect(bot.min.x, bot.min.y, top.min.x, bot.max.y);
+		delta = subpt(r.min, origin);
+		gendrawop(dst, r, src, addpt(sp, delta), mask, addpt(mp, delta), op);
+		bot.min.x = top.min.x;
+	}
+
+	/* right side */
+	if(bot.max.x > top.max.x){
+		r = Rect(top.max.x, bot.min.y, bot.max.x, bot.max.y);
+		delta = subpt(r.min, origin);
+		gendrawop(dst, r, src, addpt(sp, delta), mask, addpt(mp, delta), op);
+		bot.max.x = top.max.x;
+	}
+
+	/* top */
+	if(bot.min.y < top.min.y){
+		r = Rect(bot.min.x, bot.min.y, bot.max.x, top.min.y);
+		delta = subpt(r.min, origin);
+		gendrawop(dst, r, src, addpt(sp, delta), mask, addpt(mp, delta), op);
+		bot.min.y = top.min.y;
+	}
+
+	/* bottom */
+	if(bot.max.y > top.max.y){
+		r = Rect(bot.min.x, top.max.y, bot.max.x, bot.max.y);
+		delta = subpt(r.min, origin);
+		gendrawop(dst, r, src, addpt(sp, delta), mask, addpt(mp, delta), op);
+		bot.max.y = top.max.y;
+	}
+}
+
+static void
+drawdiff(Image *dst, Rectangle bot, Rectangle top, Image *src, Image *mask, Point p, int op)
+{
+	gendrawdiff(dst, bot, top, src, p, mask, p, op);
+}
+
+/*
+ * Translate the image in the window by delta.
+ */
+static void
+translate(Point delta)
+{
+	Point u;
+	Rectangle r, or;
+
+	if(im == nil)
+		return;
+
+	u = pclip(addpt(ul, delta), ulrange);
+	delta = subpt(u, ul);
+	if(delta.x == 0 && delta.y == 0)
+		return;
+
+	/*
+	 * The upper left corner of the image is currently at ul.
+	 * We want to move it to u.
+	 */
+	or = rectaddpt(Rpt(ZP, Pt(Dx(im->r), Dy(im->r))), ul);
+	r = rectaddpt(or, delta);
+
+	drawop(screen, r, screen, nil, ul, S);
+	ul = u;
+
+	/* fill in gray where image used to be but isn't. */
+	drawdiff(screen, insetrect(or, -2), insetrect(r, -2), gray, nil, ZP, S);
+
+	/* fill in black border */
+	drawdiff(screen, insetrect(r, -2), r, display->black, nil, ZP, S);
+
+	/* fill in image where it used to be off the screen. */
+	if(rectclip(&or, screen->r))
+		drawdiff(screen, r, rectaddpt(or, delta), im, nil, im->r.min, S);
+	else
+		drawop(screen, r, im, nil, im->r.min, S);
+	flushimage(display, 1);
+}
+
+void
+redraw(Image *screen)
+{
+	Rectangle r;
+
+	if(im == nil)
+		return;
+
+	ulrange.max = screen->r.max;
+	ulrange.min = subpt(screen->r.min, Pt(Dx(im->r), Dy(im->r)));
+
+	ul = pclip(ul, ulrange);
+	drawop(screen, screen->r, im, nil, subpt(im->r.min, subpt(ul, screen->r.min)), S);
+
+	if(im->repl)
+		return;
+
+	/* fill in any outer edges */
+	/* black border */
+	r = rectaddpt(im->r, subpt(ul, im->r.min));
+	border(screen, r, -2, display->black, ZP);
+	r.min = subpt(r.min, Pt(2,2));
+	r.max = addpt(r.max, Pt(2,2));
+
+	/* gray for the rest */
+	if(gray == nil) {
+		gray = xallocimage(display, Rect(0,0,1,1), RGB24, 1, 0x888888FF);
+		if(gray == nil) {
+			fprint(2, "g out of memory: %r\n");
+			wexits("mem");
+		}
+	}
+	border(screen, r, -4000, gray, ZP);
+//	flushimage(display, 0);	
+}
+
+void
+eresized(int new)
+{
+	Rectangle r;
+	r = screen->r;
+	if(new && getwindow(display, Refnone) < 0)
+		fprint(2,"can't reattach to window");
+	ul = addpt(ul, subpt(screen->r.min, r.min));
+	redraw(screen);
+}
+
+/* clip p to be in r */
+Point
+pclip(Point p, Rectangle r)
+{
+	if(p.x < r.min.x)
+		p.x = r.min.x;
+	else if(p.x >= r.max.x)
+		p.x = r.max.x-1;
+
+	if(p.y < r.min.y)
+		p.y = r.min.y;
+	else if(p.y >= r.max.y)
+		p.y = r.max.y-1;
+
+	return p;
+}
+
+/*
+ * resize is perhaps a misnomer. 
+ * this really just grows the window to be at least dx across
+ * and dy high.  if the window hits the bottom or right edge,
+ * it is backed up until it hits the top or left edge.
+ */
+void
+resize(int dx, int dy)
+{
+	static Rectangle sr;
+	Rectangle r, or;
+
+	dx += 2*Borderwidth;
+	dy += 2*Borderwidth;
+	if(wctlfd < 0){
+		wctlfd = open("/dev/wctl", OWRITE);
+		if(wctlfd < 0)
+			return;
+	}
+
+	r = insetrect(screen->r, -Borderwidth);
+	if(Dx(r) >= dx && Dy(r) >= dy)
+		return;
+
+	if(Dx(sr)*Dy(sr) == 0)
+		sr = screenrect();
+
+	or = r;
+
+	r.max.x = max(r.min.x+dx, r.max.x);
+	r.max.y = max(r.min.y+dy, r.max.y);
+	if(r.max.x > sr.max.x){
+		if(Dx(r) > Dx(sr)){
+			r.min.x = 0;
+			r.max.x = sr.max.x;
+		}else
+			r = rectaddpt(r, Pt(sr.max.x-r.max.x, 0));
+	}
+	if(r.max.y > sr.max.y){
+		if(Dy(r) > Dy(sr)){
+			r.min.y = 0;
+			r.max.y = sr.max.y;
+		}else
+			r = rectaddpt(r, Pt(0, sr.max.y-r.max.y));
+	}
+
+	/*
+	 * Sometimes we can't actually grow the window big enough,
+	 * and resizing it to the same shape makes it flash.
+	 */
+	if(Dx(r) == Dx(or) && Dy(r) == Dy(or))
+		return;
+
+	fprint(wctlfd, "resize -minx %d -miny %d -maxx %d -maxy %d\n",
+		r.min.x, r.min.y, r.max.x, r.max.y);
+}
+
+/*
+ * If we allocimage after a resize but before flushing the draw buffer,
+ * we won't have seen the reshape event, and we won't have called
+ * getwindow, and allocimage will fail.  So we flushimage before every alloc.
+ */
+Image*
+xallocimage(Display *d, Rectangle r, ulong chan, int repl, ulong val)
+{
+	flushimage(display, 0);
+	return allocimage(d, r, chan, repl, val);
+}
+
+/* all code below this line should be in the library, but is stolen from colors instead */
+static char*
+rdenv(char *name)
+{
+	char *v;
+	int fd, size;
+
+	fd = open(name, OREAD);
+	if(fd < 0)
+		return 0;
+	size = seek(fd, 0, 2);
+	v = malloc(size+1);
+	if(v == 0){
+		fprint(2, "page: can't malloc: %r\n");
+		wexits("no mem");
+	}
+	seek(fd, 0, 0);
+	read(fd, v, size);
+	v[size] = 0;
+	close(fd);
+	return v;
+}
+
+Rectangle
+screenrect(void)
+{
+	int fd;
+	char buf[12*5];
+
+	fd = open("/dev/screen", OREAD);
+	if(fd == -1)
+		fd=open("/mnt/term/dev/screen", OREAD);
+	if(fd == -1){
+		fprint(2, "page: can't open /dev/screen: %r\n");
+		wexits("window read");
+	}
+	if(read(fd, buf, sizeof buf) != sizeof buf){
+		fprint(2, "page: can't read /dev/screen: %r\n");
+		wexits("screen read");
+	}
+	close(fd);
+	return Rect(atoi(buf+12), atoi(buf+24), atoi(buf+36), atoi(buf+48));
+}
+
+void
+zerox(void)
+{
+	int pfd[2];
+
+	pipe(pfd);
+	switch(rfork(RFFDG|RFPROC)) {
+		case -1:
+			wexits("cannot fork in zerox: %r");
+		case 0: 
+			dup(pfd[1], 0);
+			close(pfd[0]);
+			execl("/bin/page", "page", "-w", 0);
+			wexits("cannot exec in zerox: %r\n");
+		default:
+			close(pfd[1]);
+			writeimage(pfd[0], im, 0);
+			close(pfd[0]);
+			break;
+	}
+}