Browse Source

Text: allow picking a concrete font ID in a collection.

The AbstractFont::openFile() and openData() now get an additional
argument specifying a font index to allow picking a concrete font index
for example in a TTC collection.

This index has to be specified upfront with no possibility to change it
afterwards, because that's how both FreeType and stb_truetype work. Thus
there are also new fileFontCount() and dataFontCount() APIs taking a
filename / data argument, instead of this being a fontCount() query on
an opened file. The implication from this is that -- unlike basically
all other APIs in Trade and elsewhere -- specifying an out-of-bounds
font index isn't an assertion (i.e., a programmer error) but rather a
graceful failure, because requireing the user to first call
fileFontCount() and then openFile() would mean having to open and
parse the same file twice, which is undesirable overhead.

While this isn't breaking for end users, as it's just a new optional
argument to openFile() and openData(), it's a breaking change for plugin
interfaces. The old doOpenFile() and doOpenData() interfaces are still
there to help transitioning existing plugins, but are marked as
deprecated. Delegating to those turned out to be quite involved, so
there are many new tests verifying this compatibility code path.

The plugin interface string version is bumped but in the next commits
I'm making use of the doOpenData() / doOpenFile() API breakage to do
more changes. Those won't result in further plugin interface changes, so
this set of commits should be treated as a single indivisible change to
the plugin interface. For this reason, uses of AbstractFont outside of
AbstractFontTest (including the MagnumFont plugin) aren't adapted yet
-- they pass tests as before, but compile only with deprecated APIs
enabled.

Co-authored-by: Andrew Snyder <asnyder@minitab.com>
pull/482/merge
Vladimír Vondruš 1 week ago
parent
commit
8ae70a038a
  1. 12
      doc/changelog.dox
  2. 2
      doc/credits.dox
  3. 175
      src/Magnum/Text/AbstractFont.cpp
  4. 141
      src/Magnum/Text/AbstractFont.h
  5. 2412
      src/Magnum/Text/Test/AbstractFontTest.cpp

12
doc/changelog.dox

@ -1005,6 +1005,12 @@ See also:
names and retrieving IDs for particular glyph names.
- @ref Text::AbstractFont::fillGlyphCache() now returns a @cpp bool @ce to
allow font plugin implementations to gracefully report failures
- @ref Text::AbstractFont::openFile() and
@relativeref{Text::AbstractFont,openData()} now allow picking a concrete
font ID from a font collection, along with new
@ref Text::AbstractFont::fileFontCount() and
@relativeref{Text::AbstractFont,dataFontCount()} APIs for querying font
count in a file (see [mosra/magnum#695](https://github.com/mosra/magnum/pull/695))
@subsubsection changelog-latest-changes-texturetools TextureTools library
@ -1658,6 +1664,10 @@ See also:
deprecated in favor of constructors taking an explicit
@ref PixelFormat. The internal texture format is now considered an
implementation detail.
- @ref Text::AbstractFont::doOpenFile() and
@relativeref{Text::AbstractFont,doOpenData()} overloads taking just
file / data and size are deprecated in favor of functions taking an
explicit font index
- The @cpp TextureTools::atlas() @ce utility is deprecated in favor of
@ref TextureTools::AtlasLandfill, which has a vastly better packing
efficiency, supports incremental packing and doesn't force the caller to
@ -3660,7 +3670,7 @@ Released 2019-10-24, tagged as
- @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<const void>, Float)
@cpp Text::AbstractFont::openData(Containers::ArrayView<const void>, Float) @ce
and @ref Text::AbstractFont::setFileCallback()
- @ref GL::Buffer::setData() and @ref GL::Buffer::setSubData() no longer has
overloads taking @ref std::vector / @ref std::array, but instead relies on

2
doc/credits.dox

@ -103,6 +103,8 @@ Are the below lists missing your name or something's wrong?
tick event implementation in @ref Platform::GlfwApplication
- **Andrew ([\@sheerluck](https://github.com/sheerluck))** --- Gentoo package
fixes
- **Andrew Snyder ([\@arsnyder16](https://github.com/arsnyder16))** ---
additions to the @ref Text library
- **Andy Maloney** ([\@asmaloney](https://github.com/asmaloney)) --- CMake
and GDB printer fixes, squashing documentation typos
- **Andy Somogyi** ([\@andysomogyi](https://github.com/andysomogyi)) ---

175
src/Magnum/Text/AbstractFont.cpp

@ -4,6 +4,7 @@
Copyright © 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019,
2020, 2021, 2022, 2023, 2024, 2025, 2026
Vladimír Vondruš <mosra@centrum.cz>
Copyright © 2026 Andrew Snyder <asnyder@minitab.com>
Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the "Software"),
@ -135,7 +136,24 @@ void AbstractFont::setFileCallback(Containers::Optional<Containers::ArrayView<co
void AbstractFont::doSetFileCallback(Containers::Optional<Containers::ArrayView<const char>>(*)(const std::string&, InputFileCallbackPolicy, void*), void*) {}
bool AbstractFont::openData(Containers::ArrayView<const void> data, const Float size) {
Containers::Optional<UnsignedInt> AbstractFont::dataFontCount(const Containers::ArrayView<const void> data) {
CORRADE_ASSERT(features() & FontFeature::OpenData,
"Text::AbstractFont::dataFontCount(): feature not supported", {});
/* Similarly to openData() we accept empty data here (instead of checking
for them and failing so the check doesn't be done on the plugin side)
because for some file formats it could be valid (MagnumFont in
particular). */
const Containers::Optional<UnsignedInt> count = doDataFontCount(Containers::arrayCast<const char>(data));
CORRADE_ASSERT(count != 0,
"Text::AbstractFont::dataFontCount(): implementation returned zero", {});
return count;
}
Containers::Optional<UnsignedInt> AbstractFont::doDataFontCount(Containers::ArrayView<const char>) {
return 1;
}
bool AbstractFont::openData(const Containers::ArrayView<const void> data, const Float size, const UnsignedInt fontId) {
CORRADE_ASSERT(features() & FontFeature::OpenData,
"Text::AbstractFont::openData(): feature not supported", false);
@ -143,7 +161,7 @@ bool AbstractFont::openData(Containers::ArrayView<const void> data, const Float
the check doesn't be done on the plugin side) because for some file
formats it could be valid (MagnumFont in particular). */
close();
const Properties properties = doOpenData(Containers::arrayCast<const char>(data), size);
const Properties properties = doOpenData(Containers::arrayCast<const char>(data), size, fontId);
/* If opening succeeded, save the returned values. If not, the values were
set to their default values by close() already. */
@ -159,18 +177,134 @@ bool AbstractFont::openData(Containers::ArrayView<const void> data, const Float
return false;
}
auto AbstractFont::doOpenData(const Containers::ArrayView<const char> data, const Float size, const UnsignedInt fontId) -> Properties {
#ifndef MAGNUM_BUILD_DEPRECATED
CORRADE_ASSERT_UNREACHABLE("Text::AbstractFont::openData(): feature advertised but not implemented", {});
static_cast<void>(data);
static_cast<void>(size);
static_cast<void>(fontId);
#else
/* If this function is not implemented, fall back to the deprecated
overload that doesn't take a font ID if the requested ID is 0, and fail
otherwise. */
if(fontId != 0) {
Error() << "Text::AbstractFont::openData(): cannot open font at index" << fontId;
return {};
}
CORRADE_IGNORE_DEPRECATED_PUSH
return doOpenData(data, size);
CORRADE_IGNORE_DEPRECATED_POP
#endif
}
#ifdef MAGNUM_BUILD_DEPRECATED
auto AbstractFont::doOpenData(Containers::ArrayView<const char>, Float) -> Properties {
CORRADE_ASSERT_UNREACHABLE("Text::AbstractFont::openData(): feature advertised but not implemented", {});
}
#endif
Containers::Optional<UnsignedInt> AbstractFont::fileFontCount(const Containers::StringView filename) {
/* The logic here is mirroring what's in openFile(), just with delegating
to different APIs. Comments are kept whole, just referring to different
functions, as it's easier to reason about that way. */
Containers::Optional<UnsignedInt> count;
/* If file loading callbacks are not set or the font implementation
supports handling them directly, call into the implementation */
if(!_fileCallback || (doFeatures() & FontFeature::FileCallback)) {
count = doFileFontCount(filename);
/* Otherwise, if loading from data is supported, use the callback and pass
the data through to dataFontCount(). Mark the file as ready to be closed
once opening is finished. */
} else if(doFeatures() & FontFeature::OpenData) {
/* This needs to be duplicated here and in the doFileFontCount()
implementation in order to support both following cases:
- plugins that don't support FileCallback but have their own
doFileFontCount() implementation (callback needs to be used here,
because the base doFileFontCount() 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 doFileFontCount() implementation, because this branch is
never taken in that case) */
const Containers::Optional<Containers::ArrayView<const char>> data = _fileCallback(filename, InputFileCallbackPolicy::LoadTemporary, _fileCallbackUserData);
if(!data) {
Error() << "Text::AbstractFont::fileFontCount(): cannot open file" << filename;
return {};
}
count = doDataFontCount(*data);
_fileCallback(filename, InputFileCallbackPolicy::Close, _fileCallbackUserData);
/* Shouldn't get here, the assert is fired already in setFileCallback() */
} else CORRADE_INTERNAL_ASSERT_UNREACHABLE(); /* LCOV_EXCL_LINE */
CORRADE_ASSERT(count != 0,
"Text::AbstractFont::fileFontCount(): implementation returned zero", {});
return count;
}
Containers::Optional<UnsignedInt> AbstractFont::doFileFontCount(const Containers::StringView filename) {
/* The logic here is mirroring what's in doOpenFile(), just with delegating
to different APIs and having a non-asserting default implementation.
Comments are kept whole, just referring to different functions, as it's
easier to reason about that way. */
if(!(features() & FontFeature::OpenData))
return 1;
Containers::Optional<UnsignedInt> count;
/* 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<Containers::ArrayView<const char>> data = _fileCallback(filename, InputFileCallbackPolicy::LoadTemporary, _fileCallbackUserData);
if(!data) {
Error() << "Text::AbstractFont::fileFontCount(): cannot open file" << filename;
return {};
}
count = doDataFontCount(*data);
_fileCallback(filename, InputFileCallbackPolicy::Close, _fileCallbackUserData);
/* Otherwise open the file directly */
} else {
const Containers::Optional<Containers::Array<char>> data = Utility::Path::read(filename);
if(!data) {
Error() << "Text::AbstractFont::fileFontCount(): cannot open file" << filename;
return {};
}
count = doDataFontCount(*data);
}
return count;
}
bool AbstractFont::openFile(const Containers::StringView filename, const Float size) {
bool AbstractFont::openFile(const Containers::StringView filename, const Float size, const UnsignedInt fontId) {
close();
Properties properties;
/* If file loading callbacks are not set or the font implementation
supports handling them directly, call into the implementation */
if(!_fileCallback || (doFeatures() & FontFeature::FileCallback)) {
#ifdef MAGNUM_BUILD_DEPRECATED
/* Call the deprecated doOpenFile() if fontId is 0, to make plugins
that didn't get adapted to the new APIs still work. If a plugin
doesn't implement the deprecated doOpenFile(), it delegates back to
the new doOpenFile() implementation. */
if(fontId == 0) {
CORRADE_IGNORE_DEPRECATED_PUSH
properties = doOpenFile(filename, size);
CORRADE_IGNORE_DEPRECATED_POP
} else
#endif
{
properties = doOpenFile(filename, size, fontId);
}
/* 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
@ -192,7 +326,7 @@ bool AbstractFont::openFile(const Containers::StringView filename, const Float s
return isOpened();
}
properties = doOpenData(*data, size);
properties = doOpenData(*data, size, fontId);
_fileCallback(filename, InputFileCallbackPolicy::Close, _fileCallbackUserData);
/* Shouldn't get here, the assert is fired already in setFileCallback() */
@ -212,7 +346,20 @@ bool AbstractFont::openFile(const Containers::StringView filename, const Float s
return false;
}
auto AbstractFont::doOpenFile(const Containers::StringView filename, const Float size) -> Properties {
auto AbstractFont::doOpenFile(const Containers::StringView filename, const Float size, const UnsignedInt fontId) -> Properties {
#ifdef MAGNUM_BUILD_DEPRECATED
/* If this function is not implemented and opening data isn't supported
either, assume the plugin implements only the deprecated doOpenFile()
and fail gracefully for a non-zero font ID instead of asserting below.
The reasoning is that plugin implementations should *never* assert for a
font ID out of bounds, no matter whether they were updated to the new
API or not. */
if(!(features() & FontFeature::OpenData) && fontId != 0) {
Error() << "Text::AbstractFont::openFile(): cannot open font at index" << fontId;
return {};
}
#endif
CORRADE_ASSERT(features() & FontFeature::OpenData, "Text::AbstractFont::openFile(): not implemented", {});
Properties properties;
@ -226,7 +373,7 @@ auto AbstractFont::doOpenFile(const Containers::StringView filename, const Float
return {};
}
properties = doOpenData(*data, size);
properties = doOpenData(*data, size, fontId);
_fileCallback(filename, InputFileCallbackPolicy::Close, _fileCallbackUserData);
/* Otherwise open the file directly */
@ -237,12 +384,26 @@ auto AbstractFont::doOpenFile(const Containers::StringView filename, const Float
return {};
}
properties = doOpenData(*data, size);
properties = doOpenData(*data, size, fontId);
}
return properties;
}
#ifdef MAGNUM_BUILD_DEPRECATED
auto AbstractFont::doOpenFile(const Containers::StringView filename, const Float size) -> Properties {
/* This function gets called from openFile() if fontId is 0, to call a
potential implementation in old plugins that didn't get adapted to the
new APIs yet. If a plugin doesn't implement doOpenFile() at all, this
function delegates to the default implementation that then delegates to
doOpenData() (which then again delegates to deprecated doOpenData(). If
a plugin implements the new doOpenFile(), that implementation gets
called from here for fontId 0, and from openFile() for all other font
IDs. */
return doOpenFile(filename, size, 0);
}
#endif
void AbstractFont::close() {
if(!isOpened())
return;

141
src/Magnum/Text/AbstractFont.h

@ -6,6 +6,7 @@
Copyright © 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019,
2020, 2021, 2022, 2023, 2024, 2025, 2026
Vladimír Vondruš <mosra@centrum.cz>
Copyright © 2026 Andrew Snyder <asnyder@minitab.com>
Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the "Software"),
@ -56,7 +57,10 @@ namespace Magnum { namespace Text {
@see @ref FontFeatures, @ref AbstractFont::features()
*/
enum class FontFeature: UnsignedByte {
/** Opening fonts from raw data using @ref AbstractFont::openData() */
/**
* Opening fonts from raw data using @ref AbstractFont::openData() and
* @ref AbstractFont::dataFontCount().
*/
OpenData = 1 << 0,
/**
@ -393,6 +397,57 @@ class MAGNUM_TEXT_EXPORT AbstractFont: public PluginManager::AbstractPlugin {
template<class Callback, class T> void setFileCallback(Callback callback, T& userData);
#endif
/**
* @brief Count of fonts in given data
* @param data Font data
* @m_since_latest
*
* Tries to open given raw data and returns count of fonts in the file.
* Available only if @ref FontFeature::OpenData is supported. A font
* index from this range can be passed to @ref openData() to select a
* particular font inside for example a TrueType Collection (`*.ttc`)
* file. On failure prints a message to @relativeref{Magnum,Error} and
* returns @relativeref{Corrade,Containers::NullOpt}, otherwise it's
* guaranteed to return at least @cpp 1 @ce. Calling this function
* doesn't affect the currently opened font in any way.
*
* Note that if a particular font plugin implementation doesn't support
* font index selection, the function may return @cpp 1 @ce without
* even checking for @p data being valid. In other words, if this
* function succeeds, it doesn't imply that @ref openData() will
* succeed as well. This function exists mainly for font introspection
* purposes.
* @see @ref fileFontCount()
*/
Containers::Optional<UnsignedInt> dataFontCount(Containers::ArrayView<const void> data);
/**
* @brief Count of fonts in given file
* @param filename Font file
* @m_since_latest
*
* Tries to open given font file and returns count of fonts inside. A
* font index from this range can be passed to @ref openFile() to
* select a particular font for example inside a TrueType Collection
* (`*.ttc`) file. On failure prints a message to
* @relativeref{Magnum,Error} and returns
* @relativeref{Corrade,Containers::NullOpt}, otherwise it's guaranteed
* to return at least @cpp 1 @ce. Calling this function doesn't affect
* the currently opened font in any way. 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 dataFontCount() instead. See
* @ref setFileCallback() for more information.
*
* Note that if a particular font plugin implementation doesn't support
* font index selection, the function may return @cpp 1 @ce without
* even checking for @p filename being valid. In other words, if this
* function succeeds, it doesn't imply that @ref openFile() will
* succeed as well. This function exists mainly for font introspection
* purposes.
*/
Containers::Optional<UnsignedInt> fileFontCount(Containers::StringView filename);
/** @brief Whether any file is opened */
bool isOpened() const { return doIsOpened(); }
@ -400,19 +455,29 @@ class MAGNUM_TEXT_EXPORT AbstractFont: public PluginManager::AbstractPlugin {
* @brief Open raw data
* @param data Font data
* @param size Size to open the font in, in points
* @param fontId Font index
*
* Closes previous file, if it was opened, and tries to open given
* raw data. Available only if @ref FontFeature::OpenData is supported.
* On failure prints a message to @relativeref{Magnum,Error} and
* returns @cpp false @ce.
*
* The function will fail if @p fontId is larger than the count of
* fonts in given file. The total font count can be queried using
* @ref dataFontCount(), but as font rasterization libraries commonly
* require picking a concrete font index in order to open a file at
* all and don't allow changing it afterwards, it's faster to directly
* attempt to open a concrete index without checking it against
* @ref dataFontCount() first, and handle a potential failure.
* @see @ref features(), @ref openFile()
*/
bool openData(Containers::ArrayView<const void> data, Float size);
bool openData(Containers::ArrayView<const void> data, Float size, UnsignedInt fontId = 0);
/**
* @brief Open a file
* @param filename Font file
* @param size Size to open the font in, in points
* @param fontId Font index
*
* Closes previous file, if it was opened, and tries to open given
* file. On failure prints a message to @relativeref{Magnum,Error} and
@ -421,8 +486,16 @@ class MAGNUM_TEXT_EXPORT AbstractFont: public PluginManager::AbstractPlugin {
* this function uses the callback to load the file and passes the
* memory view to @ref openData() instead. See @ref setFileCallback()
* for more information.
*
* The function will fail if @p fontId is larger than the count of
* fonts in given file. The total font count can be queried using
* @ref fileFontCount(), but as font rasterization libraries commonly
* require picking a concrete font index in order to open a file at
* all and don't allow changing it afterwards, it's faster to directly
* attempt to open a concrete index without checking it against
* @ref dataFontCount() first, and handle a potential failure.
*/
bool openFile(Containers::StringView filename, Float size);
bool openFile(Containers::StringView filename, Float size, UnsignedInt fontId = 0);
/**
* @brief Close currently opened file
@ -694,8 +767,25 @@ class MAGNUM_TEXT_EXPORT AbstractFont: public PluginManager::AbstractPlugin {
};
protected:
/**
* @brief Implementation for @ref fileFontCount()
* @m_since_latest
*
* If @ref FontFeature::OpenData is supported, default implementation
* opens the file and calls @ref doDataFontCount() with its contents,
* propagating its return value. It is allowed to call this function
* from your @ref doFileFontCount() implementation --- in particular,
* this implementation will also correctly handle callbacks set through
* @ref setFileCallback().
*
* The implementation is expected to return either
* @relativeref{Corrade,Containers::NullOpt} or a non-zero value.
*/
virtual Containers::Optional<UnsignedInt> doFileFontCount(Containers::StringView filename);
/**
* @brief Implementation for @ref openFile()
* @m_since_latest
*
* If @ref doIsOpened() returns @cpp true @ce after calling this
* function, it's assumed that opening was successful and the
@ -713,7 +803,23 @@ class MAGNUM_TEXT_EXPORT AbstractFont: public PluginManager::AbstractPlugin {
* supported --- instead, file is loaded though the callback and data
* passed through to @ref doOpenData().
*/
virtual Properties doOpenFile(Containers::StringView filename, Float size, UnsignedInt fontId);
#ifdef MAGNUM_BUILD_DEPRECATED
/**
* @brief Implementation for @ref openFile()
* @m_deprecated_since_latest Implement @ref doOpenFile(Containers::StringView, Float, UnsignedInt)
* instead.
*/
/* MSVC warns when overriding such methods and there's no way to
suppress that warning, making the RT build (which treats deprecation
warnings as errors) fail and other builds extremely noisy. So
disabling those on MSVC. */
#if !(defined(CORRADE_TARGET_MSVC) && !defined(CORRADE_TARGET_CLANG))
CORRADE_DEPRECATED("implement doOpenFile(Containers::StringView, Float, UnsignedInt) instead")
#endif
virtual Properties doOpenFile(Containers::StringView filename, Float size);
#endif
private:
/** @brief Implementation for @ref features() */
@ -733,8 +839,19 @@ class MAGNUM_TEXT_EXPORT AbstractFont: public PluginManager::AbstractPlugin {
/** @brief Implementation for @ref isOpened() */
virtual bool doIsOpened() const = 0;
/**
* @brief Implementation for @ref dataFontCount()
* @m_since_latest
*
* Default implementation returns @cpp 1 @ce. The implementation is
* expected to return either @relativeref{Corrade,Containers::NullOpt}
* or a non-zero value.
*/
virtual Containers::Optional<UnsignedInt> doDataFontCount(Containers::ArrayView<const char> data);
/**
* @brief Implementation for @ref openData()
* @m_since_latest
*
* If @ref doIsOpened() returns @cpp true @ce after calling this
* function, it's assumed that opening was successful and the
@ -742,7 +859,23 @@ class MAGNUM_TEXT_EXPORT AbstractFont: public PluginManager::AbstractPlugin {
* @ref doIsOpened() returns @cpp false @ce, the returned values are
* ignored.
*/
virtual Properties doOpenData(Containers::ArrayView<const char> data, Float size, UnsignedInt fontId);
#ifdef MAGNUM_BUILD_DEPRECATED
/**
* @brief Implementation for @ref openData()
* @m_deprecated_since_latest Implement @ref doOpenData(Containers::ArrayView<const char>, Float, UnsignedInt)
* instead.
*/
/* MSVC warns when overriding such methods and there's no way to
suppress that warning, making the RT build (which treats deprecation
warnings as errors) fail and other builds extremely noisy. So
disabling those on MSVC. */
#if !(defined(CORRADE_TARGET_MSVC) && !defined(CORRADE_TARGET_CLANG))
CORRADE_DEPRECATED("implement doOpenData(Containers::ArrayView<const char>, Float, UnsignedInt) instead")
#endif
virtual Properties doOpenData(Containers::ArrayView<const char> data, Float size);
#endif
/** @brief Implementation for @ref close() */
virtual void doClose() = 0;
@ -917,7 +1050,7 @@ updated interface string.
*/
/* Silly indentation to make the string appear in pluginInterface() docs */
#define MAGNUM_TEXT_ABSTRACTFONT_PLUGIN_INTERFACE /* [interface] */ \
"cz.mosra.magnum.Text.AbstractFont/0.3.7"
"cz.mosra.magnum.Text.AbstractFont/0.4.0"
/* [interface] */
#ifndef DOXYGEN_GENERATING_OUTPUT

2412
src/Magnum/Text/Test/AbstractFontTest.cpp

File diff suppressed because it is too large Load Diff
Loading…
Cancel
Save