Nigel Tao | 3ab9b1f | 2020-05-28 13:29:13 +1000 | [diff] [blame] | 1 | /* |
| 2 | * Copyright 2020 Google Inc. |
| 3 | * |
| 4 | * Use of this source code is governed by a BSD-style license that can be |
| 5 | * found in the LICENSE file. |
| 6 | */ |
| 7 | |
| 8 | // This program converts an image from stdin (e.g. a JPEG, PNG, etc.) to stdout |
| 9 | // (in the NIA/NIE format, a trivial image file format). |
| 10 | // |
| 11 | // The NIA/NIE file format specification is at: |
| 12 | // https://github.com/google/wuffs/blob/master/doc/spec/nie-spec.md |
| 13 | // |
| 14 | // Pass "-1" or "-first-frame-only" as a command line flag to output NIE (a |
| 15 | // still image) instead of NIA (an animated image). The output format (NIA or |
| 16 | // NIE) depends only on this flag's absence or presence, not on the stdin |
| 17 | // image's format. |
| 18 | // |
| 19 | // There are multiple codec implementations of any given image format. For |
| 20 | // example, as of May 2020, Chromium, Skia and Wuffs each have their own BMP |
| 21 | // decoder implementation. There is no standard "libbmp" that they all share. |
| 22 | // Comparing this program's output (or hashed output) to similar programs in |
| 23 | // other repositories can identify image inputs for which these decoders (or |
| 24 | // different versions of the same decoder) produce different output (pixels). |
| 25 | // |
| 26 | // An equivalent program (using the Chromium image codecs) is at: |
| 27 | // https://crrev.com/c/2210331 |
| 28 | // |
| 29 | // An equivalent program (using the Wuffs image codecs) is at: |
| 30 | // https://github.com/google/wuffs/blob/master/example/convert-to-nia/convert-to-nia.c |
| 31 | |
| 32 | #include <stdio.h> |
| 33 | #include <string.h> |
| 34 | |
| 35 | #include "include/codec/SkCodec.h" |
| 36 | #include "include/core/SkBitmap.h" |
| 37 | #include "include/core/SkData.h" |
| 38 | #include "src/core/SkAutoMalloc.h" |
| 39 | |
| 40 | static inline void set_u32le(uint8_t* ptr, uint32_t val) { |
| 41 | ptr[0] = val >> 0; |
| 42 | ptr[1] = val >> 8; |
| 43 | ptr[2] = val >> 16; |
| 44 | ptr[3] = val >> 24; |
| 45 | } |
| 46 | |
| 47 | static inline void set_u64le(uint8_t* ptr, uint64_t val) { |
| 48 | ptr[0] = val >> 0; |
| 49 | ptr[1] = val >> 8; |
| 50 | ptr[2] = val >> 16; |
| 51 | ptr[3] = val >> 24; |
| 52 | ptr[4] = val >> 32; |
| 53 | ptr[5] = val >> 40; |
| 54 | ptr[6] = val >> 48; |
| 55 | ptr[7] = val >> 56; |
| 56 | } |
| 57 | |
| 58 | static void write_nix_header(uint32_t magicU32le, uint32_t width, uint32_t height) { |
| 59 | uint8_t data[16]; |
| 60 | set_u32le(data + 0, magicU32le); |
| 61 | set_u32le(data + 4, 0x346E62FF); // 4 bytes per pixel non-premul BGRA. |
| 62 | set_u32le(data + 8, width); |
| 63 | set_u32le(data + 12, height); |
| 64 | fwrite(data, 1, 16, stdout); |
| 65 | } |
| 66 | |
| 67 | static bool write_nia_duration(uint64_t totalDurationMillis) { |
| 68 | // Flicks are NIA's unit of time. One flick (frame-tick) is 1 / 705_600_000 |
| 69 | // of a second. See https://github.com/OculusVR/Flicks |
| 70 | static constexpr uint64_t flicksPerMilli = 705600; |
| 71 | if (totalDurationMillis > (INT64_MAX / flicksPerMilli)) { |
| 72 | // Converting from millis to flicks would overflow. |
| 73 | return false; |
| 74 | } |
| 75 | |
| 76 | uint8_t data[8]; |
| 77 | set_u64le(data + 0, totalDurationMillis * flicksPerMilli); |
| 78 | fwrite(data, 1, 8, stdout); |
| 79 | return true; |
| 80 | } |
| 81 | |
| 82 | static void write_nie_pixels(uint32_t width, uint32_t height, const SkBitmap& bm) { |
| 83 | static constexpr size_t kBufferSize = 4096; |
| 84 | uint8_t buf[kBufferSize]; |
| 85 | size_t n = 0; |
| 86 | for (uint32_t y = 0; y < height; y++) { |
| 87 | for (uint32_t x = 0; x < width; x++) { |
| 88 | SkColor c = bm.getColor(x, y); |
| 89 | buf[n++] = SkColorGetB(c); |
| 90 | buf[n++] = SkColorGetG(c); |
| 91 | buf[n++] = SkColorGetR(c); |
| 92 | buf[n++] = SkColorGetA(c); |
| 93 | if (n == kBufferSize) { |
| 94 | fwrite(buf, 1, n, stdout); |
| 95 | n = 0; |
| 96 | } |
| 97 | } |
| 98 | } |
| 99 | if (n > 0) { |
| 100 | fwrite(buf, 1, n, stdout); |
| 101 | } |
| 102 | } |
| 103 | |
| 104 | static void write_nia_padding(uint32_t width, uint32_t height) { |
| 105 | // 4 bytes of padding when the width and height are both odd. |
| 106 | if (width & height & 1) { |
| 107 | uint8_t data[4]; |
| 108 | set_u32le(data + 0, 0); |
| 109 | fwrite(data, 1, 4, stdout); |
| 110 | } |
| 111 | } |
| 112 | |
| 113 | static void write_nia_footer(int repetitionCount, bool stillImage) { |
| 114 | uint8_t data[8]; |
| 115 | if (stillImage || (repetitionCount == SkCodec::kRepetitionCountInfinite)) { |
| 116 | set_u32le(data + 0, 0); |
| 117 | } else { |
| 118 | // NIA's loop count and Skia's repetition count differ by one. See |
| 119 | // https://github.com/google/wuffs/blob/master/doc/spec/nie-spec.md#nii-footer |
| 120 | set_u32le(data + 0, 1 + repetitionCount); |
| 121 | } |
| 122 | set_u32le(data + 4, 0x80000000); |
| 123 | fwrite(data, 1, 8, stdout); |
| 124 | } |
| 125 | |
| 126 | int main(int argc, char** argv) { |
| 127 | bool firstFrameOnly = false; |
| 128 | for (int a = 1; a < argc; a++) { |
| 129 | if ((strcmp(argv[a], "-1") == 0) || (strcmp(argv[a], "-first-frame-only") == 0)) { |
| 130 | firstFrameOnly = true; |
| 131 | break; |
| 132 | } |
| 133 | } |
| 134 | |
| 135 | std::unique_ptr<SkCodec> codec(SkCodec::MakeFromData(SkData::MakeFromFILE(stdin))); |
| 136 | if (!codec) { |
| 137 | SkDebugf("Decode failed.\n"); |
| 138 | return 1; |
| 139 | } |
| 140 | codec->getInfo().makeColorSpace(nullptr); |
| 141 | SkBitmap bm; |
| 142 | bm.allocPixels(codec->getInfo()); |
| 143 | size_t bmByteSize = bm.computeByteSize(); |
| 144 | |
| 145 | // Cache a frame that future frames may depend on. |
| 146 | int cachedFrame = SkCodec::kNoFrame; |
| 147 | SkAutoMalloc cachedFramePixels; |
| 148 | |
| 149 | uint64_t totalDurationMillis = 0; |
| 150 | const int frameCount = codec->getFrameCount(); |
| 151 | if (frameCount == 0) { |
| 152 | SkDebugf("No frames.\n"); |
| 153 | return 1; |
| 154 | } |
| 155 | // The SkCodec::getFrameInfo comment says that this vector will be empty |
| 156 | // for still (not animated) images, even though frameCount should be 1. |
| 157 | std::vector<SkCodec::FrameInfo> frameInfos = codec->getFrameInfo(); |
| 158 | bool stillImage = frameInfos.empty(); |
| 159 | |
| 160 | for (int i = 0; i < frameCount; i++) { |
| 161 | SkCodec::Options opts; |
| 162 | opts.fFrameIndex = i; |
| 163 | |
| 164 | if (!stillImage) { |
| 165 | int durationMillis = frameInfos[i].fDuration; |
| 166 | if (durationMillis < 0) { |
| 167 | SkDebugf("Negative animation duration.\n"); |
| 168 | return 1; |
| 169 | } |
| 170 | totalDurationMillis += static_cast<uint64_t>(durationMillis); |
| 171 | if (totalDurationMillis > INT64_MAX) { |
| 172 | SkDebugf("Unsupported animation duration.\n"); |
| 173 | return 1; |
| 174 | } |
| 175 | |
| 176 | if ((cachedFrame != SkCodec::kNoFrame) && |
| 177 | (cachedFrame == frameInfos[i].fRequiredFrame) && cachedFramePixels.get()) { |
| 178 | opts.fPriorFrame = cachedFrame; |
| 179 | memcpy(bm.getPixels(), cachedFramePixels.get(), bmByteSize); |
| 180 | } |
| 181 | } |
| 182 | |
| 183 | if (!firstFrameOnly) { |
| 184 | if (i == 0) { |
| 185 | write_nix_header(0x41AFC36E, // "nïA" magic string as a u32le. |
| 186 | bm.width(), bm.height()); |
| 187 | } |
| 188 | |
| 189 | if (!write_nia_duration(totalDurationMillis)) { |
| 190 | SkDebugf("Unsupported animation duration.\n"); |
| 191 | return 1; |
| 192 | } |
| 193 | } |
| 194 | |
| 195 | const SkCodec::Result result = |
| 196 | codec->getPixels(codec->getInfo(), bm.getPixels(), bm.rowBytes(), &opts); |
| 197 | if ((result != SkCodec::kSuccess) && (result != SkCodec::kIncompleteInput)) { |
| 198 | SkDebugf("Decode frame pixels #%d failed.\n", i); |
| 199 | return 1; |
| 200 | } |
| 201 | |
| 202 | // If the next frame depends on this one, store it in cachedFrame. It |
| 203 | // is possible that we may discard a frame that future frames depend |
| 204 | // on, but the codec will simply redecode the discarded frame. |
| 205 | if ((static_cast<size_t>(i + 1) < frameInfos.size()) && |
| 206 | (frameInfos[i + 1].fRequiredFrame == i)) { |
| 207 | cachedFrame = i; |
| 208 | memcpy(cachedFramePixels.reset(bmByteSize), bm.getPixels(), bmByteSize); |
| 209 | } |
| 210 | |
| 211 | int width = bm.width(); |
| 212 | int height = bm.height(); |
| 213 | write_nix_header(0x45AFC36E, // "nïE" magic string as a u32le. |
| 214 | width, height); |
| 215 | write_nie_pixels(width, height, bm); |
| 216 | if (result == SkCodec::kIncompleteInput) { |
| 217 | SkDebugf("Incomplete input.\n"); |
| 218 | return 1; |
| 219 | } |
| 220 | if (firstFrameOnly) { |
| 221 | return 0; |
| 222 | } |
| 223 | write_nia_padding(width, height); |
| 224 | } |
| 225 | write_nia_footer(codec->getRepetitionCount(), stillImage); |
| 226 | return 0; |
| 227 | } |