From bf650f604700ae3ee8297328984c6dbaa9b95598 Mon Sep 17 00:00:00 2001 From: reionwong Date: Mon, 31 Aug 2026 01:18:42 -0400 Subject: [PATCH] feat(imageformat): add librsvg SVG image plugin --- CMakeLists.txt | 1 + README.md | 2 +- debian/control | 1 + imageformat/CMakeLists.txt | 36 ++++ imageformat/rsvgimage.json | 4 + imageformat/rsvgimageiohandler.cpp | 266 +++++++++++++++++++++++++++++ imageformat/rsvgimageiohandler.h | 31 ++++ imageformat/rsvgimageplugin.cpp | 39 +++++ imageformat/rsvgimageplugin.h | 16 ++ 9 files changed, 395 insertions(+), 1 deletion(-) create mode 100644 imageformat/CMakeLists.txt create mode 100644 imageformat/rsvgimage.json create mode 100644 imageformat/rsvgimageiohandler.cpp create mode 100644 imageformat/rsvgimageiohandler.h create mode 100644 imageformat/rsvgimageplugin.cpp create mode 100644 imageformat/rsvgimageplugin.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 26f5eb1..ecec361 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -8,3 +8,4 @@ set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} "${CMAKE_CURRENT_SOURCE_DIR}/cmake") add_subdirectory(platformtheme) add_subdirectory(widgetstyle) +add_subdirectory(imageformat) diff --git a/README.md b/README.md index f8f24a4..567dff8 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ Unify Qt application style of CutefishOS. ## Dependencies -`sudo pacman -S gcc extra-cmake-modules qt6-base qt6-tools libqtxdg` +`sudo pacman -S gcc extra-cmake-modules qt6-base qt6-tools libqtxdg librsvg` ## Build diff --git a/debian/control b/debian/control index ce00e6a..26fc6df 100644 --- a/debian/control +++ b/debian/control @@ -6,6 +6,7 @@ Build-Depends: cmake, debhelper (>= 9), extra-cmake-modules, libqt6xdg-dev, + librsvg2-dev, pkg-config, qt6-base-dev, qt6-base-private-dev, diff --git a/imageformat/CMakeLists.txt b/imageformat/CMakeLists.txt new file mode 100644 index 0000000..decb770 --- /dev/null +++ b/imageformat/CMakeLists.txt @@ -0,0 +1,36 @@ +cmake_minimum_required(VERSION 3.16) + +project(qsvg-rsvg) + +set(TARGET qsvg-rsvg) +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_AUTOMOC ON) +set(CMAKE_INCLUDE_CURRENT_DIR ON) + +find_package(Qt6 REQUIRED COMPONENTS Core Gui) +find_package(PkgConfig REQUIRED) +pkg_check_modules(LIBRSVG REQUIRED IMPORTED_TARGET librsvg-2.0) + +set(SRCS + rsvgimageiohandler.cpp + rsvgimageiohandler.h + rsvgimageplugin.cpp + rsvgimageplugin.h +) + +add_library(${TARGET} MODULE ${SRCS}) +target_compile_definitions(${TARGET} PRIVATE QT_NO_KEYWORDS) +set_target_properties(${TARGET} PROPERTIES + LIBRARY_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/imageformats" +) +target_link_libraries(${TARGET} + Qt6::Core + Qt6::Gui + PkgConfig::LIBRSVG +) + +find_program(QT_PATHS_EXECUTABLE NAMES qtpaths6 REQUIRED) +execute_process(COMMAND "${QT_PATHS_EXECUTABLE}" -query QT_INSTALL_PLUGINS + OUTPUT_VARIABLE CMAKE_INSTALL_QTPLUGINDIR OUTPUT_STRIP_TRAILING_WHITESPACE) + +install(TARGETS ${TARGET} DESTINATION ${CMAKE_INSTALL_QTPLUGINDIR}/imageformats/) diff --git a/imageformat/rsvgimage.json b/imageformat/rsvgimage.json new file mode 100644 index 0000000..3b5a653 --- /dev/null +++ b/imageformat/rsvgimage.json @@ -0,0 +1,4 @@ +{ + "Keys": [ "svg" ], + "MimeTypes": [ "image/svg+xml" ] +} diff --git a/imageformat/rsvgimageiohandler.cpp b/imageformat/rsvgimageiohandler.cpp new file mode 100644 index 0000000..fc03874 --- /dev/null +++ b/imageformat/rsvgimageiohandler.cpp @@ -0,0 +1,266 @@ +#include "rsvgimageiohandler.h" + +#include +#include +#include + +#include + +#include +#include +#include +#include + +namespace { + +bool isSvg(const QByteArray &data) +{ + return data.left(64 * 1024).toLower().contains("; +using CairoSurfacePtr = std::unique_ptr; +using GErrorPtr = std::unique_ptr; +using RsvgHandlePtr = std::unique_ptr; + +RsvgHandlePtr createHandle(const QByteArray &data) +{ + GError *rawError = nullptr; + RsvgHandlePtr handle(rsvg_handle_new_from_data( + reinterpret_cast(data.constData()), + static_cast(data.size()), + &rawError)); + GErrorPtr error(rawError); + return handle; +} + +int toPixelDimension(gdouble dimension) +{ + if (!std::isfinite(dimension) || dimension <= 0.0 + || dimension > static_cast(std::numeric_limits::max())) { + return 0; + } + + const gdouble rounded = std::ceil(dimension); + if (rounded > static_cast(std::numeric_limits::max())) { + return 0; + } + + return std::max(1, static_cast(rounded)); +} + +} // namespace + +bool RsvgImageIOHandler::canRead() const +{ + if (!device() || !device()->isReadable()) { + return false; + } + + return format().compare("svg", Qt::CaseInsensitive) == 0 + || isSvg(device()->peek(64 * 1024)); +} + +bool RsvgImageIOHandler::read(QImage *image) +{ + if (!image || !loadIntrinsicSize()) { + return false; + } + + const QSize targetSize = m_scaledSize.isValid() ? m_scaledSize : m_intrinsicSize; + return render(targetSize, image); +} + +QVariant RsvgImageIOHandler::option(ImageOption option) const +{ + switch (option) { + case Size: + return loadIntrinsicSize() ? QVariant::fromValue(m_intrinsicSize) : QVariant(); + case ScaledSize: + return m_scaledSize; + default: + return QImageIOHandler::option(option); + } +} + +void RsvgImageIOHandler::setOption(ImageOption option, const QVariant &value) +{ + if (option == ScaledSize) { + m_scaledSize = value.canConvert() ? value.toSize() : QSize(); + return; + } + + QImageIOHandler::setOption(option, value); +} + +bool RsvgImageIOHandler::supportsOption(ImageOption option) const +{ + return option == Size || option == ScaledSize || QImageIOHandler::supportsOption(option); +} + +bool RsvgImageIOHandler::loadData() const +{ + if (m_dataLoaded) { + return !m_data.isEmpty(); + } + + m_dataLoaded = true; + if (!device()) { + return false; + } + + const qint64 position = device()->pos(); + m_data = device()->readAll(); + if (!device()->isSequential() && position >= 0) { + device()->seek(position); + } + + return !m_data.isEmpty(); +} + +bool RsvgImageIOHandler::loadIntrinsicSize() const +{ + if (m_metadataLoaded) { + return m_valid; + } + + m_metadataLoaded = true; + if (!loadData()) { + return false; + } + + const RsvgHandlePtr handle = createHandle(m_data); + if (!handle) { + return false; + } + + gdouble width = 0.0; + gdouble height = 0.0; + if (!rsvg_handle_get_intrinsic_size_in_pixels(handle.get(), &width, &height)) { + gboolean hasWidth = false; + gboolean hasHeight = false; + gboolean hasViewbox = false; + RsvgLength intrinsicWidth {}; + RsvgLength intrinsicHeight {}; + RsvgRectangle viewbox {}; + rsvg_handle_get_intrinsic_dimensions(handle.get(), + &hasWidth, + &intrinsicWidth, + &hasHeight, + &intrinsicHeight, + &hasViewbox, + &viewbox); + if (!hasViewbox || viewbox.width <= 0.0 || viewbox.height <= 0.0) { + return false; + } + width = viewbox.width; + height = viewbox.height; + } + + const int pixelWidth = toPixelDimension(width); + const int pixelHeight = toPixelDimension(height); + if (pixelWidth <= 0 || pixelHeight <= 0) { + return false; + } + + m_intrinsicSize = QSize(pixelWidth, pixelHeight); + m_valid = true; + return true; +} + +bool RsvgImageIOHandler::render(const QSize &size, QImage *image) const +{ + if (!image || !size.isValid() || !loadData()) { + return false; + } + + const RsvgHandlePtr handle = createHandle(m_data); + if (!handle) { + return false; + } + + QImage target(size, QImage::Format_ARGB32_Premultiplied); + if (target.isNull()) { + return false; + } + target.fill(Qt::transparent); + + const int cairoStride = cairo_format_stride_for_width(CAIRO_FORMAT_ARGB32, size.width()); + if (cairoStride != target.bytesPerLine()) { + return false; + } + + CairoSurfacePtr surface(cairo_image_surface_create_for_data( + target.bits(), + CAIRO_FORMAT_ARGB32, + size.width(), + size.height(), + target.bytesPerLine())); + if (!surface || cairo_surface_status(surface.get()) != CAIRO_STATUS_SUCCESS) { + return false; + } + + CairoContextPtr context(cairo_create(surface.get())); + if (!context || cairo_status(context.get()) != CAIRO_STATUS_SUCCESS) { + return false; + } + + const RsvgRectangle viewport { + 0.0, + 0.0, + static_cast(size.width()), + static_cast(size.height()) + }; + + GError *rawError = nullptr; + const gboolean rendered = rsvg_handle_render_document(handle.get(), context.get(), &viewport, &rawError); + GErrorPtr error(rawError); + if (!rendered || cairo_status(context.get()) != CAIRO_STATUS_SUCCESS) { + return false; + } + + context.reset(); + cairo_surface_flush(surface.get()); + *image = target.copy(); + return !image->isNull(); +} diff --git a/imageformat/rsvgimageiohandler.h b/imageformat/rsvgimageiohandler.h new file mode 100644 index 0000000..2a25472 --- /dev/null +++ b/imageformat/rsvgimageiohandler.h @@ -0,0 +1,31 @@ +#ifndef RSVGIMAGEIOHANDLER_H +#define RSVGIMAGEIOHANDLER_H + +#include +#include +#include + +class RsvgImageIOHandler final : public QImageIOHandler +{ +public: + bool canRead() const override; + bool read(QImage *image) override; + + QVariant option(ImageOption option) const override; + void setOption(ImageOption option, const QVariant &value) override; + bool supportsOption(ImageOption option) const override; + +private: + bool loadData() const; + bool loadIntrinsicSize() const; + bool render(const QSize &size, QImage *image) const; + + mutable QByteArray m_data; + mutable QSize m_intrinsicSize; + mutable bool m_dataLoaded = false; + mutable bool m_metadataLoaded = false; + mutable bool m_valid = false; + QSize m_scaledSize; +}; + +#endif // RSVGIMAGEIOHANDLER_H diff --git a/imageformat/rsvgimageplugin.cpp b/imageformat/rsvgimageplugin.cpp new file mode 100644 index 0000000..0244fa4 --- /dev/null +++ b/imageformat/rsvgimageplugin.cpp @@ -0,0 +1,39 @@ +#include "rsvgimageplugin.h" + +#include "rsvgimageiohandler.h" + +#include + +namespace { + +bool isSvg(const QByteArray &data) +{ + return data.left(64 * 1024).toLower().contains("isReadable()) { + return {}; + } + + if (!format.isEmpty() && format.compare("svg", Qt::CaseInsensitive) != 0) { + return {}; + } + + if (format.compare("svg", Qt::CaseInsensitive) == 0) { + return CanRead; + } + + return isSvg(device->peek(64 * 1024)) ? Capabilities(CanRead) : Capabilities(); +} + +QImageIOHandler *RsvgImagePlugin::create(QIODevice *device, const QByteArray &format) const +{ + auto *handler = new RsvgImageIOHandler; + handler->setDevice(device); + handler->setFormat(format.isEmpty() ? QByteArrayLiteral("svg") : format); + return handler; +} diff --git a/imageformat/rsvgimageplugin.h b/imageformat/rsvgimageplugin.h new file mode 100644 index 0000000..c29b92e --- /dev/null +++ b/imageformat/rsvgimageplugin.h @@ -0,0 +1,16 @@ +#ifndef RSVGIMAGEPLUGIN_H +#define RSVGIMAGEPLUGIN_H + +#include + +class RsvgImagePlugin final : public QImageIOPlugin +{ + Q_OBJECT + Q_PLUGIN_METADATA(IID "org.qt-project.Qt.QImageIOHandlerFactoryInterface" FILE "rsvgimage.json") + +public: + Capabilities capabilities(QIODevice *device, const QByteArray &format) const override; + QImageIOHandler *create(QIODevice *device, const QByteArray &format = QByteArray()) const override; +}; + +#endif // RSVGIMAGEPLUGIN_H