Skip to content

Commit

Permalink
Add support for Python-based image providers. Fixes #1
Browse files Browse the repository at this point in the history
  • Loading branch information
thp committed Oct 5, 2013
1 parent f1cbc8c commit 211a04f
Show file tree
Hide file tree
Showing 15 changed files with 631 additions and 2 deletions.
3 changes: 3 additions & 0 deletions README
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@ To run one of the included examples (after installing the plugin):
qmlscene examples/events/main.qml
qmlscene examples/atexit/main.qml
qmlscene examples/notes/main.qml
qmlscene examples/imageprovider/imageprovider.qml
qmlscene examples/mandelbrot/mandelbrot.qml
qmlscene examples/image_loader/image_loader.qml


Website and Git repository:
Expand Down
50 changes: 50 additions & 0 deletions examples/image_loader/image_loader.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
#
# PyOtherSide: Asynchronous Python 3 Bindings for Qt 5
# Copyright (c) 2011, 2013, Thomas Perl <[email protected]>
#
# 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
import random

from PIL import Image
from PIL import ImageDraw

def render(image_id, requested_size):
# width and height will be -1 if not set in QML
if requested_size == (-1, -1):
requested_size = (300, 300)

img = Image.new('RGBA', requested_size)

filename = os.path.join(os.path.dirname(__file__), image_id)
logo = Image.open(filename)

for x in range(100):
img.paste(logo, (random.randint(0, requested_size[0]), random.randint(0, requested_size[1])))

draw = ImageDraw.Draw(img)
for x in range(100):
draw.text((random.randint(0, requested_size[0]), random.randint(0, requested_size[1])),
'PyOtherSide PIL Test',
fill=(random.randint(10, 255), random.randint(10, 255), random.randint(10, 255)))
del draw

b, g, r, a = img.split()
img = Image.merge("RGBA", (r, g, b, a))
return bytearray(img.tostring()), img.size, pyotherside.format_argb32

pyotherside.set_image_provider(render)
39 changes: 39 additions & 0 deletions examples/image_loader/image_loader.qml
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@

/**
* PyOtherSide: Asynchronous Python 3 Bindings for Qt 5
* Copyright (c) 2011, 2013, Thomas Perl <[email protected]>
*
* 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

Python {
Component.onCompleted: {
// Add the directory of this .qml file to the search path
addImportPath(Qt.resolvedUrl('.').substr('file://'.length));

importModule('image_loader', function () {
image.source = 'image://python/pyotherside.png';
});
}

onError: console.log('Python error: ' + traceback)
}
}
Binary file added examples/image_loader/pyotherside.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
44 changes: 44 additions & 0 deletions examples/imageprovider/imageprovider.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
#
# PyOtherSide: Asynchronous Python 3 Bindings for Qt 5
# Copyright (c) 2011, 2013, Thomas Perl <[email protected]>
#
# 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 math

def render(image_id, requested_size):
print('image_id: "{image_id}", size: {requested_size}'.format(**locals()))

# width and height will be -1 if not set in QML
if requested_size == (-1, -1):
requested_size = (300, 300)

width, height = requested_size

# center for circle
cx, cy = width/2, 10

pixels = []
for y in range(height):
for x in range(width):
pixels.extend(reversed([
255, # alpha
int(10 + 10 * ((x - y * 0.5) % 20)), # red
20 + 10 * (y % 20), # green
int(255 * abs(math.sin(0.3*math.sqrt((cx-x)**2 + (cy-y)**2)))) # blue
]))
return bytearray(pixels), (width, height), pyotherside.format_argb32

pyotherside.set_image_provider(render)
39 changes: 39 additions & 0 deletions examples/imageprovider/imageprovider.qml
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@

/**
* PyOtherSide: Asynchronous Python 3 Bindings for Qt 5
* Copyright (c) 2011, 2013, Thomas Perl <[email protected]>
*
* 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

Python {
Component.onCompleted: {
// Add the directory of this .qml file to the search path
addImportPath(Qt.resolvedUrl('.').substr('file://'.length));

importModule('imageprovider', function () {
image.source = 'image://python/image-id-passed-from-qml';
});
}

onError: console.log('Python error: ' + traceback)
}
}
64 changes: 64 additions & 0 deletions examples/mandelbrot/mandelbrot.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
#
# PyOtherSide: Asynchronous Python 3 Bindings for Qt 5
# Copyright (c) 2011, 2013, Thomas Perl <[email protected]>
#
# 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 random

COLORS = [(255, 255, 255, 255), (255, 0, 0, 0)]

for i in range(30):
COLORS.insert(1, (
255, # alpha
random.randint(30, 255), # red
random.randint(30, 255), # green
random.randint(30, 255), # blue
))

# Of course, in production code, you don't want to calculate your mandelbrot
# set in CPython for performance reasons. This is just an example of what is
# possible with the image provider support in PyOtherSide.
def mandelbrot(x, y, steps):
a = b = 0
for i in range(steps):
a, b = a**2 - b**2 + x, 2*a*b + y
if a**2 + b**2 >= 4:
break
return i

def render_mandelbrot(image_id, requested_size):
# when the url is: "image://python/xmin/xmax/ymin/ymax"
# the image_id is: "xmin/xmax/ymin/ymax"

parts = [float(x) for x in image_id.split('/')]
x_range = parts[0:2]
y_range = parts[2:4]

# width and height will be -1 if not set in QML
if requested_size == (-1, -1):
requested_size = (200, 200)

width, height = requested_size

pixels = []
for y in range(height):
yy = y_range[0] + (y_range[1] - y_range[0]) * y / height
for x in range(width):
xx = x_range[0] + (x_range[1] - x_range[0]) * x / width
pixels.extend(reversed(COLORS[mandelbrot(xx, yy, len(COLORS))]))
return bytearray(pixels), (width, height), pyotherside.format_argb32

pyotherside.set_image_provider(render_mandelbrot)
125 changes: 125 additions & 0 deletions examples/mandelbrot/mandelbrot.qml
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@

/**
* PyOtherSide: Asynchronous Python 3 Bindings for Qt 5
* Copyright (c) 2011, 2013, Thomas Perl <[email protected]>
*
* 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

property real zoomFactor: 1.2
property real zoom: 2.5
property real xCenter: -0.5
property real yCenter: 0.0
property real xRange: zoom
property real yRange: zoom * height / width
property real mLeft: xCenter - xRange / 2
property real mRight: xCenter + xRange / 2
property real mTop: yCenter - yRange / 2
property real mBottom: yCenter + yRange / 2

onXRangeChanged: updateImageTimer.start()
onYRangeChanged: updateImageTimer.start()
onXCenterChanged: updateImageTimer.start()
onYCenterChanged: updateImageTimer.start()

sourceSize {
width: image.width / 4
height: image.height / 4
}

MouseArea {
anchors.fill: parent
property real lastPosX
property real lastPosY
onPressed: {
lastPosX = mouse.x;
lastPosY = mouse.y;
}

onPositionChanged: {
var diffX = (mouse.x - lastPosX);
var diffY = (mouse.y - lastPosY);

image.xCenter -= diffX / image.width * image.xRange;
image.yCenter -= diffY / image.height * image.yRange;

lastPosX = mouse.x;
lastPosY = mouse.y;
}
}

Column {
spacing: 10
anchors {
right: parent.right
top: parent.top
margins: 10
}

Rectangle {
width: 30; height: 30
color: '#aaffffff'
Text {
anchors.centerIn: parent
text: '-'
}
MouseArea {
anchors.fill: parent
onClicked: image.zoom *= image.zoomFactor
}
}

Rectangle {
width: 30; height: 30
color: '#aaffffff'
Text {
anchors.centerIn: parent
text: '+'
}
MouseArea {
anchors.fill: parent
onClicked: image.zoom /= image.zoomFactor
}
}
}

Python {
id: py

Component.onCompleted: {
// Add the directory of this .qml file to the search path
py.addImportPath(Qt.resolvedUrl('.').substr('file://'.length));

py.importModule('mandelbrot', function () {
// Do the first image update once the module is loaded
updateImageTimer.start();
});
}

onError: console.log('Python error: ' + traceback)
}

Timer {
id: updateImageTimer
interval: 100
onTriggered: image.source = 'image://python/' + image.mLeft + '/' + image.mRight + '/' + image.mTop + '/' + image.mBottom
}
}
8 changes: 8 additions & 0 deletions src/pyotherside_plugin.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@

#include "qpython_priv.h"
#include "qpython.h"
#include "qpython_imageprovider.h"

#include "pyotherside_plugin.h"

Expand All @@ -37,6 +38,13 @@ PyOtherSideExtensionPlugin::~PyOtherSideExtensionPlugin()
{
}

void
PyOtherSideExtensionPlugin::initializeEngine(QQmlEngine *engine, const char *uri)
{
Q_ASSERT(QString(PYOTHERSIDE_PLUGIN_ID) == uri);
engine->addImageProvider(PYOTHERSIDE_IMAGEPROVIDER_ID, new QPythonImageProvider);
}

void
PyOtherSideExtensionPlugin::registerTypes(const char *uri)
{
Expand Down
Loading

0 comments on commit 211a04f

Please sign in to comment.