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 #include #include #include #include #include #define LIVE_IMAGES 1 /* 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 */ unsigned char buf[2*BUFSIZ]; /* Temporary GIF image data */ size_t buflen; /* Length of buf in bytes */ 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 */ int state; Bool ready; /* Image fully decoded */ Bool has_color_table; int color_table_size; int clear_code, eoi_code, min_code_size, free_code; int nrworkbits, code_size, code_mask; int prev_code, bufidx, blocklen, first; int prefix[MAXSTACK], extnsn[MAXSTACK]; unsigned long workbits; } *Buffer; #define discard(b, n) memmove((b)->buf, (b)->buf + (n), (b)->buflen -= (n)) static XscmInfoRec xscm_info, xscm_gray_info; static Bool default_stdcmap, gray_stdcmap; @ To associate IDs with Buffers, a simple linear list is used. <<*>>= 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(assoclist->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; } @ [[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 void 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) { /* At the end of a row... */ #if LIVE_IMAGES if (XtIsRealized(b->canvas)) { /* ...display 1 screen row */ if (gray_stdcmap && (b->use_gray || ! default_stdcmap)) XscmDisplay(&xscm_gray_info, b->image + b->ypos * b->width, b->width, b->width, 1, b->red, b->green, b->blue, XtWindow(b->canvas), b->gc, 0, b->ypos); else if (default_stdcmap) XscmDisplay(&xscm_info, b->image + b->ypos * b->width, b->width, b->width, 1, b->red, b->green, b->blue, XtWindow(b->canvas), b->gc, 0, b->ypos); } #endif /* LIVE_IMAGES */ 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); } } } @ The routine works as a co-routine. The large switch represents the various entry points. Whenever a bit of the GIF data has been parsed, a state variable is incremented. When the routine has to stop due to insufficient data, it can resume where it left off by inspecting the state. <<*>>= /* parseGIF -- decode GIF image data and create an XImage from it */ static int parseGIF(Buffer b) { int stack[MAXSTACK]; Bool is_interlaced; Dimension dum1, dum2; /* Dummy arguments */ int i, j, drb, dgb, drg; int code, sp = 0; for (;;) { /* Interrupted by a return */ switch (b->state) { case 0: /* Initialize */ b->transparent = -1; b->state++; /* Fall through */ case 1: /* Read magic number */ if (b->buflen < 6) /* Insufficient data */ return TRUE; if (!memcmp(b->buf, "GIF87a", 6) && !memcmp(b->buf, "GIF89a", 6)) return FALSE; /* Error */ discard(b, 6); b->state++; /* Fall through */ case 2: /* Logical Screen Descriptor */ if (b->buflen < 7) return TRUE; b->has_color_table = (b->buf[4] & 0x80) == 0x80; b->color_table_size = 2 << (b->buf[4] & 0x07); discard(b, 7); b->state++; /* Fall through */ case 3: /* Global Color Table */ if (b->has_color_table) { if (b->buflen < 3 * b->color_table_size) return TRUE; b->use_gray = TRUE; for (i = 0, j = 0; i < b->color_table_size; i++) { /* Expand 0..255 --> 0..65535 */ b->red[i] = ((int) b->buf[j++]) << 8; b->green[i] = ((int) b->buf[j++]) << 8; b->blue[i] = ((int) b->buf[j++]) << 8; if (b->use_gray) { drb = abs(b->blue[i] - b->red[i]); dgb = abs(b->blue[i] - b->green[i]); drg = abs(b->green[i] - b->red[i]); if (drb > GRAYMARGIN || dgb > GRAYMARGIN || drg > GRAYMARGIN) b->use_gray = FALSE; } } } discard(b, 3 * b->color_table_size); b->state++; /* Fall through */ case 4: /* Extensions */ if (b->buflen < 1) return TRUE; if (b->buf[0] == 0x21) { if (b->buflen < 2) return TRUE; else if (b->buf[1] == 0xf9) b->state = 400; else b->state = 410; discard(b, 2); break; } b->state++; /* Fall through */ case 5: /* Image Descriptor */ if (b->buflen < 10) return TRUE; if (b->buf[0] != 0x2c) return FALSE; /* No Image Seperator */ b->width = (b->buf[6] << 8) | b->buf[5]; b->height = (b->buf[8] << 8) | b->buf[7]; b->has_color_table = (b->buf[9] & 0x80) == 0x80; is_interlaced = (b->buf[9] & 0x40) == 0x40; b->color_table_size = 2 << (b->buf[9] & 0x07); b->xpos = 0; b->ypos = 0; b->step = is_interlaced ? 8 : 1; b->pass = is_interlaced ? 0 : 3; b->rowstart = 0; b->maskrowstart = 0; newarray(b->image, b->height * b->width); if (b->transparent >= 0) { newarray(b->mask, b->height * ((b->width + 7)/8)); memset(b->mask, 0xff, b->height * ((b->width + 7)/8)); } #if 1 XtMakeResizeRequest(b->canvas, b->width, b->height, &dum1, &dum2); #endif discard(b, 10); b->state++; /* Fall through */ case 6: /* Local Color Table */ if (b->has_color_table) { if (b->buflen < 3 * b->color_table_size) return TRUE; b->use_gray = TRUE; for (i = 0, j = 0; i < b->color_table_size; i++) { /* Expand 0..255 --> 0..65535 */ b->red[i] = ((int) b->buf[j++]) << 8; b->green[i] = ((int) b->buf[j++]) << 8; b->blue[i] = ((int) b->buf[j++]) << 8; if (b->use_gray) { drb = abs(b->blue[i] - b->red[i]); dgb = abs(b->blue[i] - b->green[i]); drg = abs(b->green[i] - b->red[i]); if (drb > GRAYMARGIN || dgb > GRAYMARGIN || drg > GRAYMARGIN) b->use_gray = FALSE; } } } b->state++; /* Fall through */ case 7: /* Special codes */ assert(b->state == 7); if (b->buflen < 1) return TRUE; b->min_code_size = b->buf[0]; b->clear_code = 1 << b->min_code_size; b->eoi_code = b->clear_code + 1; b->code_size = b->min_code_size + 1; b->free_code = b->clear_code + 2; b->code_mask = (1 << b->code_size) - 1; b->nrworkbits = 0; b->workbits = 0; b->bufidx = 0; b->blocklen = 0; b->prev_code = -2; discard(b, 1); b->state++; case 8: /* Decode LZW */ /* Add bytes until we have enough bits for next code */ while (b->nrworkbits < b->code_size) { /* Read new data block if needed */ if (b->bufidx == b->blocklen) { discard(b, b->blocklen); b->bufidx = b->blocklen = 0; if (b->buflen < 1 || b->buflen < 1 + (size_t) b->buf[0]) return TRUE; b->blocklen = 1 + b->buf[0]; b->bufidx = 1; } b->workbits |= (unsigned long) b->buf[b->bufidx++] << b->nrworkbits; b->nrworkbits += 8; } /* Get next code */ code = b->workbits & b->code_mask; b->workbits >>= b->code_size; b->nrworkbits -= b->code_size; /* Force first code of image to be a clear code */ if (b->prev_code == -2) code = b->clear_code; /* Branch on type of code */ if (code == b->clear_code) { /* Reset the decoder */ b->code_size = b->min_code_size + 1; /* Original size */ b->code_mask = (1 << b->code_size) - 1; b->free_code = b->clear_code + 2; /* First pos in tables */ b->prev_code = -1; /* Next code is a root code */ } else if (code == b->eoi_code) { /* End of Information */ b->state++; break; } else if (b->prev_code == -1) { /* 1st code after clearcode */ store_pixel(b, code); /* Add to image */ b->first = b->prev_code = code; } else { /* We've got a normal code */ assert(sp == 0); if (code >= b->free_code) { /* It's a new code */ stack[sp++] = b->first; b->first = b->prev_code; } else /* It's an existing code */ b->first = code; while (b->first >= b->clear_code) { /* Push string of pixels */ stack[sp++] = b->extnsn[b->first]; b->first = b->prefix[b->first]; } stack[sp++] = b->first; /* Push string's root code */ while (sp != 0) /* Now add pixels to image */ store_pixel(b, stack[--sp]); b->prefix[b->free_code] = b->prev_code; b->extnsn[b->free_code++] = b->first; b->prev_code = code; /* Check if code_size needs to increase */ if (b->free_code > b->code_mask && b->code_size != MAX_LZW_BITS) { b->code_size++; b->code_mask = (1 << b->code_size) - 1; } } break; case 9: /* Ignore additional images */ discard(b, b->buflen); b->ready = TRUE; return TRUE; case 400: /* Graphic Control Extension */ if (b->buflen < 6) return TRUE; b->transparent = b->buf[4]; /* Index in colortable */ discard(b, 6); b->state = 4; break; case 410: /* Other extension */ if (b->buflen < 1) return TRUE; if (b->buf[0] == 0) { /* Last block */ discard(b, 1); b->state = 4; } else if (b->buflen < 1 + (size_t) b->buf[0]) { return TRUE; } else { discard(b, 1 + b->buf[0]); /* Remove one block */ } break; default: assert(! "Cannot happen"); } } } /* GIF_redraw -- redraw the indicated area of a GIF image */ static void GIF_redraw2(Buffer b, int x, int y, int wd, int ht) { Widget w = b->canvas; #if 0 XtMakeResizeRequest(b->canvas, b->width, b->height, &dum1, &dum2); #endif /* * Only once: set the window shape */ if (b->mask != NULL && b->ready) { 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 */ if (gray_stdcmap && (b->use_gray || ! default_stdcmap)) 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); else if (default_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 assert(! "Cannot happen"); } /* GIF_redraw -- action proc: 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; assert(b->canvas == w); x = ev ? ev->xexpose.x : 0; y = ev ? ev->xexpose.y : 0; if (x >= b->width || y >= b->height) return; /* Nothing to draw */ wd = ev ? min(ev->xexpose.width, b->width - x) : b->width; ht = ev ? min(ev->xexpose.height, b->height - y) : b->height; GIF_redraw2(b, x, y, wd, ht); } /* GIF_click -- action for mouse clicks on canvas */ /* ARGSUSED */ static void GIF_click(Widget w, XEvent *ev, String *params, Cardinal *nparams) { long id; struct {int x, y;} coords; assert(*nparams == 1); if (sscanf(params[0], "%ld", &id) != 1) assert(!"Missing parameter"); coords.x = ev->xbutton.x; coords.y = ev->xbutton.y; #if 0 debug("Clicked on image: (%d,%d)\n", coords.x, coords.y); #endif W3Aevent(id, POINT_SELECT, &coords); #ifndef NDEBUG { Buffer b = find(id); assert(b->canvas == w); } #endif } static XtActionsRec actions[] = { {"GIF_expose", GIF_redraw}, {"GIF_click", GIF_click} }; /* * The 1st %ld points to the Buffer, the 2nd is the ID */ static char translations[] = ": GIF_expose(%ld)\n\ ,: GIF_click(%ld)"; /* initGIF -- initialize the GIF viewer class */ EXPORT Bool initGIF(char ***mime_types, int *nrtypes, float **prefs) { static char *types[] = {"image/gif"}; static float pref[] = {1.0}; Widget toplevel = W3Atoplevel(); Display *dpy = XtDisplay(toplevel); int screen = XScreenNumberOfScreen(XtScreen(toplevel)); *mime_types = types; *nrtypes = 1; *prefs = pref; 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(b, id); b->url = newstring(doc.url); b->buflen = 0; b->image = NULL; b->mask = NULL; b->canvas = window; b->state = 0; b->ready = FALSE; sprintf(s, translations, b, id); 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); int n, rest = nbytes; while (rest) { n = min(rest, sizeof(b->buf) - b->buflen); memcpy(b->buf + b->buflen, buf, n); b->buflen += n; rest -= n; buf += n; if (! parseGIF(b)) {errno = EFORMAT; return -1;} } /* When image complete, redraw it, to correct the dithering */ if (nbytes == 0 && XtIsRealized(b->canvas)) { #if 0 XClearArea(XtDisplay(b->canvas), XtWindow(b->canvas), 0, 0, 0, 0, TRUE); #else GIF_redraw2(b, 0, 0, b->width, b->height); #endif } return nbytes; } /* closeGIF -- close a GIF viewer */ EXPORT Bool closeGIF(long id) { Buffer b = find(id); dispose(b->url); dispose(b->image); delete(id); return TRUE; } /* eventGIF -- react to events happening elsewhere */ /* ARGSUSED */ EXPORT void eventGIF(long id, long source, long eventtype, void *params) { #if 0 debug("GIF viewer %ld received event %ld from %ld\n", id, eventtype, source); #endif /* 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; }