ladybird/SharedGraphics/PNGLoader.cpp
Andreas Kling 60d25f0f4a Kernel: Introduce threads, and refactor everything in support of it.
The scheduler now operates on threads, rather than on processes.
Each process has a main thread, and can have any number of additional
threads. The process exits when the main thread exits.

This patch doesn't actually spawn any additional threads, it merely
does all the plumbing needed to make it possible. :^)
2019-03-23 22:03:17 +01:00

474 lines
14 KiB
C++

#include <SharedGraphics/PNGLoader.h>
#include <Kernel/NetworkOrdered.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <unistd.h>
#include <stdio.h>
#include <fcntl.h>
#include <string.h>
#include <SharedGraphics/puff.c>
#include <serenity.h>
//#define PNG_STOPWATCH_DEBUG
struct PNG_IHDR {
NetworkOrdered<dword> width;
NetworkOrdered<dword> height;
byte bit_depth { 0 };
byte color_type { 0 };
byte compression_method { 0 };
byte filter_method { 0 };
byte interlace_method { 0 };
};
static_assert(sizeof(PNG_IHDR) == 13);
struct Scanline {
byte filter { 0 };
ByteBuffer data;
};
struct PNGLoadingContext {
int width { -1 };
int height { -1 };
byte bit_depth { 0 };
byte color_type { 0 };
byte compression_method { 0 };
byte filter_method { 0 };
byte interlace_method { 0 };
byte bytes_per_pixel { 0 };
bool has_seen_zlib_header { false };
bool has_alpha() const { return color_type & 4; }
Vector<Scanline> scanlines;
RetainPtr<GraphicsBitmap> bitmap;
byte* decompression_buffer { nullptr };
int decompression_buffer_size { 0 };
Vector<byte> compressed_data;
};
class Streamer {
public:
Streamer(const byte* data, int size)
: m_original_data(data)
, m_original_size(size)
, m_data_ptr(data)
, m_size_remaining(size)
{
}
template<typename T>
bool read(T& value)
{
if (m_size_remaining < sizeof(T))
return false;
value = *((NetworkOrdered<T>*)m_data_ptr);
m_data_ptr += sizeof(T);
m_size_remaining -= sizeof(T);
return true;
}
bool read_bytes(byte* buffer, int count)
{
if (m_size_remaining < count)
return false;
memcpy(buffer, m_data_ptr, count);
m_data_ptr += count;
m_size_remaining -= count;
return true;
}
bool wrap_bytes(ByteBuffer& buffer, int count)
{
if (m_size_remaining < count)
return false;
buffer = ByteBuffer::wrap((void*)m_data_ptr, count);
m_data_ptr += count;
m_size_remaining -= count;
return true;
}
bool at_end() const { return !m_size_remaining; }
private:
const byte* m_original_data;
int m_original_size;
const byte* m_data_ptr;
int m_size_remaining;
};
static RetainPtr<GraphicsBitmap> load_png_impl(const byte*, int);
static bool process_chunk(Streamer&, PNGLoadingContext& context);
RetainPtr<GraphicsBitmap> load_png(const String& path)
{
int fd = open(path.characters(), O_RDONLY);
if (fd < 0) {
perror("open");
return nullptr;
}
struct stat st;
if (fstat(fd, &st) < 0) {
perror("fstat");
if (close(fd) < 0)
perror("close");
return nullptr;
}
if (st.st_size < 8) {
if (close(fd) < 0)
perror("close");
return nullptr;
}
auto* mapped_file = (byte*)mmap(nullptr, st.st_size, PROT_READ, MAP_SHARED, fd, 0);
if (mapped_file == MAP_FAILED) {
if (close(fd) < 0)
perror("close");
return nullptr;
}
auto bitmap = load_png_impl(mapped_file, st.st_size);
if (munmap(mapped_file, st.st_size) < 0)
perror("munmap");
if (close(fd) < 0)
perror("close");
return bitmap;
}
[[gnu::always_inline]] static inline byte paeth_predictor(int a, int b, int c)
{
int p = a + b - c;
int pa = abs(p - a);
int pb = abs(p - b);
int pc = abs(p - c);
if (pa <= pb && pa <= pc)
return a;
if (pb <= pc)
return b;
return c;
}
union [[gnu::packed]] Pixel {
RGBA32 rgba { 0 };
byte v[4];
struct {
byte r;
byte g;
byte b;
byte a;
};
};
static_assert(sizeof(Pixel) == 4);
template<bool has_alpha, byte filter_type>
[[gnu::always_inline]] static inline void unfilter_impl(GraphicsBitmap& bitmap, int y, const void* dummy_scanline_data)
{
auto* dummy_scanline = (const Pixel*)dummy_scanline_data;
if constexpr (filter_type == 0) {
auto* pixels = (Pixel*)bitmap.scanline(y);
for (int i = 0; i < bitmap.width(); ++i) {
auto& x = pixels[i];
swap(x.r, x.b);
}
}
if constexpr (filter_type == 1) {
auto* pixels = (Pixel*)bitmap.scanline(y);
swap(pixels[0].r, pixels[0].b);
for (int i = 1; i < bitmap.width(); ++i) {
auto& x = pixels[i];
swap(x.r, x.b);
auto& a = (const Pixel&)pixels[i - 1];
x.v[0] += a.v[0];
x.v[1] += a.v[1];
x.v[2] += a.v[2];
if constexpr (has_alpha)
x.v[3] += a.v[3];
}
return;
}
if constexpr (filter_type == 2) {
auto* pixels = (Pixel*)bitmap.scanline(y);
auto* pixels_y_minus_1 = y == 0 ? dummy_scanline : (Pixel*)bitmap.scanline(y - 1);
for (int i = 0; i < bitmap.width(); ++i) {
auto& x = pixels[i];
swap(x.r, x.b);
const Pixel& b = pixels_y_minus_1[i];
x.v[0] += b.v[0];
x.v[1] += b.v[1];
x.v[2] += b.v[2];
if constexpr (has_alpha)
x.v[3] += b.v[3];
}
return;
}
if constexpr (filter_type == 3) {
auto* pixels = (Pixel*)bitmap.scanline(y);
auto* pixels_y_minus_1 = y == 0 ? dummy_scanline : (Pixel*)bitmap.scanline(y - 1);
for (int i = 0; i < bitmap.width(); ++i) {
auto& x = pixels[i];
swap(x.r, x.b);
Pixel a;
if (i != 0) a = pixels[i - 1];
const Pixel& b = pixels_y_minus_1[i];
x.v[0] = x.v[0] + ((a.v[0] + b.v[0]) / 2);
x.v[1] = x.v[1] + ((a.v[1] + b.v[1]) / 2);
x.v[2] = x.v[2] + ((a.v[2] + b.v[2]) / 2);
if constexpr (has_alpha)
x.v[3] = x.v[3] + ((a.v[3] + b.v[3]) / 2);
}
return;
}
if constexpr (filter_type == 4) {
auto* pixels = (Pixel*)bitmap.scanline(y);
auto* pixels_y_minus_1 = y == 0 ? dummy_scanline : (Pixel*)bitmap.scanline(y - 1);
for (int i = 0; i < bitmap.width(); ++i) {
auto& x = pixels[i];
swap(x.r, x.b);
Pixel a;
const Pixel& b = pixels_y_minus_1[i];
Pixel c;
if (i != 0) {
a = pixels[i - 1];
c = pixels_y_minus_1[i - 1];
}
x.v[0] += paeth_predictor(a.v[0], b.v[0], c.v[0]);
x.v[1] += paeth_predictor(a.v[1], b.v[1], c.v[1]);
x.v[2] += paeth_predictor(a.v[2], b.v[2], c.v[2]);
if constexpr (has_alpha)
x.v[3] += paeth_predictor(a.v[3], b.v[3], c.v[3]);
}
}
}
[[gnu::noinline]] static void unfilter(PNGLoadingContext& context)
{
{
#ifdef PNG_STOPWATCH_DEBUG
Stopwatch sw("load_png_impl: unfilter: unpack");
#endif
// First unpack the scanlines to RGBA:
switch (context.color_type) {
case 2:
for (int y = 0; y < context.height; ++y) {
struct [[gnu::packed]] Triplet { byte r; byte g; byte b; };
auto* triplets = (Triplet*)context.scanlines[y].data.pointer();
for (int i = 0; i < context.width; ++i) {
auto& pixel = (Pixel&)context.bitmap->scanline(y)[i];
pixel.r = triplets[i].r;
pixel.g = triplets[i].g;
pixel.b = triplets[i].b;
pixel.a = 0xff;
}
}
break;
case 6:
for (int y = 0; y < context.height; ++y) {
memcpy(context.bitmap->scanline(y), context.scanlines[y].data.pointer(), context.scanlines[y].data.size());
}
break;
default:
ASSERT_NOT_REACHED();
break;
}
}
auto dummy_scanline = ByteBuffer::create_zeroed(context.width * sizeof(RGBA32));
#ifdef PNG_STOPWATCH_DEBUG
Stopwatch sw("load_png_impl: unfilter: process");
#endif
for (int y = 0; y < context.height; ++y) {
auto filter = context.scanlines[y].filter;
if (filter == 0) {
if (context.has_alpha())
unfilter_impl<true, 0>(*context.bitmap, y, dummy_scanline.pointer());
else
unfilter_impl<false, 0>(*context.bitmap, y, dummy_scanline.pointer());
continue;
}
if (filter == 1) {
if (context.has_alpha())
unfilter_impl<true, 1>(*context.bitmap, y, dummy_scanline.pointer());
else
unfilter_impl<false, 1>(*context.bitmap, y, dummy_scanline.pointer());
continue;
}
if (filter == 2) {
if (context.has_alpha())
unfilter_impl<true, 2>(*context.bitmap, y, dummy_scanline.pointer());
else
unfilter_impl<false, 2>(*context.bitmap, y, dummy_scanline.pointer());
continue;
}
if (filter == 3) {
if (context.has_alpha())
unfilter_impl<true, 3>(*context.bitmap, y, dummy_scanline.pointer());
else
unfilter_impl<false, 3>(*context.bitmap, y, dummy_scanline.pointer());
continue;
}
if (filter == 4) {
if (context.has_alpha())
unfilter_impl<true, 4>(*context.bitmap, y, dummy_scanline.pointer());
else
unfilter_impl<false, 4>(*context.bitmap, y, dummy_scanline.pointer());
continue;
}
}
}
static RetainPtr<GraphicsBitmap> load_png_impl(const byte* data, int data_size)
{
#ifdef PNG_STOPWATCH_DEBUG
Stopwatch sw("load_png_impl: total");
#endif
const byte* data_ptr = data;
int data_remaining = data_size;
const byte png_header[8] = { 0x89, 'P', 'N', 'G', 13, 10, 26, 10 };
if (memcmp(data, png_header, sizeof(png_header))) {
dbgprintf("Invalid PNG header\n");
return nullptr;
}
PNGLoadingContext context;
context.compressed_data.ensure_capacity(data_size);
data_ptr += sizeof(png_header);
data_remaining -= sizeof(png_header);
{
#ifdef PNG_STOPWATCH_DEBUG
Stopwatch sw("load_png_impl: read chunks");
#endif
Streamer streamer(data_ptr, data_remaining);
while (!streamer.at_end()) {
if (!process_chunk(streamer, context)) {
return nullptr;
}
}
}
{
#ifdef PNG_STOPWATCH_DEBUG
Stopwatch sw("load_png_impl: uncompress");
#endif
unsigned long srclen = context.compressed_data.size() - 6;
unsigned long destlen = context.decompression_buffer_size;
int ret = puff(context.decompression_buffer, &destlen, context.compressed_data.data() + 2, &srclen);
if (ret < 0)
return nullptr;
context.compressed_data.clear();
}
{
#ifdef PNG_STOPWATCH_DEBUG
Stopwatch sw("load_png_impl: extract scanlines");
#endif
context.scanlines.ensure_capacity(context.height);
Streamer streamer(context.decompression_buffer, context.decompression_buffer_size);
for (int y = 0; y < context.height; ++y) {
byte filter;
if (!streamer.read(filter))
return nullptr;
context.scanlines.append({ filter });
auto& scanline_buffer = context.scanlines.last().data;
if (!streamer.wrap_bytes(scanline_buffer, context.width * context.bytes_per_pixel))
return nullptr;
}
}
{
#ifdef PNG_STOPWATCH_DEBUG
Stopwatch sw("load_png_impl: create bitmap");
#endif
context.bitmap = GraphicsBitmap::create(context.has_alpha() ? GraphicsBitmap::Format::RGBA32 : GraphicsBitmap::Format::RGB32, { context.width, context.height });
}
unfilter(context);
munmap(context.decompression_buffer, context.decompression_buffer_size);
context.decompression_buffer = nullptr;
context.decompression_buffer_size = 0;
return context.bitmap;
}
static bool process_IHDR(const ByteBuffer& data, PNGLoadingContext& context)
{
if (data.size() < sizeof(PNG_IHDR))
return false;
auto& ihdr = *(const PNG_IHDR*)data.pointer();
context.width = ihdr.width;
context.height = ihdr.height;
context.bit_depth = ihdr.bit_depth;
context.color_type = ihdr.color_type;
context.compression_method = ihdr.compression_method;
context.filter_method = ihdr.filter_method;
context.interlace_method = ihdr.interlace_method;
switch (context.color_type) {
case 2:
context.bytes_per_pixel = 3;
break;
case 6:
context.bytes_per_pixel = 4;
break;
default:
ASSERT_NOT_REACHED();
}
printf("PNG: %dx%d (%d bpp)\n", context.width, context.height, context.bit_depth);
printf(" Color type: %b\n", context.color_type);
printf(" Interlace type: %b\n", context.interlace_method);
context.decompression_buffer_size = (context.width * context.height * context.bytes_per_pixel + context.height);
context.decompression_buffer = (byte*)mmap(nullptr, context.decompression_buffer_size, PROT_READ | PROT_WRITE, MAP_ANONYMOUS | MAP_PRIVATE, 0, 0);
return true;
}
static bool process_IDAT(const ByteBuffer& data, PNGLoadingContext& context)
{
context.compressed_data.append(data.pointer(), data.size());
return true;
}
static bool process_chunk(Streamer& streamer, PNGLoadingContext& context)
{
dword chunk_size;
if (!streamer.read(chunk_size)) {
printf("Bail at chunk_size\n");
return false;
}
byte chunk_type[5];
chunk_type[4] = '\0';
if (!streamer.read_bytes(chunk_type, 4)) {
printf("Bail at chunk_type\n");
return false;
}
ByteBuffer chunk_data;
if (!streamer.wrap_bytes(chunk_data, chunk_size)) {
printf("Bail at chunk_data\n");
return false;
}
dword chunk_crc;
if (!streamer.read(chunk_crc)) {
printf("Bail at chunk_crc\n");
return false;
}
printf("Chunk type: '%s', size: %u, crc: %x\n", chunk_type, chunk_size, chunk_crc);
if (!strcmp((const char*)chunk_type, "IHDR"))
return process_IHDR(chunk_data, context);
if (!strcmp((const char*)chunk_type, "IDAT"))
return process_IDAT(chunk_data, context);
return true;
}