/* Program to view fax files on an X-window screen
   Copyright (C) 1990, 1995  Frank D. Cringle.

This file is part of viewfax - g3/g4 fax processing software.
     
viewfax is free software; you can redistribute it and/or modify it
under the terms of the GNU General Public License as published by the
Free Software Foundation; either version 2 of the License, or (at your
option) any later version.
     
This program is distributed in the hope that it will be useful, but
WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
General Public License for more details.
     
You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. */

#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <sys/time.h>
/* NewImage() needs to fiddle with the Display structure */
#define XLIB_ILLEGAL_ACCESS
#include <X11/Xlib.h>
#include <X11/Xutil.h>
#include <X11/Xatom.h>
#include <X11/keysym.h>
#include <X11/keysymdef.h>
#include <X11/cursorfont.h>
#include "faxexpand.h"

#define VERSION "2.3"

/* If moving the image around with the middle mouse button is jerky or
   slow, try defining USE_MOTIONHINT.  It may help (it may also make
   things worse - it depends on the server implementation) */
#undef USE_MOTIONHINT

struct pagenode *firstpage, *lastpage, *thispage, *helppage;
struct pagenode defaultpage;

/* access the 'extra' field in a pagenode */
#define Pimage(p)	((XImage *)(p)->extra)

/* getopt declarations */
extern int getopt();
extern char *optarg;
extern int optind, opterr, optopt;

/* forward declarations */
static XImage *FlipImage(XImage *xi);
static XImage *MirrorImage(XImage *xi);
static XImage *NewImage(int w, int h, char *data, int bit_order);
static XImage *RotImage(XImage *Image);
static XImage *ZoomImage(XImage *Big);
static void FreeImage(XImage *Image);
static int GetImage(struct pagenode *pn);
static void SetupDisplay(int argc, char **argv);
static void ShowLoop(void);
static int release(int quit);
static char *suffix(char *opt, const char *str);

/* X variables */
static char *DispName = NULL;
static char *Geometry = NULL;
static Display *Disp;
static Window Root;
static Window Win;
static int Default_Screen;
static GC PaintGC;
static Cursor WorkCursor;
static Cursor ReadyCursor;
static Cursor MoveCursor;
static Cursor LRCursor;
static Cursor UDCursor;

char *ProgName;
int verbose = 0;

static int abell = 0;			/* audio bell */
static int vbell = 1;			/* visual bell */
static int zfactor = 0;			/* zoom factor */

static size_t Memused = 0;		/* image memory usage */
static size_t Memlimit = 4*1024*1024;	/* try not to exceed */

#undef min
#undef max
#define min(a,b)	((a)<(b)?(a):(b))
#define max(a,b)	((a)>(b)?(a):(b))

#ifndef EXIT_FAILURE
#define EXIT_FAILURE 1
#endif

/* OK, OK - this is a dreadful hack.  But it adequately distinguishes
   modern big- and little- endian hosts.  We only need this to set the
   byte order in XImage structures */
static union { t32bits i; unsigned char b[4]; } bo;
#define ByteOrder	bo.b[3]

static char Banner[] =
"\nviewfax version " VERSION ", Copyright (C) 1990, 1995 Frank D. Cringle.\n"
"viewfax comes with ABSOLUTELY NO WARRANTY; for details see the\n"
"file \"COPYING\" in the distribution directory.\n\n";

static char Usage[] =
"usage: %s <flags> file ...\n"
"\t-f\tfine resolution (default unless filename begins with 'fn')\n"
"\t-n\tnormal resolution\n"
"\t-h\theight (number of fax lines)\n"
"\t-w\twidth (dots per fax line)\n"
"\t-l\tturn image 90 degrees (landscape mode)\n"
"\t-u\tturn image upside down\n"
"\t-i\tinvert (black/white)\n"
"\t-d\t(or -display) use an alternate X display\n"
"\t-g\t(or -geometry) size and position of window\n"
"\t-b\tuse audio (-ba) or visual (-bv) warning bell\n"
"\t-m\tmemory usage limit\n"
"\t-r\tfax data is packed ls-bit first in input bytes\n"
"\t-v\tverbose messages\n"
"\t-z\tinitial zoom factor\n"
"\t-2\traw files are g3-2d\n"
"\t-4\traw files are g4\n";

int
main(int argc, char **argv)
{
    int c;
    int err = 0;

    bo.i = 1;
    defaultpage.vres = -1;
    defaultpage.expander = g31expand;
    opterr = 0;			/* suppress getopt error message */

    if ((ProgName = strrchr(argv[0], '/')) == NULL)
	ProgName = argv[0];
    else
	ProgName++;
    while ((c = getopt(argc, argv, "b:d:fg:h:ilm:nruvw:z:24")) != -1)
	switch(c) {
	case 'b':
	    abell = vbell = 0;
	    while (*optarg) {
		abell |= (*optarg == 'a');
		vbell |= (*optarg == 'v');
		optarg++;
	    }
	    break;
	case 'd':		/* display name */
	    if (*(DispName = suffix(optarg, "isplay")) == 0)
		DispName = argv[optind++];
	    break;
	case 'f':		/* fine resolution */
	    defaultpage.vres = 1;
	    break;
	case 'g':		/* geometry */
	    if (*(Geometry = suffix(optarg, "eometry")) == 0)
		Geometry = argv[optind++];
	    break;
	case 'h':		/* user thinks this is the height */
	    defaultpage.height = atoi(optarg);
	    break;
	case 'i':		/* invert black/white */
	    defaultpage.inverse = 1;
	    break;
	case 'l':		/* landscape */
	    defaultpage.orient |= TURN_L;
	    break;
	case 'm':		/* memory usage limit */
	    Memlimit = atoi(optarg);
	    switch(optarg[strlen(optarg)-1]) {
	    case 'M':
	    case 'm':
		Memlimit *= 1024;
	    case 'K':
	    case 'k':
		Memlimit *= 1024;
	    }
	    break;
	case 'n':		/* normal resolution */
	    defaultpage.vres = 0;
	    break;
	case 'r':		/* reverse input bits */
	    defaultpage.lsbfirst = 1;
	    break;
	case 'u':		/* upside down */
	    defaultpage.orient |= TURN_U;
	    break;
	case 'v':		/* verbose messages */
	    verbose = 1;
	    break;
	case 'w':		/* user thinks this is the width */
	    defaultpage.width = atoi(optarg);
	    break;
	case 'z':		/* zoom factor */
	    c = atoi(optarg);
	    if (c <= 0)
		c = 1;
	    for (zfactor = 1; c > 1; c >>= 1)
		zfactor <<= 1;	/* constrain to a power of 2 */
	    break;
	case '2':
	    defaultpage.expander = g32expand;
	    break;
	case '4':
	    defaultpage.expander = g4expand;
	    break;
	default:
	    err++;
	    break;
	}

    if (defaultpage.expander == g4expand && defaultpage.height == 0) {
	fputs("-h value is required to interpret raw g4 faxes\n", stderr);
	err++;
    }

    if (verbose)
	fputs(Banner, stdout);

    firstpage = lastpage = thispage = helppage = NULL;
    for (; optind < argc; optind++)
	(void) notetiff(argv[optind]);

    if (err || firstpage == NULL) {
	if (!verbose)
	    fprintf(stderr, Banner);
	fprintf(stderr, Usage, ProgName);
	exit(EXIT_FAILURE);
    }

    if ((Disp = XOpenDisplay(DispName)) == NULL) {
	fprintf(stderr, "%s: can't open display %s\n", ProgName,
		DispName ? DispName : XDisplayName((char *) NULL));
	exit(EXIT_FAILURE);
    }
    Default_Screen = XDefaultScreen(Disp);
    faxinit();
    thispage = firstpage;
    while (!GetImage(firstpage))
	/* try again */;
    SetupDisplay(argc, argv);
    ShowLoop();
    return 0;
}

/* return mismatching suffix of option name */
static char *
suffix(char *opt, const char *prefix)
{
    while (*opt && *opt == *prefix) {
	opt++; prefix++;
    }
    return opt;
}

/* Change orientation of all following pages */
static void
TurnFollowing(int How, struct pagenode *pn)
{
    while (pn) {
	if (Pimage(pn)) {
	    FreeImage(Pimage(pn));
	    pn->extra = NULL;
	}
	pn->orient ^= How;
	pn = pn->next;
    }
}

static void
drawline(pixnum *run, int LineNum, struct pagenode *pn)
{
    t32bits *p, *p1;		/* p - current line, p1 - low-res duplicate */
    pixnum *r;			/* pointer to run-lengths */
    t32bits pix;		/* current pixel value */
    t32bits acc;		/* pixel accumulator */
    int nacc;			/* number of valid bits in acc */
    int tot;			/* total pixels in line */
    int n;

    LineNum += pn->stripnum * pn->rowsperstrip;
    p = (t32bits *) (Pimage(pn)->data + LineNum*(2-pn->vres)*Pimage(pn)->bytes_per_line);
    p1 = pn->vres ? NULL : p + Pimage(pn)->bytes_per_line/sizeof(*p);
    r = run;
    acc = 0;
    nacc = 0;
    pix = pn->inverse ? ~0 : 0;
    tot = 0;
    while (tot < pn->width) {
	n = *r++;
	tot += n;
	if (pix)
	    acc |= (~(t32bits)0 >> nacc);
	else if (nacc)
	    acc &= (~(t32bits)0 << (32 - nacc));
	else
	    acc = 0;
	if (nacc + n < 32) {
	    nacc += n;
	    pix = ~pix;
	    continue;
	}
	*p++ = acc;
	if (p1)
	    *p1++ = acc;
	n -= 32 - nacc;
	while (n >= 32) {
	    n -= 32;
	    *p++ = pix;
	    if (p1)
		*p1++ = pix;
	}
	acc = pix;
	nacc = n;
	pix = ~pix;
    }
    if (nacc) {
	*p++ = acc;
	if (p1)
	    *p1++ = acc;
    }
}

static int
GetPartImage(struct pagenode *pn, int n)
{
    unsigned char *Data = getstrip(pn, n);

    if (Data == NULL)
	return 0;
    pn->stripnum = n;
    (*pn->expander)(pn, drawline);
    free(Data);
    return 1;
}

static int
GetImage(struct pagenode *pn)
{
    int i;

    if (pn->strips == NULL) {
	/* raw file; maybe we don't have the height yet */
	unsigned char *Data = getstrip(pn, 0);
	if (Data == NULL)
	    return 0;
	pn->extra = NewImage(pn->width, pn->vres ?
			     pn->height : 2*pn->height, NULL, 1);
	(*pn->expander)(pn, drawline);
    }
    else {
	/* multi-strip tiff */
	pn->extra = NewImage(pn->width, pn->vres ?
			     pn->height : 2*pn->height, NULL, 1);
	pn->stripnum = 0;
	for (i = 0; i < pn->nstrips; i++)
	    if (GetPartImage(pn, i) == 0) {
		FreeImage(Pimage(pn));
		return 0;
	    }
    }
    if (pn->orient & TURN_U)
	pn->extra = FlipImage(Pimage(pn));
    if (pn->orient & TURN_M)
	pn->extra = MirrorImage(Pimage(pn));
    if (pn->orient & TURN_L)
	pn->extra = RotImage(Pimage(pn));
    if (verbose) printf("\tmemused = %d\n", Memused);
    return 1;
}

#ifndef _HAVE_USLEEP
static int
usleep(unsigned usecs)
{
    struct timeval t;

    t.tv_sec = usecs/10000000;
    t.tv_usec = usecs%1000000;
    (void) select(1, NULL, NULL, NULL, &t);
    return 0;
}
#endif

#ifndef REAL_ROOT
/* Function Name: GetVRoot
 * Description: Gets the root window, even if it's a virtual root
 * Arguments: the display and the screen
 * Returns: the root window for the client
 *
 * by David Elliott, taken from the x-faq
 */
static Window
GetVRoot(Display *dpy, int scr)
{
    Window rootReturn, parentReturn, *children;
    unsigned int numChildren;
    Window root = RootWindow(dpy, scr);
    Atom __SWM_VROOT = None;
    int i;
 
    __SWM_VROOT = XInternAtom(dpy, "__SWM_VROOT", False);
    XQueryTree(dpy, root, &rootReturn, &parentReturn, &children,
	       &numChildren);
    for (i = 0; i < numChildren; i++) {
        Atom actual_type;
        int actual_format;
        unsigned long nitems, bytesafter;
        Window *newRoot = NULL;
 
        if (XGetWindowProperty(dpy, children[i], __SWM_VROOT, 0, 1,
			       False, XA_WINDOW, &actual_type,
			       &actual_format, &nitems, &bytesafter,
			       (unsigned char **) &newRoot)
					== Success && newRoot) {
	    if (children) XFree(children);
	    return *newRoot;
	}
    }
    return root;
}
#endif

static Atom wm_delete_window;

/* Area the user would like us to use, derived from -geometry */
static struct {
    int v, x, y;
    unsigned int w, h;
} Area = {0, 0, 0};

/* nominal border width */
#define BW 4

/* Figure out the zoom factor needed to fit the fax on the available display */
static void
SetupDisplay(int argc, char **argv)
{
    int Width, Height, i;
    XSetWindowAttributes Attr;
    XSizeHints size_hints;
    Atom wm_protocols;
    int faxh = Pimage(thispage)->height;
    int faxw = Pimage(thispage)->width;

#ifdef REAL_ROOT
    Root = RootWindow(Disp, Default_Screen);
    Width = Area.w = DisplayWidth(Disp, Default_Screen);
    Height = Area.h = DisplayHeight(Disp, Default_Screen);
#elif TVTWM_BIGWINDOW
    XWindowAttributes RootWA;
    Root = GetVRoot(Disp, Default_Screen);
    XGetWindowAttributes(Disp, Root, &RootWA);
    Width = Area.w = RootWA.width;
    Height = Area.h = RootWA.height;
#else
    Root = GetVRoot(Disp, Default_Screen);
    Width = Area.w = DisplayWidth(Disp, Default_Screen);
    Height = Area.h = DisplayHeight(Disp, Default_Screen);
#endif
    if (Geometry)
	Area.v = XParseGeometry(Geometry, &Area.x, &Area.y,
				&Area.w, &Area.h);
    Area.w = max(64, Area.w);
    Area.h = max(64, Area.h);

    if (zfactor == 0)
	for (zfactor = 1;
	     faxw / zfactor > Area.w ||
	     faxh / zfactor > Area.h;
	     zfactor *= 2)
	    ;
    Attr.background_pixel = WhitePixel(Disp, Default_Screen);
    Attr.border_pixel = BlackPixel(Disp, Default_Screen);

    for (size_hints.width = faxw, i = 1; i < zfactor; i *= 2)
	size_hints.width = (size_hints.width + 1) /2;
    for (size_hints.height = faxh, i = 1; i < zfactor; i *= 2)
	size_hints.height = (size_hints.height + 1) /2;

    switch (Area.v & (XValue|XNegative)) {
    case XValue:
	size_hints.x = Area.x + BW;
	break;
    case XValue|XNegative:
	Area.x = Width + Area.x - 2*BW - Area.w;
	size_hints.x = Area.x + Area.w - size_hints.width;
	break;
    default:
	size_hints.x = Area.x + (Area.w - size_hints.width)/2;
    }
    switch (Area.v & (YValue|YNegative)) {
    case YValue:
	size_hints.y = Area.y + BW;
	break;
    case YValue|YNegative:
	Area.y = Height + Area.y - 2*BW - Area.h;
	size_hints.y = Area.y + Area.h - size_hints.height;
	break;
    default:
	size_hints.y = Area.y + (Area.h - size_hints.height)/2;
    }

    size_hints.max_width = size_hints.width;
    size_hints.max_height = size_hints.height;
    size_hints.flags = PSize|PMaxSize;
    if (Area.v & (XValue|YValue)) size_hints.flags |= USPosition;
    if (Area.v & (HeightValue|WidthValue)) size_hints.flags |= USSize;
	
    Win = XCreateWindow(Disp, Root, size_hints.x, size_hints.y,
			size_hints.width, size_hints.height,
			BW, 0, InputOutput, CopyFromParent,
			CWBackPixel|CWBorderPixel, &Attr);
#ifdef PWinGravity
{
    XWMHints wm_hints;
    XClassHint class_hints;
    XTextProperty windowName, iconName;

    if (!XStringListToTextProperty(&thispage->name, 1, &windowName) ||
	!XStringListToTextProperty(&ProgName, 1, &iconName)) {
	fprintf(stderr, "%s: can't make window/icon name\n", ProgName);
	exit(EXIT_FAILURE);
    }
    wm_hints.initial_state = NormalState;
    wm_hints.input = True;
    wm_hints.flags = StateHint|InputHint;
    class_hints.res_name = ProgName;
    class_hints.res_class = "Faxview";
    XSetWMProperties(Disp, Win, &windowName, &iconName, argv, argc,
		     &size_hints, &wm_hints, &class_hints);
}
#else
    XSetStandardProperties(Disp, Win, thispage->name, ProgName,
			   None, argv, argc, &size_hints);
#endif

    PaintGC = XCreateGC(Disp, Win, 0L, (XGCValues *) NULL);
    XSetForeground(Disp, PaintGC, BlackPixel(Disp, Default_Screen));
    XSetBackground(Disp, PaintGC, WhitePixel(Disp, Default_Screen));
    XSetFunction(Disp, PaintGC, GXcopy);
    WorkCursor = XCreateFontCursor(Disp, XC_watch);
    ReadyCursor = XCreateFontCursor(Disp, XC_plus);
    MoveCursor = XCreateFontCursor(Disp, XC_fleur);
    LRCursor = XCreateFontCursor(Disp, XC_sb_h_double_arrow);
    UDCursor = XCreateFontCursor(Disp, XC_sb_v_double_arrow);
    XSelectInput(Disp, Win, Button2MotionMask | ButtonPressMask |
		 ButtonReleaseMask | ExposureMask | KeyPressMask |
		 SubstructureNotifyMask | OwnerGrabButtonMask |
#ifdef USE_MOTIONHINT
		 PointerMotionHintMask |
#endif
		 StructureNotifyMask);
    wm_protocols = XInternAtom(Disp, "WM_PROTOCOLS", False);
    wm_delete_window = XInternAtom(Disp, "WM_DELETE_WINDOW", False);
    XChangeProperty(Disp, Win, wm_protocols, XA_ATOM, 32,
                    PropModeAppend, (unsigned char * ) &wm_delete_window, 1);
    XMapRaised(Disp, Win);
}

#define MAXZOOM	10

/* After requesting a window size change, we throw away key and button presses
   until we get the notification that the size has changed.  If for some
   reason the notification does not come, we resume processing as normal after
   PATIENCE milliseconds */
#define PATIENCE 10000

static void
ShowLoop(void)
{
    XEvent Event;

    /* centre of image within window */
    int x = 0, ox = 0, offx = 0, nx;	/* x, old x, offset x, new x */
    int y = 0, oy = 0, offy = 0, ny;	/* y, old y, offset y, new y */

    int oz, Resize = 0, Refresh = 0;	/* old zoom, window size changed,
					   needs updating */
    int PaneWidth, PaneHeight;		/* current size of our window */
    int AbsX, AbsY;		/* absolute position of centre of window */
    int FrameWidth, FrameHeight, FrameX, FrameY;/* size/offset of decoration */
    int Oversize = 0;		/* window manager insists on oversize window */
    int Reparented = 0;
    int i;
    XImage *Image, *Images[MAXZOOM];
    struct pagenode *viewpage = NULL;	/* page viewed when help requested */
    XSizeHints size_hints;
    Time Lasttime = 0;		/* time of last accepted key/button press */
    int ExpectConfNotify = 1;

    XDefineCursor(Disp, Win, WorkCursor);
    XFlush(Disp);
    for (oz = 0; oz < MAXZOOM; oz++)
	Images[oz] = NULL;
    Image = Images[0] = Pimage(thispage);
    for (oz = 0; oz < MAXZOOM && zfactor > (1 << oz); oz++)
	Images[oz+1] = ZoomImage(Images[oz]);
    Image = Images[oz];

    /* some reasonable values,
       just in case we do not get a configurenotify first */
    AbsX = Area.w/2;
    AbsY = Area.h/2;
    FrameWidth = FrameHeight = FrameX = FrameY = 0;
    PaneWidth = Image->width;
    PaneHeight = Image->height;

    XDefineCursor(Disp, Win, ReadyCursor);

    for (;;) {
	XNextEvent(Disp, &Event);
	do {
	    switch(Event.type) {
	    case MappingNotify:
		XRefreshKeyboardMapping((XMappingEvent *)(&Event));
		break;
	    case ClientMessage:
		if (Event.xclient.data.l[0] == wm_delete_window) {
		    XCloseDisplay(Disp);
		    exit(EXIT_FAILURE);
		}
		break;
	    case Expose:
	    {
		XExposeEvent *p = (XExposeEvent *) &Event;
		XPutImage(Disp, Win, PaintGC, Image,
			  p->x + x - PaneWidth/2,
			  p->y + y - PaneHeight/2,
			  p->x, p->y,
			  p->width, p->height);
	    }
		break;
	    case ReparentNotify:
	    {
		Window Myroot = Root;
		Window Parent = Event.xreparent.parent;
		Window Frame = Parent;	/* I should be so lucky! */
		Window *Mykids;
		unsigned int Nkids;
		XWindowAttributes MyWA, FrameWA;

		if (Parent != Root)
		    do {
			Frame = Parent;
			while (!XQueryTree(Disp, Frame, &Myroot,
					   &Parent, &Mykids, &Nkids))
			    release(1);
			if (Mykids) XFree(Mykids);
		    } while (Parent != Root);
		while (!XGetWindowAttributes(Disp, Win, &MyWA))
		    release(1);
		while (!XGetWindowAttributes(Disp, Frame, &FrameWA))
		    release(1);
		/* if area is partly constrained, stay where the WM put you */
		if ((Area.v & (XValue|WidthValue)) == WidthValue) {
		    Area.v |= XValue;
		    Area.x = FrameWA.x;
		}
		if ((Area.v & (YValue|HeightValue)) == HeightValue) {
		    Area.v |= YValue;
		    Area.y = FrameWA.y;
		}
		XTranslateCoordinates(Disp, Win, Frame, 0, 0,
				      &FrameX, &FrameY, &Parent);
		FrameWidth = FrameWA.width - MyWA.width;
		FrameHeight = FrameWA.height - MyWA.height;
		Reparented = ExpectConfNotify = 1;
	    }
		break;
	    case ConfigureNotify:
	    {
		XConfigureEvent *p = (XConfigureEvent *) &Event;
		int NewX = AbsX;
		int NewY = AbsY;
#ifdef REAL_ROOT
		if (p->send_event || !Reparented) {
		    NewX = p->x + p->width/2;
		    NewY = p->y + p->height/2;
		}
#else
		/* support tvtwm */
		if (!Reparented) {
		    NewX = p->x + p->border_width + p->width/2;
		    NewY = p->y + p->border_width + p->height/2;
		}
		else if (p->send_event) {
		    /* the event info is viewport-relative, we need absolute */
		    Window w;
		    XTranslateCoordinates(Disp, Win, Root, 0, 0,
					  &NewX, &NewY, &w);
		    NewX += p->width/2;
		    NewY += p->height/2;
		}
#endif
		if (!ExpectConfNotify) {
		    /* user intervention */
		    if (PaneWidth != p->width)
			Area.w = p->width + FrameWidth;
		    if (PaneHeight != p->height)
			Area.h = p->height + FrameHeight;
		    if (NewX != AbsX || NewY != AbsY) {
			Area.x = NewX - p->width/2 - FrameX;
			Area.y = NewY - p->height/2 - FrameY;
		    }
		}
		AbsX = NewX; AbsY = NewY;
		PaneWidth = p->width;
		PaneHeight = p->height;
		Oversize = PaneWidth > Image->width ||
		    PaneHeight > Image->height;
		ExpectConfNotify = 0;
	    }
		break;
	    case KeyPress:
		if (ExpectConfNotify &&
		    (Event.xkey.time < (Lasttime + PATIENCE)))
		    break;
		Lasttime = Event.xkey.time;
		ExpectConfNotify = 0;
		switch(XKeycodeToKeysym(Disp, Event.xkey.keycode, 0)) {
		case XK_Help:
		case XK_h:
		    if (helppage == NULL) {
			if (!notetiff(HELPFILE))
			    goto nopage;
			else {
			    helppage = lastpage;
			    lastpage = helppage->prev;
			    lastpage->next = helppage->prev = NULL;
			}
		    }
		    viewpage = thispage;
		    thispage = helppage;
		    goto newpage;
		    break;
		case XK_m:
		    XDefineCursor(Disp, Win, WorkCursor);
		    XFlush(Disp);
		    thispage->extra = Images[0] = MirrorImage(Images[0]);
		    thispage->orient ^= TURN_M;
		    for (i = 1; Images[i]; i++) {
			FreeImage(Images[i]);
			Images[i] = ZoomImage(Images[i-1]);
		    }
		    Image = Images[oz];
		    if (Event.xkey.state & ShiftMask)
			TurnFollowing(TURN_M, thispage->next);
		    XPutImage(Disp, Win, PaintGC, Image,
			      x-PaneWidth/2, y-PaneHeight/2,
			      0, 0, PaneWidth, PaneHeight);
		    XDefineCursor(Disp, Win, ReadyCursor);
		    break;
		case XK_z:
		    if (Event.xkey.state & ShiftMask)
			goto Zoomout;
		    else
			goto Zoomin;
		case XK_Up:
		    y -= PaneHeight / 2;
		    break;
		case XK_Down:
		    y += PaneHeight / 2;
		    break;
		case XK_Left:
		    x -= PaneWidth / 2;
		    break;
		case XK_Right:
		    x += PaneWidth / 2;
		    break;
		case XK_Home:
		case XK_R7:		/* sun4 keyboard */
		    if (Event.xkey.state & ShiftMask) {
			thispage = firstpage;
			goto newpage;
		    }
		    x = 0;
		    y = 0;
		    break;
		case XK_End:
		case XK_R13:
		    if (Event.xkey.state & ShiftMask) {
			thispage = lastpage;
			goto newpage;
		    }
		    x = Image->width;
		    y = Image->height;
		    break;
		case XK_l:
		    XDefineCursor(Disp, Win, WorkCursor);
		    XFlush(Disp);
		    thispage->extra = Image = RotImage(Images[0]);
		    thispage->orient ^= TURN_L;
		    for (i = 1; Images[i]; i++) {
			FreeImage(Images[i]);
			Images[i] = NULL;
		    }
		    Images[0] = Image;
		    for (i = 1; i <= oz; i++)
			Images[i] = ZoomImage(Images[i-1]);
		    Image = Images[oz];
		    if (Event.xkey.state & ShiftMask)
			TurnFollowing(TURN_L, thispage->next);
		{ int t = x; x = y; y = t; }
		    Refresh = Resize = 1;
		    XDefineCursor(Disp, Win, ReadyCursor);
		    break;
		case XK_p:
		case XK_minus:
		case XK_Prior:
		case XK_R9:
		case XK_BackSpace:
		    if (thispage->prev == NULL)
			goto nopage;
		    thispage = thispage->prev;
		    goto newpage;
		case XK_n:
		case XK_plus:
		case XK_space:
		case XK_Next:
		case XK_R15:
		    if (thispage->next == NULL) {
		    nopage:
			if (abell) {
			    putchar('\a');
			    fflush(stdout);
			}
			if (vbell) {
			    XAddPixel(Image, 1);
			    XPutImage(Disp, Win, PaintGC, Image,
				      x-PaneWidth/2, y-PaneHeight/2,
				      0, 0, PaneWidth, PaneHeight);
			    XSync(Disp, 0);
			    (void) usleep(200000);
			    XAddPixel(Image, 1);
			    XPutImage(Disp, Win, PaintGC, Image,
				      x-PaneWidth/2, y-PaneHeight/2,
				      0, 0, PaneWidth, PaneHeight);
			}
			break;
		    }
		    thispage = thispage->next;
		newpage:
		    XDefineCursor(Disp, Win, WorkCursor);
		    XFlush(Disp);
		    /* if old image was not resized by the user, fit new one */
		    Resize = ((PaneWidth == Image->width ||
			       PaneWidth == Area.w - FrameWidth) &&
			      (PaneHeight == Image->height ||
			       PaneHeight == Area.h - FrameHeight));
		    for (i = 1; Images[i]; i++) {
			FreeImage(Images[i]);
			Images[i] = NULL;
		    }
		    if (Pimage(thispage) == NULL)
			while (!GetImage(thispage))
			    /* try again */;
		    Images[0] = Pimage(thispage);
		    XStoreName(Disp, Win, thispage->name);
		    for (i = 1; i <= oz; i++)
			Images[i] = ZoomImage(Images[i-1]);
		    Image = Images[oz];
		    Refresh = 1;
		    XDefineCursor(Disp, Win, ReadyCursor);
		    break;
		case XK_u:
		    XDefineCursor(Disp, Win, WorkCursor);
		    XFlush(Disp);
		    thispage->extra = Images[0] = FlipImage(Images[0]);
		    thispage->orient ^= TURN_U;
		    for (i = 1; Images[i]; i++) {
			FreeImage(Images[i]);
			Images[i] = ZoomImage(Images[i-1]);
		    }
		    Image = Images[oz];
		    if (Event.xkey.state & ShiftMask)
			TurnFollowing(TURN_U, thispage->next);
		    XPutImage(Disp, Win, PaintGC, Image,
			      x-PaneWidth/2, y-PaneHeight/2,
			      0, 0, PaneWidth, PaneHeight);
		    XDefineCursor(Disp, Win, ReadyCursor);
		    break;
		case XK_q:
		    if (viewpage) {
			thispage = viewpage;
			viewpage = NULL;
			goto newpage;
		    }
		    XCloseDisplay(Disp);
#ifdef xmalloc
		    malloc_shutdown();
#endif
		    exit((Event.xkey.state & ShiftMask) ? EXIT_FAILURE : 0);
		}
		break;
	    case ButtonPress:
		if (ExpectConfNotify &&
		    (Event.xbutton.time < (Lasttime + PATIENCE)))
		    break;
		Lasttime = Event.xbutton.time;
		ExpectConfNotify = 0;
		switch (Event.xbutton.button) {
		case Button1:
		Zoomout:
		    if (oz > 0) {
			Image = Images[--oz];
			zfactor >>= 1;
			x *= 2;
			y *= 2;
			Resize = Refresh = 1;
		    }
		    break;
		case Button2:
		    switch (((Image->width > PaneWidth)<<1) |
			    (Image->height > PaneHeight)) {
		    case 0:
			break;
		    case 1:
			XDefineCursor(Disp, Win, UDCursor);
			break;
		    case 2:
			XDefineCursor(Disp, Win, LRCursor);
			break;
		    case 3:
			XDefineCursor(Disp, Win, MoveCursor);
		    }
		    XFlush(Disp);
		    offx = Event.xbutton.x;
		    offy = Event.xbutton.y;
		    break;
		case Button3:
		Zoomin:
		    if (oz < MAXZOOM && Image->width >= 64 && zfactor < 32) {
			Image = Images[++oz];
			zfactor <<= 1;
			x /= 2;
			y /= 2;
			Resize = Refresh = 1;
		    }
		    break;
		}
		if (Image == NULL) {
		    for (i = oz; i && (Images[i] == NULL); i--)
			;
		    for (; i != oz; i++)
			Images[i+1] = ZoomImage(Images[i]);
		    Image = Images[oz];
		}
		break;
	    case MotionNotify:
#ifdef USE_MOTIONHINT
	    {
		unsigned int Junk;
		Window JunkW;
		XQueryPointer(Disp, Event.xmotion.window, &JunkW, &JunkW,
			      &Junk, &Junk, &nx, &ny, &Junk);
	    }
#else
		do {
		    nx = Event.xmotion.x;
		    ny = Event.xmotion.y;
		} while (XCheckTypedEvent(Disp, MotionNotify, &Event));
#endif
		x += offx - nx;
		y += offy - ny;
		offx = nx;
		offy = ny;
		break;
	    case ButtonRelease:
		if (Event.xbutton.button == Button2) {
		    XDefineCursor(Disp, Win, ReadyCursor);
		    XFlush(Disp);
		}
	    }
	} while (XCheckWindowEvent(Disp, Win,
				   KeyPressMask|ButtonPressMask, &Event));

	/* if someone thinks we should resize the window and it is not
	   already the right size, or if the window is too big and we
	   have not already tried to make it smaller ... */
	if ((Resize && !(Image->width == PaneWidth &&
			 Image->height == PaneHeight)) ||
	    (!Oversize && (Image->width < PaneWidth ||
			   Image->height < PaneHeight))) {
	    int PosX = AbsX - PaneWidth/2 - FrameX;
	    int PosY = AbsY - PaneHeight/2 - FrameY;
	    XWindowChanges New;
	    int ChangeMask = 0;

	    New.width = min(Area.w - FrameWidth, Image->width);
	    New.height = min(Area.h - FrameHeight, Image->height);
	    /* expect an expose if size must change */
	    Refresh &= (New.width == PaneWidth && New.height == PaneHeight);
	    New.x = max(Area.x, AbsX-New.width/2-FrameX);
	    New.x = min(New.x, Area.w-(New.width+FrameWidth)+Area.x);
	    New.y = max(Area.y, AbsY-New.height/2-FrameY);
	    New.y = min(New.y, Area.h-(New.height+FrameHeight)+Area.y);

	    /* mwm takes max_size very seriously! */
	    size_hints.flags = 0;
	    XSetNormalHints(Disp, Win, &size_hints);

	    size_hints.flags = PMaxSize;
	    size_hints.x = PosX;
	    size_hints.y = PosY;
	    size_hints.width = PaneWidth;
	    size_hints.height = PaneHeight;
	    /* only move a coordinate if the ideal new value is different
	       from the current value and either the other dimension has
	       changed or the current value is out of area */
	    if (PosX != New.x &&
		(New.height != PaneHeight || PosX < Area.x || 
		 PosX > Area.w-New.width-FrameWidth-Area.x)) {
		ChangeMask |= CWX;
		size_hints.x = New.x;
		size_hints.flags |= PPosition;
	    }
	    if (PosY != New.y &&
		(New.width != PaneWidth || PosY < Area.y ||
		 PosY > Area.h-New.height-FrameHeight-Area.y)) {
		ChangeMask |= CWY;
		size_hints.y = New.y;
		size_hints.flags |= PPosition;
	    }
	    if (New.width != PaneWidth) {
		ChangeMask |= CWWidth;
		size_hints.width = New.width;
		size_hints.flags |= PSize;
		ExpectConfNotify = Reparented;
	    }
	    if (New.height != PaneHeight) {
		ChangeMask |= CWHeight;
		size_hints.height = New.height;
		size_hints.flags |= PSize;
		ExpectConfNotify = Reparented;
	    }
	    New.border_width = 1;	/* ICCCM 4.1.5 */
	    ChangeMask |= CWBorderWidth;
	    XConfigureWindow(Disp, Win, ChangeMask, &New);
	    size_hints.max_width = Image->width;
	    size_hints.max_height = Image->height;
	    XSetNormalHints(Disp, Win, &size_hints);
	}
	x = max(x, PaneWidth/2);
	x = min(x, Image->width-PaneWidth/2);
	y = max(y, PaneHeight/2);
	y = min(y, Image->height-PaneHeight/2);
	if (x != ox || y != oy || Refresh)
	    XPutImage(Disp, Win, PaintGC, Image,
		      x-PaneWidth/2, y-PaneHeight/2,
		      0, 0, PaneWidth, PaneHeight);
	ox = x;
	oy = y;
	Resize = Refresh = 0;
    }
}

/* run this region through perl to generate the zoom table:
$lim = 1;
@c = ("0", "1", "1", "2");
print "static unsigned char Z[] = {\n";
for ($i = 0; $i < 16; $i++) {
    for ($j = 0; $j < 16; $j++) {
	$b1 = ($c[$j&3]+$c[$i&3]) > $lim;
	$b2 = ($c[($j>>2)&3]+$c[($i>>2)&3]) > $lim;
	printf " %X,", ($b2 << 1) | $b1;
    }
    print "\n";
}
print "};\n";
*/
static unsigned char Z[] = {
 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 2, 2, 2, 3,
 0, 1, 1, 1, 0, 1, 1, 1, 0, 1, 1, 1, 2, 3, 3, 3,
 0, 1, 1, 1, 0, 1, 1, 1, 0, 1, 1, 1, 2, 3, 3, 3,
 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 3, 3, 3, 3,
 0, 0, 0, 1, 2, 2, 2, 3, 2, 2, 2, 3, 2, 2, 2, 3,
 0, 1, 1, 1, 2, 3, 3, 3, 2, 3, 3, 3, 2, 3, 3, 3,
 0, 1, 1, 1, 2, 3, 3, 3, 2, 3, 3, 3, 2, 3, 3, 3,
 1, 1, 1, 1, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3,
 0, 0, 0, 1, 2, 2, 2, 3, 2, 2, 2, 3, 2, 2, 2, 3,
 0, 1, 1, 1, 2, 3, 3, 3, 2, 3, 3, 3, 2, 3, 3, 3,
 0, 1, 1, 1, 2, 3, 3, 3, 2, 3, 3, 3, 2, 3, 3, 3,
 1, 1, 1, 1, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3,
 2, 2, 2, 3, 2, 2, 2, 3, 2, 2, 2, 3, 2, 2, 2, 3,
 2, 3, 3, 3, 2, 3, 3, 3, 2, 3, 3, 3, 2, 3, 3, 3,
 2, 3, 3, 3, 2, 3, 3, 3, 2, 3, 3, 3, 2, 3, 3, 3,
 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3,
};

#define nib(n,w)	(((w)>>((n)<<2))&15)
#define zak(a,b)	Z[(a<<4)|b]

/* 2 -> 1 zoom, 4 pixels -> 1 pixel
   if #pixels <= $lim (see above), new pixel is white,
   else black.
*/
static XImage *
ZoomImage(XImage *Big)
{
    XImage *Small;
    int w, h;
    int i, j;

    XDefineCursor(Disp, Win, WorkCursor);
    XFlush(Disp);
    w = (Big->width+1) / 2;
    h = (Big->height+1) / 2;
    Small = NewImage(w, h, NULL, Big->bitmap_bit_order);
    Small->xoffset = (Big->xoffset+1)/2;
    for (i = 0; i < Big->height; i += 2) {
	t32bits *pb0 = (t32bits *) (Big->data + i * Big->bytes_per_line);
	t32bits *pb1 = pb0 + ((i == Big->height-1) ? 0 : Big->bytes_per_line/4);
	t32bits *ps = (t32bits *) (Small->data + i * Small->bytes_per_line / 2);
	for (j = 0; j < Big->bytes_per_line/8; j++) {
	    t32bits r1, r2;
	    t32bits t0 = *pb0++;
	    t32bits t1 = *pb1++;
	    r1 = (zak(nib(7,t0),nib(7,t1))<<14) |
		 (zak(nib(6,t0),nib(6,t1))<<12) |
		 (zak(nib(5,t0),nib(5,t1))<<10) |
		 (zak(nib(4,t0),nib(4,t1))<<8) |
		 (zak(nib(3,t0),nib(3,t1))<<6) |
		 (zak(nib(2,t0),nib(2,t1))<<4) |
		 (zak(nib(1,t0),nib(1,t1))<<2) |
		 (zak(nib(0,t0),nib(0,t1)));
	    t0 = *pb0++;
	    t1 = *pb1++;
	    r2 = (zak(nib(7,t0),nib(7,t1))<<14) |
		 (zak(nib(6,t0),nib(6,t1))<<12) |
		 (zak(nib(5,t0),nib(5,t1))<<10) |
		 (zak(nib(4,t0),nib(4,t1))<<8) |
		 (zak(nib(3,t0),nib(3,t1))<<6) |
		 (zak(nib(2,t0),nib(2,t1))<<4) |
		 (zak(nib(1,t0),nib(1,t1))<<2) |
		 (zak(nib(0,t0),nib(0,t1)));
	    *ps++ = (Big->bitmap_bit_order) ?
		(r1<<16)|r2 : (r2<<16)|r1;
	}
	for ( ; j < Small->bytes_per_line/4; j++) {
	    t32bits r1;
	    t32bits t0 = *pb0++;
	    t32bits t1 = *pb1++;
	    r1 = (zak(nib(7,t0),nib(7,t1))<<14) |
		 (zak(nib(6,t0),nib(6,t1))<<12) |
		 (zak(nib(5,t0),nib(5,t1))<<10) |
		 (zak(nib(4,t0),nib(4,t1))<<8) |
		 (zak(nib(3,t0),nib(3,t1))<<6) |
		 (zak(nib(2,t0),nib(2,t1))<<4) |
		 (zak(nib(1,t0),nib(1,t1))<<2) |
		 (zak(nib(0,t0),nib(0,t1)));
	    *ps++ = (Big->bitmap_bit_order) ?
		(r1<<16) : r1;
	}
    }
    XDefineCursor(Disp, Win, ReadyCursor);
    return Small;
}

static XImage *
FlipImage(XImage *Image)
{
    XImage *New = NewImage(Image->width, Image->height,
			   Image->data, !Image->bitmap_bit_order);
    t32bits *p1 = (t32bits *) Image->data;
    t32bits *p2 = (t32bits *) (Image->data + Image->height *
			     Image->bytes_per_line - 4);

    /* the first shall be last ... */
    while (p1 < p2) {
	t32bits t = *p1;
	*p1++ = *p2;
	*p2-- = t;
    }

    /* let Xlib twiddle the bits */
    New->xoffset = 32 - (Image->width & 31) - Image->xoffset;
    New->xoffset &= 31;

    Image->data = NULL;
    FreeImage(Image);
    return New;
}

static XImage *
MirrorImage(XImage *Image)
{
    int i;
    XImage *New = NewImage(Image->width, Image->height,
			   Image->data, !Image->bitmap_bit_order);

    /* reverse order of 32-bit words in each line */
    for (i = 0; i < Image->height; i++) {
	t32bits *p1 = (t32bits *) (Image->data + Image->bytes_per_line * i);
	t32bits *p2 = p1 + Image->bytes_per_line/4 - 1;
	while (p1 < p2) {
	    t32bits t = *p1;
	    *p1++ = *p2;
	    *p2-- = t;
	}
    }

    /* let Xlib twiddle the bits */
    New->xoffset = 32 - (Image->width & 31) - Image->xoffset;
    New->xoffset &= 31;

    Image->data = NULL;
    FreeImage(Image);
    return New;
}

static XImage *
RotImage(XImage *Image)
{
    XImage *New;
    int w = Image->height;
    int h = Image->width;
    int i, j, k, shift;
    int order = Image->bitmap_bit_order;
    int offs = h+Image->xoffset-1;

    New = NewImage(w, h, NULL, 1);

    k = (32 - Image->xoffset) & 3;
    for (i = h - 1; i && k; i--, k--) {
	t32bits *sp = (t32bits *) Image->data + (offs-i)/32;
	t32bits *dp = (t32bits *) (New->data+i*New->bytes_per_line);
	t32bits d0;
	shift = (offs-i)&31;
	if (order) shift = 31-shift;
	for (j = 0; j < w; j++) {
	    t32bits t = *sp;
	    sp += Image->bytes_per_line/4;
	    d0 |= ((t >> shift) & 1);
	    if ((j & 31) == 31)
		*dp++ = d0;
	    d0 <<= 1;;
	}
	if (j & 31)
	    *dp++ = d0<<(31-j);
    }
    for ( ; i >= 3; i-=4) {
	t32bits *sp = (t32bits *) Image->data + (offs-i)/32;
	t32bits *dp0 = (t32bits *) (New->data+i*New->bytes_per_line);
	t32bits *dp1 = dp0 - New->bytes_per_line/4;
	t32bits *dp2 = dp1 - New->bytes_per_line/4;
	t32bits *dp3 = dp2 - New->bytes_per_line/4;
	t32bits d0, d1, d2, d3;
	shift = (offs-i)&31;
	if (order) shift = 28-shift;
	for (j = 0; j < w; j++) {
	    t32bits t = *sp >> shift;
	    sp += Image->bytes_per_line/4;
	    d0 |= t & 1; t >>= 1;
	    d1 |= t & 1; t >>= 1;
	    d2 |= t & 1; t >>= 1;
	    d3 |= t & 1; t >>= 1;
	    if ((j & 31) == 31) {
		if (order) {
		    *dp0++ = d3;
		    *dp1++ = d2;
		    *dp2++ = d1;
		    *dp3++ = d0;
		}
		else {
		    *dp0++ = d0;
		    *dp1++ = d1;
		    *dp2++ = d2;
		    *dp3++ = d3;
		}
	    }
	    d0 <<= 1; d1 <<= 1; d2 <<= 1; d3 <<= 1;
	}
	if (j & 31) {
	    if (order) {
		*dp0++ = d3<<(31-j);
		*dp1++ = d2<<(31-j);
		*dp2++ = d1<<(31-j);
		*dp3++ = d0<<(31-j);
	    }
	    else {
		*dp0++ = d0<<(31-j);
		*dp1++ = d1<<(31-j);
		*dp2++ = d2<<(31-j);
		*dp3++ = d3<<(31-j);
	    }
	}
    }
    for (; i >= 0; i--) {
	t32bits *sp = (t32bits *) Image->data + (offs-i)/32;
	t32bits *dp = (t32bits *) (New->data+i*New->bytes_per_line);
	t32bits d0;
	shift = (offs-i)&31;
	if (order) shift = 31-shift;
	for (j = 0; j < w; j++) {
	    t32bits t = *sp;
	    sp += Image->bytes_per_line/4;
	    d0 |= ((t >> shift) & 1);
	    if ((j & 31) == 31)
		*dp++ = d0;
	    d0 <<= 1;;
	}
	if (j & 31)
	    *dp++ = d0<<(31-j);
    }
    FreeImage(Image);
    return New;
}

/* release some non-essential memory or abort */
#define Try(n)								\
    if (n && n != thispage && n->extra) {				\
	FreeImage(n->extra);						\
	n->extra = NULL;						\
	return 1;							\
    }

static int
release(int quit)
{
    struct pagenode *pn;

    if (thispage) {
	/* first consider "uninteresting" pages */
	for (pn = firstpage->next; pn; pn = pn->next)
	    if (pn->extra && pn != thispage && pn != thispage->prev &&
		pn != thispage->next && pn != lastpage) {
		FreeImage(Pimage(pn));
		pn->extra = NULL;
		return 1;
	    }
	Try(lastpage);
	Try(firstpage);
	Try(thispage->prev);
	Try(thispage->next);
    }
    if (!quit)
	return 0;
    fprintf(stderr, "%s(release): insufficient memory\n", ProgName);
    exit(EXIT_FAILURE);
}

static XImage *
NewImage(int w, int h, char *data, int bit_order)
{
    XImage *new;
    /* This idea is taken from xwud/xpr.  Use a fake display with the
       desired bit/byte order to get the image routines initialised
       correctly */
    Display fake;

    fake = *Disp;
    if (data == NULL)
	data = xmalloc(((w + 31) & ~31) * h / 8);
    fake.byte_order = ByteOrder;
    fake.bitmap_bit_order = bit_order;

    while ((new = XCreateImage(&fake, DefaultVisual(Disp, Default_Screen),
			       1, XYBitmap, 0, data, w, h, 32, 0)) == NULL)
	(void) release(1);
    Memused += new->bytes_per_line * new->height;
    return new;
}

static void
FreeImage(XImage *Image)
{
    if (Image->data)
	Memused -= Image->bytes_per_line * Image->height;
    XDestroyImage(Image);
}

#ifndef xmalloc
char *
xmalloc(unsigned int size)
{
    char *p;

    while (Memused + size > Memlimit && release(0))
	;
    while ((p = malloc(size)) == NULL)
	(void) release(1);
    return p;
}
#endif
