Initial commit
This commit is contained in:
12
LibImage/CMakeLists.txt
Normal file
12
LibImage/CMakeLists.txt
Normal 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
240
LibImage/Image.cpp
Normal 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
140
LibImage/Netbpm.cpp
Normal 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
506
LibImage/PNG.cpp
Normal 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)));
|
||||
}
|
||||
|
||||
}
|
||||
91
LibImage/include/LibImage/Image.h
Normal file
91
LibImage/include/LibImage/Image.h
Normal 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>;
|
||||
};
|
||||
|
||||
}
|
||||
11
LibImage/include/LibImage/Netbpm.h
Normal file
11
LibImage/include/LibImage/Netbpm.h
Normal 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);
|
||||
|
||||
}
|
||||
11
LibImage/include/LibImage/PNG.h
Normal file
11
LibImage/include/LibImage/PNG.h
Normal 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);
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user