diff --git a/doc/python/conf.py b/doc/python/conf.py index c81fab9..7bbf8ca 100644 --- a/doc/python/conf.py +++ b/doc/python/conf.py @@ -124,7 +124,8 @@ INPUT_DOCS = [ 'magnum.math.rst', 'magnum.platform.rst', 'magnum.scenegraph.rst', - 'magnum.shaders.rst' + 'magnum.shaders.rst', + 'magnum.trade.rst', ] LINKS_NAVBAR2 = [ diff --git a/doc/python/magnum.rst b/doc/python/magnum.rst index d65a2a4..95efa48 100644 --- a/doc/python/magnum.rst +++ b/doc/python/magnum.rst @@ -87,3 +87,39 @@ .. py:class:: magnum.MutableImageView3D See `ImageView2D` for more information. + +.. py:function:: magnum.ImageView1D.__init__(self, arg0: magnum.ImageView1D) + :raise RuntimeError: If `trade.ImageData1D.is_compressed` is :py:`True` + + This function is used to implement implicit conversion from + `trade.ImageData1D` in the `trade` module. + +.. py:function:: magnum.ImageView2D.__init__(self, arg0: magnum.ImageView2D) + :raise RuntimeError: If `trade.ImageData2D.is_compressed` is :py:`True` + + This function is used to implement implicit conversion from + `trade.ImageData2D` in the `trade` module. + +.. py:function:: magnum.ImageView3D.__init__(self, arg0: magnum.ImageView3D) + :raise RuntimeError: If `trade.ImageData3D.is_compressed` is :py:`True` + + This function is used to implement implicit conversion from + `trade.ImageData3D` in the `trade` module. + +.. py:function:: magnum.MutableImageView1D.__init__(self, arg0: magnum.MutableImageView1D) + :raise RuntimeError: If `trade.ImageData1D.is_compressed` is :py:`True` + + This function is used to implement implicit conversion from + `trade.ImageData1D` in the `trade` module. + +.. py:function:: magnum.MutableImageView2D.__init__(self, arg0: magnum.MutableImageView2D) + :raise RuntimeError: If `trade.ImageData2D.is_compressed` is :py:`True` + + This function is used to implement implicit conversion from + `trade.ImageData2D` in the `trade` module. + +.. py:function:: magnum.MutableImageView3D.__init__(self, arg0: magnum.MutableImageView3D) + :raise RuntimeError: If `trade.ImageData3D.is_compressed` is :py:`True` + + This function is used to implement implicit conversion from + `trade.ImageData3D` in the `trade` module. diff --git a/doc/python/magnum.trade.rst b/doc/python/magnum.trade.rst new file mode 100644 index 0000000..30d3a86 --- /dev/null +++ b/doc/python/magnum.trade.rst @@ -0,0 +1,130 @@ +.. + 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. +.. + +.. py:class:: magnum.trade.ImageData1D + + See `ImageData2D` for more information. + +.. py:class:: magnum.trade.ImageData2D + + Similarly to `Image2D`, holds its own data buffer, thus doesn't have an + equivalent to `ImageView2D.owner`. Implicitly convertible to `ImageView2D` + / `MutableImageView2D`, so all APIs consuming image views work with this + type as well. + +.. py:class:: magnum.trade.ImageData3D + + See `ImageData2D` for more information. + +.. py:property:: magnum.trade.ImageData1D.storage + :raise AttributeError: If `is_compressed` is :py:`True` +.. py:property:: magnum.trade.ImageData2D.storage + :raise AttributeError: If `is_compressed` is :py:`True` +.. py:property:: magnum.trade.ImageData3D.storage + :raise AttributeError: If `is_compressed` is :py:`True` + +.. py:property:: magnum.trade.ImageData1D.format + :raise AttributeError: If `is_compressed` is :py:`True` +.. py:property:: magnum.trade.ImageData2D.format + :raise AttributeError: If `is_compressed` is :py:`True` +.. py:property:: magnum.trade.ImageData3D.format + :raise AttributeError: If `is_compressed` is :py:`True` + +.. py:property:: magnum.trade.ImageData1D.pixel_size + :raise AttributeError: If `is_compressed` is :py:`True` +.. py:property:: magnum.trade.ImageData2D.pixel_size + :raise AttributeError: If `is_compressed` is :py:`True` +.. py:property:: magnum.trade.ImageData3D.pixel_size + :raise AttributeError: If `is_compressed` is :py:`True` + +.. py:property:: magnum.trade.ImageData1D.pixels + :raise AttributeError: If `is_compressed` is :py:`True` +.. py:property:: magnum.trade.ImageData2D.pixels + :raise AttributeError: If `is_compressed` is :py:`True` +.. py:property:: magnum.trade.ImageData3D.pixels + :raise AttributeError: If `is_compressed` is :py:`True` + +.. py:class:: magnum.trade.ImporterManager + :summary: Manager for `AbstractImporter` plugin instances + + Each plugin returned by `instantiate()` or `load_and_instantiate()` + references its owning `ImporterManager` through `AbstractImporter.manager`, + ensuring the manager is not deleted before the plugin instances are. + +.. py:class:: magnum.trade.AbstractImporter + + Similarly to C++, importer plugins are loaded through `ImporterManager`: + + .. + >>> from magnum import trade + + .. code:: py + + >>> manager = trade.ImporterManager() + >>> importer = manager.load_and_instantiate('PngImporter') + + Unlike C++, errors in both API usage and file parsing are reported by + raising an exception. See particular function documentation for detailed + behavior. + +.. py:function:: magnum.trade.AbstractImporter.open_data + :raise RuntimeError: If file opening fails + +.. py:function:: magnum.trade.AbstractImporter.open_file + :raise RuntimeError: If file opening fails + +.. py:property:: magnum.trade.AbstractImporter.image1d_count + :raise RuntimeError: If no file is opened +.. py:property:: magnum.trade.AbstractImporter.image2d_count + :raise RuntimeError: If no file is opened +.. py:property:: magnum.trade.AbstractImporter.image3d_count + :raise RuntimeError: If no file is opened + +.. py:function:: magnum.trade.AbstractImporter.image1d_for_name + :raise RuntimeError: If no file is opened +.. py:function:: magnum.trade.AbstractImporter.image2d_for_name + :raise RuntimeError: If no file is opened +.. py:function:: magnum.trade.AbstractImporter.image3d_for_name + :raise RuntimeError: If no file is opened + +.. py:function:: magnum.trade.AbstractImporter.image1d_name + :raise RuntimeError: If no file is opened + :raise ValueError: If :p:`id` is negative or not less than `image1d_count` +.. py:function:: magnum.trade.AbstractImporter.image2d_name + :raise RuntimeError: If no file is opened + :raise ValueError: If :p:`id` is negative or not less than `image2d_count` +.. py:function:: magnum.trade.AbstractImporter.image3d_name + :raise RuntimeError: If no file is opened + :raise ValueError: If :p:`id` is negative or not less than `image3d_count` + +.. py:function:: magnum.trade.AbstractImporter.image1d + :raise RuntimeError: If no file is opened + :raise ValueError: If :p:`id` is negative or not less than `image1d_count` +.. py:function:: magnum.trade.AbstractImporter.image2d + :raise RuntimeError: If no file is opened + :raise ValueError: If :p:`id` is negative or not less than `image2d_count` +.. py:function:: magnum.trade.AbstractImporter.image3d + :raise RuntimeError: If no file is opened + :raise ValueError: If :p:`id` is negative or not less than `image3d_count` diff --git a/src/python/corrade/pluginmanager.h b/src/python/corrade/pluginmanager.h new file mode 100644 index 0000000..9972611 --- /dev/null +++ b/src/python/corrade/pluginmanager.h @@ -0,0 +1,109 @@ +#ifndef corrade_pluginmanager_h +#define corrade_pluginmanager_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 +#include + +#include "Corrade/Python.h" + +#include "corrade/bootstrap.h" + +namespace Corrade { namespace PluginManager { + +/* Stores additional stuff needed for proper refcounting of array views. Due + to obvious reasons we can't subclass plugins so this is the only possible + way. */ +template struct PyPluginHolder: std::unique_ptr { + explicit PyPluginHolder(T*) { + /* Pybind needs this signature, but it should never be called */ + CORRADE_ASSERT_UNREACHABLE(); + } + + explicit PyPluginHolder(T* object, pybind11::object manager) noexcept: std::unique_ptr{object}, manager{std::move(manager)} {} + + PyPluginHolder(PyPluginHolder&&) noexcept = default; + PyPluginHolder(const PyPluginHolder&) = delete; + PyPluginHolder& operator=(PyPluginHolder&&) noexcept = default; + PyPluginHolder& operator=(const PyPluginHolder&) = default; + + ~PyPluginHolder() { + /* On destruction, first `manager` and then the plugin would be + destroyed, which would mean it asserts due to the manager being + destructed while plugins are still around. To flip the order, we + need to reset the pointer first */ + std::unique_ptr::reset(); + } + + pybind11::object manager; +}; + +}} + +PYBIND11_DECLARE_HOLDER_TYPE(T, Corrade::PluginManager::PyPluginHolder) + +namespace corrade { + +template void plugin(py::class_>& c) { + c + .def_property_readonly("manager", [](const T& self) { + return pyObjectHolderFor(self).manager; + }, "Manager owning this plugin instance"); +} + +template void manager(py::class_, PluginManager::AbstractManager>& c) { + c + .def(py::init(), py::arg("plugin_directory") = std::string{}, "Constructor") + .def("instantiate", [](PluginManager::Manager& self, const std::string& plugin) { + /* This causes a double lookup, but well... better than dying */ + if(!(self.loadState(plugin) & PluginManager::LoadState::Loaded)) { + PyErr_Format(PyExc_RuntimeError, "plugin %s is not loaded", plugin.data()); + throw py::error_already_set{}; + } + + auto loaded = self.instantiate(plugin); + if(!loaded) { + PyErr_Format(PyExc_RuntimeError, "can't instantiate plugin %s", plugin.data()); + throw py::error_already_set{}; + } + + return PluginManager::PyPluginHolder{loaded.release(), py::cast(self)}; + }) + .def("load_and_instantiate", [](PluginManager::Manager& self, const std::string& plugin) { + auto loaded = self.loadAndInstantiate(plugin); + if(!loaded) { + PyErr_Format(PyExc_RuntimeError, "can't load and instantiate plugin %s", plugin.data()); + throw py::error_already_set{}; + } + + return PluginManager::PyPluginHolder{loaded.release(), py::cast(self)}; + }); +} + +} + +#endif diff --git a/src/python/magnum/CMakeLists.txt b/src/python/magnum/CMakeLists.txt index 6bf30bf..12b5a86 100644 --- a/src/python/magnum/CMakeLists.txt +++ b/src/python/magnum/CMakeLists.txt @@ -131,7 +131,9 @@ if(NOT MAGNUM_BUILD_STATIC) if(Magnum_Trade_FOUND) pybind11_add_module(magnum_trade SYSTEM ${magnum_trade_SRCS}) - target_include_directories(magnum_trade PRIVATE ${PROJECT_SOURCE_DIR}/src/python) + target_include_directories(magnum_trade PRIVATE + ${PROJECT_SOURCE_DIR}/src + ${PROJECT_SOURCE_DIR}/src/python) target_link_libraries(magnum_trade PRIVATE Magnum::Trade) set_target_properties(magnum_trade PROPERTIES FOLDER "python" diff --git a/src/python/magnum/magnum.cpp b/src/python/magnum/magnum.cpp index a9ca707..fa24922 100644 --- a/src/python/magnum/magnum.cpp +++ b/src/python/magnum/magnum.cpp @@ -117,6 +117,9 @@ template void imageView(py::class_>& c) { .def(py::init([](Image& image) { return pyImageViewHolder(T{image}, image.data() ? py::cast(image) : py::none{}); }), "Construct a view on an image") + .def(py::init([](const ImageView& other) { + return pyImageViewHolder(ImageView(other), pyObjectHolderFor(other).owner); + }), "Construct from any type convertible to an image view") /* Properties */ .def_property_readonly("storage", &T::storage, "Storage of pixel data") @@ -147,7 +150,7 @@ template void imageViewFromMutable(py::class_>& c .def(py::init([](const BasicMutableImageView& other) { return pyImageViewHolder(BasicImageView(other), pyObjectHolderFor(other).owner); - }), "Constructor"); + }), "Construct from a mutable view"); } void magnum(py::module& m) { diff --git a/src/python/magnum/test/rgb.png b/src/python/magnum/test/rgb.png new file mode 100644 index 0000000..cacab70 Binary files /dev/null and b/src/python/magnum/test/rgb.png differ diff --git a/src/python/magnum/test/rgba_dxt1.dds b/src/python/magnum/test/rgba_dxt1.dds new file mode 100644 index 0000000..903752a Binary files /dev/null and b/src/python/magnum/test/rgba_dxt1.dds differ diff --git a/src/python/magnum/test/test_trade.py b/src/python/magnum/test/test_trade.py index a70215f..6360c1f 100644 --- a/src/python/magnum/test/test_trade.py +++ b/src/python/magnum/test/test_trade.py @@ -23,12 +23,182 @@ # DEALINGS IN THE SOFTWARE. # +import os +import sys import unittest +from corrade import pluginmanager from magnum import * from magnum import trade +class ImageData(unittest.TestCase): + def test(self): + # The only way to get an image instance is through a manager + importer = trade.ImporterManager().load_and_instantiate('StbImageImporter') + importer.open_file(os.path.join(os.path.dirname(__file__), "rgb.png")) + image = importer.image2d(0) + self.assertFalse(image.is_compressed) + self.assertEqual(image.storage.alignment, 1) # libPNG has 4 tho + self.assertEqual(image.format, PixelFormat.RGB8_UNORM) + self.assertEqual(image.pixel_size, 3) + self.assertEqual(image.size, Vector2i(3, 2)) + # TODO: ugh, report as bytes, not chars + self.assertEqual(ord(image.pixels[1, 2, 2]), 181) + self.assertEqual(ord(image.data[9 + 6 + 2]), 181) # libPNG has 12 + + + def test_compressed(self): + # The only way to get an image instance is through a manager + importer = trade.ImporterManager().load_and_instantiate('DdsImporter') + importer.open_file(os.path.join(os.path.dirname(__file__), "rgba_dxt1.dds")) + image = importer.image2d(0) + self.assertEqual(len(image.data), 8) + self.assertTrue(image.is_compressed) + # TODO: compressed properties + + # No compressed-image-related APIs exposed ATM, so just verifying the + # uncompressed ones fail properly + with self.assertRaisesRegex(AttributeError, "image is compressed"): + image.storage + with self.assertRaisesRegex(AttributeError, "image is compressed"): + image.format + with self.assertRaisesRegex(AttributeError, "image is compressed"): + image.pixel_size + with self.assertRaisesRegex(AttributeError, "image is compressed"): + image.pixels + + def test_convert_view(self): + # The only way to get an image instance is through a manager + importer = trade.ImporterManager().load_and_instantiate('StbImageImporter') + importer.open_file(os.path.join(os.path.dirname(__file__), "rgb.png")) + image = importer.image2d(0) + + view = ImageView2D(image) + mutable_view = MutableImageView2D(image) + + def test_convert_view_compressed(self): + # The only way to get an image instance is through a manager + importer = trade.ImporterManager().load_and_instantiate('DdsImporter') + importer.open_file(os.path.join(os.path.dirname(__file__), "rgba_dxt1.dds")) + image = importer.image2d(0) + + # No compressed-image-related APIs exposed ATM, so just verifying the + # uncompressed ones fail properly + with self.assertRaisesRegex(RuntimeError, "image is compressed"): + view = ImageView2D(image) + with self.assertRaisesRegex(RuntimeError, "image is compressed"): + mutable_view = MutableImageView2D(image) + class MeshData(unittest.TestCase): def test_init(self): # Well this doesn't do much but well a = trade.MeshData2D + +class Importer(unittest.TestCase): + def test(self): + manager = trade.ImporterManager() + self.assertIn('StbImageImporter', manager.alias_list) + self.assertEqual(manager.load_state('StbImageImporter'), pluginmanager.LoadState.NOT_LOADED) + + self.assertTrue(manager.load('StbImageImporter') & pluginmanager.LoadState.LOADED) + self.assertEqual(manager.unload('StbImageImporter'), pluginmanager.LoadState.NOT_LOADED) + + with self.assertRaisesRegex(RuntimeError, "can't load plugin"): + manager.load('NonexistentImporter') + with self.assertRaisesRegex(RuntimeError, "can't unload plugin"): + manager.unload('NonexistentImporter') + + def test_no_file_opened(self): + importer = trade.ImporterManager().load_and_instantiate('StbImageImporter') + self.assertFalse(importer.is_opened) + + with self.assertRaisesRegex(RuntimeError, "no file opened"): + importer.image1d_count + with self.assertRaisesRegex(RuntimeError, "no file opened"): + importer.image2d_count + with self.assertRaisesRegex(RuntimeError, "no file opened"): + importer.image3d_count + + with self.assertRaisesRegex(RuntimeError, "no file opened"): + importer.image1d_for_name('') + with self.assertRaisesRegex(RuntimeError, "no file opened"): + importer.image2d_for_name('') + with self.assertRaisesRegex(RuntimeError, "no file opened"): + importer.image3d_for_name('') + + with self.assertRaisesRegex(RuntimeError, "no file opened"): + importer.image1d_name(0) + with self.assertRaisesRegex(RuntimeError, "no file opened"): + importer.image2d_name(0) + with self.assertRaisesRegex(RuntimeError, "no file opened"): + importer.image3d_name(0) + + with self.assertRaisesRegex(RuntimeError, "no file opened"): + importer.image1d(0) + with self.assertRaisesRegex(RuntimeError, "no file opened"): + importer.image2d(0) + with self.assertRaisesRegex(RuntimeError, "no file opened"): + importer.image3d(0) + + def test_index_oob(self): + importer = trade.ImporterManager().load_and_instantiate('StbImageImporter') + importer.open_file(os.path.join(os.path.dirname(__file__), 'rgb.png')) + + with self.assertRaises(IndexError): + importer.image1d_name(0) + with self.assertRaises(IndexError): + importer.image2d_name(1) + with self.assertRaises(IndexError): + importer.image3d_name(0) + + with self.assertRaises(IndexError): + importer.image1d(0) + with self.assertRaises(IndexError): + importer.image2d(1) + with self.assertRaises(IndexError): + importer.image3d(0) + + def test_open_failed(self): + importer = trade.ImporterManager().load_and_instantiate('StbImageImporter') + + with self.assertRaisesRegex(RuntimeError, "opening nonexistent.png failed"): + importer.open_file('nonexistent.png') + with self.assertRaisesRegex(RuntimeError, "opening data failed"): + importer.open_data(b'') + + def test_image2d(self): + manager = trade.ImporterManager() + manager_refcount = sys.getrefcount(manager) + + # Importer references the manager to ensure it doesn't get GC'd before + # the plugin instances + importer = manager.load_and_instantiate('StbImageImporter') + self.assertIs(importer.manager, manager) + self.assertEqual(sys.getrefcount(manager), manager_refcount + 1) + + importer.open_file(os.path.join(os.path.dirname(__file__), 'rgb.png')) + self.assertEqual(importer.image2d_count, 1) + self.assertEqual(importer.image2d_name(0), '') + self.assertEqual(importer.image2d_for_name(''), -1) + + image = importer.image2d(0) + self.assertEqual(image.size, Vector2i(3, 2)) + + # Deleting the importer should decrease manager refcount again + del importer + self.assertEqual(sys.getrefcount(manager), manager_refcount) + + def test_image2d_data(self): + importer = trade.ImporterManager().load_and_instantiate('StbImageImporter') + + with open(os.path.join(os.path.dirname(__file__), "rgb.png"), 'rb') as f: + importer.open_data(f.read()) + + image = importer.image2d(0) + self.assertEqual(image.size, Vector2i(3, 2)) + + def test_image2d_failed(self): + importer = trade.ImporterManager().load_and_instantiate('StbImageImporter') + importer.open_data(b'bla') + + with self.assertRaisesRegex(RuntimeError, "import failed"): + image = importer.image2d(0) diff --git a/src/python/magnum/trade.cpp b/src/python/magnum/trade.cpp index badf451..b36551e 100644 --- a/src/python/magnum/trade.cpp +++ b/src/python/magnum/trade.cpp @@ -24,30 +24,242 @@ */ #include +#include +#include +#include +#include +#include #include #include +#include "Corrade/Containers/Python.h" +#include "Magnum/Python.h" + +#include "corrade/pluginmanager.h" #include "magnum/bootstrap.h" namespace magnum { namespace { +template PyObject* implicitlyConvertibleToImageView(PyObject* obj, PyTypeObject*) { + py::detail::make_caster> caster; + if(!caster.load(obj, false)) { + return nullptr; + } + + Trade::ImageData& data = caster; + if(data.isCompressed()) { + PyErr_SetString(PyExc_RuntimeError, "image is compressed"); + throw py::error_already_set{}; + } + + auto r = pyCastButNotShitty(pyImageViewHolder(ImageView(data), py::reinterpret_borrow(obj))).release().ptr(); + return r; +} + +template void imageData(py::class_>& c) { + /* + Missing APIs: + + Dimensions + */ + + /* These two are quite hacky attempts to bring the ImageData -> ImageView + conversion operator functionality here. Using py::implicitly_convertible + alone doesn't work as it only calls conversion constructors exposed to + Python, and we can't expose such a thing to Python because ImageView is + defined in the `magnum` module while this is `magnum.trade`, and that + would mean a cyclic dependency. + + Instead, I took the guts of py::implicitly_convertible and instead of + calling into Python I'm calling the C++ conversion operator directly + myself. That alone is not enough, as this implicit conversion is only + chosen if the target type has a Python-exposed constructor that takes a + type that's implicitly convertible from the source type. Ugh. + + If this ever breaks with a pybind update, I'm probably going to + reimplement this in a pure duck-typed fashion. I hope not tho. */ + { + auto tinfo = py::detail::get_type_info(typeid(ImageView)); + CORRADE_INTERNAL_ASSERT(tinfo); + tinfo->implicit_conversions.push_back(implicitlyConvertibleToImageView); + } { + auto tinfo = py::detail::get_type_info(typeid(ImageView)); + CORRADE_INTERNAL_ASSERT(tinfo); + tinfo->implicit_conversions.push_back(implicitlyConvertibleToImageView); + } + + c + /* There are no constructors at the moment --- expecting those types + get only created by importers. (It would also need the Array type + and movability figured out, postponing that to later.) */ + + /* Properties */ + .def_property_readonly("is_compressed", &Trade::ImageData::isCompressed, "Whether the image is compressed") + .def_property_readonly("storage", [](Trade::ImageData& self) { + if(self.isCompressed()) { + PyErr_SetString(PyExc_AttributeError, "image is compressed"); + throw py::error_already_set{}; + } + + return self.storage(); + }, "Storage of pixel data") + .def_property_readonly("format", [](Trade::ImageData& self) { + if(self.isCompressed()) { + PyErr_SetString(PyExc_AttributeError, "image is compressed"); + throw py::error_already_set{}; + } + + return self.format(); + }, "Format of pixel data") + .def_property_readonly("pixel_size", [](Trade::ImageData& self) { + if(self.isCompressed()) { + PyErr_SetString(PyExc_AttributeError, "image is compressed"); + throw py::error_already_set{}; + } + + return self.pixelSize(); + }, "Pixel size (in bytes)") + .def_property_readonly("size", [](Trade::ImageData& self) { + return PyDimensionTraits::from(self.size()); + }, "Image size") + .def_property_readonly("data", [](Trade::ImageData& self) { + return Containers::pyArrayViewHolder(self.data(), py::cast(self)); + }, "Image data") + .def_property_readonly("pixels", [](Trade::ImageData& self) { + if(self.isCompressed()) { + PyErr_SetString(PyExc_AttributeError, "image is compressed"); + throw py::error_already_set{}; + } + + return Containers::pyArrayViewHolder(self.pixels(), py::cast(self)); + }, "View on pixel data"); +} + template void meshData(py::class_& c) { c .def_property_readonly("primitive", &T::primitive, "Primitive") .def("is_indexed", &T::isIndexed, "Whether the mesh is indexed"); } +/* For some reason having ...Args as the second (and not last) template + argument does not work. So I'm listing all variants here ... which are + exactly two, in fact. */ +template R checkOpened(Trade::AbstractImporter& self) { + if(!self.isOpened()) { + PyErr_SetString(PyExc_RuntimeError, "no file opened"); + throw py::error_already_set{}; + } + return (self.*f)(); +} +template R checkOpened(Trade::AbstractImporter& self, Arg1 arg1) { + if(!self.isOpened()) { + PyErr_SetString(PyExc_RuntimeError, "no file opened"); + throw py::error_already_set{}; + } + return (self.*f)(arg1); +} + +template R checkOpenedBounds(Trade::AbstractImporter& self, UnsignedInt id) { + if(!self.isOpened()) { + PyErr_SetString(PyExc_RuntimeError, "no file opened"); + throw py::error_already_set{}; + } + + if(id >= (self.*bounds)()) { + PyErr_SetNone(PyExc_IndexError); + throw py::error_already_set{}; + } + + return (self.*f)(id); +} + +template(Trade::AbstractImporter::*f)(UnsignedInt), UnsignedInt(Trade::AbstractImporter::*bounds)() const> R checkOpenedBoundsResult(Trade::AbstractImporter& self, UnsignedInt id) { + if(!self.isOpened()) { + PyErr_SetString(PyExc_RuntimeError, "no file opened"); + throw py::error_already_set{}; + } + + if(id >= (self.*bounds)()) { + PyErr_SetNone(PyExc_IndexError); + throw py::error_already_set{}; + } + + /** @todo log redirection -- but we'd need assertions to not be part of + that so when it dies, the user can still see why */ + Containers::Optional out = (self.*f)(id); + if(!out) { + PyErr_SetString(PyExc_RuntimeError, "import failed"); + throw py::error_already_set{}; + } + + return *std::move(out); +} + } void trade(py::module& m) { m.doc() = "Data format exchange"; + /* AbstractImporter depends on this */ + py::module::import("corrade.pluginmanager"); + py::class_ meshData2D{m, "MeshData2D", "Two-dimensional mesh data"}; py::class_ meshData3D{m, "MeshData3D", "Three-dimensional mesh data"}; meshData(meshData2D); meshData(meshData3D); + + py::class_ imageData1D{m, "ImageData1D", "One-dimensional image data"}; + py::class_ imageData2D{m, "ImageData2D", "Two-dimensional image data"}; + py::class_ imageData3D{m, "ImageData3D", "Three-dimensional image data"}; + imageData(imageData1D); + imageData(imageData2D); + imageData(imageData3D); + + /* Importer. Skipping file callbacks and openState as those operate with + void*. Leaving the name as AbstractImporter (instead of Importer) to + avoid needless name differences and because in the future there *might* + be pure Python importers (not now tho). */ + py::class_> abstractImporter{m, "AbstractImporter", "Interface for importer plugins"}; + corrade::plugin(abstractImporter); + abstractImporter + /** @todo features (once moved outside of the importer) */ + .def_property_readonly("is_opened", &Trade::AbstractImporter::isOpened, "Whether any file is opened") + .def("open_data", [](Trade::AbstractImporter& self, Containers::ArrayView data) { + /** @todo log redirection -- but we'd need assertions to not be + part of that so when it dies, the user can still see why */ + if(self.openData(data)) return; + + PyErr_SetString(PyExc_RuntimeError, "opening data failed"); + throw py::error_already_set{}; + }, "Open raw data", py::arg("data")) + .def("open_file", [](Trade::AbstractImporter& self, const std::string& filename) { + /** @todo log redirection -- but we'd need assertions to not be + part of that so when it dies, the user can still see why */ + if(self.openFile(filename)) return; + + PyErr_Format(PyExc_RuntimeError, "opening %s failed", filename.data()); + throw py::error_already_set{}; + }, "Open a file", py::arg("filename")) + .def("close", &Trade::AbstractImporter::close, "Close currently opened file") + + /** @todo all other data types */ + .def_property_readonly("image1d_count", checkOpened, "One-dimensional image count") + .def_property_readonly("image2d_count", checkOpened, "Two-dimensional image count") + .def_property_readonly("image3d_count", checkOpened, "Three-dimensional image count") + .def("image1d_for_name", checkOpened, "One-dimensional image ID for given name") + .def("image2d_for_name", checkOpened, "Two-dimensional image ID for given name") + .def("image3d_for_name", checkOpened, "Three-dimensional image ID for given name") + .def("image1d_name", checkOpenedBounds, "One-dimensional image name", py::arg("id")) + .def("image2d_name", checkOpenedBounds, "Two-dimensional image name", py::arg("id")) + .def("image3d_name", checkOpenedBounds, "Three-dimensional image name", py::arg("id")) + .def("image1d", checkOpenedBoundsResult, "One-dimensional image", py::arg("id")) + .def("image2d", checkOpenedBoundsResult, "Two-dimensional image", py::arg("id")) + .def("image3d", checkOpenedBoundsResult, "Three-dimensional image", py::arg("id")); + + py::class_, PluginManager::AbstractManager> importerManager{m, "ImporterManager", "Plugin manager for importer plugins"}; + corrade::manager(importerManager); } }