You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

557 lines
20 KiB

/*
This file is part of Magnum.
Copyright © 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019,
2020, 2021, 2022, 2023 Vladimír Vondruš <mosra@centrum.cz>
Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the "Software"),
to deal in the Software without restriction, including without limitation
the rights to use, copy, modify, merge, publish, distribute, sublicense,
and/or sell copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included
in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
*/
#include <sstream>
#include <Corrade/Containers/Array.h>
#include <Corrade/Containers/StridedArrayView.h>
#include <Corrade/Containers/StringView.h>
#include <Corrade/TestSuite/Tester.h>
#include <Corrade/TestSuite/Compare/Container.h>
#include <Corrade/Utility/DebugStl.h> /** @todo drop once Debug is stream-free */
#include "Magnum/PixelFormat.h"
#include "Magnum/Text/AbstractFont.h"
#include "Magnum/Text/AbstractGlyphCache.h"
#include "Magnum/Text/AbstractShaper.h"
#include "Magnum/Text/Renderer.h"
namespace Magnum { namespace Text { namespace Test { namespace {
struct RendererTest: TestSuite::Tester {
explicit RendererTest();
void renderData();
void multiline();
#ifdef MAGNUM_TARGET_GL
void arrayGlyphCache();
void fontNotFoundInCache();
#endif
};
const struct {
TestSuite::TestCaseDescriptionSourceLocation name;
Alignment alignment;
Vector2 offset;
} RenderDataData[]{
/* Not testing all combinations, just making sure that each horizontal,
vertical, glyph bounds and integer variant is covered */
{"line left", Alignment::LineLeft,
/* This is the default (0) value, thus should result in no shift */
{}},
{"line left, glyph bounds", Alignment::LineLeftGlyphBounds,
/* The first glyph has X offset of 2.5, which is subtracted */
{-2.5f, 0.0f}},
{"top left", Alignment::TopLeft,
/* Ascent is 4.5, scaled by 0.5 */
{0.0f, -2.25f}},
{"top left, glyph bounds", Alignment::TopLeftGlyphBounds,
/* Largest Y value is 10.5f */
{-2.5f, -10.5f}},
{"top right", Alignment::TopRight,
/* Advances were 1, 2, 3, so 6 in total, ascent is 4.5; scaled by
0.5 */
{-3.0f, -2.25f}},
{"top right, glyph bounds", Alignment::TopRightGlyphBounds,
/* Basically subtracting the largest vertex value */
{-12.5f, -10.5f}},
{"top center", Alignment::TopCenter,
/* Advances were 1, 2, 3, so 6 in total, center is 3, scaled by 0.5 */
{-1.5f, -2.25f}},
{"top center, integral", Alignment::TopCenterIntegral,
/* The Y shift isn't whole units but only X is rounded here */
{-2.0f, -2.25f}},
{"top center, glyph bounds", Alignment::TopCenterGlyphBounds,
{-7.5f, -10.5f}},
{"top center, glyph bounds, integral", Alignment::TopCenterGlyphBoundsIntegral,
/* The Y shift isn't whole units but only X is rounded here */
{-8.0f, -10.5f}},
{"middle left, glyph bounds", Alignment::MiddleLeftGlyphBounds,
{-2.5f, -7.125f}},
{"middle left, glyph bounds, integral", Alignment::MiddleLeftGlyphBoundsIntegral,
/* The X shift isn't whole units but only Y is rounded here */
{-2.5f, -7.0f}},
{"middle center", Alignment::MiddleCenter,
{-1.5f, -0.5f}},
{"middle center, integral", Alignment::MiddleCenterIntegral,
/* Rounding happens on both X and Y in this case */
{-2.0f, -1.0f}},
{"middle center, glyph bounds", Alignment::MiddleCenterGlyphBounds,
/* Half size of the bounds quad */
{-7.5f, -7.125f}},
{"middle center, glyph bounds, integral", Alignment::MiddleCenterGlyphBoundsIntegral,
{-8.0f, -7.0f}},
{"bottom left", Alignment::BottomLeft,
/* Descent is -2.5; scaled by 0.5 */
{0.0f, 1.25f}},
{"bottom right", Alignment::BottomRight,
{-3.0f, 1.25f}},
{"bottom right, glyph bounds", Alignment::BottomRightGlyphBounds,
{-12.5f, -3.75f}},
};
const struct {
const char* name;
Alignment alignment;
/* The Y offset value could be calculated, but this is easier to grasp and
makes it possible to test overrideable line height later, for example */
Vector2 offset0, offset1, offset2;
} MultilineData[]{
{"line left", Alignment::LineLeft,
{0.0f, -0.0f},
{0.0f, -4.0f},
{0.0f, -12.0f}},
{"line left, glyph bounds", Alignment::LineLeftGlyphBounds,
{0.0f, 0.0f},
{0.0f, -4.0f},
{0.0f, -12.0f}},
{"middle left", Alignment::MiddleLeft,
{0.0f, 6.0f},
{0.0f, 2.0f},
{0.0f, -6.0f}},
{"middle left, glyph bounds", Alignment::MiddleLeftGlyphBounds,
{0.0f, 5.5f},
{0.0f, 1.5f},
{0.0f, -6.5f}},
{"middle left, glyph bounds, integral", Alignment::MiddleLeftGlyphBoundsIntegral,
{0.0f, 6.0f},
{0.0f, 2.0f},
{0.0f, -6.0f}},
{"middle center", Alignment::MiddleCenter,
/* The advance for the rightmost glyph is one unit larger than the
actual bounds which makes it different */
{-4.0f, 6.0f},
{-2.0f, 2.0f},
{-3.0f, -6.0f}},
{"middle center, integral", Alignment::MiddleCenterIntegral,
{-4.0f, 6.0f},
{-2.0f, 2.0f},
{-3.0f, -6.0f}},
{"middle center, glyph bounds", Alignment::MiddleCenterGlyphBounds,
{-3.5f, 5.5f},
{-1.5f, 1.5f},
{-2.5f, -6.5f}},
{"middle center, glyph bounds, integral", Alignment::MiddleCenterGlyphBoundsIntegral,
{-4.0f, 6.0f},
{-2.0f, 2.0f},
{-3.0f, -6.0f}},
{"top right", Alignment::TopRight,
{-8.0f, -0.5f},
{-4.0f, -4.5f},
{-6.0f, -12.5f}},
{"top right, glyph bounds", Alignment::TopRightGlyphBounds,
{-7.0f, -1.0f},
{-3.0f, -5.0f},
{-5.0f, -13.0f}},
{"top center", Alignment::TopCenter,
/* The advance for the rightmost glyph is one unit larger than the
actual bounds which makes it different */
{-4.0f, -0.5f},
{-2.0f, -4.5f},
{-3.0f, -12.5f}},
{"top center, integral", Alignment::TopCenterIntegral,
/* The Y shift isn't whole units but only X (which is already whole
units) would be rounded here */
{-4.0f, -0.5f},
{-2.0f, -4.5f},
{-3.0f, -12.5f}},
{"top center, glyph bounds", Alignment::TopCenterGlyphBounds,
{-3.5f, -1.0f},
{-1.5f, -5.0f},
{-2.5f, -13.0f}},
{"top center, integral", Alignment::TopCenterGlyphBoundsIntegral,
{-4.0f, -1.0f},
{-2.0f, -5.0f},
{-3.0f, -13.0f}},
};
RendererTest::RendererTest() {
addInstancedTests({&RendererTest::renderData},
Containers::arraySize(RenderDataData));
addInstancedTests({&RendererTest::multiline},
Containers::arraySize(MultilineData));
#ifdef MAGNUM_TARGET_GL
addTests({&RendererTest::arrayGlyphCache,
&RendererTest::fontNotFoundInCache});
#endif
}
struct TestShaper: AbstractShaper {
using AbstractShaper::AbstractShaper;
UnsignedInt doShape(Containers::StringView text, UnsignedInt, UnsignedInt, Containers::ArrayView<const FeatureRange>) override {
return text.size();
}
void doGlyphsInto(const Containers::StridedArrayView1D<UnsignedInt>& ids, const Containers::StridedArrayView1D<Vector2>& offsets, const Containers::StridedArrayView1D<Vector2>& advances) const override {
for(UnsignedInt i = 0; i != ids.size(); ++i) {
/* It just rotates between the three glyphs */
if(i % 3 == 0)
ids[i] = 3;
else if(i % 3 == 1)
ids[i] = 7;
else
ids[i] = 9;
/* Offset Y and advance X is getting larger with every glyph,
advance Y is flipping its sign with every glyph */
offsets[i] = Vector2::yAxis(i + 1);
advances[i] = {Float(i + 1), i % 2 ? -0.5f : +0.5f};
}
}
};
struct TestFont: AbstractFont {
FontFeatures doFeatures() const override { return {}; }
bool doIsOpened() const override { return _opened; }
void doClose() override { _opened = false; }
Properties doOpenFile(Containers::StringView, Float size) override {
_opened = true;
/* Line height isn't used for anything here so can be arbitrary */
return {size, 4.5f, -2.5f, 10000.0f, 10};
}
UnsignedInt doGlyphId(char32_t) override { return 0; }
Vector2 doGlyphSize(UnsignedInt) override { return {}; }
Vector2 doGlyphAdvance(UnsignedInt) override { return {}; }
Containers::Pointer<AbstractShaper> doCreateShaper() override {
return Containers::pointer<TestShaper>(*this);
}
bool _opened = false;
};
struct DummyGlyphCache: AbstractGlyphCache {
using AbstractGlyphCache::AbstractGlyphCache;
GlyphCacheFeatures doFeatures() const override { return {}; }
void doSetImage(const Vector2i&, const ImageView2D&) override {}
};
DummyGlyphCache testGlyphCache(AbstractFont& font) {
DummyGlyphCache cache{PixelFormat::R8Unorm, {20, 20}};
/* Add one more font to verify the right one gets picked */
cache.addFont(96);
UnsignedInt fontId = cache.addFont(font.glyphCount(), &font);
/* Three glyphs, covering bottom, top left and top right of the cache */
cache.addGlyph(fontId, 3, {5, 10}, {{}, {20, 10}});
cache.addGlyph(fontId, 7, {10, 5}, {{0, 10}, {10, 20}});
cache.addGlyph(fontId, 9, {5, 5}, {{10, 10}, {20, 20}});
return cache;
}
void RendererTest::renderData() {
auto&& data = RenderDataData[testCaseInstanceId()];
setTestCaseDescription(data.name);
TestFont font;
font.openFile({}, 0.5f);
DummyGlyphCache cache = testGlyphCache(font);
/* Capture the correct function name */
CORRADE_VERIFY(true);
std::vector<Vector2> positions;
std::vector<Vector2> textureCoordinates;
std::vector<UnsignedInt> indices;
Range2D bounds;
std::tie(positions, textureCoordinates, indices, bounds) = AbstractRenderer::render(font, cache, 0.25f, "abc", data.alignment);
/* Three glyphs, three quads -> 12 vertices, 18 indices */
CORRADE_COMPARE(positions.size(), 12);
CORRADE_COMPARE(textureCoordinates.size(), 12);
CORRADE_COMPARE(indices.size(), 18);
/* Vertex positions. Rectangles coming from the cache and offsets +
advances from the layouter are scaled by 0.5. First glyph is moved by
(scaled) 1 up and has advance of (scaled) {1, ±0.5}, every next glyph is
moved up and further distanced by (scaled) {1, ±0.5}. First glyph is
wide, the other two are square.
+-+
+-+ |c|
0---2 |b| +-+
| a | +-+
1---3 */
CORRADE_COMPARE_AS(positions, (std::vector<Vector2>{
/* Cursor is {0, 0}. Offset from the cache is {5, 10}, offset from the
renderer is {0, 1}, size is {20, 10}; all scaled by 0.5 */
Vector2{ 2.5f, 10.5f} + data.offset,
Vector2{ 2.5f, 5.5f} + data.offset,
Vector2{12.5f, 10.5f} + data.offset,
Vector2{12.5f, 5.5f} + data.offset,
/* Advance was {1, 0.5}, cursor is {1, 0.5}. Offset from the cache is
{10, 5}, offset from the renderer is {0, 2}, size is {10, 10}; all
scaled by 0.5 */
Vector2{ 5.5f, 8.75f} + data.offset,
Vector2{ 5.5f, 3.75f} + data.offset,
Vector2{10.5f, 8.75f} + data.offset,
Vector2{10.5f, 3.75f} + data.offset,
/* Advance was {2, -0.5}, cursor is {3, 0}. Offset from the cache is
{5, 5}, offset from the renderer is {0, 3}, size is {10, 10}; all
scaled by 0.5 */
Vector2{ 4.0f, 9.0f} + data.offset,
Vector2{ 4.0f, 4.0f} + data.offset,
Vector2{ 9.0f, 9.0f} + data.offset,
Vector2{ 9.0f, 4.0f} + data.offset
}), TestSuite::Compare::Container);
/* Bounds. Different depending on whether or not GlyphBounds alignment is
used. */
if(UnsignedByte(data.alignment) & Implementation::AlignmentGlyphBounds)
CORRADE_COMPARE(bounds, (Range2D{{2.5f, 3.75f}, {12.5f, 10.5f}}.translated(data.offset)));
else
CORRADE_COMPARE(bounds, (Range2D{{0.0f, -1.25f}, {3.0f, 2.25f}}.translated(data.offset)));
/* Texture coordinates. First glyph is bottom, second top left, third top
right.
+-+-+
|b|c|
0---2
| a |
1---3 */
CORRADE_COMPARE_AS(textureCoordinates, (std::vector<Vector2>{
{0.0f, 0.5f},
{0.0f, 0.0f},
{1.0f, 0.5f},
{1.0f, 0.0f},
{0.0f, 1.0f},
{0.0f, 0.5f},
{0.5f, 1.0f},
{0.5f, 0.5f},
{0.5f, 1.0f},
{0.5f, 0.5f},
{1.0f, 1.0f},
{1.0f, 0.5f}
}), TestSuite::Compare::Container);
/* Indices
0---2 0---2 5
| | | / /|
| | | / / |
| | |/ / |
1---3 1 3---4 */
CORRADE_COMPARE_AS(indices, (std::vector<UnsignedInt>{
0, 1, 2, 1, 3, 2,
4, 5, 6, 5, 7, 6,
8, 9, 10, 9, 11, 10
}), TestSuite::Compare::Container);
}
void RendererTest::multiline() {
auto&& data = MultilineData[testCaseInstanceId()];
setTestCaseDescription(data.name);
struct Shaper: AbstractShaper {
using AbstractShaper::AbstractShaper;
UnsignedInt doShape(Containers::StringView text, UnsignedInt, UnsignedInt, Containers::ArrayView<const FeatureRange>) override {
return text.size();
}
void doGlyphsInto(const Containers::StridedArrayView1D<UnsignedInt>& ids, const Containers::StridedArrayView1D<Vector2>& offsets, const Containers::StridedArrayView1D<Vector2>& advances) const override {
for(UnsignedInt i = 0; i != ids.size(); ++i) {
ids[i] = 0;
offsets[i] = {};
advances[i] = Vector2::xAxis(4.0f);
}
}
};
struct: AbstractFont {
FontFeatures doFeatures() const override { return {}; }
bool doIsOpened() const override { return _opened; }
void doClose() override { _opened = false; }
Properties doOpenFile(Containers::StringView, Float size) override {
_opened = true;
/* Compared to the glyph bounds, which are from 0 to 2, this is
shifted by one unit, thus by 0.5 in the output */
return {size, 1.0f, -1.0f, 8.0f, 10};
}
UnsignedInt doGlyphId(char32_t) override { return 0; }
Vector2 doGlyphSize(UnsignedInt) override { return {}; }
Vector2 doGlyphAdvance(UnsignedInt) override { return {}; }
Containers::Pointer<AbstractShaper> doCreateShaper() override {
return Containers::pointer<Shaper>(*this);
}
bool _opened = false;
} font;
font.openFile({}, 0.5f);
/* Just a single glyph that scales to {1, 1} in the end */
DummyGlyphCache cache{PixelFormat::R8Unorm, {20, 20}};
UnsignedInt fontId = cache.addFont(1, &font);
cache.addGlyph(fontId, 0, {}, {{}, {2, 2}});
/* Capture the correct function name */
CORRADE_VERIFY(true);
Range2D rectangle;
std::vector<UnsignedInt> indices;
std::vector<Vector2> positions, textureCoordinates;
std::tie(positions, textureCoordinates, indices, rectangle) = Renderer2D::render(font,
cache, 0.25f, "abcd\nef\n\nghi", data.alignment);
/* We're rendering text at 0.25f size and the font is scaled to 0.5f, so
the line advance should be 8.0f*0.25f/0.5f = 4.0f */
CORRADE_COMPARE(font.size(), 0.5f);
CORRADE_COMPARE(font.lineHeight(), 8.0f);
/* Bounds. The advance for the rightmost glyph is one unit larger than the
actual bounds so it's different on X between the two variants */
if(UnsignedByte(data.alignment) & Implementation::AlignmentGlyphBounds)
CORRADE_COMPARE(rectangle, Range2D({0.0f, -12.0f}, {7.0f, 1.0f}).translated(data.offset0));
else
CORRADE_COMPARE(rectangle, Range2D({0.0f, -12.5f}, {8.0f, 0.5f}).translated(data.offset0));
/* Vertices
[a] [b] [c] [d]
[e] [f]
[g] [h] [i] */
CORRADE_COMPARE_AS(positions, (std::vector<Vector2>{
Vector2{0.0f, 1.0f} + data.offset0, /* a */
Vector2{0.0f, 0.0f} + data.offset0,
Vector2{1.0f, 1.0f} + data.offset0,
Vector2{1.0f, 0.0f} + data.offset0,
Vector2{2.0f, 1.0f} + data.offset0, /* b */
Vector2{2.0f, 0.0f} + data.offset0,
Vector2{3.0f, 1.0f} + data.offset0,
Vector2{3.0f, 0.0f} + data.offset0,
Vector2{4.0f, 1.0f} + data.offset0, /* c */
Vector2{4.0f, 0.0f} + data.offset0,
Vector2{5.0f, 1.0f} + data.offset0,
Vector2{5.0f, 0.0f} + data.offset0,
Vector2{6.0f, 1.0f} + data.offset0, /* d */
Vector2{6.0f, 0.0f} + data.offset0,
Vector2{7.0f, 1.0f} + data.offset0,
Vector2{7.0f, 0.0f} + data.offset0,
Vector2{0.0f, 1.0f} + data.offset1, /* e */
Vector2{0.0f, 0.0f} + data.offset1,
Vector2{1.0f, 1.0f} + data.offset1,
Vector2{1.0f, 0.0f} + data.offset1,
Vector2{2.0f, 1.0f} + data.offset1, /* f */
Vector2{2.0f, 0.0f} + data.offset1,
Vector2{3.0f, 1.0f} + data.offset1,
Vector2{3.0f, 0.0f} + data.offset1,
/* Two linebreaks here */
Vector2{0.0f, 1.0f} + data.offset2, /* g */
Vector2{0.0f, 0.0f} + data.offset2,
Vector2{1.0f, 1.0f} + data.offset2,
Vector2{1.0f, 0.0f} + data.offset2,
Vector2{2.0f, 1.0f} + data.offset2, /* h */
Vector2{2.0f, 0.0f} + data.offset2,
Vector2{3.0f, 1.0f} + data.offset2,
Vector2{3.0f, 0.0f} + data.offset2,
Vector2{4.0f, 1.0f} + data.offset2, /* i */
Vector2{4.0f, 0.0f} + data.offset2,
Vector2{5.0f, 1.0f} + data.offset2,
Vector2{5.0f, 0.0f} + data.offset2,
}), TestSuite::Compare::Container);
/* Indices
0---2 0---2 5
| | | / /|
| | | / / |
| | |/ / |
1---3 1 3---4 */
CORRADE_COMPARE_AS(indices, (std::vector<UnsignedInt>{
0, 1, 2, 1, 3, 2,
4, 5, 6, 5, 7, 6,
8, 9, 10, 9, 11, 10,
12, 13, 14, 13, 15, 14,
16, 17, 18, 17, 19, 18,
20, 21, 22, 21, 23, 22,
24, 25, 26, 25, 27, 26,
28, 29, 30, 29, 31, 30,
32, 33, 34, 33, 35, 34
}), TestSuite::Compare::Container);
}
#ifdef MAGNUM_TARGET_GL
void RendererTest::arrayGlyphCache() {
CORRADE_SKIP_IF_NO_ASSERT();
TestFont font;
font.openFile({}, 0.5f);
struct: AbstractGlyphCache {
using AbstractGlyphCache::AbstractGlyphCache;
GlyphCacheFeatures doFeatures() const override { return {}; }
} cache{PixelFormat::R8Unorm, {100, 100, 3}};
std::ostringstream out;
Error redirectError{&out};
AbstractRenderer::render(font, cache, 0.25f, "abc");
CORRADE_COMPARE(out.str(), "Text::Renderer: array glyph caches are not supported\n");
}
void RendererTest::fontNotFoundInCache() {
CORRADE_SKIP_IF_NO_ASSERT();
TestFont font;
font.openFile({}, 0.5f);
DummyGlyphCache cache{PixelFormat::R8Unorm, {100, 100}};
cache.addFont(34);
cache.addFont(25);
std::ostringstream out;
Error redirectError{&out};
AbstractRenderer::render(font, cache, 0.25f, "abc");
CORRADE_COMPARE(out.str(), "Text::Renderer: font not found among 2 fonts in passed glyph cache\n");
}
#endif
}}}}
CORRADE_TEST_MAIN(Magnum::Text::Test::RendererTest)