mirror of
https://github.com/LadybirdBrowser/ladybird.git
synced 2024-09-11 13:36:13 +03:00
LibGfx/WebPWriter: Implement color indexing transform
If an image has 256 or fewer colors, WebP/Lossless allows storing the colors in a helper image, and then storing just indexes into that helper image in the main image's green channel, while setting r, b, and a of the main image to 0. Since constant-color channels need to space to store in WebP, this reduces storage needed to 1/4th (if alpha is used) or 1/3rd (if alpha is constant across the image). If an image has <= 16 colors, WebP lossless files pack multiple color table indexes into a single pixel's green channel, further reducing file size. This pixel packing is not yet implemented in this commit. GIFs can store at most 256 colors per frame, so animated gifs often have 256 or fewer colors, making this effective when transcoding gifs. (WebP also has a "subtract green" transform, which can be used to need to store just a single channel for grayscale images, without having to store a color table. That's not yet implemented -- for now, we'll now store grayscale images using this color indexing transform instead, which wastes to storage for the color table.) (If an image has <= 256 colors but all these colors use only a single channel, then storing a color table for these colors is also wasteful, at least if the image has > 16 colors too. That's rare in practice, but maybe we can add code for it later on.) (WebP also has a "color cache" feature where the last few used colors can be referenced using very few bits. This is what the webp spec says is similar to palettes as well. We don't implement color cache writing support yet either; maybe it's better than using a color indexing transform for some inputs.) Some numbers on my test files: sunset-retro.png: No performance or binary size impact. The input quickly uses more than 256 colors. giphy.gif (184k): 4.1M -> 3.9M, 95.5 ms ± 4.9 ms -> 106.4 ms ± 5.3 ms Most frames use more than 256 colors, but just barely. So fairly expensive runtime wise, with just a small win. (See comment on #24454 for the previous 4.9 MiB -> 4.1 MiB drop.) 7z7c.gif (11K): 118K -> 40K Every frame has less than 256 colors (but more than 16, so no packing), and so we can cut filesize roughly to 1/3rd: We only need to store an index per channel. From 10.7x as large as the input to 3.6x as large.
This commit is contained in:
parent
633f509eb4
commit
cae672e1f9
Notes:
sideshowbarker
2024-07-17 08:35:21 +09:00
Author: https://github.com/nico Commit: https://github.com/SerenityOS/serenity/commit/cae672e1f9 Pull-request: https://github.com/SerenityOS/serenity/pull/24483 Reviewed-by: https://github.com/gmta ✅
@ -9,7 +9,9 @@
|
||||
#include <AK/BitStream.h>
|
||||
#include <AK/Debug.h>
|
||||
#include <AK/Endian.h>
|
||||
#include <AK/HashTable.h>
|
||||
#include <AK/MemoryStream.h>
|
||||
#include <AK/QuickSort.h>
|
||||
#include <LibCompress/DeflateTables.h>
|
||||
#include <LibCompress/Huffman.h>
|
||||
#include <LibGfx/Bitmap.h>
|
||||
@ -320,15 +322,91 @@ static ErrorOr<void> write_VP8L_coded_image(ImageKind image_kind, LittleEndianOu
|
||||
return {};
|
||||
}
|
||||
|
||||
static ARGB32 sub_argb32(ARGB32 a, ARGB32 b)
|
||||
{
|
||||
auto a_color = Color::from_argb(a);
|
||||
auto b_color = Color::from_argb(b);
|
||||
return Color(a_color.red() - b_color.red(),
|
||||
a_color.green() - b_color.green(),
|
||||
a_color.blue() - b_color.blue(),
|
||||
a_color.alpha() - b_color.alpha())
|
||||
.value();
|
||||
}
|
||||
|
||||
static ErrorOr<NonnullRefPtr<Bitmap>> maybe_write_color_indexing_transform(LittleEndianOutputBitStream& bit_stream, NonnullRefPtr<Bitmap> bitmap, IsOpaque& is_fully_opaque)
|
||||
{
|
||||
// https://developers.google.com/speed/webp/docs/webp_lossless_bitstream_specification#44_color_indexing_transform
|
||||
unsigned color_table_size = 0;
|
||||
HashTable<ARGB32> seen_colors;
|
||||
for (ARGB32 pixel : *bitmap) {
|
||||
auto result = seen_colors.set(pixel);
|
||||
if (result == HashSetResult::InsertedNewEntry) {
|
||||
++color_table_size;
|
||||
if (color_table_size > 256)
|
||||
break;
|
||||
}
|
||||
}
|
||||
dbgln_if(WEBP_DEBUG, "WebP: Image has {}{} colors", color_table_size > 256 ? ">= " : "", color_table_size);
|
||||
|
||||
// If the image has a single color, the huffman table can encode it in 0 bits and color indexing does not help.
|
||||
// FIXME: If all colors use just a single channel, color indexing does not help either.
|
||||
if (color_table_size <= 1 || color_table_size > 256)
|
||||
return bitmap;
|
||||
|
||||
if (color_table_size <= 16) {
|
||||
// FIXME: Implement pixel bundling.
|
||||
dbgln_if(WEBP_DEBUG, "WebP: FIXME: Not writing color index because pixel bundling is not yet implemented");
|
||||
return bitmap;
|
||||
}
|
||||
|
||||
TRY(bit_stream.write_bits(1u, 1u)); // Transform present.
|
||||
TRY(bit_stream.write_bits(static_cast<unsigned>(COLOR_INDEXING_TRANSFORM), 2u));
|
||||
|
||||
// "int color_table_size = ReadBits(8) + 1;"
|
||||
TRY(bit_stream.write_bits(color_table_size - 1, 8u));
|
||||
|
||||
// Store color index to bit stream.
|
||||
Vector<ARGB32, 256> colors;
|
||||
for (ARGB32 color : seen_colors)
|
||||
colors.append(color);
|
||||
quick_sort(colors.begin(), colors.end());
|
||||
|
||||
// "The color table is stored using the image storage format itself." [...]
|
||||
// "The color table is always subtraction-coded to reduce image entropy."
|
||||
auto color_index_bitmap = TRY(Bitmap::create(BitmapFormat::BGRA8888, { static_cast<int>(color_table_size), 1 }));
|
||||
color_index_bitmap->set_pixel(0, 0, Color::from_argb(colors[0]));
|
||||
for (unsigned i = 1; i < color_table_size; ++i)
|
||||
color_index_bitmap->set_pixel(i, 0, Color::from_argb(sub_argb32(colors[i], colors[i - 1])));
|
||||
TRY(write_VP8L_coded_image(ImageKind::EntropyCoded, bit_stream, *color_index_bitmap, is_fully_opaque));
|
||||
|
||||
// Return a new bitmap with the color indexing transform applied.
|
||||
HashMap<ARGB32, u8> color_index_map;
|
||||
for (unsigned i = 0; i < color_table_size; ++i)
|
||||
color_index_map.set(colors[i], i);
|
||||
auto new_bitmap = TRY(Bitmap::create(BitmapFormat::BGRx8888, bitmap->size()));
|
||||
for (int y = 0; y < bitmap->height(); ++y) {
|
||||
for (int x = 0; x < bitmap->width(); ++x) {
|
||||
auto pixel = bitmap->get_pixel(x, y);
|
||||
auto result = color_index_map.get(pixel.value());
|
||||
VERIFY(result.has_value());
|
||||
// "The indexing is done based on the green component of the ARGB color."
|
||||
new_bitmap->set_pixel(x, y, Color(0, result.value(), 0));
|
||||
}
|
||||
}
|
||||
|
||||
return new_bitmap;
|
||||
}
|
||||
|
||||
static ErrorOr<void> write_VP8L_image_data(Stream& stream, NonnullRefPtr<Bitmap> bitmap, IsOpaque& is_fully_opaque)
|
||||
{
|
||||
LittleEndianOutputBitStream bit_stream { MaybeOwned<Stream>(stream) };
|
||||
|
||||
// image-stream = optional-transform spatially-coded-image
|
||||
// optional-transform = (%b1 transform optional-transform) / %b0
|
||||
TRY(bit_stream.write_bits(0u, 1u)); // No transform for now.
|
||||
bitmap = TRY(maybe_write_color_indexing_transform(bit_stream, bitmap, is_fully_opaque));
|
||||
TRY(bit_stream.write_bits(0u, 1u)); // No further transforms for now.
|
||||
|
||||
TRY(write_VP8L_coded_image(ImageKind::SpatiallyCoded, bit_stream, bitmap, is_fully_opaque));
|
||||
TRY(write_VP8L_coded_image(ImageKind::SpatiallyCoded, bit_stream, *bitmap, is_fully_opaque));
|
||||
|
||||
// FIXME: Make ~LittleEndianOutputBitStream do this, or make it VERIFY() that it has happened at least.
|
||||
TRY(bit_stream.align_to_byte_boundary());
|
||||
|
Loading…
Reference in New Issue
Block a user