From 9978be9ab789c61a5b5f56d884bd4507b14df93b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lukas=20Br=C3=BCbach?= Date: Tue, 20 Oct 2026 11:24:03 +0200 Subject: [PATCH] [Icons] Add SVG currentColor sentinel resolver Qt's SVG renderer has no CSS cascade, so an authored `currentColor` never resolves. Resolve the sentinel to a concrete palette color as plain bytes, before the SVG reaches QSvgRenderer. --- libcockatrice_utility/CMakeLists.txt | 2 + .../libcockatrice/utility/svg_recolor.cpp | 15 ++++++ .../libcockatrice/utility/svg_recolor.h | 26 +++++++++ tests/CMakeLists.txt | 1 + tests/svg_recolor_test.cpp | 53 +++++++++++++++++++ 5 files changed, 97 insertions(+) create mode 100644 libcockatrice_utility/libcockatrice/utility/svg_recolor.cpp create mode 100644 libcockatrice_utility/libcockatrice/utility/svg_recolor.h create mode 100644 tests/svg_recolor_test.cpp diff --git a/libcockatrice_utility/CMakeLists.txt b/libcockatrice_utility/CMakeLists.txt index 9b21a071f..a98318945 100644 --- a/libcockatrice_utility/CMakeLists.txt +++ b/libcockatrice_utility/CMakeLists.txt @@ -12,6 +12,7 @@ set(UTILITY_SOURCES libcockatrice/utility/passwordhasher.cpp libcockatrice/utility/report_categories.cpp libcockatrice/utility/server_rate_limiter.cpp + libcockatrice/utility/svg_recolor.cpp libcockatrice/utility/translation_loader.cpp libcockatrice/utility/user_data_backup.cpp libcockatrice/utility/warning_categories.cpp @@ -35,6 +36,7 @@ set(UTILITY_HEADERS libcockatrice/utility/zone_names.h libcockatrice/utility/days_years_between.h libcockatrice/utility/server_rate_limiter.h + libcockatrice/utility/svg_recolor.h libcockatrice/utility/translation_loader.h libcockatrice/utility/user_data_backup.h libcockatrice/utility/warning_categories.h diff --git a/libcockatrice_utility/libcockatrice/utility/svg_recolor.cpp b/libcockatrice_utility/libcockatrice/utility/svg_recolor.cpp new file mode 100644 index 000000000..7072c1586 --- /dev/null +++ b/libcockatrice_utility/libcockatrice/utility/svg_recolor.cpp @@ -0,0 +1,15 @@ +#include "svg_recolor.h" + +#include + +QByteArray resolveSvgColorSentinel(const QByteArray &svg, const QByteArray &color) +{ + const QString source = QString::fromUtf8(svg); + if (!source.contains(QStringLiteral("currentColor"), Qt::CaseInsensitive)) { + return svg; + } + + QString resolved = source; + resolved.replace(QStringLiteral("currentColor"), QString::fromLatin1(color), Qt::CaseInsensitive); + return resolved.toUtf8(); +} diff --git a/libcockatrice_utility/libcockatrice/utility/svg_recolor.h b/libcockatrice_utility/libcockatrice/utility/svg_recolor.h new file mode 100644 index 000000000..767d46120 --- /dev/null +++ b/libcockatrice_utility/libcockatrice/utility/svg_recolor.h @@ -0,0 +1,26 @@ +/** + * @file svg_recolor.h + * @ingroup Core + */ + +#ifndef SVG_RECOLOR_H +#define SVG_RECOLOR_H + +#include + +/** + * @brief Resolves the `currentColor` sentinel in authored SVG data. + * + * Qt's SVG renderer has no CSS cascade or ambient color context, so the + * sentinel is not resolved by Qt. Callers pass the concrete color they want + * (for example a QPalette role serialized to #RRGGBB) and receive SVG bytes + * that are safe to hand to QSvgRenderer. + * + * @param svg UTF-8 SVG source. Every `currentColor` occurrence, in any case, + * is replaced. + * @param color Concrete SVG color (for example "#ff00ff"), as ASCII bytes. + * @return The recolored SVG source; the input is left untouched. + */ +QByteArray resolveSvgColorSentinel(const QByteArray &svg, const QByteArray &color); + +#endif // SVG_RECOLOR_H diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index aac4d4818..2fd3eded9 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -177,6 +177,7 @@ cockatrice_add_test(NAME test_age_formatting SOURCES test_age_formatting.cpp LIB cockatrice_add_test(NAME user_data_backup_test SOURCES user_data_backup_test.cpp LIBS libcockatrice_utility) cockatrice_add_test(NAME password_hash_test SOURCES password_hash_test.cpp LIBS libcockatrice_utility) cockatrice_add_test(NAME report_categories_test SOURCES report_categories_test.cpp LIBS libcockatrice_utility) +cockatrice_add_test(NAME svg_recolor_test SOURCES svg_recolor_test.cpp LIBS libcockatrice_utility) cockatrice_add_test(NAME warning_categories_test SOURCES warning_categories_test.cpp LIBS libcockatrice_utility) cockatrice_add_test( NAME diff --git a/tests/svg_recolor_test.cpp b/tests/svg_recolor_test.cpp new file mode 100644 index 000000000..62990c322 --- /dev/null +++ b/tests/svg_recolor_test.cpp @@ -0,0 +1,53 @@ +#include "libcockatrice/utility/svg_recolor.h" + +#include +#include + +namespace +{ + +TEST(SvgRecolor, ReplacesEverySentinel) +{ + const QByteArray svg = R"()"; + const QByteArray result = resolveSvgColorSentinel(svg, "#ff8800"); + + EXPECT_EQ(result.count("#ff8800"), 2); + EXPECT_FALSE(result.contains("currentColor")); +} + +TEST(SvgRecolor, ReplacesSentinelRegardlessOfCase) +{ + const QByteArray svg = R"()"; + + EXPECT_EQ(resolveSvgColorSentinel(svg, "#ff8800").count("#ff8800"), 2); +} + +TEST(SvgRecolor, LeavesTheRestOfTheDocumentIntact) +{ + const QByteArray svg = + R"()"; + const QByteArray result = resolveSvgColorSentinel(svg, "#123456"); + + EXPECT_TRUE(result.contains(R"(viewBox="0 0 24 24")")); + EXPECT_TRUE(result.contains(R"(d="M0 0h24v24")")); + EXPECT_TRUE(result.contains("an ordinary comment")); +} + +TEST(SvgRecolor, DoesNotModifyTheInput) +{ + const QByteArray svg = R"()"; + const QByteArray original = svg; + + resolveSvgColorSentinel(svg, "#000000"); + + EXPECT_EQ(svg, original); +} + +TEST(SvgRecolor, NoSentinelIsANoOp) +{ + const QByteArray svg = R"()"; + + EXPECT_EQ(resolveSvgColorSentinel(svg, "#ffffff"), svg); +} + +} // namespace