diff options
Diffstat (limited to 'chromium/third_party/skia/tools/convert-to-nia.cpp')
-rw-r--r-- | chromium/third_party/skia/tools/convert-to-nia.cpp | 227 |
1 files changed, 227 insertions, 0 deletions
diff --git a/chromium/third_party/skia/tools/convert-to-nia.cpp b/chromium/third_party/skia/tools/convert-to-nia.cpp new file mode 100644 index 00000000000..58f9a67360f --- /dev/null +++ b/chromium/third_party/skia/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; +} |