#include #include #include #include #include #define DEBUG_PNG 0 // https://www.w3.org/TR/png-3/ namespace LibImage { template struct PODNetworkEndian { T raw; constexpr operator T() const { return BAN::network_endian_to_host(raw); } }; enum class ColourType : uint8_t { Greyscale = 0, Truecolour = 2, IndexedColour = 3, GreyscaleAlpha = 4, TruecolourAlpha = 6, }; enum class CompressionMethod : uint8_t { Deflate = 0, }; enum class FilterMethod : uint8_t { Adaptive = 0, }; enum class FilterType : uint8_t { None = 0, Sub = 1, Up = 2, Average = 3, Paeth = 4, }; enum class InterlaceMethod : uint8_t { NoInterlace = 0, Adam7 = 1, }; struct IHDR { PODNetworkEndian width; PODNetworkEndian height; uint8_t bit_depth; ColourType colour_type; CompressionMethod compression_method; FilterMethod filter_method; InterlaceMethod interlace_method; } __attribute__((packed)); struct PNGChunk { BAN::StringView name; BAN::ConstByteSpan data; }; BAN::ErrorOr read_and_take_chunk(BAN::ConstByteSpan& image_data) { if (image_data.size() < 12) { dwarnln_if(DEBUG_PNG, "PNG stream does not contain any more chunks"); return BAN::Error::from_errno(EINVAL); } uint32_t length = image_data.as>(); image_data = image_data.slice(4); if (image_data.size() < length + 8) { dwarnln_if(DEBUG_PNG, "PNG stream does not contain any more chunks"); return BAN::Error::from_errno(EINVAL); } PNGChunk result; result.name = BAN::StringView(image_data.as_span().data(), 4); image_data = image_data.slice(4); result.data = image_data.slice(0, length); image_data = image_data.slice(length); // FIXME: validate CRC image_data = image_data.slice(4); return result; } static bool validate_ihdr_colour_type_and_bit_depth(const IHDR& ihdr) { if (!BAN::Math::is_power_of_two(ihdr.bit_depth)) return false; switch (ihdr.colour_type) { case ColourType::Greyscale: if (ihdr.bit_depth < 1 || ihdr.bit_depth > 16) return false; return true; case ColourType::Truecolour: if (ihdr.bit_depth < 8 || ihdr.bit_depth > 16) return false; return true; case ColourType::IndexedColour: if (ihdr.bit_depth < 1 || ihdr.bit_depth > 8) return false; return true; case ColourType::GreyscaleAlpha: if (ihdr.bit_depth < 8 || ihdr.bit_depth > 16) return false; return true; case ColourType::TruecolourAlpha: if (ihdr.bit_depth < 8 || ihdr.bit_depth > 16) return false; return true; default: return false; } } static BAN::ErrorOr parse_pixel_data(BAN::Vector& color_bitmap, uint64_t image_width, uint64_t image_height, const IHDR& ihdr, const BAN::Vector& palette, BAN::ByteSpan encoded_data) { ASSERT(color_bitmap.size() >= image_height * image_width); const uint8_t bits_per_channel = ihdr.bit_depth; const uint8_t channels = [&]() -> uint8_t { switch (ihdr.colour_type) { case ColourType::Greyscale: return 1; case ColourType::Truecolour: return 3; case ColourType::IndexedColour: return 1; case ColourType::GreyscaleAlpha: return 2; case ColourType::TruecolourAlpha: return 4; default: ASSERT_NOT_REACHED(); } }(); const auto extract_channel = [&](auto& bit_buffer) -> uint8_t { uint16_t tmp = MUST(bit_buffer.take_bits(bits_per_channel)); switch (bits_per_channel) { case 1: return tmp * 0xFF; case 2: return tmp * 0xFF / 3; case 4: return tmp * 0xFF / 15; case 8: return tmp; case 16: return tmp & 0xFF; // NOTE: stored in big endian } ASSERT_NOT_REACHED(); }; const auto extract_color = [&](auto& bit_buffer) -> Image::Color { Image::Color color; switch (ihdr.colour_type) { case ColourType::Greyscale: color.r = extract_channel(bit_buffer); color.g = color.r; color.b = color.r; color.a = 0xFF; break; case ColourType::Truecolour: color.r = extract_channel(bit_buffer); color.g = extract_channel(bit_buffer); color.b = extract_channel(bit_buffer); color.a = 0xFF; break; case ColourType::IndexedColour: color = palette[MUST(bit_buffer.take_bits(bits_per_channel))]; break; case ColourType::GreyscaleAlpha: color.r = extract_channel(bit_buffer); color.g = color.r; color.b = color.r; color.a = extract_channel(bit_buffer); break; case ColourType::TruecolourAlpha: color.r = extract_channel(bit_buffer); color.g = extract_channel(bit_buffer); color.b = extract_channel(bit_buffer); color.a = extract_channel(bit_buffer); break; } return color; }; constexpr auto paeth_predictor = [](int16_t a, int16_t b, int16_t c) -> uint8_t { const int16_t p = a + b - c; const int16_t pa = BAN::Math::abs(p - a); const int16_t pb = BAN::Math::abs(p - b); const int16_t pc = BAN::Math::abs(p - c); if (pa <= pb && pa <= pc) return a; if (pb <= pc) return b; return c; }; const uint64_t bytes_per_scanline = BAN::Math::div_round_up(image_width * channels * bits_per_channel, 8); const uint64_t pitch = bytes_per_scanline + 1; if (encoded_data.size() < pitch * image_height) { dwarnln_if(DEBUG_PNG, "PNG does not contain enough image data"); return BAN::Error::from_errno(ENODATA); } BAN::Vector zero_scanline; TRY(zero_scanline.resize(bytes_per_scanline, 0)); const uint8_t filter_offset = (bits_per_channel < 8) ? 1 : channels * (bits_per_channel / 8); for (uint64_t y = 0; y < image_height; y++) { auto scanline = encoded_data.slice((y - 0) * pitch + 1, bytes_per_scanline); auto scanline_above = (y > 0) ? encoded_data.slice((y - 1) * pitch + 1, bytes_per_scanline) : BAN::ConstByteSpan(zero_scanline.span()); auto filter_type = static_cast(encoded_data[y * pitch]); switch (filter_type) { case FilterType::None: break; case FilterType::Sub: for (uint64_t x = filter_offset; x < bytes_per_scanline; x++) scanline[x] += scanline[x - filter_offset]; break; case FilterType::Up: for (uint64_t x = 0; x < bytes_per_scanline; x++) scanline[x] += scanline_above[x]; break; case FilterType::Average: for (uint8_t i = 0; i < filter_offset; i++) scanline[i] += scanline_above[i] / 2; for (uint64_t x = filter_offset; x < bytes_per_scanline; x++) scanline[x] += ((uint16_t)scanline[x - filter_offset] + (uint16_t)scanline_above[x]) / 2; break; case FilterType::Paeth: for (uint8_t i = 0; i < filter_offset; i++) scanline[i] += paeth_predictor(0, scanline_above[i], 0); for (uint64_t x = filter_offset; x < bytes_per_scanline; x++) scanline[x] += paeth_predictor(scanline[x - filter_offset], scanline_above[x], scanline_above[x - filter_offset]); break; default: dwarnln_if(DEBUG_PNG, "invalid filter type {}", static_cast(filter_type)); return BAN::Error::from_errno(EINVAL); } LibDEFLATE::BitInputStream bit_stream(scanline); for (uint64_t x = 0; x < image_width; x++) color_bitmap[y * image_width + x] = extract_color(bit_stream); } return pitch * image_height; } bool probe_png(BAN::ConstByteSpan image_data) { if (image_data.size() < 8) return false; uint64_t u64_signature = image_data.as(); return u64_signature == 0x0A1A0A0D474E5089; } BAN::ErrorOr> load_png(BAN::ConstByteSpan image_data) { if (!probe_png(image_data)) { dwarnln_if(DEBUG_PNG, "Invalid PNG data"); return BAN::Error::from_errno(EINVAL); } image_data = image_data.slice(8); auto ihdr_chunk = TRY(read_and_take_chunk(image_data)); if (ihdr_chunk.name != "IHDR") { dwarnln_if(DEBUG_PNG, "PNG stream does not start with IHDR chunk"); return BAN::Error::from_errno(EINVAL); } if (ihdr_chunk.data.size() != sizeof(IHDR)) { dwarnln_if(DEBUG_PNG, "PNG stream has invalid IHDR chunk size: {}, expected {}", ihdr_chunk.data.size(), sizeof(IHDR)); return BAN::Error::from_errno(EINVAL); } const auto& ihdr = ihdr_chunk.data.as(); if (ihdr.width == 0 || ihdr.height == 0 || ihdr.width > 0x7FFFFFFF || ihdr.height > 0x7FFFFFFF) { dwarnln_if(DEBUG_PNG, "PNG IHDR has invalid size {}x{}", (uint32_t)ihdr.width, (uint32_t)ihdr.height); return BAN::Error::from_errno(EINVAL); } if (!validate_ihdr_colour_type_and_bit_depth(ihdr)) { dwarnln_if(DEBUG_PNG, "PNG IHDR has invalid bit depth {} for colour type {}", ihdr.bit_depth, static_cast(ihdr.colour_type)); return BAN::Error::from_errno(EINVAL); } if (ihdr.compression_method != CompressionMethod::Deflate) { dwarnln_if(DEBUG_PNG, "PNG IHDR has invalid compression method {}", static_cast(ihdr.compression_method)); return BAN::Error::from_errno(EINVAL); } if (ihdr.filter_method != FilterMethod::Adaptive) { dwarnln_if(DEBUG_PNG, "PNG IHDR has invalid filter method {}", static_cast(ihdr.filter_method)); return BAN::Error::from_errno(EINVAL); } if (ihdr.interlace_method != InterlaceMethod::NoInterlace && ihdr.interlace_method != InterlaceMethod::Adam7) { dwarnln_if(DEBUG_PNG, "PNG IHDR has invalid interlace method {}", static_cast(ihdr.interlace_method)); return BAN::Error::from_errno(EINVAL); } const uint64_t image_width = ihdr.width; const uint64_t image_height = ihdr.height; dprintln_if(DEBUG_PNG, "Decoding {}x{} PNG image", image_width, image_height); dprintln_if(DEBUG_PNG, " bit depth: {}", ihdr.bit_depth); dprintln_if(DEBUG_PNG, " colour type: {}", static_cast(ihdr.colour_type)); dprintln_if(DEBUG_PNG, " compression method: {}", static_cast(ihdr.compression_method)); dprintln_if(DEBUG_PNG, " filter method: {}", static_cast(ihdr.filter_method)); dprintln_if(DEBUG_PNG, " interlace method: {}", static_cast(ihdr.interlace_method)); BAN::Vector palette; BAN::Vector zlib_stream; while (true) { PNGChunk chunk; if (auto ret = read_and_take_chunk(image_data); !ret.is_error()) chunk = ret.release_value(); else { dwarnln_if(DEBUG_PNG, "PNG stream does not end with IEND chunk"); return BAN::Error::from_errno(EINVAL); } if (chunk.name == "IHDR"_sv) { dwarnln_if(DEBUG_PNG, "PNG stream has IDHR chunk defined multiple times"); return BAN::Error::from_errno(EINVAL); } else if (chunk.name == "PLTE"_sv) { if (chunk.data.size() == 0 || chunk.data.size() % 3) { dwarnln_if(DEBUG_PNG, "PNG PLTE has invalid data size {}", chunk.data.size()); return BAN::Error::from_errno(EINVAL); } if (!palette.empty()) { dwarnln_if(DEBUG_PNG, "PNG PLTE defined multiple times"); return BAN::Error::from_errno(EINVAL); } if (ihdr.colour_type != ColourType::IndexedColour && ihdr.colour_type != ColourType::Truecolour && ihdr.colour_type != ColourType::TruecolourAlpha) { dwarnln_if(DEBUG_PNG, "PNG PLTE defined for colour type {} which does not use palette", static_cast(ihdr.colour_type)); return BAN::Error::from_errno(EINVAL); } TRY(palette.resize(chunk.data.size() / 3)); for (size_t i = 0; i < palette.size(); i++) { palette[i].r = chunk.data[3 * i + 0]; palette[i].g = chunk.data[3 * i + 1]; palette[i].b = chunk.data[3 * i + 2]; palette[i].a = 0xFF; } } else if (chunk.name == "IDAT"_sv) { TRY(zlib_stream.push_back(chunk.data)); } else if (chunk.name == "IEND"_sv) { break; } else if (chunk.name == "tEXt"_sv) { auto data_sv = BAN::StringView(chunk.data.as_span().data(), chunk.data.size()); if (auto idx = data_sv.find('\0'); !idx.has_value()) dwarnln_if(DEBUG_PNG, "PNG tEXt chunk does not contain null-byte"); else { auto keyword = data_sv.substring(0, idx.value()); auto text = data_sv.substring(idx.value() + 1); dprintln_if(DEBUG_PNG, "'{}': '{}'", keyword, text); } } else { bool ancillary = islower(chunk.name[0]); if (!ancillary) { dwarnln_if(DEBUG_PNG, "Unsupported critical chunk '{}'", chunk.name); return BAN::Error::from_errno(ENOTSUP); } dwarnln_if(DEBUG_PNG, "Skipping unsupported ancillary chunk '{}'", chunk.name); } } BAN::Vector zlib_stream_buf; BAN::ConstByteSpan zlib_stream_span; if (zlib_stream.empty()) { dwarnln_if(DEBUG_PNG, "PNG does not have zlib stream"); return BAN::Error::from_errno(EINVAL); } if (zlib_stream.size() == 1) zlib_stream_span = zlib_stream.front(); else { for (auto stream : zlib_stream) { const size_t old_size = zlib_stream_buf.size(); TRY(zlib_stream_buf.resize(old_size + stream.size())); for (size_t i = 0; i < stream.size(); i++) zlib_stream_buf[old_size + i] = stream[i]; } zlib_stream_span = zlib_stream_buf.span(); } uint64_t total_size = 0; for (auto stream : zlib_stream) total_size += stream.size(); dprintln_if(DEBUG_PNG, "PNG has {} byte zlib stream", total_size); LibDEFLATE::Decompressor decompressor(zlib_stream_span, LibDEFLATE::StreamType::Zlib); auto inflated_buffer = TRY(decompressor.decompress()); auto inflated_data = inflated_buffer.span(); dprintln_if(DEBUG_PNG, " uncompressed size {}", inflated_data.size()); dprintln_if(DEBUG_PNG, " compression ratio {}", (double)inflated_data.size() / total_size); BAN::Vector pixel_data; TRY(pixel_data.resize(image_width * image_height)); switch (ihdr.interlace_method) { case InterlaceMethod::NoInterlace: TRY(parse_pixel_data(pixel_data, image_width, image_height, ihdr, palette, inflated_data)); break; case InterlaceMethod::Adam7: { constexpr uint8_t x_start[] { 0, 4, 0, 2, 0, 1, 0 }; constexpr uint8_t x_increment[] { 8, 8, 4, 4, 2, 2, 1 }; constexpr uint8_t y_start[] { 0, 0, 4, 0, 2, 0, 1 }; constexpr uint8_t y_increment[] { 8, 8, 8, 4, 4, 2, 2 }; BAN::Vector pass_pixel_data; TRY(pass_pixel_data.resize(((image_height + 1) / 2) * image_width)); for (int pass = 0; pass < 7; pass++) { const uint64_t pass_width = BAN::Math::div_round_up(image_width - x_start[pass], x_increment[pass]); const uint64_t pass_height = BAN::Math::div_round_up(image_height - y_start[pass], y_increment[pass]); const uint64_t nparsed = TRY(parse_pixel_data(pass_pixel_data, pass_width, pass_height, ihdr, palette, inflated_data)); for (uint64_t y = 0; y < pass_height; y++) { for (uint64_t x = 0; x < pass_width; x++) { const uint64_t abs_x = x * x_increment[pass] + x_start[pass]; const uint64_t abs_y = y * y_increment[pass] + y_start[pass]; pixel_data[abs_y * image_width + abs_x] = pass_pixel_data[y * pass_width + x]; } } dprintln_if(DEBUG_PNG, "Adam7 pass {} done ({}x{})", pass + 1, pass_width, pass_height); inflated_data = inflated_data.slice(nparsed); } break; } default: ASSERT_NOT_REACHED(); } return TRY(BAN::UniqPtr::create(image_width, image_height, BAN::move(pixel_data))); } }