feat(core): render local CJK glyphs at 2x texture resolution (#4304)
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Bart Louwers <bart@emeel.net>
This commit is contained in:
@@ -119,6 +119,13 @@ public:
|
||||
}
|
||||
};
|
||||
|
||||
class KatakanaOnlyLocalGlyphRasterizer : public StubLocalGlyphRasterizer {
|
||||
public:
|
||||
bool canRasterizeGlyph(const FontStack&, GlyphID glyphID) override {
|
||||
return static_cast<char16_t>(glyphID) == u'テ';
|
||||
}
|
||||
};
|
||||
|
||||
class StubGlyphManagerObserver : public GlyphManagerObserver {
|
||||
public:
|
||||
void onGlyphsLoaded(const FontStack& fontStack, const GlyphRange& glyphRange) override {
|
||||
@@ -148,7 +155,11 @@ public:
|
||||
StubFileSource fileSource;
|
||||
StubGlyphManagerObserver observer;
|
||||
StubGlyphRequestor requestor;
|
||||
GlyphManager glyphManager{std::make_unique<StubLocalGlyphRasterizer>()};
|
||||
GlyphManager glyphManager;
|
||||
|
||||
GlyphManagerTest(
|
||||
std::unique_ptr<LocalGlyphRasterizer> localGlyphRasterizer = std::make_unique<StubLocalGlyphRasterizer>())
|
||||
: glyphManager(std::move(localGlyphRasterizer)) {}
|
||||
|
||||
void run(const std::string& url, GlyphDependencies dependencies) {
|
||||
// Squelch logging.
|
||||
@@ -317,8 +328,40 @@ TEST(GlyphManager, LoadLocalCJKGlyph) {
|
||||
GlyphDependencies{.glyphs = {{{{"Test Stack"}}, {u'中'}}}, .shapes = {}});
|
||||
}
|
||||
|
||||
TEST(GlyphManager, LoadLocalCJKGlyphAfterLoadingRangeFromURL) {
|
||||
TEST(GlyphManager, LoadLocalCJKSymbols) {
|
||||
GlyphManagerTest test;
|
||||
int glyphResponses = 0;
|
||||
|
||||
test.fileSource.glyphsResponse = [&](const Resource&) {
|
||||
glyphResponses++;
|
||||
return std::optional<Response>();
|
||||
};
|
||||
|
||||
test.observer.glyphsLoaded = [&](const FontStack&, const GlyphRange&) {
|
||||
glyphResponses++;
|
||||
};
|
||||
|
||||
test.requestor.glyphsAvailable = [&](GlyphMap glyphs) {
|
||||
EXPECT_EQ(glyphResponses, 0); // Local generation should prevent requesting any glyphs.
|
||||
|
||||
const auto& testPositions = glyphs.at(FontStackHasher()({{"Test Stack"}}));
|
||||
|
||||
ASSERT_EQ(testPositions.size(), 2u);
|
||||
ASSERT_EQ(testPositions.count(u'〒'), 1u);
|
||||
ASSERT_EQ(testPositions.count(u'!'), 1u);
|
||||
|
||||
test.end();
|
||||
};
|
||||
|
||||
test.run("test/fixtures/resources/glyphs.pbf",
|
||||
GlyphDependencies{.glyphs = {{{{"Test Stack"}}, {u'〒', u'!'}}}, .shapes = {}});
|
||||
}
|
||||
|
||||
TEST(GlyphManager, LocalCJKGlyphPrefersLocalRasterizationOverRange) {
|
||||
// Verifies that locally-rasterized glyphs (like テ) are produced by the
|
||||
// local rasterizer instead of using a glyph from an already-loaded PBF
|
||||
// range.
|
||||
GlyphManagerTest test(std::make_unique<KatakanaOnlyLocalGlyphRasterizer>());
|
||||
bool firstGlyphResponse = false;
|
||||
|
||||
test.fileSource.glyphsResponse = [&](const Resource&) {
|
||||
@@ -337,9 +380,6 @@ TEST(GlyphManager, LoadLocalCJKGlyphAfterLoadingRangeFromURL) {
|
||||
ASSERT_EQ(testPositions.size(), 1u);
|
||||
ASSERT_EQ(testPositions.count(u'々'), 1u);
|
||||
|
||||
// Katakana letter te, should be locally rasterized
|
||||
// instead of using the glyph recieved from the range
|
||||
// for the ideagraphic mark
|
||||
test.glyphManager.getGlyphs(test.requestor,
|
||||
GlyphDependencies{.glyphs = {{{{"Test Stack"}}, {u'テ'}}}, // 0x30c6
|
||||
.shapes = {}},
|
||||
@@ -443,3 +483,16 @@ TEST(GlyphManager, ImmediateFileSource) {
|
||||
test.run("test/fixtures/resources/glyphs.pbf",
|
||||
GlyphDependencies{.glyphs = {{{{"Test Stack"}}, {u'a', u'å', u' '}}}, .shapes = {}});
|
||||
}
|
||||
|
||||
// Guards against silent drift between allowsFixedWidthGlyphGeneration's range
|
||||
// coverage and the precomputed NON_IDEOGRAPH_GLYPH_RANGES_PER_FONT_STACK used
|
||||
// for offline download progress estimation.
|
||||
TEST(i18n, GlyphRangeLocalGenerationCountMatchesConstant) {
|
||||
uint32_t locallyGeneratedRanges = 0;
|
||||
for (uint32_t i = 0; i < GLYPH_RANGES_PER_FONT_STACK; ++i) {
|
||||
if (util::i18n::glyphRangeIsEntirelyLocallyGenerated(static_cast<char16_t>(i * GLYPHS_PER_GLYPH_RANGE))) {
|
||||
++locallyGeneratedRanges;
|
||||
}
|
||||
}
|
||||
EXPECT_EQ(GLYPH_RANGES_PER_FONT_STACK - locallyGeneratedRanges, NON_IDEOGRAPH_GLYPH_RANGES_PER_FONT_STACK);
|
||||
}
|
||||
|
||||
@@ -65,7 +65,7 @@ TEST(LocalGlyphRasterizer, PingFang) {
|
||||
|
||||
test.map.getStyle().loadJSON(util::read_file("test/fixtures/local_glyphs/mixed.json"));
|
||||
#if defined(__APPLE__) && !defined(__QT__)
|
||||
test.checkRendering("ping_fang", 0.0161);
|
||||
test.checkRendering("ping_fang", 0.025);
|
||||
#elif defined(__QT__)
|
||||
test.checkRendering("ping_fang_qt");
|
||||
#endif // defined(__APPLE__)
|
||||
@@ -77,7 +77,7 @@ TEST(LocalGlyphRasterizer, PingFangWithBoldInStyle) {
|
||||
ss << std::regex_replace(
|
||||
util::read_file("test/fixtures/local_glyphs/mixed.json"), std::regex("NotoCJK"), "NotoCJK Bold");
|
||||
test.map.getStyle().loadJSON(ss.str());
|
||||
test.checkRendering("ping_fang_with_bold_in_style");
|
||||
test.checkRendering("ping_fang_with_bold_in_style", 0.05);
|
||||
}
|
||||
|
||||
#if !defined(__QT__)
|
||||
@@ -85,7 +85,7 @@ TEST(LocalGlyphRasterizer, PingFangSemibold) {
|
||||
LocalGlyphRasterizerTest test(std::string("PingFang TC Semibold"));
|
||||
|
||||
test.map.getStyle().loadJSON(util::read_file("test/fixtures/local_glyphs/mixed.json"));
|
||||
test.checkRendering("ping_fang_semibold", 0.0161);
|
||||
test.checkRendering("ping_fang_semibold", 0.025);
|
||||
}
|
||||
#endif // !defined(__QT__)
|
||||
|
||||
|
||||
@@ -10,6 +10,55 @@
|
||||
using namespace mbgl;
|
||||
using namespace mbgl::style;
|
||||
|
||||
// Atlas padding is not scaled with textureScale, so a 2x glyph quad is 1
|
||||
// logical pixel smaller than its 1x equivalent. GL JS-style fractional metrics
|
||||
// keep its center aligned with the 1x quad.
|
||||
TEST(getGlyphQuads, DoubleResolutionGlyphPreservesLogicalQuadSize) {
|
||||
GlyphMetrics metrics1x;
|
||||
metrics1x.width = 24;
|
||||
metrics1x.height = 24;
|
||||
metrics1x.left = 0;
|
||||
metrics1x.top = -8;
|
||||
metrics1x.advance = 24;
|
||||
metrics1x.isDoubleResolution = false;
|
||||
|
||||
GlyphMetrics metrics2x = metrics1x;
|
||||
metrics2x.left += 0.5f;
|
||||
metrics2x.top -= 0.5f;
|
||||
metrics2x.isDoubleResolution = true;
|
||||
|
||||
// Rects reflect the real DynamicTextureAtlas output: bitmap.size + 2 *
|
||||
// ImagePosition::padding (= 2). 1x bitmap 30 -> rect 32; 2x bitmap 60 -> rect 62.
|
||||
Shaping shapedText1x;
|
||||
shapedText1x.positionedLines.emplace_back();
|
||||
shapedText1x.positionedLines.back().positionedGlyphs.emplace_back(
|
||||
PositionedGlyph(u'A', 0.0f, 0.0f, false, 0, 1.0f, Rect<uint16_t>(0, 0, 32, 32), metrics1x, std::nullopt));
|
||||
|
||||
Shaping shapedText2x;
|
||||
shapedText2x.positionedLines.emplace_back();
|
||||
shapedText2x.positionedLines.back().positionedGlyphs.emplace_back(
|
||||
PositionedGlyph(u'A', 0.0f, 0.0f, false, 0, 1.0f, Rect<uint16_t>(0, 0, 62, 62), metrics2x, std::nullopt));
|
||||
|
||||
SymbolLayoutProperties::Evaluated layout;
|
||||
SymbolQuads quads1x = getGlyphQuads(shapedText1x, {{0, 0}}, layout, SymbolPlacementType::Point, {}, false);
|
||||
SymbolQuads quads2x = getGlyphQuads(shapedText2x, {{0, 0}}, layout, SymbolPlacementType::Point, {}, false);
|
||||
|
||||
ASSERT_EQ(quads1x.size(), 1u);
|
||||
ASSERT_EQ(quads2x.size(), 1u);
|
||||
|
||||
// The 2x quad is 1 logical pixel smaller, and shifted by 0.5px to keep the
|
||||
// same center as the 1x quad.
|
||||
EXPECT_FLOAT_EQ(quads1x[0].tl.x + 0.5f, quads2x[0].tl.x);
|
||||
EXPECT_FLOAT_EQ(quads1x[0].tl.y + 0.5f, quads2x[0].tl.y);
|
||||
EXPECT_FLOAT_EQ(quads1x[0].br.x - 0.5f, quads2x[0].br.x);
|
||||
EXPECT_FLOAT_EQ(quads1x[0].br.y - 0.5f, quads2x[0].br.y);
|
||||
|
||||
EXPECT_FLOAT_EQ(quads1x[0].br.x - quads1x[0].tl.x, 32.0f);
|
||||
EXPECT_FLOAT_EQ(quads2x[0].br.x - quads2x[0].tl.x, 31.0f);
|
||||
EXPECT_FLOAT_EQ(quads1x[0].br.y - quads1x[0].tl.y, 32.0f);
|
||||
EXPECT_FLOAT_EQ(quads2x[0].br.y - quads2x[0].tl.y, 31.0f);
|
||||
}
|
||||
|
||||
TEST(getIconQuads, normal) {
|
||||
SymbolLayoutProperties::Evaluated layout;
|
||||
Anchor anchor(2.0, 3.0, 0.0, 0);
|
||||
|
||||
Reference in New Issue
Block a user