/* * Copyright (c) 2025, Jelle Raaijmakers * * SPDX-License-Identifier: BSD-2-Clause */ #include "Fuzzy.h" #include #include #include namespace TestWeb { // https://web-platform-tests.org/writing-tests/reftests.html#fuzzy-matching bool fuzzy_screenshot_match(URL::URL const& reference, Gfx::Bitmap const& bitmap_a, Gfx::Bitmap const& bitmap_b, Vector const& fuzzy_matches) { // 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; }); if (!fuzzy_match.has_value()) return diff.identical; // Apply fuzzy matching. auto color_error_matches = fuzzy_match->color_value_error.contains(diff.maximum_error); if (!color_error_matches) warnln("Fuzzy mismatch: maximum error {} is outside {}", diff.maximum_error, fuzzy_match->color_value_error); auto pixel_error_matches = fuzzy_match->pixel_error_count.contains(diff.pixel_error_count); if (!pixel_error_matches) warnln("Fuzzy mismatch: pixel error count {} is outside {}", diff.pixel_error_count, fuzzy_match->pixel_error_count); return color_error_matches && pixel_error_matches; } // https://web-platform-tests.org/writing-tests/reftests.html#fuzzy-matching ErrorOr 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 { auto maybe_value = value.to_number(); 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 parse_fuzzy_match(Optional 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 color_value_error; Optional pixel_error_count; for (auto [i, config_part] : enumerate(config_parts)) { auto named_parts = MUST(config_part.split_limit('=', 2)); 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(), }; } }