diff --git a/src/Magnum/CMakeLists.txt b/src/Magnum/CMakeLists.txt index daf72b9..d166fc6 100644 --- a/src/Magnum/CMakeLists.txt +++ b/src/Magnum/CMakeLists.txt @@ -23,6 +23,12 @@ # DEALINGS IN THE SOFTWARE. # +if(WITH_PYTHON) + add_custom_target(MagnumPython SOURCES Python.h) + set_target_properties(MagnumPython PROPERTIES FOLDER "Magnum/Python") + install(FILES Python.h DESTINATION ${MAGNUM_INCLUDE_INSTALL_DIR}) +endif() + find_package(Magnum COMPONENTS SceneGraph) if(Magnum_SceneGraph_FOUND) diff --git a/src/Magnum/Python.h b/src/Magnum/Python.h new file mode 100644 index 0000000..f72a3fe --- /dev/null +++ b/src/Magnum/Python.h @@ -0,0 +1,43 @@ +#ifndef Magnum_Python_h +#define Magnum_Python_h +/* + This file is part of Magnum. + + Copyright © 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019 + Vladimír Vondruš + + Permission is hereby granted, free of charge, to any person obtaining a + copy of this software and associated documentation files (the "Software"), + to deal in the Software without restriction, including without limitation + the rights to use, copy, modify, merge, publish, distribute, sublicense, + and/or sell copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included + in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + DEALINGS IN THE SOFTWARE. +*/ + +#include +#include + +namespace Magnum { + +/* Wrapper for ImageView holding a reference to the memory owner */ +template struct PyImageView: ImageView { + /*implicit*/ PyImageView() noexcept: owner{pybind11::none{}} {} + explicit PyImageView(const ImageView& view, pybind11::object owner) noexcept: ImageView{view}, owner{std::move(owner)} {} + + pybind11::object owner; +}; + +} + +#endif diff --git a/src/python/magnum/__init__.py b/src/python/magnum/__init__.py index 9cd9728..e3f99cf 100644 --- a/src/python/magnum/__init__.py +++ b/src/python/magnum/__init__.py @@ -86,5 +86,7 @@ __all__ = [ 'MeshPrimitive', 'MeshIndexType', - 'PixelFormat', 'PixelStorage' + 'PixelFormat', 'PixelStorage', + 'ImageView1D', 'ImageView2D', 'ImageView3D', + 'MutableImageView1D', 'MutableImageView2D', 'MutableImageView3D' ] diff --git a/src/python/magnum/gl.cpp b/src/python/magnum/gl.cpp index bfa53f5..f2b89cd 100644 --- a/src/python/magnum/gl.cpp +++ b/src/python/magnum/gl.cpp @@ -45,8 +45,6 @@ namespace magnum { void gl(py::module& m) { m.doc() = "OpenGL wrapping layer"; - py::module::import("corrade.containers"); - /* Abstract shader program */ NonDestructible{m, "AbstractShaderProgram", "Base for shader program implementations"}; diff --git a/src/python/magnum/magnum.cpp b/src/python/magnum/magnum.cpp index 90028de..e319db9 100644 --- a/src/python/magnum/magnum.cpp +++ b/src/python/magnum/magnum.cpp @@ -29,6 +29,9 @@ #include #include +#include "Magnum/Python.h" + +#include "corrade/PyArrayView.h" #include "magnum/bootstrap.h" #ifdef MAGNUM_BUILD_STATIC @@ -39,6 +42,69 @@ namespace py = pybind11; namespace magnum { namespace { +template struct PyDimensionTraits; +template struct PyDimensionTraits<1, T> { + typedef T VectorType; + static VectorType from(const Math::Vector<1, T>& vec) { return vec[0]; } +}; +template struct PyDimensionTraits<2, T> { + typedef Math::Vector2 VectorType; + static VectorType from(const Math::Vector<2, T>& vec) { return vec; } +}; +template struct PyDimensionTraits<3, T> { + typedef Math::Vector3 VectorType; + static VectorType from(const Math::Vector<3, T>& vec) { return vec; } +}; + +template void imageView(py::class_& c) { + /* + Missing APIs: + + Type, ErasedType, Dimensions + */ + + c + /* Constructors */ + .def(py::init([](const PixelStorage& storage, PixelFormat format, const typename PyDimensionTraits::VectorType& size, const corrade::PyArrayView& data) { + return T{ImageView{storage, format, size, data}, data.obj}; + }), "Constructor") + .def(py::init([](PixelFormat format, const typename PyDimensionTraits::VectorType& size, const corrade::PyArrayView& data) { + return T{ImageView{format, size, data}, data.obj}; + }), "Constructor") + .def(py::init([](const PixelStorage& storage, PixelFormat format, const typename PyDimensionTraits::VectorType& size) { + return T{ImageView{storage, format, size}, py::none{}}; + }), "Construct an empty view") + .def(py::init([](PixelFormat format, const typename PyDimensionTraits::VectorType& size) { + return T{ImageView{format, size}, py::none{}}; + }), "Construct an empty view") + + /* Properties */ + .def_property_readonly("storage", &T::storage, "Storage of pixel data") + .def_property_readonly("format", &T::format, "Format of pixel data") + .def_property_readonly("pixel_size", &T::pixelSize, "Pixel size (in bytes)") + .def_property_readonly("size", [](T& self) { + return PyDimensionTraits::from(self.size()); + }, "Image size") + .def_property("data", [](T& self) { + return corrade::PyArrayView{{static_cast(self.data().data()), self.data().size()}, self.owner}; + }, [](T& self, const corrade::PyArrayView& data) { + self.setData(data); + self.owner = data.obj; + }, "Image data") + .def_property_readonly("pixels", [](T& self) { + return corrade::PyStridedArrayView{self.pixels(), self.owner}; + }, "View on pixel data") + + .def_readonly("owner", &T::owner, "Memory owner"); +} + +template void imageViewFromMutable(py::class_& c) { + c + .def(py::init([](const PyImageView& other) { + return T{ImageView{other}, other.owner}; + }), "Constructor"); +} + void magnum(py::module& m) { py::enum_{m, "MeshPrimitive", "Mesh primitive type"} .value("POINTS", MeshPrimitive::Points) @@ -120,11 +186,32 @@ void magnum(py::module& m) { &PixelStorage::imageHeight, &PixelStorage::setImageHeight, "Image height") .def_property("skip", &PixelStorage::skip, &PixelStorage::setSkip, "Pixel, row and image skip"); + + py::class_> imageView1D{m, "ImageView1D", "One-dimensional image view"}; + py::class_> imageView2D{m, "ImageView2D", "Two-dimensional image view"}; + py::class_> imageView3D{m, "ImageView3D", "Three-dimensional image view"}; + py::class_> mutableImageView1D{m, "MutableImageView1D", "One-dimensional mutable image view"}; + py::class_> mutableImageView2D{m, "MutableImageView2D", "Two-dimensional mutable image view"}; + py::class_> mutableImageView3D{m, "MutableImageView3D", "Three-dimensional mutable image view"}; + + imageView(imageView1D); + imageView(imageView2D); + imageView(imageView3D); + imageView(mutableImageView1D); + imageView(mutableImageView2D); + imageView(mutableImageView3D); + + imageViewFromMutable(imageView1D); + imageViewFromMutable(imageView2D); + imageViewFromMutable(imageView3D); } }} PYBIND11_MODULE(_magnum, m) { + /* We need ArrayView for images */ + py::module::import("corrade.containers"); + m.doc() = "Root Magnum module"; py::module math = m.def_submodule("math"); diff --git a/src/python/magnum/test/test.py b/src/python/magnum/test/test.py index 5c07e29..2c6c437 100644 --- a/src/python/magnum/test/test.py +++ b/src/python/magnum/test/test.py @@ -23,6 +23,7 @@ # DEALINGS IN THE SOFTWARE. # +import sys import unittest from magnum import * @@ -45,3 +46,121 @@ class PixelStorage_(unittest.TestCase): self.assertEqual(a.row_length, 64) self.assertEqual(a.image_height, 256) self.assertEqual(a.skip, Vector3i(3, 1, 2)) + +class ImageView(unittest.TestCase): + def test_init(self): + # 2x4 RGB pixels, padded for alignment + data = (b'rgbRGB ' + b'abcABC ' + b'defDEF ' + b'ijkIJK ') + data_refcount = sys.getrefcount(data) + + a = ImageView2D(PixelFormat.RGB8UNORM, (2, 4), data) + self.assertEqual(a.storage, PixelStorage()) + self.assertEqual(a.size, Vector2i(2, 4)) + self.assertEqual(a.format, PixelFormat.RGB8UNORM) + self.assertEqual(a.pixel_size, 3) + self.assertEqual(len(a.data), 32) + self.assertIs(a.owner, data) + self.assertEqual(sys.getrefcount(data), data_refcount + 1) + + # Third row, second pixel, green channel + self.assertEqual(a.pixels[2][1][1], 'E') + + # Deleting the original data shouldn't make the image invalid + del data + self.assertEqual(a.pixels[2][1][1], 'E') + + def test_init_storage(self): + # 2x2x2 RGB pixels + data = (b'rgbRGB' + b'abcABC' + b'defDEF' + b'ijkIJK') + data_refcount = sys.getrefcount(data) + + storage = PixelStorage() + storage.alignment = 2 + a = ImageView3D(storage, PixelFormat.RGB8UNORM, (2, 2, 2), data) + self.assertEqual(a.storage.alignment, 2) + self.assertEqual(a.size, Vector3i(2, 2, 2)) + self.assertEqual(len(a.data), 24) + + # Second image, first row, second pixel, green channel + self.assertEqual(a.pixels[1][0][1][1], 'E') + + def test_init_empty(self): + a = MutableImageView1D(PixelFormat.RG16UI, 32) + self.assertEqual(a.storage.alignment, 4) + self.assertEqual(a.size, 32) + self.assertEqual(len(a.data), 0) + self.assertEqual(a.owner, None) + + storage = PixelStorage() + storage.alignment = 2 + b = ImageView2D(storage, PixelFormat.R32F, (8, 8)) + self.assertEqual(b.storage.alignment, 2) + self.assertEqual(b.size, Vector2i(8, 8)) + self.assertEqual(len(b.data), 0) + self.assertEqual(b.owner, None) + + def test_init_mutable(self): + # 2x4 RGB pixels, padded for alignment + data = bytearray(b'rgbRGB ' + b'abcABC ' + b'defDEF ' + b'ijkIJK ') + data_refcount = sys.getrefcount(data) + + a = MutableImageView2D(PixelFormat.RGB8UNORM, (2, 4), data) + self.assertEqual(sys.getrefcount(data), data_refcount + 1) + + a.pixels[1, 1, 1] = '_' + a.pixels[1, 0, 1] = '_' + a.pixels[2, 1, 1] = '_' + a.pixels[2, 0, 1] = '_' + + self.assertEqual(data, b'rgbRGB ' + b'a_cA_C ' + b'd_fD_F ' + b'ijkIJK ') + + # Back to immutable + b = ImageView2D(a) + self.assertEqual(b.size, Vector2i(2, 4)) + self.assertEqual(b.format, PixelFormat.RGB8UNORM) + self.assertEqual(b.pixel_size, 3) + self.assertEqual(len(b.data), 32) + self.assertIs(b.owner, data) + self.assertEqual(sys.getrefcount(data), data_refcount + 2) + + def test_set_data(self): + # 2x4 RGB pixels, padded for alignment + data = (b'rgbRGB ' + b'abcABC ' + b'defDEF ' + b'ijkIJK ') + data_refcount = sys.getrefcount(data) + + a = ImageView2D(PixelFormat.RGB8UNORM, (2, 4), data) + self.assertIs(a.owner, data) + self.assertEqual(sys.getrefcount(data), data_refcount + 1) + + data2 = (b'ijkIJK ' + b'defDEF ' + b'abcABC ' + b'rgbRGB ') + data2_refcount = sys.getrefcount(data2) + + # Replacing the data should disown the original object and point to the + # new one + a.data = data2 + self.assertEqual(bytes(a.data), ( + b'ijkIJK ' + b'defDEF ' + b'abcABC ' + b'rgbRGB ')) + self.assertIs(a.owner, data2) + self.assertEqual(sys.getrefcount(data), data_refcount) + self.assertEqual(sys.getrefcount(data2), data_refcount + 1)