        PBM viewer

	This is a W3A compliant viewer. It decodes PBM (PGM, PPM) data
	and displays it. To render the image on the screen, it uses
	the standard colormap library by Peter Kaczowka
	([[<kaczowka@ch.hp.com]], HP Chelmsford MA). This library
	assumes that an appropriate standard colormap has been
	installed previously, e.g., by the program `xscm'.

        Bert Bos [[<bert@let.rug.nl>]], 1994

<<*>>=
static char copyright[] = "Copyright NBBI, Den Haag, 1994";
#include <config.h>
#include <Xm/Xm.h>
#include <Xm/ScrolledW.h>
#include <Xm/DrawingA.h>
#include <xscmlib.h>
#include <w3a.h>
#include <str.h>
#include <ctype.h>

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

/* When decoding, the image is drawn every DELTA lines */
#define DELTA 5

typedef struct {
    Widget win;					/* Where to draw the image */
    GC gc;					/* GC to use when drawing */
    unsigned char *image;			/* 3 bytes (RGB) per pixel */
    int width, height;				/* Dimensions of image */
    int imagesize;				/* Length of image in bytes */
    int type;                                   /* 1, 2, 3, 4, 5 or 6 */
    int maxval;					/* Range of input data */
    char buf[2*BUFSIZ+1];                       /* Temporary buf for parsing */
    int buflen;					/* Length of buf */
    Bool eof;					/* TRUE if this is last buf */
    int nrvals;                                 /* # of valid bytes in image */
    int line;					/* Last line drawn */
} *Info;

static Bool has_stdcolormap = FALSE, has_stdgraymap = FALSE;
static XscmInfoRec stdcolormap, stdgraymap;

typedef struct _Assoc {long id; Info b; struct _Assoc *next;} *Assoc;

static Assoc assoclist = NULL;

/* store -- store an ID/Buffer combination */
static void store(Info b, long id)
{
    Assoc h;
    new(h); h->id = id; h->b = b; h->next = assoclist; assoclist = h;
}

/* delete -- delete an ID/Buffer combination */
static void delete(long id)
{
    Assoc g, h;
    assert(assoclist);
    if (assoclist->id == id) {
	h = assoclist; assoclist = assoclist->next; dispose(h);
    } else {
	assert(h->next);
	for (h = assoclist; h->next->id != id; h = h->next) assert(h->next);
	g = h->next; h->next = g->next; dispose(g);
    }
}

/* find -- find the Buffer associated with an ID */
static Info find(long id)
{
    Assoc h;
    assert(assoclist);
    for (h = assoclist; h->id != id; h = h->next) assert(h->next);
    return h->b;
}


/* delete_bytes -- delete first len bytes from info->buf */
static void delete_bytes(Info info, int len)
{
    assert(info != NULL && 0 <= len && len <= info->buflen);
    info->buflen -= len;
    memmove(info->buf, info->buf + len, info->buflen);
}


/* append_buf -- append data to info->buf, FALSE if no room */
static Bool append_buf(Info info, const char *buf, size_t nbytes)
{
    assert(info != NULL && errno == 0 && info->buflen >= 0);
    if (info->buflen + nbytes > sizeof(info->buf)) {
        errno = ENOMEM;                         /* No room, probably error */
        return FALSE;
    } else {
        memcpy(info->buf + info->buflen, buf, nbytes);
        info->buflen += nbytes;
        info->buf[info->buflen] = '\0';         /* Needed for P1/P2/P3 */
        return TRUE;
    }
}


/* parse_num -- parse a number, TRUE if success, else set errno */
static Bool parse_num(Info info, int *num)
{
    int len;

    assert(info != NULL && errno == 0);
    if (info->buflen == 0) {
        if (info->eof) errno = EFORMAT;         /* Missing number */
        return FALSE;                           /* Insufficient data */
    }
    if (! isdigit(info->buf[0])) {
        errno = EFORMAT;                        /* Number expected */
        return FALSE;
    }
    (void) sscanf(info->buf, "%d%n", num, &len);
    if (isspace(info->buf[len]) || info->eof) {
        delete_bytes(info, len);
        return TRUE;
    } else {
        return FALSE;                           /* More digits might follow */
    }
}


/* parse_eol -- parse up to and including newline, TRUE if newline found */
static Bool parse_eol(Info info)
{
    int i = 0;

    for (;;) {
        if (i == info->buflen) {
            if (info->eof) {                    /* Can't expect a newline */
                if (i != 0) delete_bytes(info, i);
                return TRUE;
            } else {                            /* Newline might follow */
                return FALSE;
            }
        } else if (info->buf[i] != '\n') {
            i++;
        } else {
            delete_bytes(info, i + 1);		/* Remove including eol */
            return TRUE;                        /* Eol found */
        }
    }
}


/* parse_white -- parse whitespace and comments, TRUE if non-blank found */
static Bool parse_white(Info info)
{
    int i = 0;

    assert(info != NULL && info->buflen >= 0);
    for (;;) {
        if (i == info->buflen) {
            if (i != 0) delete_bytes(info, i);	/* Remove whitespaxe so far */
            return ! info->eof;                 /* More spaces might follow */
        } else if (isspace(info->buf[i])) {
            i++;
        } else if (info->buf[i] == '#') {
            if (i != 0) delete_bytes(info, i);	/* Remove whitespace so far */
            if (! parse_eol(info)) return FALSE;
            i = 0;
        } else {                                /* Found non-blank */
            if (i != 0) delete_bytes(info, i);	/* Remove whitespace */
            return TRUE;
        }
    }
}


/* parse_type -- parse PBMPM magic number, TRUE if success, else set errno */
static Bool parse_type(Info info)
{
    assert(info != NULL && errno == 0 && info->buflen >= 0);
    if (info->buflen < 2) return FALSE;         /* Wait for more data */
    if (info->buf[0] != 'P') {errno = EFORMAT; return FALSE;}
    switch (info->buf[1]) {
    case '1': info->type = 1; break;
    case '2': info->type = 2; break;
    case '3': info->type = 3; break;
    case '4': info->type = 4; break;
    case '5': info->type = 5; break;
    case '6': info->type = 6; break;
    default: errno = EFORMAT; return FALSE;
    }
    delete_bytes(info, 2);
    return TRUE;
}


/* parse_width -- parse width parameter, TRUE if success, else set errno */
static Bool parse_width(Info info)
{
    assert(errno == 0);
    if (! parse_white(info)) return FALSE;
    if (! parse_num(info, &info->width)) return FALSE;
    return TRUE;
}


/* parse_height -- parse height parameter, TRUE if success, else set errno */
static Bool parse_height(Info info)
{
    assert(errno == 0);
    if (! parse_white(info)) return FALSE;
    if (! parse_num(info, &info->height)) return FALSE;
    return TRUE;
}


/* parse_maxval -- set or parse maxval parameter, TRUE if success */
static Bool parse_maxval(Info info)
{
    assert(errno == 0);
    if (info->type == 1 || info->type == 4) {	/* P1 & P4 are B&W formats */
        info->maxval = 1;
    } else {
        if (! parse_white(info)) return FALSE;
        if (! parse_num(info, &info->maxval)) return FALSE;
    }
    return TRUE;
}


/* parse_P1 -- parse data as 0's and 1's (PBM format) */
static void parse_P1(Info info)
{
    int i = 0;

    assert(info != NULL && info->buflen >= 0);
    assert(info->imagesize >= 0 && info->nrvals >= 0);
    assert(info->nrvals <= info->imagesize);
    for (;;) {
        if (i == info->buflen) {
            delete_bytes(info, i);		/* Remove parsed data */
            return;
        } else if (info->nrvals == info->imagesize) {
            delete_bytes(info, info->buflen);	/* Remove rest of data */
            /* errno = EFORMAT; */              /* Too much data */
            return;
        } else if (info->buf[i] == '0') {       /* White pixel */
            info->image[info->nrvals++] =
		info->image[info->nrvals++] =
		info->image[info->nrvals++] = 255;
        } else if (info->buf[i] == '1') {       /* Black pixel */
            info->image[info->nrvals++] =
		info->image[info->nrvals++] =
		info->image[info->nrvals++] = 0;
        } else if (! isspace(info->buf[i])) {   /* Illegal data */
            errno = EFORMAT;
            return;
        }
        i++;
    }
}


/* parse_P2 -- parse PGM data */
static void parse_P2(Info info)
{
    int val, len, i = 0;

    assert(info != NULL && info->buflen >= 0);
    assert(info->imagesize >= 0 && info->nrvals >= 0);
    assert(info->nrvals <= info->imagesize);
    for (;;) {
        if (i == info->buflen) {
            if (i != 0) delete_bytes(info, i);	/* Remove parsed data */
            return;                             /* Wait for more data */
        } else if (info->nrvals == info->imagesize) {
            delete_bytes(info, info->buflen);	/* Remove rest of data */
            /* errno = EFORMAT; */              /* Too much data */
            return;
        } else if (sscanf(info->buf + i, "%d%n", &val, &len) != 1) {
            errno = EFORMAT;                    /* Illegal data */
            return;
        } else if (! isspace(info->buf[i+len]) && ! info->eof) {
            if (i != 0) delete_bytes(info, i);	/* Remove parsed data */
            return;                             /* Wait for more data */
        } else if (val > info->maxval) {
            errno = EFORMAT;                    /* Illegal pixel value */
            return;
        } else {
            info->image[info->nrvals++] =
            info->image[info->nrvals++] =
            info->image[info->nrvals++] = 255 * val/info->maxval;
        }
        i++;
    }
}


/* parse_P3 -- parse PPM data */
static void parse_P3(Info info)
{
    int i = 0, val, len;

    assert(info != NULL && info->buflen >= 0);
    assert(info->imagesize >= 0 && info->nrvals >= 0);
    assert(info->nrvals <= info->imagesize);
    for (;;) {
        if (i == info->buflen) {
            if (i != 0) delete_bytes(info, i);
            return;
        } else if (info->nrvals == info->imagesize) {
            delete_bytes(info, info->buflen);	/* Remove rest of data */
            /* errno = EFORMAT; */              /* Too much data */
            return;
        } else if (sscanf(info->buf + i, "%d%n", &val, &len) != 1) {
            errno = EFORMAT;                    /* Illegal data */
            return;
        } else if (! isspace(info->buf[i+len]) && ! info->eof) {
            if (i != 0) delete_bytes(info, i);	/* Remove parsed data */
            return;                             /* Wait for more data */
        } else if (val > info->maxval) {
            errno = EFORMAT;                    /* Illegal pixel value */
            return;
        } else {
            info->image[info->nrvals] = 255 * val/info->maxval;
            info->nrvals++;
        }
        i++;
    }
}


/* parse_P4 -- parse raw PBM data */
static void parse_P4(Info info)
{
    int i;
    unsigned char mask;

    assert(info != NULL && info->buflen >= 0);
    assert(info->imagesize >= 0 && info->nrvals >= 0);
    assert(info->nrvals <= info->imagesize);
    for (i = 0; i < info->buflen; i++) {
        for (mask = 128; mask; mask >>= 1) {
	    if (info->nrvals == info->imagesize) {
                delete_bytes(info, info->buflen); /* Remove all of buf */
                return;                         /* End of image reached */
            }
	    info->image[info->nrvals++] =
		info->image[info->nrvals++] =
		info->image[info->nrvals++] = (info->buf[i] & mask) ? 0 : 255;
        }
    }
    delete_bytes(info, info->buflen);
}


/* parse_P5 -- parse raw PGM data */
static void parse_P5(Info info)
{
    int i;

    assert(info != NULL && info->buflen >= 0);
    assert(info->imagesize >= 0 && info->nrvals >= 0);
    assert(info->nrvals <= info->imagesize);
    for (i = 0; i < info->buflen; i++) {
        if (info->nrvals == info->imagesize) {
            delete_bytes(info, info->buflen);
            /* errno = EFORMAT; */              /* Too much data */
            return;
        }
        info->image[info->nrvals++] = 
	    info->image[info->nrvals++] =
	    info->image[info->nrvals++] = (unsigned char) info->buf[i];
    }
    delete_bytes(info, info->buflen);
}


/* parse_P6 -- parse raw PPM data */
static void parse_P6(Info info)
{
    int i;

    assert(info != NULL && info->buflen >= 0);
    assert(info->imagesize >= 0 && info->nrvals >= 0);
    assert(info->nrvals <= info->imagesize);
    for (i = 0; i < info->buflen; i++) {
        if (info->nrvals == info->imagesize) {
            delete_bytes(info, info->buflen);
            /* errno = EFORMAT; */              /* Too much data */
            return;
        }
        info->image[info->nrvals++] = (unsigned char) info->buf[i];
    }
    delete_bytes(info, info->buflen);
}


/* redraw -- redraw window after an Expose event */
static void redraw(Widget w, XEvent *ev, String *parms, Cardinal *nparms)
{
    Info info;
    int x, y, rowlen, wd, ht;

    assert(*nparms == 1);
    if (sscanf(parms[0], "%ld", &info) != 1) assert(! "wrong arguments");
    assert(info);
    if (! XtIsRealized(info->win) || ! info->image) return;

    x = ev->xexpose.x;
    y = ev->xexpose.y;
    if (x >= info->width || y >= info->height) return;
    wd = min(ev->xexpose.width, info->width - x);
    ht = min(ev->xexpose.height, info->height - y);
    rowlen = 3 * info->width;			/* 3 bytes per pixel */
    if ((info->type == 3 || info->type == 6 || ! has_stdgraymap)
        && has_stdcolormap) {
        XscmDisplay
	    (&stdcolormap, info->image + rowlen * y + 3 * x, rowlen, wd, ht,
	     NULL, NULL, NULL, XtWindow(info->win), info->gc, x, y);
    } else {
        assert(has_stdgraymap);
        XscmDisplay
	    (&stdgraymap, info->image + rowlen * y + 3 * x, rowlen, wd, ht,
	     NULL, NULL, NULL, XtWindow(info->win), info->gc, x, y);
    }
}


/* click -- handle a mouse click on the image */
static void click(Widget w, XEvent *ev, String *parms, Cardinal *nparms)
{
    struct {int x, y;} coords;
    long info;

    assert(*nparms == 1);
    if (sscanf(parms[0], "%ld", &info) != 1) assert(! "wrong arguments");
    coords.x = ev->xbutton.x;
    coords.y = ev->xbutton.y;
    W3Aevent(info, POINT_SELECT, &coords);
}


/* initPBM -- initialize PBM viewer */
EXPORT Bool initPBM(char ***mime_types, int *nrtypes, float **prefs)
{
    static char *types[] = {
	"image/x-pbm",
	"image/x-portable-bitmap",
	"image/x-pgm",
	"image/x-gray-bitmap",
	"image/x-ppm",
	"image/x-portable-pixmap",
	"image/x-pnm"
	"image/x-portable-anymap",
    };
    static float pref[] = {1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0};
    Widget toplevel = W3Atoplevel();
    Display *dpy = XtDisplay(toplevel);
    int screen = XScreenNumberOfScreen(XtScreen(toplevel));
    static XtActionsRec actions[] = {
        {"PBM_expose", redraw},
        {"PBM_click", click}};

    *mime_types = types;
    *nrtypes = XtNumber(types);
    *prefs = pref;
    XtAppAddActions(XtWidgetToApplicationContext(toplevel),
        actions, XtNumber(actions));

    has_stdcolormap = XscmFindColorSCM(dpy, screen, XSCM_ANY, &stdcolormap);
    has_stdgraymap = XscmFindGraySCM(dpy, screen, &stdgraymap);

    if (! has_stdcolormap && ! has_stdgraymap)
	XtAppWarning(XtWidgetToApplicationContext(W3Atoplevel()),
		     "PBM viewer: Could find neither a standard color map \
nor a standard gray map");

    return has_stdcolormap || has_stdgraymap;
}


EXPORT Bool openPBM(const W3ADocumentInfo doc, W3AWindow win, long id)
{
    XGCValues dummy;
    Widget board, drawarea;
    Info info;
    char s[200];
    static char translations[] =
        "<Expose>: PBM_expose(%ld)\n\
        <Btn1Down>,<Btn1Up>: PBM_click(%ld)";

    assert(doc.mime_type != NULL);
    assert(eq(doc.mime_type, "image/x-ppm")
        || eq(doc.mime_type, "image/x-pgm")
        || eq(doc.mime_type, "image/x-pbm"));

    assert(has_stdcolormap || has_stdgraymap);
#if 0
    board = XtVaCreateManagedWidget
	("pbm-board", xmScrolledWindowWidgetClass, win,
	 XmNscrollingPolicy, XmAUTOMATIC,
	 /* XmNshadowThickness, 0, */ NULL);
#if 1
    drawarea = XtVaCreateManagedWidget
	("pbm", xmDrawingAreaWidgetClass, board, NULL);
#else
    drawarea = XtVaCreateManagedWidget
	("pbm", xmPrimitiveWidgetClass, board, NULL);
#endif
#endif

    new(info);
    store(info, id);
#if 0
    info->win = drawarea;
#else
    info->win = win;
#endif
    info->image = NULL;
    info->width = -1;
    info->height = -1;
    info->maxval = -1;
    info->type = -1;
    info->nrvals = -1;
    info->buflen = 0;
    info->line = 0;
    info->eof = FALSE;
    info->gc = XtGetGC(info->win, 0, &dummy);
    sprintf(s, translations, (long) info, (long) info);
    XtOverrideTranslations(info->win, XtParseTranslationTable(s));

    return TRUE;
}
@

        [[writePBM]] interprets the PBM header, allocates memory to
        store the image, and then copies incoming bytes to that memory.
        Image data is also immediately displayed to the screen.

        The [[info->buf]] field is used to hold the header until type,
        width, height and maxval are known.

        Only when all parameters (type, width, height, maxval) have been
        parsed, will the function allocate the [[image]] array. Setting
        [[nrpixels]] from -1 to 0 indicates that copying of data to the
        [[image]] array can start.

        The [[image]] array will always hold 3 bytes per pixel, even
        when the data is black and white or gray scale.

<<*>>=
EXPORT int writePBM(long id, const char *buf, size_t nbytes)
{
    Info info = find(id);
    int line;

    assert(info && info->win && info->buflen >= 0);

    errno = 0;
    if (nbytes == 0) info->eof = TRUE;
    if (! append_buf(info, buf, nbytes)) return -1;
    if (info->type < 0 && ! parse_type(info)) return errno ? -1 : nbytes;
    if (info->width < 0 && ! parse_width(info)) return errno ? -1 : nbytes;
    if (info->height < 0 && ! parse_height(info)) return errno ? -1 : nbytes;
    if (info->maxval < 0 && ! parse_maxval(info)) return errno ? -1 : nbytes;
    if (info->nrvals < 0) {
        if (! parse_eol(info)) return errno ? -1 : nbytes;
        info->nrvals = 0;
        info->imagesize = info->width * info->height * 3;
        newarray(info->image, info->imagesize);
#if 0
        XtVaSetValues(XtParent(XtParent(info->win)), XtNwidth, info->width,
		      XtNheight, info->height, NULL);
        XtVaSetValues(XtParent(info->win), XtNwidth, info->width,
		      XtNheight, info->height, NULL);
#endif
        XtVaSetValues(info->win, XtNwidth, info->width,
		      XtNheight, info->height, NULL);
    }
    switch (info->type) {
    case 1: parse_P1(info); break;              /* PBM */
    case 2: parse_P2(info); break;              /* PGM */
    case 3: parse_P3(info); break;              /* PPM */
    case 4: parse_P4(info); break;              /* Raw PBM */
    case 5: parse_P5(info); break;              /* Raw PGM */
    case 6: parse_P6(info); break;              /* Raw PPM */
    default: assert(! "Cannot happen");
    }
    line = info->nrvals/3/info->width;
    if (XtIsRealized(info->win) && (line - info->line > DELTA || info->eof)) {
        if ((info->type == 3 || info->type == 6 || ! has_stdgraymap)
            && has_stdcolormap) {
            XscmDisplay
		(&stdcolormap, info->image + 3 * info->line * info->width,
		 info->width * 3, info->width, line - info->line, NULL,
		 NULL, NULL, XtWindow(info->win), info->gc, 0, info->line);
        } else {
            assert(has_stdgraymap);
            XscmDisplay
		(&stdgraymap, info->image + 3 * info->line * info->width,
		 info->width * 3, info->width, line - info->line, NULL,
		 NULL, NULL, XtWindow(info->win), info->gc, 0, info->line);
        }
	info->line = line;
    }
    return errno ? -1 : nbytes;
}


EXPORT Bool closePBM(long id)
{
    Info info = find(id);

    assert(info && info->win);
    XtReleaseGC(info->win, info->gc);
#if 0
    XtDestroyWidget(XtParent(info->win));
#endif
    dispose(info->image);
    dispose(info);
    return TRUE;
}


EXPORT void eventPBM(long id, long source, long eventtype, void *params)
{
    /* Doesn't handle events */
}


EXPORT Bool infoPBM(long id, W3ADocumentInfo *doc)
{
    /* No modifications */
    return TRUE;
}
