_tkagg.cpp 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370
  1. /* -*- mode: c++; c-basic-offset: 4 -*- */
  2. // Where is PIL?
  3. //
  4. // Many years ago, Matplotlib used to include code from PIL (the Python Imaging
  5. // Library). Since then, the code has changed a lot - the organizing principle
  6. // and methods of operation are now quite different. Because our review of
  7. // the codebase showed that all the code that came from PIL was removed or
  8. // rewritten, we have removed the PIL licensing information. If you want PIL,
  9. // you can get it at https://python-pillow.org/
  10. #define PY_SSIZE_T_CLEAN
  11. #include <Python.h>
  12. #ifdef _WIN32
  13. #define WIN32_DLL
  14. #endif
  15. #ifdef __CYGWIN__
  16. /*
  17. * Unfortunately cygwin's libdl inherits restrictions from the underlying
  18. * Windows OS, at least currently. Therefore, a symbol may be loaded from a
  19. * module by dlsym() only if it is really located in the given module,
  20. * dependencies are not included. So we have to use native WinAPI on Cygwin
  21. * also.
  22. */
  23. #define WIN32_DLL
  24. static inline PyObject *PyErr_SetFromWindowsErr(int ierr) {
  25. PyErr_SetString(PyExc_OSError, "Call to EnumProcessModules failed");
  26. return NULL;
  27. }
  28. #endif
  29. #ifdef WIN32_DLL
  30. #include <string>
  31. #include <windows.h>
  32. #include <commctrl.h>
  33. #define PSAPI_VERSION 1
  34. #include <psapi.h> // Must be linked with 'psapi' library
  35. #define dlsym GetProcAddress
  36. #else
  37. #include <dlfcn.h>
  38. #endif
  39. // Include our own excerpts from the Tcl / Tk headers
  40. #include "_tkmini.h"
  41. static int convert_voidptr(PyObject *obj, void *p)
  42. {
  43. void **val = (void **)p;
  44. *val = PyLong_AsVoidPtr(obj);
  45. return *val != NULL ? 1 : !PyErr_Occurred();
  46. }
  47. // Global vars for Tk functions. We load these symbols from the tkinter
  48. // extension module or loaded Tk libraries at run-time.
  49. static Tk_FindPhoto_t TK_FIND_PHOTO;
  50. static Tk_PhotoPutBlock_t TK_PHOTO_PUT_BLOCK;
  51. // Global vars for Tcl functions. We load these symbols from the tkinter
  52. // extension module or loaded Tcl libraries at run-time.
  53. static Tcl_SetVar_t TCL_SETVAR;
  54. static PyObject *mpl_tk_blit(PyObject *self, PyObject *args)
  55. {
  56. Tcl_Interp *interp;
  57. char const *photo_name;
  58. int height, width;
  59. unsigned char *data_ptr;
  60. int comp_rule;
  61. int put_retval;
  62. int o0, o1, o2, o3;
  63. int x1, x2, y1, y2;
  64. Tk_PhotoHandle photo;
  65. Tk_PhotoImageBlock block;
  66. if (!PyArg_ParseTuple(args, "O&s(iiO&)i(iiii)(iiii):blit",
  67. convert_voidptr, &interp, &photo_name,
  68. &height, &width, convert_voidptr, &data_ptr,
  69. &comp_rule,
  70. &o0, &o1, &o2, &o3,
  71. &x1, &x2, &y1, &y2)) {
  72. goto exit;
  73. }
  74. if (!(photo = TK_FIND_PHOTO(interp, photo_name))) {
  75. PyErr_SetString(PyExc_ValueError, "Failed to extract Tk_PhotoHandle");
  76. goto exit;
  77. }
  78. if (0 > y1 || y1 > y2 || y2 > height || 0 > x1 || x1 > x2 || x2 > width) {
  79. PyErr_SetString(PyExc_ValueError, "Attempting to draw out of bounds");
  80. goto exit;
  81. }
  82. if (comp_rule != TK_PHOTO_COMPOSITE_OVERLAY && comp_rule != TK_PHOTO_COMPOSITE_SET) {
  83. PyErr_SetString(PyExc_ValueError, "Invalid comp_rule argument");
  84. goto exit;
  85. }
  86. Py_BEGIN_ALLOW_THREADS
  87. block.pixelPtr = data_ptr + 4 * ((height - y2) * width + x1);
  88. block.width = x2 - x1;
  89. block.height = y2 - y1;
  90. block.pitch = 4 * width;
  91. block.pixelSize = 4;
  92. block.offset[0] = o0;
  93. block.offset[1] = o1;
  94. block.offset[2] = o2;
  95. block.offset[3] = o3;
  96. put_retval = TK_PHOTO_PUT_BLOCK(
  97. interp, photo, &block, x1, height - y2, x2 - x1, y2 - y1, comp_rule);
  98. Py_END_ALLOW_THREADS
  99. if (put_retval == TCL_ERROR) {
  100. return PyErr_NoMemory();
  101. }
  102. exit:
  103. if (PyErr_Occurred()) {
  104. return NULL;
  105. } else {
  106. Py_RETURN_NONE;
  107. }
  108. }
  109. #ifdef WIN32_DLL
  110. LRESULT CALLBACK
  111. DpiSubclassProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam,
  112. UINT_PTR uIdSubclass, DWORD_PTR dwRefData)
  113. {
  114. switch (uMsg) {
  115. case WM_DPICHANGED:
  116. // This function is a subclassed window procedure, and so is run during
  117. // the Tcl/Tk event loop. Unfortunately, Tkinter has a *second* lock on
  118. // Tcl threading that is not exposed publicly, but is currently taken
  119. // while we're in the window procedure. So while we can take the GIL to
  120. // call Python code, we must not also call *any* Tk code from Python.
  121. // So stay with Tcl calls in C only.
  122. {
  123. // This variable naming must match the name used in
  124. // lib/matplotlib/backends/_backend_tk.py:FigureManagerTk.
  125. std::string var_name("window_dpi");
  126. var_name += std::to_string((unsigned long long)hwnd);
  127. // X is high word, Y is low word, but they are always equal.
  128. std::string dpi = std::to_string(LOWORD(wParam));
  129. Tcl_Interp* interp = (Tcl_Interp*)dwRefData;
  130. TCL_SETVAR(interp, var_name.c_str(), dpi.c_str(), 0);
  131. }
  132. return 0;
  133. case WM_NCDESTROY:
  134. RemoveWindowSubclass(hwnd, DpiSubclassProc, uIdSubclass);
  135. break;
  136. }
  137. return DefSubclassProc(hwnd, uMsg, wParam, lParam);
  138. }
  139. #endif
  140. static PyObject*
  141. mpl_tk_enable_dpi_awareness(PyObject* self, PyObject*const* args,
  142. Py_ssize_t nargs)
  143. {
  144. if (nargs != 2) {
  145. return PyErr_Format(PyExc_TypeError,
  146. "enable_dpi_awareness() takes 2 positional "
  147. "arguments but %zd were given",
  148. nargs);
  149. }
  150. #ifdef WIN32_DLL
  151. HWND frame_handle = NULL;
  152. Tcl_Interp *interp = NULL;
  153. if (!convert_voidptr(args[0], &frame_handle)) {
  154. return NULL;
  155. }
  156. if (!convert_voidptr(args[1], &interp)) {
  157. return NULL;
  158. }
  159. #ifdef _DPI_AWARENESS_CONTEXTS_
  160. HMODULE user32 = LoadLibrary("user32.dll");
  161. typedef DPI_AWARENESS_CONTEXT (WINAPI *GetWindowDpiAwarenessContext_t)(HWND);
  162. GetWindowDpiAwarenessContext_t GetWindowDpiAwarenessContextPtr =
  163. (GetWindowDpiAwarenessContext_t)GetProcAddress(
  164. user32, "GetWindowDpiAwarenessContext");
  165. if (GetWindowDpiAwarenessContextPtr == NULL) {
  166. FreeLibrary(user32);
  167. Py_RETURN_FALSE;
  168. }
  169. typedef BOOL (WINAPI *AreDpiAwarenessContextsEqual_t)(DPI_AWARENESS_CONTEXT,
  170. DPI_AWARENESS_CONTEXT);
  171. AreDpiAwarenessContextsEqual_t AreDpiAwarenessContextsEqualPtr =
  172. (AreDpiAwarenessContextsEqual_t)GetProcAddress(
  173. user32, "AreDpiAwarenessContextsEqual");
  174. if (AreDpiAwarenessContextsEqualPtr == NULL) {
  175. FreeLibrary(user32);
  176. Py_RETURN_FALSE;
  177. }
  178. DPI_AWARENESS_CONTEXT ctx = GetWindowDpiAwarenessContextPtr(frame_handle);
  179. bool per_monitor = (
  180. AreDpiAwarenessContextsEqualPtr(
  181. ctx, DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2) ||
  182. AreDpiAwarenessContextsEqualPtr(
  183. ctx, DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE));
  184. if (per_monitor) {
  185. // Per monitor aware means we need to handle WM_DPICHANGED by wrapping
  186. // the Window Procedure, and the Python side needs to trace the Tk
  187. // window_dpi variable stored on interp.
  188. SetWindowSubclass(frame_handle, DpiSubclassProc, 0, (DWORD_PTR)interp);
  189. }
  190. FreeLibrary(user32);
  191. return PyBool_FromLong(per_monitor);
  192. #endif
  193. #endif
  194. Py_RETURN_NONE;
  195. }
  196. static PyMethodDef functions[] = {
  197. { "blit", (PyCFunction)mpl_tk_blit, METH_VARARGS },
  198. { "enable_dpi_awareness", (PyCFunction)mpl_tk_enable_dpi_awareness,
  199. METH_FASTCALL },
  200. { NULL, NULL } /* sentinel */
  201. };
  202. // Functions to fill global Tcl/Tk function pointers by dynamic loading.
  203. template <class T>
  204. bool load_tcl_tk(T lib)
  205. {
  206. // Try to fill Tcl/Tk global vars with function pointers. Return whether
  207. // all of them have been filled.
  208. if (auto ptr = dlsym(lib, "Tcl_SetVar")) {
  209. TCL_SETVAR = (Tcl_SetVar_t)ptr;
  210. }
  211. if (auto ptr = dlsym(lib, "Tk_FindPhoto")) {
  212. TK_FIND_PHOTO = (Tk_FindPhoto_t)ptr;
  213. }
  214. if (auto ptr = dlsym(lib, "Tk_PhotoPutBlock")) {
  215. TK_PHOTO_PUT_BLOCK = (Tk_PhotoPutBlock_t)ptr;
  216. }
  217. return TCL_SETVAR && TK_FIND_PHOTO && TK_PHOTO_PUT_BLOCK;
  218. }
  219. #ifdef WIN32_DLL
  220. /* On Windows, we can't load the tkinter module to get the Tcl/Tk symbols,
  221. * because Windows does not load symbols into the library name-space of
  222. * importing modules. So, knowing that tkinter has already been imported by
  223. * Python, we scan all modules in the running process for the Tcl/Tk function
  224. * names.
  225. */
  226. void load_tkinter_funcs(void)
  227. {
  228. HANDLE process = GetCurrentProcess(); // Pseudo-handle, doesn't need closing.
  229. HMODULE* modules = NULL;
  230. DWORD size;
  231. if (!EnumProcessModules(process, NULL, 0, &size)) {
  232. PyErr_SetFromWindowsErr(0);
  233. goto exit;
  234. }
  235. if (!(modules = static_cast<HMODULE*>(malloc(size)))) {
  236. PyErr_NoMemory();
  237. goto exit;
  238. }
  239. if (!EnumProcessModules(process, modules, size, &size)) {
  240. PyErr_SetFromWindowsErr(0);
  241. goto exit;
  242. }
  243. for (unsigned i = 0; i < size / sizeof(HMODULE); ++i) {
  244. if (load_tcl_tk(modules[i])) {
  245. return;
  246. }
  247. }
  248. exit:
  249. free(modules);
  250. }
  251. #else // not Windows
  252. /*
  253. * On Unix, we can get the Tk symbols from the tkinter module, because tkinter
  254. * uses these symbols, and the symbols are therefore visible in the tkinter
  255. * dynamic library (module).
  256. */
  257. void load_tkinter_funcs(void)
  258. {
  259. // Load tkinter global funcs from tkinter compiled module.
  260. void *main_program = NULL, *tkinter_lib = NULL;
  261. PyObject *module = NULL, *py_path = NULL, *py_path_b = NULL;
  262. char *path;
  263. // Try loading from the main program namespace first.
  264. main_program = dlopen(NULL, RTLD_LAZY);
  265. if (load_tcl_tk(main_program)) {
  266. goto exit;
  267. }
  268. // Clear exception triggered when we didn't find symbols above.
  269. PyErr_Clear();
  270. // Handle PyPy first, as that import will correctly fail on CPython.
  271. module = PyImport_ImportModule("_tkinter.tklib_cffi"); // PyPy
  272. if (!module) {
  273. PyErr_Clear();
  274. module = PyImport_ImportModule("_tkinter"); // CPython
  275. }
  276. if (!(module &&
  277. (py_path = PyObject_GetAttrString(module, "__file__")) &&
  278. (py_path_b = PyUnicode_EncodeFSDefault(py_path)) &&
  279. (path = PyBytes_AsString(py_path_b)))) {
  280. goto exit;
  281. }
  282. tkinter_lib = dlopen(path, RTLD_LAZY);
  283. if (!tkinter_lib) {
  284. PyErr_SetString(PyExc_RuntimeError, dlerror());
  285. goto exit;
  286. }
  287. if (load_tcl_tk(tkinter_lib)) {
  288. goto exit;
  289. }
  290. exit:
  291. // We don't need to keep a reference open as the main program & tkinter
  292. // have been imported. Try to close each library separately (otherwise the
  293. // second dlclose could clear a dlerror from the first dlclose).
  294. bool raised_dlerror = false;
  295. if (main_program && dlclose(main_program) && !raised_dlerror) {
  296. PyErr_SetString(PyExc_RuntimeError, dlerror());
  297. raised_dlerror = true;
  298. }
  299. if (tkinter_lib && dlclose(tkinter_lib) && !raised_dlerror) {
  300. PyErr_SetString(PyExc_RuntimeError, dlerror());
  301. raised_dlerror = true;
  302. }
  303. Py_XDECREF(module);
  304. Py_XDECREF(py_path);
  305. Py_XDECREF(py_path_b);
  306. }
  307. #endif // end not Windows
  308. static PyModuleDef _tkagg_module = {
  309. PyModuleDef_HEAD_INIT, "_tkagg", NULL, -1, functions
  310. };
  311. PyMODINIT_FUNC PyInit__tkagg(void)
  312. {
  313. load_tkinter_funcs();
  314. PyObject *type, *value, *traceback;
  315. PyErr_Fetch(&type, &value, &traceback);
  316. // Always raise ImportError (normalizing a previously set exception if
  317. // needed) to interact properly with backend auto-fallback.
  318. if (value) {
  319. PyErr_NormalizeException(&type, &value, &traceback);
  320. PyErr_SetObject(PyExc_ImportError, value);
  321. return NULL;
  322. } else if (!TCL_SETVAR) {
  323. PyErr_SetString(PyExc_ImportError, "Failed to load Tcl_SetVar");
  324. return NULL;
  325. } else if (!TK_FIND_PHOTO) {
  326. PyErr_SetString(PyExc_ImportError, "Failed to load Tk_FindPhoto");
  327. return NULL;
  328. } else if (!TK_PHOTO_PUT_BLOCK) {
  329. PyErr_SetString(PyExc_ImportError, "Failed to load Tk_PhotoPutBlock");
  330. return NULL;
  331. }
  332. return PyModule_Create(&_tkagg_module);
  333. }