You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
228 lines
7.8 KiB
228 lines
7.8 KiB
/*
|
|
* 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;
|
|
}
|