feat(imageformat): add librsvg SVG image plugin

main
reionwong 3 weeks ago
parent 688d4d44d9
commit bf650f6047

@ -8,3 +8,4 @@ set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} "${CMAKE_CURRENT_SOURCE_DIR}/cmake")
add_subdirectory(platformtheme) add_subdirectory(platformtheme)
add_subdirectory(widgetstyle) add_subdirectory(widgetstyle)
add_subdirectory(imageformat)

@ -4,7 +4,7 @@ Unify Qt application style of CutefishOS.
## Dependencies ## 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 ## Build

1
debian/control vendored

@ -6,6 +6,7 @@ Build-Depends: cmake,
debhelper (>= 9), debhelper (>= 9),
extra-cmake-modules, extra-cmake-modules,
libqt6xdg-dev, libqt6xdg-dev,
librsvg2-dev,
pkg-config, pkg-config,
qt6-base-dev, qt6-base-dev,
qt6-base-private-dev, qt6-base-private-dev,

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

@ -0,0 +1,4 @@
{
"Keys": [ "svg" ],
"MimeTypes": [ "image/svg+xml" ]
}

@ -0,0 +1,266 @@
#include "rsvgimageiohandler.h"
#include <QImage>
#include <QIODevice>
#include <QVariant>
#include <librsvg/rsvg.h>
#include <algorithm>
#include <cmath>
#include <limits>
#include <memory>
namespace {
bool isSvg(const QByteArray &data)
{
return data.left(64 * 1024).toLower().contains("<svg");
}
struct CairoContextDeleter
{
void operator()(cairo_t *context) const noexcept
{
if (context) {
cairo_destroy(context);
}
}
};
struct CairoSurfaceDeleter
{
void operator()(cairo_surface_t *surface) const noexcept
{
if (surface) {
cairo_surface_destroy(surface);
}
}
};
struct GErrorDeleter
{
void operator()(GError *error) const noexcept
{
if (error) {
g_error_free(error);
}
}
};
struct RsvgHandleDeleter
{
void operator()(RsvgHandle *handle) const noexcept
{
if (handle) {
g_object_unref(handle);
}
}
};
using CairoContextPtr = std::unique_ptr<cairo_t, CairoContextDeleter>;
using CairoSurfacePtr = std::unique_ptr<cairo_surface_t, CairoSurfaceDeleter>;
using GErrorPtr = std::unique_ptr<GError, GErrorDeleter>;
using RsvgHandlePtr = std::unique_ptr<RsvgHandle, RsvgHandleDeleter>;
RsvgHandlePtr createHandle(const QByteArray &data)
{
GError *rawError = nullptr;
RsvgHandlePtr handle(rsvg_handle_new_from_data(
reinterpret_cast<const guint8 *>(data.constData()),
static_cast<gsize>(data.size()),
&rawError));
GErrorPtr error(rawError);
return handle;
}
int toPixelDimension(gdouble dimension)
{
if (!std::isfinite(dimension) || dimension <= 0.0
|| dimension > static_cast<gdouble>(std::numeric_limits<int>::max())) {
return 0;
}
const gdouble rounded = std::ceil(dimension);
if (rounded > static_cast<gdouble>(std::numeric_limits<int>::max())) {
return 0;
}
return std::max(1, static_cast<int>(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<QSize>() ? 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<gdouble>(size.width()),
static_cast<gdouble>(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();
}

@ -0,0 +1,31 @@
#ifndef RSVGIMAGEIOHANDLER_H
#define RSVGIMAGEIOHANDLER_H
#include <QByteArray>
#include <QImageIOHandler>
#include <QSize>
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

@ -0,0 +1,39 @@
#include "rsvgimageplugin.h"
#include "rsvgimageiohandler.h"
#include <QIODevice>
namespace {
bool isSvg(const QByteArray &data)
{
return data.left(64 * 1024).toLower().contains("<svg");
}
} // namespace
QImageIOPlugin::Capabilities RsvgImagePlugin::capabilities(QIODevice *device, const QByteArray &format) const
{
if (!device || !device->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;
}

@ -0,0 +1,16 @@
#ifndef RSVGIMAGEPLUGIN_H
#define RSVGIMAGEPLUGIN_H
#include <QImageIOPlugin>
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
Loading…
Cancel
Save