diff --git a/CMakeLists.txt b/CMakeLists.txt index dd24534c94..5599361a71 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -308,7 +308,12 @@ else () set (_py_dev_found Python3_Development.Module_FOUND) endif () if (USE_PYTHON AND ${_py_dev_found} AND NOT BUILD_OIIOUTIL_ONLY) - add_subdirectory (src/python) + if (OIIO_BUILD_PYTHON_PYBIND11) + add_subdirectory (src/python) + endif () + if (OIIO_BUILD_PYTHON_NANOBIND) + add_subdirectory (src/python-nanobind) + endif () else () message (STATUS "Not building Python bindings: USE_PYTHON=${USE_PYTHON}, Python3_Development.Module_FOUND=${Python3_Development.Module_FOUND}") endif () diff --git a/INSTALL.md b/INSTALL.md index 3f35c35530..419c1ed7f7 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -42,6 +42,11 @@ NEW or CHANGED MINIMUM dependencies since the last major release are **bold**. * Python >= 3.9 (tested through 3.13). * pybind11 >= 2.7 (tested through 3.0) * NumPy (tested through 2.4.4) + * For the experimental nanobind migration backend: + * NumPy (tested through 2.2.4) + * For the nanobind (WIP) migration backend used in source/CMake builds: + * nanobind discoverable by CMake, or installed in the active Python + environment so `python -m nanobind --cmake_dir` works * If you want support for PNG files: * libPNG >= 1.6.0 (tested though 1.6.56) * If you want support for camera "RAW" formats: @@ -157,6 +162,12 @@ Make wrapper (`make PkgName_ROOT=...`). `USE_PYTHON=0` : Omits building the Python bindings. +`OIIO_PYTHON_BINDINGS_BACKEND=pybind11|nanobind|both` : Select which Python +binding backend(s) to configure for source/CMake builds. `both` keeps the +existing pybind11 module and also builds the nanobind (WIP) module. The +Python packaging path driven by `pyproject.toml` still targets the production +pybind11 bindings today. + `OIIO_BUILD_TESTS=0` : Omits building tests (you probably don't need them unless you are a developer of OIIO or want to verify that your build passes all tests). @@ -247,6 +258,7 @@ Additionally, a few helpful modifiers alter some build-time options: | make USE_QT=0 ... | Skip anything that needs Qt | | make MYCC=xx MYCXX=yy ... | Use custom compilers | | make USE_PYTHON=0 ... | Don't build the Python binding | +| make OIIO_PYTHON_BINDINGS_BACKEND=both ... | For source/CMake builds, build the existing pybind11 bindings and the nanobind (WIP) module | | make BUILD_SHARED_LIBS=0 | Build static library instead of shared | | make IGNORE_HOMEBREWED_DEPS=1 | Ignore homebrew-managed dependencies | | make LINKSTATIC=1 ... | Link with static external libraries when possible | diff --git a/src/cmake/externalpackages.cmake b/src/cmake/externalpackages.cmake index 52fb4e195a..2f670f77e9 100644 --- a/src/cmake/externalpackages.cmake +++ b/src/cmake/externalpackages.cmake @@ -118,9 +118,13 @@ endif() if (USE_PYTHON) find_python() endif () -if (USE_PYTHON) +if (USE_PYTHON AND OIIO_BUILD_PYTHON_PYBIND11) checked_find_package (pybind11 REQUIRED VERSION_MIN 2.7) endif () +if (USE_PYTHON AND OIIO_BUILD_PYTHON_NANOBIND) + discover_nanobind_cmake_dir() + checked_find_package (nanobind CONFIG REQUIRED) +endif () ########################################################################### diff --git a/src/cmake/pythonutils.cmake b/src/cmake/pythonutils.cmake index efab6ea63d..02a945e592 100644 --- a/src/cmake/pythonutils.cmake +++ b/src/cmake/pythonutils.cmake @@ -8,6 +8,31 @@ set (PYTHON_VERSION "" CACHE STRING "Target version of python to find") option (PYLIB_INCLUDE_SONAME "If ON, soname/soversion will be set for Python module library" OFF) option (PYLIB_LIB_PREFIX "If ON, prefix the Python module with 'lib'" OFF) set (PYMODULE_SUFFIX "" CACHE STRING "Suffix to add to Python module init namespace") +set (OIIO_PYTHON_BINDINGS_BACKEND "pybind11" CACHE STRING + "Which Python binding backend(s) to build: pybind11, nanobind, or both") +set_property (CACHE OIIO_PYTHON_BINDINGS_BACKEND PROPERTY STRINGS + pybind11 nanobind both) + +# Normalize and validate the user-facing backend selector early so the rest +# of the file can make simple boolean decisions. +string (TOLOWER "${OIIO_PYTHON_BINDINGS_BACKEND}" OIIO_PYTHON_BINDINGS_BACKEND) +if (NOT OIIO_PYTHON_BINDINGS_BACKEND MATCHES "^(pybind11|nanobind|both)$") + message (FATAL_ERROR + "OIIO_PYTHON_BINDINGS_BACKEND must be one of: pybind11, nanobind, both") +endif () + +# Derive internal switches used by the top-level CMakeLists and the Python +# helper macros below. +set (OIIO_BUILD_PYTHON_PYBIND11 OFF) +set (OIIO_BUILD_PYTHON_NANOBIND OFF) +if (OIIO_PYTHON_BINDINGS_BACKEND STREQUAL "pybind11" + OR OIIO_PYTHON_BINDINGS_BACKEND STREQUAL "both") + set (OIIO_BUILD_PYTHON_PYBIND11 ON) +endif () +if (OIIO_PYTHON_BINDINGS_BACKEND STREQUAL "nanobind" + OR OIIO_PYTHON_BINDINGS_BACKEND STREQUAL "both") + set (OIIO_BUILD_PYTHON_NANOBIND ON) +endif () if (WIN32) set (PYLIB_LIB_TYPE SHARED CACHE STRING "Type of library to build for python module (MODULE or SHARED)") else () @@ -54,6 +79,15 @@ macro (find_python) Python3_Development.Module_FOUND Python3_Interpreter_FOUND ) + if (OIIO_BUILD_PYTHON_NANOBIND) + # nanobind's CMake package expects the generic FindPython targets and + # variables (Python::Module, Python_EXECUTABLE, etc.), not the + # versioned Python3::* targets that the rest of OIIO uses today. + find_package (Python ${Python3_VERSION_MAJOR}.${Python3_VERSION_MINOR} + EXACT REQUIRED + COMPONENTS ${_py_components}) + endif () + # The version that was found may not be the default or user # defined one. set (PYTHON_VERSION_FOUND ${Python3_VERSION_MAJOR}.${Python3_VERSION_MINOR}) @@ -63,15 +97,44 @@ macro (find_python) set (PythonInterp3_FIND_VERSION PYTHON_VERSION_FOUND) set (PythonInterp3_FIND_VERSION_MAJOR ${Python3_VERSION_MAJOR}) + if (NOT DEFINED PYTHON_SITE_ROOT_DIR) + set (PYTHON_SITE_ROOT_DIR + "${CMAKE_INSTALL_LIBDIR}/python${PYTHON_VERSION_FOUND}/site-packages") + endif () if (NOT DEFINED PYTHON_SITE_DIR) - set (PYTHON_SITE_DIR "${CMAKE_INSTALL_LIBDIR}/python${PYTHON_VERSION_FOUND}/site-packages/OpenImageIO") + set (PYTHON_SITE_DIR "${PYTHON_SITE_ROOT_DIR}/OpenImageIO") endif () message (VERBOSE " Python site packages dir ${PYTHON_SITE_DIR}") + message (VERBOSE " Python site packages root ${PYTHON_SITE_ROOT_DIR}") message (VERBOSE " Python to include 'lib' prefix: ${PYLIB_LIB_PREFIX}") message (VERBOSE " Python to include SO version: ${PYLIB_INCLUDE_SONAME}") endmacro() +# Help CMake locate nanobind when it was installed as a Python package. +macro (discover_nanobind_cmake_dir) + if (nanobind_DIR OR nanobind_ROOT OR "$ENV{nanobind_DIR}" OR "$ENV{nanobind_ROOT}") + return() + endif () + + if (NOT Python3_Interpreter_FOUND) + return() + endif () + + execute_process ( + COMMAND ${Python3_EXECUTABLE} -m nanobind --cmake_dir + RESULT_VARIABLE _oiio_nanobind_result + OUTPUT_VARIABLE _oiio_nanobind_cmake_dir + OUTPUT_STRIP_TRAILING_WHITESPACE + ERROR_QUIET) + if (_oiio_nanobind_result EQUAL 0 + AND EXISTS "${_oiio_nanobind_cmake_dir}/nanobind-config.cmake") + set (nanobind_DIR "${_oiio_nanobind_cmake_dir}" CACHE PATH + "Path to the nanobind CMake package" FORCE) + endif () +endmacro() + + ########################################################################### # pybind11 @@ -163,3 +226,62 @@ macro (setup_python_module) endmacro () + +########################################################################### +# nanobind + +macro (setup_python_module_nanobind) + cmake_parse_arguments (lib "" "TARGET;MODULE" + "SOURCES;LIBS;INCLUDES;SYSTEM_INCLUDE_DIRS;PACKAGE_FILES" + ${ARGN}) + + set (target_name ${lib_TARGET}) + + if (NOT COMMAND nanobind_add_module) + discover_nanobind_cmake_dir() + find_package (nanobind CONFIG REQUIRED) + endif () + + nanobind_add_module(${target_name} ${lib_SOURCES}) + if (CMAKE_CXX_COMPILER_ID MATCHES "Clang" AND TARGET nanobind-static) + target_compile_options (nanobind-static PRIVATE -Wno-error=format-nonliteral) + endif () + + target_include_directories (${target_name} + PRIVATE ${lib_INCLUDES}) + target_include_directories (${target_name} + SYSTEM PRIVATE ${lib_SYSTEM_INCLUDE_DIRS}) + target_link_libraries (${target_name} + PRIVATE ${lib_LIBS}) + + set (_module_LINK_FLAGS "${VISIBILITY_MAP_COMMAND} ${EXTRA_DSO_LINK_ARGS}") + if (UNIX AND NOT APPLE) + set (_module_LINK_FLAGS "${_module_LINK_FLAGS} -Wl,--exclude-libs,ALL") + endif () + set_target_properties (${target_name} PROPERTIES + LINK_FLAGS ${_module_LINK_FLAGS} + OUTPUT_NAME ${lib_MODULE} + DEBUG_POSTFIX "") + + if (SKBUILD) + set (_nanobind_install_dir .) + else () + set (_nanobind_install_dir ${PYTHON_SITE_DIR}) + endif () + + # Keep nanobind modules isolated in the build tree so they don't alter + # how the existing top-level OpenImageIO module is imported during tests. + set_target_properties (${target_name} PROPERTIES + LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib/python/nanobind/OpenImageIO + ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib/python/nanobind/OpenImageIO + ) + + install (TARGETS ${target_name} + RUNTIME DESTINATION ${_nanobind_install_dir} COMPONENT user + LIBRARY DESTINATION ${_nanobind_install_dir} COMPONENT user) + + if (lib_PACKAGE_FILES) + install (FILES ${lib_PACKAGE_FILES} + DESTINATION ${_nanobind_install_dir} COMPONENT user) + endif () +endmacro () diff --git a/src/cmake/testing.cmake b/src/cmake/testing.cmake index a209c50524..5d7da6af1e 100644 --- a/src/cmake/testing.cmake +++ b/src/cmake/testing.cmake @@ -33,6 +33,7 @@ set(OIIO_TESTSUITE_IMAGEDIR "${PROJECT_BINARY_DIR}/testsuite" CACHE PATH # [ DISABLEVAR variable_name ... ] # [ SUFFIX suffix ] # [ ENVIRONMENT "VAR=value" ... ] +# [ ENVIRONMENT_MODIFICATION "VAR=op:value" ... ] # ) # # The optional argument IMAGEDIR is used to check whether external test images @@ -55,8 +56,12 @@ set(OIIO_TESTSUITE_IMAGEDIR "${PROJECT_BINARY_DIR}/testsuite" CACHE PATH # The optional ENVIRONMENT is a list of environment variables to set for the # test. # +# The optional ENVIRONMENT_MODIFICATION is a list of environment variable +# modifications in the format accepted by the CTest ENVIRONMENT_MODIFICATION +# property. +# macro (oiio_add_tests) - cmake_parse_arguments (_ats "" "SUFFIX;TESTNAME" "URL;IMAGEDIR;LABEL;FOUNDVAR;ENABLEVAR;DISABLEVAR;ENVIRONMENT" ${ARGN}) + cmake_parse_arguments (_ats "" "SUFFIX;TESTNAME" "URL;IMAGEDIR;LABEL;FOUNDVAR;ENABLEVAR;DISABLEVAR;ENVIRONMENT;ENVIRONMENT_MODIFICATION" ${ARGN}) # Arguments: args... set (_ats_testdir "${OIIO_TESTSUITE_IMAGEDIR}/${_ats_IMAGEDIR}") # If there was a FOUNDVAR param specified and that variable name is @@ -131,6 +136,11 @@ macro (oiio_add_tests) "OIIO_TESTSUITE_CUR=${_testdir}" "Python_EXECUTABLE=${Python3_EXECUTABLE}" ${_ats_ENVIRONMENT}) + if (_ats_ENVIRONMENT_MODIFICATION) + set_property(TEST ${_testname} APPEND PROPERTY + ENVIRONMENT_MODIFICATION + ${_ats_ENVIRONMENT_MODIFICATION}) + endif () if (NOT ${_ats_testdir} STREQUAL "") set_property(TEST ${_testname} APPEND PROPERTY ENVIRONMENT "OIIO_TESTSUITE_IMAGEDIR=${_ats_testdir}") @@ -228,25 +238,48 @@ macro (oiio_add_all_tests) # Python interpreter itself won't be linked with the right asan # libraries to run correctly. if (USE_PYTHON AND NOT BUILD_OIIOUTIL_ONLY AND NOT SANITIZE) - oiio_add_tests ( - docs-examples-python - python-colorconfig - python-deep - python-imagebuf - python-imagecache - python-imageoutput - python-imagespec - python-paramlist - python-roi - python-texturesys - python-typedesc - filters - ) - # These Python tests also need access to oiio-images - oiio_add_tests ( - python-imageinput python-imagebufalgo - IMAGEDIR oiio-images - ) + set (pybind11_python_path_mod + "PYTHONPATH=path_list_prepend:${CMAKE_BINARY_DIR}/lib/python/site-packages") + set (nanobind_python_tests + python-imagespec + python-paramlist + python-roi + python-typedesc) + set (nanobind_python_test_suffix ".nanobind") + if (OIIO_BUILD_PYTHON_PYBIND11) + oiio_add_tests ( + docs-examples-python + python-colorconfig + python-deep + python-imagebuf + python-imagecache + python-imageoutput + python-imagespec + python-paramlist + python-roi + python-texturesys + python-typedesc + filters + ENVIRONMENT_MODIFICATION ${pybind11_python_path_mod} + ) + # These Python tests also need access to oiio-images + oiio_add_tests ( + python-imageinput python-imagebufalgo + IMAGEDIR oiio-images + ENVIRONMENT_MODIFICATION ${pybind11_python_path_mod} + ) + else () + set (nanobind_python_test_suffix "") + endif () + + if (OIIO_BUILD_PYTHON_NANOBIND) + oiio_add_tests ( + ${nanobind_python_tests} + SUFFIX ${nanobind_python_test_suffix} + ENVIRONMENT_MODIFICATION + "PYTHONPATH=path_list_prepend:${CMAKE_BINARY_DIR}/lib/python/nanobind" + ) + endif () endif () oiio_add_tests (oiiotool-color diff --git a/src/python-nanobind/CMakeLists.txt b/src/python-nanobind/CMakeLists.txt new file mode 100644 index 0000000000..0caf163043 --- /dev/null +++ b/src/python-nanobind/CMakeLists.txt @@ -0,0 +1,28 @@ +# Copyright Contributors to the OpenImageIO project. +# SPDX-License-Identifier: Apache-2.0 +# https://github.com/AcademySoftwareFoundation/OpenImageIO + +set (nanobind_srcs + py_oiio.cpp + py_paramvalue.cpp + py_roi.cpp + py_imagespec.cpp + py_typedesc.cpp) + +set (nanobind_build_package_dir ${CMAKE_BINARY_DIR}/lib/python/nanobind/OpenImageIO) +file (MAKE_DIRECTORY ${nanobind_build_package_dir}) +configure_file (__init__.py + ${nanobind_build_package_dir}/__init__.py + COPYONLY) + +setup_python_module_nanobind ( + TARGET PyOpenImageIONanobind + MODULE _OpenImageIO + SOURCES ${nanobind_srcs} + LIBS OpenImageIO +) + +if (OIIO_PYTHON_BINDINGS_BACKEND STREQUAL "nanobind") + install (FILES __init__.py + DESTINATION ${PYTHON_SITE_DIR} COMPONENT user) +endif () diff --git a/src/python-nanobind/__init__.py b/src/python-nanobind/__init__.py new file mode 100644 index 0000000000..c7f5ccbabf --- /dev/null +++ b/src/python-nanobind/__init__.py @@ -0,0 +1,37 @@ +# Copyright Contributors to the OpenImageIO project. +# SPDX-License-Identifier: Apache-2.0 +# https://github.com/AcademySoftwareFoundation/OpenImageIO + +import os +import sys +import platform + +_here = os.path.abspath(os.path.dirname(__file__)) + +# Set $OpenImageIO_ROOT if not already set before importing helper modules. +if not os.getenv("OpenImageIO_ROOT"): + if all([os.path.exists(os.path.join(_here, i)) for i in ["share", "bin", "lib"]]): + os.environ["OpenImageIO_ROOT"] = _here + +if platform.system() == "Windows": + _bin_dir = os.path.join(_here, "bin") + if os.path.exists(_bin_dir): + os.add_dll_directory(_bin_dir) + elif sys.version_info >= (3, 8): + if os.getenv("OPENIMAGEIO_PYTHON_LOAD_DLLS_FROM_PATH", "0") == "1": + for path in os.getenv("PATH", "").split(os.pathsep): + if os.path.exists(path) and path != ".": + os.add_dll_directory(path) + +from . import _OpenImageIO as _ext # noqa: E402 +from ._OpenImageIO import * # type: ignore # noqa: E402, F401, F403 + +__doc__ = """ +OpenImageIO Python package exposing the nanobind migration bindings. +The production pybind11 bindings are not installed in this configuration. +"""[1:-1] + +__version__ = getattr(_ext, "__version__", "") + +# TODO: Restore the Python CLI entry-point trampolines when the nanobind +# package ships the full wheel-style bin/lib/share layout. diff --git a/src/python-nanobind/py_imagespec.cpp b/src/python-nanobind/py_imagespec.cpp new file mode 100644 index 0000000000..474c773e3c --- /dev/null +++ b/src/python-nanobind/py_imagespec.cpp @@ -0,0 +1,301 @@ +// Copyright Contributors to the OpenImageIO project. +// SPDX-License-Identifier: Apache-2.0 +// https://github.com/AcademySoftwareFoundation/OpenImageIO + +#include "py_oiio.h" + +namespace { + +OIIO_NAMESPACE_USING + +nb::tuple +imagespec_get_channelformats(const ImageSpec& spec, bool allow_empty = true) +{ + std::vector formats; + if (spec.channelformats.size() || !allow_empty) + spec.get_channelformats(formats); + return PyOpenImageIO::C_to_tuple(cspan(formats)); +} + + +void +imagespec_set_channelformats(ImageSpec& spec, nb::handle py_channelformats) +{ + spec.channelformats.clear(); + PyOpenImageIO::py_to_stdvector(spec.channelformats, py_channelformats); +} + + +nb::tuple +imagespec_get_channelnames(const ImageSpec& spec) +{ + return PyOpenImageIO::C_to_tuple(cspan(spec.channelnames)); +} + + +void +imagespec_set_channelnames(ImageSpec& spec, nb::handle py_channelnames) +{ + spec.channelnames.clear(); + PyOpenImageIO::py_to_stdvector(spec.channelnames, py_channelnames); +} + + +nb::object +imagespec_getattribute_typed(const ImageSpec& spec, const std::string& name, + TypeDesc type = TypeUnknown) +{ + ParamValue tmpparam; + const ParamValue* p = spec.find_attribute(name, tmpparam, type); + if (!p) + return nb::none(); + return PyOpenImageIO::make_pyobject(p->data(), p->type(), p->nvalues()); +} + + +ROI +imagespec_get_roi(const ImageSpec& spec) +{ + return get_roi(spec); +} + + +ROI +imagespec_get_roi_full(const ImageSpec& spec) +{ + return get_roi_full(spec); +} + + +void +imagespec_set_roi(ImageSpec& spec, const ROI& roi) +{ + set_roi(spec, roi); +} + + +void +imagespec_set_roi_full(ImageSpec& spec, const ROI& roi) +{ + set_roi_full(spec, roi); +} + +} // namespace + + +namespace PyOpenImageIO { + +void +declare_imagespec(nb::module_& m) +{ + nb::class_(m, "ImageSpec") + .def_rw("x", &ImageSpec::x) + .def_rw("y", &ImageSpec::y) + .def_rw("z", &ImageSpec::z) + .def_rw("width", &ImageSpec::width) + .def_rw("height", &ImageSpec::height) + .def_rw("depth", &ImageSpec::depth) + .def_rw("full_x", &ImageSpec::full_x) + .def_rw("full_y", &ImageSpec::full_y) + .def_rw("full_z", &ImageSpec::full_z) + .def_rw("full_width", &ImageSpec::full_width) + .def_rw("full_height", &ImageSpec::full_height) + .def_rw("full_depth", &ImageSpec::full_depth) + .def_rw("tile_width", &ImageSpec::tile_width) + .def_rw("tile_height", &ImageSpec::tile_height) + .def_rw("tile_depth", &ImageSpec::tile_depth) + .def_rw("nchannels", &ImageSpec::nchannels) + .def_rw("format", &ImageSpec::format) + .def_prop_rw( + "channelformats", + [](const ImageSpec& spec) { + return imagespec_get_channelformats(spec); + }, + &imagespec_set_channelformats) + .def_prop_rw("channelnames", &imagespec_get_channelnames, + &imagespec_set_channelnames) + .def_rw("alpha_channel", &ImageSpec::alpha_channel) + .def_rw("z_channel", &ImageSpec::z_channel) + .def_rw("deep", &ImageSpec::deep) + .def_prop_ro("extra_attribs", + [](const ImageSpec& spec) { return spec.extra_attribs; }) + .def_prop_rw("roi", &imagespec_get_roi, &imagespec_set_roi) + .def_prop_rw("roi_full", &imagespec_get_roi_full, + &imagespec_set_roi_full) + .def(nb::init<>()) + .def(nb::init()) + .def(nb::init()) + .def(nb::init()) + .def(nb::init()) + .def("copy", [](const ImageSpec& self) { return ImageSpec(self); }) + .def("set_format", + [](ImageSpec& self, TypeDesc t) { self.set_format(t); }) + .def("default_channel_names", &ImageSpec::default_channel_names) + .def("channel_bytes", + [](const ImageSpec& spec) { return spec.channel_bytes(); }) + .def( + "channel_bytes", + [](const ImageSpec& spec, int chan, bool native) { + return spec.channel_bytes(chan, native); + }, + "channel"_a, "native"_a = false) + .def( + "pixel_bytes", + [](const ImageSpec& spec, bool native) { + return spec.pixel_bytes(native); + }, + "native"_a = false) + .def( + "pixel_bytes", + [](const ImageSpec& spec, int chbegin, int chend, bool native) { + return spec.pixel_bytes(chbegin, chend, native); + }, + "chbegin"_a, "chend"_a, "native"_a = false) + .def( + "scanline_bytes", + [](const ImageSpec& spec, bool native) { + return spec.scanline_bytes(native); + }, + "native"_a = false) + .def("scanline_bytes", + [](const ImageSpec& spec, TypeDesc type) { + return spec.scanline_bytes(type); + }) + .def( + "tile_bytes", + [](const ImageSpec& spec, bool native) { + return spec.tile_bytes(native); + }, + "native"_a = false) + .def("tile_bytes", [](const ImageSpec& spec, + TypeDesc type) { return spec.tile_bytes(type); }) + .def( + "image_bytes", + [](const ImageSpec& spec, bool native) { + return spec.image_bytes(native); + }, + "native"_a = false) + .def("image_bytes", + [](const ImageSpec& spec, TypeDesc type) { + return spec.image_bytes(type); + }) + .def("tile_pixels", &ImageSpec::tile_pixels) + .def("image_pixels", &ImageSpec::image_pixels) + .def("size_t_safe", &ImageSpec::size_t_safe) + .def("channelformat", [](const ImageSpec& spec, + int chan) { return spec.channelformat(chan); }) + .def("channel_name", + [](const ImageSpec& spec, int chan) { + return std::string(spec.channel_name(chan)); + }) + .def("channelindex", + [](const ImageSpec& spec, const std::string& name) { + return spec.channelindex(name); + }) + .def("get_channelformats", + [](const ImageSpec& spec) { + return imagespec_get_channelformats(spec, false); + }) + .def("attribute", + [](ImageSpec& spec, const std::string& name, nb::handle obj) { + attribute_onearg(spec, name, obj); + }) + .def("attribute", + [](ImageSpec& spec, const std::string& name, TypeDesc type, + nb::handle obj) { attribute_typed(spec, name, type, obj); }) + .def( + "get_int_attribute", + [](const ImageSpec& spec, const std::string& name, int def) { + return spec.get_int_attribute(name, def); + }, + "name"_a, "defaultval"_a = 0) + .def( + "get_float_attribute", + [](const ImageSpec& spec, const std::string& name, float def) { + return spec.get_float_attribute(name, def); + }, + "name"_a, "defaultval"_a = 0.0f) + .def( + "get_string_attribute", + [](const ImageSpec& spec, const std::string& name, + const std::string& def) { + return std::string(spec.get_string_attribute(name, def)); + }, + "name"_a, "defaultval"_a = "") + .def("getattribute", &imagespec_getattribute_typed, "name"_a, + "type"_a = TypeUnknown) + .def( + "get", + [](const ImageSpec& self, const std::string& key, nb::handle def) { + ParamValue tmpparam; + auto p = self.find_attribute(key, tmpparam); + if (!p) + return nb::borrow(def); + return make_pyobject(p->data(), p->type(), 1, def); + }, + "key"_a, "default"_a = nb::none()) + .def( + "erase_attribute", + [](ImageSpec& spec, const std::string& name, TypeDesc type, + bool casesensitive) { + return spec.erase_attribute(name, type, casesensitive); + }, + "name"_a = "", "type"_a = TypeUnknown, "casesensitive"_a = false) + .def_static( + "metadata_val", + [](const ParamValue& p, bool human) { + return std::string(ImageSpec::metadata_val(p, human)); + }, + "param"_a, "human"_a = false) + .def( + "serialize", + [](const ImageSpec& spec, const std::string& format, + const std::string& verbose) { + ImageSpec::SerialFormat fmt = ImageSpec::SerialText; + if (Strutil::iequals(format, "xml")) + fmt = ImageSpec::SerialXML; + ImageSpec::SerialVerbose verb = ImageSpec::SerialDetailed; + if (Strutil::iequals(verbose, "brief")) + verb = ImageSpec::SerialBrief; + else if (Strutil::iequals(verbose, "detailed")) + verb = ImageSpec::SerialDetailed; + else if (Strutil::iequals(verbose, "detailedhuman")) + verb = ImageSpec::SerialDetailedHuman; + return std::string(spec.serialize(fmt, verb)); + }, + "format"_a = "text", "verbose"_a = "detailed") + .def( + "set_colorspace", + [](ImageSpec& self, const std::string& cs) { + self.set_colorspace(cs); + }, + "name"_a) + .def("__getitem__", + [](const ImageSpec& self, const std::string& key) { + ParamValue tmpparam; + auto p = self.find_attribute(key, tmpparam); + if (p == nullptr) { + std::string message = "key '" + key + "' does not exist"; + throw nb::key_error(message.c_str()); + } + return make_pyobject(p->data(), p->type()); + }) + .def("__setitem__", + [](ImageSpec& self, const std::string& key, nb::handle val) { + delegate_setitem(self, key, val); + }) + .def("__delitem__", + [](ImageSpec& self, const std::string& key) { + self.erase_attribute(key); + }) + .def("__contains__", [](const ImageSpec& self, const std::string& key) { + return self.extra_attribs.contains(key); + }); + + m.def("get_roi", &imagespec_get_roi); + m.def("get_roi_full", &imagespec_get_roi_full); + m.def("set_roi", &imagespec_set_roi); + m.def("set_roi_full", &imagespec_set_roi_full); +} + +} // namespace PyOpenImageIO diff --git a/src/python-nanobind/py_oiio.cpp b/src/python-nanobind/py_oiio.cpp new file mode 100644 index 0000000000..e56b657f66 --- /dev/null +++ b/src/python-nanobind/py_oiio.cpp @@ -0,0 +1,171 @@ +// Copyright Contributors to the OpenImageIO project. +// SPDX-License-Identifier: Apache-2.0 +// https://github.com/AcademySoftwareFoundation/OpenImageIO + +#include "py_oiio.h" + +namespace { + +OIIO_NAMESPACE_USING + +nb::object +oiio_getattribute_typed(const std::string& name, TypeDesc type = TypeUnknown) +{ + if (type == TypeUnknown) + return nb::none(); + char* data = OIIO_ALLOCA(char, type.size()); + if (!OIIO::getattribute(name, type, data)) + return nb::none(); + return PyOpenImageIO::make_pyobject(data, type); +} + + +struct oiio_global_attrib_wrapper { + bool attribute(string_view name, TypeDesc type, const void* data) + { + return OIIO::attribute(name, type, data); + } + bool attribute(string_view name, int val) + { + return OIIO::attribute(name, val); + } + bool attribute(string_view name, float val) + { + return OIIO::attribute(name, val); + } + bool attribute(string_view name, const std::string& val) + { + return OIIO::attribute(name, val); + } +}; + +} // namespace + + +namespace PyOpenImageIO { + +TypeDesc +typedesc_from_python_array_code(string_view code) +{ + TypeDesc t(code); + if (!t.is_unknown()) + return t; + + if (code == "b" || code == "c") + return TypeDesc::INT8; + if (code == "B") + return TypeDesc::UINT8; + if (code == "h") + return TypeDesc::INT16; + if (code == "H") + return TypeDesc::UINT16; + if (code == "i" || code == "l") + return TypeDesc::INT; + if (code == "I" || code == "L") + return TypeDesc::UINT; + if (code == "q") + return TypeDesc::INT64; + if (code == "Q") + return TypeDesc::UINT64; + if (code == "f") + return TypeDesc::FLOAT; + if (code == "d") + return TypeDesc::DOUBLE; + if (code == "float16" || code == "e") + return TypeDesc::HALF; + return TypeDesc::UNKNOWN; +} + + +nb::object +make_pyobject(const void* data, TypeDesc type, int nvalues, + nb::handle defaultvalue) +{ + if (!data || !nvalues) + return nb::borrow(defaultvalue); + if (type.basetype == TypeDesc::INT32) + return C_to_val_or_tuple(static_cast(data), type, nvalues); + if (type.basetype == TypeDesc::FLOAT) + return C_to_val_or_tuple(static_cast(data), type, + nvalues); + if (type.basetype == TypeDesc::STRING) + return C_to_val_or_tuple(static_cast(data), type, + nvalues); + if (type.basetype == TypeDesc::UINT32) + return C_to_val_or_tuple(static_cast(data), type, + nvalues); + if (type.basetype == TypeDesc::INT16) + return C_to_val_or_tuple(static_cast(data), type, + nvalues); + if (type.basetype == TypeDesc::UINT16) + return C_to_val_or_tuple(static_cast(data), type, + nvalues); + if (type.basetype == TypeDesc::INT64) + return C_to_val_or_tuple(static_cast(data), type, + nvalues); + if (type.basetype == TypeDesc::UINT64) + return C_to_val_or_tuple(static_cast(data), type, + nvalues); + if (type.basetype == TypeDesc::DOUBLE) + return C_to_val_or_tuple(static_cast(data), type, + nvalues); + if (type.basetype == TypeDesc::HALF) + return C_to_val_or_tuple(static_cast(data), type, nvalues); + if (type.basetype == TypeDesc::UINT8 && type.arraylen > 0) { + int n = type.arraylen * nvalues; + if (n <= 0) + return nb::borrow(defaultvalue); + auto* copy = new uint8_t[n]; + std::memcpy(copy, data, static_cast(n)); + return make_numpy_array(copy, static_cast(n)); + } + if (type.basetype == TypeDesc::UINT8) { + return C_to_val_or_tuple(static_cast(data), type, + nvalues); + } + return nb::borrow(defaultvalue); +} + +} // namespace PyOpenImageIO + + +NB_MODULE(_OpenImageIO, m) +{ + m.doc() = "OpenImageIO nanobind bindings."; + + PyOpenImageIO::declare_typedesc(m); + PyOpenImageIO::declare_paramvalue(m); + PyOpenImageIO::declare_roi(m); + PyOpenImageIO::declare_imagespec(m); + + m.def("attribute", [](const std::string& name, nb::handle obj) { + oiio_global_attrib_wrapper wrapper; + PyOpenImageIO::attribute_onearg(wrapper, name, obj); + }); + m.def("attribute", + [](const std::string& name, TypeDesc type, nb::handle obj) { + oiio_global_attrib_wrapper wrapper; + PyOpenImageIO::attribute_typed(wrapper, name, type, obj); + }); + m.def( + "get_int_attribute", + [](const std::string& name, int def) { + return OIIO::get_int_attribute(name, def); + }, + "name"_a, "defaultval"_a = 0); + m.def( + "get_float_attribute", + [](const std::string& name, float def) { + return OIIO::get_float_attribute(name, def); + }, + "name"_a, "defaultval"_a = 0.0f); + m.def( + "get_string_attribute", + [](const std::string& name, const std::string& def) { + return std::string(OIIO::get_string_attribute(name, def)); + }, + "name"_a, "defaultval"_a = ""); + m.def("getattribute", &oiio_getattribute_typed, "name"_a, + "type"_a = TypeUnknown); + m.attr("__version__") = OIIO_VERSION_STRING; +} diff --git a/src/python-nanobind/py_oiio.h b/src/python-nanobind/py_oiio.h new file mode 100644 index 0000000000..f33b3df7d9 --- /dev/null +++ b/src/python-nanobind/py_oiio.h @@ -0,0 +1,460 @@ +// Copyright Contributors to the OpenImageIO project. +// SPDX-License-Identifier: Apache-2.0 +// https://github.com/AcademySoftwareFoundation/OpenImageIO + +#pragma once + +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +namespace nb = nanobind; +using namespace nb::literals; + +namespace PyOpenImageIO { + +OIIO_NAMESPACE_USING + +TypeDesc +typedesc_from_python_array_code(string_view code); + +void +declare_roi(nb::module_& m); +void +declare_imagespec(nb::module_& m); +void +declare_typedesc(nb::module_& m); +void +declare_paramvalue(nb::module_& m); + +template struct PyTypeForCType {}; +template<> struct PyTypeForCType { + using type = nb::int_; +}; +template<> struct PyTypeForCType { + using type = nb::int_; +}; +template<> struct PyTypeForCType { + using type = nb::int_; +}; +template<> struct PyTypeForCType { + using type = nb::int_; +}; +template<> struct PyTypeForCType { + using type = nb::int_; +}; +template<> struct PyTypeForCType { + using type = nb::int_; +}; +template<> struct PyTypeForCType { + using type = nb::int_; +}; +template<> struct PyTypeForCType { + using type = nb::int_; +}; +template<> struct PyTypeForCType { + using type = nb::float_; +}; +template<> struct PyTypeForCType { + using type = nb::float_; +}; +template<> struct PyTypeForCType { + using type = nb::float_; +}; +template<> struct PyTypeForCType { + using type = nb::str; +}; +template<> struct PyTypeForCType { + using type = nb::str; +}; + +template +inline bool +py_indexable_pod_to_stdvector(std::vector& vals, const Obj& obj) +{ + bool ok = true; + const size_t length = obj.size(); + vals.clear(); + vals.reserve(length); + for (size_t i = 0; i < length; ++i) { + nb::handle elem = obj[i]; + if constexpr (std::is_same_v) { + if (nb::isinstance(elem)) { + vals.emplace_back(nb::cast(elem)); + } else if (nb::isinstance(elem)) { + vals.emplace_back(static_cast(nb::cast(elem))); + } else { + ok = false; + } + } else if constexpr (std::is_same_v) { + if (nb::isinstance(elem)) { + vals.emplace_back(nb::cast(elem)); + } else { + ok = false; + } + } else if constexpr (std::is_same_v) { + if (nb::isinstance(elem)) { + vals.emplace_back(nb::cast(elem)); + } else { + ok = false; + } + } else if constexpr (std::is_same_v) { + if (nb::isinstance(elem)) { + vals.emplace_back(nb::cast(elem)); + } else { + ok = false; + } + } else { + ok = false; + } + if (!ok) + break; + } + return ok; +} + +template +inline bool +py_indexable_pod_to_stdvector(std::vector& vals, const Obj& obj) +{ + bool ok = true; + const size_t length = obj.size(); + vals.clear(); + vals.reserve(length); + for (size_t i = 0; i < length; ++i) { + nb::handle elem = obj[i]; + if (nb::isinstance(elem)) { + vals.emplace_back(std::string(nb::cast(elem).c_str())); + } else { + ok = false; + break; + } + } + return ok; +} + +template +inline bool +py_indexable_pod_to_stdvector(std::vector& vals, const Obj& obj) +{ + bool ok = true; + const size_t length = obj.size(); + vals.clear(); + vals.reserve(length); + for (size_t i = 0; i < length; ++i) { + nb::handle elem = obj[i]; + if (nb::isinstance(elem)) { + vals.emplace_back(nb::cast(elem)); + } else if (nb::isinstance(elem)) { + vals.emplace_back(TypeDesc(nb::cast(elem))); + } else if (nb::isinstance(elem)) { + vals.emplace_back(TypeDesc(nb::cast(elem).c_str())); + } else { + ok = false; + break; + } + } + return ok; +} + +template +inline bool +py_scalar_pod_to_stdvector(std::vector& vals, const nb::handle& obj) +{ + using pytype = typename PyTypeForCType::type; + vals.clear(); + if (nb::isinstance(obj)) { + vals.emplace_back(nb::cast(obj)); + return true; + } + return false; +} + +template<> +inline bool +py_scalar_pod_to_stdvector(std::vector& vals, const nb::handle& obj) +{ + vals.clear(); + if (nb::isinstance(obj)) { + vals.emplace_back(nb::cast(obj)); + return true; + } + if (nb::isinstance(obj)) { + vals.emplace_back(static_cast(nb::cast(obj))); + return true; + } + return false; +} + +template<> +inline bool +py_scalar_pod_to_stdvector(std::vector& vals, + const nb::handle& obj) +{ + vals.clear(); + if (nb::isinstance(obj)) { + vals.emplace_back(std::string(nb::cast(obj).c_str())); + return true; + } + return false; +} + +template<> +inline bool +py_scalar_pod_to_stdvector(std::vector& vals, const nb::handle& obj) +{ + vals.clear(); + if (nb::isinstance(obj)) { + vals.emplace_back(nb::cast(obj)); + return true; + } + if (nb::isinstance(obj)) { + vals.emplace_back(TypeDesc(nb::cast(obj))); + return true; + } + if (nb::isinstance(obj)) { + vals.emplace_back(TypeDesc(nb::cast(obj).c_str())); + return true; + } + return false; +} + +template +inline bool +py_buffer_to_stdvector(std::vector& vals, const nb::handle& 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; + vals.clear(); + vals.reserve(count); + const unsigned char* data = static_cast(view.buf); + + for (size_t i = 0; ok && i < count; ++i) { + if constexpr (std::is_same_v) { + if (format.basetype == TypeDesc::FLOAT) + vals.emplace_back(reinterpret_cast(data)[i]); + else if (format.basetype == TypeDesc::INT) + vals.emplace_back( + static_cast(reinterpret_cast(data)[i])); + else + ok = false; + } else if constexpr (std::is_same_v) { + if (format.basetype == TypeDesc::INT) + vals.emplace_back(reinterpret_cast(data)[i]); + else + ok = false; + } else if constexpr (std::is_same_v) { + if (format.basetype == TypeDesc::UINT) + vals.emplace_back( + reinterpret_cast(data)[i]); + else + ok = false; + } else if constexpr (std::is_same_v) { + if (format.basetype == TypeDesc::UINT8) + vals.emplace_back( + reinterpret_cast(data)[i]); + else if (format.basetype == TypeDesc::UINT16) + vals.emplace_back(static_cast( + reinterpret_cast(data)[i])); + else + ok = false; + } else { + ok = false; + } + } + + PyBuffer_Release(&view); + return ok; +} + +template<> +inline bool +py_buffer_to_stdvector(std::vector&, const nb::handle&) +{ + return false; +} + +template<> +inline bool +py_buffer_to_stdvector(std::vector&, const nb::handle&) +{ + return false; +} + +template +inline bool +py_to_stdvector(std::vector& vals, const nb::handle& obj) +{ + if (PyTuple_Check(obj.ptr())) + return py_indexable_pod_to_stdvector(vals, nb::borrow(obj)); + if (PyList_Check(obj.ptr())) + return py_indexable_pod_to_stdvector(vals, nb::borrow(obj)); + if (PyObject_CheckBuffer(obj.ptr()) && !PyUnicode_Check(obj.ptr())) + return py_buffer_to_stdvector(vals, obj); + return py_scalar_pod_to_stdvector(vals, obj); +} + +template +inline nb::tuple +C_to_tuple(cspan vals) +{ + nb::list list; + for (size_t i = 0; i < vals.size(); ++i) + list.append(nb::cast(vals[i])); + return nb::steal(PyList_AsTuple(list.ptr())); +} + +template +inline nb::tuple +C_to_tuple(const T* vals, size_t size) +{ + nb::list list; + for (size_t i = 0; i < size; ++i) + list.append(nb::cast(vals[i])); + return nb::steal(PyList_AsTuple(list.ptr())); +} + +template +inline nb::object +C_to_val_or_tuple(const T* vals, TypeDesc type, int nvalues = 1) +{ + OIIO_DASSERT(vals && nvalues); + const size_t n = type.numelements() * type.aggregate * nvalues; + if (n == 1 && !type.arraylen) + return nb::cast(vals[0]); + return C_to_tuple(vals, n); +} + +template +bool +attribute_typed(T& myobj, string_view name, TypeDesc type, const Obj& dataobj) +{ + if (type.basetype == TypeDesc::INT) { + std::vector vals; + bool ok = py_to_stdvector(vals, dataobj); + ok &= (vals.size() == type.numelements() * type.aggregate); + if (ok) + myobj.attribute(name, type, vals.data()); + return ok; + } + if (type.basetype == TypeDesc::UINT) { + std::vector vals; + bool ok = py_to_stdvector(vals, dataobj); + ok &= (vals.size() == type.numelements() * type.aggregate); + if (ok) + myobj.attribute(name, type, vals.data()); + return ok; + } + if (type.basetype == TypeDesc::UINT8) { + std::vector vals; + bool ok = py_to_stdvector(vals, dataobj); + ok &= (vals.size() == type.numelements() * type.aggregate); + if (ok) + myobj.attribute(name, type, vals.data()); + return ok; + } + if (type.basetype == TypeDesc::FLOAT) { + std::vector vals; + bool ok = py_to_stdvector(vals, dataobj); + ok &= (vals.size() == type.numelements() * type.aggregate); + if (ok) + myobj.attribute(name, type, vals.data()); + return ok; + } + if (type.basetype == TypeDesc::STRING) { + std::vector vals; + bool ok = py_to_stdvector(vals, dataobj); + ok &= (vals.size() == type.numelements() * type.aggregate); + if (ok) { + std::vector u; + u.reserve(vals.size()); + for (auto& val : vals) + u.emplace_back(val); + myobj.attribute(name, type, u.data()); + } + return ok; + } + return false; +} + +template +inline void +attribute_onearg(T& myobj, string_view name, const nb::handle& obj) +{ + if (nb::isinstance(obj)) + myobj.attribute(name, nb::cast(obj)); + else if (nb::isinstance(obj)) + myobj.attribute(name, nb::cast(obj)); + else if (nb::isinstance(obj)) + myobj.attribute(name, std::string(nb::cast(obj).c_str())); + else if (nb::isinstance(obj)) { + nb::bytes bytes = nb::cast(obj); + myobj.attribute(name, std::string(bytes.c_str(), bytes.size())); + } else + throw nb::type_error("attribute() value must be int, float, or str"); +} + +template +inline nb::object +make_numpy_array(T* data, size_t size) +{ + nb::capsule owner(data, [](void* p) noexcept { + delete[] reinterpret_cast(p); + }); + nb::ndarray> array(data, { size }, owner); + return nb::cast(std::move(array), nb::rv_policy::move); +} + +nb::object +make_pyobject(const void* data, TypeDesc type, int nvalues = 1, + nb::handle defaultvalue = nb::none()); + +template +inline void +delegate_setitem(C& self, const std::string& key, const nb::handle& obj) +{ + if (nb::isinstance(obj)) + self[key] = nb::cast(obj); + else if (nb::isinstance(obj)) + self[key] = nb::cast(obj); + else if (nb::isinstance(obj)) + self[key] = std::string(nb::cast(obj).c_str()); + else if (nb::isinstance(obj)) { + nb::bytes bytes = nb::cast(obj); + self[key] = std::string(bytes.c_str(), bytes.size()); + } else + throw std::invalid_argument("Bad type for __setitem__"); +} + +} // namespace PyOpenImageIO diff --git a/src/python-nanobind/py_paramvalue.cpp b/src/python-nanobind/py_paramvalue.cpp new file mode 100644 index 0000000000..ce0e50ab86 --- /dev/null +++ b/src/python-nanobind/py_paramvalue.cpp @@ -0,0 +1,229 @@ +// Copyright Contributors to the OpenImageIO project. +// SPDX-License-Identifier: Apache-2.0 +// https://github.com/AcademySoftwareFoundation/OpenImageIO + +#include "py_oiio.h" + +namespace { + +OIIO_NAMESPACE_USING + +template +bool +attribute_typed_nvalues(T& myobj, string_view name, TypeDesc type, int nvalues, + const Obj& dataobj) +{ + if (type.basetype == TypeDesc::INT) { + std::vector vals; + bool ok = PyOpenImageIO::py_to_stdvector(vals, dataobj); + ok &= (vals.size() == type.numelements() * type.aggregate * nvalues); + if (ok) + myobj.attribute(name, type, nvalues, vals.data()); + return ok; + } + if (type.basetype == TypeDesc::UINT) { + std::vector vals; + bool ok = PyOpenImageIO::py_to_stdvector(vals, dataobj); + ok &= (vals.size() == type.numelements() * type.aggregate * nvalues); + if (ok) + myobj.attribute(name, type, nvalues, vals.data()); + return ok; + } + if (type.basetype == TypeDesc::FLOAT) { + std::vector vals; + bool ok = PyOpenImageIO::py_to_stdvector(vals, dataobj); + ok &= (vals.size() == type.numelements() * type.aggregate * nvalues); + if (ok) + myobj.attribute(name, type, nvalues, vals.data()); + return ok; + } + if (type.basetype == TypeDesc::STRING) { + std::vector vals; + bool ok = PyOpenImageIO::py_to_stdvector(vals, dataobj); + ok &= (vals.size() == type.numelements() * type.aggregate * nvalues); + if (ok) { + std::vector converted; + converted.reserve(vals.size()); + for (auto& val : vals) + converted.emplace_back(val); + myobj.attribute(name, type, nvalues, converted.data()); + } + return ok; + } + if (type.basetype == TypeDesc::UINT8) { + std::vector vals; + bool ok = PyOpenImageIO::py_to_stdvector(vals, dataobj); + ok &= (vals.size() == type.numelements() * type.aggregate * nvalues); + if (ok) + myobj.attribute(name, type, nvalues, vals.data()); + return ok; + } + return false; +} + +ParamValue +paramvalue_from_pyobject(string_view name, TypeDesc type, int nvalues, + ParamValue::Interp interp, nb::handle obj) +{ + ParamValue pv; + const size_t expected_size = static_cast( + type.numelements() * type.aggregate * nvalues); + if (type.basetype == TypeDesc::INT) { + std::vector vals; + if (PyOpenImageIO::py_to_stdvector(vals, obj) + && vals.size() >= expected_size) { + pv.init(name, type, nvalues, interp, vals.data()); + } + } else if (type.basetype == TypeDesc::UINT) { + std::vector vals; + if (PyOpenImageIO::py_to_stdvector(vals, obj) + && vals.size() >= expected_size) { + pv.init(name, type, nvalues, interp, vals.data()); + } + } else if (type.basetype == TypeDesc::FLOAT) { + std::vector vals; + if (PyOpenImageIO::py_to_stdvector(vals, obj) + && vals.size() >= expected_size) { + pv.init(name, type, nvalues, interp, vals.data()); + } + } else if (type.basetype == TypeDesc::STRING) { + std::vector vals; + if (PyOpenImageIO::py_to_stdvector(vals, obj) + && vals.size() >= expected_size) { + std::vector converted; + converted.reserve(vals.size()); + for (auto& val : vals) + converted.emplace_back(val); + pv.init(name, type, nvalues, interp, converted.data()); + } + } else if (type.basetype == TypeDesc::UINT8) { + std::vector vals; + if (PyOpenImageIO::py_to_stdvector(vals, obj) + && vals.size() >= expected_size) { + pv.init(name, type, nvalues, interp, vals.data()); + } + } + return pv; +} + +} // namespace + + +namespace PyOpenImageIO { + +void +declare_paramvalue(nb::module_& m) +{ + nb::enum_(m, "Interp") + .value("CONSTANT", ParamValue::INTERP_CONSTANT) + .value("PERPIECE", ParamValue::INTERP_PERPIECE) + .value("LINEAR", ParamValue::INTERP_LINEAR) + .value("VERTEX", ParamValue::INTERP_VERTEX) + .value("INTERP_CONSTANT", ParamValue::INTERP_CONSTANT) + .value("INTERP_PERPIECE", ParamValue::INTERP_PERPIECE) + .value("INTERP_LINEAR", ParamValue::INTERP_LINEAR) + .value("INTERP_VERTEX", ParamValue::INTERP_VERTEX); + + nb::class_(m, "ParamValue") + .def_prop_ro("name", + [](const ParamValue& self) { + return std::string(self.name().string()); + }) + .def_prop_ro("type", [](const ParamValue& self) { return self.type(); }) + .def_prop_ro("value", + [](const ParamValue& self) { + return make_pyobject(self.data(), self.type(), + self.nvalues()); + }) + .def("__len__", &ParamValue::nvalues) + .def(nb::init()) + .def(nb::init()) + .def(nb::init()) + .def("__init__", + [](ParamValue* self, const std::string& name, TypeDesc type, + nb::handle obj) { + new (self) ParamValue(paramvalue_from_pyobject( + name, type, 1, ParamValue::INTERP_CONSTANT, obj)); + }) + .def("__init__", [](ParamValue* self, const std::string& name, + TypeDesc type, int nvalues, + ParamValue::Interp interp, nb::handle obj) { + new (self) ParamValue( + paramvalue_from_pyobject(name, type, nvalues, interp, obj)); + }); + + nb::class_(m, "ParamValueList") + .def(nb::init<>()) + .def("__getitem__", + [](const ParamValueList& self, size_t i) { + if (i >= self.size()) + throw nb::index_error(); + return self[i]; + }) + .def("__getitem__", + [](const ParamValueList& self, const std::string& key) { + auto p = self.find(key); + if (p == self.end()) { + std::string message = "key '" + key + "' does not exist"; + throw nb::key_error(message.c_str()); + } + return make_pyobject(p->data(), p->type()); + }) + .def("__setitem__", + [](ParamValueList& self, const std::string& key, nb::handle val) { + delegate_setitem(self, key, val); + }) + .def("__delitem__", [](ParamValueList& self, + const std::string& key) { self.remove(key); }) + .def("__contains__", + [](const ParamValueList& self, const std::string& key) { + return self.contains(key); + }) + .def("__len__", [](const ParamValueList& self) { return self.size(); }) + .def( + "__iter__", + [](const ParamValueList& self) { + return nb::make_iterator(nb::type(), "iterator", + self.begin(), self.end()); + }, + nb::keep_alive<0, 1>()) + .def("append", [](ParamValueList& self, + const ParamValue& value) { self.push_back(value); }) + .def("clear", &ParamValueList::clear) + .def("free", &ParamValueList::free) + .def("resize", + [](ParamValueList& self, size_t size) { self.resize(size); }) + .def( + "remove", + [](ParamValueList& self, const std::string& name, TypeDesc type, + bool casesensitive) { self.remove(name, type, casesensitive); }, + "name"_a, "type"_a = TypeUnknown, "casesensitive"_a = true) + .def( + "contains", + [](const ParamValueList& self, const std::string& name, + TypeDesc type, bool casesensitive) { + return self.contains(name, type, casesensitive); + }, + "name"_a, "type"_a = TypeUnknown, "casesensitive"_a = true) + .def( + "add_or_replace", + [](ParamValueList& self, const ParamValue& pv, bool casesensitive) { + return self.add_or_replace(pv, casesensitive); + }, + "value"_a, "casesensitive"_a = true) + .def("sort", &ParamValueList::sort, "casesensitive"_a = true) + .def("merge", &ParamValueList::merge, "other"_a, "override"_a = false) + .def("attribute", + [](ParamValueList& self, const std::string& name, nb::handle val) { + attribute_onearg(self, name, val); + }) + .def("attribute", + [](ParamValueList& self, const std::string& name, TypeDesc type, + nb::handle obj) { attribute_typed(self, name, type, obj); }) + .def("attribute", [](ParamValueList& self, const std::string& name, + TypeDesc type, int nvalues, nb::handle obj) { + attribute_typed_nvalues(self, name, type, nvalues, obj); + }); +} + +} // namespace PyOpenImageIO diff --git a/src/python-nanobind/py_roi.cpp b/src/python-nanobind/py_roi.cpp new file mode 100644 index 0000000000..8d85933fb1 --- /dev/null +++ b/src/python-nanobind/py_roi.cpp @@ -0,0 +1,66 @@ +// Copyright Contributors to the OpenImageIO project. +// SPDX-License-Identifier: Apache-2.0 +// https://github.com/AcademySoftwareFoundation/OpenImageIO + +#include "py_oiio.h" + +namespace { + +OIIO_NAMESPACE_USING + +bool +roi_contains_coord(const ROI& roi, int x, int y, int z, int ch) +{ + return roi.contains(x, y, z, ch); +} + + +bool +roi_contains_roi(const ROI& roi, const ROI& other) +{ + return roi.contains(other); +} + +} // namespace + + +namespace PyOpenImageIO { + +void +declare_roi(nb::module_& m) +{ + nb::class_ roi(m, "ROI"); + roi.def_rw("xbegin", &ROI::xbegin) + .def_rw("xend", &ROI::xend) + .def_rw("ybegin", &ROI::ybegin) + .def_rw("yend", &ROI::yend) + .def_rw("zbegin", &ROI::zbegin) + .def_rw("zend", &ROI::zend) + .def_rw("chbegin", &ROI::chbegin) + .def_rw("chend", &ROI::chend) + .def(nb::init<>()) + .def(nb::init()) + .def(nb::init()) + .def(nb::init()) + .def(nb::init()) + .def_prop_ro("defined", &ROI::defined) + .def_prop_ro("width", &ROI::width) + .def_prop_ro("height", &ROI::height) + .def_prop_ro("depth", &ROI::depth) + .def_prop_ro("nchannels", &ROI::nchannels) + .def_prop_ro("npixels", &ROI::npixels) + .def("contains", &roi_contains_coord, "x"_a, "y"_a, "z"_a = 0, + "ch"_a = 0) + .def("contains", &roi_contains_roi, "other"_a) + .def_prop_ro_static("All", [](nb::handle) { return ROI::All(); }) + .def("__str__", + [](const ROI& roi_) { return Strutil::fmt::format("{}", roi_); }) + .def("copy", [](const ROI& self) { return self; }) + .def(nb::self == nb::self) + .def(nb::self != nb::self); + + m.def("union", &roi_union); + m.def("intersection", &roi_intersection); +} + +} // namespace PyOpenImageIO diff --git a/src/python-nanobind/py_typedesc.cpp b/src/python-nanobind/py_typedesc.cpp new file mode 100644 index 0000000000..7196435839 --- /dev/null +++ b/src/python-nanobind/py_typedesc.cpp @@ -0,0 +1,246 @@ +// Copyright Contributors to the OpenImageIO project. +// SPDX-License-Identifier: Apache-2.0 +// https://github.com/AcademySoftwareFoundation/OpenImageIO + +#include "py_oiio.h" + +namespace { + +OIIO_NAMESPACE_USING + +template +void +typedesc_property(TypeDesc& t, Enum value); + +template<> +void +typedesc_property(TypeDesc& t, TypeDesc::BASETYPE value) +{ + t.basetype = value; +} + +template<> +void +typedesc_property(TypeDesc& t, TypeDesc::AGGREGATE value) +{ + t.aggregate = value; +} + +template<> +void +typedesc_property(TypeDesc& t, + TypeDesc::VECSEMANTICS value) +{ + t.vecsemantics = value; +} + +} // namespace + + +namespace PyOpenImageIO { + +void +declare_typedesc(nb::module_& m) +{ + using BASETYPE = TypeDesc::BASETYPE; + using AGGREGATE = TypeDesc::AGGREGATE; + using VECSEMANTICS = TypeDesc::VECSEMANTICS; + + nb::enum_(m, "BASETYPE") + .value("UNKNOWN", TypeDesc::UNKNOWN) + .value("NONE", TypeDesc::NONE) + .value("UCHAR", TypeDesc::UCHAR) + .value("UINT8", TypeDesc::UINT8) + .value("CHAR", TypeDesc::CHAR) + .value("INT8", TypeDesc::INT8) + .value("UINT16", TypeDesc::UINT16) + .value("USHORT", TypeDesc::USHORT) + .value("SHORT", TypeDesc::SHORT) + .value("INT16", TypeDesc::INT16) + .value("UINT", TypeDesc::UINT) + .value("UINT32", TypeDesc::UINT32) + .value("INT", TypeDesc::INT) + .value("INT32", TypeDesc::INT32) + .value("ULONGLONG", TypeDesc::ULONGLONG) + .value("UINT64", TypeDesc::UINT64) + .value("LONGLONG", TypeDesc::LONGLONG) + .value("INT64", TypeDesc::INT64) + .value("HALF", TypeDesc::HALF) + .value("FLOAT", TypeDesc::FLOAT) + .value("DOUBLE", TypeDesc::DOUBLE) + .value("STRING", TypeDesc::STRING) + .value("PTR", TypeDesc::PTR) + .value("LASTBASE", TypeDesc::LASTBASE) + .export_values(); + + nb::enum_(m, "AGGREGATE") + .value("SCALAR", TypeDesc::SCALAR) + .value("VEC2", TypeDesc::VEC2) + .value("VEC3", TypeDesc::VEC3) + .value("VEC4", TypeDesc::VEC4) + .value("MATRIX33", TypeDesc::MATRIX33) + .value("MATRIX44", TypeDesc::MATRIX44) + .export_values(); + + nb::enum_(m, "VECSEMANTICS") + .value("NOXFORM", TypeDesc::NOXFORM) + .value("NOSEMANTICS", TypeDesc::NOSEMANTICS) + .value("COLOR", TypeDesc::COLOR) + .value("POINT", TypeDesc::POINT) + .value("VECTOR", TypeDesc::VECTOR) + .value("NORMAL", TypeDesc::NORMAL) + .value("TIMECODE", TypeDesc::TIMECODE) + .value("KEYCODE", TypeDesc::KEYCODE) + .value("RATIONAL", TypeDesc::RATIONAL) + .value("BOX", TypeDesc::BOX) + .export_values(); + + nb::class_(m, "TypeDesc") + .def_prop_rw( + "basetype", [](TypeDesc t) { return BASETYPE(t.basetype); }, + [](TypeDesc& t, BASETYPE b) { typedesc_property(t, b); }) + .def_prop_rw( + "aggregate", [](TypeDesc t) { return AGGREGATE(t.aggregate); }, + [](TypeDesc& t, AGGREGATE b) { typedesc_property(t, b); }) + .def_prop_rw( + "vecsemantics", + [](TypeDesc t) { return VECSEMANTICS(t.vecsemantics); }, + [](TypeDesc& t, VECSEMANTICS b) { typedesc_property(t, b); }) + .def_rw("arraylen", &TypeDesc::arraylen) + .def(nb::init<>()) + .def(nb::init()) + .def(nb::init()) + .def(nb::init()) + .def(nb::init()) + .def(nb::init()) + .def(nb::init()) + .def("c_str", + [](const TypeDesc& self) { return std::string(self.c_str()); }) + .def("numelements", &TypeDesc::numelements) + .def("basevalues", &TypeDesc::basevalues) + .def("size", &TypeDesc::size) + .def("elementtype", &TypeDesc::elementtype) + .def("elementsize", &TypeDesc::elementsize) + .def("basesize", &TypeDesc::basesize) + .def("fromstring", + [](TypeDesc& t, const char* typestring) { + t.fromstring(typestring); + }) + .def("equivalent", &TypeDesc::equivalent) + .def("unarray", &TypeDesc::unarray) + .def("is_vec2", + [](const TypeDesc& t, BASETYPE b = TypeDesc::FLOAT) { + return t.is_vec2(b); + }) + .def("is_vec3", + [](const TypeDesc& t, BASETYPE b = TypeDesc::FLOAT) { + return t.is_vec3(b); + }) + .def("is_vec4", + [](const TypeDesc& t, BASETYPE b = TypeDesc::FLOAT) { + return t.is_vec4(b); + }) + .def("is_box2", + [](const TypeDesc& t, BASETYPE b = TypeDesc::FLOAT) { + return t.is_box2(b); + }) + .def("is_box3", + [](const TypeDesc& t, BASETYPE b = TypeDesc::FLOAT) { + return t.is_box3(b); + }) + .def_static("all_types_equal", + [](const std::vector& types) { + return TypeDesc::all_types_equal(types); + }) + .def(nb::self == nb::self) + .def(nb::self != nb::self) + .def("__str__", [](TypeDesc t) { return std::string(t.c_str()); }) + .def("__repr__", [](TypeDesc t) { + return Strutil::fmt::format("", t.c_str()); + }); + + nb::implicitly_convertible(); + nb::implicitly_convertible(); + + m.attr("UNKNOWN") = nb::cast(TypeDesc::UNKNOWN); + m.attr("NONE") = nb::cast(TypeDesc::NONE); + m.attr("UCHAR") = nb::cast(TypeDesc::UCHAR); + m.attr("UINT8") = nb::cast(TypeDesc::UINT8); + m.attr("CHAR") = nb::cast(TypeDesc::CHAR); + m.attr("INT8") = nb::cast(TypeDesc::INT8); + m.attr("UINT16") = nb::cast(TypeDesc::UINT16); + m.attr("USHORT") = nb::cast(TypeDesc::USHORT); + m.attr("SHORT") = nb::cast(TypeDesc::SHORT); + m.attr("INT16") = nb::cast(TypeDesc::INT16); + m.attr("UINT") = nb::cast(TypeDesc::UINT); + m.attr("UINT32") = nb::cast(TypeDesc::UINT32); + m.attr("INT") = nb::cast(TypeDesc::INT); + m.attr("INT32") = nb::cast(TypeDesc::INT32); + m.attr("ULONGLONG") = nb::cast(TypeDesc::ULONGLONG); + m.attr("UINT64") = nb::cast(TypeDesc::UINT64); + m.attr("LONGLONG") = nb::cast(TypeDesc::LONGLONG); + m.attr("INT64") = nb::cast(TypeDesc::INT64); + m.attr("HALF") = nb::cast(TypeDesc::HALF); + m.attr("FLOAT") = nb::cast(TypeDesc::FLOAT); + m.attr("DOUBLE") = nb::cast(TypeDesc::DOUBLE); + m.attr("STRING") = nb::cast(TypeDesc::STRING); + m.attr("PTR") = nb::cast(TypeDesc::PTR); + m.attr("LASTBASE") = nb::cast(TypeDesc::LASTBASE); + + m.attr("SCALAR") = nb::cast(TypeDesc::SCALAR); + m.attr("VEC2") = nb::cast(TypeDesc::VEC2); + m.attr("VEC3") = nb::cast(TypeDesc::VEC3); + m.attr("VEC4") = nb::cast(TypeDesc::VEC4); + m.attr("MATRIX33") = nb::cast(TypeDesc::MATRIX33); + m.attr("MATRIX44") = nb::cast(TypeDesc::MATRIX44); + + m.attr("NOXFORM") = nb::cast(TypeDesc::NOXFORM); + m.attr("NOSEMANTICS") = nb::cast(TypeDesc::NOSEMANTICS); + m.attr("COLOR") = nb::cast(TypeDesc::COLOR); + m.attr("POINT") = nb::cast(TypeDesc::POINT); + m.attr("VECTOR") = nb::cast(TypeDesc::VECTOR); + m.attr("NORMAL") = nb::cast(TypeDesc::NORMAL); + m.attr("TIMECODE") = nb::cast(TypeDesc::TIMECODE); + m.attr("KEYCODE") = nb::cast(TypeDesc::KEYCODE); + m.attr("RATIONAL") = nb::cast(TypeDesc::RATIONAL); + m.attr("BOX") = nb::cast(TypeDesc::BOX); + + m.attr("TypeUnknown") = TypeUnknown; + m.attr("TypeFloat") = TypeFloat; + m.attr("TypeColor") = TypeColor; + m.attr("TypePoint") = TypePoint; + m.attr("TypeVector") = TypeVector; + m.attr("TypeNormal") = TypeNormal; + m.attr("TypeString") = TypeString; + m.attr("TypeInt") = TypeInt; + m.attr("TypeUInt") = TypeUInt; + m.attr("TypeInt64") = TypeInt64; + m.attr("TypeUInt64") = TypeUInt64; + m.attr("TypeInt32") = TypeInt32; + m.attr("TypeUInt32") = TypeUInt32; + m.attr("TypeInt16") = TypeInt16; + m.attr("TypeUInt16") = TypeUInt16; + m.attr("TypeInt8") = TypeInt8; + m.attr("TypeUInt8") = TypeUInt8; + m.attr("TypeHalf") = TypeHalf; + m.attr("TypeMatrix") = TypeMatrix; + m.attr("TypeMatrix33") = TypeMatrix33; + m.attr("TypeMatrix44") = TypeMatrix44; + m.attr("TypeTimeCode") = TypeTimeCode; + m.attr("TypeKeyCode") = TypeKeyCode; + m.attr("TypeFloat2") = TypeFloat2; + m.attr("TypeVector2") = TypeVector2; + m.attr("TypeFloat4") = TypeFloat4; + m.attr("TypeVector4") = TypeVector4; + m.attr("TypeVector2i") = TypeVector2i; + m.attr("TypeVector3i") = TypeVector3i; + m.attr("TypeBox2") = TypeBox2; + m.attr("TypeBox3") = TypeBox3; + m.attr("TypeBox2i") = TypeBox2i; + m.attr("TypeBox3i") = TypeBox3i; + m.attr("TypeRational") = TypeRational; + m.attr("TypeURational") = TypeURational; + m.attr("TypePointer") = TypePointer; +} + +} // namespace PyOpenImageIO diff --git a/testsuite/python-roi/ref/out.txt b/testsuite/python-roi/ref/out.txt index a650c974a2..c3a4e10207 100644 --- a/testsuite/python-roi/ref/out.txt +++ b/testsuite/python-roi/ref/out.txt @@ -30,6 +30,10 @@ r contains (10,10) (expect yes): True r contains (1000,10) (expect no): False r contains roi(10,20,10,20,0,1,0,1) (expect yes): True r contains roi(1010,1020,10,20,0,1,0,1) (expect no): False +ROI(0, 10, 0, 10, 2, 4) = 0 10 0 10 2 4 0 10000 +r5 contains (1,1,2,1) (expect yes): True +r5 contains (1,1,1,1) (expect no): False +r5 contains (1,1,2,3) (expect no): False A = 0 10 0 8 0 1 0 4 B = 5 15 -1 10 0 1 0 4 ROI.union(A,B) = 0 15 -1 10 0 1 0 4 diff --git a/testsuite/python-roi/src/test_roi.py b/testsuite/python-roi/src/test_roi.py index 3534e11458..5900ea86dc 100755 --- a/testsuite/python-roi/src/test_roi.py +++ b/testsuite/python-roi/src/test_roi.py @@ -57,6 +57,14 @@ print ("r contains (1000,10) (expect no): ", r.contains(1000,10)) print ("r contains roi(10,20,10,20,0,1,0,1) (expect yes): ", r.contains(oiio.ROI(10,20,10,20,0,1,0,1))) print ("r contains roi(1010,1020,10,20,0,1,0,1) (expect no): ", r.contains(oiio.ROI(1010,1020,10,20,0,1,0,1))) + # Cover the 6-argument ROI constructor and the contains(x, y, z, ch) + # overload with explicit z/channel arguments. + r4 = oiio.ROI (0, 10, 0, 10, 2, 4) + print ("ROI(0, 10, 0, 10, 2, 4) =", r4) + r5 = oiio.ROI (0, 10, 0, 10, 2, 4, 1, 3) + print ("r5 contains (1,1,2,1) (expect yes): ", r5.contains(1,1,2,1)) + print ("r5 contains (1,1,1,1) (expect no): ", r5.contains(1,1,1,1)) + print ("r5 contains (1,1,2,3) (expect no): ", r5.contains(1,1,2,3)) A = oiio.ROI (0, 10, 0, 8, 0, 1, 0, 4) B = oiio.ROI (5, 15, -1, 10, 0, 1, 0, 4) diff --git a/testsuite/python-typedesc/ref/out.txt b/testsuite/python-typedesc/ref/out.txt index 723eb9ba33..0a5f329fe5 100644 --- a/testsuite/python-typedesc/ref/out.txt +++ b/testsuite/python-typedesc/ref/out.txt @@ -159,6 +159,31 @@ equivalent(vector,color) True vector.equivalent(float) False equivalent(vector,float) False +type 'mutated FLOAT, VEC3, COLOR, array of 2' + c_str "color[2]" + basetype BASETYPE.FLOAT + aggregate AGGREGATE.VEC3 + vecsemantics VECSEMANTICS.COLOR + arraylen 2 + str(t) = "color[2]" + size = 24 + elementtype = color + numelements = 2 + basevalues = 6 + elementsize = 12 + basesize = 4 +type 'fromstring('point')' + c_str "point" +after unarray('float[2]') = float +vector is_vec2,is_vec3,is_vec4 = False True False +box2i is_box2,is_box3 = True False +all_types_equal([uint8,uint8]) = True +all_types_equal([uint8,uint16]) = False +repr(TypeFloat) = + +implicit enum ImageSpec roi = 0 8 0 9 0 1 0 3 +implicit str ImageSpec roi = 0 8 0 9 0 1 0 3 + type 'TypeFloat' c_str "float" type 'TypeColor' diff --git a/testsuite/python-typedesc/src/test_typedesc.py b/testsuite/python-typedesc/src/test_typedesc.py index c408b7acb9..1aeee8103e 100755 --- a/testsuite/python-typedesc/src/test_typedesc.py +++ b/testsuite/python-typedesc/src/test_typedesc.py @@ -74,7 +74,7 @@ def vecsemantics_enum_test(): print ("Failed VECSEMANTICS") # print the details of a type t -def breakdown_test(t: oiio.TypeDesc, name="", verbose=True): +def breakdown_test(t, name="", verbose=True): print ("type '%s'" % name) print (" c_str \"" + t.c_str() + "\"") if verbose: @@ -142,6 +142,44 @@ def breakdown_test(t: oiio.TypeDesc, name="", verbose=True): print ("equivalent(vector,float)", oiio.TypeDesc.equivalent(oiio.TypeDesc("vector"), oiio.TypeDesc("float"))) print ("") + # Exercise property mutation and helper methods that are easy to miss in + # binding ports because they are not just plain constructors/accessors. + t_mut = oiio.TypeDesc() + t_mut.basetype = oiio.FLOAT + t_mut.aggregate = oiio.VEC3 + t_mut.vecsemantics = oiio.COLOR + t_mut.arraylen = 2 + breakdown_test (t_mut, "mutated FLOAT, VEC3, COLOR, array of 2") + t_from = oiio.TypeDesc() + t_from.fromstring("point") + breakdown_test (t_from, "fromstring('point')", verbose=False) + t_unarray = oiio.TypeDesc("float[2]") + t_unarray.unarray() + print ("after unarray('float[2]') =", t_unarray) + print ("vector is_vec2,is_vec3,is_vec4 =", + oiio.TypeDesc("vector").is_vec2(oiio.FLOAT), + oiio.TypeDesc("vector").is_vec3(oiio.FLOAT), + oiio.TypeDesc("vector").is_vec4(oiio.FLOAT)) + print ("box2i is_box2,is_box3 =", + oiio.TypeDesc("box2i").is_box2(oiio.INT), + oiio.TypeDesc("box2i").is_box3(oiio.INT)) + print ("all_types_equal([uint8,uint8]) =", + oiio.TypeDesc.all_types_equal([oiio.TypeDesc("uint8"), + oiio.TypeDesc("uint8")])) + print ("all_types_equal([uint8,uint16]) =", + oiio.TypeDesc.all_types_equal([oiio.TypeDesc("uint8"), + oiio.TypeDesc("uint16")])) + print ("repr(TypeFloat) =", repr(oiio.TypeFloat)) + print ("") + + # Exercise implicit conversion paths used by the production pybind11 + # binding: BASETYPE -> TypeDesc and Python str -> TypeDesc. + implicit_enum_spec = oiio.ImageSpec(8, 9, 3, oiio.UINT8) + implicit_str_spec = oiio.ImageSpec(8, 9, 3, "uint8") + print ("implicit enum ImageSpec roi =", implicit_enum_spec.roi) + print ("implicit str ImageSpec roi =", implicit_str_spec.roi) + print ("") + # Test the pre-constructed types breakdown_test (oiio.TypeFloat, "TypeFloat", verbose=False) breakdown_test (oiio.TypeColor, "TypeColor", verbose=False) diff --git a/testsuite/runtest.py b/testsuite/runtest.py index 052b68434a..da0bfb750e 100755 --- a/testsuite/runtest.py +++ b/testsuite/runtest.py @@ -77,8 +77,8 @@ def make_relpath (path: str, start: str=os.curdir) -> str: refdir = "ref/" refdirlist = [ refdir ] mytest = os.path.split(os.path.abspath(os.getcwd()))[-1] -if str(mytest).endswith('.batch') : - mytest = mytest.split('.')[0] +if str(mytest).endswith('.batch') or str(mytest).endswith('.nanobind') : + mytest = mytest.rsplit('.', 1)[0] test_source_dir = os.getenv('OIIO_TESTSUITE_SRC', os.path.join(OIIO_TESTSUITE_ROOT, mytest))