Add convert-to-nia command-line program

Together with similar programs in other repositories, this helps find
disagreements between Chromium's, Skia's and Wuffs' image decoders.

Change-Id: I9a0d8aabb47b1d5bd29f9139755e76bf56ab4bbe
Reviewed-on: https://skia-review.googlesource.com/c/skia/+/290618
Commit-Queue: Leon Scroggins <scroggo@google.com>
Reviewed-by: Leon Scroggins <scroggo@google.com>
diff --git a/BUILD.gn b/BUILD.gn
index 333995d..f3364d8 100644
--- a/BUILD.gn
+++ b/BUILD.gn
@@ -1717,6 +1717,10 @@
     }
   }
   if (target_cpu != "wasm") {
+    test_app("convert-to-nia") {
+      sources = [ "tools/convert-to-nia.cpp" ]
+      deps = [ ":skia" ]
+    }
     test_app("imgcvt") {
       sources = [ "tools/imgcvt.cpp" ]
       deps = [
diff --git a/tools/convert-to-nia.cpp b/tools/convert-to-nia.cpp
new file mode 100644
index 0000000..58f9a67
--- /dev/null
+++ b/tools/convert-to-nia.cpp
@@ -0,0 +1,227 @@
+/*
+ * Copyright 2020 Google Inc.
+ *
+ * Use of this source code is governed by a BSD-style license that can be
+ * found in the LICENSE file.
+ */
+
+// This program converts an image from stdin (e.g. a JPEG, PNG, etc.) to stdout
+// (in the NIA/NIE format, a trivial image file format).
+//
+// The NIA/NIE file format specification is at:
+// https://github.com/google/wuffs/blob/master/doc/spec/nie-spec.md
+//
+// Pass "-1" or "-first-frame-only" as a command line flag to output NIE (a
+// still image) instead of NIA (an animated image). The output format (NIA or
+// NIE) depends only on this flag's absence or presence, not on the stdin
+// image's format.
+//
+// There are multiple codec implementations of any given image format. For
+// example, as of May 2020, Chromium, Skia and Wuffs each have their own BMP
+// decoder implementation. There is no standard "libbmp" that they all share.
+// Comparing this program's output (or hashed output) to similar programs in
+// other repositories can identify image inputs for which these decoders (or
+// different versions of the same decoder) produce different output (pixels).
+//
+// An equivalent program (using the Chromium image codecs) is at:
+// https://crrev.com/c/2210331
+//
+// An equivalent program (using the Wuffs image codecs) is at:
+// https://github.com/google/wuffs/blob/master/example/convert-to-nia/convert-to-nia.c
+
+#include <stdio.h>
+#include <string.h>
+
+#include "include/codec/SkCodec.h"
+#include "include/core/SkBitmap.h"
+#include "include/core/SkData.h"
+#include "src/core/SkAutoMalloc.h"
+
+static inline void set_u32le(uint8_t* ptr, uint32_t val) {
+    ptr[0] = val >> 0;
+    ptr[1] = val >> 8;
+    ptr[2] = val >> 16;
+    ptr[3] = val >> 24;
+}
+
+static inline void set_u64le(uint8_t* ptr, uint64_t val) {
+    ptr[0] = val >> 0;
+    ptr[1] = val >> 8;
+    ptr[2] = val >> 16;
+    ptr[3] = val >> 24;
+    ptr[4] = val >> 32;
+    ptr[5] = val >> 40;
+    ptr[6] = val >> 48;
+    ptr[7] = val >> 56;
+}
+
+static void write_nix_header(uint32_t magicU32le, uint32_t width, uint32_t height) {
+    uint8_t data[16];
+    set_u32le(data + 0, magicU32le);
+    set_u32le(data + 4, 0x346E62FF);  // 4 bytes per pixel non-premul BGRA.
+    set_u32le(data + 8, width);
+    set_u32le(data + 12, height);
+    fwrite(data, 1, 16, stdout);
+}
+
+static bool write_nia_duration(uint64_t totalDurationMillis) {
+    // Flicks are NIA's unit of time. One flick (frame-tick) is 1 / 705_600_000
+    // of a second. See https://github.com/OculusVR/Flicks
+    static constexpr uint64_t flicksPerMilli = 705600;
+    if (totalDurationMillis > (INT64_MAX / flicksPerMilli)) {
+        // Converting from millis to flicks would overflow.
+        return false;
+    }
+
+    uint8_t data[8];
+    set_u64le(data + 0, totalDurationMillis * flicksPerMilli);
+    fwrite(data, 1, 8, stdout);
+    return true;
+}
+
+static void write_nie_pixels(uint32_t width, uint32_t height, const SkBitmap& bm) {
+    static constexpr size_t kBufferSize = 4096;
+    uint8_t                 buf[kBufferSize];
+    size_t                  n = 0;
+    for (uint32_t y = 0; y < height; y++) {
+        for (uint32_t x = 0; x < width; x++) {
+            SkColor c = bm.getColor(x, y);
+            buf[n++] = SkColorGetB(c);
+            buf[n++] = SkColorGetG(c);
+            buf[n++] = SkColorGetR(c);
+            buf[n++] = SkColorGetA(c);
+            if (n == kBufferSize) {
+                fwrite(buf, 1, n, stdout);
+                n = 0;
+            }
+        }
+    }
+    if (n > 0) {
+        fwrite(buf, 1, n, stdout);
+    }
+}
+
+static void write_nia_padding(uint32_t width, uint32_t height) {
+    // 4 bytes of padding when the width and height are both odd.
+    if (width & height & 1) {
+        uint8_t data[4];
+        set_u32le(data + 0, 0);
+        fwrite(data, 1, 4, stdout);
+    }
+}
+
+static void write_nia_footer(int repetitionCount, bool stillImage) {
+    uint8_t data[8];
+    if (stillImage || (repetitionCount == SkCodec::kRepetitionCountInfinite)) {
+        set_u32le(data + 0, 0);
+    } else {
+        // NIA's loop count and Skia's repetition count differ by one. See
+        // https://github.com/google/wuffs/blob/master/doc/spec/nie-spec.md#nii-footer
+        set_u32le(data + 0, 1 + repetitionCount);
+    }
+    set_u32le(data + 4, 0x80000000);
+    fwrite(data, 1, 8, stdout);
+}
+
+int main(int argc, char** argv) {
+    bool firstFrameOnly = false;
+    for (int a = 1; a < argc; a++) {
+        if ((strcmp(argv[a], "-1") == 0) || (strcmp(argv[a], "-first-frame-only") == 0)) {
+            firstFrameOnly = true;
+            break;
+        }
+    }
+
+    std::unique_ptr<SkCodec> codec(SkCodec::MakeFromData(SkData::MakeFromFILE(stdin)));
+    if (!codec) {
+        SkDebugf("Decode failed.\n");
+        return 1;
+    }
+    codec->getInfo().makeColorSpace(nullptr);
+    SkBitmap bm;
+    bm.allocPixels(codec->getInfo());
+    size_t bmByteSize = bm.computeByteSize();
+
+    // Cache a frame that future frames may depend on.
+    int          cachedFrame = SkCodec::kNoFrame;
+    SkAutoMalloc cachedFramePixels;
+
+    uint64_t  totalDurationMillis = 0;
+    const int frameCount = codec->getFrameCount();
+    if (frameCount == 0) {
+        SkDebugf("No frames.\n");
+        return 1;
+    }
+    // The SkCodec::getFrameInfo comment says that this vector will be empty
+    // for still (not animated) images, even though frameCount should be 1.
+    std::vector<SkCodec::FrameInfo> frameInfos = codec->getFrameInfo();
+    bool                            stillImage = frameInfos.empty();
+
+    for (int i = 0; i < frameCount; i++) {
+        SkCodec::Options opts;
+        opts.fFrameIndex = i;
+
+        if (!stillImage) {
+            int durationMillis = frameInfos[i].fDuration;
+            if (durationMillis < 0) {
+                SkDebugf("Negative animation duration.\n");
+                return 1;
+            }
+            totalDurationMillis += static_cast<uint64_t>(durationMillis);
+            if (totalDurationMillis > INT64_MAX) {
+                SkDebugf("Unsupported animation duration.\n");
+                return 1;
+            }
+
+            if ((cachedFrame != SkCodec::kNoFrame) &&
+                (cachedFrame == frameInfos[i].fRequiredFrame) && cachedFramePixels.get()) {
+                opts.fPriorFrame = cachedFrame;
+                memcpy(bm.getPixels(), cachedFramePixels.get(), bmByteSize);
+            }
+        }
+
+        if (!firstFrameOnly) {
+            if (i == 0) {
+                write_nix_header(0x41AFC36E,  // "nïA" magic string as a u32le.
+                                 bm.width(), bm.height());
+            }
+
+            if (!write_nia_duration(totalDurationMillis)) {
+                SkDebugf("Unsupported animation duration.\n");
+                return 1;
+            }
+        }
+
+        const SkCodec::Result result =
+            codec->getPixels(codec->getInfo(), bm.getPixels(), bm.rowBytes(), &opts);
+        if ((result != SkCodec::kSuccess) && (result != SkCodec::kIncompleteInput)) {
+            SkDebugf("Decode frame pixels #%d failed.\n", i);
+            return 1;
+        }
+
+        // If the next frame depends on this one, store it in cachedFrame. It
+        // is possible that we may discard a frame that future frames depend
+        // on, but the codec will simply redecode the discarded frame.
+        if ((static_cast<size_t>(i + 1) < frameInfos.size()) &&
+            (frameInfos[i + 1].fRequiredFrame == i)) {
+            cachedFrame = i;
+            memcpy(cachedFramePixels.reset(bmByteSize), bm.getPixels(), bmByteSize);
+        }
+
+        int width = bm.width();
+        int height = bm.height();
+        write_nix_header(0x45AFC36E,  // "nïE" magic string as a u32le.
+                         width, height);
+        write_nie_pixels(width, height, bm);
+        if (result == SkCodec::kIncompleteInput) {
+            SkDebugf("Incomplete input.\n");
+            return 1;
+        }
+        if (firstFrameOnly) {
+            return 0;
+        }
+        write_nia_padding(width, height);
+    }
+    write_nia_footer(codec->getRepetitionCount(), stillImage);
+    return 0;
+}