LibGfx+LibWeb: Convert bitmap alpha type when creating ImmutableBitmaps

When decoding data into bitmaps, we end up with different alpha types
(premultiplied vs. unpremultiplied color data). Unfortunately, Skia only
seems to handle premultiplied color data well when scaling bitmaps with
an alpha channel. This might be due to Skia historically only supporting
premultiplied color blending, with unpremultiplied support having been
added more recently.

When using Skia to blend bitmaps, we need the color data to be
premultiplied. ImmutableBitmap gains a new method to enforce the alpha
type to be used, which is now used by SharedResourceRequest and
CanvasRenderingContext2D to enforce the right alpha type.

Our LibWeb tests actually had a couple of screenshot tests that exposed
the graphical glitches caused by Skia; see the big smiley faces in the
CSS backgrounds tests for example. The failing tests are now updated to
accommodate the new behavior.

Chromium and Firefox both seem to apply the same behavior; e.g. they
actively decode PNGs (which are unpremultiplied in nature) to a
premultiplied bitmap.

Fixes #3691.
This commit is contained in:
Jelle Raaijmakers 2025-02-26 16:53:54 +01:00
parent 2102546d32
commit bd2f206dbc
14 changed files with 74 additions and 2 deletions

View file

@ -245,4 +245,26 @@ bool Bitmap::visually_equals(Bitmap const& other) const
return true;
}
void Bitmap::set_alpha_type_destructive(AlphaType alpha_type)
{
if (alpha_type == m_alpha_type)
return;
if (m_alpha_type == AlphaType::Unpremultiplied) {
for (auto y = 0; y < height(); ++y) {
for (auto x = 0; x < width(); ++x)
set_pixel(x, y, get_pixel(x, y).to_premultiplied());
}
} else if (m_alpha_type == AlphaType::Premultiplied) {
for (auto y = 0; y < height(); ++y) {
for (auto x = 0; x < width(); ++x)
set_pixel(x, y, get_pixel(x, y).to_unpremultiplied());
}
} else {
VERIFY_NOT_REACHED();
}
m_alpha_type = alpha_type;
}
}

View file

@ -145,6 +145,7 @@ public:
[[nodiscard]] bool visually_equals(Bitmap const&) const;
[[nodiscard]] AlphaType alpha_type() const { return m_alpha_type; }
void set_alpha_type_destructive(AlphaType);
private:
Bitmap(BitmapFormat, AlphaType, IntSize, BackingStore const&);

View file

@ -253,6 +253,15 @@ public:
return color_with_alpha;
}
constexpr Color to_premultiplied() const
{
return Color(
red() * alpha() / 255,
green() * alpha() / 255,
blue() * alpha() / 255,
alpha());
}
constexpr Color to_unpremultiplied() const
{
if (alpha() == 0 || alpha() == 255)

View file

@ -87,6 +87,19 @@ NonnullRefPtr<ImmutableBitmap> ImmutableBitmap::create(NonnullRefPtr<Bitmap> bit
return adopt_ref(*new ImmutableBitmap(make<ImmutableBitmapImpl>(impl)));
}
NonnullRefPtr<ImmutableBitmap> ImmutableBitmap::create(NonnullRefPtr<Bitmap> bitmap, AlphaType alpha_type, ColorSpace color_space)
{
// Convert the source bitmap to the right alpha type on a mismatch. We want to do this when converting from a
// Bitmap to an ImmutableBitmap, since at that point we usually know the right alpha type to use in context.
auto source_bitmap = bitmap;
if (source_bitmap->alpha_type() != alpha_type) {
source_bitmap = MUST(bitmap->clone());
source_bitmap->set_alpha_type_destructive(alpha_type);
}
return create(source_bitmap, move(color_space));
}
NonnullRefPtr<ImmutableBitmap> ImmutableBitmap::create_snapshot_from_painting_surface(NonnullRefPtr<PaintingSurface> painting_surface)
{
ImmutableBitmapImpl impl;

View file

@ -23,6 +23,7 @@ struct ImmutableBitmapImpl;
class ImmutableBitmap final : public RefCounted<ImmutableBitmap> {
public:
static NonnullRefPtr<ImmutableBitmap> create(NonnullRefPtr<Bitmap> bitmap, ColorSpace color_space = {});
static NonnullRefPtr<ImmutableBitmap> create(NonnullRefPtr<Bitmap> bitmap, AlphaType, ColorSpace color_space = {});
static NonnullRefPtr<ImmutableBitmap> create_snapshot_from_painting_surface(NonnullRefPtr<PaintingSurface>);
~ImmutableBitmap();

View file

@ -478,7 +478,14 @@ void CanvasRenderingContext2D::put_image_data(ImageData const& image_data, float
// https://html.spec.whatwg.org/multipage/canvas.html#dom-context2d-putimagedata-common
if (auto* painter = this->painter()) {
auto dst_rect = Gfx::FloatRect(x, y, image_data.width(), image_data.height());
painter->draw_bitmap(dst_rect, Gfx::ImmutableBitmap::create(image_data.bitmap()), image_data.bitmap().rect(), Gfx::ScalingMode::NearestNeighbor, drawing_state().filters, 1.0f, Gfx::CompositingAndBlendingOperator::SourceOver);
painter->draw_bitmap(
dst_rect,
Gfx::ImmutableBitmap::create(image_data.bitmap(), Gfx::AlphaType::Unpremultiplied),
image_data.bitmap().rect(),
Gfx::ScalingMode::NearestNeighbor,
drawing_state().filters,
1.0f,
Gfx::CompositingAndBlendingOperator::SourceOver);
did_draw(dst_rect);
}
}

View file

@ -161,7 +161,7 @@ void SharedResourceRequest::handle_successful_fetch(URL::URL const& url_string,
Vector<AnimatedBitmapDecodedImageData::Frame> frames;
for (auto& frame : result.frames) {
frames.append(AnimatedBitmapDecodedImageData::Frame {
.bitmap = Gfx::ImmutableBitmap::create(*frame.bitmap, result.color_space),
.bitmap = Gfx::ImmutableBitmap::create(*frame.bitmap, Gfx::AlphaType::Premultiplied, result.color_space),
.duration = static_cast<int>(frame.duration),
});
}

View file

@ -0,0 +1,16 @@
<!DOCTYPE html>
<style>
* {
margin: 0;
}
body {
background-color: cyan;
}
</style>
<!-- To rebase:
1. Open image-unpremultiplied-data.html in Ladybird
2. Resize the window just above the width of the largest element
3. Right click > "Take Full Screenshot"
4. Update the image below:
-->
<img src="../images/image-unpremultiplied-data-ref.png">

Binary file not shown.

Before

Width:  |  Height:  |  Size: 334 KiB

After

Width:  |  Height:  |  Size: 330 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 258 KiB

After

Width:  |  Height:  |  Size: 262 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 114 KiB

After

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 590 KiB

After

Width:  |  Height:  |  Size: 589 KiB

View file

@ -0,0 +1,3 @@
<!DOCTYPE html>
<link rel="match" href="../expected/image-unpremultiplied-data-ref.html" />
<img style="width: 200px; height: 200px" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADIAAAAyCAYAAAAeP4ixAAAAxUlEQVRoge2Z0Q6EIAwEwf//Z+5pE0P0BCxlLTvvKOOSqm1KQghhRSmpzL5HtrzYyIZzttmDyUUsnvhboVeLZxyZUaGhRS5nvlOoW8RDAvTIdIl4SoBWmWaRFRKgRaZJZKUEeJI5vDYym8dEGNIA/1LZIxGmNMBdKvETYUwDXKUSJhGJsCERNi6rFnPFAnXlUvllQyJsSIQNfcazoV9dNtQOYmOvTiMI0fsFIbrxIMR85MznJ1Y1n58h1qyc6rrA8EIVYnd+9SZLMlMCtbAAAAAASUVORK5CYII=">