From 344e00ce117534161e92b383c39e3b95779e783f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vladim=C3=ADr=20Vondru=C5=A1?= Date: Fri, 8 Mar 2019 23:01:48 +0100 Subject: [PATCH] Text: implement file callbacks for AbstractFont. Basically mirroring the API of Trade::AbstractImporter, as that proved to be useful. The old crazy openSingleData() and openData(horribleStuff) are deprecated and will be removed in a future release. No backwards compatibility is provided for font plugins, these need to be adapted. --- doc/changelog.dox | 7 + doc/snippets/MagnumText.cpp | 87 +++ src/Magnum/Text/AbstractFont.cpp | 118 +++- src/Magnum/Text/AbstractFont.h | 277 ++++++-- src/Magnum/Text/Test/AbstractFontTest.cpp | 589 +++++++++++++++++- src/Magnum/Trade/AbstractImporter.h | 3 + src/MagnumPlugins/MagnumFont/MagnumFont.cpp | 99 +-- src/MagnumPlugins/MagnumFont/MagnumFont.h | 13 +- .../MagnumFont/Test/MagnumFontTest.cpp | 48 +- 9 files changed, 1047 insertions(+), 194 deletions(-) diff --git a/doc/changelog.dox b/doc/changelog.dox index 7c5112082..b45684dff 100644 --- a/doc/changelog.dox +++ b/doc/changelog.dox @@ -90,6 +90,8 @@ See also: @ref GL. The @ref Text::Renderer, @ref Text::GlyphCache and @ref Text::DistanceFieldGlyphCache classes are now built only if `TARGET_GL` is enabled (done by default). +- New @ref Text::AbstractFont::setFileCallback() to allow opening multi-file + fonts with an API similar to @ref Trade::AbstractImporter @subsection changelog-latest-changes Changes and improvements @@ -206,6 +208,11 @@ See also: - @cpp DebugTools::ForceRendererOptions::scale() @ce is deprecated in favor of @ref DebugTools::ForceRendererOptions::size(), as that's more consistent with the documentation and the corresponding setter. +- @cpp Text::AbstractFont::openSingleData() @ce and + @cpp Text::AbstractFont::openData() @ce taking a list of files are + deprecated in favor of + @ref Text::AbstractFont::openData(Containers::ArrayView, Float) + and @ref Text::AbstractFont::setFileCallback() @section changelog-2019-01 2019.01 diff --git a/doc/snippets/MagnumText.cpp b/doc/snippets/MagnumText.cpp index 8e06fc320..3367c298a 100644 --- a/doc/snippets/MagnumText.cpp +++ b/doc/snippets/MagnumText.cpp @@ -23,6 +23,12 @@ DEALINGS IN THE SOFTWARE. */ +#include +#include +#include +#include + +#include "Magnum/FileCallback.h" #include "Magnum/Shaders/Vector.h" #include "Magnum/Text/AbstractFont.h" #include "Magnum/Text/DistanceFieldGlyphCache.h" @@ -33,6 +39,87 @@ using namespace Magnum::Math::Literals; int main() { +{ +/* [AbstractFont-usage] */ +PluginManager::Manager manager; +Containers::Pointer font = + manager.loadAndInstantiate("StbTrueTypeFont"); +if(!font || !font->openFile("font.ttf", 16.0f)) + Fatal{} << "Can't open font.ttf with StbTrueTypeFont"; + +Text::GlyphCache cache{Vector2i{512}}; +font->fillGlyphCache(cache, "abcdefghijklmnopqrstuvwxyz" + "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + "0123456789?!:;,. "); +/* [AbstractFont-usage] */ +} + +#if defined(CORRADE_TARGET_UNIX) || (defined(CORRADE_TARGET_WINDOWS) && !defined(CORRADE_TARGET_WINDOWS_RT)) +{ +Containers::Pointer font; +/* [AbstractFont-usage-callbacks] */ +struct Data { + std::unordered_map> files; +} data; + +font->setFileCallback([](const std::string& filename, + InputFileCallbackPolicy policy, Data& data) + -> Containers::Optional> + { + auto found = data.files.find(filename); + + /* Discard the memory mapping, if not needed anymore */ + if(policy == InputFileCallbackPolicy::Close) { + if(found != data.files.end()) data.files.erase(found); + return {}; + } + + /* Load if not there yet */ + if(found == data.files.end()) found = data.files.emplace( + filename, Utility::Directory::mapRead(filename)).first; + + return Containers::arrayView(found->second); + }, data); + +font->openFile("magnum-font.conf", 13.0f); +/* [AbstractFont-usage-callbacks] */ +} +#endif + +{ +Containers::Pointer font; +/* [AbstractFont-setFileCallback] */ +font->setFileCallback([](const std::string& filename, + InputFileCallbackPolicy, void*) { + Utility::Resource rs("data"); + return Containers::optional(rs.getRaw(filename)); + }); +/* [AbstractFont-setFileCallback] */ +} + +{ +Containers::Pointer font; +/* [AbstractFont-setFileCallback-template] */ +struct Data { + std::unordered_map> files; +} data; + +font->setFileCallback([](const std::string& filename, + InputFileCallbackPolicy, Data& data) + -> Containers::Optional> + { + auto found = data.files.find(filename); + if(found == data.files.end()) { + if(!Utility::Directory::exists(filename)) + return Containers::NullOpt; + found = data.files.emplace(filename, Utility::Directory::read(filename)).first; + } + return Containers::ArrayView{found->second}; + }, data); +/* [AbstractFont-setFileCallback-template] */ +} + { /* [DistanceFieldGlyphCache-usage] */ Containers::Pointer font; diff --git a/src/Magnum/Text/AbstractFont.cpp b/src/Magnum/Text/AbstractFont.cpp index c1b279463..7142be758 100644 --- a/src/Magnum/Text/AbstractFont.cpp +++ b/src/Magnum/Text/AbstractFont.cpp @@ -26,9 +26,12 @@ #include "AbstractFont.h" #include +#include +#include #include #include +#include "Magnum/FileCallback.h" #include "Magnum/Math/Functions.h" #include "Magnum/Text/AbstractGlyphCache.h" @@ -60,7 +63,18 @@ AbstractFont::AbstractFont() = default; AbstractFont::AbstractFont(PluginManager::AbstractManager& manager, const std::string& plugin): AbstractPlugin{manager, plugin} {} -bool AbstractFont::openData(const std::vector>>& data, const Float size) { +void AbstractFont::setFileCallback(Containers::Optional>(*callback)(const std::string&, InputFileCallbackPolicy, void*), void* const userData) { + CORRADE_ASSERT(!isOpened(), "Text::AbstractFont::setFileCallback(): can't be set while a font is opened", ); + CORRADE_ASSERT(features() & (Feature::FileCallback|Feature::OpenData), "Text::AbstractFont::setFileCallback(): font plugin supports neither loading from data nor via callbacks, callbacks can't be used", ); + + _fileCallback = callback; + _fileCallbackUserData = userData; + doSetFileCallback(callback, userData); +} + +void AbstractFont::doSetFileCallback(Containers::Optional>(*)(const std::string&, InputFileCallbackPolicy, void*), void*) {} + +bool AbstractFont::openData(Containers::ArrayView data, const Float size) { CORRADE_ASSERT(features() & Feature::OpenData, "Text::AbstractFont::openData(): feature not supported", false); @@ -77,40 +91,62 @@ bool AbstractFont::openData(const std::vector>>& data, const Float size) -> Metrics { - CORRADE_ASSERT(!(features() & Feature::MultiFile), - "Text::AbstractFont::openData(): feature advertised but not implemented", {}); - CORRADE_ASSERT(data.size() == 1, - "Text::AbstractFont::openData(): expected just one file for single-file format", {}); +auto AbstractFont::doOpenData(Containers::ArrayView, Float) -> Metrics { + CORRADE_ASSERT(false, "Text::AbstractFont::openData(): feature advertised but not implemented", {}); + return {}; +} +#ifdef MAGNUM_BUILD_DEPRECATED +bool AbstractFont::openData(const std::vector>>& data, const Float size) { close(); - return doOpenSingleData(data[0].second, size); -} -bool AbstractFont::openSingleData(const Containers::ArrayView data, const Float size) { - CORRADE_ASSERT(features() & Feature::OpenData, - "Text::AbstractFont::openSingleData(): feature not supported", false); - CORRADE_ASSERT(!(features() & Feature::MultiFile), - "Text::AbstractFont::openSingleData(): the format is not single-file", false); + setFileCallback([](const std::string& file, InputFileCallbackPolicy, const std::vector>>& data) -> Containers::Optional> { + for(auto&& i: data) if(i.first == file) return i.second; + return {}; + }, data); - close(); - const Metrics metrics = doOpenSingleData(data, size); - _size = metrics.size; - _ascent = metrics.ascent; - _descent = metrics.descent; - _lineHeight = metrics.lineHeight; - CORRADE_INTERNAL_ASSERT(isOpened() || (!_size && !_ascent && !_descent && !_lineHeight)); - return isOpened(); + return !data.empty() && openData(data.front().second, size); } -auto AbstractFont::doOpenSingleData(Containers::ArrayView, Float) -> Metrics { - CORRADE_ASSERT(false, "Text::AbstractFont::openSingleData(): feature advertised but not implemented", {}); - return {}; +bool AbstractFont::openSingleData(const Containers::ArrayView data, const Float size) { + return openData(data, size); } +#endif bool AbstractFont::openFile(const std::string& filename, const Float size) { close(); - const Metrics metrics = doOpenFile(filename, size); + Metrics metrics; + + /* If file loading callbacks are not set or the font implementation + supports handling them directly, call into the implementation */ + if(!_fileCallback || (doFeatures() & Feature::FileCallback)) { + metrics = doOpenFile(filename, size); + + /* Otherwise, if loading from data is supported, use the callback and pass + the data through to openData(). Mark the file as ready to be closed once + opening is finished. */ + } else if(doFeatures() & Feature::OpenData) { + /* This needs to be duplicated here and in the doOpenFile() + implementation in order to support both following cases: + - plugins that don't support FileCallback but have their own + doOpenFile() implementation (callback needs to be used here, + because the base doOpenFile() implementation might never get + called) + - plugins that support FileCallback but want to delegate the actual + file loading to the default implementation (callback used in the + base doOpenFile() implementation, because this branch is never + taken in that case) */ + const Containers::Optional> data = _fileCallback(filename, InputFileCallbackPolicy::LoadTemporary, _fileCallbackUserData); + if(!data) { + Error() << "Text::AbstractFont::openFile(): cannot open file" << filename; + return isOpened(); + } + metrics = doOpenData(*data, size); + _fileCallback(filename, InputFileCallbackPolicy::Close, _fileCallbackUserData); + + /* Shouldn't get here, the assert is fired already in setFileCallback() */ + } else CORRADE_ASSERT_UNREACHABLE(); /* LCOV_EXCL_LINE */ + _size = metrics.size; _ascent = metrics.ascent; _descent = metrics.descent; @@ -120,16 +156,32 @@ bool AbstractFont::openFile(const std::string& filename, const Float size) { } auto AbstractFont::doOpenFile(const std::string& filename, const Float size) -> Metrics { - CORRADE_ASSERT(features() & Feature::OpenData && !(features() & Feature::MultiFile), - "Text::AbstractFont::openFile(): not implemented", {}); - - /* Open file */ - if(!Utility::Directory::exists(filename)) { - Error() << "Text::AbstractFont::openFile(): cannot open file" << filename; - return {}; + CORRADE_ASSERT(features() & Feature::OpenData, "Text::AbstractFont::openFile(): not implemented", {}); + + Metrics metrics; + + /* If callbacks are set, use them. This is the same implementation as in + openFile(), see the comment there for details. */ + if(_fileCallback) { + const Containers::Optional> data = _fileCallback(filename, InputFileCallbackPolicy::LoadTemporary, _fileCallbackUserData); + if(!data) { + Error() << "Text::AbstractFont::openFile(): cannot open file" << filename; + return {}; + } + metrics = doOpenData(*data, size); + _fileCallback(filename, InputFileCallbackPolicy::Close, _fileCallbackUserData); + + /* Otherwise open the file directly */ + } else { + if(!Utility::Directory::exists(filename)) { + Error() << "Text::AbstractFont::openFile(): cannot open file" << filename; + return {}; + } + + metrics = doOpenData(Utility::Directory::read(filename), size); } - return doOpenSingleData(Utility::Directory::read(filename), size); + return metrics; } void AbstractFont::close() { diff --git a/src/Magnum/Text/AbstractFont.h b/src/Magnum/Text/AbstractFont.h index 18ba78bc3..ebe160363 100644 --- a/src/Magnum/Text/AbstractFont.h +++ b/src/Magnum/Text/AbstractFont.h @@ -48,37 +48,76 @@ namespace Magnum { namespace Text { @brief Base for font plugins Provides interface for opening fonts, filling glyph cache and layouting the -glyphs. See @ref plugins for more information and `*Font` classes in @ref Text -namespace for available font plugins. +glyphs. @section Text-AbstractFont-usage Usage -First step is to open the font using @ref openData(), @ref openSingleData() or -@ref openFile(). Next step is to prerender all the glyphs which will be used in -text rendering later, see @ref GlyphCache for more information. See -@ref Renderer for information about text rendering. +Fonts are most commonly implemented as plugins. For example, loading a font +from the filesystem using the @ref StbTrueTypeFont plugin and prerendering all +needed glyphs can be done like this, completely with all error handling: + +@snippet MagnumText.cpp AbstractFont-usage + +See @ref plugins for more information about general plugin usage and `*Font` +classes in the @ref Text namespace for available font plugins. See +@ref GlyphCache for more information about glyph caches and @ref Renderer for +information about actual text rendering. + +@subsection Text-AbstractFont-usage-callbacks Loading data from memory, using file callbacks + +Besides loading data directly from the filesystem using @ref openFile() like +shown above, it's possible to use @ref openData() to import data from memory. +Note that the particular importer implementation must support +@ref Feature::OpenData for this method to work. + +Some font formats consist of more than one file and in that case you may want +to intercept those references and load them in a custom way as well. For font +plugins that advertise support for this with @ref Feature::FileCallback this is +done by specifying a file loading callback using @ref setFileCallback(). The +callback gets a filename, @ref InputFileCallbackPolicy and an user +pointer as parameters; returns a non-owning view on the loaded data or a +@ref Corrade::Containers::NullOpt "Containers::NullOpt" to indicate the file +loading failed. For example, loading a memory-mapped font could look like +below. Note that the file loading callback affects @ref openFile() as +well --- you don't have to load the top-level file manually and pass it to +@ref openData(), any font plugin supporting the callback feature handles that +correctly. + +@snippet MagnumText.cpp AbstractFont-usage-callbacks + +For importers that don't support @ref Feature::FileCallback directly, the base +@ref openFile() implementation will use the file callback to pass the loaded +data through to @ref openData(), in case the importer supports at least +@ref Feature::OpenData. If the importer supports neither @ref Feature::FileCallback +nor @ref Feature::OpenData, @ref setFileCallback() doesn't allow the callbacks +to be set. + +The input file callback signature is the same for @ref Text::AbstractFont and +@ref Trade::AbstractImporter to allow code reuse. @section Text-AbstractFont-subclassing Subclassing -Plugin implements @ref doFeatures(), @ref doClose(), @ref doLayout(), either -@ref doCreateGlyphCache() or @ref doFillGlyphCache() and one or more of +The plugin implements @ref doFeatures(), @ref doClose(), @ref doLayout(), +either @ref doCreateGlyphCache() or @ref doFillGlyphCache() and one or more of `doOpen*()` functions. See also @ref AbstractLayouter for more information. You don't need to do most of the redundant sanity checks, these things are checked by the implementation: -- Functions @ref doOpenData(), @ref doOpenSingleData() and @ref doOpenFile() - are called after the previous file was closed, function @ref doClose() is - called only if there is any file opened. -- Functions @ref doOpenData() and @ref doOpenSingleData() are called only if - @ref Feature::OpenData is supported. +- Functions @ref doOpenData() and @ref doOpenFile() are called after the + previous file was closed, function @ref doClose() is called only if there + is any file opened. +- Function @ref doOpenData() is called only if @ref Feature::OpenData is + supported. +- The @ref doSetFileCallback() function is called only if + @ref Feature::FileCallback is supported and there is no file opened. - All `do*()` implementations working on opened file are called only if there is any file opened. */ class MAGNUM_TEXT_EXPORT AbstractFont: public PluginManager::AbstractPlugin { public: /** - * @brief Features supported by this importer + * @brief Features supported by this font implementation * * @see @ref Features, @ref features() */ @@ -86,11 +125,25 @@ class MAGNUM_TEXT_EXPORT AbstractFont: public PluginManager::AbstractPlugin { /** Opening fonts from raw data using @ref openData() */ OpenData = 1 << 0, + /** + * Specifying callbacks for loading additional files referenced + * from the main file using @ref setFileCallback(). If the font + * doesn't expose this feature, the format is either single-file or + * loading via callbacks is not supported. + * + * See @ref Text-AbstractFont-usage-callbacks and particular font + * plugin documentation for more information. + */ + FileCallback = 1 << 1, + + #ifdef MAGNUM_BUILD_DEPRECATED /** * The format is multi-file, thus @ref openSingleData() convenience * function cannot be used. + * @deprecated Obsolete, use file callbacks instead. */ - MultiFile = 1 << 1, + MultiFile CORRADE_DEPRECATED_ENUM("obsolete, use file callbacks instead") = FileCallback, + #endif /** * The font contains prepared glyph cache. @@ -100,7 +153,7 @@ class MAGNUM_TEXT_EXPORT AbstractFont: public PluginManager::AbstractPlugin { PreparedGlyphCache = 1 << 2 }; - /** @brief Set of features supported by this importer */ + /** @brief Set of features supported by this font implementation */ typedef Containers::EnumSet Features; /** @@ -138,31 +191,118 @@ class MAGNUM_TEXT_EXPORT AbstractFont: public PluginManager::AbstractPlugin { /** @brief Features supported by this font */ Features features() const { return doFeatures(); } + /** + * @brief File opening callback function + * + * @see @ref Text-AbstractFont-usage-callbacks + */ + auto fileCallback() const -> Containers::Optional>(*)(const std::string&, InputFileCallbackPolicy, void*) { return _fileCallback; } + + /** + * @brief File opening callback user data + * + * @see @ref Text-AbstractFont-usage-callbacks + */ + void* fileCallbackUserData() const { return _fileCallbackUserData; } + + /** + * @brief Set file opening callback + * + * In case the font plugin supports @ref Feature::FileCallback, files + * opened through @ref openFile() will be loaded through the provided + * callback. Besides that, all external files referenced by the + * top-level file will be loaded through the callback function as well, + * usually on demand. The callback function gets a filename, + * @ref InputFileCallbackPolicy and the @p userData pointer as input + * and returns a non-owning view on the loaded data as output or a + * @ref Corrade::Containers::NullOpt if loading failed --- because + * empty files might also be valid in some circumstances, @cpp nullptr @ce + * can't be used to indicate a failure. + * + * In case the font plugin doesn't support @ref Feature::FileCallback but + * supports at least @ref Feature::OpenData, a file opened through + * @ref openFile() will be internally loaded through the provided + * callback and then passed to @ref openData(). First the file is + * loaded with @ref InputFileCallbackPolicy::LoadTemporary passed to + * the callback, then the returned memory view is passed to + * @ref openData() (sidestepping the potential @ref openFile() + * implementation of that particular font plugin) and after that the + * callback is called again with @ref InputFileCallbackPolicy::Close + * because the semantics of @ref openData() don't require the data to + * be alive after. In case you need a different behavior, use + * @ref openData() directly. + * + * In case @p callback is @cpp nullptr @ce, the current callback (if + * any) is reset. This function expects that the font plugin supports + * either @ref Feature::FileCallback or @ref Feature::OpenData. If an + * font plugin supports neither, callbacks can't be used. + * + * It's expected that this function is called *before* a file is + * opened. It's also expected that the loaded data are kept in scope + * for as long as the font plugin needs them, based on the value of + * @ref InputFileCallbackPolicy. Documentation of particular importers + * provides more information about the expected callback behavior. + * + * Following is an example of setting up a file loading callback for + * fetching compiled-in resources from @ref Corrade::Utility::Resource. + * See the overload below for a more convenient type-safe way to pass + * the user data pointer. + * + * @snippet MagnumText.cpp AbstractFont-setFileCallback + * + * @see @ref Text-AbstractFont-usage-callbacks + */ + void setFileCallback(Containers::Optional>(*callback)(const std::string&, InputFileCallbackPolicy, void*), void* userData = nullptr); + + /** + * @brief Set file opening callback + * + * Equivalent to calling the above with a lambda wrapper that casts + * @cpp void* @ce back to @cpp T* @ce and dereferences it in order to + * pass it to @p callback. Example usage: + * + * @snippet MagnumText.cpp AbstractFont-setFileCallback-template + * + * @see @ref Text-AbstractFont-usage-callbacks + */ + #ifdef DOXYGEN_GENERATING_OUTPUT + template void setFileCallback(Containers::Optional>(*callback)(const std::string&, InputFileCallbackPolicy, T&), T& userData); + #else + /* Otherwise the user would be forced to use the + operator to convert + a lambda to a function pointer and (besides being weird and + annoying) it's also not portable because it doesn't work on MSVC + 2015 and older versions of MSVC 2017. */ + template void setFileCallback(Callback callback, T& userData); + #endif + /** @brief Whether any file is opened */ bool isOpened() const { return doIsOpened(); } /** * @brief Open font from raw data - * @param data Pairs of filename and file data + * @param data File data * @param size Font size * * Closes previous file, if it was opened, and tries to open given * file. Available only if @ref Feature::OpenData is supported. Returns * @cpp true @ce on success, @cpp false @ce otherwise. + * @see @ref features(), @ref openFile() */ - bool openData(const std::vector>>& data, Float size); + bool openData(Containers::ArrayView data, Float size); - /** - * @brief Open font from single data - * @param data File data - * @param size Font size - * - * Closes previous file, if it was opened, and tries to open given - * file. Available only if @ref Feature::OpenData is supported and the - * plugin doesn't have @ref Feature::MultiFile. Returns @cpp true @ce - * on success, @cpp false @ce otherwise. + #ifdef MAGNUM_BUILD_DEPRECATED + /** @brief @copybrief openData(Containers::ArrayView, Float) + * @deprecated Use @ref openFile() with @ref setFileCallback() for + * opening multi-file fonts instead. */ - bool openSingleData(Containers::ArrayView data, Float size); + CORRADE_DEPRECATED("use openFile() with setFileCallback() for opening multi-file fonts instead") bool openData(const std::vector>>& data, Float size); + + /** @brief @copybrief openData(Containers::ArrayView, Float) + * @deprecated Use @ref openData(Containers::ArrayView, Float) + * instead. + */ + CORRADE_DEPRECATED("use openData(Containers::ArrayView, Float) instead") bool openSingleData(Containers::ArrayView data, Float size); + #endif /** * @brief Open font from file @@ -271,7 +411,7 @@ class MAGNUM_TEXT_EXPORT AbstractFont: public PluginManager::AbstractPlugin { /** * @brief Font metrics * - * @see @ref doOpenFile(), @ref doOpenData(), @ref doOpenSingleData() + * @see @ref doOpenFile(), @ref doOpenData() */ struct Metrics { #ifndef DOXYGEN_GENERATING_OUTPUT @@ -305,39 +445,50 @@ class MAGNUM_TEXT_EXPORT AbstractFont: public PluginManager::AbstractPlugin { Float lineHeight; }; - private: - /** @brief Implementation for @ref features() */ - virtual Features doFeatures() const = 0; - - /** @brief Implementation for @ref isOpened() */ - virtual bool doIsOpened() const = 0; - + protected: /** - * @brief Implementation for @ref openData() + * @brief Implementation for @ref openFile() * * Return metrics of opened font on successful opening, zeros - * otherwise. If the plugin doesn't have @ref Feature::MultiFile, - * default implementation calls @ref doOpenSingleData(). + * otherwise. If @ref Feature::OpenData is supported, default + * implementation opens the file and calls @ref doOpenData() with its + * contents. It is allowed to call this function from your + * @ref doOpenFile() implementation --- in particular, this + * implementation will also correctly handle callbacks set through + * @ref setFileCallback(). + * + * This function is not called when file callbacks are set through + * @ref setFileCallback() and @ref Feature::FileCallback is not + * supported --- instead, file is loaded though the callback and data + * passed through to @ref doOpenData(). */ - virtual Metrics doOpenData(const std::vector>>& data, Float size); + virtual Metrics doOpenFile(const std::string& filename, Float size); + + private: + /** @brief Implementation for @ref features() */ + virtual Features doFeatures() const = 0; /** - * @brief Implementation for @ref openSingleData() + * @brief Implementation for @ref setFileCallback() * - * Return metrics of opened font on successful opening, zeros - * otherwise. + * Useful when the font plugin needs to modify some internal state on + * callback setup. Default implementation does nothing and this + * function doesn't need to be implemented --- the callback function + * and user data pointer are available through @ref fileCallback() and + * @ref fileCallbackUserData(). */ - virtual Metrics doOpenSingleData(Containers::ArrayView data, Float size); + virtual void doSetFileCallback(Containers::Optional>(*callback)(const std::string&, InputFileCallbackPolicy, void*), void* userData); + + /** @brief Implementation for @ref isOpened() */ + virtual bool doIsOpened() const = 0; /** - * @brief Implementation for @ref openFile() + * @brief Implementation for @ref openData() * * Return metrics of opened font on successful opening, zeros - * otherwise. If @ref Feature::OpenData is supported and the plugin - * doesn't have @ref Feature::MultiFile, default implementation opens - * the file and calls @ref doOpenSingleData() with its contents. + * otherwise. */ - virtual Metrics doOpenFile(const std::string& filename, Float size); + virtual Metrics doOpenData(Containers::ArrayView data, Float size); /** @brief Implementation for @ref close() */ virtual void doClose() = 0; @@ -362,9 +513,16 @@ class MAGNUM_TEXT_EXPORT AbstractFont: public PluginManager::AbstractPlugin { /** @brief Implementation for @ref layout() */ virtual Containers::Pointer doLayout(const AbstractGlyphCache& cache, Float size, const std::string& text) = 0; - #ifdef DOXYGEN_GENERATING_OUTPUT - private: - #endif + Containers::Optional>(*_fileCallback)(const std::string&, InputFileCallbackPolicy, void*){}; + void* _fileCallbackUserData{}; + + /* Used by the templated version only */ + struct FileCallbackTemplate { + void(*callback)(); + const void* userData; + /* GCC 4.8 complains loudly about missing initializers otherwise */ + } _fileCallbackTemplate{nullptr, nullptr}; + Float _size{}, _ascent{}, _descent{}, _lineHeight{}; }; @@ -439,6 +597,21 @@ class MAGNUM_TEXT_EXPORT AbstractLayouter { UnsignedInt _glyphCount; }; +#ifndef DOXYGEN_GENERATING_OUTPUT +template void AbstractFont::setFileCallback(Callback callback, T& userData) { + /* Don't try to wrap a null function pointer. Need to cast first because + MSVC (even 2017) can't apply ! to a lambda. Ugh. */ + const auto callbackPtr = static_cast>(*)(const std::string&, InputFileCallbackPolicy, T&)>(callback); + if(!callbackPtr) return setFileCallback(nullptr); + + _fileCallbackTemplate = { reinterpret_cast(callbackPtr), static_cast(&userData) }; + setFileCallback([](const std::string& filename, const InputFileCallbackPolicy flags, void* const userData) { + auto& s = *reinterpret_cast(userData); + return reinterpret_cast>(*)(const std::string&, InputFileCallbackPolicy, T&)>(s.callback)(filename, flags, *static_cast(const_cast(s.userData))); + }, &_fileCallbackTemplate); +} +#endif + }} #endif diff --git a/src/Magnum/Text/Test/AbstractFontTest.cpp b/src/Magnum/Text/Test/AbstractFontTest.cpp index d4f8a1a63..4628427b9 100644 --- a/src/Magnum/Text/Test/AbstractFontTest.cpp +++ b/src/Magnum/Text/Test/AbstractFontTest.cpp @@ -25,9 +25,11 @@ #include #include +#include #include #include +#include "Magnum/FileCallback.h" #include "Magnum/Math/Vector2.h" #include "Magnum/Text/AbstractFont.h" #include "Magnum/Text/AbstractGlyphCache.h" @@ -39,8 +41,7 @@ namespace Magnum { namespace Text { namespace Test { namespace { struct AbstractFontTest: TestSuite::Tester { explicit AbstractFontTest(); - void openSingleData(); - + void openData(); void openFileAsData(); void openFileAsDataNotFound(); @@ -48,6 +49,24 @@ struct AbstractFontTest: TestSuite::Tester { void openDataNotSupported(); void openDataNotImplemented(); + #ifdef MAGNUM_BUILD_DEPRECATED + void openSingleDataDeprecated(); + void openMultiDataDeprecated(); + #endif + + void setFileCallback(); + void setFileCallbackTemplate(); + void setFileCallbackTemplateNull(); + void setFileCallbackTemplateConst(); + void setFileCallbackFileOpened(); + void setFileCallbackNotImplemented(); + void setFileCallbackNotSupported(); + void setFileCallbackOpenFileDirectly(); + void setFileCallbackOpenFileThroughBaseImplementation(); + void setFileCallbackOpenFileThroughBaseImplementationFailed(); + void setFileCallbackOpenFileAsData(); + void setFileCallbackOpenFileAsDataFailed(); + void glyphId(); void glyphIdNoFont(); @@ -69,8 +88,7 @@ struct AbstractFontTest: TestSuite::Tester { }; AbstractFontTest::AbstractFontTest() { - addTests({&AbstractFontTest::openSingleData, - + addTests({&AbstractFontTest::openData, &AbstractFontTest::openFileAsData, &AbstractFontTest::openFileAsDataNotFound, @@ -78,6 +96,24 @@ AbstractFontTest::AbstractFontTest() { &AbstractFontTest::openDataNotSupported, &AbstractFontTest::openDataNotImplemented, + #ifdef MAGNUM_BUILD_DEPRECATED + &AbstractFontTest::openSingleDataDeprecated, + &AbstractFontTest::openMultiDataDeprecated, + #endif + + &AbstractFontTest::setFileCallback, + &AbstractFontTest::setFileCallbackTemplate, + &AbstractFontTest::setFileCallbackTemplateNull, + &AbstractFontTest::setFileCallbackTemplateConst, + &AbstractFontTest::setFileCallbackFileOpened, + &AbstractFontTest::setFileCallbackNotImplemented, + &AbstractFontTest::setFileCallbackNotSupported, + &AbstractFontTest::setFileCallbackOpenFileDirectly, + &AbstractFontTest::setFileCallbackOpenFileThroughBaseImplementation, + &AbstractFontTest::setFileCallbackOpenFileThroughBaseImplementationFailed, + &AbstractFontTest::setFileCallbackOpenFileAsData, + &AbstractFontTest::setFileCallbackOpenFileAsDataFailed, + &AbstractFontTest::glyphId, &AbstractFontTest::glyphIdNoFont, @@ -98,45 +134,64 @@ AbstractFontTest::AbstractFontTest() { &AbstractFontTest::createGlyphCacheNoFont}); } -class SingleDataFont: public Text::AbstractFont { - public: - explicit SingleDataFont(): opened(false) {} - +void AbstractFontTest::openData() { + struct: AbstractFont { Features doFeatures() const override { return Feature::OpenData; } - bool doIsOpened() const override { return opened; } + bool doIsOpened() const override { return _opened; } void doClose() override {} - Metrics doOpenSingleData(const Containers::ArrayView data, Float) override { - opened = (data.size() == 1 && data[0] == '\xa5'); - return {}; + Metrics doOpenData(const Containers::ArrayView data, Float size) override { + _opened = (data.size() == 1 && data[0] == '\xa5'); + return {size, 1.0f, 2.0f, 3.0f}; } - UnsignedInt doGlyphId(char32_t) override { return 0; } - + UnsignedInt doGlyphId(char32_t) override { return {}; } Vector2 doGlyphAdvance(UnsignedInt) override { return {}; } - Containers::Pointer doLayout(const AbstractGlyphCache&, Float, const std::string&) override { return nullptr; } - bool opened; -}; + bool _opened = false; + } font; -void AbstractFontTest::openSingleData() { - /* doOpenData() should call doOpenSingleData() */ - SingleDataFont font; - const char data[] = {'\xa5'}; CORRADE_VERIFY(!font.isOpened()); - font.openData({{{}, data}}, 3.0f); + const char a5 = '\xa5'; + font.openData({&a5, 1}, 13.0f); CORRADE_VERIFY(font.isOpened()); + CORRADE_COMPARE(font.size(), 13.0f); + CORRADE_COMPARE(font.ascent(), 1.0f); + CORRADE_COMPARE(font.descent(), 2.0f); + CORRADE_COMPARE(font.lineHeight(), 3.0f); } void AbstractFontTest::openFileAsData() { - /* doOpenFile() should call doOpenSingleData() */ - SingleDataFont font; + struct: AbstractFont { + Features doFeatures() const override { return Feature::OpenData; } + bool doIsOpened() const override { return _opened; } + void doClose() override {} + + Metrics doOpenData(const Containers::ArrayView data, Float size) override { + _opened = (data.size() == 1 && data[0] == '\xa5'); + return {size, 1.0f, 2.0f, 3.0f}; + } + + UnsignedInt doGlyphId(char32_t) override { return {}; } + Vector2 doGlyphAdvance(UnsignedInt) override { return {}; } + Containers::Pointer doLayout(const AbstractGlyphCache&, Float, const std::string&) override { + return nullptr; + } + + bool _opened = false; + } font; + + /* doOpenFile() should call doOpenData() */ CORRADE_VERIFY(!font.isOpened()); - font.openFile(Utility::Directory::join(TEXT_TEST_DIR, "data.bin"), 3.0f); + font.openFile(Utility::Directory::join(TEXT_TEST_DIR, "data.bin"), 13.0f); CORRADE_VERIFY(font.isOpened()); + CORRADE_COMPARE(font.size(), 13.0f); + CORRADE_COMPARE(font.ascent(), 1.0f); + CORRADE_COMPARE(font.descent(), 2.0f); + CORRADE_COMPARE(font.lineHeight(), 3.0f); } void AbstractFontTest::openFileAsDataNotFound() { @@ -194,9 +249,7 @@ void AbstractFontTest::openDataNotSupported() { std::ostringstream out; Error redirectError{&out}; - /** @todo replace this with nullptr once multi-file support is done - properly via callbacks */ - font.openData({}, 34.0f); + font.openData(nullptr, 34.0f); CORRADE_COMPARE(out.str(), "Text::AbstractFont::openData(): feature not supported\n"); } @@ -215,10 +268,482 @@ void AbstractFontTest::openDataNotImplemented() { std::ostringstream out; Error redirectError{&out}; - /** @todo replace this with nullptr and openSingleData() once multi-file - support is done properly via callbacks */ - font.openData({{}}, 34.0f); - CORRADE_COMPARE(out.str(), "Text::AbstractFont::openSingleData(): feature advertised but not implemented\n"); + font.openData(nullptr, 34.0f); + CORRADE_COMPARE(out.str(), "Text::AbstractFont::openData(): feature advertised but not implemented\n"); +} + +#ifdef MAGNUM_BUILD_DEPRECATED +void AbstractFontTest::openSingleDataDeprecated() { + struct: AbstractFont { + Features doFeatures() const override { return Feature::OpenData; } + bool doIsOpened() const override { return _opened; } + void doClose() override {} + + Metrics doOpenData(const Containers::ArrayView data, Float size) override { + _opened = (data.size() == 1 && data[0] == '\xa5'); + return {size, 1.0f, 2.0f, 3.0f}; + } + + UnsignedInt doGlyphId(char32_t) override { return {}; } + Vector2 doGlyphAdvance(UnsignedInt) override { return {}; } + Containers::Pointer doLayout(const AbstractGlyphCache&, Float, const std::string&) override { + return nullptr; + } + + bool _opened = false; + } font; + + CORRADE_VERIFY(!font.isOpened()); + const char a5 = '\xa5'; + CORRADE_IGNORE_DEPRECATED_PUSH + font.openSingleData({&a5, 1}, 13.0f); + CORRADE_IGNORE_DEPRECATED_POP + CORRADE_VERIFY(font.isOpened()); + CORRADE_COMPARE(font.size(), 13.0f); + CORRADE_COMPARE(font.ascent(), 1.0f); + CORRADE_COMPARE(font.descent(), 2.0f); + CORRADE_COMPARE(font.lineHeight(), 3.0f); +} + +void AbstractFontTest::openMultiDataDeprecated() { + struct: AbstractFont { + Features doFeatures() const override { return Feature::OpenData; } + bool doIsOpened() const override { return _opened; } + void doClose() override {} + + Metrics doOpenData(const Containers::ArrayView data, Float size) override { + if(!fileCallback()) return {}; + Containers::Optional> dataExt = fileCallback()("data.ext", InputFileCallbackPolicy::LoadPernament, fileCallbackUserData()); + _opened = (data.size() == 1 && data[0] == '\xa5' && dataExt && + dataExt->size() == 2 && (*dataExt)[1] == '\xee'); + return {size, 1.0f, 2.0f, 3.0f}; + } + + UnsignedInt doGlyphId(char32_t) override { return {}; } + Vector2 doGlyphAdvance(UnsignedInt) override { return {}; } + Containers::Pointer doLayout(const AbstractGlyphCache&, Float, const std::string&) override { + return nullptr; + } + + bool _opened = false; + } font; + + CORRADE_VERIFY(!font.isOpened()); + const char a5 = '\xa5'; + const char ee[] = { '\xff', '\xee' }; + CORRADE_IGNORE_DEPRECATED_PUSH + font.openData({{"data.bin", {&a5, 1}}, + {"data.ext", ee}}, 13.0f); + CORRADE_IGNORE_DEPRECATED_POP + CORRADE_VERIFY(font.isOpened()); + CORRADE_COMPARE(font.size(), 13.0f); + CORRADE_COMPARE(font.ascent(), 1.0f); + CORRADE_COMPARE(font.descent(), 2.0f); + CORRADE_COMPARE(font.lineHeight(), 3.0f); +} +#endif + +void AbstractFontTest::setFileCallback() { + struct: AbstractFont { + Features doFeatures() const override { return Feature::OpenData|Feature::FileCallback; } + bool doIsOpened() const override { return false; } + void doClose() override {} + void doSetFileCallback(Containers::Optional>(*)(const std::string&, InputFileCallbackPolicy, void*), void* userData) override { + *static_cast(userData) = 1337; + } + + UnsignedInt doGlyphId(char32_t) override { return {}; } + Vector2 doGlyphAdvance(UnsignedInt) override { return {}; } + Containers::Pointer doLayout(const AbstractGlyphCache&, Float, const std::string&) override { + return nullptr; + } + } font; + + int a = 0; + auto lambda = [](const std::string&, InputFileCallbackPolicy, void*) { + return Containers::Optional>{}; + }; + font.setFileCallback(lambda, &a); + CORRADE_COMPARE(font.fileCallback(), lambda); + CORRADE_COMPARE(font.fileCallbackUserData(), &a); + CORRADE_COMPARE(a, 1337); +} + +void AbstractFontTest::setFileCallbackTemplate() { + struct: AbstractFont { + Features doFeatures() const override { return Feature::OpenData|Feature::FileCallback; } + bool doIsOpened() const override { return false; } + void doClose() override {} + void doSetFileCallback(Containers::Optional>(*)(const std::string&, InputFileCallbackPolicy, void*), void*) override { + called = true; + } + + UnsignedInt doGlyphId(char32_t) override { return {}; } + Vector2 doGlyphAdvance(UnsignedInt) override { return {}; } + Containers::Pointer doLayout(const AbstractGlyphCache&, Float, const std::string&) override { + return nullptr; + } + + bool called = false; + } font; + + int a = 0; + auto lambda = [](const std::string&, InputFileCallbackPolicy, int&) { + return Containers::Optional>{}; + }; + font.setFileCallback(lambda, a); + CORRADE_VERIFY(font.fileCallback()); + CORRADE_VERIFY(font.fileCallbackUserData()); + CORRADE_VERIFY(font.called); + + /* The data pointers should be wrapped, thus not the same */ + CORRADE_VERIFY(reinterpret_cast(font.fileCallback()) != reinterpret_cast(static_cast>(*)(const std::string&, InputFileCallbackPolicy, int&)>(lambda))); + CORRADE_VERIFY(font.fileCallbackUserData() != &a); +} + +void AbstractFontTest::setFileCallbackTemplateNull() { + struct: AbstractFont { + Features doFeatures() const override { return Feature::OpenData|Feature::FileCallback; } + bool doIsOpened() const override { return false; } + void doClose() override {} + void doSetFileCallback(Containers::Optional>(*callback)(const std::string&, InputFileCallbackPolicy, void*), void* userData) override { + called = !callback && !userData; + } + + UnsignedInt doGlyphId(char32_t) override { return {}; } + Vector2 doGlyphAdvance(UnsignedInt) override { return {}; } + Containers::Pointer doLayout(const AbstractGlyphCache&, Float, const std::string&) override { + return nullptr; + } + + bool called = false; + } font; + + int a = 0; + font.setFileCallback(static_cast>(*)(const std::string&, InputFileCallbackPolicy, int&)>(nullptr), a); + CORRADE_VERIFY(!font.fileCallback()); + CORRADE_VERIFY(!font.fileCallbackUserData()); + CORRADE_VERIFY(font.called); +} + +void AbstractFontTest::setFileCallbackTemplateConst() { + struct: AbstractFont { + Features doFeatures() const override { return Feature::OpenData|Feature::FileCallback; } + bool doIsOpened() const override { return false; } + void doClose() override {} + void doSetFileCallback(Containers::Optional>(*)(const std::string&, InputFileCallbackPolicy, void*), void*) override { + called = true; + } + + UnsignedInt doGlyphId(char32_t) override { return {}; } + Vector2 doGlyphAdvance(UnsignedInt) override { return {}; } + Containers::Pointer doLayout(const AbstractGlyphCache&, Float, const std::string&) override { + return nullptr; + } + + bool called = false; + } font; + + const int a = 0; + auto lambda = [](const std::string&, InputFileCallbackPolicy, const int&) { + return Containers::Optional>{}; + }; + font.setFileCallback(lambda, a); + CORRADE_VERIFY(font.fileCallback()); + CORRADE_VERIFY(font.fileCallbackUserData()); + CORRADE_VERIFY(font.called); +} + +void AbstractFontTest::setFileCallbackFileOpened() { + struct: AbstractFont { + Features doFeatures() const override { return {}; } + bool doIsOpened() const override { return true; } + void doClose() override {} + + UnsignedInt doGlyphId(char32_t) override { return {}; } + Vector2 doGlyphAdvance(UnsignedInt) override { return {}; } + Containers::Pointer doLayout(const AbstractGlyphCache&, Float, const std::string&) override { + return nullptr; + } + } font; + + std::ostringstream out; + Error redirectError{&out}; + + font.setFileCallback([](const std::string&, InputFileCallbackPolicy, void*) { + return Containers::Optional>{}; + }); + CORRADE_COMPARE(out.str(), "Text::AbstractFont::setFileCallback(): can't be set while a font is opened\n"); +} + +void AbstractFontTest::setFileCallbackNotImplemented() { + struct: AbstractFont { + Features doFeatures() const override { return Feature::FileCallback; } + bool doIsOpened() const override { return false; } + void doClose() override {} + + UnsignedInt doGlyphId(char32_t) override { return {}; } + Vector2 doGlyphAdvance(UnsignedInt) override { return {}; } + Containers::Pointer doLayout(const AbstractGlyphCache&, Float, const std::string&) override { + return nullptr; + } + } font; + + int a; + auto lambda = [](const std::string&, InputFileCallbackPolicy, void*) { + return Containers::Optional>{}; + }; + font.setFileCallback(lambda, &a); + CORRADE_COMPARE(font.fileCallback(), lambda); + CORRADE_COMPARE(font.fileCallbackUserData(), &a); + /* Should just work, no need to implement the function */ +} + +void AbstractFontTest::setFileCallbackNotSupported() { + struct: AbstractFont { + Features doFeatures() const override { return {}; } + bool doIsOpened() const override { return false; } + void doClose() override {} + + UnsignedInt doGlyphId(char32_t) override { return {}; } + Vector2 doGlyphAdvance(UnsignedInt) override { return {}; } + Containers::Pointer doLayout(const AbstractGlyphCache&, Float, const std::string&) override { + return nullptr; + } + } font; + + std::ostringstream out; + Error redirectError{&out}; + + int a; + font.setFileCallback([](const std::string&, InputFileCallbackPolicy, void*) { + return Containers::Optional>{}; + }, &a); + CORRADE_COMPARE(out.str(), "Text::AbstractFont::setFileCallback(): font plugin supports neither loading from data nor via callbacks, callbacks can't be used\n"); +} + +void AbstractFontTest::setFileCallbackOpenFileDirectly() { + struct: AbstractFont { + Features doFeatures() const override { return Feature::FileCallback|Feature::OpenData; } + bool doIsOpened() const override { return _opened; } + void doClose() override { _opened = false; } + + Metrics doOpenFile(const std::string& filename, Float size) override { + /* Called because FileCallback is supported */ + _opened = filename == "file.dat" && fileCallback() && fileCallbackUserData(); + return {size, 1.0f, 2.0f, 3.0f}; + } + + Metrics doOpenData(Containers::ArrayView, Float) override { + /* Shouldn't be called because FileCallback is supported */ + openDataCalledNotSureWhy = true; + return {}; + } + + UnsignedInt doGlyphId(char32_t) override { return {}; } + Vector2 doGlyphAdvance(UnsignedInt) override { return {}; } + Containers::Pointer doLayout(const AbstractGlyphCache&, Float, const std::string&) override { + return nullptr; + } + + bool _opened = false; + bool openDataCalledNotSureWhy = false; + } font; + + bool calledNotSureWhy = false; + font.setFileCallback([](const std::string&, InputFileCallbackPolicy, bool& calledNotSureWhy) -> Containers::Optional> { + calledNotSureWhy = true; + return {}; + }, calledNotSureWhy); + + CORRADE_VERIFY(font.openFile("file.dat", 42.0f)); + CORRADE_VERIFY(!calledNotSureWhy); + CORRADE_VERIFY(!font.openDataCalledNotSureWhy); + CORRADE_COMPARE(font.size(), 42.0f); + CORRADE_COMPARE(font.ascent(), 1.0f); + CORRADE_COMPARE(font.descent(), 2.0f); + CORRADE_COMPARE(font.lineHeight(), 3.0f); +} + +void AbstractFontTest::setFileCallbackOpenFileThroughBaseImplementation() { + struct: AbstractFont { + Features doFeatures() const override { return Feature::FileCallback|Feature::OpenData; } + bool doIsOpened() const override { return _opened; } + void doClose() override { _opened = false; } + + Metrics doOpenFile(const std::string& filename, Float size) override { + openFileCalled = filename == "file.dat" && fileCallback() && fileCallbackUserData(); + return AbstractFont::doOpenFile(filename, size); + } + + Metrics doOpenData(Containers::ArrayView data, Float size) override { + _opened = (data.size() == 1 && data[0] == '\xb0'); + return {size, 1.0f, 2.0f, 3.0f}; + } + + UnsignedInt doGlyphId(char32_t) override { return {}; } + Vector2 doGlyphAdvance(UnsignedInt) override { return {}; } + Containers::Pointer doLayout(const AbstractGlyphCache&, Float, const std::string&) override { + return nullptr; + } + + bool _opened = false; + bool openFileCalled = false; + } font; + + struct State { + const char data = '\xb0'; + bool loaded = false; + bool closed = false; + bool calledNotSureWhy = false; + } state; + font.setFileCallback([](const std::string& filename, InputFileCallbackPolicy policy, State& state) -> Containers::Optional> { + if(filename == "file.dat" && policy == InputFileCallbackPolicy::LoadTemporary) { + state.loaded = true; + return Containers::arrayView(&state.data, 1); + } + + if(filename == "file.dat" && policy == InputFileCallbackPolicy::Close) { + state.closed = true; + return {}; + } + + state.calledNotSureWhy = true; + return {}; + }, state); + + CORRADE_VERIFY(font.openFile("file.dat", 42.0f)); + CORRADE_VERIFY(font.openFileCalled); + CORRADE_VERIFY(state.loaded); + CORRADE_VERIFY(state.closed); + CORRADE_VERIFY(!state.calledNotSureWhy); + CORRADE_COMPARE(font.size(), 42.0f); + CORRADE_COMPARE(font.ascent(), 1.0f); + CORRADE_COMPARE(font.descent(), 2.0f); + CORRADE_COMPARE(font.lineHeight(), 3.0f); +} + +void AbstractFontTest::setFileCallbackOpenFileThroughBaseImplementationFailed() { + struct: AbstractFont { + Features doFeatures() const override { return Feature::FileCallback|Feature::OpenData; } + bool doIsOpened() const override { return false; } + void doClose() override {} + + Metrics doOpenFile(const std::string& filename, Float size) override { + openFileCalled = true; + return AbstractFont::doOpenFile(filename, size); + } + + UnsignedInt doGlyphId(char32_t) override { return {}; } + Vector2 doGlyphAdvance(UnsignedInt) override { return {}; } + Containers::Pointer doLayout(const AbstractGlyphCache&, Float, const std::string&) override { + return nullptr; + } + + bool openFileCalled = false; + } font; + + font.setFileCallback([](const std::string&, InputFileCallbackPolicy, void*) -> Containers::Optional> { + return {}; + }); + + std::ostringstream out; + Error redirectError{&out}; + + CORRADE_VERIFY(!font.openFile("file.dat", 42.0f)); + CORRADE_VERIFY(font.openFileCalled); + CORRADE_COMPARE(out.str(), "Text::AbstractFont::openFile(): cannot open file file.dat\n"); +} + +void AbstractFontTest::setFileCallbackOpenFileAsData() { + struct: AbstractFont { + Features doFeatures() const override { return Feature::OpenData; } + bool doIsOpened() const override { return _opened; } + void doClose() override { _opened = false; } + + Metrics doOpenFile(const std::string&, Float) override { + openFileCalled = true; + return {}; + } + + Metrics doOpenData(Containers::ArrayView data, Float size) override { + _opened = (data.size() == 1 && data[0] == '\xb0'); + return {size, 1.0f, 2.0f, 3.0f}; + } + + UnsignedInt doGlyphId(char32_t) override { return {}; } + Vector2 doGlyphAdvance(UnsignedInt) override { return {}; } + Containers::Pointer doLayout(const AbstractGlyphCache&, Float, const std::string&) override { + return nullptr; + } + + bool _opened = false; + bool openFileCalled = false; + } font; + + struct State { + const char data = '\xb0'; + bool loaded = false; + bool closed = false; + bool calledNotSureWhy = false; + } state; + + font.setFileCallback([](const std::string& filename, InputFileCallbackPolicy policy, State& state) -> Containers::Optional> { + if(filename == "file.dat" && policy == InputFileCallbackPolicy::LoadTemporary) { + state.loaded = true; + return Containers::arrayView(&state.data, 1); + } + + if(filename == "file.dat" && policy == InputFileCallbackPolicy::Close) { + state.closed = true; + return {}; + } + + state.calledNotSureWhy = true; + return {}; + }, state); + + CORRADE_VERIFY(font.openFile("file.dat", 13.0f)); + CORRADE_VERIFY(!font.openFileCalled); + CORRADE_VERIFY(state.loaded); + CORRADE_VERIFY(state.closed); + CORRADE_VERIFY(!state.calledNotSureWhy); + CORRADE_COMPARE(font.size(), 13.0f); + CORRADE_COMPARE(font.ascent(), 1.0f); + CORRADE_COMPARE(font.descent(), 2.0f); + CORRADE_COMPARE(font.lineHeight(), 3.0f); +} + +void AbstractFontTest::setFileCallbackOpenFileAsDataFailed() { + struct: AbstractFont { + Features doFeatures() const override { return Feature::OpenData; } + bool doIsOpened() const override { return false; } + void doClose() override {} + + Metrics doOpenFile(const std::string&, Float) override { + openFileCalled = true; + return {}; + } + + UnsignedInt doGlyphId(char32_t) override { return {}; } + Vector2 doGlyphAdvance(UnsignedInt) override { return {}; } + Containers::Pointer doLayout(const AbstractGlyphCache&, Float, const std::string&) override { + return nullptr; + } + + bool openFileCalled = false; + } font; + + font.setFileCallback([](const std::string&, InputFileCallbackPolicy, void*) { + return Containers::Optional>{}; + }); + + std::ostringstream out; + Error redirectError{&out}; + + CORRADE_VERIFY(!font.openFile("file.dat", 132.0f)); + CORRADE_VERIFY(!font.openFileCalled); + CORRADE_COMPARE(out.str(), "Text::AbstractFont::openFile(): cannot open file file.dat\n"); } void AbstractFontTest::glyphId() { diff --git a/src/Magnum/Trade/AbstractImporter.h b/src/Magnum/Trade/AbstractImporter.h index a383b96ee..b9050368f 100644 --- a/src/Magnum/Trade/AbstractImporter.h +++ b/src/Magnum/Trade/AbstractImporter.h @@ -97,6 +97,9 @@ data through to @ref openData(), in case the importer supports at least nor @ref Feature::OpenData, @ref setFileCallback() doesn't allow the callbacks to be set. +The input file callback signature is the same for @ref Trade::AbstractImporter +and @ref Text::AbstractFont to allow code reuse. + @subsection Trade-AbstractImporter-usage-state Internal importer state Some importers, especially ones that make use of well-known external libraries, diff --git a/src/MagnumPlugins/MagnumFont/MagnumFont.cpp b/src/MagnumPlugins/MagnumFont/MagnumFont.cpp index 3e2580b0d..44dcc7d1c 100644 --- a/src/MagnumPlugins/MagnumFont/MagnumFont.cpp +++ b/src/MagnumPlugins/MagnumFont/MagnumFont.cpp @@ -39,8 +39,14 @@ namespace Magnum { namespace Text { struct MagnumFont::Data { + /* Otherwise Clang complains about Utility::Configuration having explicit + constructor when emplace()ing the Pointer. Using = default works on + newer Clang but not older versions. */ + explicit Data() {} + Utility::Configuration conf; - Trade::ImageData2D image; + Containers::Optional image; + Containers::Optional filePath; std::unordered_map glyphId; std::vector glyphAdvance; }; @@ -66,23 +72,26 @@ MagnumFont::MagnumFont(PluginManager::AbstractManager& manager, const std::strin MagnumFont::~MagnumFont() { close(); } -auto MagnumFont::doFeatures() const -> Features { return Feature::OpenData|Feature::MultiFile|Feature::PreparedGlyphCache; } +auto MagnumFont::doFeatures() const -> Features { return Feature::OpenData|Feature::FileCallback|Feature::PreparedGlyphCache; } + +bool MagnumFont::doIsOpened() const { return _opened && _opened->image; } -bool MagnumFont::doIsOpened() const { return _opened; } +void MagnumFont::doClose() { _opened = nullptr; } -auto MagnumFont::doOpenData(const std::vector>>& data, const Float) -> Metrics { - /* We need just the configuration file and image file */ - if(data.size() != 2) { - Error() << "Text::MagnumFont::openData(): wanted two files, got" << data.size(); +auto MagnumFont::doOpenData(const Containers::ArrayView data, const Float) -> Metrics { + if(!_opened) _opened.emplace(); + + if(!_opened->filePath && !fileCallback()) { + Error{} << "Text::MagnumFont::openData(): the font can be opened only from the filesystem or if a file callback is present"; return {}; } /* Open the configuration file */ /* MSVC 2017 requires explicit std::string constructor. MSVC 2015 doesn't. */ - std::istringstream in(std::string{data[0].second.begin(), data[0].second.size()}); + std::istringstream in(std::string{data.begin(), data.size()}); Utility::Configuration conf(in, Utility::Configuration::Flag::SkipComments); if(!conf.isValid() || conf.isEmpty()) { - Error() << "Text::MagnumFont::openData(): cannot open file" << data[0].first; + Error{} << "Text::MagnumFont::openData(): font file is not valid"; return {}; } @@ -93,62 +102,16 @@ auto MagnumFont::doOpenData(const std::vector image = importer.image2D(0); - if(!image) { - Error() << "Text::MagnumFont::openData(): cannot load image file"; - return {}; - } - - return openInternal(std::move(conf), std::move(*image)); -} - -auto MagnumFont::doOpenFile(const std::string& filename, Float) -> Metrics { - /* Open the configuration file */ - Utility::Configuration conf(filename, Utility::Configuration::Flag::ReadOnly|Utility::Configuration::Flag::SkipComments); - if(!conf.isValid() || conf.isEmpty()) { - Error() << "Text::MagnumFont::openFile(): cannot open file" << filename; - return {}; - } - - /* Check version */ - if(conf.value("version") != 1) { - Error() << "Text::MagnumFont::openFile(): unsupported file version, expected 1 but got" - << conf.value("version"); - return {}; - } - - /* Open and load image file */ - const std::string imageFilename = Utility::Directory::join(Utility::Directory::path(filename), conf.value("image")); + /* Open and load image file. Error messages should be printed by the + TgaImporter already, no need to repeat them again. */ Trade::TgaImporter importer; - if(!importer.openFile(imageFilename)) { - Error() << "Text::MagnumFont::openFile(): cannot open image file" << imageFilename; - return {}; - } - Containers::Optional image = importer.image2D(0); - if(!image) { - Error() << "Text::MagnumFont::openFile(): cannot load image file"; - return {}; - } - - return openInternal(std::move(conf), std::move(*image)); -} + importer.setFileCallback(fileCallback(), fileCallbackUserData()); + if(!importer.openFile(Utility::Directory::join(_opened->filePath ? *_opened->filePath : "", conf.value("image")))) return {}; + _opened->image = importer.image2D(0); + if(!_opened->image) return {}; -auto MagnumFont::openInternal(Utility::Configuration&& conf, Trade::ImageData2D&& image) -> Metrics { /* Everything okay, save the data internally */ - _opened = new Data{std::move(conf), std::move(image), std::unordered_map{}, {}}; + _opened->conf = std::move(conf); /* Glyph advances */ const std::vector glyphs = _opened->conf.groups("glyph"); @@ -170,9 +133,11 @@ auto MagnumFont::openInternal(Utility::Configuration&& conf, Trade::ImageData2D& _opened->conf.value("lineHeight")}; } -void MagnumFont::doClose() { - delete _opened; - _opened = nullptr; +auto MagnumFont::doOpenFile(const std::string& filename, Float size) -> Metrics { + _opened.emplace(); + _opened->filePath = Utility::Directory::path(filename); + + return AbstractFont::doOpenFile(filename, size); } UnsignedInt MagnumFont::doGlyphId(const char32_t character) { @@ -188,9 +153,9 @@ Containers::Pointer MagnumFont::doCreateGlyphCache() { /* Set cache image */ Containers::Pointer cache(new Text::GlyphCache( _opened->conf.value("originalImageSize"), - _opened->image.size(), + _opened->image->size(), _opened->conf.value("padding"))); - cache->setImage({}, _opened->image); + cache->setImage({}, *_opened->image); /* Fill glyph map */ const std::vector glyphs = _opened->conf.groups("glyph"); diff --git a/src/MagnumPlugins/MagnumFont/MagnumFont.h b/src/MagnumPlugins/MagnumFont/MagnumFont.h index 58bb906a3..2b0b27fc7 100644 --- a/src/MagnumPlugins/MagnumFont/MagnumFont.h +++ b/src/MagnumPlugins/MagnumFont/MagnumFont.h @@ -32,9 +32,9 @@ #include "Magnum/configure.h" #ifdef MAGNUM_TARGET_GL -#include "Magnum/Text/AbstractFont.h" -#include "Magnum/Trade/Trade.h" +#include +#include "Magnum/Text/AbstractFont.h" #include "MagnumPlugins/MagnumFont/configure.h" #ifndef DOXYGEN_GENERATING_OUTPUT @@ -136,11 +136,9 @@ class MAGNUM_MAGNUMFONT_EXPORT MagnumFont: public AbstractFont { ~MagnumFont(); private: - struct Data; - MAGNUM_MAGNUMFONT_LOCAL Features doFeatures() const override; MAGNUM_MAGNUMFONT_LOCAL bool doIsOpened() const override; - MAGNUM_MAGNUMFONT_LOCAL Metrics doOpenData(const std::vector>>& data, Float) override; + MAGNUM_MAGNUMFONT_LOCAL Metrics doOpenData(Containers::ArrayView data, Float) override; MAGNUM_MAGNUMFONT_LOCAL Metrics doOpenFile(const std::string& filename, Float) override; MAGNUM_MAGNUMFONT_LOCAL void doClose() override; @@ -149,9 +147,8 @@ class MAGNUM_MAGNUMFONT_EXPORT MagnumFont: public AbstractFont { MAGNUM_MAGNUMFONT_LOCAL Containers::Pointer doCreateGlyphCache() override; MAGNUM_MAGNUMFONT_LOCAL Containers::Pointer doLayout(const AbstractGlyphCache& cache, Float size, const std::string& text) override; - MAGNUM_MAGNUMFONT_LOCAL Metrics openInternal(Utility::Configuration&& conf, Trade::ImageData2D&& image); - - Data* _opened; + struct Data; + Containers::Pointer _opened; }; }} diff --git a/src/MagnumPlugins/MagnumFont/Test/MagnumFontTest.cpp b/src/MagnumPlugins/MagnumFont/Test/MagnumFontTest.cpp index 81f9d191a..85d80780a 100644 --- a/src/MagnumPlugins/MagnumFont/Test/MagnumFontTest.cpp +++ b/src/MagnumPlugins/MagnumFont/Test/MagnumFontTest.cpp @@ -24,9 +24,12 @@ */ #include +#include +#include #include #include +#include "Magnum/FileCallback.h" #include "Magnum/GL/OpenGLTester.h" #include "Magnum/Text/AbstractFont.h" #include "Magnum/Text/AbstractGlyphCache.h" @@ -43,6 +46,9 @@ struct MagnumFontTest: TestSuite::Tester { void properties(); void layout(); + void fileCallbackImage(); + void fileCallbackImageNotFound(); + /* Explicitly forbid system-wide plugin dependencies */ PluginManager::Manager _importerManager{"nonexistent"}; PluginManager::Manager _fontManager{"nonexistent"}; @@ -51,7 +57,10 @@ struct MagnumFontTest: TestSuite::Tester { MagnumFontTest::MagnumFontTest() { addTests({&MagnumFontTest::nonexistent, &MagnumFontTest::properties, - &MagnumFontTest::layout}); + &MagnumFontTest::layout, + + &MagnumFontTest::fileCallbackImage, + &MagnumFontTest::fileCallbackImageNotFound}); /* Load the plugins directly from the build tree. Otherwise they're static and already loaded. */ @@ -67,7 +76,7 @@ void MagnumFontTest::nonexistent() { std::ostringstream out; Error redirectError{&out}; CORRADE_VERIFY(!font->openFile("nonexistent.conf", 0.0f)); - CORRADE_COMPARE(out.str(), "Text::MagnumFont::openFile(): cannot open file nonexistent.conf\n"); + CORRADE_COMPARE(out.str(), "Text::AbstractFont::openFile(): cannot open file nonexistent.conf\n"); } void MagnumFontTest::properties() { @@ -130,6 +139,41 @@ void MagnumFontTest::layout() { CORRADE_COMPARE(cursorPosition, Vector2(0.375f, 0.0f)); } +void MagnumFontTest::fileCallbackImage() { + Containers::Pointer font = _fontManager.instantiate("MagnumFont"); + CORRADE_VERIFY(font->features() & AbstractFont::Feature::FileCallback); + + std::unordered_map> files; + files["not/a/path/font.conf"] = Utility::Directory::read(Utility::Directory::join(MAGNUMFONT_TEST_DIR, "font.conf")); + files["not/a/path/font.tga"] = Utility::Directory::read(Utility::Directory::join(MAGNUMFONT_TEST_DIR, "font.tga")); + font->setFileCallback([](const std::string& filename, InputFileCallbackPolicy policy, + std::unordered_map>& files) { + Debug{} << "Loading" << filename << "with" << policy; + return Containers::optional(Containers::ArrayView(files.at(filename))); + }, files); + + CORRADE_VERIFY(font->openFile("not/a/path/font.conf", 13.0f)); + CORRADE_COMPARE(font->size(), 16.0f); + CORRADE_COMPARE(font->ascent(), 25.0f); + CORRADE_COMPARE(font->descent(), -10.0f); + CORRADE_COMPARE(font->lineHeight(), 39.7333f); + CORRADE_COMPARE(font->glyphAdvance(font->glyphId(U'W')), Vector2(23.0f, 0.0f)); +} + +void MagnumFontTest::fileCallbackImageNotFound() { + Containers::Pointer font = _fontManager.instantiate("MagnumFont"); + CORRADE_VERIFY(font->features() & AbstractFont::Feature::FileCallback); + + font->setFileCallback([](const std::string&, InputFileCallbackPolicy, void*) { + return Containers::Optional>{}; + }); + + std::ostringstream out; + Error redirectError{&out}; + CORRADE_VERIFY(!font->openData(Utility::Directory::read(Utility::Directory::join(MAGNUMFONT_TEST_DIR, "font.conf")), 13.0f)); + CORRADE_COMPARE(out.str(), "Trade::AbstractImporter::openFile(): cannot open file font.tga\n"); +} + }}}} CORRADE_TEST_MAIN(Magnum::Text::Test::MagnumFontTest)