[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.
This commit is contained in:
Lukas Brübach 2026-10-20 11:24:03 +02:00 • committed by GitHub
parent 0fe4fd0d8c
commit 9978be9ab7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 97 additions and 0 deletions

View file

@ -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

View file

@ -0,0 +1,15 @@
#include "svg_recolor.h"
#include <QString>
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();
}

View file

@ -0,0 +1,26 @@
/**
* @file svg_recolor.h
* @ingroup Core
*/
#ifndef SVG_RECOLOR_H
#define SVG_RECOLOR_H
#include <QByteArray>
/**
* @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

View file

@ -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

View file

@ -0,0 +1,53 @@
#include "libcockatrice/utility/svg_recolor.h"
#include <QByteArray>
#include <gtest/gtest.h>
namespace
{
TEST(SvgRecolor, ReplacesEverySentinel)
{
const QByteArray svg = R"(<svg><path fill="currentColor"/><path stroke="currentColor"/></svg>)";
const QByteArray result = resolveSvgColorSentinel(svg, "#ff8800");
EXPECT_EQ(result.count("#ff8800"), 2);
EXPECT_FALSE(result.contains("currentColor"));
}
TEST(SvgRecolor, ReplacesSentinelRegardlessOfCase)
{
const QByteArray svg = R"(<svg><path fill="CurrentColor"/><path stroke="CURRENTCOLOR"/></svg>)";
EXPECT_EQ(resolveSvgColorSentinel(svg, "#ff8800").count("#ff8800"), 2);
}
TEST(SvgRecolor, LeavesTheRestOfTheDocumentIntact)
{
const QByteArray svg =
R"(<svg viewBox="0 0 24 24"><!-- an ordinary comment --><path d="M0 0h24v24" fill="currentColor"/></svg>)";
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"(<svg><path fill="currentColor"/></svg>)";
const QByteArray original = svg;
resolveSvgColorSentinel(svg, "#000000");
EXPECT_EQ(svg, original);
}
TEST(SvgRecolor, NoSentinelIsANoOp)
{
const QByteArray svg = R"(<svg><path fill="#abcdef"/></svg>)";
EXPECT_EQ(resolveSvgColorSentinel(svg, "#ffffff"), svg);
}
} // namespace