/* * 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 #include #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 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 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(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(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; }