Add example/imageviewer
diff --git a/build-example.sh b/build-example.sh
index 052e5f9..1eaf0cc 100755
--- a/build-example.sh
+++ b/build-example.sh
@@ -43,6 +43,10 @@
     echo "Building gen/bin/example-$f"
     # example/crc32 is unusual in that it's C++, not C.
     $CXX -O3 example/$f/*.cc -o gen/bin/example-$f
+  elif [ $f = imageviewer ]; then
+    # example/imageviewer is unusual in that needs additional libraries.
+    echo "Building gen/bin/example-$f"
+    $CC -O3 example/$f/*.c -lxcb -lxcb-image -o gen/bin/example-$f
   elif [ $f = library ]; then
     # example/library is unusual in that it uses separately compiled libraries
     # (built by "wuffs genlib", e.g. by running build-all.sh) instead of
diff --git a/example/imageviewer/imageviewer.c b/example/imageviewer/imageviewer.c
new file mode 100644
index 0000000..0f95fc3
--- /dev/null
+++ b/example/imageviewer/imageviewer.c
@@ -0,0 +1,447 @@
+// Copyright 2020 The Wuffs Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//    https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// ----------------
+
+/*
+imageviewer is a simple GUI program for viewing images. On Linux, GUI means
+X11. To run:
+
+$CC imageviewer.c -lxcb -lxcb-image && \
+  ./a.out ../../test/data/bricks-*.gif; rm -f a.out
+
+for a C compiler $CC, such as clang or gcc.
+
+The Space and BackSpace keys cycle through the files, if more than one was
+given as command line arguments. If none were given, the program reads from
+stdin.
+
+The Return key is equivalent to the Space key.
+
+The Escape key quits.
+*/
+
+#include <inttypes.h>
+#include <stdio.h>
+#include <stdlib.h>
+
+// Wuffs ships as a "single file C library" or "header file library" as per
+// https://github.com/nothings/stb/blob/master/docs/stb_howto.txt
+//
+// To use that single file as a "foo.c"-like implementation, instead of a
+// "foo.h"-like header, #define WUFFS_IMPLEMENTATION before #include'ing or
+// compiling it.
+#define WUFFS_IMPLEMENTATION
+
+// Defining the WUFFS_CONFIG__MODULE* macros are optional, but it lets users of
+// release/c/etc.c whitelist which parts of Wuffs to build. That file contains
+// the entire Wuffs standard library, implementing a variety of codecs and file
+// formats. Without this macro definition, an optimizing compiler or linker may
+// very well discard Wuffs code for unused codecs, but listing the Wuffs
+// modules we use makes that process explicit. Preprocessing means that such
+// code simply isn't compiled.
+#define WUFFS_CONFIG__MODULES
+#define WUFFS_CONFIG__MODULE__BASE
+#define WUFFS_CONFIG__MODULE__GIF
+#define WUFFS_CONFIG__MODULE__LZW
+
+// If building this program in an environment that doesn't easily accommodate
+// relative includes, you can use the script/inline-c-relative-includes.go
+// program to generate a stand-alone C file.
+#include "../../release/c/wuffs-unsupported-snapshot.c"
+
+// X11 limits its image dimensions to uint16_t.
+#define MAX_DIMENSION 65535
+
+#define SRC_BUFFER_SIZE (64 * 1024)
+
+// Global variable names start with a "g_" prefix.
+
+FILE* g_file = NULL;
+const char* g_filename = NULL;
+uint32_t g_width = 0;
+uint32_t g_height = 0;
+wuffs_base__slice_u8 g_workbuf_slice = {0};
+wuffs_base__slice_u8 g_pixbuf_slice = {0};
+wuffs_base__pixel_buffer g_pixbuf = {0};
+uint8_t g_src_buffer[SRC_BUFFER_SIZE] = {0};
+wuffs_base__io_buffer g_src = {0};
+wuffs_base__image_config g_image_config = {0};
+wuffs_base__image_decoder* g_image_decoder = NULL;
+
+union {
+  wuffs_gif__decoder gif;
+} g_potential_decoders;
+
+bool read_more_src() {
+  if (g_src.meta.closed) {
+    printf("%s: unexpected end of file\n", g_filename);
+    return false;
+  }
+  wuffs_base__io_buffer__compact(&g_src);
+  g_src.meta.wi += fread(g_src.data.ptr + g_src.meta.wi, sizeof(uint8_t),
+                         g_src.data.len - g_src.meta.wi, g_file);
+  if (feof(g_file)) {
+    g_src.meta.closed = true;
+  } else if (ferror(g_file)) {
+    printf("%s: read error\n", g_filename);
+    return false;
+  }
+  return true;
+}
+
+bool load_image_type() {
+  while (g_src.meta.wi == 0) {
+    if (!read_more_src()) {
+      return false;
+    }
+  }
+
+  wuffs_base__status status;
+  switch (g_src_buffer[0]) {
+    case 'G':
+      status = wuffs_gif__decoder__initialize(
+          &g_potential_decoders.gif, sizeof g_potential_decoders.gif,
+          WUFFS_VERSION, WUFFS_INITIALIZE__DEFAULT_OPTIONS);
+      if (!wuffs_base__status__is_ok(&status)) {
+        printf("%s: %s\n", g_filename, wuffs_base__status__message(&status));
+        return false;
+      }
+      g_image_decoder =
+          wuffs_gif__decoder__upcast_as__wuffs_base__image_decoder(
+              &g_potential_decoders.gif);
+      break;
+
+    default:
+      printf("%s: unrecognized file format\n", g_filename);
+      return false;
+  }
+  return true;
+}
+
+bool load_image_config() {
+  // Decode the wuffs_base__image_config.
+  while (true) {
+    wuffs_base__status status = wuffs_base__image_decoder__decode_image_config(
+        g_image_decoder, &g_image_config, &g_src);
+
+    if (status.repr == NULL) {
+      break;
+    } else if (status.repr != wuffs_base__suspension__short_read) {
+      printf("%s: %s\n", g_filename, wuffs_base__status__message(&status));
+      return false;
+    }
+
+    if (!read_more_src()) {
+      return false;
+    }
+  }
+
+  // Read the dimensions.
+  uint32_t w = wuffs_base__pixel_config__width(&g_image_config.pixcfg);
+  uint32_t h = wuffs_base__pixel_config__height(&g_image_config.pixcfg);
+  if ((w > MAX_DIMENSION) || (h > MAX_DIMENSION)) {
+    printf("%s: image is too large\n", g_filename);
+    return false;
+  }
+  g_width = w;
+  g_height = h;
+
+  // Override the image's native pixel format to be BGRA_PREMUL.
+  wuffs_base__pixel_config__set(&g_image_config.pixcfg,
+                                WUFFS_BASE__PIXEL_FORMAT__BGRA_PREMUL,
+                                WUFFS_BASE__PIXEL_SUBSAMPLING__NONE, w, h);
+
+  // Allocate the work buffer memory.
+  uint64_t workbuf_len =
+      wuffs_base__image_decoder__workbuf_len(g_image_decoder).max_incl;
+  if (workbuf_len > SIZE_MAX) {
+    printf("%s: out of memory\n", g_filename);
+    return false;
+  }
+  if (workbuf_len > 0) {
+    void* p = malloc(workbuf_len);
+    if (!p) {
+      printf("%s: out of memory\n", g_filename);
+      return false;
+    }
+    g_workbuf_slice.ptr = p;
+    g_workbuf_slice.len = workbuf_len;
+  }
+
+  // Allocate the pixel buffer memory.
+  uint64_t num_pixels = ((uint64_t)w) * ((uint64_t)h);
+  if (num_pixels > (SIZE_MAX / sizeof(wuffs_base__color_u32_argb_premul))) {
+    printf("%s: image is too large\n", g_filename);
+    return false;
+  }
+  size_t n = num_pixels * sizeof(wuffs_base__color_u32_argb_premul);
+  void* p = calloc(n, 1);
+  if (!p) {
+    printf("%s: out of memory\n", g_filename);
+    return false;
+  }
+  g_pixbuf_slice.ptr = p;
+  g_pixbuf_slice.len = n;
+
+  // Configure the wuffs_base__pixel_buffer struct.
+  wuffs_base__status status = wuffs_base__pixel_buffer__set_from_slice(
+      &g_pixbuf, &g_image_config.pixcfg, g_pixbuf_slice);
+  if (!wuffs_base__status__is_ok(&status)) {
+    printf("%s: %s\n", g_filename, wuffs_base__status__message(&status));
+    return false;
+  }
+
+  return true;
+}
+
+// This function always returns true. If we get this far, we still display a
+// partial image, even if we encounter an error.
+bool load_image_frame() {
+  while (true) {
+    wuffs_base__status status = wuffs_base__image_decoder__decode_frame(
+        g_image_decoder, &g_pixbuf, &g_src, WUFFS_BASE__PIXEL_BLEND__SRC,
+        g_workbuf_slice, NULL);
+
+    if (status.repr == NULL) {
+      break;
+    } else if (status.repr != wuffs_base__suspension__short_read) {
+      printf("%s: %s\n", g_filename, wuffs_base__status__message(&status));
+      return true;
+    }
+
+    if (!read_more_src()) {
+      return true;
+    }
+  }
+
+  uint32_t w = wuffs_base__pixel_config__width(&g_image_config.pixcfg);
+  uint32_t h = wuffs_base__pixel_config__height(&g_image_config.pixcfg);
+  printf("%s: ok (%" PRIu32 " x %" PRIu32 ")\n", g_filename, w, h);
+  return true;
+}
+
+bool load_image(const char* filename) {
+  if (g_workbuf_slice.ptr != NULL) {
+    free(g_workbuf_slice.ptr);
+    g_workbuf_slice.ptr = NULL;
+    g_workbuf_slice.len = 0;
+  }
+  if (g_pixbuf_slice.ptr != NULL) {
+    free(g_pixbuf_slice.ptr);
+    g_pixbuf_slice.ptr = NULL;
+    g_pixbuf_slice.len = 0;
+  }
+  g_width = 0;
+  g_height = 0;
+  g_src.data.ptr = g_src_buffer;
+  g_src.data.len = SRC_BUFFER_SIZE;
+  g_src.meta.wi = 0;
+  g_src.meta.ri = 0;
+  g_src.meta.pos = 0;
+  g_src.meta.closed = false;
+  g_image_config = wuffs_base__null_image_config();
+  g_image_decoder = NULL;
+
+  g_file = stdin;
+  g_filename = "<stdin>";
+  if (filename) {
+    FILE* f = fopen(filename, "r");
+    if (f == NULL) {
+      printf("%s: could not open file\n", filename);
+      return false;
+    }
+    g_file = f;
+    g_filename = filename;
+  }
+
+  bool ret = load_image_type() && load_image_config() && load_image_frame();
+  if (filename) {
+    fclose(g_file);
+    g_file = NULL;
+  }
+  return ret;
+}
+
+  // ---------------------------------------------------------------------
+
+#if defined(__linux__)
+#define SUPPORTED_OPERATING_SYSTEM
+
+#include <xcb/xcb.h>
+#include <xcb/xcb_image.h>
+
+#define XK_BackSpace 0xFF08
+#define XK_Escape 0xFF1B
+#define XK_Return 0xFF0D
+
+xcb_atom_t g_atom_net_wm_name = XCB_NONE;
+xcb_atom_t g_atom_utf8_string = XCB_NONE;
+xcb_atom_t g_atom_wm_protocols = XCB_NONE;
+xcb_atom_t g_atom_wm_delete_window = XCB_NONE;
+xcb_pixmap_t g_pixmap = XCB_NONE;
+xcb_keysym_t* g_keysyms = NULL;
+xcb_get_keyboard_mapping_reply_t* g_keyboard_mapping = NULL;
+
+void init_keymap(xcb_connection_t* c, const xcb_setup_t* z) {
+  xcb_get_keyboard_mapping_cookie_t cookie = xcb_get_keyboard_mapping(
+      c, z->min_keycode, z->max_keycode - z->min_keycode + 1);
+  g_keyboard_mapping = xcb_get_keyboard_mapping_reply(c, cookie, NULL);
+  g_keysyms = (xcb_keysym_t*)(g_keyboard_mapping + 1);
+}
+
+xcb_window_t make_window(xcb_connection_t* c, xcb_screen_t* s) {
+  xcb_window_t w = xcb_generate_id(c);
+  uint32_t value_mask = XCB_CW_BACK_PIXEL | XCB_CW_EVENT_MASK;
+  uint32_t value_list[2];
+  value_list[0] = s->black_pixel;
+  value_list[1] = XCB_EVENT_MASK_EXPOSURE | XCB_EVENT_MASK_KEY_PRESS;
+  xcb_create_window(c, 0, w, s->root, 0, 0, 1024, 768, 0,
+                    XCB_WINDOW_CLASS_INPUT_OUTPUT, s->root_visual, value_mask,
+                    value_list);
+  xcb_change_property(c, XCB_PROP_MODE_REPLACE, w, g_atom_net_wm_name,
+                      g_atom_utf8_string, 8, 12, "Image Viewer");
+  xcb_change_property(c, XCB_PROP_MODE_REPLACE, w, g_atom_wm_protocols,
+                      XCB_ATOM_ATOM, 32, 1, &g_atom_wm_delete_window);
+  xcb_map_window(c, w);
+  return w;
+}
+
+bool load(xcb_connection_t* c,
+          xcb_screen_t* s,
+          xcb_window_t w,
+          xcb_gcontext_t g,
+          const char* filename) {
+  if (g_pixmap != XCB_NONE) {
+    xcb_free_pixmap(c, g_pixmap);
+  }
+
+  if (!load_image(filename)) {
+    return false;
+  }
+
+  xcb_create_pixmap(c, s->root_depth, g_pixmap, w, g_width, g_height);
+  xcb_image_t* image = xcb_image_create_native(
+      c, g_width, g_height, XCB_IMAGE_FORMAT_Z_PIXMAP, s->root_depth, NULL,
+      g_pixbuf_slice.len, g_pixbuf_slice.ptr);
+  xcb_image_put(c, g_pixmap, g, image, 0, 0, 0);
+  xcb_image_destroy(image);
+  return true;
+}
+
+int main(int argc, char** argv) {
+  xcb_connection_t* c = xcb_connect(NULL, NULL);
+  const xcb_setup_t* z = xcb_get_setup(c);
+  xcb_screen_t* s = xcb_setup_roots_iterator(z).data;
+
+  {
+    xcb_intern_atom_cookie_t cookie0 =
+        xcb_intern_atom(c, 1, 12, "_NET_WM_NAME");
+    xcb_intern_atom_cookie_t cookie1 = xcb_intern_atom(c, 1, 11, "UTF8_STRING");
+    xcb_intern_atom_cookie_t cookie2 =
+        xcb_intern_atom(c, 1, 12, "WM_PROTOCOLS");
+    xcb_intern_atom_cookie_t cookie3 =
+        xcb_intern_atom(c, 1, 16, "WM_DELETE_WINDOW");
+    xcb_intern_atom_reply_t* reply0 = xcb_intern_atom_reply(c, cookie0, NULL);
+    xcb_intern_atom_reply_t* reply1 = xcb_intern_atom_reply(c, cookie1, NULL);
+    xcb_intern_atom_reply_t* reply2 = xcb_intern_atom_reply(c, cookie2, NULL);
+    xcb_intern_atom_reply_t* reply3 = xcb_intern_atom_reply(c, cookie3, NULL);
+    g_atom_net_wm_name = reply0->atom;
+    g_atom_utf8_string = reply1->atom;
+    g_atom_wm_protocols = reply2->atom;
+    g_atom_wm_delete_window = reply3->atom;
+    free(reply0);
+    free(reply1);
+    free(reply2);
+    free(reply3);
+  }
+
+  xcb_window_t w = make_window(c, s);
+  xcb_gcontext_t g = xcb_generate_id(c);
+  xcb_create_gc(c, g, w, 0, NULL);
+  init_keymap(c, z);
+  xcb_flush(c);
+  g_pixmap = xcb_generate_id(c);
+
+  bool loaded = load(c, s, w, g, (argc > 1) ? argv[1] : NULL);
+  int arg = 1;
+
+  while (true) {
+    xcb_generic_event_t* event = xcb_wait_for_event(c);
+
+    switch (event->response_type & 0x7F) {
+      case XCB_EXPOSE: {
+        if (loaded) {
+          xcb_copy_area(c, g_pixmap, w, g, 0, 0, 0, 0, g_width, g_height);
+          xcb_flush(c);
+        }
+        break;
+      }
+
+      case XCB_KEY_PRESS: {
+        xcb_key_press_event_t* e = (xcb_key_press_event_t*)event;
+        uint32_t i = e->detail;
+        if ((z->min_keycode <= i) && (i <= z->max_keycode)) {
+          i = g_keysyms[(i - z->min_keycode) *
+                        g_keyboard_mapping->keysyms_per_keycode];
+          switch (i) {
+            case XK_Escape:
+              return 0;
+            case ' ':
+            case XK_BackSpace:
+            case XK_Return:
+              if (argc <= 2) {
+                break;
+              }
+              arg += (i != XK_BackSpace) ? +1 : -1;
+              if (arg == 0) {
+                arg = argc - 1;
+              } else if (arg == argc) {
+                arg = 1;
+              }
+              loaded = load(c, s, w, g, argv[arg]);
+              xcb_clear_area(c, 1, w, 0, 0, 0xFFFF, 0xFFFF);
+              xcb_flush(c);
+              break;
+          }
+        }
+        break;
+      }
+
+      case XCB_CLIENT_MESSAGE: {
+        xcb_client_message_event_t* e = (xcb_client_message_event_t*)event;
+        if (e->data.data32[0] == g_atom_wm_delete_window) {
+          return 0;
+        }
+        break;
+      }
+    }
+
+    free(event);
+  }
+  return 0;
+}
+
+#endif  // defined(__linux__)
+
+  // ---------------------------------------------------------------------
+
+#if !defined(SUPPORTED_OPERATING_SYSTEM)
+
+int main(int argc, char** argv) {
+  printf("unsupported operating system\n");
+  return 1;
+}
+
+#endif  // !defined(SUPPORTED_OPERATING_SYSTEM)