瀏覽代碼

LibWeb: Use bitmap's alpha type instead of assuming unpremultiplied

When converting a `Gfx::Bitmap` to a Skia bitmap, we cannot assume the
color data is unpremultiplied. For example, everything canvas-related
uses premultiplied color data:

  https://html.spec.whatwg.org/multipage/canvas.html#premultiplied-alpha-and-the-2d-rendering-context

We were probably assuming unpremultiplied since that is what the PNG
decoder gives us. Since we now make `Gfx::Bitmap` identify what alpha
type is being used, we can instruct Skia a bit better :^)

Update our `EdgeFlagPathRasterizer` to use premultiplied alpha instead
of unpremultiplied so we can apply alpha correctly for path masks.

This fixes the dark borders sometimes visible when SVGs are blended
with a colored background.

This also exposed an issue with our `CanvasRenderingContext2D`, which is
supposed to hold a bitmap with premultiplied alpha internally but expose
a bitmap with unpremultiplied alpha in `CanvasImageData`. Expand our C2D
test to include the alpha channel as well.

Finally, this also exposed an off-by-one issue in
`EdgeFlagPathRasterizer` which caused the last scanlines for edges to
render incorrectly. We had some reference images which included these
corruptions (they were almost unnoticeable), so update them as well.
Jelle Raaijmakers 11 月之前
父節點
當前提交
5865cf5864

二進制
Tests/LibWeb/Screenshot/images/canvas-fillstyle-rgb.png


二進制
Tests/LibWeb/Screenshot/images/canvas-path-rect-ref.png


二進制
Tests/LibWeb/Screenshot/images/canvas-text-ref.png


二進制
Tests/LibWeb/Screenshot/images/svg-background-no-natural-size-ref.png


+ 1 - 1
Tests/LibWeb/Text/input/HTML/CanvasRenderingContext2D-get-image-data-correctness.html

@@ -12,7 +12,7 @@
 
 
         let imageData = areaCtx.getImageData(0, 0, area.width, area.height);
         let imageData = areaCtx.getImageData(0, 0, area.width, area.height);
 
 
-        if (imageData.data[0] == 0xff && imageData.data[1] == 0x80 && imageData.data[2] == 0x40)
+        if (imageData.data[0] == 0xff && imageData.data[1] == 0x80 && imageData.data[2] == 0x40 && imageData.data[3] == 0xff)
             println("PASS");
             println("PASS");
         else
         else
             println("FAIL");
             println("FAIL");

+ 11 - 0
Userland/Libraries/LibGfx/Color.h

@@ -281,6 +281,17 @@ public:
         return color_with_alpha;
         return color_with_alpha;
     }
     }
 
 
+    constexpr Color to_unpremultiplied() const
+    {
+        if (alpha() == 0 || alpha() == 255)
+            return *this;
+        return Color(
+            red() * 255 / alpha(),
+            green() * 255 / alpha(),
+            blue() * 255 / alpha(),
+            alpha());
+    }
+
     constexpr Color blend(Color source) const
     constexpr Color blend(Color source) const
     {
     {
         if (alpha() == 0 || source.alpha() == 255)
         if (alpha() == 0 || source.alpha() == 255)

+ 6 - 7
Userland/Libraries/LibGfx/EdgeFlagPathRasterizer.cpp

@@ -29,7 +29,7 @@ static Vector<Detail::Edge> prepare_edges(ReadonlySpan<FloatLine> lines, unsigne
     // The first visible y value.
     // The first visible y value.
     auto top_clip = top_clip_scanline * int(samples_per_pixel);
     auto top_clip = top_clip_scanline * int(samples_per_pixel);
     // The last visible y value.
     // The last visible y value.
-    auto bottom_clip = (bottom_clip_scanline + 1) * int(samples_per_pixel) - 1;
+    auto bottom_clip = (bottom_clip_scanline + 1) * int(samples_per_pixel);
     min_edge_y = bottom_clip;
     min_edge_y = bottom_clip;
     max_edge_y = top_clip;
     max_edge_y = top_clip;
 
 
@@ -75,9 +75,8 @@ static Vector<Detail::Edge> prepare_edges(ReadonlySpan<FloatLine> lines, unsigne
             start_x += dxdy * (top_clip - min_y);
             start_x += dxdy * (top_clip - min_y);
             min_y = top_clip;
             min_y = top_clip;
         }
         }
-        if (max_y > bottom_clip) {
+        if (max_y > bottom_clip)
             max_y = bottom_clip;
             max_y = bottom_clip;
-        }
 
 
         min_edge_y = min(min_y, min_edge_y);
         min_edge_y = min(min_y, min_edge_y);
         max_edge_y = max(max_y, max_edge_y);
         max_edge_y = max(max_y, max_edge_y);
@@ -240,8 +239,8 @@ Color EdgeFlagPathRasterizer<SamplesPerPixel>::scanline_color(int scanline, int
             return function({ offset, scanline });
             return function({ offset, scanline });
         });
         });
     if (color.alpha() == 255)
     if (color.alpha() == 255)
-        return color.with_alpha(alpha);
-    return color.with_alpha(color.alpha() * alpha / 255);
+        return color.with_alpha(alpha, AlphaType::Premultiplied);
+    return color.with_alpha(color.alpha() * alpha / 255, AlphaType::Premultiplied);
 }
 }
 
 
 template<unsigned SamplesPerPixel>
 template<unsigned SamplesPerPixel>
@@ -309,7 +308,7 @@ auto EdgeFlagPathRasterizer<SamplesPerPixel>::accumulate_even_odd_scanline(EdgeE
     SampleType sample = init;
     SampleType sample = init;
     VERIFY(edge_extent.min_x >= 0);
     VERIFY(edge_extent.min_x >= 0);
     VERIFY(edge_extent.max_x < static_cast<int>(m_scanline.size()));
     VERIFY(edge_extent.max_x < static_cast<int>(m_scanline.size()));
-    for (int x = edge_extent.min_x; x <= edge_extent.max_x; x += 1) {
+    for (int x = edge_extent.min_x; x <= edge_extent.max_x; x++) {
         sample ^= m_scanline.data()[x];
         sample ^= m_scanline.data()[x];
         sample_callback(x, sample);
         sample_callback(x, sample);
         m_scanline.data()[x] = 0;
         m_scanline.data()[x] = 0;
@@ -323,7 +322,7 @@ auto EdgeFlagPathRasterizer<SamplesPerPixel>::accumulate_non_zero_scanline(EdgeE
     NonZeroAcc acc = init;
     NonZeroAcc acc = init;
     VERIFY(edge_extent.min_x >= 0);
     VERIFY(edge_extent.min_x >= 0);
     VERIFY(edge_extent.max_x < static_cast<int>(m_scanline.size()));
     VERIFY(edge_extent.max_x < static_cast<int>(m_scanline.size()));
-    for (int x = edge_extent.min_x; x <= edge_extent.max_x; x += 1) {
+    for (int x = edge_extent.min_x; x <= edge_extent.max_x; x++) {
         if (auto edges = m_scanline.data()[x]) {
         if (auto edges = m_scanline.data()[x]) {
             // We only need to process the windings when we hit some edges.
             // We only need to process the windings when we hit some edges.
             for (auto y_sub = 0u; y_sub < SamplesPerPixel; y_sub++) {
             for (auto y_sub = 0u; y_sub < SamplesPerPixel; y_sub++) {

+ 6 - 1
Userland/Libraries/LibWeb/HTML/CanvasRenderingContext2D.cpp

@@ -359,10 +359,15 @@ WebIDL::ExceptionOr<JS::GCPtr<ImageData>> CanvasRenderingContext2D::get_image_da
 
 
     // 6. Set the pixel values of imageData to be the pixels of this's output bitmap in the area specified by the source rectangle in the bitmap's coordinate space units, converted from this's color space to imageData's colorSpace using 'relative-colorimetric' rendering intent.
     // 6. Set the pixel values of imageData to be the pixels of this's output bitmap in the area specified by the source rectangle in the bitmap's coordinate space units, converted from this's color space to imageData's colorSpace using 'relative-colorimetric' rendering intent.
     // FIXME: Can't use a Gfx::Painter + blit() here as it doesn't support ImageData bitmap's RGBA8888 format.
     // FIXME: Can't use a Gfx::Painter + blit() here as it doesn't support ImageData bitmap's RGBA8888 format.
+    // NOTE: Internally we must use premultiplied alpha, but ImageData should hold unpremultiplied alpha. This conversion
+    //       might result in a loss of precision, but is according to spec.
+    //       See: https://html.spec.whatwg.org/multipage/canvas.html#premultiplied-alpha-and-the-2d-rendering-context
+    ASSERT(bitmap.alpha_type() == Gfx::AlphaType::Premultiplied);
+    ASSERT(image_data->bitmap().alpha_type() == Gfx::AlphaType::Unpremultiplied);
     for (int target_y = 0; target_y < source_rect_intersected.height(); ++target_y) {
     for (int target_y = 0; target_y < source_rect_intersected.height(); ++target_y) {
         for (int target_x = 0; target_x < source_rect_intersected.width(); ++target_x) {
         for (int target_x = 0; target_x < source_rect_intersected.width(); ++target_x) {
             auto pixel = bitmap.get_pixel(target_x + x, target_y + y);
             auto pixel = bitmap.get_pixel(target_x + x, target_y + y);
-            image_data->bitmap().set_pixel(target_x, target_y, pixel);
+            image_data->bitmap().set_pixel(target_x, target_y, pixel.to_unpremultiplied());
         }
         }
     }
     }
 
 

+ 1 - 1
Userland/Libraries/LibWeb/HTML/HTMLCanvasElement.cpp

@@ -212,7 +212,7 @@ bool HTMLCanvasElement::create_bitmap(size_t minimum_width, size_t minimum_heigh
         return false;
         return false;
     }
     }
     if (!m_bitmap || m_bitmap->size() != size) {
     if (!m_bitmap || m_bitmap->size() != size) {
-        auto bitmap_or_error = Gfx::Bitmap::create(Gfx::BitmapFormat::BGRA8888, size);
+        auto bitmap_or_error = Gfx::Bitmap::create(Gfx::BitmapFormat::BGRA8888, Gfx::AlphaType::Premultiplied, size);
         if (bitmap_or_error.is_error())
         if (bitmap_or_error.is_error())
             return false;
             return false;
         m_bitmap = bitmap_or_error.release_value_but_fixme_should_propagate_errors();
         m_bitmap = bitmap_or_error.release_value_but_fixme_should_propagate_errors();

+ 1 - 1
Userland/Libraries/LibWeb/HTML/ImageData.cpp

@@ -30,7 +30,7 @@ WebIDL::ExceptionOr<JS::NonnullGCPtr<ImageData>> ImageData::create(JS::Realm& re
     // 2. Initialize this given sw, sh, and settings set to settings.
     // 2. Initialize this given sw, sh, and settings set to settings.
     // 3. Initialize the image data of this to transparent black.
     // 3. Initialize the image data of this to transparent black.
     auto data = TRY(JS::Uint8ClampedArray::create(realm, sw * sh * 4));
     auto data = TRY(JS::Uint8ClampedArray::create(realm, sw * sh * 4));
-    auto bitmap = TRY_OR_THROW_OOM(vm, Gfx::Bitmap::create_wrapper(Gfx::BitmapFormat::RGBA8888, Gfx::AlphaType::Premultiplied, Gfx::IntSize(sw, sh), sw * sizeof(u32), data->data().data()));
+    auto bitmap = TRY_OR_THROW_OOM(vm, Gfx::Bitmap::create_wrapper(Gfx::BitmapFormat::RGBA8888, Gfx::AlphaType::Unpremultiplied, Gfx::IntSize(sw, sh), sw * sizeof(u32), data->data().data()));
 
 
     return realm.heap().allocate<ImageData>(realm, realm, bitmap, data);
     return realm.heap().allocate<ImageData>(realm, realm, bitmap, data);
 }
 }

+ 2 - 1
Userland/Libraries/LibWeb/Painting/DisplayListPlayerSkia.cpp

@@ -331,7 +331,8 @@ static SkColorType to_skia_color_type(Gfx::BitmapFormat format)
 static SkBitmap to_skia_bitmap(Gfx::Bitmap const& bitmap)
 static SkBitmap to_skia_bitmap(Gfx::Bitmap const& bitmap)
 {
 {
     SkColorType color_type = to_skia_color_type(bitmap.format());
     SkColorType color_type = to_skia_color_type(bitmap.format());
-    SkImageInfo image_info = SkImageInfo::Make(bitmap.width(), bitmap.height(), color_type, kUnpremul_SkAlphaType);
+    SkAlphaType alpha_type = bitmap.alpha_type() == Gfx::AlphaType::Premultiplied ? kPremul_SkAlphaType : kUnpremul_SkAlphaType;
+    SkImageInfo image_info = SkImageInfo::Make(bitmap.width(), bitmap.height(), color_type, alpha_type);
     SkBitmap sk_bitmap;
     SkBitmap sk_bitmap;
     sk_bitmap.setInfo(image_info);
     sk_bitmap.setInfo(image_info);
 
 

+ 1 - 1
Userland/Libraries/LibWeb/SVG/SVGDecodedImageData.cpp

@@ -87,7 +87,7 @@ void SVGDecodedImageData::visit_edges(Cell::Visitor& visitor)
 
 
 RefPtr<Gfx::Bitmap> SVGDecodedImageData::render(Gfx::IntSize size) const
 RefPtr<Gfx::Bitmap> SVGDecodedImageData::render(Gfx::IntSize size) const
 {
 {
-    auto bitmap = Gfx::Bitmap::create(Gfx::BitmapFormat::BGRA8888, size).release_value_but_fixme_should_propagate_errors();
+    auto bitmap = Gfx::Bitmap::create(Gfx::BitmapFormat::BGRA8888, Gfx::AlphaType::Premultiplied, size).release_value_but_fixme_should_propagate_errors();
     VERIFY(m_document->navigable());
     VERIFY(m_document->navigable());
     m_document->navigable()->set_viewport_size(size.to_type<CSSPixels>());
     m_document->navigable()->set_viewport_size(size.to_type<CSSPixels>());
     m_document->update_layout();
     m_document->update_layout();