LibWeb: Prevent paths thinner than 1px from disappearing

SVGs are rendered with subpixel precision. As such it can happen that
paths are rendered with less than 1px width or height and that they can
have a bounding box thinner than 1px. Due to an optimization such paths
were ignored when painting because their bounding box was incorrectly
calculated to be empty.

As a result horizontal or vertical lines inside SVGs were missing if:
* The SVG is displayed at viewbox size but the lines are defined with
  less than 1px.
* The SVG contians 1px-thin lines, but is displayed at a size smaller
  than viewbox size.

To prevent this, the bounding box of the path is now enlarged to contain
all pixels that are partially affected.
This commit is contained in:
InvalidUsernameException 2025-01-06 23:44:53 +01:00 committed by Sam Atkins
parent b08db3dc1e
commit c790cf2194
Notes: github-actions[bot] 2025-01-07 09:06:05 +00:00
4 changed files with 85 additions and 12 deletions

View file

@ -49,11 +49,12 @@ void DisplayListRecorder::fill_rect(Gfx::IntRect const& rect, Color color)
void DisplayListRecorder::fill_path(FillPathUsingColorParams params)
{
auto aa_translation = params.translation.value_or(Gfx::FloatPoint {});
auto path_bounding_rect = params.path.bounding_box().translated(aa_translation).to_type<int>();
if (path_bounding_rect.is_empty())
auto path_bounding_rect = params.path.bounding_box().translated(aa_translation);
auto path_bounding_int_rect = enclosing_int_rect(path_bounding_rect);
if (path_bounding_int_rect.is_empty())
return;
append(FillPathUsingColor {
.path_bounding_rect = path_bounding_rect,
.path_bounding_rect = path_bounding_int_rect,
.path = move(params.path),
.color = params.color,
.winding_rule = params.winding_rule,
@ -64,11 +65,12 @@ void DisplayListRecorder::fill_path(FillPathUsingColorParams params)
void DisplayListRecorder::fill_path(FillPathUsingPaintStyleParams params)
{
auto aa_translation = params.translation.value_or(Gfx::FloatPoint {});
auto path_bounding_rect = params.path.bounding_box().translated(aa_translation).to_type<int>();
if (path_bounding_rect.is_empty())
auto path_bounding_rect = params.path.bounding_box().translated(aa_translation);
auto path_bounding_int_rect = enclosing_int_rect(path_bounding_rect);
if (path_bounding_int_rect.is_empty())
return;
append(FillPathUsingPaintStyle {
.path_bounding_rect = path_bounding_rect,
.path_bounding_rect = path_bounding_int_rect,
.path = move(params.path),
.paint_style = params.paint_style,
.winding_rule = params.winding_rule,
@ -80,10 +82,11 @@ void DisplayListRecorder::fill_path(FillPathUsingPaintStyleParams params)
void DisplayListRecorder::stroke_path(StrokePathUsingColorParams params)
{
auto aa_translation = params.translation.value_or(Gfx::FloatPoint {});
auto path_bounding_rect = params.path.bounding_box().translated(aa_translation).to_type<int>();
auto path_bounding_rect = params.path.bounding_box().translated(aa_translation);
// Increase path bounding box by `thickness` to account for stroke.
path_bounding_rect.inflate(params.thickness, params.thickness);
if (path_bounding_rect.is_empty())
auto path_bounding_int_rect = enclosing_int_rect(path_bounding_rect);
if (path_bounding_int_rect.is_empty())
return;
append(StrokePathUsingColor {
.cap_style = params.cap_style,
@ -91,7 +94,7 @@ void DisplayListRecorder::stroke_path(StrokePathUsingColorParams params)
.miter_limit = params.miter_limit,
.dash_array = move(params.dash_array),
.dash_offset = params.dash_offset,
.path_bounding_rect = path_bounding_rect,
.path_bounding_rect = path_bounding_int_rect,
.path = move(params.path),
.color = params.color,
.thickness = params.thickness,
@ -102,10 +105,11 @@ void DisplayListRecorder::stroke_path(StrokePathUsingColorParams params)
void DisplayListRecorder::stroke_path(StrokePathUsingPaintStyleParams params)
{
auto aa_translation = params.translation.value_or(Gfx::FloatPoint {});
auto path_bounding_rect = params.path.bounding_box().translated(aa_translation).to_type<int>();
auto path_bounding_rect = params.path.bounding_box().translated(aa_translation);
// Increase path bounding box by `thickness` to account for stroke.
path_bounding_rect.inflate(params.thickness, params.thickness);
if (path_bounding_rect.is_empty())
auto path_bounding_int_rect = enclosing_int_rect(path_bounding_rect);
if (path_bounding_int_rect.is_empty())
return;
append(StrokePathUsingPaintStyle {
.cap_style = params.cap_style,
@ -113,7 +117,7 @@ void DisplayListRecorder::stroke_path(StrokePathUsingPaintStyleParams params)
.miter_limit = params.miter_limit,
.dash_array = move(params.dash_array),
.dash_offset = params.dash_offset,
.path_bounding_rect = path_bounding_rect,
.path_bounding_rect = path_bounding_int_rect,
.path = move(params.path),
.paint_style = params.paint_style,
.thickness = params.thickness,

View file

@ -0,0 +1,12 @@
<!DOCTYPE html>
<html>
<head>
<style>
* { margin: 0; }
body { background-color: white; }
</style>
</head>
<body>
<img src="../images/svg-paths-cardinal-directions-less-than-1px-wide-ref.png">
</body>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 200 B

View file

@ -0,0 +1,57 @@
<!DOCTYPE html>
<html>
<head>
<link rel="match" href="../expected/svg-paths-cardinal-directions-less-than-1px-wide-ref.html"/>
<style>
img, svg {
width: 24px;
height: 24px;
}
</style>
</head>
<body>
<!-- horizontal between two pixels -->
<svg viewBox="0 0 48 48" xmlns="http://www.w3.org/2000/svg">
<path d="M4 23.5h40v1h-40z" fill="currentColor"/>
</svg>
<svg viewBox="0 0 48 48" xmlns="http://www.w3.org/2000/svg">
<path d="M4 24h40" stroke="currentColor" stroke-width="1"/>
</svg>
<svg viewBox="0 0 48 48" xmlns="http://www.w3.org/2000/svg">
<line x1="4" y1="24" x2="44" y2="24" stroke="currentColor" stroke-width="1"/>
</svg>
<br>
<!-- horizontal on a single pixel -->
<svg viewBox="0 0 48 48" xmlns="http://www.w3.org/2000/svg">
<path d="M4 24h40v1h-40z" fill="currentColor"/>
</svg>
<svg viewBox="0 0 48 48" xmlns="http://www.w3.org/2000/svg">
<path d="M4 24.5h40" stroke="currentColor" stroke-width="1"/>
</svg>
<svg viewBox="0 0 48 48" xmlns="http://www.w3.org/2000/svg">
<line x1="4" y1="24.5" x2="44" y2="24.5" stroke="currentColor" stroke-width="1"/>
</svg>
<br>
<!-- vertical between two pixels -->
<svg viewBox="0 0 48 48" xmlns="http://www.w3.org/2000/svg">
<path d="M23.5 4v40h1v-40z" fill="currentColor"/>
</svg>
<svg viewBox="0 0 48 48" xmlns="http://www.w3.org/2000/svg">
<path d="M24 4v40" stroke="currentColor" stroke-width="1"/>
</svg>
<svg viewBox="0 0 48 48" xmlns="http://www.w3.org/2000/svg">
<line x1="24" y1="4" x2="24" y2="44" stroke="currentColor" stroke-width="1"/>
</svg>
<br>
<!-- vertical on a single pixel -->
<svg viewBox="0 0 48 48" xmlns="http://www.w3.org/2000/svg">
<path d="M24 4v40h1v-40z" fill="currentColor"/>
</svg>
<svg viewBox="0 0 48 48" xmlns="http://www.w3.org/2000/svg">
<path d="M24.5 4v40" stroke="currentColor" stroke-width="1"/>
</svg>
<svg viewBox="0 0 48 48" xmlns="http://www.w3.org/2000/svg">
<line x1="24.5" y1="4" x2="24.5" y2="44" stroke="currentColor" stroke-width="1"/>
</svg>
</body>
</html>