Qt: Add ThemeSVGIconEngine to replace use of Qt SVG

Uses plutosvg to render icons with a different colour scheme dependent
on the text colour. No more duplicating all the icons.
This commit is contained in:
Stenzek
2026-05-26 22:11:14 +10:00
parent 68da2ea9f5
commit 4cf5f38a8c
11 changed files with 527 additions and 9 deletions
+2 -2
View File
@@ -119,10 +119,10 @@
<!--Copy the needed dlls-->
<ItemGroup>
<QtLibNames Include="Qt6Core$(QtLibSuffix);Qt6Gui$(QtLibSuffix);Qt6Widgets$(QtLibSuffix);Qt6Svg$(QtLibSuffix)" />
<QtLibNames Include="Qt6Core$(QtLibSuffix);Qt6Gui$(QtLibSuffix);Qt6Widgets$(QtLibSuffix)" />
<QtDlls Include="@(QtLibNames -> '$(QtBinDir)%(Identity).dll')" />
<!--Filter plugins to copy based on the observation that all debug versions end in "d"-->
<QtAllPlugins Include="$(QtPluginsDir)**\*$(QtLibSuffix).dll" />
<QtAllPlugins Include="$(QtPluginsDir)**\*$(QtLibSuffix).dll" Exclude="$(QtPluginsDir)iconengines\qsvgicon$(QtLibSuffix).dll;$(QtPluginsDir)imageformats\qsvg$(QtLibSuffix).dll" />
<QtPlugins Condition="$(Configuration.Contains(Debug))" Include="@(QtAllPlugins)" />
<QtPlugins Condition="!$(Configuration.Contains(Debug))" Exclude="$(QtPluginsDir)**\*$(QtDebugSuffix).dll" Include="@(QtAllPlugins)" />
<QtPluginsDest Include="@(QtPlugins -> '$(BinaryOutputDir)$(QtPluginFolder)\%(RecursiveDir)%(Filename)%(Extension)')" />
+1 -1
View File
@@ -104,7 +104,7 @@ for i in $(find "$DEPSDIR" -iname '*.so'); do
done
echo "Running linuxdeploy to create AppDir..."
EXTRA_QT_MODULES="core;gui;svg;widgets;xcbqpa;waylandcompositor" \
EXTRA_QT_MODULES="core;gui;widgets;xcbqpa;waylandcompositor" \
EXTRA_PLATFORM_PLUGINS="libqwayland.so" \
DEPLOY_PLATFORM_THEMES="1" \
LINUXDEPLOY_EXCLUDED_LIBRARIES="libwayland-cursor*;libwayland-egl*" \
-2
View File
@@ -177,14 +177,12 @@ declare -a QTLIBS=(
"libQt6DBus.so.6"
"libQt6Gui.so.6"
"libQt6OpenGL.so.6"
"libQt6Svg.so.6"
"libQt6WaylandClient.so.6"
"libQt6Widgets.so.6"
"libQt6XcbQpa.so.6"
)
declare -a QTPLUGINS=(
"plugins/iconengines"
"plugins/imageformats"
"plugins/platforminputcontexts"
"plugins/platforms"
+6
View File
@@ -167,6 +167,8 @@ set(SRCS
setupwizarddialog.h
setupwizarddialog.ui
texturereplacementsettingsdialog.ui
themesvgiconengine.cpp
themesvgiconengine.h
togglebutton.cpp
togglebutton.h
)
@@ -205,6 +207,10 @@ endif()
# Our Qt builds may have exceptions on, so force them off.
target_compile_definitions(duckstation-qt PRIVATE QT_NO_EXCEPTIONS QT_NO_SIGNALS_SLOTS_KEYWORDS)
# We need to override QT_STATICPLUGIN to force our SVG plugin to be statically linked to the exectuable.
# Prevent the force include of the pch from interfering.
set_source_files_properties("themesvgiconengine.cpp" PROPERTIES SKIP_PRECOMPILE_HEADERS TRUE)
add_core_resources(duckstation-qt)
# Automatically generate a list of .ui calls and call qt_wrap_ui() to generate targets.
@@ -55,10 +55,15 @@
<ClCompile Include="selectdiscdialog.cpp" />
<ClCompile Include="settingswindow.cpp" />
<ClCompile Include="setupwizarddialog.cpp" />
<ClCompile Include="themesvgiconengine.cpp">
<PrecompiledHeader>NotUsing</PrecompiledHeader>
<ForcedIncludeFiles></ForcedIncludeFiles>
</ClCompile>
<ClCompile Include="togglebutton.cpp" />
<ClCompile Include="vcruntimecheck.cpp" />
</ItemGroup>
<ItemGroup>
<QtMoc Include="themesvgiconengine.h" />
<QtMoc Include="togglebutton.h" />
<QtMoc Include="setupwizarddialog.h" />
<QtMoc Include="aboutdialog.h" />
@@ -331,6 +336,7 @@
<QtUi Include="debuggingsettingswidget.ui">
<FileType>Document</FileType>
</QtUi>
<None Include="themesvgiconengine.json" />
<None Include="translations\duckstation-qt_az.ts" />
<None Include="translations\duckstation-qt_es-es.ts" />
<None Include="translations\duckstation-qt_sv.ts" />
@@ -54,6 +54,7 @@
<ClCompile Include="asyncpixmaploader.cpp" />
<ClCompile Include="debuggingsettingswidget.cpp" />
<ClCompile Include="asynchttprequest.cpp" />
<ClCompile Include="themesvgiconengine.cpp" />
</ItemGroup>
<ItemGroup>
<ClInclude Include="qtutils.h" />
@@ -120,6 +121,7 @@
<QtMoc Include="asyncpixmaploader.h" />
<QtMoc Include="debuggingsettingswidget.h" />
<QtMoc Include="asynchttprequest.h" />
<QtMoc Include="themesvgiconengine.h" />
</ItemGroup>
<ItemGroup>
<QtUi Include="consolesettingswidget.ui" />
@@ -200,6 +202,7 @@
<None Include="translations\duckstation-qt_az.ts">
<Filter>translations</Filter>
</None>
<None Include="themesvgiconengine.json" />
</ItemGroup>
<ItemGroup>
<QtTs Include="translations\duckstation-qt_de.ts">
+33
View File
@@ -62,6 +62,7 @@
#include <QtCore/QFile>
#include <QtCore/QTimer>
#include <QtCore/QTranslator>
#include <QtCore/QtPlugin>
#include <QtGui/QClipboard>
#include <QtGui/QKeyEvent>
#include <algorithm>
@@ -95,6 +96,9 @@ QT_TRANSLATE_NOOP("MAC_APPLICATION_MENU", "Quit %1")
QT_TRANSLATE_NOOP("MAC_APPLICATION_MENU", "About %1")
#endif
Q_IMPORT_PLUGIN(ThemeSVGIconEnginePlugin);
Q_IMPORT_PLUGIN(PlutoSVGImagePlugin);
static constexpr u32 SETTINGS_SAVE_DELAY = 1000;
/// Interval at which the controllers are polled when the system is not active.
@@ -272,6 +276,7 @@ bool QtHost::EarlyProcessStartup()
icon_theme_search_paths.emplace_back(":/icons"_L1);
icon_theme_search_paths.emplace_back(":/standard-icons"_L1);
QIcon::setThemeSearchPaths(icon_theme_search_paths);
QIcon::setThemeName("monochrome");
return true;
}
@@ -929,6 +934,34 @@ void QtHost::ApplyMigrations()
}
}
}
#ifdef _WIN32
#ifdef _DEBUG
#define SUFFIX "d"
#else
#define SUFFIX
#endif
// Remove Qt6Svg.dll and the plugins which use QtSvg, since the updater can't remove them itself...
for (const char* path_to_remove : {
"Qt6Svg" SUFFIX ".dll",
"QtPlugins\\iconengines\\qsvgicon" SUFFIX ".dll",
"QtPlugins\\imageformats\\qsvg" SUFFIX ".dll",
})
{
const std::string full_path = Path::Combine(EmuFolders::AppRoot, path_to_remove);
if (FileSystem::FileExists(full_path.c_str()))
{
Error error;
if (!FileSystem::DeleteFile(full_path.c_str(), &error))
{
QMessageBox::critical(nullptr, "Error"_L1,
QString::fromStdString(fmt::format("Failed to delete conflicting library {}: {}",
path_to_remove, error.GetDescription())));
}
}
}
#undef SUFFIX
#endif
}
void CoreThread::applySettings(bool display_osd_messages /* = false */)
-4
View File
@@ -326,10 +326,6 @@ bool QtHost::HasGlobalStylesheet()
void QtHost::UpdateThemeOnStyleChange()
{
const QLatin1StringView new_theme_name = IsDarkApplicationTheme() ? "white"_L1 : "black"_L1;
if (QIcon::themeName() != new_theme_name)
QIcon::setThemeName(new_theme_name);
if (NativeThemeStylesheetNeedsUpdate())
{
const QString stylesheet = GetNativeThemeStylesheet();
+372
View File
@@ -0,0 +1,372 @@
// SPDX-FileCopyrightText: 2019-2026 Connor McLaughlin <stenzek@gmail.com>
// SPDX-License-Identifier: CC-BY-NC-ND-4.0
// Needs to be defined before any includes.
#define QT_STATICPLUGIN
#include "themesvgiconengine.h"
#include "qtutils.h"
#include <QtCore/QFile>
#include <QtCore/QtPlugin>
#include <QtGui/QImage>
#include <QtGui/QPainter>
#include <QtGui/QPalette>
#include <QtGui/QPixmapCache>
#include <QtWidgets/QApplication>
#include <limits>
#include <plutosvg.h>
#include "moc_themesvgiconengine.cpp"
/// Returns the icon color for the given mode based on the application palette.
static QColor GetIconColorFromPalette(QIcon::Mode mode)
{
// Thank gosh these are copy-on-write...
const QPalette palette = QApplication::palette();
// NOTE: Active and Normal are the same in QPalette.
if (mode == QIcon::Disabled)
return palette.color(QPalette::Disabled, QPalette::WindowText);
else if (mode == QIcon::Selected)
return palette.color(QPalette::Current, QPalette::HighlightedText);
else
return palette.color(QPalette::Normal, QPalette::WindowText);
}
/// Constructs a cache key for the given resource path, pixel size, and RGBA color value.
static QString BuildCacheKey(const QString& resource_path, const QSize& size, qreal dpr, const QColor& color)
{
// Cache key includes path, size, and colour so stale entries are not reused after a palette change.
return QStringLiteral("%1_%2x%3@%4_%5")
.arg(resource_path)
.arg(size.width())
.arg(size.height())
.arg(dpr)
.arg(static_cast<unsigned>(color.rgba()), 8, 16, QLatin1Char('0'));
}
/// Callback used when freeing the QImage.
static void CleanupPlutoSVGSurface(void* surface)
{
plutovg_surface_destroy(static_cast<plutovg_surface_t*>(surface));
}
/// Renders the document at the given pixel dimensions with the given colour and inserts the
/// result into QPixmapCache under cache_key. Returns a null QPixmap on failure.
static bool RenderSVGToPixmap(QPixmap& pm, const plutosvg_document* doc, const QSize& size, const QColor& color)
{
// Pass the desired icon colour as CSS currentColor so plutosvg tints the monochrome SVG in a
// single render pass, avoiding a separate SourceIn compositing step.
const plutovg_color_t current_color = {
.r = static_cast<float>(color.redF()),
.g = static_cast<float>(color.greenF()),
.b = static_cast<float>(color.blueF()),
.a = static_cast<float>(color.alphaF()),
};
plutovg_surface_t* surface =
plutosvg_document_render_to_surface(doc, nullptr, size.width(), size.height(), &current_color, nullptr, nullptr);
if (!surface)
return false;
// plutovg surfaces are premultiplied ARGB (0xAARRGGBB, native-endian), which matches
// QImage::Format_ARGB32_Premultiplied exactly, no pixel conversion required.
// The cleanup function ensures the surface is destroyed when the QImage is done with the pixel data.
const QImage img(plutovg_surface_get_data(surface), plutovg_surface_get_width(surface),
plutovg_surface_get_height(surface), plutovg_surface_get_stride(surface),
QImage::Format_ARGB32_Premultiplied, CleanupPlutoSVGSurface, surface);
pm = QPixmap::fromImage(img);
return !pm.isNull();
}
/// Helper function to read a QFile to a DynamicHeapArray<u8>.
static bool ReadFileToByteArray(QIODevice* dev, DynamicHeapArray<u8>& out_data)
{
if (qint64 size; !dev->isSequential() && (size = dev->size()) > 0)
{
out_data.resize(static_cast<size_t>(
(sizeof(size_t) == sizeof(qint64)) ? size : std::min<qint64>(size, std::numeric_limits<size_t>::max())));
if (dev->read(reinterpret_cast<char*>(out_data.data()), size) != size)
{
out_data.deallocate();
return false;
}
}
else
{
constexpr size_t chunk_size = 1048576;
size_t read_so_far = 0;
for (;;)
{
const size_t prev_size = out_data.size();
const size_t new_size =
((read_so_far + chunk_size) < read_so_far) ? std::numeric_limits<size_t>::max() : (read_so_far + chunk_size);
const size_t space = (new_size - prev_size);
if (space > 0)
out_data.resize(new_size);
const qint64 bytes_read =
(space > 0) ? dev->read(reinterpret_cast<char*>(out_data.data() + read_so_far), static_cast<qint64>(space)) : 0;
if (bytes_read < 0)
{
out_data.deallocate();
return false;
}
else if (bytes_read == 0)
{
out_data.resize(prev_size);
break;
}
read_so_far += static_cast<size_t>(bytes_read);
}
}
return true;
}
ThemeSVGIconEngine::ThemeSVGIconEngine(const QString& resource_path) : m_resource_path(resource_path)
{
}
ThemeSVGIconEngine::~ThemeSVGIconEngine()
{
if (m_document)
plutosvg_document_destroy(m_document);
}
bool ThemeSVGIconEngine::ensureLoaded() const
{
// Previous load failed?
if (m_resource_path.isEmpty())
return false;
QFile file(m_resource_path);
if (!file.open(QFile::ReadOnly) || !ReadFileToByteArray(&file, m_svg_data))
{
qCritical() << "Failed to open SVG file: " << m_resource_path;
m_resource_path = {};
return false;
}
// The document borrows the data pointer; m_svg_data must remain valid for the document's lifetime.
m_document = plutosvg_document_load_from_data(reinterpret_cast<const char*>(m_svg_data.data()),
static_cast<int>(m_svg_data.size()), -1.0f, -1.0f, nullptr, nullptr);
if (!m_document)
{
qCritical() << "Failed to parse SVG data: " << m_resource_path;
m_resource_path = {};
return false;
}
return true;
}
void ThemeSVGIconEngine::paint(QPainter* painter, const QRect& rect, QIcon::Mode mode, QIcon::State state)
{
Q_UNUSED(state);
if (rect.isEmpty())
return;
// Apply device pixel ratio to requested size so we can cache pixmaps at the correct sizes for different DPRs.
const QPaintDevice* const device = painter->device();
const qreal dpr = device ? device->devicePixelRatio() : 1.0;
const QSize size = QtUtils::ApplyDevicePixelRatioToSize(rect.size(), dpr);
const QColor color = GetIconColorFromPalette(mode);
const QString cache_key = BuildCacheKey(m_resource_path, size, dpr, color);
QPixmap pm;
if (!QPixmapCache::find(cache_key, &pm))
{
// Don't reload multiple times if we hit the cache.
if (ensureLoaded() && RenderSVGToPixmap(pm, m_document, size, color))
{
// DPR set must be before inserting into the cache, otherwise it copies.
pm.setDevicePixelRatio(dpr);
QPixmapCache::insert(cache_key, pm);
}
}
painter->drawPixmap(rect, pm);
}
QPixmap ThemeSVGIconEngine::pixmap(const QSize& size, QIcon::Mode mode, QIcon::State state)
{
Q_UNUSED(state);
if (size.isEmpty())
return {};
const QColor color = GetIconColorFromPalette(mode);
const QString cache_key = BuildCacheKey(m_resource_path, size, 1.0, color);
QPixmap pm;
if (!QPixmapCache::find(cache_key, &pm))
{
// Don't reload multiple times if we hit the cache.
if (ensureLoaded() && RenderSVGToPixmap(pm, m_document, size, color))
QPixmapCache::insert(cache_key, pm);
}
return pm;
}
QPixmap ThemeSVGIconEngine::scaledPixmap(const QSize& size, QIcon::Mode mode, QIcon::State state, qreal scale)
{
Q_UNUSED(state);
const QSize scaled_size = QtUtils::ApplyDevicePixelRatioToSize(size, scale);
if (scaled_size.isEmpty())
return {};
const QColor color = GetIconColorFromPalette(mode);
const QString cache_key = BuildCacheKey(m_resource_path, scaled_size, scale, color);
QPixmap pm;
if (!QPixmapCache::find(cache_key, &pm))
{
// Don't reload multiple times if we hit the cache.
if (ensureLoaded() && RenderSVGToPixmap(pm, m_document, scaled_size, color))
{
pm.setDevicePixelRatio(scale);
QPixmapCache::insert(cache_key, pm);
}
}
return pm;
}
QIconEngine* ThemeSVGIconEngine::clone() const
{
return new ThemeSVGIconEngine(m_resource_path);
}
QString ThemeSVGIconEngine::key() const
{
return QStringLiteral("svg");
}
QString ThemeSVGIconEngine::iconName()
{
return m_resource_path;
}
QIconEngine* ThemeSVGIconEnginePlugin::create(const QString& resource_path)
{
if (resource_path.endsWith(".svg", Qt::CaseInsensitive))
return new ThemeSVGIconEngine(resource_path);
return nullptr;
}
bool PlutoSVGImageHandler::canRead() const
{
if (m_document)
return m_document;
// Read all data once and keep it alive; plutosvg_document borrows the raw pointer.
QIODevice* const dev = device();
if (!dev || !ReadFileToByteArray(dev, m_svg_data))
{
qCritical() << "Failed to read SVG data from device";
return false;
}
m_document = plutosvg_document_load_from_data(reinterpret_cast<const char*>(m_svg_data.data()),
static_cast<int>(m_svg_data.size()), -1.0f, -1.0f, nullptr, nullptr);
if (!m_document)
{
qCritical() << "Failed to parse SVG data";
m_svg_data = {};
return false;
}
return true;
}
bool PlutoSVGImageHandler::read(QImage* image)
{
if (!canRead())
return false;
// Determine render size: honour caller's ScaledSize, fall back to intrinsic SVG dimensions.
QSize render_size = m_scaled_size;
if (!render_size.isValid())
{
render_size = QSize(qMax(1, qRound(plutosvg_document_get_width(m_document))),
qMax(1, qRound(plutosvg_document_get_height(m_document))));
}
// Render with a solid currentColor so the SVG's own fill/stroke colours are preserved.
// Callers that want palette tinting should use ThemeSVGIconEngine instead.
const plutovg_color_t current_color = {.r = 1.0f, .g = 1.0f, .b = 1.0f, .a = 1.0f};
plutovg_surface_t* surface = plutosvg_document_render_to_surface(
m_document, nullptr, render_size.width(), render_size.height(), &current_color, nullptr, nullptr);
if (!surface)
return false;
// Wrap the surface pixels in a QImage; the cleanup function destroys the surface once Qt is done.
// plutovg uses premultiplied ARGB (0xAARRGGBB, native-endian) == QImage::Format_ARGB32_Premultiplied.
*image =
QImage(plutovg_surface_get_data(surface), plutovg_surface_get_width(surface), plutovg_surface_get_height(surface),
plutovg_surface_get_stride(surface), QImage::Format_ARGB32_Premultiplied, CleanupPlutoSVGSurface, surface);
return !image->isNull();
}
bool PlutoSVGImageHandler::supportsOption(ImageOption option) const
{
return (option == Size || option == ScaledSize);
}
QVariant PlutoSVGImageHandler::option(ImageOption option) const
{
if (option == ScaledSize)
return m_scaled_size;
if (option == Size)
{
if (!canRead())
return {};
return QSize(qMax(1, qRound(plutosvg_document_get_width(m_document))),
qMax(1, qRound(plutosvg_document_get_height(m_document))));
}
return {};
}
void PlutoSVGImageHandler::setOption(ImageOption option, const QVariant& value)
{
if (option == ScaledSize)
m_scaled_size = value.toSize();
}
QImageIOPlugin::Capabilities PlutoSVGImagePlugin::capabilities(QIODevice* device, const QByteArray& format) const
{
if (format == "svg")
return CanRead;
if (device)
{
// Require the byte sequence to contain an XML or SVG marker within a reasonable prefix.
const QByteArray prefix = device->peek(256);
if (prefix.contains("<svg") || prefix.contains("<?xml"))
return CanRead;
}
return {};
}
QImageIOHandler* PlutoSVGImagePlugin::create(QIODevice* device, const QByteArray& format) const
{
PlutoSVGImageHandler* const handler = new PlutoSVGImageHandler();
handler->setDevice(device);
handler->setFormat(format.isEmpty() ? QByteArray("svg") : format);
return handler;
}
+103
View File
@@ -0,0 +1,103 @@
// SPDX-FileCopyrightText: 2019-2026 Connor McLaughlin <stenzek@gmail.com>
// SPDX-License-Identifier: CC-BY-NC-ND-4.0
#pragma once
#include "common/heap_array.h"
#include "common/types.h"
#include <QtCore/QByteArray>
#include <QtCore/QString>
#include <QtGui/QColor>
#include <QtGui/QIcon>
#include <QtGui/QIconEngine>
#include <QtGui/QIconEnginePlugin>
#include <QtGui/QImageIOHandler>
#include <QtGui/QImageIOPlugin>
struct plutosvg_document;
/**
* Custom icon engine that renders SVG icons tinted to the current palette's WindowText colour.
* This allows a single set of monochrome SVG files to serve both light and dark themes without
* duplication. The SVG is parsed once into a plutosvg_document and cached on the engine instance.
* At paint/pixmap time plutosvg renders directly to a premultiplied-ARGB surface using the desired
* palette colour as CSS currentColor, so no secondary SourceIn compositing pass is required.
*
* Pixmaps are cached in QPixmapCache keyed by resource path, size (in device pixels), and the RGBA
* value of the resolved colour, so the cache is automatically invalidated when the palette changes.
*/
class ThemeSVGIconEngine final : public QIconEngine
{
public:
explicit ThemeSVGIconEngine(const QString& resource_path);
ThemeSVGIconEngine(const ThemeSVGIconEngine&) = delete;
~ThemeSVGIconEngine() override;
void paint(QPainter* painter, const QRect& rect, QIcon::Mode mode, QIcon::State state) override;
QPixmap pixmap(const QSize& size, QIcon::Mode mode, QIcon::State state) override;
QPixmap scaledPixmap(const QSize& size, QIcon::Mode mode, QIcon::State state, qreal scale) override;
QIconEngine* clone() const override;
QString key() const override;
QString iconName() override;
private:
/// Ensures the SVG file has been loaded.
bool ensureLoaded() const;
mutable QString m_resource_path;
mutable DynamicHeapArray<u8> m_svg_data;
mutable plutosvg_document* m_document = nullptr;
};
class ThemeSVGIconEnginePlugin : public QIconEnginePlugin
{
Q_OBJECT
Q_PLUGIN_METADATA(IID QIconEngineFactoryInterface_iid FILE "themesvgiconengine.json")
public:
QIconEngine* create(const QString& resource_path) override;
};
/**
* Qt image I/O handler — replaces the QtSvg SVG image loader.
*
* Registered statically for "svg" so that any Qt code that loads SVG images
* through QImageReader (e.g. QPixmap::load, QImage::load) will use plutosvg
* instead of QtSvg's built-in rasteriser.
*
* Callers can influence the output size through the standard QImageIOHandler
* ScaledSize option; if not set the SVG's intrinsic dimensions are used.
*/
class PlutoSVGImageHandler final : public QImageIOHandler
{
public:
PlutoSVGImageHandler() = default;
~PlutoSVGImageHandler() override = default;
/// Lazily load and parse the SVG data from device(). Returns false on failure.
bool canRead() const override;
bool read(QImage* image) override;
bool supportsOption(ImageOption option) const override;
QVariant option(ImageOption option) const override;
void setOption(ImageOption option, const QVariant& value) override;
static bool canRead(QIODevice* device);
private:
mutable DynamicHeapArray<u8> m_svg_data;
mutable plutosvg_document* m_document = nullptr;
QSize m_scaled_size;
};
/// QImageIOPlugin that installs PlutoSVGImageHandler for SVG files.
class PlutoSVGImagePlugin final : public QImageIOPlugin
{
Q_OBJECT
Q_PLUGIN_METADATA(IID QImageIOHandlerFactoryInterface_iid FILE "themesvgiconengine.json")
public:
Capabilities capabilities(QIODevice* device, const QByteArray& format) const override;
QImageIOHandler* create(QIODevice* device, const QByteArray& format = QByteArray()) const override;
};
@@ -0,0 +1 @@
{ "Keys": [ "svg" ] }