diff --git a/demo/DrawLinesFromBuffer.py b/demo/DrawLinesFromBuffer.py new file mode 100644 index 00000000..1761e3b7 --- /dev/null +++ b/demo/DrawLinesFromBuffer.py @@ -0,0 +1,131 @@ +#!/usr/bin/env python + +import time + +import wx + +npts = 100000 + +def TestLinesFromBuffer(dc, log): + global npts + try: + import numpy as np + start = time.time() + + w, h = dc.GetSize() + + vs = np.linspace(0, 1, npts).reshape((npts, 1)) + + x1 = np.cos(vs * 12 * np.pi) * w/2 * vs + w/2 + x2 = np.cos(vs * 16 * np.pi) * w/2 * vs + w/2 + y1 = np.sin(vs * 12 * np.pi) * w/2 * vs + h/2 + y2 = np.sin(vs * 16 * np.pi) * w/2 * vs + h/2 + + + # Data has to be the same size as a C integer + pts1 = np.append(x1, y1, 1).astype('intc') + pts2 = np.append(x2, y2, 1).astype('intc') + + dc.SetPen(wx.BLACK_PEN) + t1 = time.time() + dc.DrawLines(pts1) + t2 = time.time() + + dc.SetPen(wx.RED_PEN) + t3 = time.time() + dc.DrawLinesFromBuffer(pts2) + t4 = time.time() + + log.write("%s pts: %s seconds with DrawLines %s seconds with DrawLinesFromBuffer\n" % (npts, t2 - t1, t4 - t3)) + + except ImportError: + log.write("Couldn't import numpy") + pass + + + +# Class used for all the various sample pages; the mechanics are the same +# for each one with regards to the notebook. The only difference is +# the function we use to draw on it. +class DrawPanel(wx.Panel): + def __init__(self, parent, drawFun, log): + wx.Panel.__init__(self, parent, -1) + self.SetBackgroundColour(wx.WHITE) + + self.log = log + self.drawFun = drawFun + self.Bind(wx.EVT_PAINT, self.OnPaint) + + def OnSize(self, evt): + self.Refresh() + + def OnPaint(self, evt): + dc = wx.PaintDC(self) + dc.Clear() + self.drawFun(dc,self.log) + + +#---------------------------------------------------------------------- + +def runTest(frame, nb, log): + global npts + + panel = wx.Panel(nb, -1) + vsizer = wx.BoxSizer(wx.VERTICAL) + hsizer = wx.BoxSizer(wx.HORIZONTAL) + hsizer.Add(wx.StaticText(panel, -1, "# of Points"), 0, wx.ALIGN_CENTER|wx.ALL, 5) + npts_ctrl = wx.TextCtrl(panel, -1, str(npts)) + hsizer.Add(npts_ctrl, 0, wx.ALIGN_CENTER|wx.ALL, 5) + + button = wx.Button(panel, -1, "Refresh") + hsizer.Add(button, 0, wx.ALIGN_CENTER|wx.ALL, 5) + vsizer.Add(hsizer, 0, wx.ALIGN_CENTER, 0) + + win = DrawPanel(panel, TestLinesFromBuffer, log) + vsizer.Add(win, 1, wx.GROW, 0) + panel.SetSizer(vsizer) + + def update_npts(evt): + global npts + + val = npts_ctrl.GetValue() + try: + npts = int(val) + except ValueError: + log.write("Error converting %s to an int" % (val)) + win.Refresh() + button.Bind(wx.EVT_BUTTON, update_npts) + + return panel + +#---------------------------------------------------------------------- + + +overview = """\ + +The DrawLinesFromBuffer function has been added to wx.DC to provide +a way to draw directly from a numpy array or other object that implements +the python buffer protocol. + +
+ DrawLinesFromBuffer(pyBuff) ++ +The buffer object needs to provide an array of C integers organized as +x, y point pairs. The size of a C integer is platform dependent. +With numpy, the intc data type will provide the appropriate element size. + +If called with an object that doesn't support +the python buffer protocol, or if the underlying element size does not +match the size of a C integer, a TypeError exception is raised. If +the buffer provided has float data with the same element size as a +C integer, no error will be raised, but the lines will not be drawn +in the appropriate places. +""" + + +if __name__ == '__main__': + import sys,os + import run + run.main(['', os.path.basename(sys.argv[0])] + sys.argv[1:]) + diff --git a/demo/demodata.py b/demo/demodata.py index 4a75bda1..8ae01e90 100644 --- a/demo/demodata.py +++ b/demo/demodata.py @@ -50,6 +50,7 @@ _treeList = [ 'ActivityIndicator', 'GenericCheckBox', 'CheckListCtrl', + 'DrawLinesFromBuffer', ]), # managed windows == things with a (optional) caption you can close @@ -275,6 +276,7 @@ _treeList = [ 'Cairo_Snippets', 'ColourDB', 'DragScroller', + 'DrawLinesFromBuffer', 'DrawXXXList', 'FileHistory', 'FontEnumerator', diff --git a/etg/dc.py b/etg/dc.py index 0fd9514f..5cbac842 100644 --- a/etg/dc.py +++ b/etg/dc.py @@ -310,6 +310,9 @@ def run(): '(PyObject* textList, PyObject* pyPoints, PyObject* foregroundList, PyObject* backgroundList)', body="return wxPyDrawTextList(*self, textList, pyPoints, foregroundList, backgroundList);") + c.addCppMethod('PyObject*', '_DrawLinesFromBuffer', + '(PyObject* pyBuff)', + body="return wxPyDrawLinesFromBuffer(*self, pyBuff);") c.addPyMethod('DrawPointList', '(self, points, pens=None)', doc="""\ @@ -475,6 +478,28 @@ def run(): """) + c.addPyMethod('DrawLinesFromBuffer', '(self, pyBuff)', + doc="""\ + Implementation of DrawLines that can use numpy arrays, or anything else that uses the + python buffer protocol directly without any element conversion. This provides a + significant performance increase over the standard DrawLines function. + + The pyBuff argument needs to provide an array of C integers organized as + x, y point pairs. The size of a C integer is platform dependent. + With numpy, the intc data type will provide the appropriate element size. + + If called with an object that doesn't support + the python buffer protocol, or if the underlying element size does not + match the size of a C integer, a TypeError exception is raised. If + the buffer provided has float data with the same element size as a + C integer, no error will be raised, but the lines will not be drawn + in the appropriate places. + + :param pyBuff: A python buffer containing integer pairs + """, + body="""\ + return self._DrawLinesFromBuffer(pyBuff) + """) #----------------------------------------------------------------- diff --git a/src/dc_ex.cpp b/src/dc_ex.cpp index c5a120b6..24e13a33 100644 --- a/src/dc_ex.cpp +++ b/src/dc_ex.cpp @@ -9,7 +9,6 @@ // Licence: wxWindows license //-------------------------------------------------------------------------- - typedef bool (*wxPyDrawListOp_t)(wxDC& dc, PyObject* coords); PyObject* wxPyDrawXXXList(wxDC& dc, wxPyDrawListOp_t doDraw, @@ -466,3 +465,53 @@ error0: PyErr_SetString(PyExc_TypeError, "Expected a sequence of length-2 sequences or wx.Points."); return NULL; } + + +PyObject* wxPyDrawLinesFromBuffer(wxDC& dc, PyObject* pyBuff) +{ + wxPyBlock_t blocked = wxPyBeginBlockThreads(); + Py_buffer view; + PyObject* retval; + + + if (!PyObject_CheckBuffer(pyBuff)) { + goto err0; + } + + if (PyObject_GetBuffer(pyBuff, &view, PyBUF_CONTIG) < 0) { + goto err1; + } + + if (view.itemsize * 2 != sizeof(wxPoint)) { + goto err2; + } + + dc.DrawLines(view.len / view.itemsize / 2, (wxPoint *)view.buf); + + PyBuffer_Release(&view); + + Py_INCREF(Py_None); + retval = Py_None; + goto exit; + + + err0: + PyErr_SetString(PyExc_TypeError, "Expected a buffer object"); + retval = NULL; + goto exit; + + err1: + // PyObject_GetBuffer raises exception already + retval = NULL; + goto exit; + + err2: + PyErr_SetString(PyExc_TypeError, "Item size does not match wxPoint size"); + retval = NULL; + goto exit; + + exit: + wxPyEndBlockThreads(blocked); + return retval; +} + diff --git a/unittests/test_dcDrawLinesFromBuffer.py b/unittests/test_dcDrawLinesFromBuffer.py new file mode 100644 index 00000000..3bed6053 --- /dev/null +++ b/unittests/test_dcDrawLinesFromBuffer.py @@ -0,0 +1,65 @@ +import unittest +from unittests import wtc +import wx +import random +try: + import numpy as np + haveNumpy = True +except ImportError: + haveNumpy = False + +#--------------------------------------------------------------------------- + +w = 600 +h = 400 + +def makeRandomLines(): + lines = [] + + for i in range(num): + x1 = random.randint(0, w) + y1 = random.randint(0, h) + x2 = random.randint(0, w) + y2 = random.randint(0, h) + lines.append( (x1,y1, x2,y2) ) + + return lines + + + + + +#--------------------------------------------------------------------------- + + +class dcDrawLists_Tests(wtc.WidgetTestCase): + + @unittest.skipIf(not haveNumpy, "Numpy required for this test") + def test_dcDrawLinesFromBuffer(self): + pnl = wx.Panel(self.frame) + self.frame.SetSize((w,h)) + dc = wx.ClientDC(pnl) + dc.SetPen(wx.Pen("BLACK", 1)) + + xs = np.linspace(0, w, w + 1) + ys = np.sin(xs / w * 2 * np.pi) + + xs.shape = xs.size, 1 + ys.shape = ys.size, 1 + pts = np.append(xs, ys, 1) + + dc.DrawLinesFromBuffer(pts.astype('intc')) + self.assertRaises(TypeError, dc.DrawLinesFromBuffer, + pts.astype('int64') if np.intc(1).nbytes != np.int64(1).nbytes else pts.astype('int32')) + self.assertRaises(TypeError, dc.DrawLinesFromBuffer, pts.tolist()) + del dc + + + + + + +#--------------------------------------------------------------------------- + +if __name__ == '__main__': + unittest.main()