| // 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; |
| } |