Initial commit

This commit is contained in:
2026-02-07 18:32:40 +02:00
commit 219734a813
134 changed files with 20257 additions and 0 deletions

12
LibImage/CMakeLists.txt Normal file
View File

@@ -0,0 +1,12 @@
set(LIBIMAGE_SOURCES
Image.cpp
Netbpm.cpp
PNG.cpp
)
add_library(libimage ${LIBIMAGE_SOURCES})
banan_link_library(libimage ban)
banan_link_library(libimage libdeflate)
banan_install_headers(libimage)
install(TARGETS libimage OPTIONAL)

240
LibImage/Image.cpp Normal file
View File

@@ -0,0 +1,240 @@
#include <BAN/ScopeGuard.h>
#include <BAN/String.h>
#include <LibImage/Image.h>
#include <LibImage/Netbpm.h>
#include <LibImage/PNG.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <unistd.h>
#include <immintrin.h>
namespace LibImage
{
BAN::ErrorOr<BAN::UniqPtr<Image>> Image::load_from_file(BAN::StringView path)
{
int fd = -1;
if (path.data()[path.size()] == '\0')
{
fd = open(path.data(), O_RDONLY);
}
else
{
BAN::String path_str;
TRY(path_str.append(path));
fd = open(path_str.data(), O_RDONLY);
}
if (fd == -1)
{
fprintf(stddbg, "open: %s\n", strerror(errno));
return BAN::Error::from_errno(errno);
}
BAN::ScopeGuard guard_file_close([fd] { close(fd); });
struct stat st;
if (fstat(fd, &st) == -1)
{
fprintf(stddbg, "fstat: %s\n", strerror(errno));
return BAN::Error::from_errno(errno);
}
void* addr = mmap(nullptr, st.st_size, PROT_READ, MAP_PRIVATE, fd, 0);
if (addr == MAP_FAILED)
{
fprintf(stddbg, "mmap: %s\n", strerror(errno));
return BAN::Error::from_errno(errno);
}
BAN::ScopeGuard guard_munmap([&] { munmap(addr, st.st_size); });
auto image_data_span = BAN::ConstByteSpan(reinterpret_cast<uint8_t*>(addr), st.st_size);
if (probe_netbpm(image_data_span))
return TRY(load_netbpm(image_data_span));
if (probe_png(image_data_span))
return TRY(load_png(image_data_span));
fprintf(stderr, "unrecognized image format\n");
return BAN::Error::from_errno(ENOTSUP);
}
struct FloatingColor
{
__m128 vals;
FloatingColor() {}
FloatingColor(float b, float g, float r, float a)
: vals { b, g, r, a }
{}
FloatingColor(Image::Color c)
: FloatingColor(c.b, c.g, c.r, c.a)
{}
FloatingColor operator*(float value) const
{
FloatingColor color;
color.vals = _mm_mul_ps(vals, _mm_set1_ps(value));
return color;
}
FloatingColor operator+(FloatingColor other) const
{
FloatingColor color;
color.vals = _mm_add_ps(this->vals, other.vals);
return color;
}
Image::Color as_color() const
{
__m128i int32 = _mm_cvttps_epi32(this->vals);
__m128i int16 = _mm_packs_epi32(int32, _mm_setzero_si128());
__m128i int8 = _mm_packus_epi16(int16, _mm_setzero_si128());
const uint32_t temp = _mm_cvtsi128_si32(int8);
return Image::Color {
.b = reinterpret_cast<const uint8_t*>(&temp)[0],
.g = reinterpret_cast<const uint8_t*>(&temp)[1],
.r = reinterpret_cast<const uint8_t*>(&temp)[2],
.a = reinterpret_cast<const uint8_t*>(&temp)[3],
};
}
};
BAN::ErrorOr<BAN::UniqPtr<Image>> Image::resize(uint64_t new_width, uint64_t new_height, ResizeAlgorithm algorithm)
{
if (!validate_size(new_width, new_height))
return BAN::Error::from_errno(EOVERFLOW);
const float ratio_x = static_cast<float>(width()) / new_width;
const float ratio_y = static_cast<float>(height()) / new_height;
const auto get_clamped_color =
[this](int64_t x, int64_t y)
{
x = BAN::Math::clamp<int64_t>(x, 0, width() - 1);
y = BAN::Math::clamp<int64_t>(y, 0, height() - 1);
return get_color(x, y);
};
switch (algorithm)
{
case ResizeAlgorithm::Nearest:
{
BAN::Vector<Color> nearest_bitmap;
TRY(nearest_bitmap.resize(new_width * new_height));
for (uint64_t y = 0; y < new_height; y++)
for (uint64_t x = 0; x < new_width; x++)
nearest_bitmap[y * new_width + x] = get_clamped_color(x * ratio_x, y * ratio_y);
return TRY(BAN::UniqPtr<Image>::create(new_width, new_height, BAN::move(nearest_bitmap)));
}
case ResizeAlgorithm::Linear:
{
BAN::Vector<Color> bilinear_bitmap;
TRY(bilinear_bitmap.resize(new_width * new_height));
const uint64_t temp_w = width() + 1;
const uint64_t temp_h = height() + 1;
BAN::Vector<FloatingColor> floating_bitmap;
TRY(floating_bitmap.resize(temp_w * temp_h));
for (uint64_t y = 0; y < temp_h; y++)
for (uint64_t x = 0; x < temp_w; x++)
floating_bitmap[y * temp_w + x] = get_clamped_color(x, y);
for (uint64_t y = 0; y < new_height; y++)
{
for (uint64_t x = 0; x < new_width; x++)
{
const float src_x = x * ratio_x;
const float src_y = y * ratio_y;
const float weight_x = BAN::Math::fmod(src_x, 1.0f);
const float weight_y = BAN::Math::fmod(src_y, 1.0f);
const uint64_t src_x_u64 = BAN::Math::clamp<uint64_t>(src_x, 0, width() - 1);
const uint64_t src_y_u64 = BAN::Math::clamp<uint64_t>(src_y, 0, height() - 1);
const auto tl = floating_bitmap[(src_y_u64 + 0) * temp_w + (src_x_u64 + 0)];
const auto tr = floating_bitmap[(src_y_u64 + 0) * temp_w + (src_x_u64 + 1)];
const auto bl = floating_bitmap[(src_y_u64 + 1) * temp_w + (src_x_u64 + 0)];
const auto br = floating_bitmap[(src_y_u64 + 1) * temp_w + (src_x_u64 + 1)];
const auto avg_t = tl * (1.0f - weight_x) + tr * weight_x;
const auto avg_b = bl * (1.0f - weight_x) + br * weight_x;
const auto avg = avg_t * (1.0f - weight_y) + avg_b * weight_y;
bilinear_bitmap[y * new_width + x] = avg.as_color();
}
}
return TRY(BAN::UniqPtr<Image>::create(new_width, new_height, BAN::move(bilinear_bitmap)));
}
case ResizeAlgorithm::Cubic:
{
BAN::Vector<Color> bicubic_bitmap;
TRY(bicubic_bitmap.resize(new_width * new_height, {}));
constexpr auto cubic_interpolate =
[](const FloatingColor p[4], float weight) -> FloatingColor
{
const auto a = (p[0] * -0.5) + (p[1] * 1.5) + (p[2] * -1.5) + (p[3] * 0.5);
const auto b = p[0] + (p[1] * -2.5) + (p[2] * 2.0) + (p[3] * -0.5);
const auto c = (p[0] * -0.5) + (p[2] * 0.5);
const auto d = p[1];
return ((a * weight + b) * weight + c) * weight + d;
};
const uint64_t temp_w = width() + 3;
const uint64_t temp_h = height() + 3;
BAN::Vector<FloatingColor> floating_bitmap;
TRY(floating_bitmap.resize(temp_w * temp_h, {}));
for (uint64_t y = 0; y < temp_h; y++)
for (uint64_t x = 0; x < temp_w; x++)
floating_bitmap[y * temp_w + x] = get_clamped_color(
static_cast<int64_t>(x) - 1,
static_cast<int64_t>(y) - 1
);
for (uint64_t y = 0; y < new_height; y++)
{
for (uint64_t x = 0; x < new_width; x++)
{
const float src_x = x * ratio_x;
const float src_y = y * ratio_y;
const float weight_x = BAN::Math::fmod(src_x, 1.0f);
const float weight_y = BAN::Math::fmod(src_y, 1.0f);
const uint64_t src_x_u64 = BAN::Math::clamp<uint64_t>(src_x, 0, width() - 1) + 1;
const uint64_t src_y_u64 = BAN::Math::clamp<uint64_t>(src_y, 0, height() - 1) + 1;
FloatingColor values[4];
for (int64_t m = -1; m <= 2; m++)
{
const FloatingColor p[4] {
floating_bitmap[(src_y_u64 + m) * temp_w + (src_x_u64 - 1)],
floating_bitmap[(src_y_u64 + m) * temp_w + (src_x_u64 + 0)],
floating_bitmap[(src_y_u64 + m) * temp_w + (src_x_u64 + 1)],
floating_bitmap[(src_y_u64 + m) * temp_w + (src_x_u64 + 2)],
};
values[m + 1] = cubic_interpolate(p, weight_x);
}
bicubic_bitmap[y * new_width + x] = cubic_interpolate(values, weight_y).as_color();
}
}
return TRY(BAN::UniqPtr<Image>::create(new_width, new_height, BAN::move(bicubic_bitmap)));
}
}
return BAN::Error::from_errno(EINVAL);
}
}

140
LibImage/Netbpm.cpp Normal file
View File

@@ -0,0 +1,140 @@
#include <BAN/Optional.h>
#include <LibImage/Netbpm.h>
#include <ctype.h>
#include <inttypes.h>
#include <stdio.h>
namespace LibImage
{
static BAN::Optional<uint64_t> parse_u64(BAN::ConstByteSpan& data)
{
size_t digit_count = 0;
while (digit_count < data.size() && isdigit(data[digit_count]))
digit_count++;
if (digit_count == 0)
return {};
uint64_t result = 0;
for (size_t i = 0; i < digit_count; i++)
{
if (BAN::Math::will_multiplication_overflow<uint64_t>(result, 10))
return {};
result *= 10;
if (BAN::Math::will_addition_overflow<uint64_t>(result, data[i] - '0'))
return {};
result += data[i] - '0';
}
data = data.slice(digit_count);
return result;
}
bool probe_netbpm(BAN::ConstByteSpan image_data)
{
if (image_data.size() < 2)
return false;
uint16_t u16_signature = image_data.as<const uint16_t>();
switch (u16_signature)
{
case 0x3650:
case 0x3550:
case 0x3450:
case 0x3350:
case 0x3250:
case 0x3150:
return true;
default:
return false;
}
}
BAN::ErrorOr<BAN::UniqPtr<Image>> load_netbpm(BAN::ConstByteSpan image_data)
{
if (image_data.size() < 11)
{
fprintf(stddbg, "invalid Netbpm image (too small)\n");
return BAN::Error::from_errno(EINVAL);
}
if (image_data[0] != 'P')
{
fprintf(stddbg, "not Netbpm image\n");
return BAN::Error::from_errno(EINVAL);
}
if (image_data[1] != '6')
{
fprintf(stddbg, "unsupported Netbpm image\n");
return BAN::Error::from_errno(EINVAL);
}
if (image_data[2] != '\n')
{
fprintf(stddbg, "invalid Netbpm image (invalid header)\n");
return BAN::Error::from_errno(EINVAL);
}
image_data = image_data.slice(3);
auto opt_width = parse_u64(image_data);
if (!opt_width.has_value() || image_data[0] != ' ')
{
fprintf(stddbg, "invalid Netbpm image (invalid width)\n");
return BAN::Error::from_errno(EINVAL);
}
image_data = image_data.slice(1);
auto width = opt_width.value();
auto opt_height = parse_u64(image_data);
if (!opt_height.has_value() || image_data[0] != '\n')
{
fprintf(stddbg, "invalid Netbpm image (invalid height)\n");
return BAN::Error::from_errno(EINVAL);
}
image_data = image_data.slice(1);
auto height = opt_height.value();
if (!Image::validate_size(width, height))
{
fprintf(stddbg, "invalid Netbpm image size\n");
return BAN::Error::from_errno(EINVAL);
}
auto header_end = parse_u64(image_data);
if (!header_end.has_value() || *header_end != 255 || image_data[0] != '\n')
{
fprintf(stddbg, "invalid Netbpm image (invalid header end)\n");
return BAN::Error::from_errno(EINVAL);
}
image_data = image_data.slice(1);
if (image_data.size() < width * height * 3)
{
fprintf(stddbg, "invalid Netbpm image (too small file size)\n");
return BAN::Error::from_errno(EINVAL);
}
BAN::Vector<Image::Color> bitmap;
TRY(bitmap.resize(width * height));
// Fill bitmap
for (uint64_t y = 0; y < height; y++)
{
for (uint64_t x = 0; x < width; x++)
{
const uint64_t index = y * width + x;
auto& pixel = bitmap[index];
pixel.r = image_data[index * 3 + 0];
pixel.g = image_data[index * 3 + 1];
pixel.b = image_data[index * 3 + 2];
pixel.a = 0xFF;
}
}
return TRY(BAN::UniqPtr<Image>::create(width, height, BAN::move(bitmap)));
}
}

506
LibImage/PNG.cpp Normal file
View File

@@ -0,0 +1,506 @@
#include <BAN/Debug.h>
#include <BAN/Endianness.h>
#include <LibImage/PNG.h>
#include <LibDEFLATE/Decompressor.h>
#include <ctype.h>
#define DEBUG_PNG 0
// https://www.w3.org/TR/png-3/
namespace LibImage
{
template<BAN::integral T>
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<uint32_t> width;
PODNetworkEndian<uint32_t> 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<PNGChunk> 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<const BAN::NetworkEndian<uint32_t>>();
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<const char>().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<uint64_t> parse_pixel_data(BAN::Vector<Image::Color>& color_bitmap, uint64_t image_width, uint64_t image_height, const IHDR& ihdr, const BAN::Vector<Image::Color>& 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<uint64_t>(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<uint8_t> 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<FilterType>(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<uint8_t>(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<const uint64_t>();
return u64_signature == 0x0A1A0A0D474E5089;
}
BAN::ErrorOr<BAN::UniqPtr<Image>> 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<const IHDR>();
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<uint8_t>(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<uint8_t>(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<uint8_t>(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<uint8_t>(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<uint8_t>(ihdr.colour_type));
dprintln_if(DEBUG_PNG, " compression method: {}", static_cast<uint8_t>(ihdr.compression_method));
dprintln_if(DEBUG_PNG, " filter method: {}", static_cast<uint8_t>(ihdr.filter_method));
dprintln_if(DEBUG_PNG, " interlace method: {}", static_cast<uint8_t>(ihdr.interlace_method));
BAN::Vector<Image::Color> palette;
BAN::Vector<BAN::ConstByteSpan> 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<uint8_t>(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<const char>().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<uint8_t> 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<Image::Color> 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<Image::Color> 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<uint64_t>(image_width - x_start[pass], x_increment[pass]);
const uint64_t pass_height = BAN::Math::div_round_up<uint64_t>(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<Image>::create(image_width, image_height, BAN::move(pixel_data)));
}
}

View File

@@ -0,0 +1,91 @@
#pragma once
#include <BAN/Limits.h>
#include <BAN/StringView.h>
#include <BAN/UniqPtr.h>
#include <BAN/Vector.h>
namespace LibImage
{
class Image
{
public:
struct Color
{
uint8_t b;
uint8_t g;
uint8_t r;
uint8_t a;
// Calculate weighted average of colors
// weight of 0.0 returns a and weight of 1.0 returns b
static Color average(Color a, Color b, double weight)
{
const double b_mult = weight < 0.0 ? 0.0 : weight > 1.0 ? 1.0 : weight;
const double a_mult = 1.0 - b_mult;
return Color {
.b = static_cast<uint8_t>(a.b * a_mult + b.b * b_mult),
.g = static_cast<uint8_t>(a.g * a_mult + b.g * b_mult),
.r = static_cast<uint8_t>(a.r * a_mult + b.r * b_mult),
.a = static_cast<uint8_t>(a.a * a_mult + b.a * b_mult),
};
}
uint32_t as_argb() const
{
return ((uint32_t)a << 24) | ((uint32_t)r << 16) | ((uint32_t)g << 8) | b;
}
};
enum class ResizeAlgorithm
{
Nearest,
Linear,
Cubic,
};
public:
static BAN::ErrorOr<BAN::UniqPtr<Image>> load_from_file(BAN::StringView path);
BAN::ErrorOr<BAN::UniqPtr<Image>> resize(uint64_t new_width, uint64_t new_height, ResizeAlgorithm = ResizeAlgorithm::Cubic);
Color get_color(uint64_t x, uint64_t y) const { return m_bitmap[y * width() + x]; }
const BAN::Vector<Color> bitmap() const { return m_bitmap; }
uint64_t width() const { return m_width; }
uint64_t height() const { return m_height; }
static constexpr bool validate_size(uint64_t width, uint64_t height)
{
// width and height must fit in int64_t and width * height * sizeof(Color) has to not overflow
if (width > static_cast<uint64_t>(BAN::numeric_limits<int64_t>::max()))
return false;
if (height > static_cast<uint64_t>(BAN::numeric_limits<int64_t>::max()))
return false;
if (BAN::Math::will_multiplication_overflow<uint64_t>(width, height))
return false;
if (BAN::Math::will_multiplication_overflow<uint64_t>(width * height, sizeof(Color)))
return false;
return true;
}
private:
Image(uint64_t width, uint64_t height, BAN::Vector<Color>&& bitmap)
: m_width(width)
, m_height(height)
, m_bitmap(BAN::move(bitmap))
{
ASSERT(validate_size(m_width, m_height));
ASSERT(m_bitmap.size() >= m_width * m_height);
}
private:
const uint64_t m_width;
const uint64_t m_height;
const BAN::Vector<Color> m_bitmap;
friend class BAN::UniqPtr<Image>;
};
}

View File

@@ -0,0 +1,11 @@
#include <BAN/ByteSpan.h>
#include <LibImage/Image.h>
namespace LibImage
{
bool probe_netbpm(BAN::ConstByteSpan);
BAN::ErrorOr<BAN::UniqPtr<Image>> load_netbpm(BAN::ConstByteSpan);
}

View File

@@ -0,0 +1,11 @@
#include <BAN/ByteSpan.h>
#include <LibImage/Image.h>
namespace LibImage
{
bool probe_png(BAN::ConstByteSpan);
BAN::ErrorOr<BAN::UniqPtr<Image>> load_png(BAN::ConstByteSpan);
}