diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 9cfd340be..c44860c63 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -155,6 +155,7 @@ add_executable(odr_test "src/internal/pdf/pdf_writer.cpp" "src/internal/png/png_util_test.cpp" + "src/internal/png/png_test_util.cpp" "src/internal/font/cff_font.cpp" "src/internal/font/type1_charstring.cpp" diff --git a/test/src/internal/pdf/pdf_image.cpp b/test/src/internal/pdf/pdf_image.cpp index 474f99837..0bf5e48a1 100644 --- a/test/src/internal/pdf/pdf_image.cpp +++ b/test/src/internal/pdf/pdf_image.cpp @@ -1,8 +1,7 @@ #include -#include +#include #include -#include #include #include @@ -13,143 +12,12 @@ #include using namespace odr::internal::pdf; +using odr::test::png::bytes; +using odr::test::png::decode_png; +using odr::test::png::DecodedPng; namespace { -std::uint32_t be32(const std::string &data, const std::size_t offset) { - return (static_cast(static_cast(data[offset])) - << 24) | - (static_cast( - static_cast(data[offset + 1])) - << 16) | - (static_cast( - static_cast(data[offset + 2])) - << 8) | - static_cast( - static_cast(data[offset + 3])); -} - -/// Minimal PNG reader for the encoder's output: walks the chunks, inflates the -/// concatenated IDAT and strips the per-row filter byte (the encoder only emits -/// filter type 0), yielding the raw 8-bit RGB pixels. A palette image is looked -/// up into RGB. -struct DecodedPng { - std::int32_t width{0}; - std::int32_t height{0}; - std::int32_t bit_depth{0}; - std::int32_t colour_type{0}; - std::string rgb; -}; - -DecodedPng decode_png(const std::string &png) { - EXPECT_GE(png.size(), 8u); - const std::string signature = { - static_cast(0x89), 'P', 'N', 'G', '\r', '\n', - static_cast(0x1A), '\n'}; - EXPECT_EQ(png.substr(0, 8), signature); - - DecodedPng result; - std::string idat; - std::string palette; - std::size_t p = 8; - while (p + 12 <= png.size()) { - const std::uint32_t length = be32(png, p); - const std::string type = png.substr(p + 4, 4); - const std::string data = png.substr(p + 8, length); - if (type == "IHDR") { - result.width = static_cast(be32(data, 0)); - result.height = static_cast(be32(data, 4)); - result.bit_depth = static_cast(data[8]); - result.colour_type = static_cast(data[9]); - } else if (type == "PLTE") { - palette = data; - } else if (type == "IDAT") { - idat += data; - } else if (type == "IEND") { - break; - } - p += 12 + length; - } - - const bool indexed = result.colour_type == 3; - if (indexed) { - EXPECT_FALSE(palette.empty()); - } else { - EXPECT_EQ(result.bit_depth, 8); - EXPECT_EQ(result.colour_type, 2); // RGB - } - const std::string raw = odr::internal::crypto::util::zlib_inflate(idat); - const std::size_t stride = - indexed - ? (static_cast(result.width) * result.bit_depth + 7) / 8 - : static_cast(result.width) * 3; - for (std::int32_t y = 0; y < result.height; ++y) { - const std::size_t row = static_cast(y) * (stride + 1); - EXPECT_EQ(static_cast(raw[row]), 0); // filter type None - if (!indexed) { - result.rgb.append(raw, row + 1, stride); - continue; - } - for (std::int32_t x = 0; x < result.width; ++x) { - const std::size_t bit = static_cast(x) * result.bit_depth; - const auto byte = static_cast(raw[row + 1 + bit / 8]); - const std::size_t index = (byte >> (8 - result.bit_depth - bit % 8)) & - ((1u << result.bit_depth) - 1); - result.rgb.append(palette, index * 3, 3); - } - } - return result; -} - -std::string rgb_pixel(const std::string &rgb, const std::int32_t width, - const std::int32_t x, const std::int32_t y) { - return rgb.substr((static_cast(y) * width + x) * 3, 3); -} - -/// Like `decode_png` but for the RGBA encoder output (colour type 6): keeps the -/// alpha channel, yielding 4 bytes per pixel. -struct DecodedPngRgba { - std::int32_t width{0}; - std::int32_t height{0}; - std::string rgba; -}; - -DecodedPngRgba decode_png_rgba(const std::string &png) { - EXPECT_GE(png.size(), 8u); - DecodedPngRgba result; - std::string idat; - std::size_t p = 8; - while (p + 12 <= png.size()) { - const std::uint32_t length = be32(png, p); - const std::string type = png.substr(p + 4, 4); - const std::string data = png.substr(p + 8, length); - if (type == "IHDR") { - result.width = static_cast(be32(data, 0)); - result.height = static_cast(be32(data, 4)); - EXPECT_EQ(static_cast(data[8]), 8); // bit depth - EXPECT_EQ(static_cast(data[9]), 6); // colour type RGBA - } else if (type == "IDAT") { - idat += data; - } else if (type == "IEND") { - break; - } - p += 12 + length; - } - const std::string raw = odr::internal::crypto::util::zlib_inflate(idat); - const auto stride = static_cast(result.width) * 4; - for (std::int32_t y = 0; y < result.height; ++y) { - const std::size_t row = static_cast(y) * (stride + 1); - EXPECT_EQ(static_cast(raw[row]), 0); // filter type None - result.rgba.append(raw, row + 1, stride); - } - return result; -} - -std::string rgba_pixel(const std::string &rgba, const std::int32_t width, - const std::int32_t x, const std::int32_t y) { - return rgba.substr((static_cast(y) * width + x) * 4, 4); -} - ColorSpaceDef device_rgb() { ColorSpaceDef def; def.kind = ColorSpaceKind::device_rgb; @@ -164,14 +32,6 @@ ColorSpaceDef device_gray() { return def; } -std::string bytes(std::initializer_list values) { - std::string result; - for (const int v : values) { - result.push_back(static_cast(v)); - } - return result; -} - } // namespace TEST(PdfImage, encode_rgb_8bpc) { @@ -179,17 +39,17 @@ TEST(PdfImage, encode_rgb_8bpc) { bytes({10, 20, 30, 40, 50, 60, 70, 80, 90, 100, 110, 120}); const DecodedPng png = decode_png(encode_image_png(samples, 2, 2, 8, device_rgb(), {})); - EXPECT_EQ(png.rgb, samples); // identity for DeviceRGB 8bpc + EXPECT_EQ(png.pixels, samples); // identity for DeviceRGB 8bpc } TEST(PdfImage, encode_gray_8bpc_expands_to_rgb) { const std::string samples = bytes({0, 128, 200, 255}); const DecodedPng png = decode_png(encode_image_png(samples, 2, 2, 8, device_gray(), {})); - EXPECT_EQ(rgb_pixel(png.rgb, 2, 0, 0), bytes({0, 0, 0})); - EXPECT_EQ(rgb_pixel(png.rgb, 2, 1, 0), bytes({128, 128, 128})); - EXPECT_EQ(rgb_pixel(png.rgb, 2, 0, 1), bytes({200, 200, 200})); - EXPECT_EQ(rgb_pixel(png.rgb, 2, 1, 1), bytes({255, 255, 255})); + EXPECT_EQ(png.pixel(0, 0), bytes({0, 0, 0})); + EXPECT_EQ(png.pixel(1, 0), bytes({128, 128, 128})); + EXPECT_EQ(png.pixel(0, 1), bytes({200, 200, 200})); + EXPECT_EQ(png.pixel(1, 1), bytes({255, 255, 255})); } TEST(PdfImage, encode_indexed_2x2) { @@ -205,10 +65,10 @@ TEST(PdfImage, encode_indexed_2x2) { const std::string samples = bytes({0, 1, 1, 0}); const DecodedPng png = decode_png(encode_image_png(samples, 2, 2, 8, indexed, {})); - EXPECT_EQ(rgb_pixel(png.rgb, 2, 0, 0), bytes({255, 0, 0})); - EXPECT_EQ(rgb_pixel(png.rgb, 2, 1, 0), bytes({0, 255, 0})); - EXPECT_EQ(rgb_pixel(png.rgb, 2, 0, 1), bytes({0, 255, 0})); - EXPECT_EQ(rgb_pixel(png.rgb, 2, 1, 1), bytes({255, 0, 0})); + EXPECT_EQ(png.pixel(0, 0), bytes({255, 0, 0})); + EXPECT_EQ(png.pixel(1, 0), bytes({0, 255, 0})); + EXPECT_EQ(png.pixel(0, 1), bytes({0, 255, 0})); + EXPECT_EQ(png.pixel(1, 1), bytes({255, 0, 0})); } TEST(PdfImage, encode_indexed_1bpc_packs_and_pads_rows) { @@ -224,9 +84,9 @@ TEST(PdfImage, encode_indexed_1bpc_packs_and_pads_rows) { const std::string samples = bytes({0b10100000}); const DecodedPng png = decode_png(encode_image_png(samples, 3, 1, 1, indexed, {})); - EXPECT_EQ(rgb_pixel(png.rgb, 3, 0, 0), bytes({255, 255, 255})); - EXPECT_EQ(rgb_pixel(png.rgb, 3, 1, 0), bytes({0, 0, 0})); - EXPECT_EQ(rgb_pixel(png.rgb, 3, 2, 0), bytes({255, 255, 255})); + EXPECT_EQ(png.pixel(0, 0), bytes({255, 255, 255})); + EXPECT_EQ(png.pixel(1, 0), bytes({0, 0, 0})); + EXPECT_EQ(png.pixel(2, 0), bytes({255, 255, 255})); EXPECT_EQ(png.bit_depth, 1); } @@ -238,18 +98,18 @@ TEST(PdfImage, encode_gray_1bpc_as_a_palette) { decode_png(encode_image_png(samples, 2, 2, 1, device_gray(), decode)); EXPECT_EQ(png.colour_type, 3); EXPECT_EQ(png.bit_depth, 1); - EXPECT_EQ(rgb_pixel(png.rgb, 2, 0, 0), bytes({255, 255, 255})); - EXPECT_EQ(rgb_pixel(png.rgb, 2, 1, 0), bytes({0, 0, 0})); - EXPECT_EQ(rgb_pixel(png.rgb, 2, 0, 1), bytes({0, 0, 0})); - EXPECT_EQ(rgb_pixel(png.rgb, 2, 1, 1), bytes({255, 255, 255})); + EXPECT_EQ(png.pixel(0, 0), bytes({255, 255, 255})); + EXPECT_EQ(png.pixel(1, 0), bytes({0, 0, 0})); + EXPECT_EQ(png.pixel(0, 1), bytes({0, 0, 0})); + EXPECT_EQ(png.pixel(1, 1), bytes({255, 255, 255})); } // Rows the samples do not reach read as zero, as on the 8-bit path. TEST(PdfImage, encode_gray_1bpc_pads_short_samples) { const DecodedPng png = decode_png(encode_image_png(bytes({0xff}), 8, 2, 1, device_gray(), {})); - EXPECT_EQ(rgb_pixel(png.rgb, 8, 0, 0), bytes({255, 255, 255})); - EXPECT_EQ(rgb_pixel(png.rgb, 8, 0, 1), bytes({0, 0, 0})); + EXPECT_EQ(png.pixel(0, 0), bytes({255, 255, 255})); + EXPECT_EQ(png.pixel(0, 1), bytes({0, 0, 0})); } TEST(PdfImage, encode_gray_4bpc) { @@ -257,8 +117,8 @@ TEST(PdfImage, encode_gray_4bpc) { const std::string samples = bytes({0x0F}); const DecodedPng png = decode_png(encode_image_png(samples, 2, 1, 4, device_gray(), {})); - EXPECT_EQ(rgb_pixel(png.rgb, 2, 0, 0), bytes({0, 0, 0})); - EXPECT_EQ(rgb_pixel(png.rgb, 2, 1, 0), bytes({255, 255, 255})); + EXPECT_EQ(png.pixel(0, 0), bytes({0, 0, 0})); + EXPECT_EQ(png.pixel(1, 0), bytes({255, 255, 255})); } TEST(PdfImage, encode_honours_decode_array) { @@ -267,8 +127,8 @@ TEST(PdfImage, encode_honours_decode_array) { const std::array decode = {1.0, 0.0}; const DecodedPng png = decode_png(encode_image_png(samples, 2, 1, 8, device_gray(), decode)); - EXPECT_EQ(rgb_pixel(png.rgb, 2, 0, 0), bytes({255, 255, 255})); - EXPECT_EQ(rgb_pixel(png.rgb, 2, 1, 0), bytes({0, 0, 0})); + EXPECT_EQ(png.pixel(0, 0), bytes({255, 255, 255})); + EXPECT_EQ(png.pixel(1, 0), bytes({0, 0, 0})); } TEST(PdfImage, encode_rejects_bad_parameters) { @@ -279,54 +139,44 @@ TEST(PdfImage, encode_rejects_bad_parameters) { EXPECT_TRUE(encode_image_png("", 1, 1, 8, zero, {}).empty()); } -TEST(PdfImage, png_rgba_round_trip) { - // 2x1: opaque red, half-transparent green. - const std::string rgba = bytes({255, 0, 0, 255, 0, 255, 0, 128}); - const DecodedPngRgba png = - decode_png_rgba(odr::internal::png::write(rgba, 2, 1, 4)); - EXPECT_EQ(png.width, 2); - EXPECT_EQ(png.height, 1); - EXPECT_EQ(png.rgba, rgba); -} - TEST(PdfImage, encode_with_alpha_plane_emits_rgba) { // DeviceGray 2x1, samples black/white, alpha plane opaque/transparent. const std::string samples = bytes({0, 255}); const std::vector alpha = {255, 0}; - const DecodedPngRgba png = decode_png_rgba( - encode_image_png(samples, 2, 1, 8, device_gray(), {}, alpha)); - EXPECT_EQ(rgba_pixel(png.rgba, 2, 0, 0), bytes({0, 0, 0, 255})); - EXPECT_EQ(rgba_pixel(png.rgba, 2, 1, 0), bytes({255, 255, 255, 0})); + const DecodedPng png = + decode_png(encode_image_png(samples, 2, 1, 8, device_gray(), {}, alpha)); + EXPECT_EQ(png.pixel(0, 0), bytes({0, 0, 0, 255})); + EXPECT_EQ(png.pixel(1, 0), bytes({255, 255, 255, 0})); } TEST(PdfImage, encode_with_colour_key_masks_matching_pixels) { // DeviceRGB 2x1: pure red is keyed out, the other pixel stays opaque. const std::string samples = bytes({255, 0, 0, 10, 20, 30}); const std::vector color_key = {255, 255, 0, 0, 0, 0}; - const DecodedPngRgba png = decode_png_rgba( + const DecodedPng png = decode_png( encode_image_png(samples, 2, 1, 8, device_rgb(), {}, {}, color_key)); - EXPECT_EQ(rgba_pixel(png.rgba, 2, 0, 0), bytes({255, 0, 0, 0})); - EXPECT_EQ(rgba_pixel(png.rgba, 2, 1, 0), bytes({10, 20, 30, 255})); + EXPECT_EQ(png.pixel(0, 0), bytes({255, 0, 0, 0})); + EXPECT_EQ(png.pixel(1, 0), bytes({10, 20, 30, 255})); } TEST(PdfImage, encode_stencil_paints_fill_colour_through_mask) { // 1 bpc, 2x1: bits 0,1 -> 0b01000000 (row padded to a byte). Default /Decode // [0 1]: a 0 paints the fill colour, a 1 is transparent. const std::string samples = bytes({0b01000000}); - const DecodedPngRgba png = - decode_png_rgba(encode_stencil_png(samples, 2, 1, {1.0, 0.0, 0.0}, {})); - EXPECT_EQ(rgba_pixel(png.rgba, 2, 0, 0), bytes({255, 0, 0, 255})); - EXPECT_EQ(static_cast(rgba_pixel(png.rgba, 2, 1, 0)[3]), 0); + const DecodedPng png = + decode_png(encode_stencil_png(samples, 2, 1, {1.0, 0.0, 0.0}, {})); + EXPECT_EQ(png.pixel(0, 0), bytes({255, 0, 0, 255})); + EXPECT_EQ(static_cast(png.pixel(1, 0)[3]), 0); } TEST(PdfImage, encode_stencil_decode_inverts) { // /Decode [1 0] swaps which sample paints: now the 1 paints, the 0 is clear. const std::string samples = bytes({0b01000000}); const std::array decode = {1.0, 0.0}; - const DecodedPngRgba png = decode_png_rgba( - encode_stencil_png(samples, 2, 1, {0.0, 0.0, 1.0}, decode)); - EXPECT_EQ(static_cast(rgba_pixel(png.rgba, 2, 0, 0)[3]), 0); - EXPECT_EQ(rgba_pixel(png.rgba, 2, 1, 0), bytes({0, 0, 255, 255})); + const DecodedPng png = + decode_png(encode_stencil_png(samples, 2, 1, {0.0, 0.0, 1.0}, decode)); + EXPECT_EQ(static_cast(png.pixel(0, 0)[3]), 0); + EXPECT_EQ(png.pixel(1, 0), bytes({0, 0, 255, 255})); } TEST(PdfImage, decode_mask_alpha_soft_mask_grey_to_alpha) { diff --git a/test/src/internal/png/png_test_util.cpp b/test/src/internal/png/png_test_util.cpp new file mode 100644 index 000000000..2774129f2 --- /dev/null +++ b/test/src/internal/png/png_test_util.cpp @@ -0,0 +1,119 @@ +#include + +#include + +#include + +namespace odr::test { + +namespace { + +void require(const bool valid, const char *message) { + if (!valid) { + throw std::runtime_error(message); + } +} + +std::uint32_t be32(const std::string_view data, const std::size_t offset) { + require(offset <= data.size() && data.size() - offset >= 4, + "truncated PNG integer"); + std::uint32_t value = 0; + for (const char byte : data.substr(offset, 4)) { + value = (value << 8) | static_cast(byte); + } + return value; +} + +} // namespace + +std::string png::bytes(const std::initializer_list values) { + return {values.begin(), values.end()}; +} + +std::string png::DecodedPng::pixel(const std::uint32_t x, + const std::uint32_t y) const { + require(x < width && y < height, "PNG pixel outside image"); + const std::size_t channels = colour_type == 6 ? 4 : 3; + return pixels.substr((static_cast(y) * width + x) * channels, + channels); +} + +png::DecodedPng png::decode_png(const std::string_view data) { + require(data.starts_with("\x89PNG\r\n\x1a\n"), "invalid PNG signature"); + DecodedPng result; + std::string idat; + std::string_view palette; + std::size_t at = 8; + while (true) { + require(at <= data.size() && data.size() - at >= 12, "truncated PNG chunk"); + const std::size_t length = be32(data, at); + require(length <= data.size() - at - 12 && length <= INT32_MAX, + "invalid PNG chunk length"); + const auto type = data.substr(at + 4, 4); + const auto payload = data.substr(at + 8, length); + require(be32(data, at + 8 + length) == + internal::crypto::util::crc32(data.substr(at + 4, length + 4)), + "invalid PNG chunk CRC"); + require(result.width != 0 || type == "IHDR", "missing PNG header"); + if (type == "IHDR") { + require(at == 8 && length == 13, "invalid PNG header"); + result.width = be32(payload, 0); + result.height = be32(payload, 4); + result.bit_depth = static_cast(payload[8]); + result.colour_type = static_cast(payload[9]); + require(result.width != 0 && result.width <= INT32_MAX && + result.height != 0 && result.height <= INT32_MAX, + "invalid PNG dimensions"); + const bool indexed = result.colour_type == 3; + require(indexed ? (result.bit_depth == 1 || result.bit_depth == 2 || + result.bit_depth == 4 || result.bit_depth == 8) + : ((result.colour_type == 2 || result.colour_type == 6) && + result.bit_depth == 8), + "unsupported PNG pixel format"); + require(payload[10] == 0 && payload[11] == 0 && payload[12] == 0, + "unsupported PNG compression, filtering or interlace"); + } else if (type == "PLTE") { + require(length != 0 && length <= 768 && length % 3 == 0, + "invalid PNG palette"); + palette = payload; + } else if (type == "IDAT") { + ++result.idat_chunks; + idat += payload; + } else if (type == "IEND") { + require(length == 0 && at + 12 == data.size(), "invalid PNG end chunk"); + break; + } + at += length + 12; + } + + const bool indexed = result.colour_type == 3; + const std::uint64_t channels = result.colour_type == 6 ? 4 : 3; + const std::uint64_t bits = indexed ? result.bit_depth : channels * 8; + const std::uint64_t stride = (result.width * bits + 7) / 8; + const std::string raw = internal::crypto::util::zlib_inflate(idat); + require(result.height <= raw.size() / (stride + 1) && + (stride + 1) * result.height == raw.size(), + "invalid PNG pixel data length"); + const std::uint64_t pixel_bytes = result.width * channels * result.height; + require(pixel_bytes <= result.pixels.max_size(), "PNG pixel data too large"); + result.pixels.reserve(static_cast(pixel_bytes)); + for (std::uint32_t y = 0; y < result.height; ++y) { + const auto row = static_cast(y * (stride + 1)); + require(raw[row] == 0, "unsupported PNG row filter"); + if (!indexed) { + result.pixels.append(raw, row + 1, static_cast(stride)); + continue; + } + for (std::uint32_t x = 0; x < result.width; ++x) { + const std::uint64_t bit = x * bits; + const auto byte = static_cast(raw[row + 1 + bit / 8]); + const std::size_t index = + (byte >> (8 - bits - bit % 8)) & ((1u << bits) - 1); + require(index < palette.size() / 3, "PNG palette index out of bounds"); + result.pixels.append(palette.substr(index * 3, 3)); + } + } + return result; +} + +} // namespace odr::test diff --git a/test/src/internal/png/png_test_util.hpp b/test/src/internal/png/png_test_util.hpp new file mode 100644 index 000000000..6d6aa6da6 --- /dev/null +++ b/test/src/internal/png/png_test_util.hpp @@ -0,0 +1,27 @@ +#pragma once + +#include +#include +#include +#include +#include + +namespace odr::test::png { + +struct DecodedPng { + std::uint32_t width{}; + std::uint32_t height{}; + std::uint8_t bit_depth{}; + std::uint8_t colour_type{}; + std::size_t idat_chunks{}; + std::string pixels; + + [[nodiscard]] std::string pixel(std::uint32_t x, std::uint32_t y) const; +}; + +/// Decode the encoder's unfiltered RGB, RGBA or indexed output; throw on +/// corruption. +DecodedPng decode_png(std::string_view data); +std::string bytes(std::initializer_list values); + +} // namespace odr::test::png diff --git a/test/src/internal/png/png_util_test.cpp b/test/src/internal/png/png_util_test.cpp index 13b319701..8ce2828d0 100644 --- a/test/src/internal/png/png_util_test.cpp +++ b/test/src/internal/png/png_util_test.cpp @@ -1,6 +1,6 @@ #include -#include +#include #include #include @@ -9,70 +9,9 @@ #include using namespace odr::internal; - -namespace { - -std::string bytes(const std::initializer_list values) { - std::string result; - for (const std::uint8_t value : values) { - result.push_back(static_cast(value)); - } - return result; -} - -std::uint32_t be32(const std::string &data, const std::size_t at) { - return static_cast(data[at]) << 24 | - static_cast(data[at + 1]) << 16 | - static_cast(data[at + 2]) << 8 | - static_cast(data[at + 3]); -} - -struct DecodedPng final { - std::int32_t width{}; - std::int32_t height{}; - std::string rgb; - std::size_t idat_chunks{0}; -}; - -/// Reads back what @ref png::write wrote: the chunks, then the one -/// zlib stream their `IDAT` holds, minus the filter byte per row. -DecodedPng decode_png(const std::string &png) { - DecodedPng result; - EXPECT_EQ(png.substr(1, 3), "PNG"); - - std::string idat; - std::size_t at = 8; - while (at + 12 <= png.size()) { - const auto length = static_cast(be32(png, at)); - const std::string type = png.substr(at + 4, 4); - EXPECT_LE(length, 0x7fffffffU); - const std::string data = png.substr(at + 8, length); - EXPECT_EQ(be32(png, at + 8 + length), crypto::util::crc32(type + data)); - if (type == "IHDR") { - result.width = static_cast(be32(data, 0)); - result.height = static_cast(be32(data, 4)); - EXPECT_EQ(static_cast(data[8]), 8); // bit depth - EXPECT_EQ(static_cast(data[9]), 2); // colour type rgb - } else if (type == "IDAT") { - ++result.idat_chunks; - idat += data; - } else if (type == "IEND") { - break; - } - at += 12 + length; - } - - const std::string raw = crypto::util::zlib_inflate(idat); - const auto stride = static_cast(result.width) * 3; - for (std::int32_t y = 0; y < result.height; ++y) { - const std::size_t row = static_cast(y) * (stride + 1); - EXPECT_EQ(static_cast(raw[row]), 0); // filter type none - result.rgb.append(raw, row + 1, stride); - } - return result; -} - -} // namespace +using odr::test::png::bytes; +using odr::test::png::decode_png; +using odr::test::png::DecodedPng; TEST(PngUtil, rgb_round_trip) { // 2x2: red, green / blue, white @@ -83,7 +22,7 @@ TEST(PngUtil, rgb_round_trip) { EXPECT_EQ(2, png.width); EXPECT_EQ(2, png.height); - EXPECT_EQ(rgb, png.rgb); + EXPECT_EQ(rgb, png.pixels); } TEST(PngUtil, a_buffer_too_short_for_the_size_is_refused) { @@ -118,5 +57,14 @@ TEST(PngUtil, image_data_spans_chunks_without_restarting_compression) { } const DecodedPng png = decode_png(png::write(rgb, 256, 256, 3)); EXPECT_GT(png.idat_chunks, 1); - EXPECT_EQ(png.rgb, rgb); + EXPECT_EQ(png.pixels, rgb); +} + +TEST(PngUtil, rgba_round_trip) { + // 2x1: opaque red, half-transparent green. + const std::string rgba = bytes({255, 0, 0, 255, 0, 255, 0, 128}); + const DecodedPng png = decode_png(odr::internal::png::write(rgba, 2, 1, 4)); + EXPECT_EQ(png.width, 2); + EXPECT_EQ(png.height, 1); + EXPECT_EQ(png.pixels, rgba); }