GIF viewer

This is a WWW applet (conforming to the W3A API) for viewing images in GIF format. Apart from showing the image, it also accepts mouse clicks, which it passes to the browser by means of the W3Aevent function.

Note that this viewer is not really interested in the information about the document that it is viewing. It simply ignores the W3ADocumentInfo that is passed to openGIF, except that it keeps the URL for use in a possible error message.

A mouse click causes a call to W3Aevent, with event type 5000 and a pointer to a pair (x,y) of integers as parameter.

The GIF viewer draws directly into the widget that is provided by the main browser. Since it is not known what kind of widget it is (probably Composite or XmManager), the GIF viewer assumes only that it is a subclass of Core. It installs a set of translations and actions to override whatever the widget already has.

The action functions are registered only once, when the GIF viewer is initialized (initGIF). The translations are installed in every widget that is passed to openGIF.

The action functions have a single parameter, which is used to pass the Buffer with info about the image to the action function. Since action functions can only have string arguments, the pointer to the Buffer is encoded as a decimal number..

NB. This viewer is easily generalized to more image types. It only needs more image decoding functions.

TO DO: When the image uses only (almost) gray colors, the standard gray colormap shouldbe used instead of the default map.

Acknowledgments

This software incorporates algorithms from several sources, as well as a few that are my own invention. It makes use of the XSCM library (by Peter Kaczowka , copyright Hewlett Packard 1994). The algorithm for splitting off a number of bits from the stream of bytes (using `workbits') is taken from `giflib' by Kirk L. Johnson (1989, 1990). The idea to loop over the LZW codes and call out to `store_pixel' instead of looping over the image coordinates and calling `next_code', is taken from `lug-tools' by Paul Rivero (8th Jan 1992).

Bert Bos , 11 Aug 1994

<<*>> =
#include <config.h> #include <w3a.h> #include <str.h> #include <Xm/Xm.h> #include <X11/extensions/shape.h> #include <xscmlib.h> /* Defined in w3a.h: */ /* #define POINT_SELECT 5000 /* W3A event type */ #define CMAP 256 /* Max. GIF color table */ #define MAX_LZW_BITS 12 /* Code can grow to this */ #define MAXSTACK 4096 /* Maximum with 12 bits */ #define GIF_SUCCESS 0 /* Decoded successfully */ #define NO_GIF 1 /* Wrong magic number */ #define SHORT_DATA 2 /* Unexpected end of data */ #define IMG_START 3 /* Image seperator missing */ #define GRAYMARGIN 10000 /* Max. distance from grayrey */ #define abs(a) ((a) < 0 ? -(a) : (a)) #define min(a, b) ((a) < (b) ? (a) : (b)) static char *err_msg[] = { "OK", "GIF viewer failed: image is not a GIF87a or GIF89a\n(%s)", "GIF viewer failed: image is incomplete\n(%s)", "GIF viewer failed: start of image not found\n(%s)", }; typedef struct { /* A viewer instance */ char *url; /* Only used for error msg */ char *buf; /* GIF image data */ size_t buflen; /* Length of buf in bytes */ size_t offset; /* Amount processed */ Widget canvas; /* The widget to draw into */ GC gc; /* GC for image */ int width, height; /* Image size */ int red[CMAP], green[CMAP], blue[CMAP]; /* Color map table */ Bool use_gray; /* Use std. grayrey map? */ int transparent; /* Transp. color index or -1 */ int xpos, ypos, step, pass; /* Coords. & interlace step */ int rowstart; /* == ypos * width */ int maskrowstart; /* == ypos * ((width+7)/8) */ char *mask; /* Transparency mask */ unsigned char *image; /* Decoded image */ } *Buffer; static XscmInfoRec xscm_info, xscm_gray_info; static Bool default_stdcmap, gray_stdcmap; typedef struct _Assoc {long id; Buffer b; struct _Assoc *next;} *Assoc; static Assoc assoclist = NULL; /* store -- store an ID/Buffer combination */ static void store(Buffer 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 Buffer find(long id) { Assoc h; assert(assoclist); for (h = assoclist; h->id != id; h = h->next) assert(h->next); return h->b; } /* readbuf -- read next n bytes from b, false if insufficient bytes */ static Bool readbuf(Buffer b, void *buf, size_t nbytes) { if (b->offset + nbytes > b->buflen) return FALSE; memcpy(buf, b->buf + b->offset, nbytes); b->offset += nbytes; return TRUE; }

index is an index into the image's color table. It is passed as an int for convenience, but it should be in the range 0..255. It is added to the decoded image at the current coordinates.

If index indicates the transparent color, a bit is cleared in the mask bitmap.

<<*>> +=
/* store_pixel -- add pixel to decoded image */ static store_pixel(Buffer b, int index) { static int stepsize[] = {8, 8, 4, 2, 0}; static int startrow[] = {0, 4, 2, 1, 0}; assert(0 <= index && index <= 255); assert(b->rowstart == b->ypos * b->width); assert(b->maskrowstart == b->ypos * ((b->width + 7)/8)); if (index == b->transparent) b->mask[b->maskrowstart + b->xpos/8] &= ~(1 << (b->xpos % 8)); b->image[b->rowstart + b->xpos] = index; if (++b->xpos == b->width) { b->xpos = 0; b->ypos += b->step; b->rowstart += b->step * b->width; b->maskrowstart += b->step * ((b->width + 7)/8); if (b->ypos >= b->height) { b->pass++; b->step = stepsize[b->pass]; b->ypos = startrow[b->pass]; b->rowstart = b->ypos * b->width; b->maskrowstart = b->ypos * ((b->width + 7)/8); /* Update the screen, so users can see what's there */ if (XtIsRealized(b->canvas)) { if (! b->use_gray || ! gray_stdcmap) XscmDisplay(&xscm_info, b->image, b->width, b->width, b->height, b->red, b->green, b->blue, XtWindow(b->canvas), b->gc, 0, 0); else XscmDisplay(&xscm_gray_info, b->image, b->width, b->width, b->height, b->red, b->green, b->blue, XtWindow(b->canvas), b->gc, 0, 0); XFlush(XtDisplay(b->canvas)); } } } } /* decodeLZW -- loop over the data for an image, until an EOI code */ static int decodeLZW(Buffer b) { int clear_code, eoi_code, min_code_size, free_code; int code = -1, first; int nrworkbits = 0, code_size, code_mask, sp = 0; int bufidx = 0, buflen = 0, prev_code = -2; unsigned char buf[256]; unsigned long workbits = 0; int stack[MAXSTACK], prefix[MAXSTACK], extnsn[MAXSTACK]; /* Read initial code size, initialize special codes */ if (! readbuf(b, buf, 1)) return SHORT_DATA; min_code_size = buf[0]; clear_code = 1 << min_code_size; eoi_code = clear_code + 1; free_code = clear_code + 2; code_size = min_code_size + 1; code_mask = (1 << code_size) - 1; /* Decode until we find an End-of-Information code */ while (code != eoi_code) { /* Add bytes until we have enough bits for next code */ while (nrworkbits < code_size) { /* Read new data block if needed */ if (bufidx == buflen) { if (! readbuf(b, buf, 1) || buf[0] == '\0') return SHORT_DATA; buflen = buf[0]; if (! readbuf(b, buf, buflen)) return SHORT_DATA; bufidx = 0; } workbits |= ((unsigned long) buf[bufidx++]) << nrworkbits; nrworkbits += 8; } /* Get next code */ code = workbits & code_mask; workbits >>= code_size; nrworkbits -= code_size; /* Force first code of image to be a clear code */ if (prev_code == -2) code = clear_code; /* Branch on type of code */ if (code == clear_code) { /* Reset the decoder */ code_size = min_code_size + 1; /* Original code size */ code_mask = (1 << code_size) - 1; /* Corresponding mask */ free_code = clear_code + 2; /* First pos. in tables */ prev_code = -1; /* Next code is a root code */ } else if (code == eoi_code) { /* End of Information */ /* skip */ } else if (prev_code == -1) { /* 1st code after clearcode */ store_pixel(b, code); /* Add to image */ first = prev_code = code; } else { /* We've got a normal code */ if (code >= free_code) { /* It's a new code */ stack[sp++] = first; first = prev_code; } else /* It's an existing code */ first = code; while (first >= clear_code) { /* Push string of pixels */ stack[sp++] = extnsn[first]; first = prefix[first]; } stack[sp++] = first; /* Push string's root code */ while (sp != 0) /* Now add pixels to image */ store_pixel(b, stack[--sp]); prefix[free_code] = prev_code; extnsn[free_code++] = first; prev_code = code; /* Check if code_size needs to increase */ if (free_code > code_mask && code_size != MAX_LZW_BITS) { code_size++; code_mask = (1 << code_size) - 1; } } } return GIF_SUCCESS; } /* read_color_table -- read GIF color table, check for `almost grayrey' */ static Bool read_color_table(Buffer b, int color_table_size) { unsigned char buf[3]; int i, drb, dgb, dbg; b->use_gray = TRUE; for (i = 0; i < color_table_size; i++) { if (! readbuf(b, buf, 3)) return FALSE; /* RGB triplet */ b->red[i] = ((int) buf[0]) << 8; /* 0..255 --> 0..65535 */ b->green[i] = ((int) buf[1]) << 8; b->blue[i] = ((int) buf[2]) << 8; drb = abs(b->blue[i] - b->red[i]); dgb = abs(b->blue[i] - b->green[i]); dbg = abs(b->green[i] - b->red[i]); if (drb > GRAYMARGIN || dgb > GRAYMARGIN || dbg > GRAYMARGIN) b->use_gray = FALSE; } return TRUE; } /* parseGIF -- decode GIF image data and create an XImage from it */ static int parseGIF(Buffer b) { unsigned char buf[256]; Bool has_color_table, is_interlaced; unsigned int color_table_size; int status; int screen = XScreenNumberOfScreen(XtScreen(b->canvas)); Display *dpy = XtDisplay(b->canvas); unsigned int depth = DefaultDepth(dpy, screen); XGCValues values; /* Initialize */ b->transparent = -1; /* Header */ if (!readbuf(b, buf, 6)) return SHORT_DATA; if (!memcmp(buf,"GIF87a", 6) && !memcmp(buf, "GIF89a", 6)) return NO_GIF; /* Logical Screen Descriptor */ if (! readbuf(b, buf, 7)) return SHORT_DATA; has_color_table = (buf[4] & 0x80) == 0x80; color_table_size = 2 << (buf[4] & 0x07); /* Global Color Table */ if (has_color_table) if (! read_color_table(b, color_table_size)) return SHORT_DATA; /* Extensions */ if (! readbuf(b, buf, 1)) return SHORT_DATA; while (buf[0] == 0x21) { /* It's an extension */ if (! readbuf(b, buf, 1)) return SHORT_DATA; /* Label */ switch (buf[0]) { case 0xf9: /* Graphic Control Extension */ if (! readbuf(b, buf, 6)) return SHORT_DATA; b->transparent = buf[4]; /* Index in colortable */ break; default: /* Other extension */ if (! readbuf(b, buf, 1)) return SHORT_DATA; while (buf[0] != 0) { if (! readbuf(b, buf, buf[0])) return SHORT_DATA; if (! readbuf(b, buf, 1)) return SHORT_DATA; } } if (! readbuf(b, buf, 1)) return SHORT_DATA; } /* Image Descriptor */ if (buf[0] != 0x2c) return IMG_START; /* Check Image Seperator */ if (! readbuf(b, buf, 9)) return SHORT_DATA; /* Image descriptor */ b->width = (buf[5] << 8) | buf[4]; b->height = (buf[7] << 8) | buf[6]; has_color_table = (buf[8] & 0x80) == 0x80; is_interlaced = (buf[8] & 0x40) == 0x40; color_table_size = 2 << (buf[8] & 0x07); /* Local Color Table */ if (has_color_table) if (! read_color_table(b, color_table_size)) return SHORT_DATA; /* Create room for decoded image and its mask */ newarray(b->image, b->height * b->width); newarray(b->mask, b->height * ((b->width + 7)/8)); memset(b->mask, 0xff, b->height * ((b->width + 7)/8)); XtVaSetValues(b->canvas, XmNwidth, b->width, XmNheight, b->height, NULL); /* Decode first image (ignore other images, if present) */ b->xpos = 0; b->ypos = 0; b->step = is_interlaced ? 8 : 1; b->pass = is_interlaced ? 0 : 3; b->rowstart = 0; b->maskrowstart = 0; status = decodeLZW(b); #ifdef DEBUG debug("b->xpos=%d b->ypos=%d b->step=%d\n", b->xpos, b->ypos, b->step); if (b->xpos != 0 || b->ypos != 0) XtAppWarning(XtWidgetToApplicationContext(b->canvas), "Incorrect GIF???"); #endif /* DEBUG */ return status; } /* GIF_redraw -- redraw the exposed area of a GIF image */ static void GIF_redraw(Widget w, XEvent *ev, String *parms, Cardinal *nparms) { Buffer b; int wd, ht, x, y; /* Decode string argument as a Buffer pointer */ assert(*nparms == 1); if (sscanf(parms[0], "%ld", &b) != 1) assert(!"Missing parameter"); if (! XtIsRealized(w) || b->image == NULL) return; /* Only on the first ExposeEvent: set the window shape */ if (b->mask != NULL) { Pixmap mask; mask = XCreateBitmapFromData(XtDisplay(w), XtWindow(w), b->mask, b->width, b->height); if (mask != None) { XShapeCombineMask(XtDisplay(w), XtWindow(w), ShapeBounding, 0, 0, mask, ShapeSet); XFreePixmap(XtDisplay(w), mask); } dispose(b->mask); b->mask = NULL; } /* Draw image to exposed rectangle */ x = ev->xexpose.x; y = ev->xexpose.y; if (x >= b->width || y >= b->height) return; wd = min(ev->xexpose.width, b->width - x); ht = min(ev->xexpose.height, b->height - y); if (! b->use_gray || ! gray_stdcmap) XscmDisplay(&xscm_info, b->image + y * b->width + x, b->width, wd, ht, b->red, b->green, b->blue, XtWindow(w), b->gc, x, y); else XscmDisplay(&xscm_gray_info, b->image + y * b->width + x, b->width, wd, ht, b->red, b->green, b->blue, XtWindow(w), b->gc, x, y); } /* GIF_click -- action for mouse clicks on canvas */ /* ARGSUSED */ static void GIF_click(Widget w, XEvent *ev, String *params, Cardinal *nparams) { long b; struct {int x, y;} coords; assert(*nparams == 1); if (sscanf(params[0], "%ld", &b) != 1) assert(!"Missing parameter"); coords.x = ev->xbutton.x; coords.y = ev->xbutton.y; debug("Clicked on image: (%d,%d)\n", coords.x, coords.y); W3Aevent(b, POINT_SELECT, &coords); } static XtActionsRec actions[] = { {"GIF_expose", GIF_redraw}, {"GIF_click", GIF_click} }; static char translations[] = "<Expose>: GIF_expose(%ld)\n\ <Btn1Down>,<Btn1Up>: GIF_click(%ld)"; /* initGIF -- initialize the GIF viewer class */ EXPORT Bool initGIF(char ***mime_types, int *nrtypes) { static char *types[] = {"image/gif"}; Widget toplevel = W3Atoplevel(); Display *dpy = XtDisplay(toplevel); int screen = XScreenNumberOfScreen(XtScreen(toplevel)); *mime_types = types; *nrtypes = 1; XtAppAddActions(XtWidgetToApplicationContext(toplevel), actions, XtNumber(actions)); default_stdcmap = XscmFindColorSCM(dpy, screen, XSCM_ANY, &xscm_info); gray_stdcmap = XscmFindGraySCM(dpy, screen, &xscm_gray_info); if (! default_stdcmap && ! gray_stdcmap) XtAppWarning(XtWidgetToApplicationContext(W3Atoplevel()), "GIF viewer: Could find neither a standard color map \ nor a standard gray map"); return default_stdcmap || gray_stdcmap; } /* openGIF -- start a new GIF viewer, return its ID */ EXPORT Bool openGIF(const W3ADocumentInfo doc, W3AWindow window, long id) { int screen = XScreenNumberOfScreen(XtScreen(window)); Display *dpy = XtDisplay(window); char s[256]; Buffer b; new(b); store(n, id); b->url = newstring(doc.url); b->buf = NULL; b->buflen = 0; b->offset = 0; b->image = NULL; b->mask = NULL; b->canvas = window; sprintf(s, translations, (long) b, (long) b); XtOverrideTranslations(b->canvas, XtParseTranslationTable(s)); b->gc = DefaultGC(dpy, screen); return TRUE; } /* writeGIF -- add image data to the buffer, decode when complete */ EXPORT int writeGIF(long id, const char *buf, size_t nbytes) { Buffer b = find(id); static char msg[2048]; int status; if (nbytes != 0) { /* Add data to buffer */ renewarray(b->buf, b->buflen + nbytes); memcpy(b->buf + b->buflen, buf, nbytes); b->buflen += nbytes; } else if ((status = parseGIF(b)) != GIF_SUCCESS) { sprintf(msg, err_msg[status], b->url); XtAppWarning(XtWidgetToApplicationContext(b->canvas), msg); } else { dispose(b->buf); /* No longer needed */ } return nbytes; } /* closeGIF -- close a GIF viewer */ EXPORT Bool closeGIF(long id) { Buffer b = find(id); dispose(b->url); dispose(b->image); XtDestroyWidget(b->canvas); return TRUE; } /* eventGIF -- react to events happening elsewhere */ /* ARGSUSED */ EXPORT void eventGIF(long id, long source, long eventtype, void *params) { debug("GIF viewer %ld received event %ld from %ld\n", id, eventtype, source); /* Doesn't handle events */ } /* infoGIF -- a chance to modify the document info based on the contents */ /* ARGSUSED */ EXPORT Bool infoGIF(long id, W3ADocumentInfo *doc) { /* No modifications */ return TRUE; }