From 631b9b8ae547783030d91ee2c552ce22889a82a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vladim=C3=ADr=20Vondru=C5=A1?= Date: Tue, 23 Apr 2024 16:50:04 -0700 Subject: [PATCH] Text: AbstractFont::fillGlyphCache() overload taking glyph IDs directly. --- doc/changelog.dox | 3 + src/Magnum/Text/AbstractFont.cpp | 69 ++++- src/Magnum/Text/AbstractFont.h | 28 +- src/Magnum/Text/Test/AbstractFontTest.cpp | 241 ++++++++++++++++-- .../Test/MagnumFontConverterTest.cpp | 5 +- 5 files changed, 312 insertions(+), 34 deletions(-) diff --git a/doc/changelog.dox b/doc/changelog.dox index b8e865e34..1a3d16ff2 100644 --- a/doc/changelog.dox +++ b/doc/changelog.dox @@ -837,6 +837,9 @@ See also: - Added a @ref Text::GlyphCache::GlyphCache(NoCreateT) and @ref Text::DistanceFieldGlyphCache::DistanceFieldGlyphCache(NoCreateT) constructor allowing to construct the object without a GL context present +- New @ref Text::AbstractFont::fillGlyphCache() overload taking a list of + glyph IDs instead of a UTF-8 string to allow filling the glyph cache with + glyphs that don't directly map to Unicode - @ref Text::AbstractFont::fillGlyphCache() now returns a @cpp bool @ce to allow font plugin implementations to gracefully report failures diff --git a/src/Magnum/Text/AbstractFont.cpp b/src/Magnum/Text/AbstractFont.cpp index 3711fb03d..692c21f84 100644 --- a/src/Magnum/Text/AbstractFont.cpp +++ b/src/Magnum/Text/AbstractFont.cpp @@ -27,7 +27,9 @@ #include /** @todo remove once file callbacks are -free */ #include +#include #include +#include #include #include #include @@ -294,14 +296,71 @@ bool AbstractFont::fillGlyphCache(AbstractGlyphCache& cache, const Containers::S CORRADE_ASSERT(!(features() & FontFeature::PreparedGlyphCache), "Text::AbstractFont::fillGlyphCache(): feature not supported", {}); - const Containers::Optional> utf32 = Utility::Unicode::utf32(characters); - CORRADE_ASSERT(utf32, - "Text::AbstractFont::fillGlyphCache(): not a valid UTF-8 string:" << characters, {}); + struct Glyph { + char32_t character; + UnsignedInt glyph; + }; + + /* Convert UTF-8 to Unicode codepoints */ + Containers::Array glyphs; + arrayReserve(glyphs, characters.size()); + for(std::size_t i = 0; i != characters.size(); ) { + const Containers::Pair next = Utility::Unicode::nextChar(characters, i); + CORRADE_ASSERT(next.first() != U'\xffffffff', + "Text::AbstractFont::fillGlyphCache(): not a valid UTF-8 string:" << characters, {}); + arrayAppend(glyphs, InPlaceInit, next.first(), 0u); + i = next.second(); + } + + /* Convert the codepoints to glyph IDs */ + glyphIdsInto(stridedArrayView(glyphs).slice(&Glyph::character), + stridedArrayView(glyphs).slice(&Glyph::glyph)); + + /* If this font isn't in the cache yet, include also the invalid glyph */ + if(!cache.findFont(*this)) + arrayAppend(glyphs, InPlaceInit, U'\0', 0u); + + /* Create a unique (ordered) set */ + /** @todo reuse the memory from `glyphs` for this somehow? tho there could + be thousands of glyphs and the `glyphs` might be just a few entries */ + Containers::BitArray uniqueGlyphs{ValueInit, _glyphCount}; + for(const Glyph& glyph: glyphs) + uniqueGlyphs.set(glyph.glyph); + + /* Convert the unique set back to a list of glyph IDs, reusing the original + glyph memory */ + const std::size_t uniqueCount = uniqueGlyphs.count(); + CORRADE_INTERNAL_ASSERT(uniqueCount <= glyphs.size()); + std::size_t offset = 0; + for(UnsignedInt i = 0; i != uniqueGlyphs.size(); ++i) + if(uniqueGlyphs[i]) glyphs[offset++].glyph = i; + CORRADE_INTERNAL_ASSERT(offset == uniqueCount); + + /* Pass the unique set to the implementation */ + return doFillGlyphCache(cache, stridedArrayView(glyphs).slice(&Glyph::glyph).prefix(uniqueCount)); +} + +bool AbstractFont::fillGlyphCache(AbstractGlyphCache& cache, const Containers::StridedArrayView1D& glyphs) { + CORRADE_ASSERT(isOpened(), + "Text::AbstractFont::fillGlyphCache(): no font opened", {}); + CORRADE_ASSERT(!(features() & FontFeature::PreparedGlyphCache), + "Text::AbstractFont::fillGlyphCache(): feature not supported", {}); + + #ifndef CORRADE_NO_DEBUG_ASSERT + Containers::BitArray uniqueGlyphs{ValueInit, _glyphCount}; + for(const UnsignedInt& glyph: glyphs) { + CORRADE_DEBUG_ASSERT(glyph < _glyphCount, + "Text::AbstractFont::fillGlyphCache(): index" << glyph << "out of range for" << _glyphCount << "glyphs", {}); + CORRADE_DEBUG_ASSERT(!uniqueGlyphs[glyph], + "Text::AbstractFont::fillGlyphCache(): duplicate glyph" << glyph, {}); + uniqueGlyphs.set(glyph); + } + #endif - return doFillGlyphCache(cache, *utf32); + return doFillGlyphCache(cache, glyphs); } -bool AbstractFont::doFillGlyphCache(AbstractGlyphCache&, Containers::ArrayView) { +bool AbstractFont::doFillGlyphCache(AbstractGlyphCache&, const Containers::StridedArrayView1D&) { CORRADE_ASSERT_UNREACHABLE("Text::AbstractFont::fillGlyphCache(): feature advertised but not implemented", {}); return {}; } diff --git a/src/Magnum/Text/AbstractFont.h b/src/Magnum/Text/AbstractFont.h index 11cec7a0e..9764d7226 100644 --- a/src/Magnum/Text/AbstractFont.h +++ b/src/Magnum/Text/AbstractFont.h @@ -503,20 +503,34 @@ class MAGNUM_TEXT_EXPORT AbstractFont: public PluginManager::AbstractPlugin { Vector2 glyphAdvance(UnsignedInt glyph); /** - * @brief Fill glyph cache with given character set + * @brief Fill glyph cache with given glyph IDs * @param cache Glyph cache instance - * @param characters UTF-8 characters to render + * @param glyphs Glyph IDs to render + * @m_since_latest * - * Fills the cache with given characters. Fonts having + * Fills the cache with given glyph IDs. Fonts having * @ref FontFeature::PreparedGlyphCache do not support partial glyph * cache filling, use @ref createGlyphCache() instead. Expects that a - * font is opened and @p characters is valid UTF-8. + * font is opened and @p glyphs are all unique and less than + * @ref glyphCount(). * * On success returns @cpp true @ce. On failure, for example if the * @p cache doesn't have expected format or the @p characters can't * fit, prints a message to @relativeref{Magnum,Error} and returns * @cpp false @ce. */ + bool fillGlyphCache(AbstractGlyphCache& cache, const Containers::StridedArrayView1D& glyphs); + + /** + * @brief Fill glyph cache with given character set + * @param cache Glyph cache instance + * @param characters UTF-8 characters to render + * + * Converts @p characters to a list of Unicode codepoints, gets glyph + * IDs for them using @ref glyphIdsInto(), removes duplicates, adds the + * glyph @cpp 0 @ce if the font is not in @p cache already, and + * delegates to @ref fillGlyphCache(AbstractGlyphCache&, const Containers::StridedArrayView1D&). + */ bool fillGlyphCache(AbstractGlyphCache& cache, Containers::StringView characters); /** @@ -684,10 +698,10 @@ class MAGNUM_TEXT_EXPORT AbstractFont: public PluginManager::AbstractPlugin { /** * @brief Implementation for @ref fillGlyphCache() * - * The string is converted from UTF-8 to UTF-32, duplicate characters - * are *not* removed. + * The @p glyphs are guaranteed to be unique and all less than + * @ref glyphCount(). */ - virtual bool doFillGlyphCache(AbstractGlyphCache& cache, Containers::ArrayView characters); + virtual bool doFillGlyphCache(AbstractGlyphCache& cache, const Containers::StridedArrayView1D& glyphs); /** @brief Implementation for @ref createGlyphCache() */ virtual Containers::Pointer doCreateGlyphCache(); diff --git a/src/Magnum/Text/Test/AbstractFontTest.cpp b/src/Magnum/Text/Test/AbstractFontTest.cpp index cedd8c153..0d31ee63d 100644 --- a/src/Magnum/Text/Test/AbstractFontTest.cpp +++ b/src/Magnum/Text/Test/AbstractFontTest.cpp @@ -91,6 +91,9 @@ struct AbstractFontTest: TestSuite::Tester { void glyphSizeAdvanceOutOfRange(); void fillGlyphCache(); + void fillGlyphCacheOutOfRange(); + void fillGlyphCacheNotUnique(); + void fillGlyphCacheFromString(); void fillGlyphCacheFailed(); void fillGlyphCacheNotSupported(); void fillGlyphCacheNotImplemented(); @@ -157,6 +160,9 @@ AbstractFontTest::AbstractFontTest() { &AbstractFontTest::glyphSizeAdvanceOutOfRange, &AbstractFontTest::fillGlyphCache, + &AbstractFontTest::fillGlyphCacheOutOfRange, + &AbstractFontTest::fillGlyphCacheNotUnique, + &AbstractFontTest::fillGlyphCacheFromString, &AbstractFontTest::fillGlyphCacheFailed, &AbstractFontTest::fillGlyphCacheNotSupported, &AbstractFontTest::fillGlyphCacheNotImplemented, @@ -1020,59 +1026,226 @@ struct DummyGlyphCache: AbstractGlyphCache { void AbstractFontTest::fillGlyphCache() { struct MyFont: AbstractFont { - FontFeatures doFeatures() const override { return {}; } - bool doIsOpened() const override { return true; } + FontFeatures doFeatures() const override { + return FontFeature::OpenData; + } + bool doIsOpened() const override { return _opened; } void doClose() override {} - void doGlyphIdsInto(const Containers::StridedArrayView1D&, const Containers::StridedArrayView1D&) override {} + Properties doOpenData(Containers::ArrayView, Float) override { + _opened = true; + return {0.0f, 0.0f, 0.0f, 0.0f, 17}; + } + + void doGlyphIdsInto(const Containers::StridedArrayView1D&, const Containers::StridedArrayView1D&) override { + CORRADE_FAIL("This should not be called."); + } Vector2 doGlyphSize(UnsignedInt) override { return {}; } Vector2 doGlyphAdvance(UnsignedInt) override { return {}; } Containers::Pointer doCreateShaper() override { return {}; } - bool doFillGlyphCache(AbstractGlyphCache& cache, Containers::ArrayView characters) override { + bool doFillGlyphCache(AbstractGlyphCache& cache, const Containers::StridedArrayView1D& glyphs) override { CORRADE_COMPARE(cache.size(), (Vector3i{100, 100, 1})); - CORRADE_COMPARE_AS(characters, Containers::arrayView({ - 'h', 'e', 'l', 'o' + /* The glyph list isn't sorted in this case, nothing is implicitly + added to it either */ + CORRADE_COMPARE_AS(glyphs, Containers::arrayView({ + 16u, 5u, 11u, 2u }), TestSuite::Compare::Container); called = true; return true; } bool called = false; + + private: + bool _opened = false; } font; - /* Capture correct function name */ - CORRADE_VERIFY(true); + /* Have to explicitly open in order to make glyphCount() non-zero */ + CORRADE_VERIFY(font.openData(nullptr, 0.0f)); DummyGlyphCache cache{PixelFormat::R8Unorm, {100, 100}}; - CORRADE_VERIFY(font.fillGlyphCache(cache, "helo")); + CORRADE_VERIFY(font.fillGlyphCache(cache, Containers::arrayView({16u, 5u, 11u, 2u}))); CORRADE_VERIFY(font.called); } -void AbstractFontTest::fillGlyphCacheFailed() { +void AbstractFontTest::fillGlyphCacheOutOfRange() { + CORRADE_SKIP_IF_NO_DEBUG_ASSERT(); + struct MyFont: AbstractFont { - FontFeatures doFeatures() const override { return {}; } - bool doIsOpened() const override { return true; } + FontFeatures doFeatures() const override { + return FontFeature::OpenData; + } + bool doIsOpened() const override { return _opened; } + void doClose() override {} + + Properties doOpenData(Containers::ArrayView, Float) override { + _opened = true; + return {0.0f, 0.0f, 0.0f, 0.0f, 16}; + } + + void doGlyphIdsInto(const Containers::StridedArrayView1D&, const Containers::StridedArrayView1D&) override {} + Vector2 doGlyphSize(UnsignedInt) override { return {}; } + Vector2 doGlyphAdvance(UnsignedInt) override { return {}; } + Containers::Pointer doCreateShaper() override { return {}; } + + private: + bool _opened = false; + } font; + + /* Have to explicitly open in order to make glyphCount() non-zero */ + CORRADE_VERIFY(font.openData(nullptr, 0.0f)); + + std::ostringstream out; + Error redirectError{&out}; + DummyGlyphCache cache{PixelFormat::R8Unorm, {100, 100}}; + font.fillGlyphCache(cache, Containers::arrayView({0u, 15u, 3u, 16u, 80u})); + CORRADE_COMPARE(out.str(), + "Text::AbstractFont::fillGlyphCache(): index 16 out of range for 16 glyphs\n"); +} + +void AbstractFontTest::fillGlyphCacheNotUnique() { + CORRADE_SKIP_IF_NO_DEBUG_ASSERT(); + + struct MyFont: AbstractFont { + FontFeatures doFeatures() const override { + return FontFeature::OpenData; + } + bool doIsOpened() const override { return _opened; } void doClose() override {} + Properties doOpenData(Containers::ArrayView, Float) override { + _opened = true; + return {0.0f, 0.0f, 0.0f, 0.0f, 16}; + } + void doGlyphIdsInto(const Containers::StridedArrayView1D&, const Containers::StridedArrayView1D&) override {} Vector2 doGlyphSize(UnsignedInt) override { return {}; } Vector2 doGlyphAdvance(UnsignedInt) override { return {}; } Containers::Pointer doCreateShaper() override { return {}; } - bool doFillGlyphCache(AbstractGlyphCache&, Containers::ArrayView) override { + private: + bool _opened = false; + } font; + + /* Have to explicitly open in order to make glyphCount() non-zero */ + CORRADE_VERIFY(font.openData(nullptr, 0.0f)); + + std::ostringstream out; + Error redirectError{&out}; + DummyGlyphCache cache{PixelFormat::R8Unorm, {100, 100}}; + font.fillGlyphCache(cache, Containers::arrayView({0u, 15u, 3u, 15u, 80u})); + CORRADE_COMPARE(out.str(), + "Text::AbstractFont::fillGlyphCache(): duplicate glyph 15\n"); +} + +void AbstractFontTest::fillGlyphCacheFromString() { + struct MyFont: AbstractFont { + FontFeatures doFeatures() const override { + return FontFeature::OpenData; + } + bool doIsOpened() const override { return _opened; } + void doClose() override {} + + Properties doOpenData(Containers::ArrayView, Float) override { + _opened = true; + return {0.0f, 0.0f, 0.0f, 0.0f, 17}; + } + + void doGlyphIdsInto(const Containers::StridedArrayView1D& characters, const Containers::StridedArrayView1D& glyphs) override { + CORRADE_COMPARE_AS(characters, Containers::arrayView({ + 'h', 'e', 'l', 'l', 'o' + }), TestSuite::Compare::Container); + glyphs[0] = 16; + glyphs[1] = 2; + glyphs[2] = 11; + glyphs[3] = 11; + glyphs[4] = 5; + ++glyphIdsIntoCalled; + } + Vector2 doGlyphSize(UnsignedInt) override { return {}; } + Vector2 doGlyphAdvance(UnsignedInt) override { return {}; } + Containers::Pointer doCreateShaper() override { return {}; } + + bool doFillGlyphCache(AbstractGlyphCache& cache, const Containers::StridedArrayView1D& glyphs) override { + CORRADE_COMPARE(cache.size(), (Vector3i{100, 100, 1})); + /* The array should be sorted by ID, without duplicates and with + the first ID being 0 if the cache doesn't have this font yet */ + if(!cache.fontCount()) + CORRADE_COMPARE_AS(glyphs, Containers::arrayView({ + 0u, 2u, 5u, 11u, 16u + }), TestSuite::Compare::Container); + else + CORRADE_COMPARE_AS(glyphs, Containers::arrayView({ + 2u, 5u, 11u, 16u + }), TestSuite::Compare::Container); + ++fillGlyphCacheCalled; + return true; + } + + Int glyphIdsIntoCalled = 0, + fillGlyphCacheCalled = 0; + + private: + bool _opened = false; + } font; + + /* Have to explicitly open in order to make glyphCount() non-zero */ + CORRADE_VERIFY(font.openData(nullptr, 0.0f)); + + DummyGlyphCache cache{PixelFormat::R8Unorm, {100, 100}}; + + /* First time it should include the zero glyph as well */ + CORRADE_VERIFY(font.fillGlyphCache(cache, "hello")); + CORRADE_COMPARE(font.glyphIdsIntoCalled, 1); + CORRADE_COMPARE(font.fillGlyphCacheCalled, 1); + + /* Second time not anymore */ + cache.addFont(10, &font); + CORRADE_VERIFY(font.fillGlyphCache(cache, "hello")); + CORRADE_COMPARE(font.glyphIdsIntoCalled, 2); + CORRADE_COMPARE(font.fillGlyphCacheCalled, 2); +} + +void AbstractFontTest::fillGlyphCacheFailed() { + struct MyFont: AbstractFont { + FontFeatures doFeatures() const override { + return FontFeature::OpenData; + } + bool doIsOpened() const override { return _opened; } + void doClose() override {} + + Properties doOpenData(Containers::ArrayView, Float) override { + _opened = true; + return {0.0f, 0.0f, 0.0f, 0.0f, 1}; + } + + void doGlyphIdsInto(const Containers::StridedArrayView1D&, const Containers::StridedArrayView1D& glyphs) override { + /* Set all to 0 to avoid an assert that the IDs are out of range */ + for(UnsignedInt& i: glyphs) + i = 0; + } + Vector2 doGlyphSize(UnsignedInt) override { return {}; } + Vector2 doGlyphAdvance(UnsignedInt) override { return {}; } + Containers::Pointer doCreateShaper() override { return {}; } + + bool doFillGlyphCache(AbstractGlyphCache&, const Containers::StridedArrayView1D&) override { return false; } bool called = false; + + private: + bool _opened = false; } font; - /* Capture correct function name */ - CORRADE_VERIFY(true); + /* Have to explicitly open in order to make glyphCount() non-zero */ + CORRADE_VERIFY(font.openData(nullptr, 0.0f)); DummyGlyphCache cache{PixelFormat::R8Unorm, {100, 100}}; + CORRADE_VERIFY(!font.fillGlyphCache(cache, Containers::ArrayView{})); CORRADE_VERIFY(!font.fillGlyphCache(cache, "")); } @@ -1084,7 +1257,11 @@ void AbstractFontTest::fillGlyphCacheNotSupported() { bool doIsOpened() const override { return true; } void doClose() override {} - void doGlyphIdsInto(const Containers::StridedArrayView1D&, const Containers::StridedArrayView1D&) override {} + void doGlyphIdsInto(const Containers::StridedArrayView1D&, const Containers::StridedArrayView1D& glyphs) override { + /* Set all to 0 to avoid an assert that the IDs are out of range */ + for(UnsignedInt& i: glyphs) + i = 0; + } Vector2 doGlyphSize(UnsignedInt) override { return {}; } Vector2 doGlyphAdvance(UnsignedInt) override { return {}; } Containers::Pointer doCreateShaper() override { return {}; } @@ -1093,29 +1270,48 @@ void AbstractFontTest::fillGlyphCacheNotSupported() { std::ostringstream out; Error redirectError{&out}; DummyGlyphCache cache{PixelFormat::R8Unorm, {100, 100}}; + font.fillGlyphCache(cache, Containers::arrayView({0u, 15u})); font.fillGlyphCache(cache, "hello"); - CORRADE_COMPARE(out.str(), "Text::AbstractFont::fillGlyphCache(): feature not supported\n"); + CORRADE_COMPARE(out.str(), + "Text::AbstractFont::fillGlyphCache(): feature not supported\n" + "Text::AbstractFont::fillGlyphCache(): feature not supported\n"); } void AbstractFontTest::fillGlyphCacheNotImplemented() { CORRADE_SKIP_IF_NO_ASSERT(); struct MyFont: AbstractFont { - FontFeatures doFeatures() const override { return {}; } - bool doIsOpened() const override { return true; } + FontFeatures doFeatures() const override { + return FontFeature::OpenData; + } + bool doIsOpened() const override { return _opened; } void doClose() override {} + Properties doOpenData(Containers::ArrayView, Float) override { + _opened = true; + return {0.0f, 0.0f, 0.0f, 0.0f, 1}; + } + void doGlyphIdsInto(const Containers::StridedArrayView1D&, const Containers::StridedArrayView1D&) override {} Vector2 doGlyphSize(UnsignedInt) override { return {}; } Vector2 doGlyphAdvance(UnsignedInt) override { return {}; } Containers::Pointer doCreateShaper() override { return {}; } + + private: + bool _opened = false; } font; + /* Have to explicitly open in order to make glyphCount() non-zero */ + CORRADE_VERIFY(font.openData(nullptr, 0.0f)); + std::ostringstream out; Error redirectError{&out}; DummyGlyphCache cache{PixelFormat::R8Unorm, {100, 100}}; + font.fillGlyphCache(cache, Containers::arrayView({0u})); font.fillGlyphCache(cache, "hello"); - CORRADE_COMPARE(out.str(), "Text::AbstractFont::fillGlyphCache(): feature advertised but not implemented\n"); + CORRADE_COMPARE(out.str(), + "Text::AbstractFont::fillGlyphCache(): feature advertised but not implemented\n" + "Text::AbstractFont::fillGlyphCache(): feature advertised but not implemented\n"); } void AbstractFontTest::fillGlyphCacheNoFont() { @@ -1135,8 +1331,11 @@ void AbstractFontTest::fillGlyphCacheNoFont() { std::ostringstream out; Error redirectError{&out}; DummyGlyphCache cache{PixelFormat::R8Unorm, {100, 100}}; + font.fillGlyphCache(cache, Containers::arrayView({0u, 15u})); font.fillGlyphCache(cache, "hello"); - CORRADE_COMPARE(out.str(), "Text::AbstractFont::fillGlyphCache(): no font opened\n"); + CORRADE_COMPARE(out.str(), + "Text::AbstractFont::fillGlyphCache(): no font opened\n" + "Text::AbstractFont::fillGlyphCache(): no font opened\n"); } void AbstractFontTest::fillGlyphCacheInvalidUtf8() { diff --git a/src/MagnumPlugins/MagnumFontConverter/Test/MagnumFontConverterTest.cpp b/src/MagnumPlugins/MagnumFontConverter/Test/MagnumFontConverterTest.cpp index 91d9bcc54..be8294ede 100644 --- a/src/MagnumPlugins/MagnumFontConverter/Test/MagnumFontConverterTest.cpp +++ b/src/MagnumPlugins/MagnumFontConverter/Test/MagnumFontConverterTest.cpp @@ -523,7 +523,10 @@ void MagnumFontConverterTest::exportFontImageConversionFailed() { return {16.0f, 25.0f, -10.0f, 39.7333f, 3}; } - void doGlyphIdsInto(const Containers::StridedArrayView1D&, const Containers::StridedArrayView1D&) override {} + void doGlyphIdsInto(const Containers::StridedArrayView1D&, const Containers::StridedArrayView1D& glyphs) override { + for(UnsignedInt& i: glyphs) + i = 0; + } Vector2 doGlyphSize(UnsignedInt) override { return {}; } Vector2 doGlyphAdvance(UnsignedInt) override { return {}; } Containers::Pointer doCreateShaper() override { return nullptr; }