diff --git a/docs/index.rst b/docs/index.rst index 6fbfa1b..63b7e9e 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -380,6 +380,12 @@ The following constants have been added in PyOtherSide 1.3: **pyotherside.version** Version of PyOtherSide as string. +.. versionadded:: 1.5.0 + +The following constants have been added in PyOtherSide 1.5: + +**pyotherside.format_svg_data** + SVG image XML data Data Type Mapping @@ -465,9 +471,11 @@ The image provider must return a tuple ``(data, size, format)``: pixel data in pixels. **format** - The pixel format of ``data`` (see `constants`_), or + The pixel format of ``data`` (see `constants`_), ``pyotherside.format_data`` if ``data`` contains an - encoded (PNG/JPEG) image instead of raw pixel data. + encoded (PNG/JPEG) image instead of raw pixel data + or ``pyotherside.format_svg_data`` if ``data`` contains + SVG image XML data. In order to register the image provider with PyOtherSide for use as provider for ``image://python/`` URLs, the image provider function diff --git a/examples/imageprovider_svg_data.py b/examples/imageprovider_svg_data.py new file mode 100644 index 0000000..67d27b9 --- /dev/null +++ b/examples/imageprovider_svg_data.py @@ -0,0 +1,32 @@ +# +# PyOtherSide: Asynchronous Python 3 Bindings for Qt 5 +# Copyright (c) 2011, 2013, Thomas Perl +# +# Permission to use, copy, modify, and/or distribute this software for any +# purpose with or without fee is hereby granted, provided that the above +# copyright notice and this permission notice appear in all copies. +# +# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +# REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND +# FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +# INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +# LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +# OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +# PERFORMANCE OF THIS SOFTWARE. +# + +import pyotherside + +import os + +def load_data(image_id, requested_size): + # If you return data in the format "pyotherside.format_svg_data" requested_size + # is is used as the target size when rendering the SVG image + + # We use the image id to get name of the SVG file to render + with open(image_id, 'rb') as f: + svg_image_data = f.read() + + return bytearray(svg_image_data), requested_size, pyotherside.format_svg_data + +pyotherside.set_image_provider(load_data) diff --git a/examples/imageprovider_svg_data.qml b/examples/imageprovider_svg_data.qml new file mode 100644 index 0000000..f04d87d --- /dev/null +++ b/examples/imageprovider_svg_data.qml @@ -0,0 +1,41 @@ + +/** + * PyOtherSide: Asynchronous Python 3 Bindings for Qt 5 + * Copyright (c) 2011, 2013, Thomas Perl + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH + * REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND + * FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, + * INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM + * LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR + * OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR + * PERFORMANCE OF THIS SOFTWARE. + **/ + +import QtQuick 2.0 +import io.thp.pyotherside 1.0 + +Image { + id: image + width: 300 + height: 300 + sourceSize.width: 300 + sourceSize.height: 300 + + Python { + Component.onCompleted: { + // Add the directory of this .qml file to the search path + addImportPath(Qt.resolvedUrl('.')); + + importModule('imageprovider_svg_data', function () { + image.source = 'image://python/python_logo.svg'; + }); + } + + onError: console.log('Python error: ' + traceback) + } +} diff --git a/examples/python_logo.svg b/examples/python_logo.svg new file mode 100644 index 0000000..366f52f --- /dev/null +++ b/examples/python_logo.svg @@ -0,0 +1,113 @@ + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + diff --git a/src/qpython_imageprovider.cpp b/src/qpython_imageprovider.cpp index 4511afd..6089c90 100644 --- a/src/qpython_imageprovider.cpp +++ b/src/qpython_imageprovider.cpp @@ -22,6 +22,8 @@ #include "ensure_gil_state.h" #include +#include +#include QPythonImageProvider::QPythonImageProvider() @@ -109,7 +111,9 @@ QPythonImageProvider::requestImage(const QString &id, QSize *size, const QSize & } switch (format) { - case -1: /* pyotherside.format_data */ + case PYOTHERSIDE_IMAGE_FORMAT_ENCODED: /* pyotherside.format_data */ + break; + case PYOTHERSIDE_IMAGE_FORMAT_SVG: /* pyotherside.format_svg_data */ break; case QImage::Format_Mono: case QImage::Format_MonoLSB: @@ -141,7 +145,7 @@ QPythonImageProvider::requestImage(const QString &id, QSize *size, const QSize & // While we could re-pack the data to be aligned, we don't want to do that // for performance reasons. // If we're using 32-bit data (e.g. ARGB32), it will always be aligned. - if (format != -1 && bitsPerPixel != 32) { + if (format >= 0 && bitsPerPixel != 32) { if ((bitsPerPixel * width) % 32 != 0) { // If actualBytes > requiredBytes, we can check if there are enough // bytes to consider the data 32-bit aligned (from Python) and avoid @@ -155,7 +159,7 @@ QPythonImageProvider::requestImage(const QString &id, QSize *size, const QSize & } } - if (format != -1 && requiredBytes > actualBytes) { + if (format >= 0 && requiredBytes > actualBytes) { qDebug() << "Format" << (enum QImage::Format)format << "at size" << QSize(width, height) << "requires at least" << requiredBytes << @@ -163,11 +167,72 @@ QPythonImageProvider::requestImage(const QString &id, QSize *size, const QSize & goto cleanup; } + if (format < 0) { + switch (format) { + case PYOTHERSIDE_IMAGE_FORMAT_ENCODED: { + // Pixel data is actually encoded image data that we need to decode + img.loadFromData((const unsigned char*)PyByteArray_AsString(pixels), PyByteArray_Size(pixels)); + break; + } + case PYOTHERSIDE_IMAGE_FORMAT_SVG: { + // Convert the Python byte array to a QByteArray + QByteArray svgDataArray(PyByteArray_AsString(pixels), PyByteArray_Size(pixels)); - if (format == -1) { - // Pixel data is actually encoded image data that we need to decode - img.loadFromData((const unsigned char*)PyByteArray_AsString(pixels), - PyByteArray_Size(pixels)); + // Load the SVG data to the SVG renderer + QSvgRenderer renderer(svgDataArray); + + // Handle width, height or both not being set + if (width < 0 && height < 0) { + // Both Width and Height have not been set - use the defaults from the SVG data + // (each SVG image has a default size) + // NOTE: we get a -1,-1 requestedSize only if sourceSize is not set at all, + // if either width or height is set then the other one is 0, not -1 + width = renderer.defaultSize().width(); + height = renderer.defaultSize().height(); + } else { // At least width or height is valid + if (width <= 0) { + // Width is not set, use default width scaled according to height to keep + // aspect ratio + if (renderer.defaultSize().height() != 0) { // Protect from division by zero + width = (float)renderer.defaultSize().width()*((float)height/(float)renderer.defaultSize().height()); + } + } + + if (height <= 0) { + // Height is not set, use default height scaled according to width to keep + // aspect ratio + if (renderer.defaultSize().width() != 0) { // Protect from division by zero + height = (float)renderer.defaultSize().height()*((float)width/(float)renderer.defaultSize().width()); + } + } + } + + // The pixel data is actually SVG image data that we need to render at correct size + // + // Note: according to the QImage and QPainter documentation the optimal QImage + // format for drawing is Format_ARGB32_Premultiplied + img = QImage(width, height, QImage::Format_ARGB32_Premultiplied); + + // According to the documentation an empty QImage needs to be "flushed" before + // being used with QPainter to prevent rendering artifacts from showing up + img.fill(Qt::transparent); + + // Convert the Python byte array to a QByteArray + QByteArray svgDataArray(PyByteArray_AsString(pixels), PyByteArray_Size(pixels)); + + // Load the SVG data to the SVG renderer + QSvgRenderer renderer(svgDataArray); + + // Paints the rendered SVG to the QImage instance + QPainter painter(&img); + renderer.render(&painter); + break; + } + default: + qWarning() << "Unknown format" << format << + "has been specified and will not be handled."; + break; + } } else { // Need to keep a reference to the byte array object, as it contains // the backing store data for the QImage. diff --git a/src/qpython_priv.cpp b/src/qpython_priv.cpp index 653bfd7..e569336 100644 --- a/src/qpython_priv.cpp +++ b/src/qpython_priv.cpp @@ -454,7 +454,8 @@ PyOtherSide_init() PyModule_AddIntConstant(pyotherside, "format_rgb444", QImage::Format_RGB444); // Custom constant - pixels are to be interpreted as encoded image file data - PyModule_AddIntConstant(pyotherside, "format_data", -1); + PyModule_AddIntConstant(pyotherside, "format_data", PYOTHERSIDE_IMAGE_FORMAT_ENCODED); + PyModule_AddIntConstant(pyotherside, "format_svg_data", PYOTHERSIDE_IMAGE_FORMAT_SVG); // Version of PyOtherSide (new in 1.3) PyModule_AddStringConstant(pyotherside, "version", PYOTHERSIDE_VERSION); diff --git a/src/qpython_priv.h b/src/qpython_priv.h index 4e3bfd9..236beee 100644 --- a/src/qpython_priv.h +++ b/src/qpython_priv.h @@ -28,6 +28,10 @@ #include #include +enum PyOtherSideImageFormat { + PYOTHERSIDE_IMAGE_FORMAT_ENCODED = -1, + PYOTHERSIDE_IMAGE_FORMAT_SVG = -2, +}; class QPythonPriv : public QObject { Q_OBJECT diff --git a/src/src.pro b/src/src.pro index 108394d..9f6b15d 100644 --- a/src/src.pro +++ b/src/src.pro @@ -8,7 +8,7 @@ PLUGIN_IMPORT_PATH = io/thp/pyotherside TEMPLATE = lib CONFIG += qt plugin -QT += qml quick +QT += qml quick svg target.path = $$[QT_INSTALL_QML]/$$PLUGIN_IMPORT_PATH INSTALLS += target