From fb4d7c33c43ad92b492c5815408e46d0a3e690c9 Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Wed, 11 Sep 2024 07:56:06 -0400 Subject: [PATCH 1/8] Check ndim in check_trailing_shape helpers This was previously checked by using an `array_view`, but moving to pybind11 we won't have that until cast to `unchecked`. --- src/mplutils.h | 12 ++++++++++++ src/numpy_cpp.h | 8 ++++---- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/src/mplutils.h b/src/mplutils.h index 58eb32d190ec..05c3436626e2 100644 --- a/src/mplutils.h +++ b/src/mplutils.h @@ -71,6 +71,12 @@ inline int prepare_and_add_type(PyTypeObject *type, PyObject *module) template inline bool check_trailing_shape(T array, char const* name, long d1) { + if (array.ndim() != 2) { + PyErr_Format(PyExc_ValueError, + "Expected 2-dimensional array, got %ld", + array.ndim()); + return false; + } if (array.shape(1) != d1) { PyErr_Format(PyExc_ValueError, "%s must have shape (N, %ld), got (%ld, %ld)", @@ -83,6 +89,12 @@ inline bool check_trailing_shape(T array, char const* name, long d1) template inline bool check_trailing_shape(T array, char const* name, long d1, long d2) { + if (array.ndim() != 3) { + PyErr_Format(PyExc_ValueError, + "Expected 3-dimensional array, got %ld", + array.ndim()); + return false; + } if (array.shape(1) != d1 || array.shape(2) != d2) { PyErr_Format(PyExc_ValueError, "%s must have shape (N, %ld, %ld), got (%ld, %ld, %ld)", diff --git a/src/numpy_cpp.h b/src/numpy_cpp.h index 6165789b7603..6b7446337bb7 100644 --- a/src/numpy_cpp.h +++ b/src/numpy_cpp.h @@ -365,10 +365,6 @@ class array_view : public detail::array_view_accessors public: typedef T value_type; - enum { - ndim = ND - }; - array_view() : m_arr(NULL), m_data(NULL) { m_shape = zeros; @@ -492,6 +488,10 @@ class array_view : public detail::array_view_accessors return true; } + npy_intp ndim() const { + return ND; + } + npy_intp shape(size_t i) const { if (i >= ND) { From 45ab00eac5660789f55c3939049c91d7daea4c86 Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Fri, 13 Sep 2024 02:21:21 -0400 Subject: [PATCH 2/8] Port py_adaptors to pybind11 --- src/_backend_agg_wrapper.cpp | 6 +- src/_path_wrapper.cpp | 12 +-- src/py_adaptors.h | 164 ++++++++++++++++++----------------- src/py_converters_11.h | 23 ----- 4 files changed, 87 insertions(+), 118 deletions(-) diff --git a/src/_backend_agg_wrapper.cpp b/src/_backend_agg_wrapper.cpp index 79cab02e419d..71042be73cc5 100644 --- a/src/_backend_agg_wrapper.cpp +++ b/src/_backend_agg_wrapper.cpp @@ -111,7 +111,7 @@ static void PyRendererAgg_draw_path_collection(RendererAgg *self, GCAgg &gc, agg::trans_affine master_transform, - py::object paths_obj, + mpl::PathGenerator paths, py::object transforms_obj, py::object offsets_obj, agg::trans_affine offset_trans, @@ -124,7 +124,6 @@ PyRendererAgg_draw_path_collection(RendererAgg *self, // offset position is no longer used py::object Py_UNUSED(offset_position_obj)) { - mpl::PathGenerator paths; numpy::array_view transforms; numpy::array_view offsets; numpy::array_view facecolors; @@ -132,9 +131,6 @@ PyRendererAgg_draw_path_collection(RendererAgg *self, numpy::array_view linewidths; numpy::array_view antialiaseds; - if (!convert_pathgen(paths_obj.ptr(), &paths)) { - throw py::error_already_set(); - } if (!convert_transforms(transforms_obj.ptr(), &transforms)) { throw py::error_already_set(); } diff --git a/src/_path_wrapper.cpp b/src/_path_wrapper.cpp index b4eb5d19177f..83a6402740d4 100644 --- a/src/_path_wrapper.cpp +++ b/src/_path_wrapper.cpp @@ -123,17 +123,13 @@ Py_update_path_extents(mpl::PathIterator path, agg::trans_affine trans, static py::tuple Py_get_path_collection_extents(agg::trans_affine master_transform, - py::object paths_obj, py::object transforms_obj, + mpl::PathGenerator paths, py::object transforms_obj, py::object offsets_obj, agg::trans_affine offset_trans) { - mpl::PathGenerator paths; numpy::array_view transforms; numpy::array_view offsets; extent_limits e; - if (!convert_pathgen(paths_obj.ptr(), &paths)) { - throw py::error_already_set(); - } if (!convert_transforms(transforms_obj.ptr(), &transforms)) { throw py::error_already_set(); } @@ -161,18 +157,14 @@ Py_get_path_collection_extents(agg::trans_affine master_transform, static py::object Py_point_in_path_collection(double x, double y, double radius, - agg::trans_affine master_transform, py::object paths_obj, + agg::trans_affine master_transform, mpl::PathGenerator paths, py::object transforms_obj, py::object offsets_obj, agg::trans_affine offset_trans, bool filled) { - mpl::PathGenerator paths; numpy::array_view transforms; numpy::array_view offsets; std::vector result; - if (!convert_pathgen(paths_obj.ptr(), &paths)) { - throw py::error_already_set(); - } if (!convert_transforms(transforms_obj.ptr(), &transforms)) { throw py::error_already_set(); } diff --git a/src/py_adaptors.h b/src/py_adaptors.h index b0cec6c1d004..298943006ce8 100644 --- a/src/py_adaptors.h +++ b/src/py_adaptors.h @@ -8,16 +8,12 @@ * structures to C++ and Agg-friendly interfaces. */ -#include - -#include "numpy/arrayobject.h" +#include +#include #include "agg_basics.h" -#include "py_exceptions.h" -extern "C" { -int convert_path(PyObject *obj, void *pathp); -} +namespace py = pybind11; namespace mpl { @@ -35,8 +31,8 @@ class PathIterator underlying data arrays, so that Python reference counting can work. */ - PyArrayObject *m_vertices; - PyArrayObject *m_codes; + py::array_t m_vertices; + py::array_t m_codes; unsigned m_iterator; unsigned m_total_vertices; @@ -50,38 +46,29 @@ class PathIterator public: inline PathIterator() - : m_vertices(NULL), - m_codes(NULL), - m_iterator(0), + : m_iterator(0), m_total_vertices(0), m_should_simplify(false), m_simplify_threshold(1.0 / 9.0) { } - inline PathIterator(PyObject *vertices, - PyObject *codes, - bool should_simplify, + inline PathIterator(py::object vertices, py::object codes, bool should_simplify, double simplify_threshold) - : m_vertices(NULL), m_codes(NULL), m_iterator(0) + : m_iterator(0) { - if (!set(vertices, codes, should_simplify, simplify_threshold)) - throw mpl::exception(); + set(vertices, codes, should_simplify, simplify_threshold); } - inline PathIterator(PyObject *vertices, PyObject *codes) - : m_vertices(NULL), m_codes(NULL), m_iterator(0) + inline PathIterator(py::object vertices, py::object codes) + : m_iterator(0) { - if (!set(vertices, codes)) - throw mpl::exception(); + set(vertices, codes); } inline PathIterator(const PathIterator &other) { - Py_XINCREF(other.m_vertices); m_vertices = other.m_vertices; - - Py_XINCREF(other.m_codes); m_codes = other.m_codes; m_iterator = 0; @@ -91,47 +78,45 @@ class PathIterator m_simplify_threshold = other.m_simplify_threshold; } - ~PathIterator() - { - Py_XDECREF(m_vertices); - Py_XDECREF(m_codes); - } - - inline int - set(PyObject *vertices, PyObject *codes, bool should_simplify, double simplify_threshold) + inline void + set(py::object vertices, py::object codes, bool should_simplify, double simplify_threshold) { m_should_simplify = should_simplify; m_simplify_threshold = simplify_threshold; - Py_XDECREF(m_vertices); - m_vertices = (PyArrayObject *)PyArray_FromObject(vertices, NPY_DOUBLE, 2, 2); - - if (!m_vertices || PyArray_DIM(m_vertices, 1) != 2) { - PyErr_SetString(PyExc_ValueError, "Invalid vertices array"); - return 0; + m_vertices = vertices.cast>(); + if (m_vertices.ndim() != 2 || m_vertices.shape(1) != 2) { + throw py::value_error("Invalid vertices array"); } + m_total_vertices = m_vertices.shape(0); - Py_XDECREF(m_codes); - m_codes = NULL; - - if (codes != NULL && codes != Py_None) { - m_codes = (PyArrayObject *)PyArray_FromObject(codes, NPY_UINT8, 1, 1); - - if (!m_codes || PyArray_DIM(m_codes, 0) != PyArray_DIM(m_vertices, 0)) { - PyErr_SetString(PyExc_ValueError, "Invalid codes array"); - return 0; + m_codes.release().dec_ref(); + if (!codes.is_none()) { + m_codes = codes.cast>(); + if (m_codes.ndim() != 1 || m_codes.shape(0) != m_total_vertices) { + throw py::value_error("Invalid codes array"); } } - m_total_vertices = (unsigned)PyArray_DIM(m_vertices, 0); m_iterator = 0; + } + inline int + set(PyObject *vertices, PyObject *codes, bool should_simplify, double simplify_threshold) + { + try { + set(py::reinterpret_borrow(vertices), + py::reinterpret_borrow(codes), + should_simplify, simplify_threshold); + } catch(const py::error_already_set &) { + return 0; + } return 1; } - inline int set(PyObject *vertices, PyObject *codes) + inline void set(py::object vertices, py::object codes) { - return set(vertices, codes, false, 0.0); + set(vertices, codes, false, 0.0); } inline unsigned vertex(double *x, double *y) @@ -144,12 +129,11 @@ class PathIterator const size_t idx = m_iterator++; - char *pair = (char *)PyArray_GETPTR2(m_vertices, idx, 0); - *x = *(double *)pair; - *y = *(double *)(pair + PyArray_STRIDE(m_vertices, 1)); + *x = *m_vertices.data(idx, 0); + *y = *m_vertices.data(idx, 1); - if (m_codes != NULL) { - return (unsigned)(*(char *)PyArray_GETPTR1(m_codes, idx)); + if (m_codes) { + return *m_codes.data(idx); } else { return idx == 0 ? agg::path_cmd_move_to : agg::path_cmd_line_to; } @@ -177,42 +161,38 @@ class PathIterator inline bool has_codes() const { - return m_codes != NULL; + return bool(m_codes); } inline void *get_id() { - return (void *)m_vertices; + return (void *)m_vertices.ptr(); } }; class PathGenerator { - PyObject *m_paths; + py::sequence m_paths; Py_ssize_t m_npaths; public: typedef PathIterator path_iterator; - PathGenerator() : m_paths(NULL), m_npaths(0) {} + PathGenerator() : m_npaths(0) {} - ~PathGenerator() + void set(py::object obj) { - Py_XDECREF(m_paths); + m_paths = obj.cast(); + m_npaths = m_paths.size(); } int set(PyObject *obj) { - if (!PySequence_Check(obj)) { + try { + set(py::reinterpret_borrow(obj)); + } catch(const py::error_already_set &) { return 0; } - - Py_XDECREF(m_paths); - m_paths = obj; - Py_INCREF(m_paths); - - m_npaths = PySequence_Size(m_paths); - return 1; } @@ -229,20 +209,44 @@ class PathGenerator path_iterator operator()(size_t i) { path_iterator path; - PyObject *item; - item = PySequence_GetItem(m_paths, i % m_npaths); - if (item == NULL) { - throw mpl::exception(); - } - if (!convert_path(item, &path)) { - Py_DECREF(item); - throw mpl::exception(); - } - Py_DECREF(item); + auto item = m_paths[i % m_npaths]; + path = item.cast(); return path; } }; } +namespace PYBIND11_NAMESPACE { namespace detail { + template <> struct type_caster { + public: + PYBIND11_TYPE_CASTER(mpl::PathIterator, const_name("PathIterator")); + + bool load(handle src, bool) { + if (src.is_none()) { + return true; + } + + py::object vertices = src.attr("vertices"); + py::object codes = src.attr("codes"); + auto should_simplify = src.attr("should_simplify").cast(); + auto simplify_threshold = src.attr("simplify_threshold").cast(); + + value.set(vertices, codes, should_simplify, simplify_threshold); + + return true; + } + }; + + template <> struct type_caster { + public: + PYBIND11_TYPE_CASTER(mpl::PathGenerator, const_name("PathGenerator")); + + bool load(handle src, bool) { + value.set(py::reinterpret_borrow(src)); + return true; + } + }; +}} // namespace PYBIND11_NAMESPACE::detail + #endif diff --git a/src/py_converters_11.h b/src/py_converters_11.h index ef5d8989c072..6bf85733ebfd 100644 --- a/src/py_converters_11.h +++ b/src/py_converters_11.h @@ -167,29 +167,6 @@ namespace PYBIND11_NAMESPACE { namespace detail { return true; } }; - - template <> struct type_caster { - public: - PYBIND11_TYPE_CASTER(mpl::PathIterator, const_name("PathIterator")); - - bool load(handle src, bool) { - if (src.is_none()) { - return true; - } - - py::object vertices = src.attr("vertices"); - py::object codes = src.attr("codes"); - auto should_simplify = src.attr("should_simplify").cast(); - auto simplify_threshold = src.attr("simplify_threshold").cast(); - - if (!value.set(vertices.inc_ref().ptr(), codes.inc_ref().ptr(), - should_simplify, simplify_threshold)) { - throw py::error_already_set(); - } - - return true; - } - }; #endif /* Remove all this macro magic after dropping NumPy usage and just include `_backend_agg_basic_types.h`. */ From 086add18d724fb1ba7d25767938b2c78f72975e4 Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Fri, 20 Sep 2024 20:21:05 -0400 Subject: [PATCH 3/8] Don't enforce trailing shape on empty arrays They need only be the same number of dimensions, as sometimes code does `np.atleast_3d(array)` on something empty, which inserts the 0-dimension in a spot that messes with the expected trailing shape. --- src/mplutils.h | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/mplutils.h b/src/mplutils.h index 05c3436626e2..cac710617626 100644 --- a/src/mplutils.h +++ b/src/mplutils.h @@ -77,6 +77,11 @@ inline bool check_trailing_shape(T array, char const* name, long d1) array.ndim()); return false; } + if (array.size() == 0) { + // Sometimes things come through as atleast_2d, etc., but they're empty, so + // don't bother enforcing the trailing shape. + return true; + } if (array.shape(1) != d1) { PyErr_Format(PyExc_ValueError, "%s must have shape (N, %ld), got (%ld, %ld)", @@ -95,6 +100,11 @@ inline bool check_trailing_shape(T array, char const* name, long d1, long d2) array.ndim()); return false; } + if (array.size() == 0) { + // Sometimes things come through as atleast_3d, etc., but they're empty, so + // don't bother enforcing the trailing shape. + return true; + } if (array.shape(1) != d1 || array.shape(2) != d2) { PyErr_Format(PyExc_ValueError, "%s must have shape (N, %ld, %ld), got (%ld, %ld, %ld)", From 0305729159b8703c1addd73d53fc19b248916a01 Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Fri, 13 Sep 2024 19:37:09 -0400 Subject: [PATCH 4/8] Convert remaining `array_view` to pybind11 `array_t` --- src/_backend_agg_wrapper.cpp | 65 ++++++++++-------------------------- src/_path.h | 47 +++++++++++++------------- src/_path_wrapper.cpp | 51 ++++++++-------------------- src/mplutils.h | 23 +++++++++++++ src/py_converters_11.h | 32 ++++++++++++++++++ 5 files changed, 110 insertions(+), 108 deletions(-) diff --git a/src/_backend_agg_wrapper.cpp b/src/_backend_agg_wrapper.cpp index 71042be73cc5..fb241b217fe9 100644 --- a/src/_backend_agg_wrapper.cpp +++ b/src/_backend_agg_wrapper.cpp @@ -112,43 +112,24 @@ PyRendererAgg_draw_path_collection(RendererAgg *self, GCAgg &gc, agg::trans_affine master_transform, mpl::PathGenerator paths, - py::object transforms_obj, - py::object offsets_obj, + py::array_t transforms_obj, + py::array_t offsets_obj, agg::trans_affine offset_trans, - py::object facecolors_obj, - py::object edgecolors_obj, - py::object linewidths_obj, + py::array_t facecolors_obj, + py::array_t edgecolors_obj, + py::array_t linewidths_obj, DashesVector dashes, - py::object antialiaseds_obj, + py::array_t antialiaseds_obj, py::object Py_UNUSED(ignored_obj), // offset position is no longer used py::object Py_UNUSED(offset_position_obj)) { - numpy::array_view transforms; - numpy::array_view offsets; - numpy::array_view facecolors; - numpy::array_view edgecolors; - numpy::array_view linewidths; - numpy::array_view antialiaseds; - - if (!convert_transforms(transforms_obj.ptr(), &transforms)) { - throw py::error_already_set(); - } - if (!convert_points(offsets_obj.ptr(), &offsets)) { - throw py::error_already_set(); - } - if (!convert_colors(facecolors_obj.ptr(), &facecolors)) { - throw py::error_already_set(); - } - if (!convert_colors(edgecolors_obj.ptr(), &edgecolors)) { - throw py::error_already_set(); - } - if (!linewidths.converter(linewidths_obj.ptr(), &linewidths)) { - throw py::error_already_set(); - } - if (!antialiaseds.converter(antialiaseds_obj.ptr(), &antialiaseds)) { - throw py::error_already_set(); - } + auto transforms = convert_transforms(transforms_obj); + auto offsets = convert_points(offsets_obj); + auto facecolors = convert_colors(facecolors_obj); + auto edgecolors = convert_colors(edgecolors_obj); + auto linewidths = linewidths_obj.unchecked<1>(); + auto antialiaseds = antialiaseds_obj.unchecked<1>(); self->draw_path_collection(gc, master_transform, @@ -170,26 +151,16 @@ PyRendererAgg_draw_quad_mesh(RendererAgg *self, unsigned int mesh_width, unsigned int mesh_height, py::array_t coordinates_obj, - py::object offsets_obj, + py::array_t offsets_obj, agg::trans_affine offset_trans, - py::object facecolors_obj, + py::array_t facecolors_obj, bool antialiased, - py::object edgecolors_obj) + py::array_t edgecolors_obj) { - numpy::array_view offsets; - numpy::array_view facecolors; - numpy::array_view edgecolors; - auto coordinates = coordinates_obj.mutable_unchecked<3>(); - if (!convert_points(offsets_obj.ptr(), &offsets)) { - throw py::error_already_set(); - } - if (!convert_colors(facecolors_obj.ptr(), &facecolors)) { - throw py::error_already_set(); - } - if (!convert_colors(edgecolors_obj.ptr(), &edgecolors)) { - throw py::error_already_set(); - } + auto offsets = convert_points(offsets_obj); + auto facecolors = convert_colors(facecolors_obj); + auto edgecolors = convert_colors(edgecolors_obj); self->draw_quad_mesh(gc, master_transform, diff --git a/src/_path.h b/src/_path.h index 7f17d0bc2933..0e1561223442 100644 --- a/src/_path.h +++ b/src/_path.h @@ -245,8 +245,7 @@ inline void points_in_path(PointArray &points, typedef agg::conv_curve curve_t; typedef agg::conv_contour contour_t; - size_t i; - for (i = 0; i < safe_first_shape(points); ++i) { + for (auto i = 0; i < safe_first_shape(points); ++i) { result[i] = false; } @@ -270,10 +269,11 @@ template inline bool point_in_path( double x, double y, const double r, PathIterator &path, agg::trans_affine &trans) { - npy_intp shape[] = {1, 2}; - numpy::array_view points(shape); - points(0, 0) = x; - points(0, 1) = y; + py::ssize_t shape[] = {1, 2}; + py::array_t points_arr(shape); + *points_arr.mutable_data(0, 0) = x; + *points_arr.mutable_data(0, 1) = y; + auto points = points_arr.mutable_unchecked<2>(); int result[1]; result[0] = 0; @@ -292,10 +292,11 @@ inline bool point_on_path( typedef agg::conv_curve curve_t; typedef agg::conv_stroke stroke_t; - npy_intp shape[] = {1, 2}; - numpy::array_view points(shape); - points(0, 0) = x; - points(0, 1) = y; + py::ssize_t shape[] = {1, 2}; + py::array_t points_arr(shape); + *points_arr.mutable_data(0, 0) = x; + *points_arr.mutable_data(0, 1) = y; + auto points = points_arr.mutable_unchecked<2>(); int result[1]; result[0] = 0; @@ -382,20 +383,19 @@ void get_path_collection_extents(agg::trans_affine &master_transform, throw std::runtime_error("Offsets array must have shape (N, 2)"); } - size_t Npaths = paths.size(); - size_t Noffsets = safe_first_shape(offsets); - size_t N = std::max(Npaths, Noffsets); - size_t Ntransforms = std::min(safe_first_shape(transforms), N); - size_t i; + auto Npaths = paths.size(); + auto Noffsets = safe_first_shape(offsets); + auto N = std::max(Npaths, Noffsets); + auto Ntransforms = std::min(safe_first_shape(transforms), N); agg::trans_affine trans; reset_limits(extent); - for (i = 0; i < N; ++i) { + for (auto i = 0; i < N; ++i) { typename PathGenerator::path_iterator path(paths(i % Npaths)); if (Ntransforms) { - size_t ti = i % Ntransforms; + py::ssize_t ti = i % Ntransforms; trans = agg::trans_affine(transforms(ti, 0, 0), transforms(ti, 1, 0), transforms(ti, 0, 1), @@ -429,24 +429,23 @@ void point_in_path_collection(double x, bool filled, std::vector &result) { - size_t Npaths = paths.size(); + auto Npaths = paths.size(); if (Npaths == 0) { return; } - size_t Noffsets = safe_first_shape(offsets); - size_t N = std::max(Npaths, Noffsets); - size_t Ntransforms = std::min(safe_first_shape(transforms), N); - size_t i; + auto Noffsets = safe_first_shape(offsets); + auto N = std::max(Npaths, Noffsets); + auto Ntransforms = std::min(safe_first_shape(transforms), N); agg::trans_affine trans; - for (i = 0; i < N; ++i) { + for (auto i = 0; i < N; ++i) { typename PathGenerator::path_iterator path = paths(i % Npaths); if (Ntransforms) { - size_t ti = i % Ntransforms; + auto ti = i % Ntransforms; trans = agg::trans_affine(transforms(ti, 0, 0), transforms(ti, 1, 0), transforms(ti, 0, 1), diff --git a/src/_path_wrapper.cpp b/src/_path_wrapper.cpp index 83a6402740d4..d3a42ca0830d 100644 --- a/src/_path_wrapper.cpp +++ b/src/_path_wrapper.cpp @@ -44,17 +44,9 @@ static py::array_t Py_points_in_path(py::array_t points_obj, double r, mpl::PathIterator path, agg::trans_affine trans) { - numpy::array_view points; + auto points = convert_points(points_obj); - if (!convert_points(points_obj.ptr(), &points)) { - throw py::error_already_set(); - } - - if (!check_trailing_shape(points, "points", 2)) { - throw py::error_already_set(); - } - - py::ssize_t dims[] = { static_cast(points.size()) }; + py::ssize_t dims[] = { points.shape(0) }; py::array_t results(dims); auto results_mutable = results.mutable_unchecked<1>(); @@ -123,20 +115,15 @@ Py_update_path_extents(mpl::PathIterator path, agg::trans_affine trans, static py::tuple Py_get_path_collection_extents(agg::trans_affine master_transform, - mpl::PathGenerator paths, py::object transforms_obj, - py::object offsets_obj, agg::trans_affine offset_trans) + mpl::PathGenerator paths, + py::array_t transforms_obj, + py::array_t offsets_obj, + agg::trans_affine offset_trans) { - numpy::array_view transforms; - numpy::array_view offsets; + auto transforms = convert_transforms(transforms_obj); + auto offsets = convert_points(offsets_obj); extent_limits e; - if (!convert_transforms(transforms_obj.ptr(), &transforms)) { - throw py::error_already_set(); - } - if (!convert_points(offsets_obj.ptr(), &offsets)) { - throw py::error_already_set(); - } - get_path_collection_extents( master_transform, paths, transforms, offsets, offset_trans, e); @@ -158,20 +145,14 @@ Py_get_path_collection_extents(agg::trans_affine master_transform, static py::object Py_point_in_path_collection(double x, double y, double radius, agg::trans_affine master_transform, mpl::PathGenerator paths, - py::object transforms_obj, py::object offsets_obj, + py::array_t transforms_obj, + py::array_t offsets_obj, agg::trans_affine offset_trans, bool filled) { - numpy::array_view transforms; - numpy::array_view offsets; + auto transforms = convert_transforms(transforms_obj); + auto offsets = convert_points(offsets_obj); std::vector result; - if (!convert_transforms(transforms_obj.ptr(), &transforms)) { - throw py::error_already_set(); - } - if (!convert_points(offsets_obj.ptr(), &offsets)) { - throw py::error_already_set(); - } - point_in_path_collection(x, y, radius, master_transform, paths, transforms, offsets, offset_trans, filled, result); @@ -229,13 +210,9 @@ Py_affine_transform(py::array_t bboxes_obj) { - numpy::array_view bboxes; - - if (!convert_bboxes(bboxes_obj.ptr(), &bboxes)) { - throw py::error_already_set(); - } + auto bboxes = convert_bboxes(bboxes_obj); return count_bboxes_overlapping_bbox(bbox, bboxes); } diff --git a/src/mplutils.h b/src/mplutils.h index cac710617626..b7a80a84429c 100644 --- a/src/mplutils.h +++ b/src/mplutils.h @@ -67,6 +67,10 @@ inline int prepare_and_add_type(PyTypeObject *type, PyObject *module) #ifdef __cplusplus // not for macosx.m // Check that array has shape (N, d1) or (N, d1, d2). We cast d1, d2 to longs // so that we don't need to access the NPY_INTP_FMT macro here. +#include +#include + +namespace py = pybind11; template inline bool check_trailing_shape(T array, char const* name, long d1) @@ -113,6 +117,25 @@ inline bool check_trailing_shape(T array, char const* name, long d1, long d2) } return true; } + +/* In most cases, code should use safe_first_shape(obj) instead of obj.shape(0), since + safe_first_shape(obj) == 0 when any dimension is 0. */ +template +py::ssize_t +safe_first_shape(const py::detail::unchecked_reference &a) +{ + bool empty = (ND == 0); + for (py::ssize_t i = 0; i < ND; i++) { + if (a.shape(i) == 0) { + empty = true; + } + } + if (empty) { + return 0; + } else { + return a.shape(0); + } +} #endif #endif diff --git a/src/py_converters_11.h b/src/py_converters_11.h index 6bf85733ebfd..e57e6072ef79 100644 --- a/src/py_converters_11.h +++ b/src/py_converters_11.h @@ -17,6 +17,38 @@ namespace py = pybind11; void convert_trans_affine(const py::object& transform, agg::trans_affine& affine); +inline auto convert_points(py::array_t obj) +{ + if (!check_trailing_shape(obj, "points", 2)) { + throw py::error_already_set(); + } + return obj.unchecked<2>(); +} + +inline auto convert_transforms(py::array_t obj) +{ + if (!check_trailing_shape(obj, "transforms", 3, 3)) { + throw py::error_already_set(); + } + return obj.unchecked<3>(); +} + +inline auto convert_bboxes(py::array_t obj) +{ + if (!check_trailing_shape(obj, "bbox array", 2, 2)) { + throw py::error_already_set(); + } + return obj.unchecked<3>(); +} + +inline auto convert_colors(py::array_t obj) +{ + if (!check_trailing_shape(obj, "colors", 4)) { + throw py::error_already_set(); + } + return obj.unchecked<2>(); +} + namespace PYBIND11_NAMESPACE { namespace detail { template <> struct type_caster { public: From 2b2baa4f2caf2826c3cf223e6a7a8944d344131e Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Fri, 20 Sep 2024 21:21:51 -0400 Subject: [PATCH 5/8] Use pybind11 type caster for GCAgg.clippath Now that everything else is using pybind11, this works without issue. --- src/py_converters_11.h | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/py_converters_11.h b/src/py_converters_11.h index e57e6072ef79..3d5673548366 100644 --- a/src/py_converters_11.h +++ b/src/py_converters_11.h @@ -294,8 +294,7 @@ namespace PYBIND11_NAMESPACE { namespace detail { value.join = src.attr("_joinstyle").cast(); value.dashes = src.attr("get_dashes")().cast(); value.cliprect = src.attr("_cliprect").cast(); - /* value.clippath = src.attr("get_clip_path")().cast(); */ - convert_clippath(src.attr("get_clip_path")().ptr(), &value.clippath); + value.clippath = src.attr("get_clip_path")().cast(); value.snap_mode = src.attr("get_snap")().cast(); value.hatchpath = src.attr("get_hatch_path")().cast(); value.hatch_color = src.attr("get_hatch_color")().cast(); From bab748c21a1b9be2a5de8e817995043aac072a88 Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Fri, 20 Sep 2024 22:07:48 -0400 Subject: [PATCH 6/8] Move type casters into files that define their types Since we're using pybind11 everywhere, it should be fine now to access it in any header, and putting the type caster there is clearer. We don't need the weird macro checks to conditionally define them either. --- src/_backend_agg_basic_types.h | 136 ++++++++++++++++++++++++++++ src/path_converters.h | 20 +++++ src/py_converters_11.h | 159 +-------------------------------- 3 files changed, 157 insertions(+), 158 deletions(-) diff --git a/src/_backend_agg_basic_types.h b/src/_backend_agg_basic_types.h index 4fbf846d8cb4..bbd636e68b33 100644 --- a/src/_backend_agg_basic_types.h +++ b/src/_backend_agg_basic_types.h @@ -4,6 +4,9 @@ /* Contains some simple types from the Agg backend that are also used by other modules */ +#include + +#include #include #include "agg_color_rgba.h" @@ -13,6 +16,8 @@ #include "py_adaptors.h" +namespace py = pybind11; + struct ClipPath { mpl::PathIterator path; @@ -121,4 +126,135 @@ class GCAgg GCAgg &operator=(const GCAgg &); }; +namespace PYBIND11_NAMESPACE { namespace detail { + template <> struct type_caster { + public: + PYBIND11_TYPE_CASTER(agg::line_cap_e, const_name("line_cap_e")); + + bool load(handle src, bool) { + const std::unordered_map enum_values = { + {"butt", agg::butt_cap}, + {"round", agg::round_cap}, + {"projecting", agg::square_cap}, + }; + value = enum_values.at(src.cast()); + return true; + } + }; + + template <> struct type_caster { + public: + PYBIND11_TYPE_CASTER(agg::line_join_e, const_name("line_join_e")); + + bool load(handle src, bool) { + const std::unordered_map enum_values = { + {"miter", agg::miter_join_revert}, + {"round", agg::round_join}, + {"bevel", agg::bevel_join}, + }; + value = agg::miter_join_revert; + value = enum_values.at(src.cast()); + return true; + } + }; + + template <> struct type_caster { + public: + PYBIND11_TYPE_CASTER(ClipPath, const_name("ClipPath")); + + bool load(handle src, bool) { + if (src.is_none()) { + return true; + } + + auto clippath_tuple = src.cast(); + + auto path = clippath_tuple[0]; + if (!path.is_none()) { + value.path = path.cast(); + } + value.trans = clippath_tuple[1].cast(); + + return true; + } + }; + + template <> struct type_caster { + public: + PYBIND11_TYPE_CASTER(Dashes, const_name("Dashes")); + + bool load(handle src, bool) { + auto dash_tuple = src.cast(); + auto dash_offset = dash_tuple[0].cast(); + auto dashes_seq_or_none = dash_tuple[1]; + + if (dashes_seq_or_none.is_none()) { + return true; + } + + auto dashes_seq = dashes_seq_or_none.cast(); + + auto nentries = dashes_seq.size(); + // If the dashpattern has odd length, iterate through it twice (in + // accordance with the pdf/ps/svg specs). + auto dash_pattern_length = (nentries % 2) ? 2 * nentries : nentries; + + for (py::size_t i = 0; i < dash_pattern_length; i += 2) { + auto length = dashes_seq[i % nentries].cast(); + auto skip = dashes_seq[(i + 1) % nentries].cast(); + + value.add_dash_pair(length, skip); + } + + value.set_dash_offset(dash_offset); + + return true; + } + }; + + template <> struct type_caster { + public: + PYBIND11_TYPE_CASTER(SketchParams, const_name("SketchParams")); + + bool load(handle src, bool) { + if (src.is_none()) { + value.scale = 0.0; + value.length = 0.0; + value.randomness = 0.0; + return true; + } + + auto params = src.cast>(); + std::tie(value.scale, value.length, value.randomness) = params; + + return true; + } + }; + + template <> struct type_caster { + public: + PYBIND11_TYPE_CASTER(GCAgg, const_name("GCAgg")); + + bool load(handle src, bool) { + value.linewidth = src.attr("_linewidth").cast(); + value.alpha = src.attr("_alpha").cast(); + value.forced_alpha = src.attr("_forced_alpha").cast(); + value.color = src.attr("_rgb").cast(); + value.isaa = src.attr("_antialiased").cast(); + value.cap = src.attr("_capstyle").cast(); + value.join = src.attr("_joinstyle").cast(); + value.dashes = src.attr("get_dashes")().cast(); + value.cliprect = src.attr("_cliprect").cast(); + value.clippath = src.attr("get_clip_path")().cast(); + value.snap_mode = src.attr("get_snap")().cast(); + value.hatchpath = src.attr("get_hatch_path")().cast(); + value.hatch_color = src.attr("get_hatch_color")().cast(); + value.hatch_linewidth = src.attr("get_hatch_linewidth")().cast(); + value.sketch = src.attr("get_sketch_params")().cast(); + + return true; + } + }; +}} // namespace PYBIND11_NAMESPACE::detail + #endif diff --git a/src/path_converters.h b/src/path_converters.h index 6d242e74415b..6877ab6ed4c3 100644 --- a/src/path_converters.h +++ b/src/path_converters.h @@ -3,6 +3,8 @@ #ifndef MPL_PATH_CONVERTERS_H #define MPL_PATH_CONVERTERS_H +#include + #include #include #include @@ -530,6 +532,24 @@ enum e_snap_mode { SNAP_TRUE }; +namespace PYBIND11_NAMESPACE { namespace detail { + template <> struct type_caster { + public: + PYBIND11_TYPE_CASTER(e_snap_mode, const_name("e_snap_mode")); + + bool load(handle src, bool) { + if (src.is_none()) { + value = SNAP_AUTO; + return true; + } + + value = src.cast() ? SNAP_TRUE : SNAP_FALSE; + + return true; + } + }; +}} // namespace PYBIND11_NAMESPACE::detail + template class PathSnapper { diff --git a/src/py_converters_11.h b/src/py_converters_11.h index 3d5673548366..b093f71b181a 100644 --- a/src/py_converters_11.h +++ b/src/py_converters_11.h @@ -8,12 +8,10 @@ namespace py = pybind11; -#include - #include "agg_basics.h" #include "agg_color_rgba.h" #include "agg_trans_affine.h" -#include "path_converters.h" +#include "mplutils.h" void convert_trans_affine(const py::object& transform, agg::trans_affine& affine); @@ -150,161 +148,6 @@ namespace PYBIND11_NAMESPACE { namespace detail { return true; } }; - - template <> struct type_caster { - public: - PYBIND11_TYPE_CASTER(e_snap_mode, const_name("e_snap_mode")); - - bool load(handle src, bool) { - if (src.is_none()) { - value = SNAP_AUTO; - return true; - } - - value = src.cast() ? SNAP_TRUE : SNAP_FALSE; - - return true; - } - }; - -/* Remove all this macro magic after dropping NumPy usage and just include `py_adaptors.h`. */ -#ifdef MPL_PY_ADAPTORS_H - template <> struct type_caster { - public: - PYBIND11_TYPE_CASTER(agg::line_cap_e, const_name("line_cap_e")); - - bool load(handle src, bool) { - const std::unordered_map enum_values = { - {"butt", agg::butt_cap}, - {"round", agg::round_cap}, - {"projecting", agg::square_cap}, - }; - value = enum_values.at(src.cast()); - return true; - } - }; - - template <> struct type_caster { - public: - PYBIND11_TYPE_CASTER(agg::line_join_e, const_name("line_join_e")); - - bool load(handle src, bool) { - const std::unordered_map enum_values = { - {"miter", agg::miter_join_revert}, - {"round", agg::round_join}, - {"bevel", agg::bevel_join}, - }; - value = agg::miter_join_revert; - value = enum_values.at(src.cast()); - return true; - } - }; -#endif - -/* Remove all this macro magic after dropping NumPy usage and just include `_backend_agg_basic_types.h`. */ -#ifdef MPL_BACKEND_AGG_BASIC_TYPES_H -# ifndef MPL_PY_ADAPTORS_H -# error "py_adaptors.h must be included to get Agg type casters" -# endif - - template <> struct type_caster { - public: - PYBIND11_TYPE_CASTER(ClipPath, const_name("ClipPath")); - - bool load(handle src, bool) { - if (src.is_none()) { - return true; - } - - auto clippath_tuple = src.cast(); - - auto path = clippath_tuple[0]; - if (!path.is_none()) { - value.path = path.cast(); - } - value.trans = clippath_tuple[1].cast(); - - return true; - } - }; - - template <> struct type_caster { - public: - PYBIND11_TYPE_CASTER(Dashes, const_name("Dashes")); - - bool load(handle src, bool) { - auto dash_tuple = src.cast(); - auto dash_offset = dash_tuple[0].cast(); - auto dashes_seq_or_none = dash_tuple[1]; - - if (dashes_seq_or_none.is_none()) { - return true; - } - - auto dashes_seq = dashes_seq_or_none.cast(); - - auto nentries = dashes_seq.size(); - // If the dashpattern has odd length, iterate through it twice (in - // accordance with the pdf/ps/svg specs). - auto dash_pattern_length = (nentries % 2) ? 2 * nentries : nentries; - - for (py::size_t i = 0; i < dash_pattern_length; i += 2) { - auto length = dashes_seq[i % nentries].cast(); - auto skip = dashes_seq[(i + 1) % nentries].cast(); - - value.add_dash_pair(length, skip); - } - - value.set_dash_offset(dash_offset); - - return true; - } - }; - - template <> struct type_caster { - public: - PYBIND11_TYPE_CASTER(SketchParams, const_name("SketchParams")); - - bool load(handle src, bool) { - if (src.is_none()) { - value.scale = 0.0; - value.length = 0.0; - value.randomness = 0.0; - return true; - } - - auto params = src.cast>(); - std::tie(value.scale, value.length, value.randomness) = params; - - return true; - } - }; - - template <> struct type_caster { - public: - PYBIND11_TYPE_CASTER(GCAgg, const_name("GCAgg")); - - bool load(handle src, bool) { - value.linewidth = src.attr("_linewidth").cast(); - value.alpha = src.attr("_alpha").cast(); - value.forced_alpha = src.attr("_forced_alpha").cast(); - value.color = src.attr("_rgb").cast(); - value.isaa = src.attr("_antialiased").cast(); - value.cap = src.attr("_capstyle").cast(); - value.join = src.attr("_joinstyle").cast(); - value.dashes = src.attr("get_dashes")().cast(); - value.cliprect = src.attr("_cliprect").cast(); - value.clippath = src.attr("get_clip_path")().cast(); - value.snap_mode = src.attr("get_snap")().cast(); - value.hatchpath = src.attr("get_hatch_path")().cast(); - value.hatch_color = src.attr("get_hatch_color")().cast(); - value.hatch_linewidth = src.attr("get_hatch_linewidth")().cast(); - value.sketch = src.attr("get_sketch_params")().cast(); - - return true; - } - }; -#endif }} // namespace PYBIND11_NAMESPACE::detail #endif /* MPL_PY_CONVERTERS_11_H */ From 1818c7bbb4cae5e5a173268ab840eeb414306116 Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Fri, 20 Sep 2024 22:39:11 -0400 Subject: [PATCH 7/8] Convert _path.is_sorted_and_has_non_nan to pybind11 --- src/_path.h | 10 ++++------ src/_path_wrapper.cpp | 45 +++++++++++++++---------------------------- 2 files changed, 19 insertions(+), 36 deletions(-) diff --git a/src/_path.h b/src/_path.h index 0e1561223442..693862c7a829 100644 --- a/src/_path.h +++ b/src/_path.h @@ -1223,17 +1223,15 @@ bool convert_to_string(PathIterator &path, } template -bool is_sorted_and_has_non_nan(PyArrayObject *array) +bool is_sorted_and_has_non_nan(py::array_t array) { - char* ptr = PyArray_BYTES(array); - npy_intp size = PyArray_DIM(array, 0), - stride = PyArray_STRIDE(array, 0); + auto size = array.shape(0); using limits = std::numeric_limits; T last = limits::has_infinity ? -limits::infinity() : limits::min(); bool found_non_nan = false; - for (npy_intp i = 0; i < size; ++i, ptr += stride) { - T current = *(T*)ptr; + for (auto i = 0; i < size; ++i) { + T current = *array.data(i); // The following tests !isnan(current), but also works for integral // types. (The isnan(IntegralType) overload is absent on MSVC.) if (current == current) { diff --git a/src/_path_wrapper.cpp b/src/_path_wrapper.cpp index d3a42ca0830d..13431601e5af 100644 --- a/src/_path_wrapper.cpp +++ b/src/_path_wrapper.cpp @@ -350,41 +350,26 @@ Py_is_sorted_and_has_non_nan(py::object obj) { bool result; - PyArrayObject *array = (PyArrayObject *)PyArray_CheckFromAny( - obj.ptr(), NULL, 1, 1, NPY_ARRAY_NOTSWAPPED, NULL); - - if (array == NULL) { - throw py::error_already_set(); + py::array array = py::array::ensure(obj); + if (array.ndim() != 1) { + throw std::invalid_argument("array must be 1D"); } + auto dtype = array.dtype(); /* Handle just the most common types here, otherwise coerce to double */ - switch (PyArray_TYPE(array)) { - case NPY_INT: - result = is_sorted_and_has_non_nan(array); - break; - case NPY_LONG: - result = is_sorted_and_has_non_nan(array); - break; - case NPY_LONGLONG: - result = is_sorted_and_has_non_nan(array); - break; - case NPY_FLOAT: - result = is_sorted_and_has_non_nan(array); - break; - case NPY_DOUBLE: - result = is_sorted_and_has_non_nan(array); - break; - default: - Py_DECREF(array); - array = (PyArrayObject *)PyArray_FromObject(obj.ptr(), NPY_DOUBLE, 1, 1); - if (array == NULL) { - throw py::error_already_set(); - } - result = is_sorted_and_has_non_nan(array); + if (dtype.equal(py::dtype::of())) { + result = is_sorted_and_has_non_nan(array); + } else if (dtype.equal(py::dtype::of())) { + result = is_sorted_and_has_non_nan(array); + } else if (dtype.equal(py::dtype::of())) { + result = is_sorted_and_has_non_nan(array); + } else if (dtype.equal(py::dtype::of())) { + result = is_sorted_and_has_non_nan(array); + } else { + array = py::array_t::ensure(obj); + result = is_sorted_and_has_non_nan(array); } - Py_DECREF(array); - return result; } From 37206c2fc1a9fe174ca0a4933d3b395b84395e84 Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Tue, 1 Oct 2024 22:22:40 -0400 Subject: [PATCH 8/8] Clean up some pybind11 Agg type casters Co-authored-by: Antony Lee --- src/_backend_agg_basic_types.h | 21 +++++++++------------ 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/src/_backend_agg_basic_types.h b/src/_backend_agg_basic_types.h index bbd636e68b33..e3e6be9a4532 100644 --- a/src/_backend_agg_basic_types.h +++ b/src/_backend_agg_basic_types.h @@ -152,7 +152,6 @@ namespace PYBIND11_NAMESPACE { namespace detail { {"round", agg::round_join}, {"bevel", agg::bevel_join}, }; - value = agg::miter_join_revert; value = enum_values.at(src.cast()); return true; } @@ -167,13 +166,12 @@ namespace PYBIND11_NAMESPACE { namespace detail { return true; } - auto clippath_tuple = src.cast(); - - auto path = clippath_tuple[0]; - if (!path.is_none()) { - value.path = path.cast(); + auto [path, trans] = + src.cast, agg::trans_affine>>(); + if (path) { + value.path = *path; } - value.trans = clippath_tuple[1].cast(); + value.trans = trans; return true; } @@ -184,15 +182,14 @@ namespace PYBIND11_NAMESPACE { namespace detail { PYBIND11_TYPE_CASTER(Dashes, const_name("Dashes")); bool load(handle src, bool) { - auto dash_tuple = src.cast(); - auto dash_offset = dash_tuple[0].cast(); - auto dashes_seq_or_none = dash_tuple[1]; + auto [dash_offset, dashes_seq_or_none] = + src.cast>>(); - if (dashes_seq_or_none.is_none()) { + if (!dashes_seq_or_none) { return true; } - auto dashes_seq = dashes_seq_or_none.cast(); + auto dashes_seq = *dashes_seq_or_none; auto nentries = dashes_seq.size(); // If the dashpattern has odd length, iterate through it twice (in