diff --git a/setup.py b/setup.py index c969280..7edc290 100755 --- a/setup.py +++ b/setup.py @@ -1,36 +1,136 @@ #!/usr/bin/env python2.7 import glob +import numpy as np import os +import sys +from distutils.command.build_ext import build_ext from setuptools import setup +from setuptools.extension import Extension + + +try: + import _tkinter +except (ImportError, OSError): + # pypy emits an oserror + _tkinter = None + + +# Borrowed from https://bitbucket.org/django/django/src/tip/setup.py +def fullsplit(path, result=None): + """ + Split a pathname into components (the opposite of os.path.join) in a + platform-neutral way. + """ + if result is None: + result = [] + head, tail = os.path.split(path) + if head == "": + return [tail] + result + if head == path: + return result + return fullsplit(head, [tail] + result) + + +def _add_directory(path, dir, where=None): + if dir is None: + return + dir = os.path.realpath(dir) + if os.path.isdir(dir) and dir not in path: + if where is None: + path.append(dir) + else: + path.insert(where, dir) + + +class sgdf_build_ext(build_ext): + def _find_library_file(self, library): + # Fix for 3.2.x <3.2.4, 3.3.0, shared lib extension is the python shared + # lib extension, not the system shared lib extension: e.g. .cpython-33.so + # vs .so. See Python bug http://bugs.python.org/16754 + if 'cpython' in self.compiler.shared_lib_extension: + existing = self.compiler.shared_lib_extension + self.compiler.shared_lib_extension = "." + existing.split('.')[-1] + ret = self.compiler.find_library_file( + self.compiler.library_dirs, library) + self.compiler.shared_lib_extension = existing + return ret + else: + return self.compiler.find_library_file( + self.compiler.library_dirs, library) + + def build_extensions(self): + # Extra support for _numpytk native library + # Borrowed from https://github.com/python-pillow/Pillow/blob/master/setup.py + + exts = [] + + if _tkinter: + TCL_VERSION = _tkinter.TCL_VERSION[:3] + + version = TCL_VERSION[0] + TCL_VERSION[2] + if self._find_library_file("tcl" + version): + tcl_feature = "tcl" + version + elif self._find_library_file("tcl" + TCL_VERSION): + tcl_feature = "tcl" + TCL_VERSION + if self._find_library_file("tk" + version): + tk_feature = "tk" + version + elif self._find_library_file("tk" + TCL_VERSION): + tk_feature = "tk" + TCL_VERSION + + if sys.platform == "darwin": + # locate Tcl/Tk frameworks + frameworks = [] + framework_roots = ["/Library/Frameworks", + "/System/Library/Frameworks"] + for root in framework_roots: + root_tcl = os.path.join(root, "Tcl.framework") + root_tk = os.path.join(root, "Tk.framework") + if (os.path.exists(root_tcl) and os.path.exists(root_tk)): + print("--- using frameworks at %s" % root) + frameworks = ["-framework", "Tcl", "-framework", "Tk"] + dir = os.path.join(root_tcl, "Headers") + _add_directory(self.compiler.include_dirs, dir, 0) + dir = os.path.join(root_tk, "Headers") + _add_directory(self.compiler.include_dirs, dir, 1) + break + assert frameworks, "Cannot find Tcl/Tk headers" + if frameworks: + exts.append(Extension("sgdf.gui._numpytk", + glob.glob(os.path.join("sgdf", "gui", "_numpytk", "*")), + extra_compile_args=frameworks, + extra_link_args=frameworks, + include_dirs=[np.get_include(), "/usr/X11/include"])) + else: + assert tcl_feature and tk_feature + exts.append(Extension("sgdf.gui._numpytk", + glob.glob(os.path.join("sgdf", "gui", "_numpytk", "*")), + libraries=[tcl_feature, tk_feature], + include_dirs=[np.get_include()])) + + self.extensions[:] = exts + + build_ext.build_extensions(self) if __name__ == "__main__": - # Borrowed from https://bitbucket.org/django/django/src/tip/setup.py - def fullsplit(path, result=None): - """ - Split a pathname into components (the opposite of os.path.join) in a - platform-neutral way. - """ - if result is None: - result = [] - head, tail = os.path.split(path) - if head == "": - return [tail] + result - if head == path: - return result - return fullsplit(head, [tail] + result) - - packages, data_files = [], [] + packages, extensions, data_files = [], [], [] root_dir = os.path.dirname(__file__) if root_dir != "": os.chdir(root_dir) + for dirpath, dirnames, filenames in os.walk("sgdf"): - # Ignore dirnames that start with "." - for i, dirname in enumerate(dirnames): - if dirname.startswith("."): - del dirnames[i] - if "__init__.py" in filenames: + if os.path.basename(dirpath).startswith("_"): + # if os.path.basename(dirpath) == "_numpytk": + # # The _numpytk library is configured manually + # continue + c_sources = filter(lambda name: name.endswith(".c"), filenames) + print "Extension:", ".".join(fullsplit(dirpath)) + extension = Extension(".".join(fullsplit(dirpath)), + map(lambda name: os.path.join(dirpath, name), c_sources), + include_dirs=[np.get_include()]) + extensions.append(extension) + elif "__init__.py" in filenames: packages.append(".".join(fullsplit(dirpath))) elif filenames: data_files.append([dirpath, [os.path.join(dirpath, f) for f in filenames]]) @@ -44,4 +144,6 @@ def fullsplit(path, result=None): data_files=data_files, install_requires=["matplotlib", "numpy", "scipy"], test_suite="nose.collector", - tests_require=["nose", "unittest2"]) + tests_require=["nose", "unittest2"], + cmdclass={"build_ext": sgdf_build_ext}, + ext_modules=extensions) diff --git a/sgdf/gui/_numpytk/numpytk.c b/sgdf/gui/_numpytk/numpytk.c new file mode 100644 index 0000000..74e3819 --- /dev/null +++ b/sgdf/gui/_numpytk/numpytk.c @@ -0,0 +1,99 @@ +#include +#include "Python.h" +#include "numpy/arrayobject.h" +#include "tk.h" +#include "tk.h" + +/* copied from _tkinter.c (this isn't as bad as it may seem: for new + versions, we use _tkinter's interpaddr hook instead, and all older + versions use this structure layout) */ + +typedef struct { + PyObject_HEAD + Tcl_Interp* interp; +} TkappObject; + + +static PyArrayObject *lookup_ndarray(const char *id) { + return (PyArrayObject *) atol(id); +} + +static int PyNumpyTkPut(ClientData clientdata, Tcl_Interp* interp, int argc, const char **argv) { + Tk_PhotoImageBlock block; + Tk_PhotoHandle photo; + PyArrayObject *ndarray; + + if (argc != 3) { + Tcl_AppendResult(interp, "usage: ", argv[0], + " destPhoto srcNdarray", (char *) NULL); + return TCL_ERROR; + } + + photo = Tk_FindPhoto(interp, argv[1]); + if (photo == NULL) { + Tcl_AppendResult(interp, "destination photo must exist", (char *) NULL); + return TCL_ERROR; + } + + ndarray = lookup_ndarray(argv[2]); + + if (PyArray_NDIM(ndarray) != 3) { + Tcl_AppendResult(interp, "ndarray dimensions should be 3", (char *) NULL); + return TCL_ERROR; + } + + npy_intp *ndarray_dims = PyArray_DIMS(ndarray); + if (ndarray_dims[2] != 3) { + Tcl_AppendResult(interp, "ndarray 3rd dimension should have size 3", (char *) NULL); + return TCL_ERROR; + } + + npy_intp *ndarray_strides = PyArray_STRIDES(ndarray); + + block.pixelSize = ndarray_strides[1]; + block.offset[0] = 0; + block.offset[1] = 1; + block.offset[2] = 2; + block.offset[3] = 0; + block.height = ndarray_dims[0]; + block.width = ndarray_dims[1]; + block.pitch = ndarray_strides[0]; + block.pixelPtr = (unsigned char *) PyArray_BYTES(ndarray); + + return Tk_PhotoPutBlock(interp, photo, &block, 0, 0, block.width, block.height, + TK_PHOTO_COMPOSITE_SET); +} + +static PyObject *_tkinit(PyObject *self, PyObject *args) { + Tcl_Interp *interp; + + Py_ssize_t arg; + int is_interp; + if (!PyArg_ParseTuple(args, "ni", &arg, &is_interp)) + return NULL; + + if (is_interp) + interp = (Tcl_Interp *) arg; + else { + TkappObject *app; + /* Do it the hard way. This will break if the TkappObject + layout changes */ + app = (TkappObject *) arg; + interp = app->interp; + } + + Tcl_CreateCommand(interp, "PyNumpyTkPut", &PyNumpyTkPut, (ClientData) 0, (Tcl_CmdDeleteProc*) NULL); + + Py_INCREF(Py_None); + return Py_None; +} + +static PyMethodDef functions[] = { + {"tkinit", (PyCFunction)_tkinit, 1}, + {NULL, NULL} +}; + +PyMODINIT_FUNC +init_numpytk(void) { + Py_InitModule("_numpytk", functions); +} diff --git a/sgdf/gui/editor/canvas.py b/sgdf/gui/editor/canvas.py index 3a1d87f..f17b581 100644 --- a/sgdf/gui/editor/canvas.py +++ b/sgdf/gui/editor/canvas.py @@ -1,6 +1,6 @@ import Tkinter as tk -from PIL import Image, ImageTk from sgdf.benchmarking import log_timer +from sgdf.gui.numpytk import PhotoImage class EditorViewCanvas(tk.Canvas): @@ -16,7 +16,7 @@ def draw_numpy(self, ndarray): """ with log_timer("EditorViewCanvas.draw_numpy"): if self.active_image_container is None: - self.active_image_container = ImageTk.PhotoImage(Image.fromarray(ndarray)) + self.active_image_container = PhotoImage(ndarray=ndarray) self.itemconfig(self.active_image_id, image=self.active_image_container) else: - self.active_image_container.paste(Image.fromarray(ndarray)) + self.active_image_container.paste(ndarray=ndarray) diff --git a/sgdf/gui/numpytk.py b/sgdf/gui/numpytk.py new file mode 100644 index 0000000..d7d1906 --- /dev/null +++ b/sgdf/gui/numpytk.py @@ -0,0 +1,49 @@ +import Tkinter as tk +import numpy as np + + +class PhotoImage(object): + def __init__(self, ndarray=None, size=None, **kwargs): + assert ndarray is not None or size is not None, "Please give me either a image or size" + + if ndarray is not None: + size = ndarray.shape[:2] + elif size is not None: + ndarray = np.zeros(size) + self.__nparray = ndarray + + kwargs["height"], kwargs["width"] = size[:2] + self.__size = self.__nparray.shape + self.__photo = tk.PhotoImage(**kwargs) + self.tk = self.__photo.tk + self.paste(ndarray) + + def __str__(self): + """ + Get the Tkinter photo image identifier. This method is automatically called by Tkinter + whenever a PhotoImage object is passed to a Tkinter method. + + """ + return str(self.__photo) + + def width(self): + return self.__size[0] + + def height(self): + return self.__size[1] + + def paste(self, ndarray): + interp = self.__photo.tk + try: + interp.call("PyNumpyTkPut", self.__photo, id(ndarray)) + except tk.TclError: + # activate Tkinter hook + try: + from sgdf.gui import _numpytk + try: + _numpytk.tkinit(interp.interpaddr(), 1) + except AttributeError: + _numpytk.tkinit(id(interp), 0) + interp.call("PyNumpyTkPut", self.__photo, id(ndarray)) + except (ImportError, AttributeError, tk.TclError): + raise # configuration problem; cannot attach to Tkinter