Browse Source

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.
pull/168/head
Vladimír Vondruš 3 years ago
parent
commit
44ffce9067
  1. 69
      doc/snippets/MagnumText.cpp
  2. 197
      src/Magnum/Text/AbstractFont.h
  3. 63
      src/Magnum/Text/AbstractFontConverter.h
  4. 14
      src/Magnum/Text/AbstractGlyphCache.h
  5. 8
      src/Magnum/Text/DistanceFieldGlyphCache.cpp
  6. 46
      src/Magnum/Text/DistanceFieldGlyphCache.h
  7. 27
      src/Magnum/Text/GlyphCache.h
  8. 74
      src/Magnum/Text/Renderer.h

69
doc/snippets/MagnumText.cpp

@ -79,6 +79,12 @@ CORRADE_PLUGIN_REGISTER(MyFontConverter, MyNamespace::MyFontConverter,
MAGNUM_TEXT_ABSTRACTFONTCONVERTER_PLUGIN_INTERFACE) MAGNUM_TEXT_ABSTRACTFONTCONVERTER_PLUGIN_INTERFACE)
/* [MAGNUM_TEXT_ABSTRACTFONTCONVERTER_PLUGIN_INTERFACE] */ /* [MAGNUM_TEXT_ABSTRACTFONTCONVERTER_PLUGIN_INTERFACE] */
namespace {
Vector2i windowSize() { return {}; }
Vector2i framebufferSize() { return {}; }
Vector2 dpiScaling() { return {}; }
}
int main() { int main() {
{ {
@ -86,16 +92,28 @@ int main() {
PluginManager::Manager<Text::AbstractFont> manager; PluginManager::Manager<Text::AbstractFont> manager;
Containers::Pointer<Text::AbstractFont> font = Containers::Pointer<Text::AbstractFont> font =
manager.loadAndInstantiate("StbTrueTypeFont"); 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"; Fatal{} << "Can't open font.ttf with StbTrueTypeFont";
Text::GlyphCache cache{Vector2i{512}}; Text::GlyphCache cache{Vector2i{128}};
font->fillGlyphCache(cache, "abcdefghijklmnopqrstuvwxyz" font->fillGlyphCache(cache, "abcdefghijklmnopqrstuvwxyz"
"ABCDEFGHIJKLMNOPQRSTUVWXYZ" "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
"0123456789?!:;,. "); "0123456789?!:;,. ");
/* [AbstractFont-usage] */ /* [AbstractFont-usage] */
} }
{
PluginManager::Manager<Text::AbstractFont> manager;
Containers::Pointer<Text::AbstractFont> font =
manager.loadAndInstantiate("StbTrueTypeFont");
/* [AbstractFont-usage-data] */
Utility::Resource rs{"data"};
Containers::ArrayView<const char> 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)) #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 /* -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! */ not more! */
PluginManager::Manager<Text::AbstractFont> manager; PluginManager::Manager<Text::AbstractFont> manager;
/* [DistanceFieldGlyphCache-usage] */ /* [DistanceFieldGlyphCache-usage] */
Containers::Pointer<Text::AbstractFont> font = DOXYGEN_ELLIPSIS(manager.loadAndInstantiate("SomethingWhatever")); Containers::Pointer<Text::AbstractFont> font = DOXYGEN_ELLIPSIS(manager.loadAndInstantiate(""));
Text::DistanceFieldGlyphCache cache{Vector2i{2048}, Vector2i{384}, 16}; font->openFile("font.ttf", 96.0f);
Text::DistanceFieldGlyphCache cache{Vector2i{1024}, Vector2i{128}, 12};
font->fillGlyphCache(cache, "abcdefghijklmnopqrstuvwxyz" font->fillGlyphCache(cache, "abcdefghijklmnopqrstuvwxyz"
"ABCDEFGHIJKLMNOPQRSTUVWXYZ" "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
"0123456789?!:;,. "); "0123456789?!:;,. ");
@ -185,8 +205,10 @@ font->fillGlyphCache(cache, "abcdefghijklmnopqrstuvwxyz"
not more! */ not more! */
PluginManager::Manager<Text::AbstractFont> manager; PluginManager::Manager<Text::AbstractFont> manager;
/* [GlyphCache-usage] */ /* [GlyphCache-usage] */
Containers::Pointer<Text::AbstractFont> font = DOXYGEN_ELLIPSIS(manager.loadAndInstantiate("SomethingWhatever")); Containers::Pointer<Text::AbstractFont> font = DOXYGEN_ELLIPSIS(manager.loadAndInstantiate(""));
Text::GlyphCache cache{Vector2i{512}}; font->openFile("font.ttf", 12.0f);
Text::GlyphCache cache{Vector2i{128}};
font->fillGlyphCache(cache, "abcdefghijklmnopqrstuvwxyz" font->fillGlyphCache(cache, "abcdefghijklmnopqrstuvwxyz"
"ABCDEFGHIJKLMNOPQRSTUVWXYZ" "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
"0123456789?!:;,. "); "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 /* -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, the font pointer. I don't care, I just want you to check compilation errors,
not more! */ not more! */
PluginManager::Manager<Text::AbstractFont> manager; PluginManager::Manager<Text::AbstractFont> manager;
/* [Renderer-usage1] */ /* [Renderer-usage1] */
/* Font instance, received from a plugin manager */ /* Font instance, received from a plugin manager */
Containers::Pointer<Text::AbstractFont> font = DOXYGEN_ELLIPSIS(manager.loadAndInstantiate("SomethingWhatever")); Containers::Pointer<Text::AbstractFont> font = DOXYGEN_ELLIPSIS(manager.loadAndInstantiate(""));
/* Configured glyph cache */ /* Open a 12 pt font */
Text::GlyphCache cache{Vector2i{512}}; font->openFile("font.ttf", 12.0f);
/* Populate a glyph cache */
Text::GlyphCache cache{Vector2i{128}};
font->fillGlyphCache(cache, "abcdefghijklmnopqrstuvwxyz" font->fillGlyphCache(cache, "abcdefghijklmnopqrstuvwxyz"
"ABCDEFGHIJKLMNOPQRSTUVWXYZ" "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
"0123456789?!:;,. "); "0123456789?!:;,. ");
@ -213,13 +237,18 @@ Shaders::VectorGL2D shader;
GL::Buffer vertexBuffer, indexBuffer; GL::Buffer vertexBuffer, indexBuffer;
GL::Mesh mesh; GL::Mesh mesh;
/* Render the text, centered */ /* Render a 12 pt text, centered */
std::tie(mesh, std::ignore) = Text::Renderer2D::render(*font, cache, 0.15f, std::tie(mesh, std::ignore) = Text::Renderer2D::render(*font, cache, 12.0f,
"Hello World!", vertexBuffer, indexBuffer, GL::BufferUsage::StaticDraw, "Hello World!", vertexBuffer, indexBuffer, GL::BufferUsage::StaticDraw,
Text::Alignment::LineCenter); 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 */ /* Draw the text on the screen */
shader.setTransformationProjectionMatrix(projectionMatrix) shader
.setTransformationProjectionMatrix(projectionMatrix)
.setColor(0xffffff_rgbf) .setColor(0xffffff_rgbf)
.bindVectorTexture(cache.texture()) .bindVectorTexture(cache.texture())
.draw(mesh); .draw(mesh);
@ -227,7 +256,7 @@ shader.setTransformationProjectionMatrix(projectionMatrix)
/* [Renderer-usage2] */ /* [Renderer-usage2] */
/* Initialize the renderer and reserve memory for enough glyphs */ /* 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); renderer.reserve(32, GL::BufferUsage::DynamicDraw, GL::BufferUsage::StaticDraw);
/* Update the text occasionally */ /* Update the text occasionally */
@ -241,4 +270,16 @@ shader.setTransformationProjectionMatrix(projectionMatrix)
/* [Renderer-usage2] */ /* [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<void>(interfaceSize);
static_cast<void>(sizeMultiplier);
}
} }

197
src/Magnum/Text/AbstractFont.h

@ -73,8 +73,7 @@ enum class FontFeature: UnsignedByte {
#endif #endif
/** /**
* The font contains prepared glyph cache. * The font contains a prepared glyph cache.
*
* @see @ref AbstractFont::fillGlyphCache(), * @see @ref AbstractFont::fillGlyphCache(),
* @ref AbstractFont::createGlyphCache() * @ref AbstractFont::createGlyphCache()
*/ */
@ -100,14 +99,20 @@ MAGNUM_TEXT_EXPORT Debug& operator<<(Debug& debug, FontFeatures value);
/** /**
@brief Base for font plugins @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. glyphs.
@section Text-AbstractFont-usage Usage @section Text-AbstractFont-usage Usage
Fonts are most commonly implemented as plugins. For example, loading a font Fonts are most commonly implemented as plugins, which means the concrete
from the filesystem using the @ref StbTrueTypeFont plugin and prerendering all font implementation is loaded and instantiated through a @relativeref{Corrade,PluginManager::Manager}. A font is opened using either
needed glyphs can be done like this, completely with all error handling: @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 @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 plugins. See @ref GlyphCache for more information about glyph caches and
@ref Renderer for information about actual text rendering. @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 @subsection Text-AbstractFont-usage-callbacks Loading data from memory, using file callbacks
Besides loading data directly from the filesystem using @ref openFile() like 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 Note that the particular importer implementation must support
@ref FontFeature::OpenData for this method to work. @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 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 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 plugins that advertise support for this with @ref FontFeature::FileCallback
this is done by specifying a file loading callback using @ref setFileCallback(). this is done by specifying a file loading callback using @ref setFileCallback().
The callback gets a filename, @ref InputFileCallbackPolicy and an user The callback gets a filename, @ref InputFileCallbackPolicy and an user
pointer as parameters; returns a non-owning view on the loaded data or a pointer as parameters; returns a non-owning view on the loaded data or a
@ref Corrade::Containers::NullOpt "Containers::NullOpt" to indicate the file @relativeref{Corrade,Containers::NullOpt} to indicate the file loading failed.
loading failed. For example, loading a memory-mapped font could look like For example, loading a memory-mapped font could look like below. Note that the
below. Note that the file loading callback affects @ref openFile() as file loading callback affects @ref openFile() as well --- you don't have to
well --- you don't have to load the top-level file manually and pass it to load the top-level file manually and pass it to @ref openData(), any font
@ref openData(), any font plugin supporting the callback feature handles that plugin supporting the callback feature handles that correctly.
correctly.
@snippet MagnumText.cpp AbstractFont-usage-callbacks @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 @ref ShaderTools::AbstractConverter and @ref Trade::AbstractImporter to allow
code reuse. 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 @section Text-AbstractFont-subclassing Subclassing
The plugin implements @ref doFeatures(), @ref doClose(), @ref doLayout(), The plugin needs to implement the @ref doFeatures(), @ref doClose(),
either @ref doCreateGlyphCache() or @ref doFillGlyphCache() and one or more of @ref doLayout() functions, either @ref doCreateGlyphCache() or
`doOpen*()` functions. See also @ref AbstractLayouter for more information. @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 You don't need to do most of the redundant sanity checks, these things are
checked by the implementation: checked by the implementation:
- Functions @ref doOpenData() and @ref doOpenFile() are called after the - The @ref doOpenData() and @ref doOpenFile() functions are called after the
previous file was closed, function @ref doClose() is called only if there previous file was closed, @ref doClose() is called only if there is any
is any file opened. file opened.
- Function @ref doOpenData() is called only if @ref FontFeature::OpenData is - The @ref doOpenData() is called only if @ref FontFeature::OpenData is
supported. supported.
- The @ref doSetFileCallback() function is called only if - The @ref doSetFileCallback() function is called only if
@ref FontFeature::FileCallback is supported and there is no file opened. @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 * @brief Open raw data
* @param data File 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 * 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 * On failure prints a message to @relativeref{Magnum,Error} and
* returns @cpp false @ce. * returns @cpp false @ce.
* @see @ref features(), @ref openFile() * @see @ref features(), @ref openFile()
@ -343,50 +387,70 @@ class MAGNUM_TEXT_EXPORT AbstractFont: public PluginManager::AbstractPlugin {
/** /**
* @brief Open a file * @brief Open a file
* @param filename Font 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 * Closes previous file, if it was opened, and tries to open given
* file. On failure prints a message to @relativeref{Magnum,Error} and * 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); 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(); void close();
/** /**
* @brief Font size * @brief Font size in points
* *
* Returns scale in which @ref lineHeight(), @ref ascent(), * Font size is defined as the distance between @ref ascent() and
* @ref descent() and @ref glyphAdvance() is returned. Expects that a * @ref descent(), thus the value of @cpp (ascent - descent)*0.75f @ce
* font is opened. * (i.e., converted from pixels) is equal to @ref size().
* @see @ref lineHeight(), @ref glyphAdvance()
*/ */
Float size() const; Float size() const;
/** /**
* @brief Font ascent * @brief Font ascent in pixels
* *
* Distance from baseline to top, scaled to font size. Positive value. * Distance from baseline to top, positive value. Font size is defined
* Expects that a font is opened. * as the distance between @ref ascent() and @ref descent(), thus the
* @see @ref size(), @ref descent(), @ref lineHeight() * 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; Float ascent() const;
/** /**
* @brief Font descent * @brief Font descent in pixels
* *
* Distance from baseline to bottom, scalled to font size. Negative * Distance from baseline to bottom, negative value. Font size is defined
* value. Expects that a font is opened. * as the distance between @ref ascent() and @ref descent(), thus the
* @see @ref size(), @ref ascent(), @ref lineHeight() * 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; Float descent() const;
/** /**
* @brief Line height * @brief Line height in pixels
* *
* Returns line height scaled to font size. Expects that a font is * Distance between baselines in consecutive text lines that
* opened. * corresponds to @ref ascent() and @ref descent(). Expects that a font
* @see @ref size(), @ref ascent(), @ref descent() * is opened.
* @see @ref size(), @ref glyphAdvance()
*/ */
Float lineHeight() const; Float lineHeight() const;
@ -401,11 +465,12 @@ class MAGNUM_TEXT_EXPORT AbstractFont: public PluginManager::AbstractPlugin {
UnsignedInt glyphId(char32_t character); UnsignedInt glyphId(char32_t character);
/** /**
* @brief Glyph advance * @brief Glyph advance in pixels
* @param glyph Glyph ID * @param glyph Glyph ID
* *
* Returns glyph advance scaled to font size. Expects that a font is * Distance the cursor for the next glyph that follows @p glyph.
* opened. * 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 * @note This function is meant to be used only for font observations
* and conversions. In performance-critical code the @ref layout() * and conversions. In performance-critical code the @ref layout()
* function should be used instead. * 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 * @brief Layout the text using font's own layouter
* @param cache Glyph cache * @param cache Glyph cache
* @param size Font size * @param size Size to layout the text in, in pooints
* @param text Text to layout * @param text Text to layout
* *
* Note that the layouters support rendering of single-line text only. * Note that the layouters support rendering of single-line text only.
@ -462,25 +527,25 @@ class MAGNUM_TEXT_EXPORT AbstractFont: public PluginManager::AbstractPlugin {
#endif #endif
/** /**
* Font size * Font size in points
* @see @ref size() * @see @ref size()
*/ */
Float size; Float size;
/** /**
* Font ascent * Font ascent in pixels
* @see @ref ascent() * @see @ref ascent()
*/ */
Float ascent; Float ascent;
/** /**
* Font descent * Font descent in pixels
* @see @ref descent() * @see @ref descent()
*/ */
Float descent; Float descent;
/** /**
* Line height * Line height in pixels
* @see @ref lineHeight() * @see @ref lineHeight()
*/ */
Float lineHeight; Float lineHeight;
@ -543,8 +608,8 @@ class MAGNUM_TEXT_EXPORT AbstractFont: public PluginManager::AbstractPlugin {
/** /**
* @brief Implementation for @ref fillGlyphCache() * @brief Implementation for @ref fillGlyphCache()
* *
* The string is converted from UTF-8 to UTF-32, unique characters are * The string is converted from UTF-8 to UTF-32, duplicate characters
* *not* removed. * are *not* removed.
*/ */
virtual void doFillGlyphCache(AbstractGlyphCache& cache, const std::u32string& characters); virtual void doFillGlyphCache(AbstractGlyphCache& cache, const std::u32string& characters);
@ -574,9 +639,11 @@ Returned by @ref AbstractFont::layout().
@section Text-AbstractLayouter-subclassing Subclassing @section Text-AbstractLayouter-subclassing Subclassing
Plugin creates private subclass (no need to expose it to end users) and The @ref AbstractFont plugin creates a local @ref AbstractLayouter subclass and
implements @ref doRenderGlyph(). Bounds checking on @p i is done automatically implements @ref doRenderGlyph(). You don't need to do most of the redundant
in the wrapping @ref renderGlyph() function. 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 { class MAGNUM_TEXT_EXPORT AbstractLayouter {
public: public:
@ -599,13 +666,14 @@ class MAGNUM_TEXT_EXPORT AbstractLayouter {
/** /**
* @brief Render a glyph * @brief Render a glyph
* @param i Glyph index * @param[in] i Glyph index
* @param cursorPosition Cursor position * @param[in,out] cursorPosition Cursor position
* @param rectangle Bounding rectangle * @param[in,out] rectangle Bounding rectangle
* *
* The function returns pair of quad position and texture coordinates, * The function returns a pair of quad position and texture
* advances @p cursorPosition to next character and updates @p rectangle * coordinates, advances @p cursorPosition to next character and
* with extended bounds. * updates @p rectangle with extended bounds. Expects that @p i is less
* than @ref glyphCount().
*/ */
std::pair<Range2D, Range2D> renderGlyph(UnsignedInt i, Vector2& cursorPosition, Range2D& rectangle); std::pair<Range2D, Range2D> renderGlyph(UnsignedInt i, Vector2& cursorPosition, Range2D& rectangle);
@ -616,23 +684,16 @@ class MAGNUM_TEXT_EXPORT AbstractLayouter {
*/ */
explicit AbstractLayouter(UnsignedInt glyphCount); explicit AbstractLayouter(UnsignedInt glyphCount);
#ifdef DOXYGEN_GENERATING_OUTPUT
protected:
#else
private: private:
#endif
/** /**
* @brief Implementation for @ref renderGlyph() * @brief Implementation for @ref renderGlyph()
* @param i Glyph index * @param i Glyph index
* *
* Return quad position (relative to current cursor position), texture * Returns quad position (relative to current cursor position), texture
* coordinates and advance to next glyph. * coordinates and advance to the next glyph.
*/ */
virtual std::tuple<Range2D, Range2D, Vector2> doRenderGlyph(UnsignedInt i) = 0; virtual std::tuple<Range2D, Range2D, Vector2> doRenderGlyph(UnsignedInt i) = 0;
#ifdef DOXYGEN_GENERATING_OUTPUT
private:
#endif
UnsignedInt _glyphCount; UnsignedInt _glyphCount;
}; };

63
src/Magnum/Text/AbstractFontConverter.h

@ -118,14 +118,11 @@ MAGNUM_TEXT_EXPORT Debug& operator<<(Debug& debug, FontConverterFeatures value);
/** /**
@brief Base for font converter plugins @brief Base for font converter plugins
Provides functionality for converting arbitrary font to different format. See Provides functionality for converting an arbitrary font to different format.
@ref plugins for more information and the list of See @ref plugins for more information and the list of
@m_class{m-doc} [derived classes](#derived-classes) for available font @m_class{m-doc} [derived classes](#derived-classes) for available font
converter plugins. 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} @m_class{m-note m-success}
@par @par
@ -133,31 +130,53 @@ font conversion on command-line.
exposes functionality of all font converter plugins through a command line exposes functionality of all font converter plugins through a command line
interface. 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 @section Text-AbstractFontConverter-subclassing Subclassing
Plugin implements @ref doFeatures() and one or more of `exportTo*()` / The plugin needs to implement @ref doFeatures() function and one or more of
`importFrom*()` functions based on what features are supported. Characters @ref doExportFontToData() / @ref doExportFontToSingleData() /
passed to font exporting functions are converted to list of unique UTF-32 @ref doExportFontToFile(), @ref doExportGlyphCacheToData() /
characters. @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 You don't need to do most of the redundant sanity checks, these things are
checked by the implementation: checked by the implementation:
- Functions `doExportFontTo*()` are called only if - The `doExportFontTo*()` functions are called only if
@ref FontConverterFeature::ExportFont is supported, functions @ref FontConverterFeature::ExportFont is supported, the
`doExportGlyphCacheTo*()` are called only if `doExportGlyphCacheTo*()` functions are called only if
@ref FontConverterFeature::ExportGlyphCache is supported. @ref FontConverterFeature::ExportGlyphCache is supported.
- Functions `doImportGlyphCacheFrom*()` are called only if - The `doImportGlyphCacheFrom*()` functions are called only if
@ref FontConverterFeature::ImportGlyphCache is supported. @ref FontConverterFeature::ImportGlyphCache is supported.
- Functions `doExport*To*Data()` and `doImport*From*Data()` are called only - The `doExport*To*Data()` and `doImport*From*Data()` functions are called
if @ref FontConverterFeature::ConvertData is supported. only if @ref FontConverterFeature::ConvertData is supported.
- Function `doImport*FromData()` is called only if there is at least one data - The `doImport*FromData()` function is called only if there is at least one
array passed. data array passed.
@attention @ref Corrade::Containers::Array instances returned from the plugin <b></b>
should *not* use anything else than the default deleter, otherwise this can
cause dangling function pointer call on array destruction if the plugin @m_class{m-block m-warning}
gets unloaded before the array is destroyed.
@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 { class MAGNUM_TEXT_EXPORT AbstractFontConverter: public PluginManager::AbstractPlugin {
public: public:

14
src/Magnum/Text/AbstractGlyphCache.h

@ -93,7 +93,7 @@ class MAGNUM_TEXT_EXPORT AbstractGlyphCache {
/** @brief Features supported by this glyph cache implementation */ /** @brief Features supported by this glyph cache implementation */
GlyphCacheFeatures features() const { return doFeatures(); } GlyphCacheFeatures features() const { return doFeatures(); }
/** Glyph cache texture size */ /** @brief Glyph cache texture size */
Vector2i textureSize() const { return _size; } Vector2i textureSize() const { return _size; }
/** @brief Glyph padding */ /** @brief Glyph padding */
@ -148,7 +148,7 @@ class MAGNUM_TEXT_EXPORT AbstractGlyphCache {
std::vector<Range2Di> reserve(const std::vector<Vector2i>& sizes); std::vector<Range2Di> reserve(const std::vector<Vector2i>& sizes);
/** /**
* @brief Insert glyph to cache * @brief Insert a glyph to the cache
* @param glyph Glyph ID * @param glyph Glyph ID
* @param position Position relative to point on baseline * @param position Position relative to point on baseline
* @param rectangle Region in texture atlas * @param rectangle Region in texture atlas
@ -159,8 +159,8 @@ class MAGNUM_TEXT_EXPORT AbstractGlyphCache {
* *
* Glyph parameters are expected to be without padding. * Glyph parameters are expected to be without padding.
* *
* See also @ref setImage() to upload glyph image. * Use @ref setImage() to upload an image corresponding to the glyphs.
* @see @ref padding() * @see @ref padding(), @ref AbstractFont::fillGlyphCache()
*/ */
void insert(UnsignedInt glyph, const Vector2i& position, const Range2Di& rectangle); void insert(UnsignedInt glyph, const Vector2i& position, const Range2Di& rectangle);
@ -168,9 +168,9 @@ class MAGNUM_TEXT_EXPORT AbstractGlyphCache {
* @brief Set cache image * @brief Set cache image
* *
* Uploads image for one or more glyphs to given offset in cache * Uploads image for one or more glyphs to given offset in cache
* texture. Calls @ref doSetImage(). The @p offset and * texture. The @p offset and @ref ImageView::size() are expected to be
* @ref ImageView::size() are expected tro be in bounds for * in bounds for @ref textureSize().
* @ref textureSize(). * @see @ref AbstractFont::fillGlyphCache()
*/ */
void setImage(const Vector2i& offset, const ImageView2D& image); void setImage(const Vector2i& offset, const ImageView2D& image);

8
src/Magnum/Text/DistanceFieldGlyphCache.cpp

@ -36,15 +36,15 @@
namespace Magnum { namespace Text { 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)) #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) #elif !defined(MAGNUM_TARGET_WEBGL)
/* Luminance is not renderable in most cases */ /* Luminance is not renderable in most cases */
GlyphCache(GL::Context::current().isExtensionSupported<GL::Extensions::EXT::texture_rg>() ? GlyphCache(GL::Context::current().isExtensionSupported<GL::Extensions::EXT::texture_rg>() ?
GL::TextureFormat::R8 : GL::TextureFormat::RGB8, originalSize, size, Vector2i(radius)), GL::TextureFormat::R8 : GL::TextureFormat::RGB8, sourceSize, size, Vector2i(radius)),
#else #else
GlyphCache(GL::TextureFormat::RGB, originalSize, size, Vector2i(radius)), GlyphCache(GL::TextureFormat::RGB, sourceSize, size, Vector2i(radius)),
#endif #endif
_size{size}, _distanceField{radius} _size{size}, _distanceField{radius}
{ {

46
src/Magnum/Text/DistanceFieldGlyphCache.h

@ -40,14 +40,31 @@ namespace Magnum { namespace Text {
/** /**
@brief Glyph cache with distance field rendering @brief Glyph cache with distance field rendering
Unlike original @ref GlyphCache converts each binary image to distance field. Unlike the base @ref GlyphCache, this class converts each binary image to a
It is not possible to use non-binary colors with this cache, internal texture distance field. It's not possible to use non-binary colors with this cache as
format is red channel only. the internal texture format is single-channel.
@section Text-DistanceFieldGlyphCache-usage Usage @section Text-DistanceFieldGlyphCache-usage Usage
Usage is similar to @ref GlyphCache, additionally you need to specify size of In order to create a distance field glyph cache, the font has to be loaded at a
resulting distance field texture. 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 @snippet MagnumText.cpp DistanceFieldGlyphCache-usage
@ -61,20 +78,19 @@ class MAGNUM_TEXT_EXPORT DistanceFieldGlyphCache: public GlyphCache {
public: public:
/** /**
* @brief Constructor * @brief Constructor
* @param originalSize Unscaled glyph cache texture size * @param sourceSize Size of the source image
* @param size Actual glyph cache texture size * @param size Resulting distance field texture size
* @param radius Distance field computation radius * @param radius Distance field computation radius
* *
* See @ref TextureTools::DistanceField for more information about the * See @ref TextureTools::DistanceField for more information about the
* parameters. Sets internal texture format to red channel only. On * parameters. Sets the internal texture format to single-channel.
* desktop OpenGL requires @gl_extension{ARB,texture_rg} (also part of * On OpenGL ES 3.0+ and WebGL 2 uses @ref GL::TextureFormat::R8. On
* OpenGL ES 3.0), in ES2 uses @gl_extension{EXT,texture_rg} if * desktop OpenGL requires @gl_extension{ARB,texture_rg} (part of
* available or @ref GL::TextureFormat::RGB as fallback. * OpenGL 3.0), on ES2 uses @gl_extension{EXT,texture_rg} if available
* @todo Is Luminance format renderable anywhere? Also would it be * or @ref GL::TextureFormat::RGB as fallback, on WebGL 1 uses
* possible to convert the RGB texture to Luminance after it has * @ref GL::TextureFormat::RGB always.
* been rendered when blitting is not supported to save memory?
*/ */
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 * @brief Distance field texture size

27
src/Magnum/Text/GlyphCache.h

@ -40,18 +40,18 @@ namespace Magnum { namespace Text {
/** /**
@brief Glyph cache @brief Glyph cache
Contains font glyphs prerendered into texture atlas. Contains font glyphs rendered into a texture atlas.
@section Text-GlyphCache-usage Usage @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. @ref AbstractFont::createGlyphCache() to fill it with glyphs.
@snippet MagnumText.cpp GlyphCache-usage @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(), @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. 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 * @brief Constructor
* @param internalFormat Internal texture format * @param internalFormat Internal texture format
* @param originalSize Unscaled glyph cache texture size * @param originalSize Unscaled glyph cache texture size in pixels
* @param size Actual glyph cache texture size * @param size Actual glyph cache texture size in pixels
* @param padding Padding around every glyph * @param padding Padding around every glyph in pixels
* *
* All glyphs parameters are saved relative to @p originalSize, * All glyphs parameters are saved relative to @p originalSize,
* although the actual glyph cache texture has @p size. Glyph * although the actual glyph cache texture has @p size. Glyph
@ -81,16 +81,18 @@ class MAGNUM_TEXT_EXPORT GlyphCache: public AbstractGlyphCache {
/** /**
* @brief Constructor * @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 = {}); explicit GlyphCache(GL::TextureFormat internalFormat, const Vector2i& size, const Vector2i& padding = {});
/** /**
* @brief Constructor * @brief Constructor
* *
* Sets internal texture format to red channel only. On desktop OpenGL * Sets the internal texture format to single-channel. On OpenGL ES
* requires @gl_extension{ARB,texture_rg} (also part of OpenGL ES 3.0 and * 3.0+ and WebGL 2 uses @ref GL::TextureFormat::R8. On desktop OpenGL
* WebGL 2), on ES2 unconditionally uses @ref GL::TextureFormat::Luminance. * 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 * This is done for consistency with @ref GL::pixelFormat(), which
* unconditionally returns @ref GL::PixelFormat::Luminance for * unconditionally returns @ref GL::PixelFormat::Luminance for
* @ref PixelFormat::R8Unorm. See * @ref PixelFormat::R8Unorm. See
@ -102,7 +104,8 @@ class MAGNUM_TEXT_EXPORT GlyphCache: public AbstractGlyphCache {
/** /**
* @brief Constructor * @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 = {}); explicit GlyphCache(const Vector2i& size, const Vector2i& padding = {});

74
src/Magnum/Text/Renderer.h

@ -53,7 +53,8 @@ namespace Magnum { namespace Text {
/** /**
@brief Base for text renderers @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 @see @ref Renderer2D, @ref Renderer3D
*/ */
class MAGNUM_TEXT_EXPORT AbstractRenderer { class MAGNUM_TEXT_EXPORT AbstractRenderer {
@ -62,7 +63,7 @@ class MAGNUM_TEXT_EXPORT AbstractRenderer {
* @brief Render text * @brief Render text
* @param font Font * @param font Font
* @param cache Glyph cache * @param cache Glyph cache
* @param size Font size * @param size Font size in points
* @param text Text to render * @param text Text to render
* @param alignment Text alignment * @param alignment Text alignment
* *
@ -79,7 +80,7 @@ class MAGNUM_TEXT_EXPORT AbstractRenderer {
UnsignedInt capacity() const { return _capacity; } UnsignedInt capacity() const { return _capacity; }
/** /**
* @brief Font size * @brief Font size in points
* @m_since_latest * @m_since_latest
*/ */
Float fontSize() const { return _fontSize; } Float fontSize() const { return _fontSize; }
@ -199,6 +200,73 @@ that doesn't recreate everything on each text change:
@snippet MagnumText.cpp Renderer-usage2 @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 @section Text-Renderer-required-opengl-functionality Required OpenGL functionality
Mutable text rendering requires @gl_extension{ARB,map_buffer_range} on desktop Mutable text rendering requires @gl_extension{ARB,map_buffer_range} on desktop

Loading…
Cancel
Save