From e6fdfd7d8ebbb57b505b8875f50a3afbff7fd9e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vladim=C3=ADr=20Vondru=C5=A1?= Date: Fri, 2 Sep 2022 18:39:01 +0200 Subject: [PATCH] python: expose the essentials from the Text library. Everything needed to make a Python variant of the Text example. The Oxygen.ttf font file is the same as in MagnumPlugins/FreeTypeFont/Test in the magnum-plugins repository. --- doc/python/conf.py | 4 +- doc/python/magnum.text.rst | 69 +++++++++ doc/python/pages/changelog.rst | 1 + src/python/CMakeLists.txt | 1 + src/python/magnum/CMakeLists.txt | 20 +++ src/python/magnum/__init__.py | 2 +- src/python/magnum/bootstrap.h | 1 + src/python/magnum/magnum.cpp | 5 + src/python/magnum/staticconfigure.h.cmake | 1 + src/python/magnum/test/Oxygen.ttf | Bin 0 -> 39728 bytes src/python/magnum/test/test_text.py | 104 +++++++++++++ src/python/magnum/test/test_text_gl.py | 75 +++++++++ src/python/magnum/text.cpp | 181 ++++++++++++++++++++++ src/python/setup.py.cmake | 1 + 14 files changed, 463 insertions(+), 2 deletions(-) create mode 100644 doc/python/magnum.text.rst create mode 100644 src/python/magnum/test/Oxygen.ttf create mode 100644 src/python/magnum/test/test_text.py create mode 100644 src/python/magnum/test/test_text_gl.py create mode 100644 src/python/magnum/text.cpp diff --git a/doc/python/conf.py b/doc/python/conf.py index 0e63b78..0e9631f 100644 --- a/doc/python/conf.py +++ b/doc/python/conf.py @@ -19,12 +19,13 @@ import magnum.platform.sdl2 import magnum.primitives import magnum.shaders import magnum.scenegraph +import magnum.text import magnum.trade # So the doc see everything # TODO: use just +=, m.css should reorder this on its own corrade.__all__ = ['containers', 'pluginmanager', 'BUILD_STATIC', 'BUILD_MULTITHREADED', 'TARGET_UNIX', 'TARGET_APPLE', 'TARGET_IOS', 'TARGET_IOS_SIMULATOR', 'TARGET_WINDOWS', 'TARGET_WINDOWS_RT', 'TARGET_EMSCRIPTEN', 'TARGET_ANDROID'] -magnum.__all__ = ['math', 'gl', 'meshtools', 'platform', 'primitives', 'shaders', 'scenegraph', 'trade', 'BUILD_STATIC', 'TARGET_GL', 'TARGET_GLES', 'TARGET_GLES2', 'TARGET_WEBGL', 'TARGET_VK'] + magnum.__all__ +magnum.__all__ = ['math', 'gl', 'meshtools', 'platform', 'primitives', 'shaders', 'scenegraph', 'text', 'trade', 'BUILD_STATIC', 'TARGET_GL', 'TARGET_GLES', 'TARGET_GLES2', 'TARGET_WEBGL', 'TARGET_VK'] + magnum.__all__ # hide values of the preprocessor defines to avoid confusion by assigning a # class without __repr__ to them @@ -126,6 +127,7 @@ INPUT_DOCS = [ 'magnum.platform.rst', 'magnum.scenegraph.rst', 'magnum.shaders.rst', + 'magnum.text.rst', 'magnum.trade.rst', ] diff --git a/doc/python/magnum.text.rst b/doc/python/magnum.text.rst new file mode 100644 index 0000000..5488806 --- /dev/null +++ b/doc/python/magnum.text.rst @@ -0,0 +1,69 @@ +.. + This file is part of Magnum. + + Copyright © 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019, + 2020, 2021, 2022 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.text.FontManager + :summary: Manager for :ref:`AbstractFont` plugin instances + + Each plugin returned by :ref:`instantiate()` or :ref:`load_and_instantiate()` + references its owning :ref:`FontManager` through + :ref:`AbstractFont.manager`, ensuring the manager is not deleted before the + plugin instances are. + +.. py:class:: magnum.text.AbstractFont + + Similarly to C++, font plugins are loaded through :ref:`FontManager`: + + .. + >>> from magnum import text + + .. code:: py + + >>> manager = text.FontManager() + >>> font = manager.load_and_instantiate('StbTrueTypeFont') + + 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.text.AbstractFont.open_data + :raise RuntimeError: If file opening fails + +.. py:function:: magnum.text.AbstractFont.open_file + :raise RuntimeError: If file opening fails + +.. py:property:: magnum.text.AbstractFont.size + :raise AssertionError: If no file is opened +.. py:property:: magnum.text.AbstractFont.ascent + :raise AssertionError: If no file is opened +.. py:property:: magnum.text.AbstractFont.descent + :raise AssertionError: If no file is opened +.. py:property:: magnum.text.AbstractFont.line_height + :raise AssertionError: If no file is opened +.. py:function:: magnum.text.AbstractFont.glyph_id + :raise AssertionError: If no file is opened +.. py:function:: magnum.text.AbstractFont.glyph_advance + :raise AssertionError: If no file is opened +.. py:function:: magnum.text.AbstractFont.fill_glyph_cache + :raise AssertionError: If no file is opened diff --git a/doc/python/pages/changelog.rst b/doc/python/pages/changelog.rst index 2191f61..ec83b42 100644 --- a/doc/python/pages/changelog.rst +++ b/doc/python/pages/changelog.rst @@ -95,6 +95,7 @@ Changelog :ref:`trade.AbstractSceneConverter` - Exposed :ref:`Color3.red()` and other convenience constructors (see :gh:`mosra/magnum-bindings#12`) +- Exposed the :ref:`text` library - Fixed issues with an in-source build (see :gh:`mosra/magnum-bindings#13`) - All CMake build options are now prefixed with ``MAGNUM_``. For backwards compatibility, unless ``MAGNUM_BUILD_DEPRECATED`` is disabled and unless a diff --git a/src/python/CMakeLists.txt b/src/python/CMakeLists.txt index f831e89..7356df0 100644 --- a/src/python/CMakeLists.txt +++ b/src/python/CMakeLists.txt @@ -71,6 +71,7 @@ foreach(target magnum_platform_glx magnum_platform_glfw magnum_platform_sdl2 + magnum_text magnum_trade) if(TARGET ${target}) set(${target}_file $) diff --git a/src/python/magnum/CMakeLists.txt b/src/python/magnum/CMakeLists.txt index b4d72e9..09e3432 100644 --- a/src/python/magnum/CMakeLists.txt +++ b/src/python/magnum/CMakeLists.txt @@ -34,6 +34,7 @@ find_package(Magnum COMPONENTS Primitives SceneGraph Shaders + Text Trade GlfwApplication @@ -80,6 +81,9 @@ set(magnum_scenegraph_SRCS set(magnum_shaders_SRCS shaders.cpp) +set(magnum_text_SRCS + text.cpp) + set(magnum_trade_SRCS trade.cpp) @@ -137,6 +141,17 @@ if(NOT MAGNUM_BUILD_STATIC) LIBRARY_OUTPUT_DIRECTORY ${output_dir}/magnum) endif() + if(Magnum_Text_FOUND) + pybind11_add_module(magnum_text ${pybind11_add_module_SYSTEM} ${magnum_text_SRCS}) + target_include_directories(magnum_text PRIVATE + ${PROJECT_SOURCE_DIR}/src + ${PROJECT_SOURCE_DIR}/src/python) + target_link_libraries(magnum_text PRIVATE Magnum::Text) + set_target_properties(magnum_text PROPERTIES + OUTPUT_NAME "text" + LIBRARY_OUTPUT_DIRECTORY ${output_dir}/magnum) + endif() + if(Magnum_Trade_FOUND) pybind11_add_module(magnum_trade ${pybind11_add_module_SYSTEM} ${magnum_trade_SRCS}) target_include_directories(magnum_trade PRIVATE @@ -180,6 +195,11 @@ else() list(APPEND magnum_LIBS Magnum::Shaders) endif() + if(Magnum_Text_FOUND) + list(APPEND magnum_SRCS ${magnum_text_SRCS}) + list(APPEND magnum_LIBS Magnum::Text) + endif() + if(Magnum_Trade_FOUND) list(APPEND magnum_SRCS ${magnum_trade_SRCS}) list(APPEND magnum_LIBS Magnum::Trade) diff --git a/src/python/magnum/__init__.py b/src/python/magnum/__init__.py index 399b1ad..dbe3878 100644 --- a/src/python/magnum/__init__.py +++ b/src/python/magnum/__init__.py @@ -35,7 +35,7 @@ sys.modules['magnum.math'] = math # In case Magnum is built statically, the whole core project is put into # _magnum. Then we need to do the same as above but for all modules. -for i in ['gl', 'meshtools', 'platform', 'primitives', 'scenegraph', 'shaders', 'trade']: +for i in ['gl', 'meshtools', 'platform', 'primitives', 'scenegraph', 'shaders', 'text', 'trade']: if i in globals(): sys.modules['magnum.' + i] = globals()[i] # Platform has subpackages diff --git a/src/python/magnum/bootstrap.h b/src/python/magnum/bootstrap.h index 75a96d6..8765409 100644 --- a/src/python/magnum/bootstrap.h +++ b/src/python/magnum/bootstrap.h @@ -74,6 +74,7 @@ void meshtools(py::module_& m); void primitives(py::module_& m); void scenegraph(py::module_& m); void shaders(py::module_& m); +void text(py::module_& m); void trade(py::module_& m); namespace platform { diff --git a/src/python/magnum/magnum.cpp b/src/python/magnum/magnum.cpp index 4508d85..8b7e86e 100644 --- a/src/python/magnum/magnum.cpp +++ b/src/python/magnum/magnum.cpp @@ -367,6 +367,11 @@ PYBIND11_MODULE(_magnum, m) { magnum::scenegraph(scenegraph); #endif + #ifdef Magnum_Text_FOUND + py::module_ text = m.def_submodule("text"); + magnum::text(text); + #endif + #ifdef Magnum_Trade_FOUND py::module_ trade = m.def_submodule("trade"); magnum::trade(trade); diff --git a/src/python/magnum/staticconfigure.h.cmake b/src/python/magnum/staticconfigure.h.cmake index 44142e5..4642636 100644 --- a/src/python/magnum/staticconfigure.h.cmake +++ b/src/python/magnum/staticconfigure.h.cmake @@ -31,6 +31,7 @@ #cmakedefine Magnum_Primitives_FOUND #cmakedefine Magnum_SceneGraph_FOUND #cmakedefine Magnum_Shaders_FOUND +#cmakedefine Magnum_Text_FOUND #cmakedefine Magnum_Trade_FOUND #cmakedefine Magnum_GlfwApplication_FOUND diff --git a/src/python/magnum/test/Oxygen.ttf b/src/python/magnum/test/Oxygen.ttf new file mode 100644 index 0000000000000000000000000000000000000000..9737cec794080c5bcd0111d555f6df63610238d2 GIT binary patch literal 39728 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 GgZ~RTqQ@ox literal 0 HcmV?d00001 diff --git a/src/python/magnum/test/test_text.py b/src/python/magnum/test/test_text.py new file mode 100644 index 0000000..ef6b4c0 --- /dev/null +++ b/src/python/magnum/test/test_text.py @@ -0,0 +1,104 @@ +# +# This file is part of Magnum. +# +# Copyright © 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019, +# 2020, 2021, 2022 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. +# + +import os +import sys +import unittest + +from corrade import pluginmanager +from magnum import * +from magnum import text + +class Font(unittest.TestCase): + def test(self): + manager = text.FontManager() + self.assertIn('StbTrueTypeFont', manager.alias_list) + self.assertEqual(manager.load_state('StbTrueTypeFont'), pluginmanager.LoadState.NOT_LOADED) + + self.assertTrue(manager.load('StbTrueTypeFont') & pluginmanager.LoadState.LOADED) + self.assertEqual(manager.unload('StbTrueTypeFont'), pluginmanager.LoadState.NOT_LOADED) + + with self.assertRaisesRegex(RuntimeError, "can't load plugin"): + manager.load('NonexistentFont') + with self.assertRaisesRegex(RuntimeError, "can't unload plugin"): + manager.unload('NonexistentFont') + + def test_no_file_opened(self): + font = text.FontManager().load_and_instantiate('StbTrueTypeFont') + self.assertFalse(font.is_opened) + + with self.assertRaisesRegex(AssertionError, "no file opened"): + font.size + with self.assertRaisesRegex(AssertionError, "no file opened"): + font.ascent + with self.assertRaisesRegex(AssertionError, "no file opened"): + font.descent + with self.assertRaisesRegex(AssertionError, "no file opened"): + font.line_height + with self.assertRaisesRegex(AssertionError, "no file opened"): + font.glyph_id('A') + with self.assertRaisesRegex(AssertionError, "no file opened"): + font.glyph_advance(0) + # fill_glyph_cache() not tested as it needs a GL context; assuming it's + # correct + + def test_open_failed(self): + font = text.FontManager().load_and_instantiate('StbTrueTypeFont') + + with self.assertRaisesRegex(RuntimeError, "opening nonexistent.ttf failed"): + font.open_file('nonexistent.ttf', 16.0) + with self.assertRaisesRegex(RuntimeError, "opening data failed"): + font.open_data(b'', 16.0) + + def test_open(self): + manager = text.FontManager() + manager_refcount = sys.getrefcount(manager) + + # Font references the manager to ensure it doesn't get GC'd before the + # plugin instances + font = manager.load_and_instantiate('StbTrueTypeFont') + self.assertIs(font.manager, manager) + self.assertEqual(sys.getrefcount(manager), manager_refcount + 1) + + font.open_file(os.path.join(os.path.dirname(__file__), 'Oxygen.ttf'), 16.0) + self.assertTrue(font.is_opened) + self.assertEqual(font.size, 16.0) + self.assertEqual(font.ascent, 17.011186599731445) + self.assertEqual(font.descent, -4.322147846221924) + self.assertEqual(font.line_height, 21.33333396911621) + self.assertEqual(font.glyph_id('A'), 36) + self.assertEqual(font.glyph_advance(36), (11.7136, 0.0)) + + # Deleting the font should decrease manager refcount again + del font + self.assertEqual(sys.getrefcount(manager), manager_refcount) + + def test_open_data(self): + font = text.FontManager().load_and_instantiate('StbTrueTypeFont') + + with open(os.path.join(os.path.dirname(__file__), 'Oxygen.ttf'), 'rb') as f: + font.open_data(f.read(), 16.0) + + self.assertEqual(font.size, 16.0) diff --git a/src/python/magnum/test/test_text_gl.py b/src/python/magnum/test/test_text_gl.py new file mode 100644 index 0000000..f8d9288 --- /dev/null +++ b/src/python/magnum/test/test_text_gl.py @@ -0,0 +1,75 @@ +# +# This file is part of Magnum. +# +# Copyright © 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019, +# 2020, 2021, 2022 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. +# + +import os +import sys +import unittest + +# setUpModule gets called before everything else, skipping if GL tests can't +# be run +from . import GLTestCase, setUpModule + +from corrade import pluginmanager +from magnum import * +from magnum import gl, text + +class GlyphCache(GLTestCase): + def test(self): + cache = text.GlyphCache((128, 128), (2, 2)) + + self.assertEqual(cache.texture_size, (128, 128)) + self.assertEqual(cache.padding, (2, 2)) + + cache_refcount = sys.getrefcount(cache) + + # Returned texture references the cache to ensure it doesn't get GC'd + # before the texture instances + texture = cache.texture + self.assertEqual(sys.getrefcount(cache), cache_refcount + 1) + + # Deleting the texture should decrease cache refcount again + del texture + self.assertEqual(sys.getrefcount(cache), cache_refcount) + +class DistanceFieldGlyphCache(GLTestCase): + def test(self): + cache = text.DistanceFieldGlyphCache((1024, 1024), (128, 128), 2) + + self.assertEqual(cache.texture_size, (1024, 1024)) + self.assertEqual(cache.padding, (2, 2)) + +class Renderer2D(GLTestCase): + def test(self): + font = text.FontManager().load_and_instantiate('StbTrueTypeFont') + font.open_file(os.path.join(os.path.dirname(__file__), 'Oxygen.ttf'), 16.0) + + cache = text.GlyphCache((128, 128)) + font.fill_glyph_cache(cache, "hello") + + renderer = text.Renderer2D(font, cache, 1.0) + renderer.reserve(16) + renderer.render("hello") + + self.assertEqual(renderer.rectangle, Range2D((0.0625, -0.0625), (2.4807, 0.875))) diff --git a/src/python/magnum/text.cpp b/src/python/magnum/text.cpp new file mode 100644 index 0000000..8d3b8a8 --- /dev/null +++ b/src/python/magnum/text.cpp @@ -0,0 +1,181 @@ +/* + This file is part of Magnum. + + Copyright © 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019, + 2020, 2021, 2022 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 /** @todo drop once we have our string casters */ +#include +#include +#include +#include + +#include "corrade/pluginmanager.h" +#include "magnum/bootstrap.h" + +namespace magnum { + +namespace { + +/* For some reason having ...Args as the second (and not last) template + argument does not work. So I'm listing all variants used more than once. */ +template R checkOpened(Text::AbstractFont& self) { + if(!self.isOpened()) { + PyErr_SetString(PyExc_AssertionError, "no file opened"); + throw py::error_already_set{}; + } + return (self.*f)(); +} +template R checkOpened(Text::AbstractFont& self, Arg1 arg1) { + if(!self.isOpened()) { + PyErr_SetString(PyExc_AssertionError, "no file opened"); + throw py::error_already_set{}; + } + return (self.*f)(arg1); +} + +} + +void text(py::module_& m) { + m.doc() = "Text rendering"; + + /* AbstractFont depends on this */ + py::module_::import("corrade.pluginmanager"); + + #ifndef MAGNUM_BUILD_STATIC + /* These are a part of the same module in the static build, no need to + import (also can't import because there it's _magnum.*) */ + py::module_::import("magnum.gl"); + #endif + + /* Glyph caches */ + py::class_ abstractGlyphCache{m, "AbstractGlyphCache", "Base for glyph caches"}; + abstractGlyphCache + /** @todo features */ + .def_property_readonly("texture_size", &Text::AbstractGlyphCache::textureSize, "Glyph cache texture size") + .def_property_readonly("padding", &Text::AbstractGlyphCache::padding, "Glyph padding") + /** @todo glyph iteration and population */ + ; + py::class_ glyphCache{m, "GlyphCache", "Glyph cache"}; + glyphCache + .def(py::init(), "Constructor", py::arg("internal_format"), py::arg("original_size"), py::arg("size"), py::arg("padding")) + .def(py::init(), "Constructor", py::arg("internal_format"), py::arg("size"), py::arg("padding") = Vector2i{}) + .def(py::init(), "Constructor", py::arg("original_size"), py::arg("size"), py::arg("padding")) + .def(py::init(), "Constructor", py::arg("size"), py::arg("padding") = Vector2i{}) + /* The default behavior when returning a reference seems to be that it + increfs the originating instance and decrefs it again after the + variable gets deleted. This is verified in test_text_gl.py to be + extra sure. */ + .def_property_readonly("texture", &Text::GlyphCache::texture, "Cache texture"); + py::class_ distanceFieldGlyphCache{m, "DistanceFieldGlyphCache", "Glyph cache with distance field rendering"}; + distanceFieldGlyphCache + .def(py::init(), "Constructor", py::arg("original_size"), py::arg("size"), py::arg("radius")) + /** @todo setDistanceFieldImage, once needed for anything */ + ; + + /* Font */ + py::class_> abstractFont{m, "AbstractFont", "Interface for font plugins"}; + abstractFont + /** @todo features */ + .def_property_readonly("is_opened", &Text::AbstractFont::isOpened, "Whether any file is opened") + .def("open_data", [](Text::AbstractFont& self, Containers::ArrayView data, Float size) { + /** @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, size)) return; + + PyErr_SetString(PyExc_RuntimeError, "opening data failed"); + throw py::error_already_set{}; + }, "Open raw data", py::arg("data"), py::arg("size")) + .def("open_file", [](Text::AbstractFont& self, const std::string& filename, Float size) { + /** @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, size)) return; + + PyErr_Format(PyExc_RuntimeError, "opening %s failed", filename.data()); + throw py::error_already_set{}; + }, "Open a file", py::arg("filename"), py::arg("size")) + .def("close", &Text::AbstractFont::close, "Close currently opened file") + + .def_property_readonly("size", checkOpened, "Font size") + .def_property_readonly("ascent", checkOpened, "Font ascent") + .def_property_readonly("descent", checkOpened, "Font descent") + .def_property_readonly("line_height", checkOpened, "Line height") + .def("glyph_id", checkOpened, "Glyph ID for given character", py::arg("character")) + .def("glyph_advance", checkOpened, "Glyph advance", py::arg("glyph")) + .def("fill_glyph_cache", [](Text::AbstractFont& self, Text::AbstractGlyphCache& cache, const std::string& characters) { + if(!self.isOpened()) { + PyErr_SetString(PyExc_AssertionError, "no file opened"); + throw py::error_already_set{}; + } + return self.fillGlyphCache(cache, characters); + }, "Fill glyph cache with given character set", py::arg("cache"), py::arg("characters")) + /** @todo createGlyphCache() */ + /** @todo layout and AbstractLayouter, once needed for anything */ + ; + corrade::plugin(abstractFont); + + py::class_, PluginManager::AbstractManager> fontManager{m, "FontManager", "Manager for font plugins"}; + corrade::manager(fontManager); + + py::enum_{m, "Alignment", "Text rendering alignment"} + .value("LINE_LEFT", Text::Alignment::LineLeft) + .value("LINE_CENTER", Text::Alignment::LineCenter) + .value("LINE_RIGHT", Text::Alignment::LineRight) + .value("MIDDLE_LEFT", Text::Alignment::MiddleLeft) + .value("MIDDLE_CENTER", Text::Alignment::MiddleCenter) + .value("MIDDLE_RIGHT", Text::Alignment::MiddleRight) + .value("TOP_LEFT", Text::Alignment::TopLeft) + .value("TOP_CENTER", Text::Alignment::TopCenter) + .value("TOP_RIGHT", Text::Alignment::TopRight) + .value("LINE_CENTER_INTEGRAL", Text::Alignment::LineCenterIntegral) + .value("MIDDLE_LEFT_INTEGRAL", Text::Alignment::MiddleLeftIntegral) + .value("MIDDLE_CENTER_INTEGRAL", Text::Alignment::MiddleCenterIntegral) + .value("MIDDLE_RIGHT_INTEGRAL", Text::Alignment::MiddleRightIntegral); + + /** @todo any reason to expose a 3D renderer? it isn't any different + currently */ + py::class_ renderer2D{m, "Renderer2D", "2D text renderer"}; + renderer2D + .def(py::init(), "Constructor", py::arg("font"), py::arg("cache"), py::arg("size"), py::arg("alignment") = Text::Alignment::LineLeft) + .def_property_readonly("capacity", &Text::Renderer2D::capacity, "Capacity for rendered glyphs") + .def_property_readonly("rectangle", &Text::Renderer2D::rectangle, "Rectangle spanning the rendered text") + /** @todo are the buffers useful for anything? */ + /* The default behavior when returning a reference seems to be that it + increfs the originating instance and decrefs it again after the + variable gets deleted. This is verified in test_text.py to be extra + sure. */ + .def_property_readonly("mesh", &Text::Renderer2D::mesh, "Mesh") + .def("reserve", &Text::Renderer2D::reserve, "Reserve capacity for renderered glyphs", py::arg("glyph_count"), py::arg("vertex_buffer_usage") = GL::BufferUsage::StaticDraw, py::arg("index_buffer_usage") = GL::BufferUsage::StaticDraw) + .def("render", static_cast(&Text::Renderer2D::render), "Render text", py::arg("text")); +} + +} + +#ifndef MAGNUM_BUILD_STATIC +/* TODO: remove declaration when https://github.com/pybind/pybind11/pull/1863 + is released */ +extern "C" PYBIND11_EXPORT PyObject* PyInit_text(); +PYBIND11_MODULE(text, m) { + magnum::text(m); +} +#endif diff --git a/src/python/setup.py.cmake b/src/python/setup.py.cmake index a1ceb9e..e2cc7dd 100644 --- a/src/python/setup.py.cmake +++ b/src/python/setup.py.cmake @@ -46,6 +46,7 @@ extension_paths = { 'magnum.platform.glx': '${magnum_platform_glx_file}', 'magnum.platform.glfw': '${magnum_platform_glfw_file}', 'magnum.platform.sdl2': '${magnum_platform_sdl2_file}', + 'magnum.text': '${magnum_text_file}', 'magnum.trade': '${magnum_trade_file}', }