Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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 ()
Expand Down
10 changes: 10 additions & 0 deletions INSTALL.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,9 @@ 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.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.50)
* If you want support for camera "RAW" formats:
Expand Down Expand Up @@ -157,6 +160,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).
Expand Down Expand Up @@ -247,6 +256,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 |
Expand Down
6 changes: 5 additions & 1 deletion src/cmake/externalpackages.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -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 ()


###########################################################################
Expand Down
124 changes: 123 additions & 1 deletion src/cmake/pythonutils.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -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 ()
Expand Down Expand Up @@ -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})
Expand All @@ -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

Expand Down Expand Up @@ -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 ()
27 changes: 27 additions & 0 deletions src/python-nanobind/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# Copyright Contributors to the OpenImageIO project.
# SPDX-License-Identifier: Apache-2.0
# https://github.com/AcademySoftwareFoundation/OpenImageIO

set (nanobind_srcs
py_oiio.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 ()
37 changes: 37 additions & 0 deletions src/python-nanobind/__init__.py
Original file line number Diff line number Diff line change
@@ -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.
78 changes: 78 additions & 0 deletions src/python-nanobind/py_imagespec.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
// 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

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)
{
// This is intentionally not a full ImageSpec port yet. It only exists
// to support ROI parity until py_imagespec.cpp grows into the real
// binding.
nb::class_<ImageSpec>(m, "ImageSpec")
.def(nb::init<>())
.def("__init__",
[](ImageSpec* self, int xres, int yres, int nchans,
const TypeDesc& format) {
new (self) ImageSpec(xres, yres, nchans, format);
})
.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("nchannels", &ImageSpec::nchannels)
.def_prop_ro("roi", &imagespec_get_roi)
.def_prop_ro("roi_full", &imagespec_get_roi_full);

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
16 changes: 16 additions & 0 deletions src/python-nanobind/py_oiio.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// Copyright Contributors to the OpenImageIO project.
// SPDX-License-Identifier: Apache-2.0
// https://github.com/AcademySoftwareFoundation/OpenImageIO

#include "py_oiio.h"


NB_MODULE(_OpenImageIO, m)
{
m.doc() = "OpenImageIO nanobind bindings.";

PyOpenImageIO::declare_typedesc(m);
PyOpenImageIO::declare_roi(m);
PyOpenImageIO::declare_imagespec(m);
m.attr("__version__") = OIIO_VERSION_STRING;
}
Loading
Loading