Add script/print-image-metadata.cc

Updates #58
diff --git a/doc/std/image-decoders.md b/doc/std/image-decoders.md
index 74f9fb6..63698d5 100644
--- a/doc/std/image-decoders.md
+++ b/doc/std/image-decoders.md
@@ -130,6 +130,7 @@
 - [example/imageviewer](/example/imageviewer)
 - [example/sdl-imageviewer](/example/sdl-imageviewer)
 - [script/print-average-pixel](/script/print-average-pixel.cc)
+- [script/print-image-metadata](/script/print-image-metadata.cc)
 
 Examples in other repositories:
 
diff --git a/doc/std/metadata.md b/doc/std/metadata.md
index 2ec954e..8f1d323 100644
--- a/doc/std/metadata.md
+++ b/doc/std/metadata.md
@@ -46,3 +46,17 @@
 parameters) when `tell_me_more` returns a NULL status, meaning ok, when the
 metadata is complete. Afterwards, call the original action (e.g. `decode_etc`)
 again to resume after the "@metadata reported" detour.
+
+`tell_me_more` both returns a status and fills in the `dst` and `minfo` out
+parameters. Subtly, the caller should process the out parameters before
+considering the status. It is valid for the callee to fill in the out
+parameters with partial results when returning an error status or with full
+results when returning an ok status. 'No partial results' is represented by an
+unchanged `dst` or `minfo`. For example, if `minfo` was reset to zero before
+each `tell_me_more` call then `minfo.flavor` remaining zero means that the
+`minfo` was unchanged, as valid flavor values are non-zero.
+
+
+## Examples
+
+- [script/print-image-metadata](/script/print-image-metadata.cc)
diff --git a/script/print-image-metadata.cc b/script/print-image-metadata.cc
new file mode 100644
index 0000000..4b50287
--- /dev/null
+++ b/script/print-image-metadata.cc
@@ -0,0 +1,441 @@
+// Copyright 2022 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.
+
+// ----------------
+
+/*
+print-image-metadata prints images' metadata.
+*/
+
+#include <inttypes.h>
+#include <stdio.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__STATIC_FUNCTIONS macro is optional, but when
+// combined with WUFFS_IMPLEMENTATION, it demonstrates making all of Wuffs'
+// functions have static storage.
+//
+// This can help the compiler ignore or discard unused code, which can produce
+// faster compiles and smaller binaries. Other motivations are discussed in the
+// "ALLOW STATIC IMPLEMENTATION" section of
+// https://raw.githubusercontent.com/nothings/stb/master/docs/stb_howto.txt
+#define WUFFS_CONFIG__STATIC_FUNCTIONS
+
+// Defining the WUFFS_CONFIG__MODULE* macros are optional, but it lets users of
+// release/c/etc.c choose 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__ADLER32
+#define WUFFS_CONFIG__MODULE__BASE
+#define WUFFS_CONFIG__MODULE__BMP
+#define WUFFS_CONFIG__MODULE__CRC32
+#define WUFFS_CONFIG__MODULE__DEFLATE
+#define WUFFS_CONFIG__MODULE__GIF
+#define WUFFS_CONFIG__MODULE__LZW
+#define WUFFS_CONFIG__MODULE__NIE
+#define WUFFS_CONFIG__MODULE__PNG
+#define WUFFS_CONFIG__MODULE__TGA
+#define WUFFS_CONFIG__MODULE__WBMP
+#define WUFFS_CONFIG__MODULE__ZLIB
+
+// 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"
+
+// ----
+
+#ifndef SRC_BUFFER_ARRAY_SIZE
+#define SRC_BUFFER_ARRAY_SIZE (64 * 1024)
+#endif
+
+#ifndef META_BUFFER_ARRAY_SIZE
+#define META_BUFFER_ARRAY_SIZE (64 * 1024)
+#endif
+
+#ifndef WORKBUF_ARRAY_SIZE
+#define WORKBUF_ARRAY_SIZE (256 * 1024 * 1024)
+#endif
+
+#define PRINTBUF_ARRAY_SIZE 80
+
+uint8_t g_src_buffer_array[SRC_BUFFER_ARRAY_SIZE] = {0};
+uint8_t g_meta_buffer_array[META_BUFFER_ARRAY_SIZE] = {0};
+uint8_t g_workbuf_array[WORKBUF_ARRAY_SIZE] = {0};
+
+uint8_t g_printbuf_array[PRINTBUF_ARRAY_SIZE] = {0};
+uint32_t g_printbuf_index = 0;
+
+// ----
+
+#define TRY(error_msg)         \
+  do {                         \
+    const char* z = error_msg; \
+    if (z) {                   \
+      return z;                \
+    }                          \
+  } while (false)
+
+const uint8_t  //
+    hexify[16] = {
+        '0', '1', '2', '3', '4', '5', '6', '7',  //
+        '8', '9', 'A', 'B', 'C', 'D', 'E', 'F',  //
+};
+
+const uint8_t  //
+    printable_ascii[256] = {
+        0x2E, 0x2E, 0x2E, 0x2E, 0x2E, 0x2E, 0x2E, 0x2E,  //
+        0x2E, 0x2E, 0x2E, 0x2E, 0x2E, 0x2E, 0x2E, 0x2E,  //
+        0x2E, 0x2E, 0x2E, 0x2E, 0x2E, 0x2E, 0x2E, 0x2E,  //
+        0x2E, 0x2E, 0x2E, 0x2E, 0x2E, 0x2E, 0x2E, 0x2E,  //
+        0x20, 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27,  //
+        0x28, 0x29, 0x2A, 0x2B, 0x2C, 0x2D, 0x2E, 0x2F,  //
+        0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37,  //
+        0x38, 0x39, 0x3A, 0x3B, 0x3C, 0x3D, 0x3E, 0x3F,  //
+
+        0x40, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47,  //
+        0x48, 0x49, 0x4A, 0x4B, 0x4C, 0x4D, 0x4E, 0x4F,  //
+        0x50, 0x51, 0x52, 0x53, 0x54, 0x55, 0x56, 0x57,  //
+        0x58, 0x59, 0x5A, 0x5B, 0x5C, 0x5D, 0x5E, 0x5F,  //
+        0x60, 0x61, 0x62, 0x63, 0x64, 0x65, 0x66, 0x67,  //
+        0x68, 0x69, 0x6A, 0x6B, 0x6C, 0x6D, 0x6E, 0x6F,  //
+        0x70, 0x71, 0x72, 0x73, 0x74, 0x75, 0x76, 0x77,  //
+        0x78, 0x79, 0x7A, 0x7B, 0x7C, 0x7D, 0x7E, 0x7F,  //
+
+        0x2E, 0x2E, 0x2E, 0x2E, 0x2E, 0x2E, 0x2E, 0x2E,  //
+        0x2E, 0x2E, 0x2E, 0x2E, 0x2E, 0x2E, 0x2E, 0x2E,  //
+        0x2E, 0x2E, 0x2E, 0x2E, 0x2E, 0x2E, 0x2E, 0x2E,  //
+        0x2E, 0x2E, 0x2E, 0x2E, 0x2E, 0x2E, 0x2E, 0x2E,  //
+        0x2E, 0x2E, 0x2E, 0x2E, 0x2E, 0x2E, 0x2E, 0x2E,  //
+        0x2E, 0x2E, 0x2E, 0x2E, 0x2E, 0x2E, 0x2E, 0x2E,  //
+        0x2E, 0x2E, 0x2E, 0x2E, 0x2E, 0x2E, 0x2E, 0x2E,  //
+        0x2E, 0x2E, 0x2E, 0x2E, 0x2E, 0x2E, 0x2E, 0x2E,  //
+
+        0x2E, 0x2E, 0x2E, 0x2E, 0x2E, 0x2E, 0x2E, 0x2E,  //
+        0x2E, 0x2E, 0x2E, 0x2E, 0x2E, 0x2E, 0x2E, 0x2E,  //
+        0x2E, 0x2E, 0x2E, 0x2E, 0x2E, 0x2E, 0x2E, 0x2E,  //
+        0x2E, 0x2E, 0x2E, 0x2E, 0x2E, 0x2E, 0x2E, 0x2E,  //
+        0x2E, 0x2E, 0x2E, 0x2E, 0x2E, 0x2E, 0x2E, 0x2E,  //
+        0x2E, 0x2E, 0x2E, 0x2E, 0x2E, 0x2E, 0x2E, 0x2E,  //
+        0x2E, 0x2E, 0x2E, 0x2E, 0x2E, 0x2E, 0x2E, 0x2E,  //
+        0x2E, 0x2E, 0x2E, 0x2E, 0x2E, 0x2E, 0x2E, 0x2E,  //
+};
+
+// ----
+
+const char*  //
+read_buffer_from_file(wuffs_base__io_buffer* buf, FILE* f) {
+  if (buf->meta.closed) {
+    return "main: unexpected end of file";
+  }
+  buf->compact();
+  size_t n = fread(buf->writer_pointer(), 1, buf->writer_length(), f);
+  buf->meta.wi += n;
+  buf->meta.closed = feof(f);
+  return ferror(f) ? "main: error reading file" : nullptr;
+}
+
+void  //
+print_fourcc(uint32_t fourcc) {
+  printf("  %c%c%c%c\n",           //
+         (0xFF & (fourcc >> 24)),  //
+         (0xFF & (fourcc >> 16)),  //
+         (0xFF & (fourcc >> 8)),   //
+         (0xFF & (fourcc >> 0)));
+}
+
+void  //
+flush_hex_dump() {
+  if (g_printbuf_index == 0) {
+    return;
+  }
+  puts(static_cast<const char*>(static_cast<const void*>(g_printbuf_array)));
+  g_printbuf_index = 0;
+}
+
+void  //
+print_hex_dump(const uint8_t* ptr, size_t len) {
+  while (len--) {
+    if (g_printbuf_index == 0) {
+      const char* s =
+          "    -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --    "
+          "----------------";
+      size_t n = strlen(s);
+      if ((n + 1) > PRINTBUF_ARRAY_SIZE) {
+        exit(1);
+      }
+      memcpy(&g_printbuf_array[0], s, n + 1);
+    }
+    const uint8_t c = *ptr++;
+    g_printbuf_array[(3 * g_printbuf_index) + 4] = hexify[c >> 4];
+    g_printbuf_array[(3 * g_printbuf_index) + 5] = hexify[c & 15];
+    g_printbuf_array[g_printbuf_index + 55] = printable_ascii[c];
+    g_printbuf_index++;
+    if (g_printbuf_index == 16) {
+      puts(
+          static_cast<const char*>(static_cast<const void*>(g_printbuf_array)));
+      g_printbuf_index = 0;
+    }
+  }
+}
+
+const char*  //
+print_raw_passthrough(wuffs_base__io_buffer* src,
+                      FILE* f,
+                      wuffs_base__range_ie_u64 r) {
+  if (r.is_empty()) {
+    return nullptr;
+  }
+
+  // Advance src so that its reader_position is r.min_incl.
+  if (src->reader_position() > r.min_incl) {
+    return "main: unsupported metadata range";
+  }
+  while (src->reader_position() < r.min_incl) {
+    if (src->writer_position() >= r.min_incl) {
+      src->meta.ri = r.min_incl - src->meta.pos;
+      break;
+    }
+    src->meta.ri = src->meta.wi;
+    TRY(read_buffer_from_file(src, f));
+  }
+
+  // Print the passthrough bytes until src's reader_position is r.max_excl.
+  while (true) {
+    uint64_t n0 = r.max_excl - src->reader_position();
+    if (n0 == 0) {
+      break;
+    }
+    uint64_t n1 = src->reader_length();
+    uint64_t n = wuffs_base__u64__min(n0, n1);
+    print_hex_dump(src->reader_pointer(), n);
+    src->meta.ri += n;
+  }
+
+  return nullptr;
+}
+
+const char*  //
+print_metadata(wuffs_base__image_decoder* dec,
+               wuffs_base__io_buffer* src,
+               FILE* f) {
+  bool printed_fourcc = false;
+  while (true) {
+    auto meta = wuffs_base__ptr_u8__writer(&g_meta_buffer_array[0],
+                                           META_BUFFER_ARRAY_SIZE);
+    auto minfo = wuffs_base__empty_more_information();
+    auto tmm_status = dec->tell_me_more(&meta, &minfo, src);
+
+    if (minfo.flavor) {
+      if (!printed_fourcc) {
+        printed_fourcc = true;
+        print_fourcc(minfo.metadata__fourcc());
+      }
+
+      switch (minfo.flavor) {
+        case WUFFS_BASE__MORE_INFORMATION__FLAVOR__METADATA_RAW_PASSTHROUGH:
+          TRY(print_raw_passthrough(src, f,
+                                    minfo.metadata_raw_passthrough__range()));
+          break;
+
+        case WUFFS_BASE__MORE_INFORMATION__FLAVOR__METADATA_RAW_TRANSFORM:
+          print_hex_dump(meta.reader_pointer(), meta.reader_length());
+          meta.meta.ri = meta.meta.wi;
+          break;
+
+        case WUFFS_BASE__MORE_INFORMATION__FLAVOR__METADATA_PARSED:
+          switch (minfo.metadata__fourcc()) {
+            case WUFFS_BASE__FOURCC__CHRM:
+              for (uint32_t i = 0; i < 8; i++) {
+                printf("    %" PRId32 "\n", minfo.metadata_parsed__chrm(i));
+              }
+              break;
+            case WUFFS_BASE__FOURCC__GAMA:
+              printf("    %" PRIu32 "\n", minfo.metadata_parsed__gama());
+              break;
+            case WUFFS_BASE__FOURCC__SRGB:
+              printf("    %" PRIu32 "\n", minfo.metadata_parsed__srgb());
+              break;
+            default:
+              return "main: unsupported metadata FourCC";
+          }
+          break;
+
+        default:
+          return "main: unsupported metadata flavor";
+      }
+    }
+
+    if (tmm_status.is_ok()) {
+      break;
+    } else if (tmm_status.repr == wuffs_base__suspension__short_read) {
+      TRY(read_buffer_from_file(src, f));
+      continue;
+    } else if (tmm_status.repr == wuffs_base__suspension__short_write) {
+      continue;
+    } else if (tmm_status.repr !=
+               wuffs_base__suspension__even_more_information) {
+      return tmm_status.message();
+    }
+  }
+  flush_hex_dump();
+
+  return nullptr;
+}
+
+const char*  //
+handle_redirect(int32_t* out_fourcc,
+                wuffs_base__image_decoder* dec,
+                wuffs_base__io_buffer* src,
+                FILE* f) {
+  auto empty = wuffs_base__empty_io_buffer();
+  auto minfo = wuffs_base__empty_more_information();
+  auto tmm_status = dec->tell_me_more(&empty, &minfo, src);
+  if (tmm_status.repr != NULL) {
+    return tmm_status.message();
+  } else if (minfo.flavor !=
+             WUFFS_BASE__MORE_INFORMATION__FLAVOR__IO_REDIRECT) {
+    return "main: unsupported file format";
+  }
+  *out_fourcc = (int32_t)(minfo.io_redirect__fourcc());
+  if (*out_fourcc <= 0) {
+    return "main: unsupported file format";
+  }
+
+  // Advance src so that its reader_position is r.min_incl.
+  auto r = minfo.io_redirect__range();
+  if (src->reader_position() > r.min_incl) {
+    return "main: unsupported I/O redirect range";
+  }
+  while (src->reader_position() < r.min_incl) {
+    if (src->writer_position() >= r.min_incl) {
+      src->meta.ri = r.min_incl - src->meta.pos;
+      break;
+    }
+    src->meta.ri = src->meta.wi;
+    TRY(read_buffer_from_file(src, f));
+  }
+  return nullptr;
+}
+
+const char*  //
+handle(const char* filename, FILE* f) {
+  auto src =
+      wuffs_base__ptr_u8__writer(&g_src_buffer_array[0], SRC_BUFFER_ARRAY_SIZE);
+  auto work =
+      wuffs_base__ptr_u8__writer(&g_workbuf_array[0], WORKBUF_ARRAY_SIZE);
+
+  int32_t fourcc = 0;
+  while (true) {
+    fourcc = wuffs_base__magic_number_guess_fourcc(src.reader_slice(),
+                                                   src.meta.closed);
+    if (fourcc > 0) {
+      break;
+    } else if (fourcc == 0) {
+      return "main: unrecognized file format";
+    } else {
+      TRY(read_buffer_from_file(&src, f));
+    }
+  }
+
+  bool redirected = false;
+redirect:
+  do {
+    print_fourcc(fourcc);
+    wuffs_base__image_decoder::unique_ptr dec(nullptr, &free);
+    switch (fourcc) {
+      case WUFFS_BASE__FOURCC__BMP:
+        dec = wuffs_bmp__decoder::alloc_as__wuffs_base__image_decoder();
+        break;
+      case WUFFS_BASE__FOURCC__GIF:
+        dec = wuffs_gif__decoder::alloc_as__wuffs_base__image_decoder();
+        break;
+      case WUFFS_BASE__FOURCC__NIE:
+        dec = wuffs_nie__decoder::alloc_as__wuffs_base__image_decoder();
+        break;
+      case WUFFS_BASE__FOURCC__PNG:
+        dec = wuffs_png__decoder::alloc_as__wuffs_base__image_decoder();
+        break;
+      case WUFFS_BASE__FOURCC__TGA:
+        dec = wuffs_tga__decoder::alloc_as__wuffs_base__image_decoder();
+        break;
+      case WUFFS_BASE__FOURCC__WBMP:
+        dec = wuffs_wbmp__decoder::alloc_as__wuffs_base__image_decoder();
+        break;
+      default:
+        return "main: unsupported file format";
+    }
+
+    dec->set_report_metadata(WUFFS_BASE__FOURCC__CHRM, true);
+    dec->set_report_metadata(WUFFS_BASE__FOURCC__EXIF, true);
+    dec->set_report_metadata(WUFFS_BASE__FOURCC__GAMA, true);
+    dec->set_report_metadata(WUFFS_BASE__FOURCC__ICCP, true);
+    dec->set_report_metadata(WUFFS_BASE__FOURCC__KVP, true);
+    dec->set_report_metadata(WUFFS_BASE__FOURCC__SRGB, true);
+    dec->set_report_metadata(WUFFS_BASE__FOURCC__XMP, true);
+
+    while (true) {
+      auto dfc_status = dec->decode_frame_config(NULL, &src);
+      if (dfc_status.is_ok()) {
+        // No-op.
+      } else if (dfc_status.repr == wuffs_base__note__end_of_data) {
+        break;
+      } else if (dfc_status.repr == wuffs_base__note__metadata_reported) {
+        TRY(print_metadata(dec.get(), &src, f));
+      } else if (dfc_status.repr == wuffs_base__note__i_o_redirect) {
+        if (redirected) {
+          return "main: unsupported file format";
+        }
+        redirected = true;
+        TRY(handle_redirect(&fourcc, dec.get(), &src, f));
+        goto redirect;
+      } else if (dfc_status.repr == wuffs_base__suspension__short_read) {
+        TRY(read_buffer_from_file(&src, f));
+      } else {
+        return dfc_status.message();
+      }
+    }
+  } while (false);
+
+  return nullptr;
+}
+
+int  //
+main(int argc, char** argv) {
+  for (int i = 1; i < argc; i++) {
+    FILE* f = fopen(argv[i], "r");
+    if (!f) {
+      printf("%s\n  %s\n", argv[i], strerror(errno));
+      continue;
+    }
+    printf("%s\n", argv[i]);
+    const char* err = handle(argv[i], f);
+    if (err) {
+      printf("  %s\n", err);
+    }
+    fclose(f);
+  }
+  return 0;
+}