From ceef749ac3e66d2e6afbf298f8bfbe66387af436 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lukas=20Br=C3=BCbach?= <44814898+BruebachL@users.noreply.github.com> Date: Fri, 6 Nov 2026 22:45:32 +0100 Subject: [PATCH] [Icons] Render authored theme SVG art without the SVG image plugin `QPixmap("theme:file.svg")` needs Qt's SVG *image-format* plugin. Debian builds that as the separate `qt6-svg-plugins` package, which is only a Recommends of `qt6-svg-dev`, so a `--no-install-recommends` install (CI, minimal user installs) does not have it. Authored theme art then loaded as a null pixmap: the two authored-art render tests failed on Debian 13 and on the Ubuntu sanitize leg while passing on a developer machine whose Qt bundles the format. Render authored SVG assets through QSvgRenderer instead, which is already linked for the sentinel path and needs no plugin. Raster assets keep the QPixmap loader. The test suite now hides the plugin directories, so the dependency cannot come back unnoticed on platforms that ship the plugin. --- cockatrice/src/interface/theme_icon.cpp | 18 +++++- .../theme_icon_render_test.cpp | 56 +++++++++++++++++++ 2 files changed, 71 insertions(+), 3 deletions(-) diff --git a/cockatrice/src/interface/theme_icon.cpp b/cockatrice/src/interface/theme_icon.cpp index d85e84f87..9c04929ba 100644 --- a/cockatrice/src/interface/theme_icon.cpp +++ b/cockatrice/src/interface/theme_icon.cpp @@ -256,9 +256,21 @@ private: QPixmap fromAsset(const QSize &pixelSize, QIcon::Mode mode) const { - QPixmap pixmap(QStringLiteral("theme:") + resolvedPath); - if (!pixelSize.isEmpty() && pixmap.size() != pixelSize) { - pixmap = pixmap.scaled(pixelSize, Qt::KeepAspectRatio, Qt::SmoothTransformation); + QPixmap pixmap; + if (resolvedAssetIsSvg) { + // Render authored SVG art through QSvgRenderer rather than QPixmap. + // Loading an `.svg` through QPixmap needs Qt's SVG *image-format* + // plugin, which is a separate package on Linux distributions and is + // absent from minimal installs and from CI (it is a Recommends of + // qt6-svg-dev, so `--no-install-recommends` drops it). Art a theme + // ships must not disappear because of a packaging choice, and the + // renderer is already linked for the sentinel path. + pixmap = renderSvg(resolvedSvg, pixelSize.isEmpty() ? intrinsicSize() : pixelSize, 1.0); + } else { + pixmap = QPixmap(QStringLiteral("theme:") + resolvedPath); + if (!pixelSize.isEmpty() && pixmap.size() != pixelSize) { + pixmap = pixmap.scaled(pixelSize, Qt::KeepAspectRatio, Qt::SmoothTransformation); + } } if (mode == QIcon::Disabled && !pixmap.isNull()) { pixmap = QApplication::style()->generatedIconPixmap(QIcon::Disabled, pixmap, nullptr); diff --git a/tests/theme_migration/theme_icon_render_test.cpp b/tests/theme_migration/theme_icon_render_test.cpp index c03b91703..f9f4d669c 100644 --- a/tests/theme_migration/theme_icon_render_test.cpp +++ b/tests/theme_migration/theme_icon_render_test.cpp @@ -8,6 +8,7 @@ #include #include #include +#include #include #include #include @@ -478,6 +479,61 @@ TEST_F(ThemeIconRenderTest, SentinelSvgBadgeMatchesTheEngine) EXPECT_GT(countOpaquePixels(pixmap, ThemeIconBadge::glyphColor(tone)), 0); } +/** + * @brief Hides Qt's plugin directories while it lives, then restores them. + * + * Only the image-format plugins matter here: the platform plugin the fixture + * needs is already loaded by the time a test runs. + */ +class ScopedPluginPathHiding +{ +public: + ScopedPluginPathHiding() : saved(QCoreApplication::libraryPaths()) + { + QCoreApplication::setLibraryPaths({}); + } + ~ScopedPluginPathHiding() + { + QCoreApplication::setLibraryPaths(saved); + } + ScopedPluginPathHiding(const ScopedPluginPathHiding &) = delete; + ScopedPluginPathHiding &operator=(const ScopedPluginPathHiding &) = delete; + +private: + QStringList saved; +}; + +/** + * @brief Authored SVG art must not depend on Qt's SVG image-format plugin. + * + * Loading an `.svg` through QPixmap needs the separate `qsvg` image plugin. + * Distributions that build Qt with the plugin split out (Debian, and therefore + * CI) ship it in its own package, and a `--no-install-recommends` install does + * not have it, so authored theme art rendered blank there while working on a + * developer machine whose Qt bundles the format. Hiding the plugin directories + * reproduces that install. + */ +TEST_F(ThemeIconRenderTest, AuthoredArtDoesNotNeedTheSvgImagePlugin) +{ + QTemporaryDir themeDir; + ASSERT_TRUE(themeDir.isValid()); + QFile file(QDir(themeDir.path()).absoluteFilePath(QStringLiteral("plain.svg"))); + ASSERT_TRUE(file.open(QIODevice::WriteOnly)); + file.write(svgWith("#336699")); + file.close(); + QDir::addSearchPath(QStringLiteral("theme"), themeDir.path()); + + // The offscreen platform plugin is already loaded by the fixture, so an + // empty library path only removes the image-format plugins. + const ScopedPluginPathHiding hidePlugins; + + const QIcon icon = loadThemedIcon(QStringLiteral("plain")); + const QPixmap pixmap = icon.pixmap(QSize(32, 32)); + + ASSERT_FALSE(pixmap.isNull()) << "authored SVG art went through the QPixmap loader"; + EXPECT_GT(countOpaquePixels(pixmap, QColor(QStringLiteral("#336699"))), 0); +} + /** @brief Badge never paints authored art: the asset keeps its own colors. */ TEST_F(ThemeIconRenderTest, BadgeOnAuthoredArtUsesTheAssetPath) {