2025-07-16 11:39:55 +02:00
/*
* Copyright ( c ) 2025 , Jelle Raaijmakers < jelle @ ladybird . org >
*
* SPDX - License - Identifier : BSD - 2 - Clause
*/
# include "Fuzzy.h"
# include <AK/Enumerate.h>
# include <AK/Format.h>
# include <LibGfx/Bitmap.h>
namespace TestWeb {
// https://web-platform-tests.org/writing-tests/reftests.html#fuzzy-matching
2025-08-06 12:20:30 +01:00
bool fuzzy_screenshot_match ( URL : : URL const & test_url , URL : : URL const & reference , Gfx : : Bitmap const & bitmap_a , Gfx : : Bitmap const & bitmap_b ,
2025-07-16 11:39:55 +02:00
Vector < FuzzyMatch > const & fuzzy_matches )
{
2025-09-02 22:07:55 +02:00
if ( bitmap_a . width ( ) ! = bitmap_b . width ( ) | | bitmap_a . height ( ) ! = bitmap_b . height ( ) )
return false ;
2025-07-16 11:39:55 +02:00
// If the bitmaps are identical, we don't perform fuzzy matching.
auto diff = bitmap_a . diff ( bitmap_b ) ;
if ( diff . identical )
return true ;
// Find a single fuzzy config to apply.
auto fuzzy_match = fuzzy_matches . first_matching ( [ & reference ] ( FuzzyMatch const & fuzzy_match ) {
if ( fuzzy_match . reference . has_value ( ) )
return fuzzy_match . reference . value ( ) . equals ( reference ) ;
return true ;
} ) ;
2025-08-06 12:00:28 +01:00
if ( ! fuzzy_match . has_value ( ) ) {
2025-08-06 12:20:30 +01:00
warnln ( " {}: Screenshot mismatch: pixel error count {}, with maximum error {}. (No fuzzy config defined) " , test_url , diff . pixel_error_count , diff . maximum_error ) ;
2025-08-06 12:00:28 +01:00
return false ;
}
2025-07-16 11:39:55 +02:00
// Apply fuzzy matching.
auto color_error_matches = fuzzy_match - > color_value_error . contains ( diff . maximum_error ) ;
if ( ! color_error_matches )
2025-08-06 12:20:30 +01:00
warnln ( " {}: Fuzzy mismatch: maximum error {} is outside {} " , test_url , diff . maximum_error , fuzzy_match - > color_value_error ) ;
2025-07-16 11:39:55 +02:00
auto pixel_error_matches = fuzzy_match - > pixel_error_count . contains ( diff . pixel_error_count ) ;
if ( ! pixel_error_matches )
2025-08-06 12:20:30 +01:00
warnln ( " {}: Fuzzy mismatch: pixel error count {} is outside {} " , test_url , diff . pixel_error_count , fuzzy_match - > pixel_error_count ) ;
2025-07-16 11:39:55 +02:00
return color_error_matches & & pixel_error_matches ;
}
// https://web-platform-tests.org/writing-tests/reftests.html#fuzzy-matching
ErrorOr < FuzzyRange > parse_fuzzy_range ( String const & fuzzy_range )
{
auto range_parts = MUST ( fuzzy_range . split ( ' - ' ) ) ;
if ( range_parts . is_empty ( ) | | range_parts . size ( ) > 2 )
return Error : : from_string_view ( " Invalid fuzzy range format " sv ) ;
auto parse_value = [ ] ( String const & value ) - > ErrorOr < u64 > {
auto maybe_value = value . to_number < u64 > ( ) ;
if ( ! maybe_value . has_value ( ) )
return Error : : from_string_view ( " Fuzzy range value is not a valid integer " sv ) ;
return maybe_value . release_value ( ) ;
} ;
auto minimum_value = TRY ( parse_value ( range_parts [ 0 ] ) ) ;
auto maximum_value = minimum_value ;
if ( range_parts . size ( ) = = 2 )
maximum_value = TRY ( parse_value ( range_parts [ 1 ] ) ) ;
if ( minimum_value > maximum_value )
return Error : : from_string_view ( " Fuzzy range minimum is higher than its maximum " sv ) ;
return FuzzyRange { . minimum_value = minimum_value , . maximum_value = maximum_value } ;
}
// https://web-platform-tests.org/writing-tests/reftests.html#fuzzy-matching
ErrorOr < FuzzyMatch > parse_fuzzy_match ( Optional < URL : : URL const & > reference , String const & content )
{
// Match configuration values. Two formats are supported:
// * Named: "maxDifference=(#X-)#Y;totalPixels=(#X-)#Y"
// * Unnamed: "(#X-)#Y;(#X-)#Y" (maxDifference and totalPixels are assumed in this order)
auto config_parts = MUST ( content . split ( ' ; ' ) ) ;
if ( config_parts . size ( ) ! = 2 )
return Error : : from_string_view ( " Fuzzy configuration must have exactly two parameters " sv ) ;
Optional < FuzzyRange > color_value_error ;
Optional < FuzzyRange > pixel_error_count ;
for ( auto [ i , config_part ] : enumerate ( config_parts ) ) {
2025-08-06 11:51:11 +01:00
auto named_parts = MUST ( MUST ( config_part . trim_ascii_whitespace ( ) ) . split_limit ( ' = ' , 2 ) ) ;
2025-07-16 11:39:55 +02:00
if ( named_parts . is_empty ( ) )
return Error : : from_string_view ( " Fuzzy configuration value cannot be empty " sv ) ;
if ( named_parts . size ( ) = = 2 ) {
if ( named_parts [ 0 ] = = " maxDifference " sv & & ! color_value_error . has_value ( ) )
color_value_error = TRY ( parse_fuzzy_range ( named_parts [ 1 ] ) ) ;
else if ( named_parts [ 0 ] = = " totalPixels " sv & & ! pixel_error_count . has_value ( ) )
pixel_error_count = TRY ( parse_fuzzy_range ( named_parts [ 1 ] ) ) ;
else
return Error : : from_string_view ( " Invalid fuzzy configuration parameter " sv ) ;
} else {
if ( i = = 0 & & ! color_value_error . has_value ( ) )
color_value_error = TRY ( parse_fuzzy_range ( config_part ) ) ;
else if ( i = = 1 & & ! pixel_error_count . has_value ( ) )
pixel_error_count = TRY ( parse_fuzzy_range ( config_part ) ) ;
else
return Error : : from_string_view ( " Invalid fuzzy configuration parameter " sv ) ;
}
}
return FuzzyMatch {
. reference = reference . map ( [ ] ( URL : : URL const & reference ) { return reference ; } ) ,
. color_value_error = color_value_error . release_value ( ) ,
. pixel_error_count = pixel_error_count . release_value ( ) ,
} ;
}
}