LibGfx+LibWeb: Use mipmaps for downscaling images
This changes Gfx::ScalingMode to reflect the three modes of scaling we support using Skia, which makes it a bit easier to reason about the mode to select. New is ::BilinearMipmap, which uses linear interpolation between mipmap levels to produce higher quality downscaled images. The cubic resampling options Mitchell and its sibling CatmullRom both produced weird artifacts or resulted in a worse quality than BilinearMipmap when downscaling. We might not have been using these correctly, but the new ::BilinearMipmap method seems to mirror what Chrome uses for downscaled images.
Author: https://github.com/gmta
Commit: 3f6cbeb87e
Pull-request: https://github.com/LadybirdBrowser/ladybird/pull/6792
Reviewed-by: https://github.com/kalenikaliaksandr ✅
|
|
@ -1,5 +1,6 @@
|
|||
/*
|
||||
* Copyright (c) 2024, Andreas Kling <andreas@ladybird.org>
|
||||
* Copyright (c) 2025, Jelle Raaijmakers <jelle@ladybird.org>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
|
@ -9,11 +10,10 @@
|
|||
namespace Gfx {
|
||||
|
||||
enum class ScalingMode {
|
||||
NearestNeighbor,
|
||||
SmoothPixels,
|
||||
BilinearBlend,
|
||||
BoxSampling,
|
||||
None,
|
||||
Bilinear,
|
||||
BilinearMipmap,
|
||||
NearestNeighbor,
|
||||
};
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -118,19 +118,19 @@ constexpr SkPathFillType to_skia_path_fill_type(Gfx::WindingRule winding_rule)
|
|||
VERIFY_NOT_REACHED();
|
||||
}
|
||||
|
||||
constexpr SkSamplingOptions to_skia_sampling_options(Gfx::ScalingMode scaling_mode)
|
||||
constexpr SkSamplingOptions to_skia_sampling_options(ScalingMode scaling_mode)
|
||||
{
|
||||
switch (scaling_mode) {
|
||||
case Gfx::ScalingMode::NearestNeighbor:
|
||||
case Gfx::ScalingMode::SmoothPixels:
|
||||
return SkSamplingOptions(SkFilterMode::kNearest);
|
||||
case Gfx::ScalingMode::BilinearBlend:
|
||||
case ScalingMode::None:
|
||||
return SkSamplingOptions();
|
||||
case ScalingMode::Bilinear:
|
||||
return SkSamplingOptions(SkFilterMode::kLinear);
|
||||
case Gfx::ScalingMode::BoxSampling:
|
||||
return SkSamplingOptions(SkCubicResampler::Mitchell());
|
||||
default:
|
||||
VERIFY_NOT_REACHED();
|
||||
case ScalingMode::BilinearMipmap:
|
||||
return SkSamplingOptions(SkFilterMode::kLinear, SkMipmapMode::kLinear);
|
||||
case ScalingMode::NearestNeighbor:
|
||||
return SkSamplingOptions(SkFilterMode::kNearest);
|
||||
}
|
||||
VERIFY_NOT_REACHED();
|
||||
}
|
||||
|
||||
SkPath to_skia_path(Path const& path);
|
||||
|
|
|
|||
|
|
@ -455,17 +455,19 @@ struct TextDecorationThickness {
|
|||
// FIXME: Find a better place for this helper.
|
||||
inline Gfx::ScalingMode to_gfx_scaling_mode(ImageRendering css_value, Gfx::IntRect source, Gfx::IntRect target)
|
||||
{
|
||||
if (source.size() == target.size())
|
||||
return Gfx::ScalingMode::None;
|
||||
|
||||
switch (css_value) {
|
||||
case ImageRendering::Auto:
|
||||
case ImageRendering::HighQuality:
|
||||
case ImageRendering::Smooth:
|
||||
if (target.width() < source.width() || target.height() < source.height())
|
||||
return Gfx::ScalingMode::BoxSampling;
|
||||
return Gfx::ScalingMode::BilinearBlend;
|
||||
if (target.width() < source.width() && target.height() < source.height())
|
||||
return Gfx::ScalingMode::BilinearMipmap;
|
||||
return Gfx::ScalingMode::Bilinear;
|
||||
case ImageRendering::CrispEdges:
|
||||
return Gfx::ScalingMode::NearestNeighbor;
|
||||
case ImageRendering::Pixelated:
|
||||
return Gfx::ScalingMode::SmoothPixels;
|
||||
return Gfx::ScalingMode::NearestNeighbor;
|
||||
}
|
||||
VERIFY_NOT_REACHED();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -188,7 +188,7 @@ WebIDL::ExceptionOr<void> CanvasRenderingContext2D::draw_image_internal(CanvasIm
|
|||
auto scaling_mode = Gfx::ScalingMode::NearestNeighbor;
|
||||
if (drawing_state().image_smoothing_enabled) {
|
||||
// FIXME: Honor drawing_state().image_smoothing_quality
|
||||
scaling_mode = Gfx::ScalingMode::BilinearBlend;
|
||||
scaling_mode = Gfx::ScalingMode::BilinearMipmap;
|
||||
}
|
||||
|
||||
if (auto* painter = this->painter()) {
|
||||
|
|
|
|||
|
|
@ -214,11 +214,11 @@ static ErrorOr<NonnullRefPtr<Gfx::Bitmap>> crop_to_the_source_rectangle_with_for
|
|||
// The "high" value indicates a preference for a high level of image interpolation quality. High-quality image interpolation may be more computationally expensive than lower settings.
|
||||
case Bindings::ResizeQuality::Medium:
|
||||
// The "medium" value indicates a preference for a medium level of image interpolation quality.
|
||||
scaling_passes.append(ScalingPass { .mode = Gfx::ScalingMode::BoxSampling, .width = output_width, .height = output_height });
|
||||
scaling_passes.append(ScalingPass { .mode = Gfx::ScalingMode::BilinearMipmap, .width = output_width, .height = output_height });
|
||||
break;
|
||||
case Bindings::ResizeQuality::Low:
|
||||
// The "low" value indicates a preference for a low level of image interpolation quality. Low-quality image interpolation may be more computationally efficient than higher settings.
|
||||
scaling_passes.append(ScalingPass { .mode = Gfx::ScalingMode::BilinearBlend, .width = output_width, .height = output_height });
|
||||
scaling_passes.append(ScalingPass { .mode = Gfx::ScalingMode::Bilinear, .width = output_width, .height = output_height });
|
||||
break;
|
||||
case Bindings::ResizeQuality::Pixelated: {
|
||||
// The "pixelated" value indicates a preference for scaling the image to preserve the pixelation of the original as much as possible, with minor smoothing as necessary to avoid distorting the image when the target size is not a clean multiple of the original.
|
||||
|
|
@ -237,7 +237,7 @@ static ErrorOr<NonnullRefPtr<Gfx::Bitmap>> crop_to_the_source_rectangle_with_for
|
|||
scaling_passes.append(ScalingPass { .mode = Gfx::ScalingMode::NearestNeighbor, .width = source_width * width_multiple, .height = source_height * height_multiple });
|
||||
|
||||
// then scale it the rest of the way to the target size using bilinear interpolation.
|
||||
scaling_passes.append(ScalingPass { .mode = Gfx::ScalingMode::BilinearBlend, .width = output_width, .height = output_height });
|
||||
scaling_passes.append(ScalingPass { .mode = Gfx::ScalingMode::Bilinear, .width = output_width, .height = output_height });
|
||||
} break;
|
||||
}
|
||||
for (ScalingPass& scaling_pass : scaling_passes) {
|
||||
|
|
|
|||
BIN
Tests/LibWeb/Screenshot/data/angled-stripes.png
Normal file
|
After Width: | Height: | Size: 45 KiB |
|
|
@ -8,4 +8,4 @@
|
|||
background-color: white;
|
||||
}
|
||||
</style>
|
||||
<img src="../images/canvas-filters.png">
|
||||
<img src="../images/canvas-filters-ref.png">
|
||||
|
|
|
|||
16
Tests/LibWeb/Screenshot/expected/image-downscaling-ref.html
Normal 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-downscaling-ref.png">
|
||||
|
|
@ -13,4 +13,4 @@
|
|||
2. Right click > "Take Full Screenshot"
|
||||
3. Update the image below:
|
||||
-->
|
||||
<img src="../images/object-fit-position.png">
|
||||
<img src="../images/object-fit-position-ref.png">
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 382 KiB After Width: | Height: | Size: 328 KiB |
BIN
Tests/LibWeb/Screenshot/images/canvas-filters-ref.png
Normal file
|
After Width: | Height: | Size: 257 KiB |
|
Before Width: | Height: | Size: 262 KiB |
|
Before Width: | Height: | Size: 90 KiB After Width: | Height: | Size: 88 KiB |
|
Before Width: | Height: | Size: 109 KiB After Width: | Height: | Size: 89 KiB |
BIN
Tests/LibWeb/Screenshot/images/image-downscaling-ref.png
Normal file
|
After Width: | Height: | Size: 7.5 KiB |
BIN
Tests/LibWeb/Screenshot/images/object-fit-position-ref.png
Normal file
|
After Width: | Height: | Size: 575 KiB |
|
Before Width: | Height: | Size: 589 KiB |
|
|
@ -1,7 +1,7 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<link rel="match" href="../expected/border-radius-ref.html" />
|
||||
<meta name="fuzzy" content="maxDifference=0-8;totalPixels=0-28801">
|
||||
<meta name="fuzzy" content="maxDifference=0-8;totalPixels=0-28873">
|
||||
<head>
|
||||
<style>
|
||||
.box {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
<!DOCTYPE html>
|
||||
<link rel="match" href="../expected/canvas-filters-ref.html"/>
|
||||
<meta name="fuzzy" content="maxDifference=0-6;totalPixels=0-17784">
|
||||
<meta name="fuzzy" content="maxDifference=0-6;totalPixels=0-17540">
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
<meta charset="UTF-8" />
|
||||
<title>CSS Background Tests</title>
|
||||
<link rel="match" href="../expected/css-backgrounds-ref.html" />
|
||||
<meta name="fuzzy" content="maxDifference=0-21;totalPixels=0-36803">
|
||||
<meta name="fuzzy" content="maxDifference=0-21;totalPixels=0-72032">
|
||||
<style>
|
||||
.box {
|
||||
width: 180px;
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<link rel="match" href="../expected/css-filter-ref.html" />
|
||||
<meta name="fuzzy" content="maxDifference=0-1;totalPixels=0-1063">
|
||||
<meta name="fuzzy" content="maxDifference=0-2;totalPixels=0-1396">
|
||||
<style>
|
||||
body {
|
||||
font-size: 0;
|
||||
|
|
|
|||
19
Tests/LibWeb/Screenshot/input/image-downscaling.html
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
<!DOCTYPE html>
|
||||
<link rel="match" href="../expected/image-downscaling-ref.html" />
|
||||
<meta name="fuzzy" content="maxDifference=0-1;totalPixels=0-1">
|
||||
<style>
|
||||
#a {
|
||||
width: 40px;
|
||||
}
|
||||
#b {
|
||||
height: 40px;
|
||||
width: 80px;
|
||||
}
|
||||
#c {
|
||||
height: 80px;
|
||||
width: 20px;
|
||||
}
|
||||
</style>
|
||||
<img id="a" src="../data/angled-stripes.png">
|
||||
<img id="b" src="../data/angled-stripes.png">
|
||||
<img id="c" src="../data/angled-stripes.png">
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
<!DOCTYPE html>
|
||||
<link rel="match" href="../expected/object-fit-position-ref.html"/>
|
||||
<meta name="fuzzy" content="maxDifference=0-1;totalPixels=0-316">
|
||||
<meta name="fuzzy" content="maxDifference=0-1;totalPixels=0-465">
|
||||
<style>
|
||||
.images img {
|
||||
border: 1px solid black;
|
||||
|
|
|
|||