mirror of
https://github.com/LadybirdBrowser/ladybird.git
synced 2024-11-21 23:20:20 +00:00
LibGfx: Implement PNG filtering on write
Is it another great upgrade to our PNG encoder like in 9aafaec259? Well, not really - it's not a 2x or 55x improvement like you saw there, but still it saves something: - a screenshot of a blank Serenity desktop dropped from about 45 KiB to 40 KiB. - re-encoding NASA photo of the Earth to PNG again saves about 25% (16.5 MiB -> 12.3 MiB), compared to not using filters. [1]: https://commons.wikimedia.org/wiki/File:The_Blue_Marble_(remastered).jpg
This commit is contained in:
parent
98a90d79de
commit
b5420b8a9a
Notes:
sideshowbarker
2024-07-17 09:32:11 +09:00
Author: https://github.com/krkk Commit: https://github.com/SerenityOS/serenity/commit/b5420b8a9a Pull-request: https://github.com/SerenityOS/serenity/pull/14537 Reviewed-by: https://github.com/awesomekling
3 changed files with 124 additions and 6 deletions
|
@ -36,6 +36,18 @@ ALWAYS_INLINE static constexpr u32x4 expand4(u32 u)
|
|||
|
||||
// Casting
|
||||
|
||||
template<typename TSrc>
|
||||
ALWAYS_INLINE static u8x4 to_u8x4(TSrc v)
|
||||
{
|
||||
return __builtin_convertvector(v, u8x4);
|
||||
}
|
||||
|
||||
template<typename TSrc>
|
||||
ALWAYS_INLINE static u16x4 to_u16x4(TSrc v)
|
||||
{
|
||||
return __builtin_convertvector(v, u16x4);
|
||||
}
|
||||
|
||||
template<typename TSrc>
|
||||
ALWAYS_INLINE static u32x4 to_u32x4(TSrc v)
|
||||
{
|
||||
|
|
|
@ -43,4 +43,14 @@ ALWAYS_INLINE u8 paeth_predictor(u8 a, u8 b, u8 c)
|
|||
return c;
|
||||
}
|
||||
|
||||
ALWAYS_INLINE AK::SIMD::u8x4 paeth_predictor(AK::SIMD::u8x4 a, AK::SIMD::u8x4 b, AK::SIMD::u8x4 c)
|
||||
{
|
||||
return AK::SIMD::u8x4 {
|
||||
paeth_predictor(a[0], b[0], c[0]),
|
||||
paeth_predictor(a[1], b[1], c[1]),
|
||||
paeth_predictor(a[2], b[2], c[2]),
|
||||
paeth_predictor(a[3], b[3], c[3]),
|
||||
};
|
||||
}
|
||||
|
||||
};
|
||||
|
|
|
@ -7,12 +7,15 @@
|
|||
*/
|
||||
|
||||
#include <AK/Concepts.h>
|
||||
#include <AK/SIMDExtras.h>
|
||||
#include <AK/String.h>
|
||||
#include <LibCompress/Zlib.h>
|
||||
#include <LibCrypto/Checksum/CRC32.h>
|
||||
#include <LibGfx/Bitmap.h>
|
||||
#include <LibGfx/PNGWriter.h>
|
||||
|
||||
#pragma GCC diagnostic ignored "-Wpsabi"
|
||||
|
||||
namespace Gfx {
|
||||
|
||||
class PNGChunk {
|
||||
|
@ -134,6 +137,24 @@ void PNGWriter::add_IEND_chunk()
|
|||
add_chunk(png_chunk);
|
||||
}
|
||||
|
||||
union [[gnu::packed]] Pixel {
|
||||
ARGB32 rgba { 0 };
|
||||
struct {
|
||||
u8 red;
|
||||
u8 green;
|
||||
u8 blue;
|
||||
u8 alpha;
|
||||
};
|
||||
AK::SIMD::u8x4 simd;
|
||||
|
||||
ALWAYS_INLINE static AK::SIMD::u8x4 gfx_to_png(Pixel pixel)
|
||||
{
|
||||
swap(pixel.red, pixel.blue);
|
||||
return pixel.simd;
|
||||
}
|
||||
};
|
||||
static_assert(AssertSize<Pixel, 4>());
|
||||
|
||||
void PNGWriter::add_IDAT_chunk(Gfx::Bitmap const& bitmap)
|
||||
{
|
||||
PNGChunk png_chunk { "IDAT" };
|
||||
|
@ -142,16 +163,91 @@ void PNGWriter::add_IDAT_chunk(Gfx::Bitmap const& bitmap)
|
|||
ByteBuffer uncompressed_block_data;
|
||||
uncompressed_block_data.ensure_capacity(bitmap.size_in_bytes() + bitmap.height());
|
||||
|
||||
Pixel const dummy_scanline[bitmap.width()] {};
|
||||
auto* scanline_minus_1 = dummy_scanline;
|
||||
|
||||
for (int y = 0; y < bitmap.height(); ++y) {
|
||||
uncompressed_block_data.append(0);
|
||||
auto* scanline = reinterpret_cast<Pixel const*>(bitmap.scanline(y));
|
||||
|
||||
struct Filter {
|
||||
PNG::FilterType type;
|
||||
ByteBuffer buffer {};
|
||||
int sum = 0;
|
||||
|
||||
void append(u8 byte)
|
||||
{
|
||||
buffer.append(byte);
|
||||
sum += static_cast<i8>(byte);
|
||||
}
|
||||
|
||||
void append(AK::SIMD::u8x4 simd)
|
||||
{
|
||||
append(simd[0]);
|
||||
append(simd[1]);
|
||||
append(simd[2]);
|
||||
append(simd[3]);
|
||||
}
|
||||
};
|
||||
|
||||
Filter none_filter { .type = PNG::FilterType::None };
|
||||
none_filter.buffer.ensure_capacity(sizeof(Pixel) * bitmap.height());
|
||||
|
||||
Filter sub_filter { .type = PNG::FilterType::Sub };
|
||||
sub_filter.buffer.ensure_capacity(sizeof(Pixel) * bitmap.height());
|
||||
|
||||
Filter up_filter { .type = PNG::FilterType::Up };
|
||||
up_filter.buffer.ensure_capacity(sizeof(Pixel) * bitmap.height());
|
||||
|
||||
Filter average_filter { .type = PNG::FilterType::Average };
|
||||
average_filter.buffer.ensure_capacity(sizeof(ARGB32) * bitmap.height());
|
||||
|
||||
Filter paeth_filter { .type = PNG::FilterType::Paeth };
|
||||
paeth_filter.buffer.ensure_capacity(sizeof(ARGB32) * bitmap.height());
|
||||
|
||||
auto pixel_x_minus_1 = Pixel::gfx_to_png(*dummy_scanline);
|
||||
auto pixel_xy_minus_1 = Pixel::gfx_to_png(*dummy_scanline);
|
||||
|
||||
for (int x = 0; x < bitmap.width(); ++x) {
|
||||
auto pixel = bitmap.get_pixel(x, y);
|
||||
uncompressed_block_data.append(pixel.red());
|
||||
uncompressed_block_data.append(pixel.green());
|
||||
uncompressed_block_data.append(pixel.blue());
|
||||
uncompressed_block_data.append(pixel.alpha());
|
||||
auto pixel = Pixel::gfx_to_png(scanline[x]);
|
||||
auto pixel_y_minus_1 = Pixel::gfx_to_png(scanline_minus_1[x]);
|
||||
|
||||
none_filter.append(pixel);
|
||||
|
||||
sub_filter.append(pixel - pixel_x_minus_1);
|
||||
|
||||
up_filter.append(pixel - pixel_y_minus_1);
|
||||
|
||||
// The sum Orig(a) + Orig(b) shall be performed without overflow (using at least nine-bit arithmetic).
|
||||
auto sum = AK::SIMD::to_u16x4(pixel_x_minus_1) + AK::SIMD::to_u16x4(pixel_y_minus_1);
|
||||
auto average = AK::SIMD::to_u8x4(sum / 2);
|
||||
average_filter.append(pixel - average);
|
||||
|
||||
paeth_filter.append(pixel - PNG::paeth_predictor(pixel_x_minus_1, pixel_y_minus_1, pixel_xy_minus_1));
|
||||
|
||||
pixel_x_minus_1 = pixel;
|
||||
pixel_xy_minus_1 = pixel_y_minus_1;
|
||||
}
|
||||
|
||||
scanline_minus_1 = scanline;
|
||||
|
||||
// 12.8 Filter selection: https://www.w3.org/TR/PNG/#12Filter-selection
|
||||
// For best compression of truecolour and greyscale images, the recommended approach
|
||||
// is adaptive filtering in which a filter is chosen for each scanline.
|
||||
// The following simple heuristic has performed well in early tests:
|
||||
// compute the output scanline using all five filters, and select the filter that gives the smallest sum of absolute values of outputs.
|
||||
// (Consider the output bytes as signed differences for this test.)
|
||||
Filter& best_filter = none_filter;
|
||||
if (abs(best_filter.sum) > abs(sub_filter.sum))
|
||||
best_filter = sub_filter;
|
||||
if (abs(best_filter.sum) > abs(up_filter.sum))
|
||||
best_filter = up_filter;
|
||||
if (abs(best_filter.sum) > abs(average_filter.sum))
|
||||
best_filter = average_filter;
|
||||
if (abs(best_filter.sum) > abs(paeth_filter.sum))
|
||||
best_filter = paeth_filter;
|
||||
|
||||
uncompressed_block_data.append(to_underlying(best_filter.type));
|
||||
uncompressed_block_data.append(best_filter.buffer);
|
||||
}
|
||||
|
||||
auto maybe_zlib_buffer = Compress::ZlibCompressor::compress_all(uncompressed_block_data);
|
||||
|
|
Loading…
Reference in a new issue