diff --git a/CMakeLists.txt b/CMakeLists.txt index e1b77ee..ab220e6 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,8 +1,10 @@ -project (pimageformats) +project(pimageformats) -cmake_minimum_required (VERSION 3.9.5) +cmake_minimum_required(VERSION 3.9.5) -include (GNUInstallDirs) +include(GNUInstallDirs) + +option(BUILD_EXPERIMENTAL_PLUGINS "Enable the build of imageformats plugin(s) that might not working" ON) set(CMAKE_AUTOMOC ON) set(CMAKE_AUTORCC ON) @@ -10,5 +12,8 @@ set(QT_MINIMUM_VERSION "5.10") find_package(Qt5 ${QT_MINIMUM_VERSION} CONFIG REQUIRED Gui) -add_subdirectory(vendor/libsai) +if(BUILD_EXPERIMENTAL_PLUGINS) + add_subdirectory(${CMAKE_SOURCE_DIR}/vendor/libsai) +endif() + add_subdirectory(imageformats) diff --git a/imageformats/CMakeLists.txt b/imageformats/CMakeLists.txt index 248e1e4..74128d2 100644 --- a/imageformats/CMakeLists.txt +++ b/imageformats/CMakeLists.txt @@ -1,17 +1,4 @@ -set (plugin pimg_sai) - -set (PLUGIN_SOURCES - sai_p.h - sai.cpp -) - -# {{{ KCM style -set(CMAKE_SHARED_MODULE_PREFIX "") -unset(CMAKE_LIBRARY_OUTPUT_DIRECTORY) -set(CMAKE_LIBRARY_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin") -# }}} - -add_library(${plugin} MODULE ${PLUGIN_SOURCES}) -set_property(TARGET ${plugin} APPEND PROPERTY AUTOGEN_TARGET_DEPENDS "sai.json") -set_target_properties(${plugin} PROPERTIES LIBRARY_OUTPUT_DIRECTORY "${CMAKE_LIBRARY_OUTPUT_DIRECTORY}/imageformats") -target_link_libraries(${plugin} Qt5::Gui sai) +if(BUILD_EXPERIMENTAL_PLUGINS) + add_subdirectory(sai) +endif() +add_subdirectory(svg) diff --git a/imageformats/sai/CMakeLists.txt b/imageformats/sai/CMakeLists.txt new file mode 100644 index 0000000..248e1e4 --- /dev/null +++ b/imageformats/sai/CMakeLists.txt @@ -0,0 +1,17 @@ +set (plugin pimg_sai) + +set (PLUGIN_SOURCES + sai_p.h + sai.cpp +) + +# {{{ KCM style +set(CMAKE_SHARED_MODULE_PREFIX "") +unset(CMAKE_LIBRARY_OUTPUT_DIRECTORY) +set(CMAKE_LIBRARY_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin") +# }}} + +add_library(${plugin} MODULE ${PLUGIN_SOURCES}) +set_property(TARGET ${plugin} APPEND PROPERTY AUTOGEN_TARGET_DEPENDS "sai.json") +set_target_properties(${plugin} PROPERTIES LIBRARY_OUTPUT_DIRECTORY "${CMAKE_LIBRARY_OUTPUT_DIRECTORY}/imageformats") +target_link_libraries(${plugin} Qt5::Gui sai) diff --git a/imageformats/sai.cpp b/imageformats/sai/sai.cpp similarity index 100% rename from imageformats/sai.cpp rename to imageformats/sai/sai.cpp diff --git a/imageformats/sai.json b/imageformats/sai/sai.json similarity index 100% rename from imageformats/sai.json rename to imageformats/sai/sai.json diff --git a/imageformats/sai_p.h b/imageformats/sai/sai_p.h similarity index 100% rename from imageformats/sai_p.h rename to imageformats/sai/sai_p.h diff --git a/imageformats/svg/CMakeLists.txt b/imageformats/svg/CMakeLists.txt new file mode 100644 index 0000000..680b4c7 --- /dev/null +++ b/imageformats/svg/CMakeLists.txt @@ -0,0 +1,26 @@ +set (plugin pimg_svg) + +set (PLUGIN_SOURCES + main.cpp + svg_p.h + svg.cpp +# since dtkgui still cannot compile under platforms other than Linux... + dsvgrenderer.h + dsvgrenderer.cpp +) + +find_package(Qt5 ${QT_MINIMUM_VERSION} CONFIG REQUIRED Svg) + +find_package(PkgConfig REQUIRED) +pkg_check_modules(rsvg REQUIRED librsvg-2.0 IMPORTED_TARGET) + +# {{{ KCM style +set(CMAKE_SHARED_MODULE_PREFIX "") +unset(CMAKE_LIBRARY_OUTPUT_DIRECTORY) +set(CMAKE_LIBRARY_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin") +# }}} + +add_library(${plugin} MODULE ${PLUGIN_SOURCES}) +set_property(TARGET ${plugin} APPEND PROPERTY AUTOGEN_TARGET_DEPENDS "svg.json") +set_target_properties(${plugin} PROPERTIES LIBRARY_OUTPUT_DIRECTORY "${CMAKE_LIBRARY_OUTPUT_DIRECTORY}/imageformats") +target_link_libraries(${plugin} Qt5::Gui Qt5::Svg PkgConfig::rsvg) diff --git a/imageformats/svg/dsvgrenderer.cpp b/imageformats/svg/dsvgrenderer.cpp new file mode 100644 index 0000000..4957015 --- /dev/null +++ b/imageformats/svg/dsvgrenderer.cpp @@ -0,0 +1,280 @@ +// SPDX-FileCopyrightText: 2022 - 2023 UnionTech Software Technology Co., Ltd. +// +// SPDX-License-Identifier: LGPL-3.0-or-later + +#ifndef DTK_DISABLE_LIBRSVG +#include +#else +#include +#endif + +#include "dsvgrenderer.h" + +#include +#include +#include +#include +#include + +class DSvgRendererPrivate : public QObject +{ +public: + explicit DSvgRendererPrivate(DSvgRenderer *qq); + + QImage getImage(const QSize &size, const QString &elementId) const; + + RsvgHandle *handle = nullptr; + QSize defaultSize; + mutable QRectF viewBox; +}; + +DSvgRendererPrivate::DSvgRendererPrivate(DSvgRenderer *qq) + : QObject(qq) // qq ==> DObject +{ + +} + +QImage DSvgRendererPrivate::getImage(const QSize &size, const QString &elementId) const +{ + QImage image(size, QImage::Format_ARGB32_Premultiplied); + + image.fill(Qt::transparent); + + cairo_surface_t *surface = cairo_image_surface_create_for_data(image.bits(), CAIRO_FORMAT_ARGB32, image.width(), image.height(), image.bytesPerLine()); + cairo_t *cairo = cairo_create(surface); + cairo_scale(cairo, image.width() / viewBox.width(), image.height() / viewBox.height()); + cairo_translate(cairo, -viewBox.x(), -viewBox.y()); + + if (elementId.isEmpty()) + rsvg_handle_render_cairo(handle, cairo); + else + rsvg_handle_render_cairo_sub(handle, cairo, elementId.toUtf8().constData()); + + cairo_destroy(cairo); + cairo_surface_destroy(surface); + + return image; +} + +/*! + \class Dtk::Gui::DSvgRenderer + \inmodule dtkgui + \brief 提供了将SVG文件的内容绘制到绘制设备上的方法. + + SVG图形可以在构造 DSvgRenderer 时加载,也可以稍后使用load()函数加载。 + 因为渲染是使用 QPainter 执行的,所以可以在 QPaintDevice 的任何子类上渲染SVG图形。 + 如果加载了有效文件,则无论是在构造时还是以后某个时间,isValid()都将返回true;否则将返回false。 + DSvgRenderer提供render()插槽,用于使用给定的 QPainter 渲染当前文档或动画文档的当前帧 + \note 使用 DSvgRenderer 需要 librsvg库 + */ + +DSvgRenderer::DSvgRenderer(QObject *parent) + : QObject(parent) +{ +} + +DSvgRenderer::DSvgRenderer(const QString &filename, QObject *parent) + : DSvgRenderer(parent) +{ + load(filename); +} + +DSvgRenderer::DSvgRenderer(const QByteArray &contents, QObject *parent) + : DSvgRenderer(parent) +{ + load(contents); +} + +DSvgRenderer::~DSvgRenderer() +{ + Q_D(DSvgRenderer); + + if (d->handle) { + Q_ASSERT(isValid()); + g_object_unref(d->handle); + } +} + +bool DSvgRenderer::isValid() const +{ + Q_D(const DSvgRenderer); + return d->handle; +} + +QSize DSvgRenderer::defaultSize() const +{ + Q_D(const DSvgRenderer); + return d->defaultSize; +} + +QRect DSvgRenderer::viewBox() const +{ + Q_D(const DSvgRenderer); + return d->handle ? d->viewBox.toRect() : QRect(); +} + +QRectF DSvgRenderer::viewBoxF() const +{ + Q_D(const DSvgRenderer); + return d->handle ? d->viewBox : QRectF(); +} + +void DSvgRenderer::setViewBox(const QRect &viewbox) +{ + setViewBox(QRectF(viewbox)); +} + +void DSvgRenderer::setViewBox(const QRectF &viewbox) +{ + Q_D(DSvgRenderer); + if (d->handle) + d->viewBox = viewbox; +} + +QRectF DSvgRenderer::boundsOnElement(const QString &id) const +{ + Q_D(const DSvgRenderer); + if (!d->handle) + return QRectF(); + + const QByteArray &id_data = id.toUtf8(); + + RsvgDimensionData dimension_data; + + if (!rsvg_handle_get_dimensions_sub(d->handle, &dimension_data, id_data.constData())) + return QRectF(); + + RsvgPositionData pos_data; + + if (!rsvg_handle_get_position_sub(d->handle, &pos_data, id_data.constData())) + return QRectF(); + + return QRectF(pos_data.x, pos_data.y, dimension_data.width, dimension_data.height); +} + +bool DSvgRenderer::elementExists(const QString &id) const +{ + Q_D(const DSvgRenderer); + if (!d->handle) + return false; + + return rsvg_handle_has_sub(d->handle, id.toUtf8().constData()); +} + +QImage DSvgRenderer::toImage(const QSize sz, const QString &elementId) const +{ + Q_D(const DSvgRenderer); + + return d->getImage(sz, elementId); +} + +static QByteArray updateXmlAttribute(const QString &contents) +{ + QByteArray data; + QXmlStreamWriter writer(&data); + QXmlStreamReader reader(contents); + while(reader.readNext() != QXmlStreamReader::Invalid && !reader.atEnd()) { + if (reader.tokenType() != QXmlStreamReader::StartElement || + !reader.attributes().hasAttribute("href")) { + writer.writeCurrentToken(reader); + continue; + } + + for (const auto &nd : reader.namespaceDeclarations()) + writer.writeNamespace(nd.namespaceUri().toString(), nd.prefix().toString()); + + writer.writeStartElement(reader.namespaceUri().toString(), reader.name().toString()); + + for (const auto &attr : reader.attributes()) { + if (attr.name() == "href") { + writer.writeAttribute("xlink:href", attr.value().toString()); + continue; + } + writer.writeAttribute(attr); + } + } + + return data; +} + +static QByteArray format(const QByteArray &contents) +{ + QXmlStreamReader reader(contents); + while (reader.readNextStartElement()) { + if (reader.attributes().hasAttribute("href")) + return updateXmlAttribute(contents); + } + + return contents; +} + +bool DSvgRenderer::load(const QString &filename) +{ + QFile file(filename); + + if (file.open(QIODevice::ReadOnly)) { + // TODO: if `href` attribute is adapted after librsvg upgrade revert me + return load(format(file.readAll())); + } + + return false; +} + +bool DSvgRenderer::load(const QByteArray &contents) +{ + Q_D(DSvgRenderer);qDebug() << "drsvg load()"; + + if (d->handle) { + g_object_unref(d->handle); + d->handle = nullptr; + } + + GError *error = nullptr; + d->handle = rsvg_handle_new_from_data((const guint8*)contents.constData(), contents.length(), &error); + + if (error) { + qWarning("DSvgRenderer::load: %s", error->message); + g_error_free(error); + + return false; + } + + RsvgDimensionData rsvg_data; + + rsvg_handle_get_dimensions(d->handle, &rsvg_data); + + d->defaultSize.setWidth(rsvg_data.width); + d->defaultSize.setHeight(rsvg_data.height); + d->viewBox = QRectF(QPointF(0, 0), d->defaultSize); + + return true; +} + +void DSvgRenderer::render(QPainter *p) +{ + render(p, QString(), QRectF()); +} + +void DSvgRenderer::render(QPainter *p, const QRectF &bounds) +{ + render(p, QString(), bounds); +} + +void DSvgRenderer::render(QPainter *p, const QString &elementId, const QRectF &bounds) +{ + Q_D(DSvgRenderer); + if (!d->handle) + return; + + p->save(); + + const QImage image = d->getImage(QSize(p->device()->width(), p->device()->height()), elementId); + + if (bounds.isEmpty()) + p->drawImage(0, 0, image); + else + p->drawImage(bounds, image); + + p->restore(); +} + diff --git a/imageformats/svg/dsvgrenderer.h b/imageformats/svg/dsvgrenderer.h new file mode 100644 index 0000000..9c1a120 --- /dev/null +++ b/imageformats/svg/dsvgrenderer.h @@ -0,0 +1,52 @@ +// SPDX-FileCopyrightText: 2022 UnionTech Software Technology Co., Ltd. +// +// SPDX-License-Identifier: LGPL-3.0-or-later + +#ifndef DSVGRENDERER_H +#define DSVGRENDERER_H + +#include +#include + +QT_BEGIN_NAMESPACE +class QPainter; +QT_END_NAMESPACE + +class DSvgRendererPrivate; +class DSvgRenderer : public QObject +{ + Q_PROPERTY(QRectF viewBox READ viewBoxF WRITE setViewBox) +public: + explicit DSvgRenderer(QObject *parent = Q_NULLPTR); + DSvgRenderer(const QString &filename, QObject *parent = Q_NULLPTR); + DSvgRenderer(const QByteArray &contents, QObject *parent = Q_NULLPTR); + ~DSvgRenderer(); + + bool isValid() const; + + QSize defaultSize() const; + + QRect viewBox() const; + QRectF viewBoxF() const; + void setViewBox(const QRect &viewbox); + void setViewBox(const QRectF &viewbox); + + QRectF boundsOnElement(const QString &id) const; + bool elementExists(const QString &id) const; + + QImage toImage(const QSize sz, const QString &elementId = QString()) const; + +public Q_SLOTS: + bool load(const QString &filename); + bool load(const QByteArray &contents); + void render(QPainter *p); + void render(QPainter *p, const QRectF &bounds); + + void render(QPainter *p, const QString &elementId, + const QRectF &bounds = QRectF()); + +private: + Q_DECLARE_PRIVATE(DSvgRenderer) +}; + +#endif // DSVGRENDERER_H diff --git a/imageformats/svg/main.cpp b/imageformats/svg/main.cpp new file mode 100644 index 0000000..f01f5a6 --- /dev/null +++ b/imageformats/svg/main.cpp @@ -0,0 +1,53 @@ +// Copyright (C) 2016 The Qt Company Ltd. +// SPDX-License-Identifier: LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only +#include +#include + +#include "svg_p.h" + +#include +#include +#include + +QT_BEGIN_NAMESPACE + +class QSvgPlugin : public QImageIOPlugin +{ + Q_OBJECT + Q_PLUGIN_METADATA(IID "org.qt-project.Qt.QImageIOHandlerFactoryInterface" FILE "svg.json") + +public: + QStringList keys() const; + Capabilities capabilities(QIODevice *device, const QByteArray &format) const; + QImageIOHandler *create(QIODevice *device, const QByteArray &format = QByteArray()) const; +}; + +QStringList QSvgPlugin::keys() const +{ + return QStringList() << QLatin1String("svg") << QLatin1String("svgz"); +} + +QImageIOPlugin::Capabilities QSvgPlugin::capabilities(QIODevice *device, const QByteArray &format) const +{ + if (format == "svg" || format == "svgz") + return Capabilities(CanRead); + if (!format.isEmpty()) + return 0; + + Capabilities cap; + if (device->isReadable() && QSvgIOHandler::canRead(device)) + cap |= CanRead; + return cap; +} + +QImageIOHandler *QSvgPlugin::create(QIODevice *device, const QByteArray &format) const +{ + QSvgIOHandler *hand = new QSvgIOHandler(); + hand->setDevice(device); + hand->setFormat(format); + return hand; +} + +QT_END_NAMESPACE + +#include "main.moc" diff --git a/imageformats/svg/svg.cpp b/imageformats/svg/svg.cpp new file mode 100644 index 0000000..681930c --- /dev/null +++ b/imageformats/svg/svg.cpp @@ -0,0 +1,234 @@ +// Copyright (C) 2016 The Qt Company Ltd. +// SPDX-License-Identifier: LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only +#include "svg_p.h" + +#ifndef QT_NO_SVGRENDERER + +#include "qimage.h" +#include "qpixmap.h" +#include "qpainter.h" +#include "qvariant.h" +#include "qbuffer.h" +#include "qdebug.h" + +#include "dsvgrenderer.h" + +QT_BEGIN_NAMESPACE + +class QSvgIOHandlerPrivate +{ +public: + QSvgIOHandlerPrivate(QSvgIOHandler *qq) + : q(qq), loaded(false), readDone(false), backColor(Qt::transparent) + {} + + bool load(QIODevice *device); + + QSvgIOHandler *q; + DSvgRenderer r; + QSize defaultSize; + QRect clipRect; + QSize scaledSize; + QRect scaledClipRect; + bool loaded; + bool readDone; + QColor backColor; +}; + +bool QSvgIOHandlerPrivate::load(QIODevice *device) +{qDebug() << "load() started"; + if (!device) + return false; +qDebug() << "device not null"; + if (loaded) + return true; + if (q->format().isEmpty()) + q->canRead(); +qDebug() << "can read to set format"; + // # The SVG renderer doesn't handle trailing, unrelated data, so we must + // assume that all available data in the device is to be read. + bool res = false; + QBuffer *buf = qobject_cast(device); + if (buf) { + const QByteArray &ba = buf->data(); + res = r.load(QByteArray::fromRawData(ba.constData() + buf->pos(), ba.size() - buf->pos())); + buf->seek(ba.size()); + } else if (q->format() == "svgz") { + res = r.load(device->readAll()); + } else { + res = r.load(device->readAll()); + } + + if (res) { + defaultSize = QSize(r.viewBox().width(), r.viewBox().height()); + loaded = true; + } + + return loaded; +} + + +QSvgIOHandler::QSvgIOHandler() + : d(new QSvgIOHandlerPrivate(this)) +{ + +} + + +QSvgIOHandler::~QSvgIOHandler() +{ +// delete d; +} + + +bool QSvgIOHandler::canRead() const +{ + if (!device()) + return false; + if (d->loaded && !d->readDone) + return true; // Will happen if we have been asked for the size + + QByteArray buf = device()->peek(8); + if (buf.startsWith("\x1f\x8b")) { + setFormat("svgz"); + return true; + } else if (buf.contains("readDone || d->load(device())) {qDebug() << "loaded"; + bool xform = (d->clipRect.isValid() || d->scaledSize.isValid() || d->scaledClipRect.isValid()); + QSize finalSize = d->defaultSize; + QRectF bounds; + if (xform && !d->defaultSize.isEmpty()) {qDebug() << "defaultSize not empty"; + bounds = QRectF(QPointF(0,0), QSizeF(d->defaultSize)); + QPoint tr1, tr2; + QSizeF sc(1, 1); + if (d->clipRect.isValid()) { + tr1 = -d->clipRect.topLeft(); + finalSize = d->clipRect.size(); + } + if (d->scaledSize.isValid()) { + sc = QSizeF(qreal(d->scaledSize.width()) / finalSize.width(), + qreal(d->scaledSize.height()) / finalSize.height()); + finalSize = d->scaledSize; + } + if (d->scaledClipRect.isValid()) { + tr2 = -d->scaledClipRect.topLeft(); + finalSize = d->scaledClipRect.size(); + } + QTransform t; + t.translate(tr2.x(), tr2.y()); + t.scale(sc.width(), sc.height()); + t.translate(tr1.x(), tr1.y()); + bounds = t.mapRect(bounds); + } + if (!finalSize.isEmpty()) {qDebug() << "finalSize not empty"; + if (bounds.isEmpty() && d->backColor.alpha() == 0) { + *image = d->r.toImage(finalSize); + } else { + *image = QImage(finalSize, QImage::Format_ARGB32_Premultiplied); + image->fill(d->backColor.rgba()); + QPainter p(image); + p.setRenderHints(QPainter::SmoothPixmapTransform); + d->r.render(&p, bounds); + p.end(); + } + }qDebug() << "readDone"; + d->readDone = true; + return true; + } + + return false; +} + + +QVariant QSvgIOHandler::option(ImageOption option) const +{ + switch(option) { + case ImageFormat: + return QImage::Format_ARGB32_Premultiplied; + break; + case Size: + d->load(device()); + return d->defaultSize; + break; + case ClipRect: + return d->clipRect; + break; + case ScaledSize: + return d->scaledSize; + break; + case ScaledClipRect: + return d->scaledClipRect; + break; + case BackgroundColor: + return d->backColor; + break; + default: + break; + } + return QVariant(); +} + + +void QSvgIOHandler::setOption(ImageOption option, const QVariant & value) +{ + switch(option) { + case ClipRect: + d->clipRect = value.toRect(); + break; + case ScaledSize: + d->scaledSize = value.toSize(); + break; + case ScaledClipRect: + d->scaledClipRect = value.toRect(); + break; + case BackgroundColor: + d->backColor = value.value(); + break; + default: + break; + } +} + + +bool QSvgIOHandler::supportsOption(ImageOption option) const +{ + switch(option) + { + case ImageFormat: + case Size: + case ClipRect: + case ScaledSize: + case ScaledClipRect: + case BackgroundColor: + return true; + default: + break; + } + return false; +} + + +bool QSvgIOHandler::canRead(QIODevice *device) +{ + QByteArray buf = device->peek(8); + return buf.startsWith("\x1f\x8b") || buf.contains(" + +QT_BEGIN_NAMESPACE + +class QImage; +class QByteArray; +class QIODevice; +class QVariant; + +class QSvgIOHandlerPrivate; +class QSvgIOHandler : public QImageIOHandler +{ +public: + QSvgIOHandler(); + ~QSvgIOHandler(); + virtual bool canRead() const; + virtual QByteArray name() const; + virtual bool read(QImage *image); + static bool canRead(QIODevice *device); + virtual QVariant option(ImageOption option) const; + virtual void setOption(ImageOption option, const QVariant & value); + virtual bool supportsOption(ImageOption option) const; + +private: + QSvgIOHandlerPrivate *d; +}; + +QT_END_NAMESPACE + +#endif // QSVGIOHANDLER_H