diff --git a/.travis.yml b/.travis.yml index 991d07c2743e..ffa2f4dde1ec 100644 --- a/.travis.yml +++ b/.travis.yml @@ -119,6 +119,10 @@ script: # multiple processes - python -c "from matplotlib import font_manager" - | + echo Testing import of tkagg backend + MPLBACKEND="tkagg" python -c 'import matplotlib.pyplot as plt; print(plt.get_backend())' + echo Testing using $NPROC processes + echo The following args are passed to nose $NOSE_ARGS if [[ $BUILD_DOCS == false ]]; then export MPL_REPO_DIR=$PWD # needed for pep8-conformance test of the examples gdb -return-child-result -batch -ex r -ex bt --args python tests.py -s --processes=$NPROC --process-timeout=300 $TEST_ARGS diff --git a/appveyor.yml b/appveyor.yml new file mode 100644 index 000000000000..1e1fcfb755f3 --- /dev/null +++ b/appveyor.yml @@ -0,0 +1,136 @@ +# With infos from +# http://tjelvarolsson.com/blog/how-to-continuously-test-your-python-code-on-windows-using-appveyor/ +# https://packaging.python.org/en/latest/appveyor/ +# https://github.com/rmcgibbo/python-appveyor-conda-example + +# Backslashes in quotes need to be escaped: \ -> "\\" + +environment: + + global: + # SDK v7.0 MSVC Express 2008's SetEnv.cmd script will fail if the + # /E:ON and /V:ON options are not enabled in the batch script intepreter + # See: http://stackoverflow.com/a/13751649/163740 + CMD_IN_ENV: "cmd /E:ON /V:ON /C obvci_appveyor_python_build_env.cmd" + # Workaround for https://github.com/conda/conda-build/issues/636 + PYTHONIOENCODING: "UTF-8" + + matrix: + # for testing purpose: numpy 1.8 on py2.7, for the rest use 1.10/latest + - TARGET_ARCH: "x86" + CONDA_PY: "27" + CONDA_NPY: "18" + PYTHON_VERSION: "2.7" + CONDA_INSTALL_LOCN: "C:\\Miniconda" + - TARGET_ARCH: "x64" + CONDA_PY: "27" + CONDA_NPY: "18" + PYTHON_VERSION: "2.7" + CONDA_INSTALL_LOCN: "C:\\Miniconda-x64" + - TARGET_ARCH: "x64" + CONDA_PY: "34" + CONDA_NPY: "110" + PYTHON_VERSION: "3.4" + CONDA_INSTALL_LOCN: "C:\\Miniconda3-x64" + - TARGET_ARCH: "x64" + CONDA_PY: "35" + CONDA_NPY: "110" + PYTHON_VERSION: "3.5" + CONDA_INSTALL_LOCN: "C:\\Miniconda35-x64" + + +# We always use a 64-bit machine, but can build x86 distributions +# with the PYTHON_ARCH variable (which is used by CMD_IN_ENV). +platform: + - x64 + +# all our python builds have to happen in tests_script... +build: false + +init: + - cmd: "ECHO %PYTHON_VERSION% %CONDA_INSTALL_LOCN%" + +install: + - cmd: set PATH=%CONDA_INSTALL_LOCN%;%CONDA_INSTALL_LOCN%\scripts;%PATH%; + - cmd: set PYTHONUNBUFFERED=1 + - cmd: conda install -c http://conda.anaconda.org/pelson/channel/development --yes --quiet obvious-ci + - cmd: obvci_install_conda_build_tools.py + - cmd: conda config --set show_channel_urls yes + # for msinttypes + - cmd: conda config --add channels conda-forge + # this is now the downloaded conda... + - conda info -a + + # Fix the appveyor build environment to work with conda build + # workaround for missing vcvars64.bat in py34 64bit + - cmd: copy ci\appveyor\vcvars64.bat "C:\Program Files (x86)\Microsoft Visual Studio 10.0\VC\bin\amd64" + # workaround for conda build on py27 prefering the normal installed + # VS tools instead of the also installed Py27 VS compiler (which wouldn't need this workarounds...) + - cmd: copy "C:\Program Files (x86)\Microsoft Visual Studio 9.0\VC\bin\vcvars64.bat" "C:\Program Files (x86)\Microsoft Visual Studio 9.0\VC\bin\amd64\vcvarsamd64.bat" + + # same things as the requirements in ci/conda_recipe/meta.yaml + - cmd: conda create -y -q -n test-environment python=%PYTHON_VERSION% pip setuptools numpy python-dateutil freetype msinttypes tk pyparsing pytz tornado libpng zlib pyqt cycler nose mock msvc_runtime + - activate test-environment + - cmd: echo %PYTHON_VERSION% %TARGET_ARCH% + - cmd: IF %PYTHON_VERSION% == 2.7 conda install -y functools32 + + # Let the install prefer the static builds of the libs + - set LIBRARY_LIB=%CONDA_DEFAULT_ENV%\Library\lib + - cmd: 'mkdir lib || cmd /c "exit /b 0"' + - copy %LIBRARY_LIB%\zlibstatic.lib lib\z.lib + - copy %LIBRARY_LIB%\libpng_static.lib lib\png.lib + - set MPLBASEDIRLIST=%CONDA_DEFAULT_ENV%\Library\;. + # enables the local freetype build + - copy ci\travis\setup.cfg . + # Show the installed packages + versions + - conda list + +test_script: + # Now build the thing.. + - '%CMD_IN_ENV% python setup.py develop' + # Test import of tkagg backend + - python -c "import matplotlib as m; m.use('tkagg'); import matplotlib.pyplot as plt; print(plt.get_backend())" + # tests + - python tests.py + # remove to get around libpng issue? + - python visual_tests.py + +after_test: + # After the tests were a success, build packages (wheels and conda) + + # Build the wheel + # Hide the output, the copied files really clutter the build log... + - cmd: '%CMD_IN_ENV% python setup.py bdist_wheel > NUL:' + + # And now the conda build after a cleanup... + # cleanup build files so that they don't pollute the conda build but keep the wheel in dist... + - cmd: git clean -d -x -f -e dist/ + # cleanup the environment so that the test-environment does not leak into the conda build... + - cmd: set MPLBASEDIRLIST= + - cmd: set LIBRARY_LIB= + - cmd: deactivate + - cmd: path + - cmd: where python + - cmd: '%CMD_IN_ENV% conda config --get channels' + # - cmd: '%CMD_IN_ENV% conda build .\ci\conda_recipe' + + # Move the conda package into the dist directory, to register it + # as an "artifact" for Appveyor. + - cmd: 'copy /Y %CONDA_INSTALL_LOCN%\conda-bld\win-32\*.bz2 dist || cmd /c "exit /b 0"' + - cmd: 'copy /Y %CONDA_INSTALL_LOCN%\conda-bld\win-64\*.bz2 dist || cmd /c "exit /b 0"' + - cmd: dir dist\ + - cmd: echo finished... + +artifacts: + - path: dist\* + name: packages + + - path: result_images\* + name: result_images + type: zip + +on_failure: + - python visual_tests.py + - echo zipping images after a failure... + - 7z a result_images.zip result_images\ |grep -v "Compressing" + - appveyor PushArtifact result_images.zip diff --git a/setupext.py b/setupext.py index ff6cbca54018..fc9ff3ec3f77 100755 --- a/setupext.py +++ b/setupext.py @@ -1283,6 +1283,7 @@ def get_install_requires(self): class BackendAgg(OptionalBackendPackage): name = "agg" + force = True def get_extension(self): sources = [ @@ -1300,36 +1301,10 @@ def get_extension(self): class BackendTkAgg(OptionalBackendPackage): name = "tkagg" + force = True - def __init__(self): - self.tcl_tk_cache = None - - def check_requirements(self): - try: - if PY3min: - import tkinter as Tkinter - else: - import Tkinter - except ImportError: - raise CheckFailed('TKAgg requires Tkinter.') - except RuntimeError: - raise CheckFailed('Tkinter present but import failed.') - else: - if Tkinter.TkVersion < 8.3: - raise CheckFailed("Tcl/Tk v8.3 or later required.") - - ext = self.get_extension() - check_include_file(ext.include_dirs, "tk.h", "Tk") - - try: - tk_v = Tkinter.__version__.split()[-2] - except (AttributeError, IndexError): - # Tkinter.__version__ has been removed in python 3 - tk_v = 'not identified' - - BackendAgg.force = True - - return "version %s" % tk_v + def check(self): + return "installing; run-time loading from Python Tcl / Tk" def get_extension(self): sources = [ @@ -1343,251 +1318,11 @@ def get_extension(self): LibAgg().add_flags(ext, add_sources=False) return ext - def query_tcltk(self): - """ - Tries to open a Tk window in order to query the Tk object - about its library paths. This should never be called more - than once by the same process, as Tk intricacies may cause the - Python interpreter to hang. The function also has a workaround - if no X server is running (useful for autobuild systems). - """ - # Use cached values if they exist, which ensures this function - # only executes once - if self.tcl_tk_cache is not None: - return self.tcl_tk_cache - - # By this point, we already know that Tkinter imports correctly - if PY3min: - import tkinter as Tkinter - else: - import Tkinter - tcl_lib_dir = '' - tk_lib_dir = '' - # First try to open a Tk window (requires a running X server) - try: - tk = Tkinter.Tk() - except Tkinter.TclError: - # Next, start Tcl interpreter without opening a Tk window - # (no need for X server) This feature is available in - # python version 2.4 and up - try: - tcl = Tkinter.Tcl() - except AttributeError: # Python version not high enough - pass - except Tkinter.TclError: # Something went wrong while opening Tcl - pass - else: - tcl_lib_dir = str(tcl.getvar('tcl_library')) - # Guess Tk location based on Tcl location - (head, tail) = os.path.split(tcl_lib_dir) - tail = tail.replace('Tcl', 'Tk').replace('tcl', 'tk') - tk_lib_dir = os.path.join(head, tail) - if not os.path.exists(tk_lib_dir): - tk_lib_dir = tcl_lib_dir.replace( - 'Tcl', 'Tk').replace('tcl', 'tk') - else: - # Obtain Tcl and Tk locations from Tk widget - tk.withdraw() - tcl_lib_dir = str(tk.getvar('tcl_library')) - tk_lib_dir = str(tk.getvar('tk_library')) - tk.destroy() - - # Save directories and version string to cache - self.tcl_tk_cache = tcl_lib_dir, tk_lib_dir, str(Tkinter.TkVersion)[:3] - return self.tcl_tk_cache - - def parse_tcl_config(self, tcl_lib_dir, tk_lib_dir): - try: - if PY3min: - import tkinter as Tkinter - else: - import Tkinter - except ImportError: - return None - - tcl_poss = [tcl_lib_dir, - os.path.normpath(os.path.join(tcl_lib_dir, '..')), - "/usr/lib/tcl" + str(Tkinter.TclVersion), - "/usr/lib"] - tk_poss = [tk_lib_dir, - os.path.normpath(os.path.join(tk_lib_dir, '..')), - "/usr/lib/tk" + str(Tkinter.TkVersion), - "/usr/lib"] - for ptcl, ptk in zip(tcl_poss, tk_poss): - tcl_config = os.path.join(ptcl, "tclConfig.sh") - tk_config = os.path.join(ptk, "tkConfig.sh") - if (os.path.exists(tcl_config) and os.path.exists(tk_config)): - break - if not (os.path.exists(tcl_config) and os.path.exists(tk_config)): - return None - - def get_var(file, varname): - p = subprocess.Popen( - '. %s ; eval echo ${%s}' % (file, varname), - shell=True, - executable="/bin/sh", - stdout=subprocess.PIPE) - result = p.communicate()[0] - return result.decode('ascii') - - tcl_lib_dir = get_var( - tcl_config, 'TCL_LIB_SPEC').split()[0][2:].strip() - tcl_inc_dir = get_var( - tcl_config, 'TCL_INCLUDE_SPEC')[2:].strip() - tcl_lib = get_var(tcl_config, 'TCL_LIB_FLAG')[2:].strip() - - tk_lib_dir = get_var(tk_config, 'TK_LIB_SPEC').split()[0][2:].strip() - tk_inc_dir = get_var(tk_config, 'TK_INCLUDE_SPEC').strip() - if tk_inc_dir == '': - tk_inc_dir = tcl_inc_dir - else: - tk_inc_dir = tk_inc_dir[2:] - tk_lib = get_var(tk_config, 'TK_LIB_FLAG')[2:].strip() - - if not os.path.exists(os.path.join(tk_inc_dir, 'tk.h')): - return None - - return (tcl_lib_dir, tcl_inc_dir, tcl_lib, - tk_lib_dir, tk_inc_dir, tk_lib) - - def guess_tcl_config(self, tcl_lib_dir, tk_lib_dir, tk_ver): - if not (os.path.exists(tcl_lib_dir) and os.path.exists(tk_lib_dir)): - return None - - tcl_lib = os.path.normpath(os.path.join(tcl_lib_dir, '../')) - tk_lib = os.path.normpath(os.path.join(tk_lib_dir, '../')) - - tcl_inc = os.path.normpath( - os.path.join(tcl_lib_dir, - '../../include/tcl' + tk_ver)) - if not os.path.exists(tcl_inc): - tcl_inc = os.path.normpath( - os.path.join(tcl_lib_dir, - '../../include')) - - tk_inc = os.path.normpath(os.path.join( - tk_lib_dir, - '../../include/tk' + tk_ver)) - if not os.path.exists(tk_inc): - tk_inc = os.path.normpath(os.path.join( - tk_lib_dir, - '../../include')) - - if not os.path.exists(os.path.join(tk_inc, 'tk.h')): - tk_inc = tcl_inc - - if not os.path.exists(tcl_inc): - # this is a hack for suse linux, which is broken - if (sys.platform.startswith('linux') and - os.path.exists('/usr/include/tcl.h') and - os.path.exists('/usr/include/tk.h')): - tcl_inc = '/usr/include' - tk_inc = '/usr/include' - - if not os.path.exists(os.path.join(tk_inc, 'tk.h')): - return None - - return tcl_lib, tcl_inc, 'tcl' + tk_ver, tk_lib, tk_inc, 'tk' + tk_ver - - def hardcoded_tcl_config(self): - tcl_inc = "/usr/local/include" - tk_inc = "/usr/local/include" - tcl_lib = "/usr/local/lib" - tk_lib = "/usr/local/lib" - return tcl_lib, tcl_inc, 'tcl', tk_lib, tk_inc, 'tk' - def add_flags(self, ext): + ext.include_dirs.extend(['src']) if sys.platform == 'win32': - major, minor1, minor2, s, tmp = sys.version_info - if sys.version_info[0:2] < (3, 4): - ext.include_dirs.extend(['win32_static/include/tcl85']) - ext.libraries.extend(['tk85', 'tcl85']) - else: - ext.include_dirs.extend(['win32_static/include/tcl86']) - ext.libraries.extend(['tk86t', 'tcl86t']) - ext.library_dirs.extend([os.path.join(sys.prefix, 'dlls')]) - - elif sys.platform == 'darwin': - # this config section lifted directly from Imaging - thanks to - # the effbot! - - # First test for a MacOSX/darwin framework install - from os.path import join, exists - framework_dirs = [ - join(os.getenv('HOME'), '/Library/Frameworks'), - '/Library/Frameworks', - '/System/Library/Frameworks/', - ] - - # Find the directory that contains the Tcl.framework and - # Tk.framework bundles. - tk_framework_found = 0 - for F in framework_dirs: - # both Tcl.framework and Tk.framework should be present - for fw in 'Tcl', 'Tk': - if not exists(join(F, fw + '.framework')): - break - else: - # ok, F is now directory with both frameworks. Continure - # building - tk_framework_found = 1 - break - if tk_framework_found: - # For 8.4a2, we must add -I options that point inside - # the Tcl and Tk frameworks. In later release we - # should hopefully be able to pass the -F option to - # gcc, which specifies a framework lookup path. - - tk_include_dirs = [ - join(F, fw + '.framework', H) - for fw in ('Tcl', 'Tk') - for H in ('Headers', 'Versions/Current/PrivateHeaders') - ] - - # For 8.4a2, the X11 headers are not included. Rather - # than include a complicated search, this is a - # hard-coded path. It could bail out if X11 libs are - # not found... - - # tk_include_dirs.append('/usr/X11R6/include') - frameworks = ['-framework', 'Tcl', '-framework', 'Tk'] - ext.include_dirs.extend(tk_include_dirs) - ext.extra_link_args.extend(frameworks) - ext.extra_compile_args.extend(frameworks) - - # you're still here? ok we'll try it this way... - else: - # There are 3 methods to try, in decreasing order of "smartness" - # - # 1. Parse the tclConfig.sh and tkConfig.sh files that have - # all the information we need - # - # 2. Guess the include and lib dirs based on the location of - # Tkinter's 'tcl_library' and 'tk_library' variables. - # - # 3. Use some hardcoded locations that seem to work on a lot - # of distros. - - # Query Tcl/Tk system for library paths and version string - try: - tcl_lib_dir, tk_lib_dir, tk_ver = self.query_tcltk() - except: - tk_ver = '' - result = self.hardcoded_tcl_config() - else: - result = self.parse_tcl_config(tcl_lib_dir, tk_lib_dir) - if result is None: - result = self.guess_tcl_config( - tcl_lib_dir, tk_lib_dir, tk_ver) - if result is None: - result = self.hardcoded_tcl_config() - - # Add final versions of directories and libraries to ext lists - (tcl_lib_dir, tcl_inc_dir, tcl_lib, - tk_lib_dir, tk_inc_dir, tk_lib) = result - ext.include_dirs.extend([tcl_inc_dir, tk_inc_dir]) - ext.library_dirs.extend([tcl_lib_dir, tk_lib_dir]) - ext.libraries.extend([tcl_lib, tk_lib]) + # PSAPI library needed for finding Tcl / Tk at run time + ext.libraries.extend(['psapi']) class BackendGtk(OptionalBackendPackage): @@ -1701,8 +1436,6 @@ def check(self): return super(BackendGtkAgg, self).check() except: raise - else: - BackendAgg.force = True def get_package_data(self): return {'matplotlib': ['mpl-data/*.glade']} @@ -1778,7 +1511,6 @@ def check_requirements(self): p.join() if success: - BackendAgg.force = True return msg else: raise CheckFailed(msg) @@ -1851,7 +1583,6 @@ def check_requirements(self): p.join() if success: - BackendAgg.force = True return msg else: raise CheckFailed(msg) @@ -1895,8 +1626,6 @@ def check_requirements(self): raise CheckFailed( "Requires wxPython 2.8, found %s" % backend_version) - BackendAgg.force = True - return "version %s" % backend_version @@ -1935,7 +1664,7 @@ def check_requirements(self): config = self.get_config() if config is False: raise CheckFailed("skipping due to configuration") - return "installing" + return "" def get_extension(self): sources = [ @@ -2010,7 +1739,6 @@ def backend_pyside_internal_check(self): except ImportError: raise CheckFailed("PySide not found") else: - BackendAgg.force = True return ("Qt: %s, PySide: %s" % (QtCore.__version__, __version__)) @@ -2027,7 +1755,6 @@ def backend_pyqt4_internal_check(self): except AttributeError: raise CheckFailed('PyQt4 not correctly imported') else: - BackendAgg.force = True return ("Qt: %s, PyQt: %s" % (self.convert_qt_version(qt_version), pyqt_version_str)) @@ -2069,7 +1796,6 @@ def backend_qt5_internal_check(self): except AttributeError: raise CheckFailed('PyQt5 not correctly imported') else: - BackendAgg.force = True return ("Qt: %s, PyQt: %s" % (self.convert_qt_version(qt_version), pyqt_version_str)) diff --git a/src/_tkagg.cpp b/src/_tkagg.cpp index a193410a4514..9191992f8bca 100644 --- a/src/_tkagg.cpp +++ b/src/_tkagg.cpp @@ -6,11 +6,6 @@ * */ -/* This is needed for (at least) Tk 8.4.1, otherwise the signature of -** Tk_PhotoPutBlock changes. -*/ -#define USE_COMPOSITELESS_PHOTO_PUT_BLOCK - #include #include #include @@ -18,19 +13,8 @@ #include "py_converters.h" -extern "C" -{ -#ifdef __APPLE__ -# ifdef TK_FRAMEWORK -# include -# include -# else -# include -# endif -#else -# include -#endif -} +// Include our own excerpts from the Tcl / Tk headers +#include "_tkmini.h" #if defined(_MSC_VER) # define SIZE_T_FORMAT "%Iu" @@ -44,7 +28,17 @@ typedef struct Tcl_Interp *interp; } TkappObject; -static int PyAggImagePhoto(ClientData clientdata, Tcl_Interp *interp, int argc, char **argv) +// Global vars for Tcl / Tk functions. We load these symbols from the tkinter +// extension module or loaded Tcl / Tk libraries at run-time. +static Tcl_CreateCommand_t TCL_CREATE_COMMAND; +static Tcl_AppendResult_t TCL_APPEND_RESULT; +static Tk_MainWindow_t TK_MAIN_WINDOW; +static Tk_FindPhoto_t TK_FIND_PHOTO; +static Tk_PhotoPutBlock_NoComposite_t TK_PHOTO_PUT_BLOCK_NO_COMPOSITE; +static Tk_PhotoBlank_t TK_PHOTO_BLANK; + +static int PyAggImagePhoto(ClientData clientdata, Tcl_Interp *interp, int + argc, char **argv) { Tk_PhotoHandle photo; Tk_PhotoImageBlock block; @@ -61,25 +55,25 @@ static int PyAggImagePhoto(ClientData clientdata, Tcl_Interp *interp, int argc, long mode; long nval; - if (Tk_MainWindow(interp) == NULL) { + if (TK_MAIN_WINDOW(interp) == NULL) { // Will throw a _tkinter.TclError with "this isn't a Tk application" return TCL_ERROR; } if (argc != 5) { - Tcl_AppendResult(interp, "usage: ", argv[0], " destPhoto srcImage", (char *)NULL); + TCL_APPEND_RESULT(interp, "usage: ", argv[0], " destPhoto srcImage", (char *)NULL); return TCL_ERROR; } /* get Tcl PhotoImage handle */ - photo = Tk_FindPhoto(interp, argv[1]); + photo = TK_FIND_PHOTO(interp, argv[1]); if (photo == NULL) { - Tcl_AppendResult(interp, "destination photo must exist", (char *)NULL); + TCL_APPEND_RESULT(interp, "destination photo must exist", (char *)NULL); return TCL_ERROR; } /* get array (or object that can be converted to array) pointer */ if (sscanf(argv[2], SIZE_T_FORMAT, &aggl) != 1) { - Tcl_AppendResult(interp, "error casting pointer", (char *)NULL); + TCL_APPEND_RESULT(interp, "error casting pointer", (char *)NULL); return TCL_ERROR; } bufferobj = (PyObject *)aggl; @@ -88,7 +82,7 @@ static int PyAggImagePhoto(ClientData clientdata, Tcl_Interp *interp, int argc, try { buffer = numpy::array_view(bufferobj); } catch (...) { - Tcl_AppendResult(interp, "buffer is of wrong type", (char *)NULL); + TCL_APPEND_RESULT(interp, "buffer is of wrong type", (char *)NULL); PyErr_Clear(); return TCL_ERROR; } @@ -99,13 +93,13 @@ static int PyAggImagePhoto(ClientData clientdata, Tcl_Interp *interp, int argc, /* get array mode (0=mono, 1=rgb, 2=rgba) */ mode = atol(argv[3]); if ((mode != 0) && (mode != 1) && (mode != 2)) { - Tcl_AppendResult(interp, "illegal image mode", (char *)NULL); + TCL_APPEND_RESULT(interp, "illegal image mode", (char *)NULL); return TCL_ERROR; } /* check for bbox/blitting */ if (sscanf(argv[4], SIZE_T_FORMAT, &bboxl) != 1) { - Tcl_AppendResult(interp, "error casting pointer", (char *)NULL); + TCL_APPEND_RESULT(interp, "error casting pointer", (char *)NULL); return TCL_ERROR; } bboxo = (PyObject *)bboxl; @@ -126,7 +120,7 @@ static int PyAggImagePhoto(ClientData clientdata, Tcl_Interp *interp, int argc, destbuffer = new agg::int8u[deststride * destheight]; if (destbuffer == NULL) { - Tcl_AppendResult(interp, "could not allocate memory", (char *)NULL); + TCL_APPEND_RESULT(interp, "could not allocate memory", (char *)NULL); return TCL_ERROR; } @@ -167,7 +161,8 @@ static int PyAggImagePhoto(ClientData clientdata, Tcl_Interp *interp, int argc, block.pitch = deststride; block.pixelPtr = destbuffer; - Tk_PhotoPutBlock(photo, &block, destx, desty, destwidth, destheight); + TK_PHOTO_PUT_BLOCK_NO_COMPOSITE(photo, &block, destx, desty, + destwidth, destheight); delete[] destbuffer; } else { @@ -177,9 +172,10 @@ static int PyAggImagePhoto(ClientData clientdata, Tcl_Interp *interp, int argc, block.pixelPtr = buffer.data(); /* Clear current contents */ - Tk_PhotoBlank(photo); + TK_PHOTO_BLANK(photo); /* Copy opaque block to photo image, and leave the rest to TK */ - Tk_PhotoPutBlock(photo, &block, 0, 0, block.width, block.height); + TK_PHOTO_PUT_BLOCK_NO_COMPOSITE(photo, &block, 0, 0, block.width, + block.height); } return TCL_OK; @@ -216,11 +212,11 @@ static PyObject *_tkinit(PyObject *self, PyObject *args) /* This will bomb if interp is invalid... */ - Tcl_CreateCommand(interp, - "PyAggImagePhoto", - (Tcl_CmdProc *)PyAggImagePhoto, - (ClientData)0, - (Tcl_CmdDeleteProc *)NULL); + TCL_CREATE_COMMAND(interp, + "PyAggImagePhoto", + (Tcl_CmdProc *)PyAggImagePhoto, + (ClientData)0, + (Tcl_CmdDeleteProc *)NULL); Py_INCREF(Py_None); return Py_None; @@ -232,6 +228,221 @@ static PyMethodDef functions[] = { { NULL, NULL } /* sentinel */ }; +// Functions to fill global TCL / Tk function pointers by dynamic loading +#if defined(_WIN32) || defined(__WIN32__) || defined(WIN32) + +/* + * On Windows, we can't load the tkinter module to get the TCL or Tk symbols, + * because Windows does not load symbols into the library name-space of + * importing modules. So, knowing that tkinter has already been imported by + * Python, we scan all modules in the running process for the TCL and Tk + * function names. + */ +#include +#define PSAPI_VERSION 1 +#include +// Must be linked with 'psapi' library + +FARPROC _dfunc(HMODULE lib_handle, const char *func_name) +{ + // Load function `func_name` from `lib_handle`. + // Set Python exception if we can't find `func_name` in `lib_handle`. + // Returns function pointer or NULL if not present. + + char message[100]; + + FARPROC func = GetProcAddress(lib_handle, func_name); + if (func == NULL) { + sprintf(message, "Cannot load function %s", func_name); + PyErr_SetString(PyExc_RuntimeError, message); + } + return func; +} + +int get_tcl(HMODULE hMod) +{ + // Try to fill TCL global vars with function pointers. Return 0 for no + // functions found, 1 for all functions found, -1 for some but not all + // functions found. + TCL_CREATE_COMMAND = (Tcl_CreateCommand_t) + GetProcAddress(hMod, "Tcl_CreateCommand"); + if (TCL_CREATE_COMMAND == NULL) { // Maybe not TCL module + return 0; + } + TCL_APPEND_RESULT = (Tcl_AppendResult_t) _dfunc(hMod, + "Tcl_AppendResult"); + return (TCL_APPEND_RESULT == NULL) ? -1 : 1; +} + +int get_tk(HMODULE hMod) +{ + // Try to fill Tk global vars with function pointers. Return 0 for no + // functions found, 1 for all functions found, -1 for some but not all + // functions found. + TK_MAIN_WINDOW = (Tk_MainWindow_t) + GetProcAddress(hMod, "Tk_MainWindow"); + if (TK_MAIN_WINDOW == NULL) { // Maybe not Tk module + return 0; + } + return ( // -1 if any remaining symbols are NULL + ((TK_FIND_PHOTO = (Tk_FindPhoto_t) + _dfunc(hMod, "Tk_FindPhoto")) == NULL) || + ((TK_PHOTO_PUT_BLOCK_NO_COMPOSITE = (Tk_PhotoPutBlock_NoComposite_t) + _dfunc(hMod, "Tk_PhotoPutBlock_NoComposite")) == NULL) || + ((TK_PHOTO_BLANK = (Tk_PhotoBlank_t) + _dfunc(hMod, "Tk_PhotoBlank")) == NULL)) + ? -1 : 1; +} + +int load_tkinter_funcs(void) +{ + // Load TCL and Tk functions by searching all modules in current process. + // Return 0 for success, non-zero for failure. + + HMODULE hMods[1024]; + HANDLE hProcess; + DWORD cbNeeded; + unsigned int i; + int found_tcl = 0; + int found_tk = 0; + + // Returns pseudo-handle that does not need to be closed + hProcess = GetCurrentProcess(); + + // Iterate through modules in this process looking for TCL / Tk names + if (EnumProcessModules(hProcess, hMods, sizeof(hMods), &cbNeeded)) { + for (i = 0; i < (cbNeeded / sizeof(HMODULE)); i++) { + if (!found_tcl) { + found_tcl = get_tcl(hMods[i]); + if (found_tcl == -1) { + return 1; + } + } + if (!found_tk) { + found_tk = get_tk(hMods[i]); + if (found_tk == -1) { + return 1; + } + } + if (found_tcl && found_tk) { + return 0; + } + } + } + + if (found_tcl == 0) { + PyErr_SetString(PyExc_RuntimeError, "Could not find TCL routines"); + } else { + PyErr_SetString(PyExc_RuntimeError, "Could not find Tk routines"); + } + return 1; +} + +#else // not Windows + +/* + * On Unix, we can get the TCL and Tk synbols from the tkinter module, because + * tkinter uses these symbols, and the symbols are therefore visible in the + * tkinter dynamic library (module). + */ +#if PY3K +#define TKINTER_PKG "tkinter" +#define TKINTER_MOD "_tkinter" +// From module __file__ attribute to char *string for dlopen. +char *fname2char(PyObject *fname) +{ + PyObject *bytes = PyUnicode_EncodeFSDefault(fname); + if (bytes == NULL) { + return NULL; + } + return PyBytes_AsString(bytes); +} +#else +#define TKINTER_PKG "Tkinter" +#define TKINTER_MOD "tkinter" +// From module __file__ attribute to char *string for dlopen +#define fname2char(s) (PyString_AsString(s)) +#endif + +#include + +void *_dfunc(void *lib_handle, const char *func_name) +{ + // Load function `func_name` from `lib_handle`. + // Set Python exception if we can't find `func_name` in `lib_handle`. + // Returns function pointer or NULL if not present. + + // Reset errors. + dlerror(); + void *func = dlsym(lib_handle, func_name); + if (func == NULL) { + const char *error = dlerror(); + PyErr_SetString(PyExc_RuntimeError, error); + } + return func; +} + +int _func_loader(void *lib) +{ + // Fill global function pointers from dynamic lib. + // Return 1 if any pointer is NULL, 0 otherwise. + return ( + ((TCL_CREATE_COMMAND = (Tcl_CreateCommand_t) + _dfunc(lib, "Tcl_CreateCommand")) == NULL) || + ((TCL_APPEND_RESULT = (Tcl_AppendResult_t) + _dfunc(lib, "Tcl_AppendResult")) == NULL) || + ((TK_MAIN_WINDOW = (Tk_MainWindow_t) + _dfunc(lib, "Tk_MainWindow")) == NULL) || + ((TK_FIND_PHOTO = (Tk_FindPhoto_t) + _dfunc(lib, "Tk_FindPhoto")) == NULL) || + ((TK_PHOTO_PUT_BLOCK_NO_COMPOSITE = (Tk_PhotoPutBlock_NoComposite_t) + _dfunc(lib, "Tk_PhotoPutBlock_NoComposite")) == NULL) || + ((TK_PHOTO_BLANK = (Tk_PhotoBlank_t) + _dfunc(lib, "Tk_PhotoBlank")) == NULL)); +} + +int load_tkinter_funcs(void) +{ + // Load tkinter global funcs from tkinter compiled module. + // Return 0 for success, non-zero for failure. + int ret = -1; + void *tkinter_lib; + char *tkinter_libname; + PyObject *pModule = NULL, *pSubmodule = NULL, *pString = NULL; + + pModule = PyImport_ImportModule(TKINTER_PKG); + if (pModule == NULL) { + goto exit; + } + pSubmodule = PyObject_GetAttrString(pModule, TKINTER_MOD); + if (pSubmodule == NULL) { + goto exit; + } + pString = PyObject_GetAttrString(pSubmodule, "__file__"); + if (pString == NULL) { + goto exit; + } + tkinter_libname = fname2char(pString); + if (tkinter_libname == NULL) { + goto exit; + } + tkinter_lib = dlopen(tkinter_libname, RTLD_LAZY); + if (tkinter_lib == NULL) { + PyErr_SetString(PyExc_RuntimeError, + "Cannot dlopen tkinter module file"); + goto exit; + } + ret = _func_loader(tkinter_lib); + // dlclose probably safe because tkinter has been imported. + dlclose(tkinter_lib); +exit: + Py_XDECREF(pModule); + Py_XDECREF(pSubmodule); + Py_XDECREF(pString); + return ret; +} +#endif // end not Windows + #if PY3K static PyModuleDef _tkagg_module = { PyModuleDef_HEAD_INIT, "_tkagg", "", -1, functions, NULL, NULL, NULL, NULL }; @@ -244,7 +455,7 @@ PyMODINIT_FUNC PyInit__tkagg(void) import_array(); - return m; + return (load_tkinter_funcs() == 0) ? m : NULL; } #else PyMODINIT_FUNC init_tkagg(void) @@ -252,5 +463,7 @@ PyMODINIT_FUNC init_tkagg(void) import_array(); Py_InitModule("_tkagg", functions); + + load_tkinter_funcs(); } #endif diff --git a/src/_tkmini.h b/src/_tkmini.h new file mode 100644 index 000000000000..9b730b6c8c1f --- /dev/null +++ b/src/_tkmini.h @@ -0,0 +1,128 @@ +/* Small excerpts from the Tcl / Tk 8.6 headers + * + * License terms copied from: + * http://www.tcl.tk/software/tcltk/license.html + * as of 20 May 2016. + * + * Copyright (c) 1987-1994 The Regents of the University of California. + * Copyright (c) 1993-1996 Lucent Technologies. + * Copyright (c) 1994-1998 Sun Microsystems, Inc. + * Copyright (c) 1998-2000 by Scriptics Corporation. + * Copyright (c) 2002 by Kevin B. Kenny. All rights reserved. + * + * This software is copyrighted by the Regents of the University + * of California, Sun Microsystems, Inc., Scriptics Corporation, + * and other parties. The following terms apply to all files + * associated with the software unless explicitly disclaimed in + * individual files. + * + * The authors hereby grant permission to use, copy, modify, + * distribute, and license this software and its documentation + * for any purpose, provided that existing copyright notices are + * retained in all copies and that this notice is included + * verbatim in any distributions. No written agreement, license, + * or royalty fee is required for any of the authorized uses. + * Modifications to this software may be copyrighted by their + * authors and need not follow the licensing terms described + * here, provided that the new terms are clearly indicated on + * the first page of each file where they apply. + * + * IN NO EVENT SHALL THE AUTHORS OR DISTRIBUTORS BE LIABLE TO + * ANY PARTY FOR DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR + * CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OF THIS + * SOFTWARE, ITS DOCUMENTATION, OR ANY DERIVATIVES THEREOF, EVEN + * IF THE AUTHORS HAVE BEEN ADVISED OF THE POSSIBILITY OF SUCH + * DAMAGE. + * + * THE AUTHORS AND DISTRIBUTORS SPECIFICALLY DISCLAIM ANY + * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR + * PURPOSE, AND NON-INFRINGEMENT. THIS SOFTWARE IS PROVIDED ON + * AN "AS IS" BASIS, AND THE AUTHORS AND DISTRIBUTORS HAVE NO + * OBLIGATION TO PROVIDE MAINTENANCE, SUPPORT, UPDATES, + * ENHANCEMENTS, OR MODIFICATIONS. + * + * GOVERNMENT USE: If you are acquiring this software on behalf + * of the U.S. government, the Government shall have only + * "Restricted Rights" in the software and related documentation + * as defined in the Federal Acquisition Regulations (FARs) in + * Clause 52.227.19 (c) (2). If you are acquiring the software + * on behalf of the Department of Defense, the software shall be + * classified as "Commercial Computer Software" and the + * Government shall have only "Restricted Rights" as defined in + * Clause 252.227-7013 (c) (1) of DFARs. Notwithstanding the + * foregoing, the authors grant the U.S. Government and others + * acting in its behalf permission to use and distribute the + * software in accordance with the terms specified in this + * license + */ + +/* + * Unless otherwise noted, these definitions are stable from Tcl / Tk 8.5 + * through Tck / Tk master as of 21 May 2016 + */ + +#ifdef __cplusplus +extern "C" { +#endif + +/* Tcl header excerpts */ +#define TCL_OK 0 +#define TCL_ERROR 1 + +/* + * Users of versions of Tcl >= 8.6 encouraged to tread Tcl_Interp as an opaque + * pointer. The following definition results when TCL_NO_DEPRECATED defined. + */ +typedef struct Tcl_Interp Tcl_Interp; + +typedef struct Tcl_Command_ *Tcl_Command; +typedef void *ClientData; + +typedef int (Tcl_CmdProc) (ClientData clientData, Tcl_Interp + *interp, int argc, const char *argv[]); +typedef void (Tcl_CmdDeleteProc) (ClientData clientData); + +/* Typedefs derived from function signatures in Tcl header */ +/* Tcl_CreateCommand */ +typedef Tcl_Command (*Tcl_CreateCommand_t)(Tcl_Interp *interp, + const char *cmdName, Tcl_CmdProc *proc, + ClientData clientData, + Tcl_CmdDeleteProc *deleteProc); +/* Tcl_AppendResult */ +typedef void (*Tcl_AppendResult_t) (Tcl_Interp *interp, ...); + +/* Tk header excerpts */ +typedef struct Tk_Window_ *Tk_Window; + +typedef void *Tk_PhotoHandle; + +typedef struct Tk_PhotoImageBlock +{ + unsigned char *pixelPtr; + int width; + int height; + int pitch; + int pixelSize; + int offset[4]; +} Tk_PhotoImageBlock; + +/* Typedefs derived from function signatures in Tk header */ +/* Tk_MainWindow */ +typedef Tk_Window (*Tk_MainWindow_t) (Tcl_Interp *interp); +typedef Tk_PhotoHandle (*Tk_FindPhoto_t) (Tcl_Interp *interp, const char + *imageName); +/* Tk_PhotoPutBLock_NoComposite typedef */ +typedef void (*Tk_PhotoPutBlock_NoComposite_t) (Tk_PhotoHandle handle, + Tk_PhotoImageBlock *blockPtr, int x, int y, + int width, int height); +/* Tk_PhotoBlank */ +typedef void (*Tk_PhotoBlank_t) (Tk_PhotoHandle handle); + +/* + * end block for C++ + */ + +#ifdef __cplusplus +} +#endif