blob: 54bcf234ed51fd31d067cf3b4aa2e0225e100624 [file] [log] [blame]
// Copyright 2020 The Chromium Authors. All rights reserved.
// 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 Skia image codecs) is at:
// https://skia-review.googlesource.com/c/skia/+/290618
//
// 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 <iostream>
#include "base/command_line.h"
#include "base/files/file_util.h"
#include "base/task/single_thread_task_executor.h"
#include "third_party/blink/public/platform/platform.h"
#include "third_party/blink/renderer/platform/image-decoders/image_decoder.h"
#include "third_party/blink/renderer/platform/wtf/shared_buffer.h"
#include "third_party/skia/include/core/SkColor.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;
}
void write_nix_header(uint32_t magic_u32le, uint32_t width, uint32_t height) {
uint8_t data[16];
set_u32le(data + 0, magic_u32le);
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);
}
bool write_nia_duration(uint64_t total_duration_micros) {
// 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 flicks_per_ten_micros = 7056;
uint64_t d = total_duration_micros / 10;
if (d > (INT64_MAX / flicks_per_ten_micros)) {
// Converting from micros to flicks would overflow.
return false;
}
d *= flicks_per_ten_micros;
uint8_t data[8];
set_u64le(data + 0, d);
fwrite(data, 1, 8, stdout);
return true;
}
void write_nie_pixels(uint32_t width,
uint32_t height,
blink::ImageFrame* frame) {
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++) {
uint32_t pix = *(frame->GetAddr(x, y));
buf[n++] = pix >> SK_B32_SHIFT;
buf[n++] = pix >> SK_G32_SHIFT;
buf[n++] = pix >> SK_R32_SHIFT;
buf[n++] = pix >> SK_A32_SHIFT;
if (n == kBufferSize) {
fwrite(buf, 1, n, stdout);
n = 0;
}
}
}
if (n > 0) {
fwrite(buf, 1, n, stdout);
}
}
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);
}
}
void write_nia_footer(int repetition_count) {
uint8_t data[8];
// kAnimationNone means a still image.
if ((repetition_count == blink::kAnimationNone) ||
(repetition_count == blink::kAnimationLoopInfinite)) {
set_u32le(data + 0, 0);
} else {
// NIA's loop count and Chromium/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 + repetition_count);
}
set_u32le(data + 4, 0x80000000);
fwrite(data, 1, 8, stdout);
}
int main(int argc, char* argv[]) {
base::SingleThreadTaskExecutor main_task_executor;
base::CommandLine::Init(argc, argv);
std::unique_ptr<blink::Platform> platform =
std::make_unique<blink::Platform>();
blink::Platform::CreateMainThreadAndInitialize(platform.get());
base::CommandLine* command_line = base::CommandLine::ForCurrentProcess();
bool first_frame_only = command_line->HasSwitch("1") ||
command_line->HasSwitch("first-frame-only");
std::string src;
if (!base::ReadStreamToString(stdin, &src)) {
std::cerr << "could not read stdin\n";
return 1;
}
static constexpr bool data_complete = true;
std::unique_ptr<blink::ImageDecoder> decoder = blink::ImageDecoder::Create(
WTF::SharedBuffer::Create(src.data(), src.size()), data_complete,
blink::ImageDecoder::kAlphaNotPremultiplied,
blink::ImageDecoder::kDefaultBitDepth, blink::ColorBehavior::Ignore());
const size_t frame_count = decoder->FrameCount();
if (frame_count == 0) {
std::cerr << "no frames\n";
return 1;
}
int image_width;
int image_height;
uint64_t total_duration_micros = 0;
for (size_t i = 0; i < frame_count; i++) {
blink::ImageFrame* frame = decoder->DecodeFrameBufferAtIndex(i);
if (!frame) {
std::cerr << "could not decode frame #" << i << "\n";
return 1;
}
if (frame->GetPixelFormat() != blink::ImageFrame::kN32) {
std::cerr << "unsupported pixel format\n";
return 1;
}
const int frame_width = decoder->Size().Width();
const int frame_height = decoder->Size().Height();
if ((frame_width < 0) || (frame_height < 0)) {
std::cerr << "negative dimension\n";
return 1;
}
int64_t duration_micros = decoder->FrameDurationAtIndex(i).InMicroseconds();
if (duration_micros < 0) {
std::cerr << "negative animation duration\n";
return 1;
}
total_duration_micros += static_cast<uint64_t>(duration_micros);
if (total_duration_micros > INT64_MAX) {
std::cerr << "unsupported animation duration\n";
return 1;
}
if (!first_frame_only) {
if (i == 0) {
image_width = frame_width;
image_height = frame_height;
write_nix_header(0x41AFC36E, // "nïA" magic string as a u32le.
frame_width, frame_height);
} else if ((image_width != frame_width) ||
(image_height != frame_height)) {
std::cerr << "non-constant animation dimensions\n";
return 1;
}
if (!write_nia_duration(total_duration_micros)) {
std::cerr << "unsupported animation duration\n";
return 1;
}
}
write_nix_header(0x45AFC36E, // "nïE" magic string as a u32le.
frame_width, frame_height);
write_nie_pixels(frame_width, frame_height, frame);
if (first_frame_only) {
return 0;
}
write_nia_padding(frame_width, frame_height);
}
write_nia_footer(decoder->RepetitionCount());
return 0;
}