From 44ffce9067522e0a635781acbec5e9316a77c822 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vladim=C3=ADr=20Vondru=C5=A1?= Date: Fri, 4 Aug 2023 23:51:32 +0200 Subject: [PATCH] Text: review, expand and fix documentation of this library. Especially information related to font sizes, what units are they in and how they relate to actual size on screen was critically lacking. --- doc/snippets/MagnumText.cpp | 69 +++++-- src/Magnum/Text/AbstractFont.h | 199 +++++++++++++------- src/Magnum/Text/AbstractFontConverter.h | 63 ++++--- src/Magnum/Text/AbstractGlyphCache.h | 14 +- src/Magnum/Text/DistanceFieldGlyphCache.cpp | 8 +- src/Magnum/Text/DistanceFieldGlyphCache.h | 46 +++-- src/Magnum/Text/GlyphCache.h | 27 +-- src/Magnum/Text/Renderer.h | 74 +++++++- 8 files changed, 354 insertions(+), 146 deletions(-) diff --git a/doc/snippets/MagnumText.cpp b/doc/snippets/MagnumText.cpp index 93fd37526..0ff2e1f0a 100644 --- a/doc/snippets/MagnumText.cpp +++ b/doc/snippets/MagnumText.cpp @@ -79,6 +79,12 @@ CORRADE_PLUGIN_REGISTER(MyFontConverter, MyNamespace::MyFontConverter, MAGNUM_TEXT_ABSTRACTFONTCONVERTER_PLUGIN_INTERFACE) /* [MAGNUM_TEXT_ABSTRACTFONTCONVERTER_PLUGIN_INTERFACE] */ +namespace { + Vector2i windowSize() { return {}; } + Vector2i framebufferSize() { return {}; } + Vector2 dpiScaling() { return {}; } +} + int main() { { @@ -86,16 +92,28 @@ int main() { PluginManager::Manager manager; Containers::Pointer font = manager.loadAndInstantiate("StbTrueTypeFont"); -if(!font || !font->openFile("font.ttf", 16.0f)) +if(!font->openFile("font.ttf", 12.0f)) Fatal{} << "Can't open font.ttf with StbTrueTypeFont"; -Text::GlyphCache cache{Vector2i{512}}; +Text::GlyphCache cache{Vector2i{128}}; font->fillGlyphCache(cache, "abcdefghijklmnopqrstuvwxyz" "ABCDEFGHIJKLMNOPQRSTUVWXYZ" "0123456789?!:;,. "); /* [AbstractFont-usage] */ } +{ +PluginManager::Manager manager; +Containers::Pointer font = + manager.loadAndInstantiate("StbTrueTypeFont"); +/* [AbstractFont-usage-data] */ +Utility::Resource rs{"data"}; +Containers::ArrayView data = rs.getRaw("font.ttf"); +if(!font->openData(data, 12.0f)) + Fatal{} << "Can't open font data with StbTrueTypeFont"; +/* [AbstractFont-usage-data] */ +} + #if defined(CORRADE_TARGET_UNIX) || (defined(CORRADE_TARGET_WINDOWS) && !defined(CORRADE_TARGET_WINDOWS_RT)) { /* -Wnonnull in GCC 11+ "helpfully" says "this is null" if I don't initialize @@ -171,8 +189,10 @@ font->setFileCallback([](const std::string& filename, not more! */ PluginManager::Manager manager; /* [DistanceFieldGlyphCache-usage] */ -Containers::Pointer font = DOXYGEN_ELLIPSIS(manager.loadAndInstantiate("SomethingWhatever")); -Text::DistanceFieldGlyphCache cache{Vector2i{2048}, Vector2i{384}, 16}; +Containers::Pointer font = DOXYGEN_ELLIPSIS(manager.loadAndInstantiate("")); +font->openFile("font.ttf", 96.0f); + +Text::DistanceFieldGlyphCache cache{Vector2i{1024}, Vector2i{128}, 12}; font->fillGlyphCache(cache, "abcdefghijklmnopqrstuvwxyz" "ABCDEFGHIJKLMNOPQRSTUVWXYZ" "0123456789?!:;,. "); @@ -185,8 +205,10 @@ font->fillGlyphCache(cache, "abcdefghijklmnopqrstuvwxyz" not more! */ PluginManager::Manager manager; /* [GlyphCache-usage] */ -Containers::Pointer font = DOXYGEN_ELLIPSIS(manager.loadAndInstantiate("SomethingWhatever")); -Text::GlyphCache cache{Vector2i{512}}; +Containers::Pointer font = DOXYGEN_ELLIPSIS(manager.loadAndInstantiate("")); +font->openFile("font.ttf", 12.0f); + +Text::GlyphCache cache{Vector2i{128}}; font->fillGlyphCache(cache, "abcdefghijklmnopqrstuvwxyz" "ABCDEFGHIJKLMNOPQRSTUVWXYZ" "0123456789?!:;,. "); @@ -194,17 +216,19 @@ font->fillGlyphCache(cache, "abcdefghijklmnopqrstuvwxyz" } { -Matrix3 projectionMatrix; /* -Wnonnull in GCC 11+ "helpfully" says "this is null" if I don't initialize the font pointer. I don't care, I just want you to check compilation errors, not more! */ PluginManager::Manager manager; /* [Renderer-usage1] */ /* Font instance, received from a plugin manager */ -Containers::Pointer font = DOXYGEN_ELLIPSIS(manager.loadAndInstantiate("SomethingWhatever")); +Containers::Pointer font = DOXYGEN_ELLIPSIS(manager.loadAndInstantiate("")); -/* Configured glyph cache */ -Text::GlyphCache cache{Vector2i{512}}; +/* Open a 12 pt font */ +font->openFile("font.ttf", 12.0f); + +/* Populate a glyph cache */ +Text::GlyphCache cache{Vector2i{128}}; font->fillGlyphCache(cache, "abcdefghijklmnopqrstuvwxyz" "ABCDEFGHIJKLMNOPQRSTUVWXYZ" "0123456789?!:;,. "); @@ -213,13 +237,18 @@ Shaders::VectorGL2D shader; GL::Buffer vertexBuffer, indexBuffer; GL::Mesh mesh; -/* Render the text, centered */ -std::tie(mesh, std::ignore) = Text::Renderer2D::render(*font, cache, 0.15f, +/* Render a 12 pt text, centered */ +std::tie(mesh, std::ignore) = Text::Renderer2D::render(*font, cache, 12.0f, "Hello World!", vertexBuffer, indexBuffer, GL::BufferUsage::StaticDraw, Text::Alignment::LineCenter); +/* Projection matrix is matching application window size to have the size match + 12 pt in other applications, assuming a 96 DPI display and no UI scaling. */ +Matrix3 projectionMatrix = Matrix3::projection(Vector2{windowSize()}); + /* Draw the text on the screen */ -shader.setTransformationProjectionMatrix(projectionMatrix) +shader + .setTransformationProjectionMatrix(projectionMatrix) .setColor(0xffffff_rgbf) .bindVectorTexture(cache.texture()) .draw(mesh); @@ -227,7 +256,7 @@ shader.setTransformationProjectionMatrix(projectionMatrix) /* [Renderer-usage2] */ /* Initialize the renderer and reserve memory for enough glyphs */ -Text::Renderer2D renderer{*font, cache, 0.15f, Text::Alignment::LineCenter}; +Text::Renderer2D renderer{*font, cache, 12.0f, Text::Alignment::LineCenter}; renderer.reserve(32, GL::BufferUsage::DynamicDraw, GL::BufferUsage::StaticDraw); /* Update the text occasionally */ @@ -241,4 +270,16 @@ shader.setTransformationProjectionMatrix(projectionMatrix) /* [Renderer-usage2] */ } +{ +/* [Renderer-dpi-interface-size] */ +Vector2 interfaceSize = Vector2{windowSize()}/dpiScaling(); +/* [Renderer-dpi-interface-size] */ +/* [Renderer-dpi-size-multiplier] */ +Float sizeMultiplier = + (Vector2{framebufferSize()}*dpiScaling()/Vector2{windowSize()}).max(); +/* [Renderer-dpi-size-multiplier] */ +static_cast(interfaceSize); +static_cast(sizeMultiplier); +} + } diff --git a/src/Magnum/Text/AbstractFont.h b/src/Magnum/Text/AbstractFont.h index 32aa023d6..01ca370e2 100644 --- a/src/Magnum/Text/AbstractFont.h +++ b/src/Magnum/Text/AbstractFont.h @@ -73,8 +73,7 @@ enum class FontFeature: UnsignedByte { #endif /** - * The font contains prepared glyph cache. - * + * The font contains a prepared glyph cache. * @see @ref AbstractFont::fillGlyphCache(), * @ref AbstractFont::createGlyphCache() */ @@ -100,14 +99,20 @@ MAGNUM_TEXT_EXPORT Debug& operator<<(Debug& debug, FontFeatures value); /** @brief Base for font plugins -Provides interface for opening fonts, filling glyph cache and layouting the +Provides interface for opening fonts, filling a glyph cache and layouting the glyphs. @section Text-AbstractFont-usage Usage -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: +Fonts are most commonly implemented as plugins, which means the concrete +font implementation is loaded and instantiated through a @relativeref{Corrade,PluginManager::Manager}. A font is opened using either +@ref openFile() or @ref openData() together with specifying the size at which +glyphs will be rasterized. Then it stays open until the font is destroyed, +@ref close() or another font is opened. + +In the following example a font is loaded from the filesystem using the +@ref StbTrueTypeFont plugin, prerendering all needed glyphs, completely with +all error handling: @snippet MagnumText.cpp AbstractFont-usage @@ -116,6 +121,28 @@ of @m_class{m-doc} [derived classes](#derived-classes) 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-font-size Font size + +Font libraries specify font size in *points*, where 1 pt = ~1.333 px at 96 DPI, +so in the above snippet a 12 pt font corresponds to 16 px on a 96 DPI display. +The font size corresponds to the height of the [EM quad](https://en.wikipedia.org/wiki/Em_(typography)) +which is defined as the distance between ascent and descent. + +Upon opening the font, the size in points is exposed in @ref size(). Derived +properties are specified *in pixels* in @ref lineHeight(), @ref ascent() and +@ref descent(). + +The font size used when opening the font affects how large the glyphs will be +when rendered into the @ref GlyphCache. Actual text rendering with @ref Renderer +however uses its own font size, and the rendered size is then additionally +depending on the actual projection used. This decoupling of font sizes is +useful for example in case of @ref DistanceFieldGlyphCache, where a single +prerendered glyph size can be used to render arbitrarily large font sizes +without becoming blurry or jaggy. When not using a distance field glyph cache, +it's usually desirable to have the font size and the actual rendered size +match. See @ref Text-Renderer-usage-font-size "the Renderer documentation" for +further information about picking font sizes. + @subsection Text-AbstractFont-usage-callbacks Loading data from memory, using file callbacks Besides loading data directly from the filesystem using @ref openFile() like @@ -123,18 +150,19 @@ shown above, it's possible to use @ref openData() to import data from memory. Note that the particular importer implementation must support @ref FontFeature::OpenData for this method to work. +@snippet MagnumText.cpp AbstractFont-usage-data + 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 FontFeature::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. +@relativeref{Corrade,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 @@ -149,19 +177,35 @@ The input file callback signature is the same for @ref Text::AbstractFont, @ref ShaderTools::AbstractConverter and @ref Trade::AbstractImporter to allow code reuse. +@section Text-AbstractFont-data-dependency Data dependency + +The @ref AbstractLayouter instances returned from @ref layout() have a code and +data dependency on the dynamic plugin module --- since their implementation is +in the plugin module itself, the plugin can't be unloaded until the returned +instance is destroyed. + @section Text-AbstractFont-subclassing Subclassing -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. +The plugin needs to implement the @ref doFeatures(), @ref doClose(), +@ref doLayout() functions, either @ref doCreateGlyphCache() or +@ref doFillGlyphCache() and one or more of `doOpen*()` functions. See also +@ref AbstractLayouter for more information. + +In order to support @ref FontFeature::FileCallback, the font needs to properly +use the callbacks to both load the top-level file in @ref doOpenFile() and also +load any external files when needed. The @ref doOpenFile() can delegate back +into the base implementation, but it should remember at least the base file +path to pass correct paths to subsequent file callbacks. The +@ref doSetFileCallback() can be overridden in case it's desired to respond to +file loading callback setup, but doesn't have to be. You don't need to do most of the redundant sanity checks, these things are checked by the implementation: -- 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 FontFeature::OpenData is +- The @ref doOpenData() and @ref doOpenFile() functions are called after the + previous file was closed, @ref doClose() is called only if there is any + file opened. +- The @ref doOpenData() is called only if @ref FontFeature::OpenData is supported. - The @ref doSetFileCallback() function is called only if @ref FontFeature::FileCallback is supported and there is no file opened. @@ -316,10 +360,10 @@ class MAGNUM_TEXT_EXPORT AbstractFont: public PluginManager::AbstractPlugin { /** * @brief Open raw data * @param data File data - * @param size Font size + * @param size Font size in points * * Closes previous file, if it was opened, and tries to open given - * file. Available only if @ref FontFeature::OpenData is supported. + * raw data. Available only if @ref FontFeature::OpenData is supported. * On failure prints a message to @relativeref{Magnum,Error} and * returns @cpp false @ce. * @see @ref features(), @ref openFile() @@ -343,50 +387,70 @@ class MAGNUM_TEXT_EXPORT AbstractFont: public PluginManager::AbstractPlugin { /** * @brief Open a file * @param filename Font file - * @param size Font size + * @param size Size to open the font in, in points * * Closes previous file, if it was opened, and tries to open given * file. On failure prints a message to @relativeref{Magnum,Error} and - * returns @cpp false @ce. + * returns @cpp false @ce. If file loading callbacks are set via + * @ref setFileCallback() and @ref FontFeature::OpenData is supported, + * this function uses the callback to load the file and passes the + * memory view to @ref openData() instead. See @ref setFileCallback() + * for more information. */ bool openFile(const std::string& filename, Float size); - /** @brief Close currently opened file */ + /** + * @brief Close currently opened file + * + * On certain implementations an explicit call to this function when + * the file is no longer needed but the font instance is going to be + * reused further may result in freed memory. This call is also done + * automatically when the font instance gets destructed or when another + * file is opened. If no file is opened, does nothing. After this + * function is called, @ref isOpened() is guaranteed to return + * @cpp false @ce. + */ void close(); /** - * @brief Font size + * @brief Font size in points * - * Returns scale in which @ref lineHeight(), @ref ascent(), - * @ref descent() and @ref glyphAdvance() is returned. Expects that a - * font is opened. + * Font size is defined as the distance between @ref ascent() and + * @ref descent(), thus the value of @cpp (ascent - descent)*0.75f @ce + * (i.e., converted from pixels) is equal to @ref size(). + * @see @ref lineHeight(), @ref glyphAdvance() */ Float size() const; /** - * @brief Font ascent + * @brief Font ascent in pixels * - * Distance from baseline to top, scaled to font size. Positive value. - * Expects that a font is opened. - * @see @ref size(), @ref descent(), @ref lineHeight() + * Distance from baseline to top, positive value. Font size is defined + * as the distance between @ref ascent() and @ref descent(), thus the + * value of @cpp (ascent - descent)*0.75f @ce (i.e., converted to + * points) is equal to @ref size(). Expects that a font is opened. + * @see @ref lineHeight(), @ref glyphAdvance() */ Float ascent() const; /** - * @brief Font descent + * @brief Font descent in pixels * - * Distance from baseline to bottom, scalled to font size. Negative - * value. Expects that a font is opened. - * @see @ref size(), @ref ascent(), @ref lineHeight() + * Distance from baseline to bottom, negative value. Font size is defined + * as the distance between @ref ascent() and @ref descent(), thus the + * value of @cpp (ascent - descent)*0.75f @ce (i.e., converted to + * points) is equal to @ref size(). Expects that a font is opened. + * @see @ref lineHeight(), @ref glyphAdvance() */ Float descent() const; /** - * @brief Line height + * @brief Line height in pixels * - * Returns line height scaled to font size. Expects that a font is - * opened. - * @see @ref size(), @ref ascent(), @ref descent() + * Distance between baselines in consecutive text lines that + * corresponds to @ref ascent() and @ref descent(). Expects that a font + * is opened. + * @see @ref size(), @ref glyphAdvance() */ Float lineHeight() const; @@ -401,11 +465,12 @@ class MAGNUM_TEXT_EXPORT AbstractFont: public PluginManager::AbstractPlugin { UnsignedInt glyphId(char32_t character); /** - * @brief Glyph advance + * @brief Glyph advance in pixels * @param glyph Glyph ID * - * Returns glyph advance scaled to font size. Expects that a font is - * opened. + * Distance the cursor for the next glyph that follows @p glyph. + * Doesn't consider kerning or any other advanced shaping features. + * Expects that a font is opened. * @note This function is meant to be used only for font observations * and conversions. In performance-critical code the @ref layout() * function should be used instead. @@ -438,7 +503,7 @@ class MAGNUM_TEXT_EXPORT AbstractFont: public PluginManager::AbstractPlugin { /** * @brief Layout the text using font's own layouter * @param cache Glyph cache - * @param size Font size + * @param size Size to layout the text in, in pooints * @param text Text to layout * * Note that the layouters support rendering of single-line text only. @@ -462,25 +527,25 @@ class MAGNUM_TEXT_EXPORT AbstractFont: public PluginManager::AbstractPlugin { #endif /** - * Font size + * Font size in points * @see @ref size() */ Float size; /** - * Font ascent + * Font ascent in pixels * @see @ref ascent() */ Float ascent; /** - * Font descent + * Font descent in pixels * @see @ref descent() */ Float descent; /** - * Line height + * Line height in pixels * @see @ref lineHeight() */ Float lineHeight; @@ -543,8 +608,8 @@ class MAGNUM_TEXT_EXPORT AbstractFont: public PluginManager::AbstractPlugin { /** * @brief Implementation for @ref fillGlyphCache() * - * The string is converted from UTF-8 to UTF-32, unique characters are - * *not* removed. + * The string is converted from UTF-8 to UTF-32, duplicate characters + * are *not* removed. */ virtual void doFillGlyphCache(AbstractGlyphCache& cache, const std::u32string& characters); @@ -574,9 +639,11 @@ Returned by @ref AbstractFont::layout(). @section Text-AbstractLayouter-subclassing Subclassing -Plugin creates private subclass (no need to expose it to end users) and -implements @ref doRenderGlyph(). Bounds checking on @p i is done automatically -in the wrapping @ref renderGlyph() function. +The @ref AbstractFont plugin creates a local @ref AbstractLayouter subclass and +implements @ref doRenderGlyph(). You don't need to do most of the redundant +sanity checks, these things are checked by the implementation: + +- The @ref doRenderGlyph() is called only if `i` is from valid range */ class MAGNUM_TEXT_EXPORT AbstractLayouter { public: @@ -599,13 +666,14 @@ class MAGNUM_TEXT_EXPORT AbstractLayouter { /** * @brief Render a glyph - * @param i Glyph index - * @param cursorPosition Cursor position - * @param rectangle Bounding rectangle - * - * The function returns pair of quad position and texture coordinates, - * advances @p cursorPosition to next character and updates @p rectangle - * with extended bounds. + * @param[in] i Glyph index + * @param[in,out] cursorPosition Cursor position + * @param[in,out] rectangle Bounding rectangle + * + * The function returns a pair of quad position and texture + * coordinates, advances @p cursorPosition to next character and + * updates @p rectangle with extended bounds. Expects that @p i is less + * than @ref glyphCount(). */ std::pair renderGlyph(UnsignedInt i, Vector2& cursorPosition, Range2D& rectangle); @@ -616,23 +684,16 @@ class MAGNUM_TEXT_EXPORT AbstractLayouter { */ explicit AbstractLayouter(UnsignedInt glyphCount); - #ifdef DOXYGEN_GENERATING_OUTPUT - protected: - #else private: - #endif /** * @brief Implementation for @ref renderGlyph() * @param i Glyph index * - * Return quad position (relative to current cursor position), texture - * coordinates and advance to next glyph. + * Returns quad position (relative to current cursor position), texture + * coordinates and advance to the next glyph. */ virtual std::tuple doRenderGlyph(UnsignedInt i) = 0; - #ifdef DOXYGEN_GENERATING_OUTPUT - private: - #endif UnsignedInt _glyphCount; }; diff --git a/src/Magnum/Text/AbstractFontConverter.h b/src/Magnum/Text/AbstractFontConverter.h index 1ab272d28..ddd3436bc 100644 --- a/src/Magnum/Text/AbstractFontConverter.h +++ b/src/Magnum/Text/AbstractFontConverter.h @@ -118,14 +118,11 @@ MAGNUM_TEXT_EXPORT Debug& operator<<(Debug& debug, FontConverterFeatures value); /** @brief Base for font converter plugins -Provides functionality for converting arbitrary font to different format. See -@ref plugins for more information and the list of +Provides functionality for converting an arbitrary font to different format. +See @ref plugins for more information and the list of @m_class{m-doc} [derived classes](#derived-classes) for available font converter plugins. -You can use the @ref magnum-fontconverter "magnum-fontconverter" utility to do -font conversion on command-line. - @m_class{m-note m-success} @par @@ -133,31 +130,53 @@ font conversion on command-line. exposes functionality of all font converter plugins through a command line interface. +@section Text-AbstractFontConverter-data-dependency Data dependency + +The @ref AbstractGlyphCache instances returned from various functions +* *by design* have no dependency on the converter instance and neither on the +dynamic plugin module. In other words, you don't need to keep the converter +instance (or the plugin manager instance) around in order to have the +@ref AbstractGlyphCache instances valid. Moreover, all returned +@relativeref{Corrade,Containers::Array} instances are only allowed to have +default deleters --- this is to avoid potential dangling function pointer calls +when destructing such instances after the plugin module has been unloaded. + @section Text-AbstractFontConverter-subclassing Subclassing -Plugin implements @ref doFeatures() and one or more of `exportTo*()` / -`importFrom*()` functions based on what features are supported. Characters -passed to font exporting functions are converted to list of unique UTF-32 -characters. +The plugin needs to implement @ref doFeatures() function and one or more of +@ref doExportFontToData() / @ref doExportFontToSingleData() / +@ref doExportFontToFile(), @ref doExportGlyphCacheToData() / +@ref doExportGlyphCacheToSingleData() / @ref doExportGlyphCacheToFile() or +@ref doImportGlyphCacheFromData() / @ref doImportGlyphCacheFromSingleData() / +@ref doImportGlyphCacheFromFile() functions based on what features are +supported. Characters passed to font exporting functions are converted to list +of unique UTF-32 characters. You don't need to do most of the redundant sanity checks, these things are checked by the implementation: -- Functions `doExportFontTo*()` are called only if - @ref FontConverterFeature::ExportFont is supported, functions - `doExportGlyphCacheTo*()` are called only if +- The `doExportFontTo*()` functions are called only if + @ref FontConverterFeature::ExportFont is supported, the + `doExportGlyphCacheTo*()` functions are called only if @ref FontConverterFeature::ExportGlyphCache is supported. -- Functions `doImportGlyphCacheFrom*()` are called only if +- The `doImportGlyphCacheFrom*()` functions are called only if @ref FontConverterFeature::ImportGlyphCache is supported. -- Functions `doExport*To*Data()` and `doImport*From*Data()` are called only - if @ref FontConverterFeature::ConvertData is supported. -- Function `doImport*FromData()` is called only if there is at least one data - array passed. - -@attention @ref Corrade::Containers::Array instances returned from the plugin - should *not* use anything else than the default deleter, otherwise this can - cause dangling function pointer call on array destruction if the plugin - gets unloaded before the array is destroyed. +- The `doExport*To*Data()` and `doImport*From*Data()` functions are called + only if @ref FontConverterFeature::ConvertData is supported. +- The `doImport*FromData()` function is called only if there is at least one + data array passed. + + + +@m_class{m-block m-warning} + +@par Dangling function pointers on plugin unload + As @ref Text-AbstractFontConverter-data-dependency "mentioned above", + @relativeref{Corrade::Containers,Array} instances returned from plugin + implementations are not allowed to use anything else than the default + deleter, otherwise this could cause dangling function pointer call on array + destruction if the plugin gets unloaded before the array is destroyed. This + is asserted by the base implementation on return. */ class MAGNUM_TEXT_EXPORT AbstractFontConverter: public PluginManager::AbstractPlugin { public: diff --git a/src/Magnum/Text/AbstractGlyphCache.h b/src/Magnum/Text/AbstractGlyphCache.h index c76fe88be..edc551c61 100644 --- a/src/Magnum/Text/AbstractGlyphCache.h +++ b/src/Magnum/Text/AbstractGlyphCache.h @@ -93,7 +93,7 @@ class MAGNUM_TEXT_EXPORT AbstractGlyphCache { /** @brief Features supported by this glyph cache implementation */ GlyphCacheFeatures features() const { return doFeatures(); } - /** Glyph cache texture size */ + /** @brief Glyph cache texture size */ Vector2i textureSize() const { return _size; } /** @brief Glyph padding */ @@ -148,7 +148,7 @@ class MAGNUM_TEXT_EXPORT AbstractGlyphCache { std::vector reserve(const std::vector& sizes); /** - * @brief Insert glyph to cache + * @brief Insert a glyph to the cache * @param glyph Glyph ID * @param position Position relative to point on baseline * @param rectangle Region in texture atlas @@ -159,8 +159,8 @@ class MAGNUM_TEXT_EXPORT AbstractGlyphCache { * * Glyph parameters are expected to be without padding. * - * See also @ref setImage() to upload glyph image. - * @see @ref padding() + * Use @ref setImage() to upload an image corresponding to the glyphs. + * @see @ref padding(), @ref AbstractFont::fillGlyphCache() */ void insert(UnsignedInt glyph, const Vector2i& position, const Range2Di& rectangle); @@ -168,9 +168,9 @@ class MAGNUM_TEXT_EXPORT AbstractGlyphCache { * @brief Set cache image * * Uploads image for one or more glyphs to given offset in cache - * texture. Calls @ref doSetImage(). The @p offset and - * @ref ImageView::size() are expected tro be in bounds for - * @ref textureSize(). + * texture. The @p offset and @ref ImageView::size() are expected to be + * in bounds for @ref textureSize(). + * @see @ref AbstractFont::fillGlyphCache() */ void setImage(const Vector2i& offset, const ImageView2D& image); diff --git a/src/Magnum/Text/DistanceFieldGlyphCache.cpp b/src/Magnum/Text/DistanceFieldGlyphCache.cpp index ee950acce..f9c8d73e7 100644 --- a/src/Magnum/Text/DistanceFieldGlyphCache.cpp +++ b/src/Magnum/Text/DistanceFieldGlyphCache.cpp @@ -36,15 +36,15 @@ namespace Magnum { namespace Text { -DistanceFieldGlyphCache::DistanceFieldGlyphCache(const Vector2i& originalSize, const Vector2i& size, const UnsignedInt radius): +DistanceFieldGlyphCache::DistanceFieldGlyphCache(const Vector2i& sourceSize, const Vector2i& size, const UnsignedInt radius): #if !(defined(MAGNUM_TARGET_GLES) && defined(MAGNUM_TARGET_GLES2)) - GlyphCache(GL::TextureFormat::R8, originalSize, size, Vector2i(radius)), + GlyphCache(GL::TextureFormat::R8, sourceSize, size, Vector2i(radius)), #elif !defined(MAGNUM_TARGET_WEBGL) /* Luminance is not renderable in most cases */ GlyphCache(GL::Context::current().isExtensionSupported() ? - GL::TextureFormat::R8 : GL::TextureFormat::RGB8, originalSize, size, Vector2i(radius)), + GL::TextureFormat::R8 : GL::TextureFormat::RGB8, sourceSize, size, Vector2i(radius)), #else - GlyphCache(GL::TextureFormat::RGB, originalSize, size, Vector2i(radius)), + GlyphCache(GL::TextureFormat::RGB, sourceSize, size, Vector2i(radius)), #endif _size{size}, _distanceField{radius} { diff --git a/src/Magnum/Text/DistanceFieldGlyphCache.h b/src/Magnum/Text/DistanceFieldGlyphCache.h index 363d1f7b5..bfc754c62 100644 --- a/src/Magnum/Text/DistanceFieldGlyphCache.h +++ b/src/Magnum/Text/DistanceFieldGlyphCache.h @@ -40,14 +40,31 @@ namespace Magnum { namespace Text { /** @brief Glyph cache with distance field rendering -Unlike original @ref GlyphCache converts each binary image to distance field. -It is not possible to use non-binary colors with this cache, internal texture -format is red channel only. +Unlike the base @ref GlyphCache, this class converts each binary image to a +distance field. It's not possible to use non-binary colors with this cache as +the internal texture format is single-channel. @section Text-DistanceFieldGlyphCache-usage Usage -Usage is similar to @ref GlyphCache, additionally you need to specify size of -resulting distance field texture. +In order to create a distance field glyph cache, the font has to be loaded at a +size significantly larger than what the resulting text will be. The distance +field conversion process then converts the input to a fraction of its size +again, transferring the the extra spatial resolution to distance values. The +distance values are then used to render an arbitrarily sized text without it +being jaggy at small sizes and blurry when large. + +The process requires three input parameters, size of the source image, size of +the resulting glyph cache image and a radius for the distance field creation. +The ratio between the input and output image size is usually four or eight +times, and the size of the font should match the larger size. So, for example, +if a @cpp {128, 128} @ce @ref GlyphCache was filled with a 12 pt font, a +@cpp {1024, 1024} @ce source image for the distance field should use a 96 pt +font. The radius should then be chosen so it's at least one or two pixels in +the scaled-down result, so in this case at least 8. Values less than that will +result in aliasing artifacts. Very high radius values are needed only if +outlining, thinning, thickening or shadow effects will be used when rendering, +using them leads to precision loss when the distance field is stored in 8-bit +channels. @snippet MagnumText.cpp DistanceFieldGlyphCache-usage @@ -61,20 +78,19 @@ class MAGNUM_TEXT_EXPORT DistanceFieldGlyphCache: public GlyphCache { public: /** * @brief Constructor - * @param originalSize Unscaled glyph cache texture size - * @param size Actual glyph cache texture size + * @param sourceSize Size of the source image + * @param size Resulting distance field texture size * @param radius Distance field computation radius * * See @ref TextureTools::DistanceField for more information about the - * parameters. Sets internal texture format to red channel only. On - * desktop OpenGL requires @gl_extension{ARB,texture_rg} (also part of - * OpenGL ES 3.0), in ES2 uses @gl_extension{EXT,texture_rg} if - * available or @ref GL::TextureFormat::RGB as fallback. - * @todo Is Luminance format renderable anywhere? Also would it be - * possible to convert the RGB texture to Luminance after it has - * been rendered when blitting is not supported to save memory? + * parameters. Sets the internal texture format to single-channel. + * On OpenGL ES 3.0+ and WebGL 2 uses @ref GL::TextureFormat::R8. On + * desktop OpenGL requires @gl_extension{ARB,texture_rg} (part of + * OpenGL 3.0), on ES2 uses @gl_extension{EXT,texture_rg} if available + * or @ref GL::TextureFormat::RGB as fallback, on WebGL 1 uses + * @ref GL::TextureFormat::RGB always. */ - explicit DistanceFieldGlyphCache(const Vector2i& originalSize, const Vector2i& size, UnsignedInt radius); + explicit DistanceFieldGlyphCache(const Vector2i& sourceSize, const Vector2i& size, UnsignedInt radius); /** * @brief Distance field texture size diff --git a/src/Magnum/Text/GlyphCache.h b/src/Magnum/Text/GlyphCache.h index 270f3c0d0..7c5318175 100644 --- a/src/Magnum/Text/GlyphCache.h +++ b/src/Magnum/Text/GlyphCache.h @@ -40,18 +40,18 @@ namespace Magnum { namespace Text { /** @brief Glyph cache -Contains font glyphs prerendered into texture atlas. +Contains font glyphs rendered into a texture atlas. @section Text-GlyphCache-usage Usage -Create GlyphCache object with sufficient size and then call +Create the @ref GlyphCache object with sufficient size and then call @ref AbstractFont::createGlyphCache() to fill it with glyphs. @snippet MagnumText.cpp GlyphCache-usage -See @ref Renderer for information about text rendering. +See the @ref Renderer class for information about text rendering. -This class supports the @ref GlyphCacheFeature::ImageDownload (and thus calling +This class supports @ref GlyphCacheFeature::ImageDownload (and thus calling @ref image()) only on desktop OpenGL, due to using @ref GL::Texture::image(), which is not available on @ref MAGNUM_TARGET_GLES "OpenGL ES" platforms. @@ -68,9 +68,9 @@ class MAGNUM_TEXT_EXPORT GlyphCache: public AbstractGlyphCache { /** * @brief Constructor * @param internalFormat Internal texture format - * @param originalSize Unscaled glyph cache texture size - * @param size Actual glyph cache texture size - * @param padding Padding around every glyph + * @param originalSize Unscaled glyph cache texture size in pixels + * @param size Actual glyph cache texture size in pixels + * @param padding Padding around every glyph in pixels * * All glyphs parameters are saved relative to @p originalSize, * although the actual glyph cache texture has @p size. Glyph @@ -81,16 +81,18 @@ class MAGNUM_TEXT_EXPORT GlyphCache: public AbstractGlyphCache { /** * @brief Constructor * - * Same as calling the above with @p originalSize and @p size the same. + * Same as calling the above with @p originalSize and @p size being set + * to the same value. */ explicit GlyphCache(GL::TextureFormat internalFormat, const Vector2i& size, const Vector2i& padding = {}); /** * @brief Constructor * - * Sets internal texture format to red channel only. On desktop OpenGL - * requires @gl_extension{ARB,texture_rg} (also part of OpenGL ES 3.0 and - * WebGL 2), on ES2 unconditionally uses @ref GL::TextureFormat::Luminance. + * Sets the internal texture format to single-channel. On OpenGL ES + * 3.0+ and WebGL 2 uses @ref GL::TextureFormat::R8. On desktop OpenGL + * requires @gl_extension{ARB,texture_rg} (also part of OpenGL 3.0). On + * ES2 and WebGL 1 unconditionally uses @ref GL::TextureFormat::Luminance. * This is done for consistency with @ref GL::pixelFormat(), which * unconditionally returns @ref GL::PixelFormat::Luminance for * @ref PixelFormat::R8Unorm. See @@ -102,7 +104,8 @@ class MAGNUM_TEXT_EXPORT GlyphCache: public AbstractGlyphCache { /** * @brief Constructor * - * Same as calling the above with @p originalSize and @p size the same. + * Same as calling the above with @p originalSize and @p size being set + * to the same value. */ explicit GlyphCache(const Vector2i& size, const Vector2i& padding = {}); diff --git a/src/Magnum/Text/Renderer.h b/src/Magnum/Text/Renderer.h index f383c04c0..fda88e09f 100644 --- a/src/Magnum/Text/Renderer.h +++ b/src/Magnum/Text/Renderer.h @@ -53,7 +53,8 @@ namespace Magnum { namespace Text { /** @brief Base for text renderers -Not meant to be used directly, see @ref Renderer for more information. +Not meant to be used directly, see the @ref Renderer class for more +information. @see @ref Renderer2D, @ref Renderer3D */ class MAGNUM_TEXT_EXPORT AbstractRenderer { @@ -62,7 +63,7 @@ class MAGNUM_TEXT_EXPORT AbstractRenderer { * @brief Render text * @param font Font * @param cache Glyph cache - * @param size Font size + * @param size Font size in points * @param text Text to render * @param alignment Text alignment * @@ -79,7 +80,7 @@ class MAGNUM_TEXT_EXPORT AbstractRenderer { UnsignedInt capacity() const { return _capacity; } /** - * @brief Font size + * @brief Font size in points * @m_since_latest */ Float fontSize() const { return _fontSize; } @@ -199,6 +200,73 @@ that doesn't recreate everything on each text change: @snippet MagnumText.cpp Renderer-usage2 +@subsection Text-Renderer-usage-font-size Font size + +As mentioned in @ref Text-AbstractFont-usage-font-size "AbstractFont class documentation", +the size at which the font is loaded is decoupled from the size at which a +concrete text is rendered. In particular, with a concrete projection matrix, +the size you pass to either @ref render() or to the @ref Renderer() constructor +will always result in the same size of the rendered text, independently of the +size the font was loaded in. Size of the loaded font is the size at which the +glyphs get prerendered into the glyph cache, affecting visual quality. + +When rendering the text, there are two common approaches --- either setting up +the size to match a global user interface scale, or having the text size +proportional to the window size. The first approach results in e.g. a 12 pt +font matching a 12 pt font in other applications, and is what's shown in the +above snippets. The most straightforward way to achieve that is to set up the +projection matrix size to match actual window pixels, such as @ref Platform::Sdl2Application::windowSize() "Platform::*Application::windowSize()". +If using the regular @ref GlyphCache, for best visual quality it should be +created with the @ref AbstractFont loaded at the same size as the text to be +rendered, although often a double supersampling achieves a crisper result. +I.e., loading the font with 24 pt, but rendering with 12 pt. See below for +@ref Text-Renderer-usage-font-size-dpi "additional considerations for proper DPI awareness". + +The second approach, with text size being relative to the window size, is for +cases where the text is meant to match surrounding art, such as in a game menu. +In this case the projection size is usually something arbitrary that doesn't +match window pixels, and the text point size then has to be relative to that. +For this use case a @ref DistanceFieldGlyphCache is the better match, as it can +provide text at different sizes with minimal quality loss. See its +documentation for details about picking the right font size and other +parameters for best results. + +@subsection Text-Renderer-usage-font-size-dpi DPI awareness + +To achieve crisp rendering and/or text size matching other applications on +HiDPI displays, additional steps need to be taken. There are two separate +concepts for DPI-aware rendering: + +- Interface size --- size at which the interface elements are positioned on + the screen. Often, for simplicity, the interface is using some "virtual + units", so a 12 pt font is still a 12 pt font independently of how the + interface is scaled compared to actual display properties (for example by + setting a global 150% scale in the desktop environment, or by zooming a + browser window). The size used by the @ref Renderer should match these + virtual units. +- Framebuffer size --- how many pixels is actually there. If a 192 DPI + display has a 200% interface scale, a 12 pt font would be 32 pixels. But if + it only has a 150% scale, all interface elements will be smaller, and a 12 + pt font would be only 24 pixels. The size used by the @ref AbstractFont and + @ref GlyphCache should be chosen with respect to the actual physical + pixels. + +When using for example @ref Platform::Sdl2Application or other `*Application` +implementations, you usually have three values at your disposal --- +@ref Platform::Sdl2Application::windowSize() "windowSize()", +@ref Platform::Sdl2Application::framebufferSize() "framebufferSize()" and +@ref Platform::Sdl2Application::dpiScaling() "dpiScaling()". Their relation is +documented thoroughly in @ref Platform-Sdl2Application-dpi, for this particular +case a scaled interface size, used instead of window size for the projection, +would be calculated like this: + +@snippet MagnumText.cpp Renderer-dpi-interface-size + +And a multiplier for the @ref AbstractFont and @ref GlyphCache font size like +this. The @ref Renderer keeps using the size without this multiplier. + +@snippet MagnumText.cpp Renderer-dpi-size-multiplier + @section Text-Renderer-required-opengl-functionality Required OpenGL functionality Mutable text rendering requires @gl_extension{ARB,map_buffer_range} on desktop