Add example/convert-to-nia
diff --git a/doc/changelog.md b/doc/changelog.md
index d4532cf..c80763d 100644
--- a/doc/changelog.md
+++ b/doc/changelog.md
@@ -8,6 +8,7 @@
 - Added `base` library support for UTF-8.
 - Added `base` library support for `atoi`-like string conversion.
 - Added `endwhile` syntax.
+- Added `example/convert-to-nia`.
 - Added `example/imageviewer`.
 - Added `example/jsonptr`.
 - Added `std/bmp`.
diff --git a/example/convert-to-nia/convert-to-nia.c b/example/convert-to-nia/convert-to-nia.c
new file mode 100644
index 0000000..c8a7927
--- /dev/null
+++ b/example/convert-to-nia/convert-to-nia.c
@@ -0,0 +1,557 @@
+// 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.
+
+// ----------------
+
+/*
+convert-to-nia converts an image from stdin (e.g. in the BMP, GIF, JPEG or PNG
+format) to stdout (in the NIA/NIE format).
+
+See the "const char* g_usage" string below for details.
+*/
+
+#include <errno.h>
+#include <inttypes.h>
+#include <unistd.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__BMP
+#define WUFFS_CONFIG__MODULE__GIF
+#define WUFFS_CONFIG__MODULE__LZW
+#define WUFFS_CONFIG__MODULE__WBMP
+
+// 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"
+
+// ----
+
+#if defined(__linux__)
+#include <linux/prctl.h>
+#include <linux/seccomp.h>
+#include <sys/prctl.h>
+#include <sys/syscall.h>
+#define WUFFS_EXAMPLE_USE_SECCOMP
+#endif
+
+#define TRY(error_msg)         \
+  do {                         \
+    const char* z = error_msg; \
+    if (z) {                   \
+      return z;                \
+    }                          \
+  } while (false)
+
+static const char* g_usage =
+    "Usage: convert-to-nia -flags < src.img > dst.nia\n"
+    "\n"
+    "Flags:\n"
+    "    -1      -first-frame-only\n"
+    "            -fail-if-unsandboxed\n"
+    "\n"
+    "convert-to-nia converts an image from stdin (e.g. in the BMP, GIF, JPEG\n"
+    "or PNG format) to stdout (in the NIA format, or in the NIE format if\n"
+    "the -first-frame-only flag is given).\n"
+    "\n"
+    "NIA/NIE is a trivial animated/still image file format, specified at\n"
+    "https://github.com/google/wuffs/blob/master/doc/spec/nie-spec.md\n"
+    "\n"
+    "The -fail-if-unsandboxed flag causes the program to exit if it does not\n"
+    "self-impose a sandbox. On Linux, it self-imposes a SECCOMP_MODE_STRICT\n"
+    "sandbox, regardless of whether this flag was set.";
+
+// ----
+
+#define STDIN_FD 0
+#define STDOUT_FD 1
+#define STDERR_FD 2
+
+bool g_sandboxed = false;
+
+wuffs_base__pixel_buffer g_pixbuf = {0};
+wuffs_base__slice_u8 g_pixbuf_slice = {0};
+wuffs_base__slice_u8 g_pixbuf_backup_slice = {0};
+wuffs_base__io_buffer g_src = {0};
+wuffs_base__slice_u8 g_workbuf_slice = {0};
+
+wuffs_base__image_config g_image_config = {0};
+wuffs_base__frame_config g_frame_config = {0};
+uint32_t g_width = 0;
+uint32_t g_height = 0;
+
+wuffs_base__image_decoder* g_image_decoder = NULL;
+union {
+  wuffs_bmp__decoder bmp;
+  wuffs_gif__decoder gif;
+  wuffs_wbmp__decoder wbmp;
+} g_potential_decoders;
+
+// ----
+
+#define BYTES_PER_PIXEL 4
+
+#ifndef MAX_DIMENSION
+#define MAX_DIMENSION 65535
+#endif
+
+#ifndef SRC_BUFFER_ARRAY_SIZE
+#define SRC_BUFFER_ARRAY_SIZE (64 * 1024)
+#endif
+
+#ifndef WORKBUF_ARRAY_SIZE
+#define WORKBUF_ARRAY_SIZE (1024 * 1024)
+#endif
+
+#ifndef PIXBUF_ARRAY_SIZE
+#define PIXBUF_ARRAY_SIZE (256 * 1024 * 1024)
+#endif
+
+uint8_t g_src_buffer_array[SRC_BUFFER_ARRAY_SIZE] = {0};
+uint8_t g_workbuf_array[WORKBUF_ARRAY_SIZE] = {0};
+uint8_t g_pixbuf_array[PIXBUF_ARRAY_SIZE] = {0};
+
+// ----
+
+struct {
+  int remaining_argc;
+  char** remaining_argv;
+
+  bool fail_if_unsandboxed;
+  bool first_frame_only;
+} g_flags = {0};
+
+const char*  //
+parse_flags(int argc, char** argv) {
+  int c = (argc > 0) ? 1 : 0;  // Skip argv[0], the program name.
+  for (; c < argc; c++) {
+    char* arg = argv[c];
+    if (*arg++ != '-') {
+      break;
+    }
+
+    // A double-dash "--foo" is equivalent to a single-dash "-foo". As special
+    // cases, a bare "-" is not a flag (some programs may interpret it as
+    // stdin) and a bare "--" means to stop parsing flags.
+    if (*arg == '\x00') {
+      break;
+    } else if (*arg == '-') {
+      arg++;
+      if (*arg == '\x00') {
+        c++;
+        break;
+      }
+    }
+
+    if (!strcmp(arg, "fail-if-unsandboxed")) {
+      g_flags.fail_if_unsandboxed = true;
+      continue;
+    }
+    if (!strcmp(arg, "1") || !strcmp(arg, "first-frame-only")) {
+      g_flags.first_frame_only = true;
+      continue;
+    }
+
+    return g_usage;
+  }
+
+  g_flags.remaining_argc = argc - c;
+  g_flags.remaining_argv = argv + c;
+  return NULL;
+}
+
+// ----
+
+// ignore_return_value suppresses errors from -Wall -Werror.
+static void  //
+ignore_return_value(int ignored) {}
+
+const char*  //
+read_more_src() {
+  if (g_src.meta.closed) {
+    return "main: unexpected end of file";
+  }
+  wuffs_base__io_buffer__compact(&g_src);
+  ssize_t n = read(STDIN_FD, g_src.data.ptr + g_src.meta.wi,
+                   g_src.data.len - g_src.meta.wi);
+  if (n > 0) {
+    g_src.meta.wi += n;
+  } else if (errno == 0) {
+    if (n < 0) {
+      return "main: unexpected negative-count read";
+    }
+    g_src.meta.closed = true;
+  } else if (errno != EINTR) {
+    return strerror(errno);
+  }
+  return NULL;
+}
+
+const char*  //
+load_image_type() {
+  while (g_src.meta.ri >= g_src.meta.wi) {
+    TRY(read_more_src());
+  }
+
+  wuffs_base__status status;
+  switch (g_src_buffer_array[0]) {
+    case '\x00':
+      status = wuffs_wbmp__decoder__initialize(
+          &g_potential_decoders.wbmp, sizeof g_potential_decoders.wbmp,
+          WUFFS_VERSION, WUFFS_INITIALIZE__DEFAULT_OPTIONS);
+      TRY(wuffs_base__status__message(&status));
+      g_image_decoder =
+          wuffs_wbmp__decoder__upcast_as__wuffs_base__image_decoder(
+              &g_potential_decoders.wbmp);
+      break;
+
+    case 'B':
+      status = wuffs_bmp__decoder__initialize(
+          &g_potential_decoders.bmp, sizeof g_potential_decoders.bmp,
+          WUFFS_VERSION, WUFFS_INITIALIZE__DEFAULT_OPTIONS);
+      TRY(wuffs_base__status__message(&status));
+      g_image_decoder =
+          wuffs_bmp__decoder__upcast_as__wuffs_base__image_decoder(
+              &g_potential_decoders.bmp);
+      break;
+
+    case 'G':
+      status = wuffs_gif__decoder__initialize(
+          &g_potential_decoders.gif, sizeof g_potential_decoders.gif,
+          WUFFS_VERSION, WUFFS_INITIALIZE__DEFAULT_OPTIONS);
+      TRY(wuffs_base__status__message(&status));
+      g_image_decoder =
+          wuffs_gif__decoder__upcast_as__wuffs_base__image_decoder(
+              &g_potential_decoders.gif);
+      break;
+
+    default:
+      return "main: unrecognized file format";
+  }
+  return NULL;
+}
+
+const char*  //
+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) {
+      return wuffs_base__status__message(&status);
+    }
+    TRY(read_more_src());
+  }
+
+  // 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)) {
+    return "main: image is too large";
+  }
+  g_width = w;
+  g_height = h;
+
+  // Override the image's native pixel format to be BGRA_NONPREMUL.
+  wuffs_base__pixel_config__set(&g_image_config.pixcfg,
+                                WUFFS_BASE__PIXEL_FORMAT__BGRA_NONPREMUL,
+                                WUFFS_BASE__PIXEL_SUBSAMPLING__NONE, w, h);
+
+  // Configure the work buffer.
+  uint64_t workbuf_len =
+      wuffs_base__image_decoder__workbuf_len(g_image_decoder).max_incl;
+  if (workbuf_len > WORKBUF_ARRAY_SIZE) {
+    return "main: image is too large (to configure work buffer)";
+  }
+  g_workbuf_slice.ptr = &g_workbuf_array[0];
+  g_workbuf_slice.len = workbuf_len;
+
+  // Configure the pixel buffer and (if there's capacity) its backup buffer.
+  uint64_t num_pixels = ((uint64_t)w) * ((uint64_t)h);
+  if (num_pixels > (PIXBUF_ARRAY_SIZE / BYTES_PER_PIXEL)) {
+    return "main: image is too large (to configure pixel buffer)";
+  }
+  g_pixbuf_slice.ptr = &g_pixbuf_array[0];
+  g_pixbuf_slice.len = num_pixels * BYTES_PER_PIXEL;
+  size_t pixbuf_array_remaining = PIXBUF_ARRAY_SIZE - g_pixbuf_slice.len;
+  if (pixbuf_array_remaining >= g_pixbuf_slice.len) {
+    g_pixbuf_backup_slice.ptr = &g_pixbuf_array[g_pixbuf_slice.len];
+    g_pixbuf_backup_slice.len = g_pixbuf_slice.len;
+  }
+
+  // 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);
+  TRY(wuffs_base__status__message(&status));
+
+  wuffs_base__table_u8 tab = wuffs_base__pixel_buffer__plane(&g_pixbuf, 0);
+  if ((tab.width != (g_width * BYTES_PER_PIXEL)) || (tab.height != g_height)) {
+    return "main: inconsistent pixel buffer dimensions";
+  }
+
+  return NULL;
+}
+
+void  //
+fill_rectangle(wuffs_base__rect_ie_u32 rect,
+               wuffs_base__color_u32_argb_premul color) {
+  if (rect.max_excl_x > g_width) {
+    rect.max_excl_x = g_width;
+  }
+  if (rect.max_excl_y > g_height) {
+    rect.max_excl_y = g_height;
+  }
+  uint32_t nonpremul = wuffs_base__nonpremul_u32_axxx(color);
+  wuffs_base__table_u8 tab = wuffs_base__pixel_buffer__plane(&g_pixbuf, 0);
+
+  uint32_t y;
+  for (y = rect.min_incl_y; y < rect.max_excl_y; y++) {
+    uint8_t* p =
+        tab.ptr + (y * tab.stride) + (rect.min_incl_x * BYTES_PER_PIXEL);
+    uint32_t x;
+    for (x = rect.min_incl_x; x < rect.max_excl_x; x++) {
+      wuffs_base__store_u32le__no_bounds_check(p, nonpremul);
+      p += BYTES_PER_PIXEL;
+    }
+  }
+}
+
+void  //
+print_nix_header(uint32_t magic_u32le) {
+  static const uint32_t version1_bn4_u32le = 0x346E62FF;
+  uint8_t data[16];
+  wuffs_base__store_u32le__no_bounds_check(data + 0x00, magic_u32le);
+  wuffs_base__store_u32le__no_bounds_check(data + 0x04, version1_bn4_u32le);
+  wuffs_base__store_u32le__no_bounds_check(data + 0x08, g_width);
+  wuffs_base__store_u32le__no_bounds_check(data + 0x0C, g_height);
+  ignore_return_value(write(STDOUT_FD, &data[0], 16));
+}
+
+void  //
+print_nia_duration(wuffs_base__flicks duration) {
+  uint8_t data[8];
+  wuffs_base__store_u64le__no_bounds_check(data + 0x00, duration);
+  ignore_return_value(write(STDOUT_FD, &data[0], 8));
+}
+
+void  //
+print_nie_frame() {
+  print_nix_header(0x45AFC36E);  // "nïE" as a u32le.
+  wuffs_base__table_u8 tab = wuffs_base__pixel_buffer__plane(&g_pixbuf, 0);
+  if (tab.width == tab.stride) {
+    ignore_return_value(write(STDOUT_FD, tab.ptr, tab.width * tab.height));
+  } else {
+    size_t y;
+    for (y = 0; y < tab.height; y++) {
+      ignore_return_value(
+          write(STDOUT_FD, tab.ptr + (y * tab.stride), tab.width));
+      break;
+    }
+  }
+}
+
+void  //
+print_nia_padding() {
+  if (g_width & g_height & 1) {
+    uint8_t data[4];
+    wuffs_base__store_u32le__no_bounds_check(data + 0x00, 0);
+    ignore_return_value(write(STDOUT_FD, &data[0], 4));
+  }
+}
+
+void  //
+print_nia_footer() {
+  uint8_t data[8];
+  wuffs_base__store_u32le__no_bounds_check(
+      data + 0x00,
+      wuffs_base__image_decoder__num_animation_loops(g_image_decoder));
+  wuffs_base__store_u32le__no_bounds_check(data + 0x04, 0x80000000);
+  ignore_return_value(write(STDOUT_FD, &data[0], 8));
+}
+
+const char*  //
+main1(int argc, char** argv) {
+  TRY(parse_flags(argc, argv));
+  if (g_flags.fail_if_unsandboxed && !g_sandboxed) {
+    return "main: unsandboxed";
+  }
+
+  g_src.data.ptr = g_src_buffer_array;
+  g_src.data.len = SRC_BUFFER_ARRAY_SIZE;
+
+  TRY(load_image_type());
+  TRY(load_image_config());
+  if (!g_flags.first_frame_only) {
+    print_nix_header(0x41AFC36E);  // "nïA" as a u32le.
+  }
+
+  wuffs_base__flicks total_duration = 0;
+  while (true) {
+    // Decode the wuffs_base__frame_config.
+    while (true) {
+      wuffs_base__status dfc_status =
+          wuffs_base__image_decoder__decode_frame_config(
+              g_image_decoder, &g_frame_config, &g_src);
+      if (dfc_status.repr == NULL) {
+        break;
+      } else if (dfc_status.repr == wuffs_base__note__end_of_data) {
+        goto done;
+      } else if (dfc_status.repr != wuffs_base__suspension__short_read) {
+        return wuffs_base__status__message(&dfc_status);
+      }
+      TRY(read_more_src());
+    }
+
+    wuffs_base__flicks duration =
+        wuffs_base__frame_config__duration(&g_frame_config);
+    if (duration < 0) {
+      return "main: animation frame duration is negative";
+    } else if (total_duration > (INT64_MAX - duration)) {
+      return "main: animation frame duration overflow";
+    }
+    total_duration += duration;
+    if (!g_flags.first_frame_only) {
+      print_nia_duration(total_duration);
+    }
+
+    if (wuffs_base__frame_config__index(&g_frame_config) == 0) {
+      fill_rectangle(
+          wuffs_base__pixel_config__bounds(&g_image_config.pixcfg),
+          wuffs_base__frame_config__background_color(&g_frame_config));
+    }
+
+    switch (wuffs_base__frame_config__disposal(&g_frame_config)) {
+      case WUFFS_BASE__ANIMATION_DISPOSAL__RESTORE_PREVIOUS: {
+        if (g_pixbuf_slice.len != g_pixbuf_backup_slice.len) {
+          return "main: image is too large (to configure pixel backup buffer)";
+        }
+        memcpy(g_pixbuf_backup_slice.ptr, g_pixbuf_slice.ptr,
+               g_pixbuf_slice.len);
+        break;
+      }
+    }
+
+    // Decode the frame (the pixels).
+    wuffs_base__status df_status;
+    while (true) {
+      df_status = wuffs_base__image_decoder__decode_frame(
+          g_image_decoder, &g_pixbuf, &g_src,
+          wuffs_base__frame_config__overwrite_instead_of_blend(&g_frame_config)
+              ? WUFFS_BASE__PIXEL_BLEND__SRC
+              : WUFFS_BASE__PIXEL_BLEND__SRC_OVER,
+          g_workbuf_slice, NULL);
+      if (df_status.repr != wuffs_base__suspension__short_read) {
+        break;
+      }
+      TRY(read_more_src());
+    }
+
+    print_nie_frame();
+
+    if (df_status.repr != NULL) {
+      return wuffs_base__status__message(&df_status);
+    } else if (g_flags.first_frame_only) {
+      return NULL;
+    }
+    print_nia_padding();
+
+    switch (wuffs_base__frame_config__disposal(&g_frame_config)) {
+      case WUFFS_BASE__ANIMATION_DISPOSAL__RESTORE_BACKGROUND: {
+        fill_rectangle(
+            wuffs_base__frame_config__bounds(&g_frame_config),
+            wuffs_base__frame_config__background_color(&g_frame_config));
+        break;
+      }
+      case WUFFS_BASE__ANIMATION_DISPOSAL__RESTORE_PREVIOUS: {
+        if (g_pixbuf_slice.len != g_pixbuf_backup_slice.len) {
+          return "main: image is too large (to configure pixel backup buffer)";
+        }
+        memcpy(g_pixbuf_slice.ptr, g_pixbuf_backup_slice.ptr,
+               g_pixbuf_slice.len);
+        break;
+      }
+    }
+  }
+
+done:
+  print_nia_footer();
+  return NULL;
+}
+
+int  //
+compute_exit_code(const char* status_msg) {
+  if (!status_msg) {
+    return 0;
+  }
+  size_t n;
+  if (status_msg == g_usage) {
+    n = strlen(status_msg);
+  } else {
+    n = strnlen(status_msg, 2047);
+    if (n >= 2047) {
+      status_msg = "main: internal error: error message is too long";
+      n = strnlen(status_msg, 2047);
+    }
+  }
+  ignore_return_value(write(STDERR_FD, status_msg, n));
+  ignore_return_value(write(STDERR_FD, "\n", 1));
+  // Return an exit code of 1 for regular (forseen) errors, e.g. badly
+  // formatted or unsupported input.
+  //
+  // Return an exit code of 2 for internal (exceptional) errors, e.g. defensive
+  // run-time checks found that an internal invariant did not hold.
+  //
+  // Automated testing, including badly formatted inputs, can therefore
+  // discriminate between expected failure (exit code 1) and unexpected failure
+  // (other non-zero exit codes). Specifically, exit code 2 for internal
+  // invariant violation, exit code 139 (which is 128 + SIGSEGV on x86_64
+  // linux) for a segmentation fault (e.g. null pointer dereference).
+  return strstr(status_msg, "internal error:") ? 2 : 1;
+}
+
+int  //
+main(int argc, char** argv) {
+#if defined(WUFFS_EXAMPLE_USE_SECCOMP)
+  prctl(PR_SET_SECCOMP, SECCOMP_MODE_STRICT);
+  g_sandboxed = true;
+#endif
+
+  int exit_code = compute_exit_code(main1(argc, argv));
+
+#if defined(WUFFS_EXAMPLE_USE_SECCOMP)
+  // Call SYS_exit explicitly, instead of calling SYS_exit_group implicitly by
+  // either calling _exit or returning from main. SECCOMP_MODE_STRICT allows
+  // only SYS_exit.
+  syscall(SYS_exit, exit_code);
+#endif
+  return exit_code;
+}
diff --git a/test/data/animated-red-blue.nia b/test/data/animated-red-blue.nia
new file mode 100644
index 0000000..0131e98
--- /dev/null
+++ b/test/data/animated-red-blue.nia
Binary files differ
diff --git a/test/data/hippopotamus.nie b/test/data/hippopotamus.nie
new file mode 100644
index 0000000..b14c29e
--- /dev/null
+++ b/test/data/hippopotamus.nie
Binary files differ