Files
AnyPS5/core/Decoder/Png/tests/Png.cpp
T

397 lines
20 KiB
C++

#include "Decoder/Png.hpp"
#include <algorithm>
#include <cstdint>
#include <cstdlib>
#include <optional>
#include <stdexcept>
#include <vector>
#define STB_IMAGE_STATIC
#define STB_IMAGE_IMPLEMENTATION
#define STBI_ONLY_PNG
#define STBI_NO_STDIO
#include "stb_image.h"
static void Require(bool value) { if (!value) std::abort(); }
template<typename TFunction>
static bool ThrowsInvalidArgument(TFunction function) {
try {
function();
} catch (const std::invalid_argument&) {
return true;
}
return false;
}
static const std::uint8_t PALETTE_TRNS[] = {
0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, 0x00, 0x00, 0x00, 0x0D, 0x49, 0x48, 0x44, 0x52,
0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x02, 0x02, 0x03, 0x00, 0x00, 0x00, 0x0F, 0xD8, 0xE5,
0xB7, 0x00, 0x00, 0x00, 0x0C, 0x50, 0x4C, 0x54, 0x45, 0xFF, 0x00, 0x00, 0x00, 0xFF, 0x00, 0x00,
0x00, 0xFF, 0xFF, 0xFF, 0x00, 0xD6, 0x02, 0x8F, 0x7B, 0x00, 0x00, 0x00, 0x04, 0x74, 0x52, 0x4E,
0x53, 0xFF, 0x00, 0xFF, 0x80, 0x13, 0x0A, 0x1E, 0x39, 0x00, 0x00, 0x00, 0x0C, 0x49, 0x44, 0x41,
0x54, 0x78, 0x9C, 0x63, 0x10, 0x60, 0xD8, 0x00, 0x00, 0x00, 0xE4, 0x00, 0xC1, 0x27, 0xA8, 0xE8,
0x57, 0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4E, 0x44, 0xAE, 0x42, 0x60, 0x82,
};
static const std::uint8_t GRAY16[] = {
0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, 0x00, 0x00, 0x00, 0x0D, 0x49, 0x48, 0x44, 0x52,
0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x01, 0x10, 0x00, 0x00, 0x00, 0x00, 0x81, 0xD9, 0xFC,
0x15, 0x00, 0x00, 0x00, 0x0D, 0x49, 0x44, 0x41, 0x54, 0x78, 0x9C, 0x63, 0x10, 0x32, 0x59, 0x7D,
0x16, 0x00, 0x03, 0x0C, 0x01, 0xBF, 0x6E, 0xB9, 0xC6, 0x5D, 0x00, 0x00, 0x00, 0x00, 0x49, 0x45,
0x4E, 0x44, 0xAE, 0x42, 0x60, 0x82,
};
static const std::uint8_t GRAY1[] = {
0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, 0x00, 0x00, 0x00, 0x0D, 0x49, 0x48, 0x44, 0x52,
0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0xCB, 0x7B, 0xD2,
0xEE, 0x00, 0x00, 0x00, 0x0A, 0x49, 0x44, 0x41, 0x54, 0x78, 0x9C, 0x63, 0x88, 0x04, 0x00, 0x00,
0x5B, 0x00, 0x5A, 0x7C, 0xA5, 0x93, 0x54, 0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4E, 0x44, 0xAE,
0x42, 0x60, 0x82,
};
static const std::uint8_t GRAY1_TRNS[] = {
0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, 0x00, 0x00, 0x00, 0x0D, 0x49, 0x48, 0x44, 0x52,
0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0xDC, 0x59, 0x42,
0x27, 0x00, 0x00, 0x00, 0x02, 0x74, 0x52, 0x4E, 0x53, 0x00, 0x02, 0x98, 0x9D, 0xAC, 0x14, 0x00,
0x00, 0x00, 0x0A, 0x49, 0x44, 0x41, 0x54, 0x78, 0xDA, 0x63, 0x70, 0x00, 0x00, 0x00, 0x42, 0x00,
0x41, 0x84, 0xBF, 0x8E, 0x62, 0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4E, 0x44, 0xAE, 0x42, 0x60,
0x82,
};
static const std::uint8_t PNG_SIGNATURE[] = {0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A};
static void AppendBigEndian32(std::vector<std::uint8_t>& bytes, std::uint32_t value) {
for (int shift = 24; shift >= 0; shift -= 8) bytes.push_back(static_cast<std::uint8_t>(value >> shift));
}
static std::vector<std::uint8_t> HeaderOnly(std::uint32_t width, std::uint32_t height, std::uint8_t bitDepth,
std::uint8_t colorType, std::uint8_t interlace) {
std::vector<std::uint8_t> bytes{0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, 0, 0, 0, 13, 'I', 'H', 'D', 'R'};
AppendBigEndian32(bytes, width);
AppendBigEndian32(bytes, height);
bytes.insert(bytes.end(), {bitDepth, colorType, 0, 0, interlace, 0, 0, 0, 0});
bytes.insert(bytes.end(), {0, 0, 0, 0, 'I', 'E', 'N', 'D', 0, 0, 0, 0});
return bytes;
}
static std::uint32_t Crc32(std::span<const std::uint8_t> bytes) {
std::uint32_t crc = 0xFFFFFFFFu;
for (const std::uint8_t byte : bytes) {
crc ^= byte;
for (int bit = 0; bit < 8; ++bit) crc = (crc >> 1) ^ ((crc & 1) != 0 ? 0xEDB88320u : 0u);
}
return ~crc;
}
static std::uint32_t Adler32(std::span<const std::uint8_t> bytes) {
std::uint32_t low = 1;
std::uint32_t high = 0;
for (const std::uint8_t byte : bytes) {
low = (low + byte) % 65521;
high = (high + low) % 65521;
}
return high << 16 | low;
}
static void AppendTestChunk(std::vector<std::uint8_t>& png, const char* type, std::span<const std::uint8_t> data) {
AppendBigEndian32(png, static_cast<std::uint32_t>(data.size()));
const std::size_t start = png.size();
png.insert(png.end(), type, type + 4);
png.insert(png.end(), data.begin(), data.end());
AppendBigEndian32(png, Crc32(std::span<const std::uint8_t>(png.data() + start, png.size() - start)));
}
static std::vector<std::uint8_t> ZlibStore(std::span<const std::uint8_t> bytes) {
Require(bytes.size() <= 0xFFFF);
const std::uint16_t length = static_cast<std::uint16_t>(bytes.size());
const std::uint16_t inverse = static_cast<std::uint16_t>(~length);
std::vector<std::uint8_t> output{0x78, 0x01, 0x01, static_cast<std::uint8_t>(length), static_cast<std::uint8_t>(length >> 8),
static_cast<std::uint8_t>(inverse), static_cast<std::uint8_t>(inverse >> 8)};
output.insert(output.end(), bytes.begin(), bytes.end());
AppendBigEndian32(output, Adler32(bytes));
return output;
}
static std::vector<std::uint8_t> GrayLowBitPng(std::uint8_t bitDepth, std::span<const std::uint8_t> samples,
std::optional<std::uint16_t> transparency,
std::optional<std::uint16_t> precedingTransparency = std::nullopt) {
Require(bitDepth == 1 || bitDepth == 2 || bitDepth == 4);
std::vector<std::uint8_t> rows(1 + (samples.size() * bitDepth + 7) / 8, 0);
std::size_t bit = 0;
for (const std::uint8_t sample : samples) {
Require(sample < (1u << bitDepth));
rows[1 + bit / 8] |= static_cast<std::uint8_t>(sample << (8 - bitDepth - bit % 8));
bit += bitDepth;
}
std::vector<std::uint8_t> ihdr;
AppendBigEndian32(ihdr, static_cast<std::uint32_t>(samples.size()));
AppendBigEndian32(ihdr, 1);
ihdr.insert(ihdr.end(), {bitDepth, 0, 0, 0, 0});
std::vector<std::uint8_t> png(PNG_SIGNATURE, PNG_SIGNATURE + sizeof(PNG_SIGNATURE));
AppendTestChunk(png, "IHDR", ihdr);
if (precedingTransparency.has_value()) {
const std::uint8_t key[2] = {static_cast<std::uint8_t>(*precedingTransparency >> 8), static_cast<std::uint8_t>(*precedingTransparency)};
AppendTestChunk(png, "tRNS", key);
}
if (transparency.has_value()) {
const std::uint8_t key[2] = {static_cast<std::uint8_t>(*transparency >> 8), static_cast<std::uint8_t>(*transparency)};
AppendTestChunk(png, "tRNS", key);
}
AppendTestChunk(png, "IDAT", ZlibStore(rows));
AppendTestChunk(png, "IEND", std::span<const std::uint8_t>());
return png;
}
static std::uint8_t GrayScale(std::uint8_t bitDepth) {
if (bitDepth == 1) return 255;
if (bitDepth == 2) return 85;
return 17;
}
static void RequireGrayTransparency(std::uint8_t bitDepth, std::span<const std::uint8_t> samples, std::uint16_t key,
std::uint8_t transparentSample, std::optional<std::uint16_t> precedingKey = std::nullopt) {
const std::vector<std::uint8_t> png = GrayLowBitPng(bitDepth, samples, key, precedingKey);
const std::vector<std::uint8_t> original = png;
const auto header = Decoder::Png::ParseHeader(png);
Require(header.has_value());
Require(header->width == static_cast<std::uint32_t>(samples.size()) && header->height == 1 && header->bitDepth == bitDepth);
Require(header->colorType == Decoder::Png::ColorType::Grayscale && header->hasTransparency);
const auto image = Decoder::Png::Decode(png);
Require(image.has_value() && image->pixels.size() == samples.size() * 4);
const std::uint8_t scale = GrayScale(bitDepth);
for (std::size_t i = 0; i < samples.size(); ++i) {
const std::uint8_t gray = static_cast<std::uint8_t>(samples[i] * scale);
Require(image->pixels[i * 4] == gray && image->pixels[i * 4 + 1] == gray && image->pixels[i * 4 + 2] == gray);
Require(image->pixels[i * 4 + 3] == (samples[i] == transparentSample ? 0 : 255));
}
Require(png == original);
}
static void RequireGrayOpaque(std::uint8_t bitDepth, std::span<const std::uint8_t> samples) {
const std::vector<std::uint8_t> png = GrayLowBitPng(bitDepth, samples, std::nullopt);
const std::vector<std::uint8_t> original = png;
const auto header = Decoder::Png::ParseHeader(png);
Require(header.has_value() && !header->hasTransparency);
const auto image = Decoder::Png::Decode(png);
Require(image.has_value() && image->pixels.size() == samples.size() * 4);
const std::uint8_t scale = GrayScale(bitDepth);
for (std::size_t i = 0; i < samples.size(); ++i) {
const std::uint8_t gray = static_cast<std::uint8_t>(samples[i] * scale);
Require(image->pixels[i * 4] == gray && image->pixels[i * 4 + 1] == gray && image->pixels[i * 4 + 2] == gray);
Require(image->pixels[i * 4 + 3] == 255);
}
Require(png == original);
}
static void RequireTruncatedTransparency() {
const std::uint8_t samples[] = {0, 1};
const std::vector<std::uint8_t> png = GrayLowBitPng(1, samples, 0x0002);
const std::size_t transparencyOffset = sizeof(PNG_SIGNATURE) + 25;
const std::vector<std::uint8_t> truncated(png.begin(), png.begin() + transparencyOffset + 9);
const std::vector<std::uint8_t> original = truncated;
const auto header = Decoder::Png::ParseHeader(truncated);
Require(header.has_value() && !header->hasTransparency);
(void)Decoder::Png::Decode(truncated);
Require(truncated == original);
}
static void RequireRoundTrip(std::uint32_t channels, Decoder::Png::ColorType colorType) {
constexpr std::uint32_t width = 20;
constexpr std::uint32_t height = 10;
std::vector<std::uint8_t> pixels(width * height * channels);
for (std::size_t i = 0; i < pixels.size(); ++i) pixels[i] = static_cast<std::uint8_t>(i * 37 + 11);
const std::vector<std::uint8_t> png = Decoder::Png::Encode(pixels, width, height, channels);
const auto header = Decoder::Png::ParseHeader(png);
Require(header.has_value());
Require(header->width == width && header->height == height && header->bitDepth == 8);
Require(header->colorType == colorType && !header->interlaced && !header->hasTransparency);
const auto image = Decoder::Png::Decode(png);
Require(image.has_value());
Require(image->width == width && image->height == height && image->pixels.size() == width * height * 4);
for (std::size_t pixel = 0; pixel < width * height; ++pixel) {
const std::uint8_t* in = &pixels[pixel * channels];
const std::uint8_t* out = &image->pixels[pixel * 4];
const bool gray = channels <= 2;
Require(out[0] == in[0]);
Require(out[1] == (gray ? in[0] : in[1]));
Require(out[2] == (gray ? in[0] : in[2]));
const bool hasAlpha = channels == 2 || channels == 4;
Require(out[3] == (hasAlpha ? in[channels - 1] : 255));
}
}
static std::vector<std::uint8_t> RowFilters(const std::vector<std::uint8_t>& png, std::uint32_t height, std::uint32_t rowSize) {
std::vector<std::uint8_t> stream;
for (std::size_t offset = 8; offset + 12 <= png.size();) {
const std::size_t length = static_cast<std::size_t>(png[offset]) << 24 | static_cast<std::size_t>(png[offset + 1]) << 16
| static_cast<std::size_t>(png[offset + 2]) << 8 | png[offset + 3];
Require(length <= png.size() - offset - 12);
const std::uint8_t* data = png.data() + offset + 8;
if (std::equal(data - 4, data, "IDAT")) stream.insert(stream.end(), data, data + length);
offset += 12 + length;
}
int size = 0;
char* rows = stbi_zlib_decode_malloc(reinterpret_cast<const char*>(stream.data()), static_cast<int>(stream.size()), &size);
Require(rows && static_cast<std::size_t>(size) == static_cast<std::size_t>(height) * (rowSize + 1));
std::vector<std::uint8_t> filters;
for (std::uint32_t y = 0; y < height; ++y) filters.push_back(static_cast<std::uint8_t>(rows[y * (rowSize + 1)]));
stbi_image_free(rows);
return filters;
}
static void RequireFilterSets(std::uint32_t channels) {
constexpr std::uint32_t width = 16;
constexpr std::uint32_t height = 6;
const std::uint32_t rowSize = width * channels;
std::vector<std::uint8_t> pixels(rowSize * height);
std::uint32_t noise = 12345;
for (std::uint32_t y = 0; y < height; ++y) {
for (std::uint32_t i = 0; i < rowSize; ++i) {
noise = noise * 1103515245u + 12345u;
pixels[y * rowSize + i] = y == 2 ? static_cast<std::uint8_t>(noise >> 16) : static_cast<std::uint8_t>(i / channels * 8);
}
}
std::copy_n(pixels.begin() + 2 * rowSize, rowSize, pixels.begin() + 3 * rowSize);
for (std::uint8_t filters = 1; filters <= Decoder::Png::FILTER_ALL; ++filters) {
const std::vector<std::uint8_t> png = Decoder::Png::Encode(pixels, width, height, channels, {6, filters});
for (const std::uint8_t filter : RowFilters(png, height, rowSize)) Require(filter < 5 && (filters >> filter & 1) != 0);
const auto image = Decoder::Png::Decode(png);
Require(image.has_value() && image->width == width && image->height == height);
for (std::size_t pixel = 0; pixel < width * height; ++pixel) {
Require(image->pixels[pixel * 4] == pixels[pixel * channels]);
Require(image->pixels[pixel * 4 + 3] == (channels % 2 == 0 ? pixels[pixel * channels + channels - 1] : 255));
}
}
const std::vector<std::uint8_t> subOrUp = RowFilters(
Decoder::Png::Encode(pixels, width, height, channels, {6, Decoder::Png::FILTER_SUB | Decoder::Png::FILTER_UP}), height, rowSize);
Require(subOrUp[0] == 1 && subOrUp[1] == 2 && subOrUp[3] == 2 && subOrUp[4] == 1 && subOrUp[5] == 2);
}
int main() {
RequireRoundTrip(1, Decoder::Png::ColorType::Grayscale);
RequireRoundTrip(2, Decoder::Png::ColorType::GrayscaleAlpha);
RequireRoundTrip(3, Decoder::Png::ColorType::Rgb);
RequireRoundTrip(4, Decoder::Png::ColorType::Rgba);
for (const std::uint32_t channels : {1, 2, 3, 4}) RequireFilterSets(channels);
const auto paletteHeader = Decoder::Png::ParseHeader(PALETTE_TRNS);
Require(paletteHeader.has_value());
Require(paletteHeader->width == 2 && paletteHeader->height == 2 && paletteHeader->bitDepth == 2);
Require(paletteHeader->colorType == Decoder::Png::ColorType::Palette && paletteHeader->hasTransparency);
for (const std::size_t size : {std::size_t{69}, std::size_t{71}}) {
const auto truncated = Decoder::Png::ParseHeader(std::span<const std::uint8_t>(PALETTE_TRNS, size));
Require(truncated.has_value() && !truncated->hasTransparency);
}
const auto completeTransparency = Decoder::Png::ParseHeader(std::span<const std::uint8_t>(PALETTE_TRNS, 73));
Require(completeTransparency.has_value() && completeTransparency->hasTransparency);
const auto palette = Decoder::Png::Decode(PALETTE_TRNS);
Require(palette.has_value());
Require(palette->pixels == std::vector<std::uint8_t>({255, 0, 0, 255, 0, 255, 0, 0, 0, 0, 255, 255, 255, 255, 0, 128}));
const auto gray16Header = Decoder::Png::ParseHeader(GRAY16);
Require(gray16Header.has_value() && gray16Header->bitDepth == 16 && gray16Header->colorType == Decoder::Png::ColorType::Grayscale);
const auto gray16 = Decoder::Png::Decode(GRAY16);
Require(gray16.has_value());
Require(gray16->pixels == std::vector<std::uint8_t>({0x12, 0x12, 0x12, 255, 0xAB, 0xAB, 0xAB, 255}));
const auto gray1Header = Decoder::Png::ParseHeader(GRAY1);
Require(gray1Header.has_value() && gray1Header->bitDepth == 1 && gray1Header->width == 8);
const auto gray1 = Decoder::Png::Decode(GRAY1);
Require(gray1.has_value());
const std::uint8_t levels[8] = {0, 255, 0, 255, 255, 0, 0, 255};
for (int i = 0; i < 8; ++i) Require(gray1->pixels[i * 4] == levels[i] && gray1->pixels[i * 4 + 3] == 255);
const auto gray1TrnsHeader = Decoder::Png::ParseHeader(GRAY1_TRNS);
Require(gray1TrnsHeader.has_value());
Require(gray1TrnsHeader->width == 2 && gray1TrnsHeader->height == 1 && gray1TrnsHeader->bitDepth == 1);
Require(gray1TrnsHeader->colorType == Decoder::Png::ColorType::Grayscale && gray1TrnsHeader->hasTransparency);
const auto gray1Trns = Decoder::Png::Decode(GRAY1_TRNS);
Require(gray1Trns.has_value());
Require(gray1Trns->pixels == std::vector<std::uint8_t>({0, 0, 0, 0, 255, 255, 255, 255}));
const std::uint8_t gray1Samples[] = {0, 1, 0, 1, 1, 0, 0, 1};
RequireGrayTransparency(1, gray1Samples, 0x0000, 0);
RequireGrayTransparency(1, gray1Samples, 0x0002, 0);
RequireGrayTransparency(1, gray1Samples, 0x0100, 0);
RequireGrayTransparency(1, gray1Samples, 0xFF02, 0);
RequireGrayTransparency(1, gray1Samples, 0xFFFF, 1);
RequireGrayTransparency(1, gray1Samples, 0x0001, 1, 0x0002);
RequireGrayOpaque(1, gray1Samples);
const std::uint8_t gray2Samples[] = {0, 1, 2, 3};
RequireGrayTransparency(2, gray2Samples, 0x0001, 1);
RequireGrayTransparency(2, gray2Samples, 0x0005, 1);
RequireGrayTransparency(2, gray2Samples, 0x0100, 0);
RequireGrayTransparency(2, gray2Samples, 0x0101, 1);
RequireGrayTransparency(2, gray2Samples, 0xFF05, 1);
RequireGrayTransparency(2, gray2Samples, 0xFFFF, 3);
RequireGrayOpaque(2, gray2Samples);
const std::uint8_t gray4Samples[] = {0, 3, 10, 15, 1};
RequireGrayTransparency(4, gray4Samples, 0x0003, 3);
RequireGrayTransparency(4, gray4Samples, 0x0013, 3);
RequireGrayTransparency(4, gray4Samples, 0x0100, 0);
RequireGrayTransparency(4, gray4Samples, 0x0103, 3);
RequireGrayTransparency(4, gray4Samples, 0xFF13, 3);
RequireGrayTransparency(4, gray4Samples, 0xFFFF, 15);
RequireGrayOpaque(4, gray4Samples);
RequireTruncatedTransparency();
const auto interlaced = Decoder::Png::ParseHeader(HeaderOnly(3, 4, 8, 6, 1));
Require(interlaced.has_value() && interlaced->interlaced && !interlaced->hasTransparency);
Require(!Decoder::Png::ParseHeader(HeaderOnly(0, 4, 8, 6, 0)).has_value());
Require(!Decoder::Png::ParseHeader(HeaderOnly(3, 4, 8, 5, 0)).has_value());
Require(!Decoder::Png::ParseHeader(HeaderOnly(3, 4, 16, 3, 0)).has_value());
Require(!Decoder::Png::ParseHeader(HeaderOnly(3, 4, 8, 6, 2)).has_value());
std::vector<std::uint8_t> badSignature = HeaderOnly(3, 4, 8, 6, 0);
badSignature[1] = 'X';
Require(!Decoder::Png::ParseHeader(badSignature).has_value());
std::vector<std::uint8_t> hugeChunk = HeaderOnly(3, 4, 8, 6, 0);
hugeChunk[33] = 0xFF;
hugeChunk[37] = 't';
hugeChunk[38] = 'E';
hugeChunk[39] = 'X';
hugeChunk[40] = 't';
const auto hugeChunkHeader = Decoder::Png::ParseHeader(hugeChunk);
Require(hugeChunkHeader.has_value() && !hugeChunkHeader->hasTransparency);
Require(!Decoder::Png::ParseHeader(std::span<const std::uint8_t>(PALETTE_TRNS, 32)).has_value());
Require(!Decoder::Png::Decode({}).has_value());
Require(!Decoder::Png::Decode(std::span<const std::uint8_t>(PALETTE_TRNS, 60)).has_value());
const std::vector<std::uint8_t> garbage{1, 2, 3, 4, 5, 6, 7, 8};
Require(!Decoder::Png::Decode(garbage).has_value());
const std::vector<std::uint8_t> pixels(16 * 4);
Require(ThrowsInvalidArgument([&] { Decoder::Png::Encode(pixels, 4, 4, 0); }));
Require(ThrowsInvalidArgument([&] { Decoder::Png::Encode(pixels, 4, 4, 5); }));
Require(ThrowsInvalidArgument([&] { Decoder::Png::Encode(pixels, 0, 4, 4); }));
Require(ThrowsInvalidArgument([&] { Decoder::Png::Encode(pixels, 4, 5, 4); }));
Require(ThrowsInvalidArgument([&] { Decoder::Png::Encode(pixels, 4, 4, 4, {10, Decoder::Png::FILTER_ALL}); }));
Require(ThrowsInvalidArgument([&] { Decoder::Png::Encode(pixels, 4, 4, 4, {8, 0}); }));
Require(ThrowsInvalidArgument([&] { Decoder::Png::Encode(pixels, 4, 4, 4, {8, Decoder::Png::FILTER_ALL + 1}); }));
std::vector<std::uint8_t> gradient(64 * 64 * 3);
for (std::size_t i = 0; i < gradient.size(); ++i) gradient[i] = static_cast<std::uint8_t>(i / 3 % 64 * 4);
const auto stored = Decoder::Png::Encode(gradient, 64, 64, 3, {0, Decoder::Png::FILTER_NONE});
const auto compressed = Decoder::Png::Encode(gradient, 64, 64, 3, {9, Decoder::Png::FILTER_SUB});
Require(compressed.size() < stored.size());
for (const auto& png : {stored, compressed}) {
const auto image = Decoder::Png::Decode(png);
Require(image.has_value() && image->width == 64 && image->height == 64);
for (std::size_t pixel = 0; pixel < 64 * 64; ++pixel) Require(image->pixels[pixel * 4] == gradient[pixel * 3]);
}
return 0;
}