From 14391a6da45e845b2686c65f18c9bd423e381696 Mon Sep 17 00:00:00 2001 From: Aleksandr Motsjonov Date: Sat, 11 Jul 2026 13:12:32 +1000 Subject: [PATCH] feat(python): Expand dual-backend bindings and harden edge cases Bring remaining Python modules onto the shared pybind11/nanobind sources, simplify helpers (buffer request, make_numpy_array shapes, module attrs), and add OIIO_PY_PROP_RW_NONE for None-assignable properties. Add regression coverage for 0-d ImageBuf buffers, get_bytes_attribute default conversion (str/bytes/None), and related colorconfig/imagebuf paths. Update MIGRATION_STATUS to reflect full dual-backend coverage. Assisted-by: Cursor / Grok Signed-off-by: Aleksandr Motsjonov --- src/cmake/testing.cmake | 18 ++ src/python-nanobind/CMakeLists.txt | 12 +- src/python/CMakeLists.txt | 10 +- src/python/MIGRATION_STATUS.md | 73 ++--- src/python/py_backend.h | 20 +- src/python/py_colorconfig.cpp | 13 +- src/python/py_deepdata.cpp | 37 ++- src/python/py_imagebuf.cpp | 184 +++++++------ src/python/py_imagebufalgo.cpp | 89 ++++--- src/python/py_imagecache.cpp | 12 +- src/python/py_imageinput.cpp | 6 +- src/python/py_imageoutput.cpp | 49 ++-- src/python/py_imagespec.cpp | 14 +- src/python/py_oiio.cpp | 249 ++++++++++++++---- src/python/py_oiio.h | 189 +++++-------- src/python/py_texturesys.cpp | 56 ++-- .../python-colorconfig/ref/out-ocio230.txt | 2 + .../python-colorconfig/ref/out-ocio230b.txt | 2 + .../python-colorconfig/ref/out-ocio232.txt | 2 + .../python-colorconfig/ref/out-ocio24.txt | 2 + .../python-colorconfig/ref/out-ocio25.txt | 2 + testsuite/python-colorconfig/ref/out.txt | 2 + .../src/test_colorconfig.py | 9 + testsuite/python-imagebuf/ref/out-alt.txt | 5 + testsuite/python-imagebuf/ref/out.txt | 5 + .../python-imagebuf/src/test_imagebuf.py | 15 ++ testsuite/python-oiio/ref/out.txt | 11 + testsuite/python-oiio/src/test_oiio.py | 20 ++ 28 files changed, 679 insertions(+), 429 deletions(-) diff --git a/src/cmake/testing.cmake b/src/cmake/testing.cmake index 554d9e8e89..3eeca449bc 100644 --- a/src/cmake/testing.cmake +++ b/src/cmake/testing.cmake @@ -259,11 +259,23 @@ macro (oiio_add_all_tests) endif () oiio_tests_pythonpath_env_entry (_nanobind_tests_pythonpath "${CMAKE_BINARY_DIR}/lib/python/nanobind") + # Keep in sync with the pybind11 python-* tests below as dual-backend + # coverage expands. imageinput/imagebufalgo also need oiio-images. set (nanobind_python_tests + python-colorconfig + python-deep + python-imagebuf + python-imagecache + python-imageoutput python-imagespec + python-oiio python-paramlist python-roi + python-texturesys python-typedesc) + set (nanobind_python_tests_imagedir + python-imageinput + python-imagebufalgo) set (nanobind_python_test_suffix ".nanobind") if (OIIO_BUILD_PYTHON_PYBIND11) oiio_add_tests ( @@ -298,6 +310,12 @@ macro (oiio_add_all_tests) SUFFIX ${nanobind_python_test_suffix} ENVIRONMENT "${_nanobind_tests_pythonpath}" ) + oiio_add_tests ( + ${nanobind_python_tests_imagedir} + SUFFIX ${nanobind_python_test_suffix} + IMAGEDIR oiio-images + ENVIRONMENT "${_nanobind_tests_pythonpath}" + ) endif () endif () diff --git a/src/python-nanobind/CMakeLists.txt b/src/python-nanobind/CMakeLists.txt index 4fd40d037d..66c9b00c03 100644 --- a/src/python-nanobind/CMakeLists.txt +++ b/src/python-nanobind/CMakeLists.txt @@ -4,10 +4,18 @@ set (nanobind_srcs ${CMAKE_CURRENT_SOURCE_DIR}/../python/py_oiio.cpp - ${CMAKE_CURRENT_SOURCE_DIR}/../python/py_paramvalue.cpp ${CMAKE_CURRENT_SOURCE_DIR}/../python/py_roi.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/../python/py_typedesc.cpp ${CMAKE_CURRENT_SOURCE_DIR}/../python/py_imagespec.cpp - ${CMAKE_CURRENT_SOURCE_DIR}/../python/py_typedesc.cpp) + ${CMAKE_CURRENT_SOURCE_DIR}/../python/py_paramvalue.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/../python/py_deepdata.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/../python/py_colorconfig.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/../python/py_imageinput.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/../python/py_imageoutput.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/../python/py_imagebuf.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/../python/py_imagecache.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/../python/py_texturesys.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/../python/py_imagebufalgo.cpp) set (nanobind_build_package_dir ${CMAKE_BINARY_DIR}/lib/python/nanobind/OpenImageIO) file (MAKE_DIRECTORY ${nanobind_build_package_dir}) diff --git a/src/python/CMakeLists.txt b/src/python/CMakeLists.txt index 6a3b7bb537..aab0cdd856 100644 --- a/src/python/CMakeLists.txt +++ b/src/python/CMakeLists.txt @@ -8,7 +8,15 @@ set (python_dual_backend_srcs py_roi.cpp py_typedesc.cpp py_imagespec.cpp - py_paramvalue.cpp) + py_paramvalue.cpp + py_deepdata.cpp + py_colorconfig.cpp + py_imageinput.cpp + py_imageoutput.cpp + py_imagebuf.cpp + py_imagecache.cpp + py_texturesys.cpp + py_imagebufalgo.cpp) # Full pybind11 module (pybind11 or both backends). if (OIIO_BUILD_PYTHON_PYBIND11) diff --git a/src/python/MIGRATION_STATUS.md b/src/python/MIGRATION_STATUS.md index d3ac0d2671..f2e8be46cb 100644 --- a/src/python/MIGRATION_STATUS.md +++ b/src/python/MIGRATION_STATUS.md @@ -5,47 +5,50 @@ Nanobind shares binding sources with pybind11 under `src/python/` (see with `-DOIIO_PYTHON_BINDINGS_BACKEND=nanobind` for nanobind-only (`PyOpenImageIO` / module `OpenImageIO` in site-packages), or `both` to also build `PyOpenImageIONanobind` (`_OpenImageIO` under `lib/python/nanobind/OpenImageIO`). -Nanobind-only code paths live in `py_oiio.h` / `py_oiio.cpp` behind +Nanobind-only code paths live in binding `.cpp` files and `py_oiio.cpp` behind `OIIO_PY_BACKEND_NANOBIND`. Shared Python↔C++ conversion helpers live in -`py_oiio.h` for both backends. +`py_oiio.h` / `py_backend.h` for both backends. -## Migrated — full parity with pybind (no known gaps for this surface) +## Migrated — full dual-backend sources -| Source file | Python / C++ API | -| --- | --- | -| `py_roi.cpp` | ROI
Free functions:
| -| `py_typedesc.cpp` | TypeDesc
Enums: BASETYPE, AGGREGATE, VECSEMANTICS
Module Type* constants | -| `py_imagespec.cpp` | ImageSpec (bound methods/properties, typed attribute / buffer paths via shared helpers). | -| `py_paramvalue.cpp` | ParamValue, ParamValueList
Enum: Interp | - -## Migrated — partial (gaps or intentional deltas vs pybind) - -| Source file | Migrated (vs pybind) | Missing or divergent (vs pybind) | -| --- | --- | --- | -| `py_oiio.cpp` (`OpenImageIO` module) | | | -| `__init__.py` (package) | Shared with pybind11: env / DLL path setup, from .OpenImageIO import *. | TODO: Python CLI entry-point trampolines when the install layout matches the full wheel. | - ---- - -## Not migrated — entire pybind modules - -These exist only in the pybind11 module today (not in `python_dual_backend_srcs`). +All modules below compile for both pybind11 and nanobind from `src/python/`: | Source file | Python / C++ API | | --- | --- | -| `py_imageinput.cpp` | | -| `py_imageoutput.cpp` | ImageOutput | -| `py_imagebuf.cpp` | ImageBuf | -| `py_imagebufalgo.cpp` | | -| `py_texturesys.cpp` | | -| `py_imagecache.cpp` | ImageCache (wrapped) | -| `py_colorconfig.cpp` | ColorConfig | -| `py_deepdata.cpp` | DeepData | - ---- +| `py_roi.cpp` | `ROI`, free functions (`union`, `intersection`, `get_roi`, …) | +| `py_typedesc.cpp` | `TypeDesc`, enums, module `Type*` constants | +| `py_imagespec.cpp` | `ImageSpec` | +| `py_paramvalue.cpp` | `ParamValue`, `ParamValueList`, `Interp` | +| `py_deepdata.cpp` | `DeepData` | +| `py_colorconfig.cpp` | `ColorConfig`, module color constants | +| `py_imageinput.cpp` | `ImageInput` | +| `py_imageoutput.cpp` | `ImageOutput` | +| `py_imagebuf.cpp` | `ImageBuf` | +| `py_imagecache.cpp` | `ImageCache` (wrapped) | +| `py_texturesys.cpp` | `Wrap`, `MipMode`, `InterpMode`, `TextureOpt`, `TextureSystem` | +| `py_imagebufalgo.cpp` | `ImageBufAlgo`, `PixelStats`, `CompareResults`, `IBA_*` | +| `py_oiio.cpp` | Module-level attributes and global helpers | + +## Consumer-visible differences (pybind11 vs nanobind) + +**None intended.** Callers of the pybind11 module should see the same Python API +and behavior with the nanobind build. Binding-side `#if` / caster / constructor +differences exist only to keep that parity. + +If you find a behavioral difference, treat it as a bug and add a regression test. + +## Packaging / install layout + +| Item | Notes | +| --- | --- | +| `__init__.py` | Shared env setup; CLI entry-point trampolines still TODO for full wheel layout. | +| `both` layout | With `OIIO_PYTHON_BINDINGS_BACKEND=both`, nanobind installs under `lib/python/nanobind/` via `src/python-nanobind/` (`_OpenImageIO` + package `__init__.py`). Default pybind11 install path is unchanged. | -## Conventions +## Conventions (maintainers) -When adding coverage, prefer mirroring the existing `declare_*` split in `src/python/` unless a file becomes too large. +- Binding macros: `.OIIO_PY_RW`, `.OIIO_PY_PROP_RO`, `.OIIO_PY_PROP_RW`, `.OIIO_PY_PROP_RW_NONE`, `.OIIO_PY_RO`, `.OIIO_PY_RO_STATIC` (see `py_backend.h`). +- Declare functions use `py_module&`, not `py::module&`. +- Buffer I/O: `oiio_py_request_buffer()` / `oiio_bufinfo_from_object()` (both backends). +- `#if defined(OIIO_PY_BACKEND_NANOBIND)` only where backends genuinely differ. -Extend **testsuite** coverage for any migrated code that is not already covered, so **parity with pybind11** is demonstrated rather than only claimed. Follow the existing `testsuite/python-*` scripts and `ref/out.txt` pattern where applicable. +Extend **testsuite** coverage when adding behavior; run both pybind and `*.nanobind` ctest variants when `OIIO_PYTHON_BINDINGS_BACKEND=both`. diff --git a/src/python/py_backend.h b/src/python/py_backend.h index 81072aee08..68d85b468f 100644 --- a/src/python/py_backend.h +++ b/src/python/py_backend.h @@ -10,7 +10,11 @@ # include # include # include +# include +# include # include +# include +# include # include namespace py = nanobind; @@ -18,9 +22,14 @@ using py_module = nanobind::module_; using namespace py::literals; # define OIIO_PY_RW def_rw +# define OIIO_PY_RO def_ro # define OIIO_PY_PROP_RO def_prop_ro # define OIIO_PY_PROP_RW def_prop_rw +// Allow assigning None to a property (nanobind rejects it unless annotated). +# define OIIO_PY_PROP_RW_NONE(name, getter, setter) \ + def_prop_rw(name, getter, setter, py::for_setter(py::arg().none())) # define OIIO_PY_RO_STATIC def_prop_ro_static +# define OIIO_PY_RO def_ro namespace oiio_py { @@ -41,13 +50,16 @@ make_tuple(size_t size, F&& fill) return py::steal(PyList_AsTuple(list.ptr())); } +inline constexpr auto ref = py::rv_policy::reference; inline constexpr auto ref_internal = py::rv_policy::reference_internal; +// Copy into std::string so temporary string_view contents stay valid; +// nanobind's string caster turns this into a Python str. template -inline auto +inline std::string str(T&& x) { - return std::forward(x); + return std::string(std::forward(x)); } inline void @@ -102,9 +114,12 @@ using py_module = pybind11::module; using namespace py::literals; # define OIIO_PY_RW def_readwrite +# define OIIO_PY_RO def_readonly # define OIIO_PY_PROP_RO def_property_readonly # define OIIO_PY_PROP_RW def_property +# define OIIO_PY_PROP_RW_NONE def_property # define OIIO_PY_RO_STATIC def_property_readonly_static +# define OIIO_PY_RO def_readonly namespace oiio_py { @@ -125,6 +140,7 @@ make_tuple(size_t size, F&& fill) return result; } +inline constexpr auto ref = py::return_value_policy::reference; inline constexpr auto ref_internal = py::return_value_policy::reference_internal; template diff --git a/src/python/py_colorconfig.cpp b/src/python/py_colorconfig.cpp index 075c1f3e84..a3d99528de 100644 --- a/src/python/py_colorconfig.cpp +++ b/src/python/py_colorconfig.cpp @@ -12,10 +12,8 @@ namespace PyOpenImageIO { // Declare the OIIO ColorConfig class to Python void -declare_colorconfig(py::module& m) +declare_colorconfig(py_module& m) { - using namespace pybind11::literals; - py::class_(m, "ColorConfig") .def(py::init<>()) @@ -192,9 +190,12 @@ declare_colorconfig(py::module& m) return std::nullopt; }) .def("configname", &ColorConfig::configname) - .def_static("default_colorconfig", []() -> const ColorConfig& { - return ColorConfig::default_colorconfig(); - }); + .def_static( + "default_colorconfig", + []() -> const ColorConfig& { + return ColorConfig::default_colorconfig(); + }, + oiio_py::ref); m.attr("supportsOpenColorIO") = ColorConfig::supportsOpenColorIO(); m.attr("OpenColorIO_version_hex") = ColorConfig::OpenColorIO_version_hex(); diff --git a/src/python/py_deepdata.cpp b/src/python/py_deepdata.cpp index 43e5aed6d2..4d57bfc8cb 100644 --- a/src/python/py_deepdata.cpp +++ b/src/python/py_deepdata.cpp @@ -70,29 +70,24 @@ DeepData_merge_deep_pixels(DeepData& dd, int64_t pixel, const DeepData& src, // Declare the OIIO DeepData class to Python void -declare_deepdata(py::module& m) +declare_deepdata(py_module& m) { - using namespace pybind11::literals; - py::class_(m, "DeepData") - .def_property_readonly("pixels", - [](const DeepData& d) { return d.pixels(); }) - .def_property_readonly("channels", - [](const DeepData& d) { return d.channels(); }) - .def_property_readonly("A_channel", - [](const DeepData& d) { return d.A_channel(); }) - .def_property_readonly("AR_channel", - [](const DeepData& d) { return d.AR_channel(); }) - .def_property_readonly("AG_channel", - [](const DeepData& d) { return d.AG_channel(); }) - .def_property_readonly("AB_channel", - [](const DeepData& d) { return d.AB_channel(); }) - .def_property_readonly("Z_channel", - [](const DeepData& d) { return d.Z_channel(); }) - .def_property_readonly("Zback_channel", - [](const DeepData& d) { - return d.Zback_channel(); - }) + .OIIO_PY_PROP_RO("pixels", [](const DeepData& d) { return d.pixels(); }) + .OIIO_PY_PROP_RO("channels", + [](const DeepData& d) { return d.channels(); }) + .OIIO_PY_PROP_RO("A_channel", + [](const DeepData& d) { return d.A_channel(); }) + .OIIO_PY_PROP_RO("AR_channel", + [](const DeepData& d) { return d.AR_channel(); }) + .OIIO_PY_PROP_RO("AG_channel", + [](const DeepData& d) { return d.AG_channel(); }) + .OIIO_PY_PROP_RO("AB_channel", + [](const DeepData& d) { return d.AB_channel(); }) + .OIIO_PY_PROP_RO("Z_channel", + [](const DeepData& d) { return d.Z_channel(); }) + .OIIO_PY_PROP_RO("Zback_channel", + [](const DeepData& d) { return d.Zback_channel(); }) .def(py::init<>()) .def("init", &DeepData_init, "npixels"_a, "nchannels"_a, diff --git a/src/python/py_imagebuf.cpp b/src/python/py_imagebuf.cpp index 540b077185..7329c8cfad 100644 --- a/src/python/py_imagebuf.cpp +++ b/src/python/py_imagebuf.cpp @@ -15,20 +15,18 @@ namespace PyOpenImageIO { static ImageBuf -ImageBuf_from_buffer(const py::buffer& buffer) +ImageBuf_from_buffer(const py::object& buffer) { ImageBuf ib; - const py::buffer_info info = buffer.request(); + const oiio_py_buffer_view info = oiio_py_request_buffer(buffer); TypeDesc format; - if (info.format.size()) + if (info.format.size()) { format = typedesc_from_python_array_code(info.format); - if (format == TypeUnknown) + } + if (format == TypeUnknown) { return ib; - // Strutil::print("IB from {} buffer: dims = {}\n", format, info.ndim); - // for (int i = 0; i < info.ndim; ++i) - // Strutil::print("IB from buffer: dim[{}]: size = {}, stride = {}\n", i, - // info.shape[i], info.strides[i]); - if (size_t(info.strides[info.ndim - 1]) != format.size()) { + } + if (info.ndim > 0 && size_t(info.strides[info.ndim - 1]) != format.size()) { ib.errorfmt( "ImageBuf-from-numpy-array must have contiguous stride within pixels"); return ib; @@ -38,23 +36,23 @@ ImageBuf_from_buffer(const py::buffer& buffer) stride_t xstride = AutoStride, ystride = AutoStride, zstride = AutoStride; if (info.ndim == 3) { // Assume [y][x][c] - width = info.shape[1]; - height = info.shape[0]; - nchans = info.shape[2]; + width = int(info.shape[1]); + height = int(info.shape[0]); + nchans = int(info.shape[2]); xstride = info.strides[1]; ystride = info.strides[0]; } else if (info.ndim == 2) { // Assume [y][x], single channel - width = info.shape[1]; - height = info.shape[0]; + width = int(info.shape[1]); + height = int(info.shape[0]); xstride = info.strides[1]; ystride = info.strides[0]; } else if (info.ndim == 4) { // Assume volume [z][y][x][c] - width = info.shape[2]; - height = info.shape[1]; - depth = info.shape[0]; - nchans = info.shape[3]; + width = int(info.shape[2]); + height = int(info.shape[1]); + depth = int(info.shape[0]); + nchans = int(info.shape[3]); xstride = info.strides[2]; ystride = info.strides[1]; zstride = info.strides[0]; @@ -208,7 +206,7 @@ ImageBuf_set_deep_value_uint(ImageBuf& buf, int x, int y, int z, int c, int s, bool -ImageBuf_set_pixels_buffer(ImageBuf& self, ROI roi, py::buffer& buffer) +ImageBuf_set_pixels_buffer(ImageBuf& self, ROI roi, const py::object& buffer) { if (!roi.defined()) roi = self.roi(); @@ -217,8 +215,10 @@ ImageBuf_set_pixels_buffer(ImageBuf& self, ROI roi, py::buffer& buffer) if (size == 0) { return true; // done } - oiio_bufinfo buf(buffer.request(), roi.nchannels(), roi.width(), - roi.height(), roi.depth(), self.spec().depth > 1 ? 3 : 2); + oiio_bufinfo buf = oiio_bufinfo_from_object(buffer, roi.nchannels(), + roi.width(), roi.height(), + roi.depth(), + self.spec().depth > 1 ? 3 : 2); if (!buf.data || buf.error.size()) { self.errorfmt("set_pixels error: {}", buf.error.size() ? buf.error.c_str() : "unspecified"); @@ -285,15 +285,35 @@ ImageBuf_repr_png(const ImageBuf& self) void -declare_imagebuf(py::module& m) +declare_imagebuf(py_module& m) { - using namespace pybind11::literals; - py::class_(m, "ImageBuf") .def(py::init<>()) .def(py::init()) .def(py::init()) .def(py::init()) +#if defined(OIIO_PY_BACKEND_NANOBIND) + .def( + "__init__", + [](ImageBuf* self, const ImageSpec& spec, bool zero) { + new (self) ImageBuf(spec, zero ? InitializePixels::Yes + : InitializePixels::No); + }, + "spec"_a, "zero"_a) + .def( + "__init__", + [](ImageBuf* self, const std::string& name, int subimage, + int miplevel, const ImageSpec& config) { + new (self) ImageBuf(name, subimage, miplevel, nullptr, &config); + }, + "name"_a, "subimage"_a, "miplevel"_a, "config"_a) + .def( + "__init__", + [](ImageBuf* self, const py::object& buffer) { + new (self) ImageBuf(ImageBuf_from_buffer(buffer)); + }, + "buffer"_a) +#else .def(py::init([](const ImageSpec& spec, bool zero) { auto z = zero ? InitializePixels::Yes : InitializePixels::No; return ImageBuf(spec, z); @@ -303,10 +323,11 @@ declare_imagebuf(py::module& m) return ImageBuf(name, subimage, miplevel, nullptr, &config); }), "name"_a, "subimage"_a, "miplevel"_a, "config"_a) - .def(py::init([](const py::buffer& buffer) { + .def(py::init([](const py::object& buffer) { return ImageBuf_from_buffer(buffer); }), "buffer"_a) +#endif .def("clear", &ImageBuf::clear) .def( "reset", @@ -330,15 +351,13 @@ declare_imagebuf(py::module& m) "spec"_a, "zero"_a = true) .def( "reset", - [](ImageBuf& self, const py::buffer& buffer) { + [](ImageBuf& self, const py::object& buffer) { self = ImageBuf_from_buffer(buffer); }, "buffer"_a) - .def_property_readonly("initialized", - [](const ImageBuf& self) { - return self.initialized(); - }) + .OIIO_PY_PROP_RO("initialized", + [](const ImageBuf& self) { return self.initialized(); }) .def( "init_spec", [](ImageBuf& self, std::string filename, int subimage, @@ -393,64 +412,61 @@ declare_imagebuf(py::module& m) // FIXME -- write(ImageOut&) .def("set_write_tiles", &ImageBuf::set_write_tiles, "width"_a = 0, "height"_a = 0, "depth"_a = 0) - .def("spec", &ImageBuf::spec, - py::return_value_policy::reference_internal) - .def("nativespec", &ImageBuf::nativespec, - py::return_value_policy::reference_internal) - .def("specmod", &ImageBuf::specmod, - py::return_value_policy::reference_internal) - .def_property_readonly("has_thumbnail", - [](const ImageBuf& self) { - return self.has_thumbnail(); - }) + .def("spec", &ImageBuf::spec, oiio_py::ref_internal) + .def("nativespec", &ImageBuf::nativespec, oiio_py::ref_internal) + .def("specmod", &ImageBuf::specmod, oiio_py::ref_internal) + .OIIO_PY_PROP_RO("has_thumbnail", + [](const ImageBuf& self) { + return self.has_thumbnail(); + }) .def("clear_thumbnail", &ImageBuf::clear_thumbnail) .def("set_thumbnail", &ImageBuf::set_thumbnail, "thumb"_a) .def("get_thumbnail", [](const ImageBuf& self) { return *self.get_thumbnail(); }) - .def_property_readonly("name", - [](const ImageBuf& self) { - return PY_STR(self.name()); - }) - .def_property_readonly("file_format_name", - [](const ImageBuf& self) { - return PY_STR(self.file_format_name()); - }) - .def_property_readonly("subimage", &ImageBuf::subimage) - .def_property_readonly("nsubimages", &ImageBuf::nsubimages) - .def_property_readonly("miplevel", &ImageBuf::miplevel) - .def_property_readonly("nmiplevels", &ImageBuf::nmiplevels) - .def_property_readonly("nchannels", &ImageBuf::nchannels) - .def_property("orientation", &ImageBuf::orientation, - &ImageBuf::set_orientation) - .def_property_readonly("oriented_width", &ImageBuf::oriented_width) - .def_property_readonly("oriented_height", &ImageBuf::oriented_height) - .def_property_readonly("oriented_x", &ImageBuf::oriented_x) - .def_property_readonly("oriented_y", &ImageBuf::oriented_y) - .def_property_readonly("oriented_full_width", - &ImageBuf::oriented_full_width) - .def_property_readonly("oriented_full_height", - &ImageBuf::oriented_full_height) - .def_property_readonly("oriented_full_x", &ImageBuf::oriented_full_x) - .def_property_readonly("oriented_full_y", &ImageBuf::oriented_full_y) - .def_property_readonly("xbegin", &ImageBuf::xbegin) - .def_property_readonly("xend", &ImageBuf::xend) - .def_property_readonly("ybegin", &ImageBuf::ybegin) - .def_property_readonly("yend", &ImageBuf::yend) - .def_property_readonly("zbegin", &ImageBuf::zbegin) - .def_property_readonly("zend", &ImageBuf::zend) - .def_property_readonly("xmin", &ImageBuf::xmin) - .def_property_readonly("xmax", &ImageBuf::xmax) - .def_property_readonly("ymin", &ImageBuf::ymin) - .def_property_readonly("ymax", &ImageBuf::ymax) - .def_property_readonly("zmin", &ImageBuf::zmin) - .def_property_readonly("zmax", &ImageBuf::zmax) - .def_property_readonly("roi", &ImageBuf::roi) - .def_property("roi_full", &ImageBuf::roi_full, &ImageBuf::set_roi_full) + .OIIO_PY_PROP_RO("name", + [](const ImageBuf& self) { + return PY_STR(self.name()); + }) + .OIIO_PY_PROP_RO("file_format_name", + [](const ImageBuf& self) { + return PY_STR(self.file_format_name()); + }) + .OIIO_PY_PROP_RO("subimage", &ImageBuf::subimage) + .OIIO_PY_PROP_RO("nsubimages", &ImageBuf::nsubimages) + .OIIO_PY_PROP_RO("miplevel", &ImageBuf::miplevel) + .OIIO_PY_PROP_RO("nmiplevels", &ImageBuf::nmiplevels) + .OIIO_PY_PROP_RO("nchannels", &ImageBuf::nchannels) + .OIIO_PY_PROP_RW("orientation", &ImageBuf::orientation, + &ImageBuf::set_orientation) + .OIIO_PY_PROP_RO("oriented_width", &ImageBuf::oriented_width) + .OIIO_PY_PROP_RO("oriented_height", &ImageBuf::oriented_height) + .OIIO_PY_PROP_RO("oriented_x", &ImageBuf::oriented_x) + .OIIO_PY_PROP_RO("oriented_y", &ImageBuf::oriented_y) + .OIIO_PY_PROP_RO("oriented_full_width", &ImageBuf::oriented_full_width) + .OIIO_PY_PROP_RO("oriented_full_height", + &ImageBuf::oriented_full_height) + .OIIO_PY_PROP_RO("oriented_full_x", &ImageBuf::oriented_full_x) + .OIIO_PY_PROP_RO("oriented_full_y", &ImageBuf::oriented_full_y) + .OIIO_PY_PROP_RO("xbegin", &ImageBuf::xbegin) + .OIIO_PY_PROP_RO("xend", &ImageBuf::xend) + .OIIO_PY_PROP_RO("ybegin", &ImageBuf::ybegin) + .OIIO_PY_PROP_RO("yend", &ImageBuf::yend) + .OIIO_PY_PROP_RO("zbegin", &ImageBuf::zbegin) + .OIIO_PY_PROP_RO("zend", &ImageBuf::zend) + .OIIO_PY_PROP_RO("xmin", &ImageBuf::xmin) + .OIIO_PY_PROP_RO("xmax", &ImageBuf::xmax) + .OIIO_PY_PROP_RO("ymin", &ImageBuf::ymin) + .OIIO_PY_PROP_RO("ymax", &ImageBuf::ymax) + .OIIO_PY_PROP_RO("zmin", &ImageBuf::zmin) + .OIIO_PY_PROP_RO("zmax", &ImageBuf::zmax) + .OIIO_PY_PROP_RO("roi", &ImageBuf::roi) + .OIIO_PY_PROP_RW("roi_full", &ImageBuf::roi_full, + &ImageBuf::set_roi_full) .def("set_origin", &ImageBuf::set_origin, "x"_a, "y"_a, "z"_a = 0) .def("set_full", &ImageBuf::set_full) - .def_property_readonly("pixels_valid", &ImageBuf::pixels_valid) - .def_property_readonly("pixeltype", &ImageBuf::pixeltype) - .def_property_readonly("has_error", &ImageBuf::has_error) + .OIIO_PY_PROP_RO("pixels_valid", &ImageBuf::pixels_valid) + .OIIO_PY_PROP_RO("pixeltype", &ImageBuf::pixeltype) + .OIIO_PY_PROP_RO("has_error", &ImageBuf::has_error) .def( "geterror", [](const ImageBuf& self, bool clear) { @@ -506,7 +522,7 @@ declare_imagebuf(py::module& m) "roi"_a = ROI::All()) .def("set_pixels", &ImageBuf_set_pixels_buffer, "roi"_a, "pixels"_a) - .def_property_readonly("deep", &ImageBuf::deep) + .OIIO_PY_PROP_RO("deep", &ImageBuf::deep) .def("deep_samples", &ImageBuf::deep_samples, "x"_a, "y"_a, "z"_a = 0) .def("set_deep_samples", &ImageBuf::set_deep_samples, "x"_a, "y"_a, "z"_a = 0, "nsamples"_a = 1) @@ -524,7 +540,7 @@ declare_imagebuf(py::module& m) "z"_a, "channel"_a, "sample"_a, "value"_a = 0) .def( "deepdata", [](ImageBuf& self) { return *self.deepdata(); }, - py::return_value_policy::reference_internal) + oiio_py::ref_internal) .def("_repr_png_", &ImageBuf_repr_png) // FIXME -- do we want to provide pixel iterators? diff --git a/src/python/py_imagebufalgo.cpp b/src/python/py_imagebufalgo.cpp index 894fa2deee..999397fc25 100644 --- a/src/python/py_imagebufalgo.cpp +++ b/src/python/py_imagebufalgo.cpp @@ -185,7 +185,7 @@ IBA_channels(ImageBuf& dst, const ImageBuf& src, py::tuple channelorder_, py::tuple newchannelnames_, bool shuffle_channel_names, int nthreads) { - size_t nchannels = (size_t)len(channelorder_); + size_t nchannels = (size_t)py::len(channelorder_); if (nchannels < 1) { dst.errorfmt("No channels selected"); return false; @@ -195,11 +195,11 @@ IBA_channels(ImageBuf& dst, const ImageBuf& src, py::tuple channelorder_, for (size_t i = 0; i < nchannels; ++i) { auto orderi = channelorder_[i]; if (py::isinstance(orderi)) { - channelorder[i] = orderi.cast(); + channelorder[i] = py::cast(orderi); } else if (py::isinstance(orderi)) { - channelvalues[i] = orderi.cast(); + channelvalues[i] = py::cast(orderi); } else if (py::isinstance(orderi)) { - std::string chname = orderi.cast(); + std::string chname = oiio_py::str_to_stdstring(orderi); for (int c = 0; c < src.nchannels(); ++c) { if (src.spec().channelnames[c] == chname) channelorder[i] = c; @@ -2497,51 +2497,66 @@ IBA_demosaic(ImageBuf& dst, const ImageBuf& src, } void -declare_imagebufalgo(py::module& m) +declare_imagebufalgo(py_module& m) { - using namespace pybind11::literals; - using py::arg; - - py::enum_(m, "NonFiniteFixMode") - .value("NONFINITE_NONE", ImageBufAlgo::NONFINITE_NONE) + py::enum_ nonfinite_fix_mode( + m, "NonFiniteFixMode"); + nonfinite_fix_mode.value("NONFINITE_NONE", ImageBufAlgo::NONFINITE_NONE) .value("NONFINITE_BLACK", ImageBufAlgo::NONFINITE_BLACK) - .value("NONFINITE_BOX3", ImageBufAlgo::NONFINITE_BOX3) - .export_values(); - - py::enum_(m, "MakeTextureMode") - .value("MakeTxTexture", ImageBufAlgo::MakeTxTexture) + .value("NONFINITE_BOX3", ImageBufAlgo::NONFINITE_BOX3); +#if !defined(OIIO_PY_BACKEND_NANOBIND) + nonfinite_fix_mode.export_values(); +#else + m.attr("NONFINITE_NONE") = py::cast(ImageBufAlgo::NONFINITE_NONE); + m.attr("NONFINITE_BLACK") = py::cast(ImageBufAlgo::NONFINITE_BLACK); + m.attr("NONFINITE_BOX3") = py::cast(ImageBufAlgo::NONFINITE_BOX3); +#endif + + py::enum_ make_texture_mode( + m, "MakeTextureMode"); + make_texture_mode.value("MakeTxTexture", ImageBufAlgo::MakeTxTexture) .value("MakeTxShadow", ImageBufAlgo::MakeTxShadow) .value("MakeTxEnvLatl", ImageBufAlgo::MakeTxEnvLatl) .value("MakeTxEnvLatlFromLightProbe", ImageBufAlgo::MakeTxEnvLatlFromLightProbe) - .value("MakeTxBumpWithSlopes", ImageBufAlgo::MakeTxBumpWithSlopes) - .export_values(); + .value("MakeTxBumpWithSlopes", ImageBufAlgo::MakeTxBumpWithSlopes); +#if !defined(OIIO_PY_BACKEND_NANOBIND) + make_texture_mode.export_values(); +#else + m.attr("MakeTxTexture") = py::cast(ImageBufAlgo::MakeTxTexture); + m.attr("MakeTxShadow") = py::cast(ImageBufAlgo::MakeTxShadow); + m.attr("MakeTxEnvLatl") = py::cast(ImageBufAlgo::MakeTxEnvLatl); + m.attr("MakeTxEnvLatlFromLightProbe") = py::cast( + ImageBufAlgo::MakeTxEnvLatlFromLightProbe); + m.attr("MakeTxBumpWithSlopes") = py::cast( + ImageBufAlgo::MakeTxBumpWithSlopes); +#endif py::class_(m, "PixelStats") .def(py::init<>()) - .def_readonly("min", &ImageBufAlgo::PixelStats::min) - .def_readonly("max", &ImageBufAlgo::PixelStats::max) - .def_readonly("avg", &ImageBufAlgo::PixelStats::avg) - .def_readonly("stddev", &ImageBufAlgo::PixelStats::stddev) - .def_readonly("nancount", &ImageBufAlgo::PixelStats::nancount) - .def_readonly("infcount", &ImageBufAlgo::PixelStats::infcount) - .def_readonly("finitecount", &ImageBufAlgo::PixelStats::finitecount) - .def_readonly("sum", &ImageBufAlgo::PixelStats::sum) - .def_readonly("sum2", &ImageBufAlgo::PixelStats::sum2); + .OIIO_PY_RO("min", &ImageBufAlgo::PixelStats::min) + .OIIO_PY_RO("max", &ImageBufAlgo::PixelStats::max) + .OIIO_PY_RO("avg", &ImageBufAlgo::PixelStats::avg) + .OIIO_PY_RO("stddev", &ImageBufAlgo::PixelStats::stddev) + .OIIO_PY_RO("nancount", &ImageBufAlgo::PixelStats::nancount) + .OIIO_PY_RO("infcount", &ImageBufAlgo::PixelStats::infcount) + .OIIO_PY_RO("finitecount", &ImageBufAlgo::PixelStats::finitecount) + .OIIO_PY_RO("sum", &ImageBufAlgo::PixelStats::sum) + .OIIO_PY_RO("sum2", &ImageBufAlgo::PixelStats::sum2); py::class_(m, "CompareResults") .def(py::init<>()) - .def_readonly("meanerror", &ImageBufAlgo::CompareResults::meanerror) - .def_readonly("rms_error", &ImageBufAlgo::CompareResults::rms_error) - .def_readonly("PSNR", &ImageBufAlgo::CompareResults::PSNR) - .def_readonly("maxerror", &ImageBufAlgo::CompareResults::maxerror) - .def_readonly("maxx", &ImageBufAlgo::CompareResults::maxx) - .def_readonly("maxy", &ImageBufAlgo::CompareResults::maxy) - .def_readonly("maxz", &ImageBufAlgo::CompareResults::maxz) - .def_readonly("maxc", &ImageBufAlgo::CompareResults::maxc) - .def_readonly("nwarn", &ImageBufAlgo::CompareResults::nwarn) - .def_readonly("nfail", &ImageBufAlgo::CompareResults::nfail) - .def_readonly("error", &ImageBufAlgo::CompareResults::error); + .OIIO_PY_RO("meanerror", &ImageBufAlgo::CompareResults::meanerror) + .OIIO_PY_RO("rms_error", &ImageBufAlgo::CompareResults::rms_error) + .OIIO_PY_RO("PSNR", &ImageBufAlgo::CompareResults::PSNR) + .OIIO_PY_RO("maxerror", &ImageBufAlgo::CompareResults::maxerror) + .OIIO_PY_RO("maxx", &ImageBufAlgo::CompareResults::maxx) + .OIIO_PY_RO("maxy", &ImageBufAlgo::CompareResults::maxy) + .OIIO_PY_RO("maxz", &ImageBufAlgo::CompareResults::maxz) + .OIIO_PY_RO("maxc", &ImageBufAlgo::CompareResults::maxc) + .OIIO_PY_RO("nwarn", &ImageBufAlgo::CompareResults::nwarn) + .OIIO_PY_RO("nfail", &ImageBufAlgo::CompareResults::nfail) + .OIIO_PY_RO("error", &ImageBufAlgo::CompareResults::error); // Put this all inside "ImageBufAlgo" py::class_(m, "ImageBufAlgo") diff --git a/src/python/py_imagecache.cpp b/src/python/py_imagecache.cpp index 706f2597e9..89cbe59032 100644 --- a/src/python/py_imagecache.cpp +++ b/src/python/py_imagecache.cpp @@ -69,10 +69,8 @@ ImageCacheWrap::get_pixels(const std::string& filename_, int subimage, void -declare_imagecache(py::module& m) +declare_imagecache(py_module& m) { - using namespace pybind11::literals; - py::class_(m, "ImageCache") .def(py::init(), "shared"_a = true) // .def_static("create", &ImageCacheWrap::create, @@ -144,10 +142,10 @@ declare_imagecache(py::module& m) // .def("release_tile", &ImageCacheWrap::release_tile) // .def("tile_pixels", &ImageCacheWrap::tile_pixels) - .def_property_readonly("has_error", - [](ImageCacheWrap& self) { - return self.m_cache->has_error(); - }) + .OIIO_PY_PROP_RO("has_error", + [](ImageCacheWrap& self) { + return self.m_cache->has_error(); + }) .def( "geterror", [](ImageCacheWrap& self, bool clear) { diff --git a/src/python/py_imageinput.cpp b/src/python/py_imageinput.cpp index 6f930f6d40..092cc61c30 100644 --- a/src/python/py_imageinput.cpp +++ b/src/python/py_imageinput.cpp @@ -173,10 +173,8 @@ ImageInput_read_native_deep_image(ImageInput& self, int subimage, int miplevel) void -declare_imageinput(py::module& m) +declare_imageinput(py_module& m) { - using namespace pybind11::literals; - py::class_(m, "ImageInput") .def_static( "create", @@ -335,7 +333,7 @@ declare_imageinput(py::module& m) return buf; }, "subimage"_a = 0) - .def_property_readonly("has_error", &ImageInput::has_error) + .OIIO_PY_PROP_RO("has_error", &ImageInput::has_error) .def( "geterror", [](ImageInput& self, bool clear) { diff --git a/src/python/py_imageoutput.cpp b/src/python/py_imageoutput.cpp index 5e0fc094d2..c6e09fa8b3 100644 --- a/src/python/py_imageoutput.cpp +++ b/src/python/py_imageoutput.cpp @@ -13,14 +13,14 @@ bool ImageOutput_open_specs(ImageOutput& self, const std::string& name, py::tuple& specs) { - const size_t length = len(specs); + const size_t length = py::len(specs); if (length == 0) return false; std::vector Cspecs(length); for (size_t i = 0; i < length; ++i) { auto s = specs[i]; if (py::isinstance(s)) - Cspecs[i] = s.cast(); + Cspecs[i] = py::cast(s); else return false; // Tuple item was not an ImageSpec } @@ -30,14 +30,16 @@ ImageOutput_open_specs(ImageOutput& self, const std::string& name, bool -ImageOutput_write_scanline(ImageOutput& self, int y, int z, py::buffer& buffer) +ImageOutput_write_scanline(ImageOutput& self, int y, int z, + const py::object& buffer) { const ImageSpec& spec(self.spec()); if (spec.tile_width != 0) { self.errorfmt("Cannot write scanlines to a tiled file."); return false; } - oiio_bufinfo buf(buffer.request(), spec.nchannels, spec.width, 1, 1, 1); + oiio_bufinfo buf = oiio_bufinfo_from_object(buffer, spec.nchannels, + spec.width, 1, 1, 1); if (!buf.data || buf.error.size()) { self.errorfmt("Pixel data array error: {}", buf.error.size() ? buf.error.c_str() : "unspecified"); @@ -56,15 +58,16 @@ ImageOutput_write_scanline(ImageOutput& self, int y, int z, py::buffer& buffer) bool ImageOutput_write_scanlines(ImageOutput& self, int ybegin, int yend, int z, - py::buffer& buffer) + const py::object& buffer) { const ImageSpec& spec(self.spec()); if (spec.tile_width != 0) { self.errorfmt("Cannot write scanlines to a filed file."); return false; } - oiio_bufinfo buf(buffer.request(), spec.nchannels, spec.width, - yend - ybegin, 1, 2); + oiio_bufinfo buf = oiio_bufinfo_from_object(buffer, spec.nchannels, + spec.width, yend - ybegin, 1, + 2); if (!buf.data || buf.error.size()) { self.errorfmt("Pixel data array error: {}", buf.error.size() ? buf.error.c_str() : "unspecified"); @@ -84,16 +87,17 @@ ImageOutput_write_scanlines(ImageOutput& self, int ybegin, int yend, int z, bool ImageOutput_write_tile(ImageOutput& self, int x, int y, int z, - py::buffer& buffer) + const py::object& buffer) { const ImageSpec& spec(self.spec()); if (spec.tile_width == 0) { self.errorfmt("Cannot write tiles to a scanline file."); return false; } - oiio_bufinfo buf(buffer.request(), spec.nchannels, spec.tile_width, - spec.tile_height, spec.tile_depth, - spec.tile_depth > 1 ? 3 : 2); + oiio_bufinfo buf + = oiio_bufinfo_from_object(buffer, spec.nchannels, spec.tile_width, + spec.tile_height, spec.tile_depth, + spec.tile_depth > 1 ? 3 : 2); if (!buf.data || buf.error.size()) { self.errorfmt("Pixel data array error: {}", buf.error.size() ? buf.error.c_str() : "unspecified"); @@ -112,15 +116,18 @@ ImageOutput_write_tile(ImageOutput& self, int x, int y, int z, bool ImageOutput_write_tiles(ImageOutput& self, int xbegin, int xend, int ybegin, - int yend, int zbegin, int zend, py::buffer& buffer) + int yend, int zbegin, int zend, + const py::object& buffer) { const ImageSpec& spec(self.spec()); if (spec.tile_width == 0) { self.errorfmt("Cannot write tiles to a scanline file."); return false; } - oiio_bufinfo buf(buffer.request(), spec.nchannels, xend - xbegin, - yend - ybegin, zend - zbegin, spec.tile_depth > 1 ? 3 : 2); + oiio_bufinfo buf = oiio_bufinfo_from_object(buffer, spec.nchannels, + xend - xbegin, yend - ybegin, + zend - zbegin, + spec.tile_depth > 1 ? 3 : 2); if (!buf.data || buf.error.size()) { self.errorfmt("Pixel data array error: {}", buf.error.size() ? buf.error.c_str() : "unspecified"); @@ -141,11 +148,13 @@ ImageOutput_write_tiles(ImageOutput& self, int xbegin, int xend, int ybegin, bool -ImageOutput_write_image(ImageOutput& self, py::buffer& buffer) +ImageOutput_write_image(ImageOutput& self, const py::object& buffer) { const ImageSpec& spec(self.spec()); - oiio_bufinfo buf(buffer.request(), spec.nchannels, spec.width, spec.height, - spec.depth, spec.depth > 1 ? 3 : 2); + oiio_bufinfo buf = oiio_bufinfo_from_object(buffer, spec.nchannels, + spec.width, spec.height, + spec.depth, + spec.depth > 1 ? 3 : 2); if (!buf.data || buf.size < spec.image_pixels() * spec.nchannels || buf.error.size()) { self.errorfmt("Pixel data array error: {}", @@ -189,10 +198,8 @@ ImageOutput_write_deep_image(ImageOutput& self, const DeepData& deepdata) void -declare_imageoutput(py::module& m) +declare_imageoutput(py_module& m) { - using namespace pybind11::literals; - py::class_(m, "ImageOutput") .def_static( "create", @@ -250,7 +257,7 @@ declare_imageoutput(py::module& m) }) .def("copy_image", [](ImageOutput& self, ImageInput& in) { return self.copy_image(&in); }) - .def_property_readonly("has_error", &ImageOutput::has_error) + .OIIO_PY_PROP_RO("has_error", &ImageOutput::has_error) .def( "geterror", [](ImageOutput& self, bool clear) { diff --git a/src/python/py_imagespec.cpp b/src/python/py_imagespec.cpp index ea64538f4d..3e1643c135 100644 --- a/src/python/py_imagespec.cpp +++ b/src/python/py_imagespec.cpp @@ -159,7 +159,7 @@ declare_imagespec(py_module& m) int chan) { return spec.channelformat(chan); }) .def("channel_name", [](const ImageSpec& spec, int chan) { - return oiio_py::str(std::string(spec.channel_name(chan))); + return oiio_py::str(spec.channel_name(chan)); }) .def("channelindex", [](const ImageSpec& spec, const std::string& name) { @@ -200,8 +200,7 @@ declare_imagespec(py_module& m) "get_string_attribute", [](const ImageSpec& spec, const std::string& name, const std::string& def) { - return oiio_py::str( - std::string(spec.get_string_attribute(name, def))); + return oiio_py::str(spec.get_string_attribute(name, def)); }, "name"_a, "defaultval"_a = "") .def( @@ -234,8 +233,7 @@ declare_imagespec(py_module& m) .def_static( "metadata_val", [](const ParamValue& p, bool human) { - return oiio_py::str( - std::string(ImageSpec::metadata_val(p, human))); + return oiio_py::str(ImageSpec::metadata_val(p, human)); }, "param"_a, "human"_a = false) .def( @@ -252,13 +250,11 @@ declare_imagespec(py_module& m) verb = ImageSpec::SerialDetailed; else if (Strutil::iequals(verbose, "detailedhuman")) verb = ImageSpec::SerialDetailedHuman; - return oiio_py::str(std::string(spec.serialize(fmt, verb))); + return oiio_py::str(spec.serialize(fmt, verb)); }, "format"_a = "text", "verbose"_a = "detailed") .def("to_xml", - [](const ImageSpec& spec) { - return oiio_py::str(std::string(spec.to_xml())); - }) + [](const ImageSpec& spec) { return oiio_py::str(spec.to_xml()); }) .def("from_xml", [](ImageSpec& self, const std::string& xml) { self.from_xml(xml.c_str()); diff --git a/src/python/py_oiio.cpp b/src/python/py_oiio.cpp index 464162a5aa..35e029c7ae 100644 --- a/src/python/py_oiio.cpp +++ b/src/python/py_oiio.cpp @@ -4,12 +4,13 @@ #include "py_oiio.h" -#include +#include +#include #if defined(OIIO_PY_BACKEND_NANOBIND) # include -# include #endif +#include namespace PyOpenImageIO { @@ -71,18 +72,60 @@ typedesc_from_python_array_code(string_view code) -#ifndef OIIO_PY_BACKEND_NANOBIND +oiio_py_buffer_view +oiio_py_request_buffer(const py::object& obj) +{ + oiio_py_buffer_view info; +#if defined(OIIO_PY_BACKEND_NANOBIND) + Py_buffer view; + if (PyObject_GetBuffer(obj.ptr(), &view, PyBUF_STRIDES | PyBUF_FORMAT) + != 0) { + PyErr_Clear(); + return info; + } + info.ptr = view.buf; + info.itemsize = view.itemsize; + info.ndim = view.ndim; + if (view.ndim > 0) { + info.shape.assign(view.shape, view.shape + view.ndim); + info.strides.assign(view.strides, view.strides + view.ndim); + info.size = 1; + for (int i = 0; i < view.ndim; ++i) { + info.size *= view.shape[i]; + } + } else { + info.size = view.len / std::max(view.itemsize, 1); + } + if (view.format && view.format[0]) { + info.format = view.format; + } + PyBuffer_Release(&view); +#else + const py::buffer_info req = py::cast(obj).request(); + info.ptr = req.ptr; + info.itemsize = req.itemsize; + info.size = req.size; + info.ndim = req.ndim; + info.shape.assign(req.shape.begin(), req.shape.end()); + info.strides.assign(req.strides.begin(), req.strides.end()); + info.format = req.format; +#endif + return info; +} + + -oiio_bufinfo::oiio_bufinfo(const py::buffer_info& pybuf) +oiio_bufinfo::oiio_bufinfo(const oiio_py_buffer_view& pybuf) { - if (pybuf.format.size()) + if (pybuf.format.size()) { format = typedesc_from_python_array_code(pybuf.format); + } if (format != TypeUnknown) { data = pybuf.ptr; xstride = format.size(); size = 1; for (int i = pybuf.ndim - 1; i >= 0; --i) { - if (pybuf.strides[i] != py::ssize_t(size * xstride)) { + if (pybuf.strides[i] != Py_ssize_t(size * xstride)) { // Just can't handle non-contiguous strides format = TypeUnknown; size = 0; @@ -95,11 +138,12 @@ oiio_bufinfo::oiio_bufinfo(const py::buffer_info& pybuf) -oiio_bufinfo::oiio_bufinfo(const py::buffer_info& pybuf, int nchans, int width, - int height, int depth, int pixeldims) +oiio_bufinfo::oiio_bufinfo(const oiio_py_buffer_view& pybuf, int nchans, + int width, int height, int depth, int pixeldims) { - if (pybuf.format.size()) + if (pybuf.format.size()) { format = typedesc_from_python_array_code(pybuf.format); + } if (size_t(pybuf.itemsize) != format.size() || pybuf.size != int64_t(width) * int64_t(height) * int64_t(depth * nchans)) { @@ -141,10 +185,10 @@ oiio_bufinfo::oiio_bufinfo(const py::buffer_info& pybuf, int nchans, int width, // Somebody collapsed a dimension. Is it [pixel][c] with x&y // combined, or is it [y][xpixel] with channels mushed together? if (pybuf.shape[0] == int64_t(width) * int64_t(height) - && pybuf.shape[1] == nchans) + && pybuf.shape[1] == nchans) { xstride = pybuf.strides[0]; - else if (pybuf.shape[0] == height - && pybuf.shape[1] == int64_t(width) * int64_t(nchans)) { + } else if (pybuf.shape[0] == height + && pybuf.shape[1] == int64_t(width) * int64_t(nchans)) { ystride = pybuf.strides[1]; xstride = pybuf.strides[0] * nchans; } else { @@ -163,7 +207,8 @@ oiio_bufinfo::oiio_bufinfo(const py::buffer_info& pybuf, int nchans, int width, format = TypeUnknown; // No idea what's going on -- error error = Strutil::fmt::format( "Python array shape is [{:,}] but expecting h={}, w={}, ch={}", - cspan(pybuf.shape), height, width, nchans); + cspan(pybuf.shape.data(), pybuf.shape.size()), + height, width, nchans); } } else if (pixeldims == 1) { // Reading a 1D scanline span @@ -187,18 +232,146 @@ oiio_bufinfo::oiio_bufinfo(const py::buffer_info& pybuf, int nchans, int width, pybuf.ndim); } - if (nchans > 1 && format.size() + if (nchans > 1 && format.size() && pybuf.ndim > 0 && size_t(pybuf.strides.back()) != format.size()) { format = TypeUnknown; // can't handle noncontig channels error = "Can't handle numpy array with noncontiguous channels"; } - if (format != TypeUnknown) + if (format != TypeUnknown) { data = pybuf.ptr; + } } -#endif // !OIIO_PY_BACKEND_NANOBIND +namespace { + + // Shared image layout → NumPy shape (C-contiguous pixel data). + void numpy_image_shape(std::vector& shape, int dims, size_t chans, + size_t width, size_t height, size_t depth, + size_t size) + { + if (dims == 4) { // volumetric + shape.assign({ depth, height, width, chans }); + } else if (dims == 3 && depth == 1) { // 2D+channels + shape.assign({ height, width, chans }); + } else if (dims == 2 && depth == 1 + && height == 1) { // 1D (scanline) + channels + shape.assign({ width, chans }); + } else { // punt -- make it a 1D array + shape.assign({ size }); + } + } + + + + template + py::object make_numpy_array_t(T* data, int dims, size_t chans, size_t width, + size_t height, size_t depth) + { + const size_t size = chans * width * height * depth; + T* mem = data ? data : new T[size]; + std::vector shape; + numpy_image_shape(shape, dims, chans, width, height, depth, size); +#if defined(OIIO_PY_BACKEND_NANOBIND) + py::capsule owner(mem, [](void* p) noexcept { + delete[] reinterpret_cast(p); + }); + // nullptr strides → C-contiguous (element strides). + return py::cast(py::ndarray(mem, shape.size(), + shape.data(), owner), + py::rv_policy::move); +#else + // Create a Python object that will free the allocated memory when + // destroyed. Shape-only ctor assumes C-contiguous byte layout. + py::capsule free_when_done(mem, [](void* f) { + delete[] (reinterpret_cast(f)); + }); + return py::array_t(shape, mem, free_when_done); +#endif + } + + + +#if defined(OIIO_PY_BACKEND_NANOBIND) + // Build a real numpy float16 array (nanobind has no half dtype). + template<> + py::object make_numpy_array_t(half* data, int dims, size_t chans, + size_t width, size_t height, + size_t depth) + { + const size_t size = chans * width * height * depth; + half* mem = data ? data : new half[size]; + std::vector shape; + numpy_image_shape(shape, dims, chans, width, height, depth, size); + py::object np = py::module_::import_("numpy"); + py::list shape_list; + for (size_t d : shape) + shape_list.append(d); + py::object arr = np.attr("empty")(py::tuple(shape_list), "float16"); + // Write through a uint16 view of the same buffer. + auto u16 = py::cast>( + arr.attr("view")(np.attr("uint16"))); + std::memcpy(u16.data(), mem, size * sizeof(half)); + delete[] mem; + return arr; + } +#endif + +} // namespace + + + +template +py::object +make_numpy_array(T* data, int dims, size_t chans, size_t width, size_t height, + size_t depth) +{ + return make_numpy_array_t(data, dims, chans, width, height, depth); +} + + + +py::object +make_numpy_array(TypeDesc format, void* data, int dims, size_t chans, + size_t width, size_t height, size_t depth) +{ + if (format == TypeDesc::FLOAT) { + return make_numpy_array((float*)data, dims, chans, width, height, + depth); + } + if (format == TypeDesc::UINT8) { + return make_numpy_array((unsigned char*)data, dims, chans, width, + height, depth); + } + if (format == TypeDesc::UINT16) { + return make_numpy_array((unsigned short*)data, dims, chans, width, + height, depth); + } + if (format == TypeDesc::INT8) { + return make_numpy_array((char*)data, dims, chans, width, height, depth); + } + if (format == TypeDesc::INT16) { + return make_numpy_array((short*)data, dims, chans, width, height, + depth); + } + if (format == TypeDesc::DOUBLE) { + return make_numpy_array((double*)data, dims, chans, width, height, + depth); + } + if (format == TypeDesc::HALF) { + return make_numpy_array((half*)data, dims, chans, width, height, depth); + } + if (format == TypeDesc::UINT) { + return make_numpy_array((unsigned int*)data, dims, chans, width, height, + depth); + } + if (format == TypeDesc::INT) { + return make_numpy_array((int*)data, dims, chans, width, height, depth); + } + delete[] (char*)data; + return py::none(); +} @@ -288,14 +461,6 @@ declare_global_bindings(py_module& m) declare_paramvalue(m); declare_imagespec(m); declare_roi(m); -} - - - -#ifndef OIIO_PY_BACKEND_NANOBIND -static void -declare_pybind_bindings(py_module& m) -{ declare_deepdata(m); declare_colorconfig(m); @@ -314,7 +479,6 @@ declare_pybind_bindings(py_module& m) declare_imagebufalgo(m); } -#endif @@ -346,28 +510,29 @@ declare_global_attribute_functions(py_module& m) m.def( "get_string_attribute", [](const std::string& name, const std::string& def) { - return oiio_py::str( - std::string(OIIO::get_string_attribute(name, def))); + return oiio_py::str(OIIO::get_string_attribute(name, def)); }, "name"_a, "defaultval"_a = ""); m.def("getattribute", &oiio_getattribute_typed, "name"_a, "type"_a = TypeUnknown); -} - - - -#ifndef OIIO_PY_BACKEND_NANOBIND -static void -declare_pybind_global_functions(py_module& m) -{ m.def("geterror", &OIIO::geterror, "clear"_a = true); m.def( "get_bytes_attribute", - [](const std::string& name, const std::string& def) { - std::string s(OIIO::get_string_attribute(name, def)); + [](const std::string& name, const py::object& def) { + // Accept str, bytes, or None (None → empty default). + std::string defstr; + if (!def.is_none()) { + if (PyBytes_Check(def.ptr())) { + defstr = oiio_py::bytes_to_stdstring( + py::cast(def)); + } else { + defstr = oiio_py::str_to_stdstring(def); + } + } + std::string s(OIIO::get_string_attribute(name, defstr)); return py::bytes(s.data(), s.size()); }, - "name"_a, "defaultval"_a = ""); + "name"_a, "defaultval"_a.none() = ""); m.def( "set_colorspace", [](ImageSpec& spec, const std::string& name) { @@ -393,17 +558,12 @@ declare_pybind_global_functions(py_module& m) }, "name"_a); } -#endif static void declare_module_attributes(py_module& m) { -#if defined(OIIO_PY_BACKEND_NANOBIND) - m.attr("__version__") = OIIO_VERSION_STRING; - m.attr("VERSION_STRING") = OIIO_VERSION_STRING; -#else m.attr("AutoStride") = AutoStride; m.attr("openimageio_version") = OIIO_VERSION; m.attr("VERSION") = OIIO_VERSION; @@ -413,7 +573,6 @@ declare_module_attributes(py_module& m) m.attr("VERSION_PATCH") = OIIO_VERSION_PATCH; m.attr("INTRO_STRING") = oiio_py::str(OIIO_INTRO_STRING); m.attr("__version__") = oiio_py::str(OIIO_VERSION_STRING); -#endif } @@ -451,9 +610,7 @@ OIIO_DECLARE_PYMODULE(PYMODULE_NAME) Sysutil::setup_crash_stacktrace("stdout"); declare_global_bindings(m); - declare_pybind_bindings(m); declare_global_attribute_functions(m); - declare_pybind_global_functions(m); declare_module_attributes(m); } diff --git a/src/python/py_oiio.h b/src/python/py_oiio.h index 36e61dc422..80e4b75746 100644 --- a/src/python/py_oiio.h +++ b/src/python/py_oiio.h @@ -29,6 +29,8 @@ # undef copysign #endif +#include + #include #include #include @@ -95,23 +97,23 @@ using namespace OIIO; // clang-format off -void declare_imagespec (py_module& m); -void declare_imageinput (py_module& m); -void declare_imageoutput (py_module& m); -void declare_typedesc (py_module& m); -void declare_roi (py_module& m); -void declare_deepdata (py_module& m); -void declare_colorconfig (py_module& m); -void declare_imagecache (py_module& m); -void declare_imagebuf (py_module& m); -void declare_imagebufalgo (py_module& m); -void declare_paramvalue (py_module& m); -void declare_global (py_module& m); -void declare_wrap (py_module& m); -void declare_mipmpode (py_module& m); -void declare_interpmode (py_module& m); -void declare_textureopt (py_module& m); -void declare_texturesystem (py_module& m); +void declare_imagespec(py_module& m); +void declare_imageinput(py_module& m); +void declare_imageoutput(py_module& m); +void declare_typedesc(py_module& m); +void declare_roi(py_module& m); +void declare_deepdata(py_module& m); +void declare_colorconfig(py_module& m); +void declare_imagecache(py_module& m); +void declare_imagebuf(py_module& m); +void declare_imagebufalgo(py_module& m); +void declare_paramvalue(py_module& m); +void declare_global(py_module& m); +void declare_wrap(py_module& m); +void declare_mipmpode(py_module& m); +void declare_interpmode(py_module& m); +void declare_textureopt(py_module& m); +void declare_texturesystem(py_module& m); // bool PyProgressCallback(void*, float); // object C_array_to_Python_array (const char *data, TypeDesc type, size_t size); @@ -145,9 +147,21 @@ template<> struct PyTypeForCType { typedef py::str type; }; // clang-format on -#ifndef OIIO_PY_BACKEND_NANOBIND -// Struct that holds OIIO style buffer info, constructed from -// py::buffer_info +// Buffer view shared by both backends (pybind buffer_info / Py_buffer). +struct oiio_py_buffer_view { + void* ptr = nullptr; + Py_ssize_t itemsize = 0; + Py_ssize_t size = 0; + int ndim = 0; + std::vector shape; + std::vector strides; + std::string format; +}; + +oiio_py_buffer_view +oiio_py_request_buffer(const py::object& obj); + +// Struct that holds OIIO style buffer info, constructed from a buffer object. struct oiio_bufinfo { TypeDesc format = TypeUnknown; void* data = nullptr; @@ -155,18 +169,33 @@ struct oiio_bufinfo { size_t size = 0; std::string error; + oiio_bufinfo() = default; // Just raw buffer, no idea what to expect, treat like a flat array. // Only works for "contiguous" buffers. - oiio_bufinfo(const py::buffer_info& pybuf); - + oiio_bufinfo(const oiio_py_buffer_view& pybuf); // Expect a certain layout, figure out how to make sense of the buffer. - oiio_bufinfo(const py::buffer_info& pybuf, int nchans, int width, + oiio_bufinfo(const oiio_py_buffer_view& pybuf, int nchans, int width, int height, int depth, int pixeldims); - // Retrieve presumed contiguous data value index i. - template T dataval(size_t i) { return ((const T*)data)[i]; } + template T dataval(size_t i) + { // Retrieve presumed contiguous data value index i. + return ((const T*)data)[i]; + } }; -#endif + +inline oiio_bufinfo +oiio_bufinfo_from_object(const py::object& obj) +{ + return oiio_bufinfo(oiio_py_request_buffer(obj)); +} + +inline oiio_bufinfo +oiio_bufinfo_from_object(const py::object& obj, int nchans, int width, + int height, int depth, int pixeldims) +{ + return oiio_bufinfo(oiio_py_request_buffer(obj), nchans, width, height, + depth, pixeldims); +} @@ -352,28 +381,9 @@ template inline bool py_buffer_to_stdvector(std::vector& vals, const py::object& obj) { - Py_buffer view; - if (PyObject_GetBuffer(obj.ptr(), &view, PyBUF_FORMAT | PyBUF_C_CONTIGUOUS) - != 0) { - PyErr_Clear(); - return false; - } - - bool ok = view.itemsize > 0 && view.len % view.itemsize == 0; - if (!ok) { - PyBuffer_Release(&view); - return false; - } - - TypeDesc format = TypeUnknown; - if (view.format && view.format[0]) { - format = typedesc_from_python_array_code(view.format); - } - - const size_t count = static_cast(view.len) / view.itemsize; - ok = buffer_format_to_stdvector(vals, format, view.buf, count); - PyBuffer_Release(&view); - return ok; + oiio_bufinfo binfo(oiio_py_request_buffer(obj)); + return buffer_format_to_stdvector(vals, binfo.format, binfo.data, + binfo.size); } @@ -401,7 +411,7 @@ inline bool py_buffer_to_stdvector(std::vector& vals, const py::buffer& obj) { OIIO_DASSERT(py::isinstance(obj)); - oiio_bufinfo binfo(obj.request()); + oiio_bufinfo binfo(oiio_py_request_buffer(py::cast(obj))); return buffer_format_to_stdvector(vals, binfo.format, binfo.data, binfo.size); } @@ -647,88 +657,17 @@ getattribute_typed(const T& obj, const std::string& name, -#ifndef OIIO_PY_BACKEND_NANOBIND -// TRANSFERS ownership of the data pointer! +// TRANSFERS ownership of the data pointer! Multi-dimensional numpy arrays. // N.B. There is some evidence that this doesn't work properly with // non-float arrays. Maybe a limitation of pybind11? template -inline py::array_t +py::object make_numpy_array(T* data, int dims, size_t chans, size_t width, size_t height, - size_t depth = 1) -{ - const size_t size = chans * width * height * depth; - T* mem = data ? data : new T[size]; + size_t depth = 1); - // Create a Python object that will free the allocated memory when - // destroyed: - py::capsule free_when_done(mem, [](void* f) { - delete[] (reinterpret_cast(f)); - }); - - std::vector shape, strides; - if (dims == 4) { // volumetric - shape.assign({ depth, height, width, chans }); - strides.assign({ height * width * chans * sizeof(T), - width * chans * sizeof(T), chans * sizeof(T), - sizeof(T) }); - } else if (dims == 3 && depth == 1) { // 2D+channels - shape.assign({ height, width, chans }); - strides.assign( - { width * chans * sizeof(T), chans * sizeof(T), sizeof(T) }); - } else if (dims == 2 && depth == 1 - && height == 1) { // 1D (scanline) + channels - shape.assign({ width, chans }); - strides.assign({ chans * sizeof(T), sizeof(T) }); - } else { // punt -- make it a 1D array - shape.assign({ size }); - strides.assign({ sizeof(T) }); - } - return py::array_t(shape, strides, mem, free_when_done); -} - - - -inline py::object +py::object make_numpy_array(TypeDesc format, void* data, int dims, size_t chans, - size_t width, size_t height, size_t depth = 1) -{ - if (format == TypeDesc::FLOAT) { - return make_numpy_array((float*)data, dims, chans, width, height, - depth); - } - if (format == TypeDesc::UINT8) { - return make_numpy_array((unsigned char*)data, dims, chans, width, - height, depth); - } - if (format == TypeDesc::UINT16) { - return make_numpy_array((unsigned short*)data, dims, chans, width, - height, depth); - } - if (format == TypeDesc::INT8) { - return make_numpy_array((char*)data, dims, chans, width, height, depth); - } - if (format == TypeDesc::INT16) { - return make_numpy_array((short*)data, dims, chans, width, height, - depth); - } - if (format == TypeDesc::DOUBLE) { - return make_numpy_array((double*)data, dims, chans, width, height, - depth); - } - if (format == TypeDesc::HALF) { - return make_numpy_array((half*)data, dims, chans, width, height, depth); - } - if (format == TypeDesc::UINT) { - return make_numpy_array((unsigned int*)data, dims, chans, width, height, - depth); - } - if (format == TypeDesc::INT) { - return make_numpy_array((int*)data, dims, chans, width, height, depth); - } - delete[] (char*)data; - return py::none(); -} -#endif + size_t width, size_t height, size_t depth = 1); diff --git a/src/python/py_texturesys.cpp b/src/python/py_texturesys.cpp index e24b1f4a46..a10a4d95f1 100644 --- a/src/python/py_texturesys.cpp +++ b/src/python/py_texturesys.cpp @@ -63,7 +63,7 @@ class TextureSystemWrap { void -declare_wrap(py::module& m) +declare_wrap(py_module& m) { py::enum_(m, "Wrap") .value("Default", Tex::Wrap::Default) @@ -79,7 +79,7 @@ declare_wrap(py::module& m) void -declare_mipmpode(py::module& m) +declare_mipmpode(py_module& m) { py::enum_(m, "MipMode") .value("Default", Tex::MipMode::Default) @@ -92,7 +92,7 @@ declare_mipmpode(py::module& m) void -declare_interpmode(py::module& m) +declare_interpmode(py_module& m) { py::enum_(m, "InterpMode") .value("Closest", Tex::InterpMode::Closest) @@ -104,13 +104,13 @@ declare_interpmode(py::module& m) void -declare_textureopt(py::module& m) +declare_textureopt(py_module& m) { py::class_(m, "TextureOpt") .def(py::init<>()) - .def_readwrite("firstchannel", &TextureOptWrap::firstchannel) - .def_readwrite("subimage", &TextureOptWrap::subimage) - .def_property( + .OIIO_PY_RW("firstchannel", &TextureOptWrap::firstchannel) + .OIIO_PY_RW("subimage", &TextureOptWrap::subimage) + .OIIO_PY_PROP_RW( "subimagename", [](const TextureOptWrap& texopt) { return std::string(texopt.subimagename); @@ -118,19 +118,19 @@ declare_textureopt(py::module& m) [](TextureOptWrap& texopt, const std::string& subimagename) { texopt.subimagename = subimagename; }) - .def_property( + .OIIO_PY_PROP_RW( "swrap", [](const TextureOptWrap& texopt) { return (Tex::Wrap)texopt.swrap; }, [](TextureOptWrap& texopt, const Tex::Wrap wrap) { texopt.swrap = (TextureOpt::Wrap)wrap; }) - .def_property( + .OIIO_PY_PROP_RW( "twrap", [](const TextureOptWrap& texopt) { return (Tex::Wrap)texopt.twrap; }, [](TextureOptWrap& texopt, const Tex::Wrap wrap) { texopt.twrap = (TextureOpt::Wrap)wrap; }) - .def_property( + .OIIO_PY_PROP_RW( "mipmode", [](const TextureOptWrap& texopt) { return (Tex::MipMode)texopt.mipmode; @@ -138,7 +138,7 @@ declare_textureopt(py::module& m) [](TextureOptWrap& texopt, const Tex::MipMode mipmode) { texopt.mipmode = (TextureOpt::MipMode)mipmode; }) - .def_property( + .OIIO_PY_PROP_RW( "interpmode", [](const TextureOptWrap& texopt) { return (Tex::InterpMode)texopt.interpmode; @@ -146,33 +146,30 @@ declare_textureopt(py::module& m) [](TextureOptWrap& texopt, const Tex::InterpMode interp) { texopt.interpmode = (TextureOpt::InterpMode)interp; }) - .def_readwrite("anisotropic", &TextureOptWrap::anisotropic) - .def_readwrite("conservative_filter", - &TextureOptWrap::conservative_filter) - .def_readwrite("sblur", &TextureOptWrap::sblur) - .def_readwrite("tblur", &TextureOptWrap::tblur) - .def_readwrite("swidth", &TextureOptWrap::swidth) - .def_readwrite("twidth", &TextureOptWrap::twidth) - .def_readwrite("fill", &TextureOptWrap::fill) - .def_property("missingcolor", &TextureOptWrap::get_missingcolor, - &TextureOptWrap::set_missingcolor) - .def_readwrite("rnd", &TextureOptWrap::rnd) - .def_property( + .OIIO_PY_RW("anisotropic", &TextureOptWrap::anisotropic) + .OIIO_PY_RW("conservative_filter", &TextureOptWrap::conservative_filter) + .OIIO_PY_RW("sblur", &TextureOptWrap::sblur) + .OIIO_PY_RW("tblur", &TextureOptWrap::tblur) + .OIIO_PY_RW("swidth", &TextureOptWrap::swidth) + .OIIO_PY_RW("twidth", &TextureOptWrap::twidth) + .OIIO_PY_RW("fill", &TextureOptWrap::fill) + .OIIO_PY_PROP_RW_NONE("missingcolor", &TextureOptWrap::get_missingcolor, + &TextureOptWrap::set_missingcolor) + .OIIO_PY_RW("rnd", &TextureOptWrap::rnd) + .OIIO_PY_PROP_RW( "rwrap", [](const TextureOptWrap& texopt) { return (Tex::Wrap)texopt.rwrap; }, [](TextureOptWrap& texopt, const Tex::Wrap wrap) { texopt.rwrap = (TextureOpt::Wrap)wrap; }) - .def_readwrite("rwidth", &TextureOptWrap::rwidth); + .OIIO_PY_RW("rwidth", &TextureOptWrap::rwidth); } void -declare_texturesystem(py::module& m) +declare_texturesystem(py_module& m) { - using namespace pybind11::literals; - py::class_(m, "TextureSystem") .def(py::init(), "shared"_a = true) .def_static("destroy", &TextureSystemWrap::destroy) @@ -322,9 +319,10 @@ declare_texturesystem(py::module& m) std::vector filenames; ts.m_texsys->inventory_udim(ustring(filename), filenames, nutiles, nvtiles); - std::vector strs; - for (auto f : filenames) + std::vector strs; + for (auto f : filenames) { strs.emplace_back(f.string()); + } py::tuple ret = py::make_tuple(nutiles, nvtiles, strs); return ret; }, diff --git a/testsuite/python-colorconfig/ref/out-ocio230.txt b/testsuite/python-colorconfig/ref/out-ocio230.txt index 17071b04df..ca19551d3d 100644 --- a/testsuite/python-colorconfig/ref/out-ocio230.txt +++ b/testsuite/python-colorconfig/ref/out-ocio230.txt @@ -1,3 +1,5 @@ +default_colorconfig same instance = True +default_colorconfig usable = True getNumColorSpaces = 15 getColorSpaceNames = ['ACES2065-1', 'ACEScc', 'ACEScct', 'ACEScg', 'Linear P3-D65', 'Linear Rec.2020', 'Linear Rec.709 (sRGB)', 'Gamma 1.8 Rec.709 - Texture', 'Gamma 2.2 AP1 - Texture', 'Gamma 2.2 Rec.709 - Texture', 'Gamma 2.4 Rec.709 - Texture', 'sRGB Encoded AP1 - Texture', 'sRGB Encoded P3-D65 - Texture', 'sRGB - Texture', 'Raw'] Index of 'lin_srgb' = 6 diff --git a/testsuite/python-colorconfig/ref/out-ocio230b.txt b/testsuite/python-colorconfig/ref/out-ocio230b.txt index d52992d9dd..542aa2a3bb 100644 --- a/testsuite/python-colorconfig/ref/out-ocio230b.txt +++ b/testsuite/python-colorconfig/ref/out-ocio230b.txt @@ -1,3 +1,5 @@ +default_colorconfig same instance = True +default_colorconfig usable = True getNumColorSpaces = 15 getColorSpaceNames = ['ACES2065-1', 'ACEScc', 'ACEScct', 'ACEScg', 'Linear P3-D65', 'Linear Rec.2020', 'Linear Rec.709 (sRGB)', 'Gamma 1.8 Rec.709 - Texture', 'Gamma 2.2 AP1 - Texture', 'Gamma 2.2 Rec.709 - Texture', 'Gamma 2.4 Rec.709 - Texture', 'sRGB Encoded AP1 - Texture', 'sRGB Encoded P3-D65 - Texture', 'sRGB - Texture', 'Raw'] Index of 'lin_srgb' = 6 diff --git a/testsuite/python-colorconfig/ref/out-ocio232.txt b/testsuite/python-colorconfig/ref/out-ocio232.txt index c3452050e1..9c063cdd9a 100644 --- a/testsuite/python-colorconfig/ref/out-ocio232.txt +++ b/testsuite/python-colorconfig/ref/out-ocio232.txt @@ -1,3 +1,5 @@ +default_colorconfig same instance = True +default_colorconfig usable = True getNumColorSpaces = 15 getColorSpaceNames = ['ACES2065-1', 'ACEScc', 'ACEScct', 'ACEScg', 'Linear P3-D65', 'Linear Rec.2020', 'Linear Rec.709 (sRGB)', 'Gamma 1.8 Rec.709 - Texture', 'Gamma 2.2 AP1 - Texture', 'Gamma 2.2 Rec.709 - Texture', 'Gamma 2.4 Rec.709 - Texture', 'sRGB Encoded AP1 - Texture', 'sRGB Encoded P3-D65 - Texture', 'sRGB - Texture', 'Raw'] Index of 'lin_srgb' = 6 diff --git a/testsuite/python-colorconfig/ref/out-ocio24.txt b/testsuite/python-colorconfig/ref/out-ocio24.txt index c568d2266d..a8dab6c064 100644 --- a/testsuite/python-colorconfig/ref/out-ocio24.txt +++ b/testsuite/python-colorconfig/ref/out-ocio24.txt @@ -1,3 +1,5 @@ +default_colorconfig same instance = True +default_colorconfig usable = True getNumColorSpaces = 23 getColorSpaceNames = ['sRGB - Display', 'Display P3 - Display', 'Rec.1886 Rec.709 - Display', 'Rec.2100-PQ - Display', 'ST2084-P3-D65 - Display', 'P3-D65 - Display', 'ACES2065-1', 'ACEScc', 'ACEScct', 'ACEScg', 'sRGB Encoded Rec.709 (sRGB)', 'Gamma 1.8 Encoded Rec.709', 'Gamma 2.2 Encoded Rec.709', 'Gamma 2.4 Encoded Rec.709', 'sRGB Encoded P3-D65', 'Gamma 2.2 Encoded AdobeRGB', 'sRGB Encoded AP1', 'Gamma 2.2 Encoded AP1', 'Linear Rec.709 (sRGB)', 'Linear P3-D65', 'Linear AdobeRGB', 'Linear Rec.2020', 'Raw'] Index of 'lin_srgb' = 18 diff --git a/testsuite/python-colorconfig/ref/out-ocio25.txt b/testsuite/python-colorconfig/ref/out-ocio25.txt index ced1f69a1c..83e831b137 100644 --- a/testsuite/python-colorconfig/ref/out-ocio25.txt +++ b/testsuite/python-colorconfig/ref/out-ocio25.txt @@ -1,3 +1,5 @@ +default_colorconfig same instance = True +default_colorconfig usable = True getNumColorSpaces = 25 getColorSpaceNames = ['sRGB - Display', 'Gamma 2.2 Rec.709 - Display', 'Display P3 - Display', 'Display P3 HDR - Display', 'P3-D65 - Display', 'Rec.1886 Rec.709 - Display', 'Rec.2100-PQ - Display', 'ST2084-P3-D65 - Display', 'ACES2065-1', 'ACEScc', 'ACEScct', 'ACEScg', 'sRGB Encoded Rec.709 (sRGB)', 'Gamma 1.8 Encoded Rec.709', 'Gamma 2.2 Encoded Rec.709', 'Gamma 2.4 Encoded Rec.709', 'sRGB Encoded P3-D65', 'Gamma 2.2 Encoded AdobeRGB', 'sRGB Encoded AP1', 'Gamma 2.2 Encoded AP1', 'Linear AdobeRGB', 'Linear P3-D65', 'Linear Rec.2020', 'Linear Rec.709 (sRGB)', 'Raw'] Index of 'lin_srgb' = 23 diff --git a/testsuite/python-colorconfig/ref/out.txt b/testsuite/python-colorconfig/ref/out.txt index 590a997026..711f2d88e4 100644 --- a/testsuite/python-colorconfig/ref/out.txt +++ b/testsuite/python-colorconfig/ref/out.txt @@ -1,3 +1,5 @@ +default_colorconfig same instance = True +default_colorconfig usable = True getNumColorSpaces = 14 getColorSpaceNames = ['ACES2065-1', 'ACEScc', 'ACEScct', 'ACEScg', 'Linear P3-D65', 'Linear Rec.2020', 'Linear Rec.709 (sRGB)', 'Gamma 1.8 Rec.709 - Texture', 'Gamma 2.2 AP1 - Texture', 'Gamma 2.2 Rec.709 - Texture', 'Gamma 2.4 Rec.709 - Texture', 'sRGB Encoded AP1 - Texture', 'sRGB - Texture', 'Raw'] Index of 'lin_srgb' = 6 diff --git a/testsuite/python-colorconfig/src/test_colorconfig.py b/testsuite/python-colorconfig/src/test_colorconfig.py index 16f3ce0ef2..cbbba63dc9 100755 --- a/testsuite/python-colorconfig/src/test_colorconfig.py +++ b/testsuite/python-colorconfig/src/test_colorconfig.py @@ -14,6 +14,15 @@ TEST_CONFIG_PATH: Path = Path(__file__).parent/"oiio_test_v0.9.2.ocio" try: + # Regression test: default_colorconfig() returns a non-copyable singleton + # reference and must use a reference return-value policy (without it, + # callers can get a copy/wrapper that is not the shared instance, or the + # binding can fail for non-copyable ColorConfig). + default_a = oiio.ColorConfig.default_colorconfig() + default_b = oiio.ColorConfig.default_colorconfig() + print ("default_colorconfig same instance =", default_a is default_b) + print ("default_colorconfig usable =", default_a.getNumColorSpaces() > 0) + config = oiio.ColorConfig() print ("getNumColorSpaces =", config.getNumColorSpaces()) print ("getColorSpaceNames =", config.getColorSpaceNames()) diff --git a/testsuite/python-imagebuf/ref/out-alt.txt b/testsuite/python-imagebuf/ref/out-alt.txt index cb2f545958..1c557eca39 100644 --- a/testsuite/python-imagebuf/ref/out-alt.txt +++ b/testsuite/python-imagebuf/ref/out-alt.txt @@ -14,6 +14,10 @@ Resetting to be a writable 640x480,3 Float: alpha channel = -1 z channel = -1 deep = False +Constructing with ImageBuf(spec, zero): + zero=True pixel (0,0): (0.0, 0.0, 0.0) + zero=False then setpixel (0,0): (0.25, 0.5, 0.75) + Constructing from a bare numpy array: from 3D, shape is float 0 2 0 3 0 1 0 4 resolution 2x3+0+0 @@ -29,6 +33,7 @@ Constructing from a bare numpy array: from 4D, shape is float 0 2 0 2 0 2 0 4 + from 0-d array has_error: True Testing read of ../common/textures/grid.tx: channels: 4 name: ../common/textures/grid.tx diff --git a/testsuite/python-imagebuf/ref/out.txt b/testsuite/python-imagebuf/ref/out.txt index cb2f545958..1c557eca39 100644 --- a/testsuite/python-imagebuf/ref/out.txt +++ b/testsuite/python-imagebuf/ref/out.txt @@ -14,6 +14,10 @@ Resetting to be a writable 640x480,3 Float: alpha channel = -1 z channel = -1 deep = False +Constructing with ImageBuf(spec, zero): + zero=True pixel (0,0): (0.0, 0.0, 0.0) + zero=False then setpixel (0,0): (0.25, 0.5, 0.75) + Constructing from a bare numpy array: from 3D, shape is float 0 2 0 3 0 1 0 4 resolution 2x3+0+0 @@ -29,6 +33,7 @@ Constructing from a bare numpy array: from 4D, shape is float 0 2 0 2 0 2 0 4 + from 0-d array has_error: True Testing read of ../common/textures/grid.tx: channels: 4 name: ../common/textures/grid.tx diff --git a/testsuite/python-imagebuf/src/test_imagebuf.py b/testsuite/python-imagebuf/src/test_imagebuf.py index 789efb3724..5469227e6d 100755 --- a/testsuite/python-imagebuf/src/test_imagebuf.py +++ b/testsuite/python-imagebuf/src/test_imagebuf.py @@ -235,6 +235,15 @@ def test_outofrange_subimage_miplevel() : b.reset (oiio.ImageSpec(640,480,3,oiio.FLOAT)) print_imagespec (b.spec()) + # Cover ImageBuf(spec, zero) placement-new constructor (both backends). + print ("Constructing with ImageBuf(spec, zero):") + b = oiio.ImageBuf(oiio.ImageSpec(2, 2, 3, oiio.FLOAT), True) + print (" zero=True pixel (0,0):", ftupstr(b.getpixel(0, 0))) + b = oiio.ImageBuf(oiio.ImageSpec(2, 2, 3, oiio.FLOAT), False) + b.setpixel(0, 0, (0.25, 0.5, 0.75)) + print (" zero=False then setpixel (0,0):", ftupstr(b.getpixel(0, 0))) + print ("") + print ("Constructing from a bare numpy array:") b = oiio.ImageBuf(numpy.array([[[0.1,0.0,0.9,1.0], [0.2,0.0,0.7,1.0]], [[0.3,0.0,0.8,1.0], [0.4,0.0,0.6,1.0]], @@ -257,6 +266,12 @@ def test_outofrange_subimage_miplevel() : print (" from 4D, shape is", b.spec().format, b.roi) print ("") + # Regression test: 0-d numpy arrays must not crash ImageBuf construction. + # Without the ndim > 0 guard, strides[ndim - 1] was out-of-bounds UB. + b = oiio.ImageBuf(numpy.array(1.0, dtype="f")) + print (" from 0-d array has_error:", b.has_error) + b.geterror() # clear pending error so destruction is quiet + # Test reading from disk print ("Testing read of ../common/textures/grid.tx:") b = oiio.ImageBuf ("../common/textures/grid.tx") diff --git a/testsuite/python-oiio/ref/out.txt b/testsuite/python-oiio/ref/out.txt index e213a52b48..97ec265539 100644 --- a/testsuite/python-oiio/ref/out.txt +++ b/testsuite/python-oiio/ref/out.txt @@ -48,6 +48,17 @@ Testing global get_*_attribute defaults: get_int plugin_searchpath (wrong type) default: 77 get_float on int attribute returns default: True +Testing get_bytes_attribute default branches: + omitted default type bytes: True + omitted default value: b'' + None default type bytes: True + None default value: b'' + str default type bytes: True + str default value: b'from_str' + bytes default type bytes: True + bytes default value: b'from_bytes' + present attr ignores default: True + Testing getattribute() with TypeUnknown: getattribute(missing, TypeUnknown): None diff --git a/testsuite/python-oiio/src/test_oiio.py b/testsuite/python-oiio/src/test_oiio.py index 6e53be5af0..6075f875f6 100644 --- a/testsuite/python-oiio/src/test_oiio.py +++ b/testsuite/python-oiio/src/test_oiio.py @@ -114,6 +114,26 @@ def version_code(major: int, minor: int, patch: int) -> int: oiio.get_float_attribute ("threads", -1.0) == -1.0) print ("") + # Cover get_bytes_attribute default conversion branches (str / bytes / None / + # omitted). Nanobind does not auto-convert bytes→std::string like pybind11. + print ("Testing get_bytes_attribute default branches:") + missing = "not_a_real_attr_for_bytes_default" + omitted = oiio.get_bytes_attribute (missing) + none_def = oiio.get_bytes_attribute (missing, None) + str_def = oiio.get_bytes_attribute (missing, "from_str") + bytes_def = oiio.get_bytes_attribute (missing, b"from_bytes") + print (" omitted default type bytes:", isinstance (omitted, bytes)) + print (" omitted default value:", omitted) + print (" None default type bytes:", isinstance (none_def, bytes)) + print (" None default value:", none_def) + print (" str default type bytes:", isinstance (str_def, bytes)) + print (" str default value:", str_def) + print (" bytes default type bytes:", isinstance (bytes_def, bytes)) + print (" bytes default value:", bytes_def) + present = oiio.get_bytes_attribute ("font_searchpath", b"unused") + print (" present attr ignores default:", present == b"/fonts") + print ("") + print ("Testing getattribute() with TypeUnknown:") print (" getattribute(missing, TypeUnknown):", oiio.getattribute ("not_a_real_attr", oiio.TypeUnknown))