tplaceholder; does not yet build - plan9port - [fork] Plan 9 from user space
git clone git://src.adamsgaard.dk/plan9port
Log
Files
Refs
README
LICENSE
---
commit 24c02865d8fcc97d1fb5cb9281810d8074aa5eb1
parent d1e9002f81f14fbfef1ebc4261edccd9eb97b72c
Author: rsc 
Date:   Tue,  4 Jan 2005 21:23:50 +0000

placeholder; does not yet build

Diffstat:
  A src/cmd/page/filter.c               |     107 +++++++++++++++++++++++++++++++
  A src/cmd/page/gfx.c                  |     331 +++++++++++++++++++++++++++++++
  A src/cmd/page/gs.c                   |     342 +++++++++++++++++++++++++++++++
  A src/cmd/page/mkfile                 |      23 +++++++++++++++++++++++
  A src/cmd/page/nrotate.c              |     277 +++++++++++++++++++++++++++++++
  A src/cmd/page/page.c                 |     236 +++++++++++++++++++++++++++++++
  A src/cmd/page/page.h                 |      84 +++++++++++++++++++++++++++++++
  A src/cmd/page/pdf.c                  |     155 +++++++++++++++++++++++++++++++
  A src/cmd/page/pdfprolog.c            |      29 +++++++++++++++++++++++++++++
  A src/cmd/page/ps.c                   |     450 +++++++++++++++++++++++++++++++
  A src/cmd/page/rotate.c               |     474 +++++++++++++++++++++++++++++++
  A src/cmd/page/util.c                 |     131 +++++++++++++++++++++++++++++++
  A src/cmd/page/view.c                 |    1022 +++++++++++++++++++++++++++++++

13 files changed, 3661 insertions(+), 0 deletions(-)
---
diff --git a/src/cmd/page/filter.c b/src/cmd/page/filter.c
t@@ -0,0 +1,107 @@
+#include 
+#include 
+#include 
+#include 
+#include 
+#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
t@@ -0,0 +1,331 @@
+/*
+ * graphics file reading for page
+ */
+
+#include 
+#include 
+#include 
+#include 
+#include 
+#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; iextra;
+
+        assert((name == nil) ^ (buf == nil));
+        assert(name != nil || doc->npage == 0);
+
+        for(i=0; inpage; 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; inpage; 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
t@@ -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 
+#include 
+#include 
+#include 
+#include 
+#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
t@@ -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
t@@ -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 
+#include 
+#include 
+#include 
+#include 
+#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
t@@ -0,0 +1,236 @@
+#include 
+#include 
+#include 
+#include 
+#include 
+#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
t@@ -0,0 +1,84 @@
+#include 
+
+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
t@@ -0,0 +1,155 @@
+/*
+ * pdf.c
+ * 
+ * pdf file support for page
+ */
+
+#include 
+#include 
+#include 
+#include 
+#include 
+#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; igs, "%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
t@@ -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 {        %  pdfshowpage_mysetpage \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                %  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
t@@ -0,0 +1,450 @@
+/*
+ * ps.c
+ * 
+ * provide postscript file reading support for page
+ */
+
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#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; inpage = 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
t@@ -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 
+#include 
+#include 
+#include 
+#include 
+#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; granr); 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; granr); 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= 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= 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; ichan){
+        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; ir); 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; ir)); i++){
+                nscan[i] = malloc(bpl);
+                if(nscan[i] == nil)
+                        sysfatal("can't allocate: %r");
+        }
+
+        /* resample in X */
+        nchan = from->depth/8;
+        for(i=0; ir); i++){
+                for(j=0; jchan==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; ir), nscan, ysize);
+
+        /* pack data into destination */
+        bpl = bytesperline(to->r, from->depth);
+        for(i=0; ir); 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
t@@ -0,0 +1,131 @@
+#include 
+#include 
+#include 
+#include 
+#include 
+#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
t@@ -0,0 +1,1022 @@
+/*
+ * the actual viewer that handles screen stuff
+ */
+
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#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;
+        }
+}