Add HarfBuzz Text Shaping and Font Fallback Support (#3611)

Co-authored-by: alan wei chen <alanchenboy@gmail.com>
This commit is contained in:
Stefan Karschti
2025-07-29 23:00:27 +03:00
committed by GitHub
parent 60264349c1
commit 09d395d0db
82 changed files with 27006 additions and 151 deletions

View File

@@ -1612,6 +1612,82 @@ TEST(Map, ObserveShaderRegistration) {
EXPECT_EQ(observedRegistry, false);
}
TEST(Map, ResourceError) {
MapTest<> test;
test.fileSource->glyphsResponse = [&](const Resource&) {
Response response;
response.error = std::make_unique<Response::Error>(Response::Error::Reason::Server, "Font file Server failed");
return response;
};
test.map.getStyle().loadJSON(
R"(
{
"version": 8,
"zoom": 0,
"center": [-14.41400, 39.09187],
"sources": {
"mapbox": {
"type": "geojson",
"data": {
"type": "FeatureCollection",
"features": [
{
"type": "Feature",
"properties": {
"name": "ជនជាប់សង្ស័យ"
},
"geometry": {
"type": "LineString",
"coordinates": [
[
-14.4195556640625,
39.091699613104595
],
[
102.3046875,
39.36827914916014
]
]
}
}
]
}
}
},
"glyphs": "local://glyphs/{fontstack}/{range}.pbf",
"fonts": "local://glyphs/{fontstack}/{language}.pbf",
"layers": [
{
"id": "background",
"type": "background",
"paint": {
"background-color": "white"
}
},
{
"id": "lines-symbol",
"type": "symbol",
"source": "mapbox",
"layout": {
"text-field": "{name}",
"symbol-placement": "point",
"symbol-spacing": 150,
"text-allow-overlap": true,
"text-font": [ "abc" ],
"text-size": 24
}
}
]
})");
try {
test.frontend.render(test.map);
} catch (...) {
auto error = std::current_exception(); // captur
EXPECT_EQ(mbgl::util::toString(error), "Font file Server failed");
}
}
TEST(Map, StencilOverflow) {
MapTest<> test;

View File

@@ -135,7 +135,7 @@ public:
class StubGlyphRequestor : public GlyphRequestor {
public:
void onGlyphsAvailable(GlyphMap glyphs) override {
void onGlyphsAvailable(GlyphMap glyphs, HBShapeRequests) override {
if (glyphsAvailable) glyphsAvailable(std::move(glyphs));
}
@@ -181,7 +181,7 @@ TEST(GlyphManager, LoadingSuccess) {
test.observer.glyphsLoaded = [&](const FontStack& fontStack, const GlyphRange& range) {
ASSERT_EQ(fontStack, FontStack{{"Test Stack"}});
ASSERT_EQ(range, GlyphRange(0, 255));
ASSERT_EQ(range, GlyphRange(0, 255, GlyphIDType::FontPBF));
};
test.requestor.glyphsAvailable = [&](GlyphMap glyphs) {
@@ -196,7 +196,7 @@ TEST(GlyphManager, LoadingSuccess) {
test.end();
};
test.run("test/fixtures/resources/glyphs.pbf", GlyphDependencies{{{{"Test Stack"}}, {u'a', u'å', u' '}}});
test.run("test/fixtures/resources/glyphs.pbf", GlyphDependencies{{{{{"Test Stack"}}, {u'a', u'å', u' '}}}, {}});
}
TEST(GlyphManager, LoadingFail) {
@@ -211,7 +211,7 @@ TEST(GlyphManager, LoadingFail) {
test.observer.glyphsError =
[&](const FontStack& fontStack, const GlyphRange& glyphRange, std::exception_ptr error) {
EXPECT_EQ(fontStack, FontStack({"Test Stack"}));
EXPECT_EQ(glyphRange, GlyphRange(0, 255));
EXPECT_EQ(glyphRange, GlyphRange(0, 255, GlyphIDType::FontPBF));
EXPECT_TRUE(error != nullptr);
EXPECT_EQ(util::toString(error), "Failed by the test case");
@@ -224,7 +224,7 @@ TEST(GlyphManager, LoadingFail) {
test.end();
};
test.run("test/fixtures/resources/glyphs.pbf", GlyphDependencies{{{{"Test Stack"}}, {u'a', u'å'}}});
test.run("test/fixtures/resources/glyphs.pbf", GlyphDependencies{{{{{"Test Stack"}}, {u'a', u'å'}}}, {}});
}
TEST(GlyphManager, LoadingCorrupted) {
@@ -239,7 +239,7 @@ TEST(GlyphManager, LoadingCorrupted) {
test.observer.glyphsError =
[&](const FontStack& fontStack, const GlyphRange& glyphRange, std::exception_ptr error) {
EXPECT_EQ(fontStack, FontStack({"Test Stack"}));
EXPECT_EQ(glyphRange, GlyphRange(0, 255));
EXPECT_EQ(glyphRange, GlyphRange(0, 255, GlyphIDType::FontPBF));
EXPECT_TRUE(error != nullptr);
EXPECT_EQ(util::toString(error), "unknown pbf field type exception");
@@ -252,7 +252,7 @@ TEST(GlyphManager, LoadingCorrupted) {
test.end();
};
test.run("test/fixtures/resources/glyphs.pbf", GlyphDependencies{{{{"Test Stack"}}, {u'a', u'å'}}});
test.run("test/fixtures/resources/glyphs.pbf", GlyphDependencies{{{{{"Test Stack"}}, {u'a', u'å'}}}, {}});
}
TEST(GlyphManager, LoadingCancel) {
@@ -267,7 +267,7 @@ TEST(GlyphManager, LoadingCancel) {
FAIL() << "Should never be called";
};
test.run("test/fixtures/resources/glyphs.pbf", GlyphDependencies{{{{"Test Stack"}}, {u'a', u'å'}}});
test.run("test/fixtures/resources/glyphs.pbf", GlyphDependencies{{{{{"Test Stack"}}, {u'a', u'å'}}}, {}});
}
TEST(GlyphManager, LoadLocalCJKGlyph) {
@@ -293,7 +293,7 @@ TEST(GlyphManager, LoadLocalCJKGlyph) {
ASSERT_EQ(testPositions.count(u'中'), 1u);
Immutable<Glyph> glyph = *testPositions.at(u'中');
EXPECT_EQ(glyph->id, u'中');
EXPECT_EQ(glyph->id.complex.code, u'中');
EXPECT_EQ(glyph->metrics.width, 24ul);
EXPECT_EQ(glyph->metrics.height, 24ul);
EXPECT_EQ(glyph->metrics.left, 0);
@@ -309,7 +309,7 @@ TEST(GlyphManager, LoadLocalCJKGlyph) {
test.end();
};
test.run("test/fixtures/resources/glyphs.pbf", GlyphDependencies{{{{"Test Stack"}}, {u'中'}}});
test.run("test/fixtures/resources/glyphs.pbf", GlyphDependencies{{{{{"Test Stack"}}, {u'中'}}}, {}});
}
TEST(GlyphManager, LoadLocalCJKGlyphAfterLoadingRangeFromURL) {
@@ -336,16 +336,15 @@ TEST(GlyphManager, LoadLocalCJKGlyphAfterLoadingRangeFromURL) {
// instead of using the glyph recieved from the range
// for the ideagraphic mark
test.glyphManager.getGlyphs(test.requestor,
GlyphDependencies{
{{{"Test Stack"}}, {u'テ'}} // 0x30c6
},
GlyphDependencies{{{{{"Test Stack"}}, {u'テ'}}}, // 0x30c6
{}},
test.fileSource);
} else {
ASSERT_EQ(testPositions.size(), 1u);
ASSERT_EQ(testPositions.count(u'テ'), 1u);
Immutable<Glyph> glyph = *testPositions.at(u'テ');
EXPECT_EQ(glyph->id, u'テ');
EXPECT_EQ(glyph->id.complex.code, u'テ');
EXPECT_EQ(glyph->metrics.width, 24ul);
EXPECT_EQ(glyph->metrics.height, 24ul);
EXPECT_EQ(glyph->metrics.left, 0);
@@ -358,9 +357,8 @@ TEST(GlyphManager, LoadLocalCJKGlyphAfterLoadingRangeFromURL) {
};
test.run("test/fixtures/resources/glyphs-12244-12543.pbf",
GlyphDependencies{
{{{"Test Stack"}}, {u'々'}} // 0x3005
});
GlyphDependencies{{{{{"Test Stack"}}, {u'々'}}}, // 0x3005
{}});
}
TEST(GlyphManager, LoadingInvalid) {
@@ -380,7 +378,7 @@ TEST(GlyphManager, LoadingInvalid) {
test.observer.glyphsLoaded = [&](const FontStack& fontStack, const GlyphRange& range) {
ASSERT_EQ(fontStack, FontStack{{"Test Stack"}});
ASSERT_EQ(range, GlyphRange(0, 255));
ASSERT_EQ(range, GlyphRange(0, 255, GlyphIDType::FontPBF));
};
test.requestor.glyphsAvailable = [&](GlyphMap glyphs) {
@@ -393,7 +391,7 @@ TEST(GlyphManager, LoadingInvalid) {
test.end();
};
test.run("test/fixtures/resources/glyphs.pbf", GlyphDependencies{{{{"Test Stack"}}, {u'A', u'E'}}});
test.run("test/fixtures/resources/glyphs.pbf", GlyphDependencies{{{{{"Test Stack"}}, {u'A', u'E'}}}, {}});
}
TEST(GlyphManager, ImmediateFileSource) {
@@ -436,5 +434,5 @@ TEST(GlyphManager, ImmediateFileSource) {
test.end();
};
test.run("test/fixtures/resources/glyphs.pbf", GlyphDependencies{{{{"Test Stack"}}, {u'a', u'å', u' '}}});
test.run("test/fixtures/resources/glyphs.pbf", GlyphDependencies{{{{{"Test Stack"}}, {u'a', u'å', u' '}}}, {}});
}

View File

@@ -7,11 +7,12 @@ using namespace mbgl;
TEST(GlyphPBF, Parsing) {
// The fake glyphs contain a number of invalid glyphs, which should be skipped by the parser.
auto sdfs = parseGlyphPBF(GlyphRange{0, 255}, util::read_file("test/fixtures/resources/fake_glyphs-0-255.pbf"));
auto sdfs = parseGlyphPBF(GlyphRange{0, 255, GlyphIDType::FontPBF},
util::read_file("test/fixtures/resources/fake_glyphs-0-255.pbf"));
EXPECT_TRUE(sdfs.size() == 1);
auto& sdf = sdfs[0];
EXPECT_EQ(69u, sdf.id);
EXPECT_EQ(69u, sdf.id.complex.code);
AlphaImage expected({7, 7});
expected.fill('x');
EXPECT_EQ(expected, sdf.bitmap);

View File

@@ -24,7 +24,7 @@ TEST(Shaping, ZWSP) {
BiDi bidi;
auto immutableGlyph = Immutable<Glyph>(makeMutable<Glyph>(std::move(glyph)));
const std::vector<std::string> fontStack{{"font-stack"}};
const SectionOptions sectionOptions(1.0f, fontStack);
const SectionOptions sectionOptions(1.0f, fontStack, GlyphIDType::FontPBF, 0);
const float layoutTextSize = 16.0f;
const float layoutTextSizeAtBucketZoomLevel = 16.0f;
GlyphMap glyphs = {{FontStackHasher()(fontStack), {{u'中', std::move(immutableGlyph)}}}};

View File

@@ -6,22 +6,22 @@
using namespace mbgl;
TEST(TaggedString, Trim) {
TaggedString basic(u" \t\ntrim that and not this \n\t", SectionOptions(1.0f, {}));
TaggedString basic(u" \t\ntrim that and not this \n\t", SectionOptions(1.0f, {}, GlyphIDType::FontPBF, 0));
basic.trim();
EXPECT_EQ(basic.rawText(), u"trim that and not this");
TaggedString twoSections;
twoSections.addTextSection(u" \t\ntrim that", 1.5f, {});
twoSections.addTextSection(u" and not this \n\t", 0.5f, {});
twoSections.addTextSection(u" \t\ntrim that", 1.5f, {}, GlyphIDType::FontPBF, false, {});
twoSections.addTextSection(u" and not this \n\t", 0.5f, {}, GlyphIDType::FontPBF, false, {});
twoSections.trim();
EXPECT_EQ(twoSections.rawText(), u"trim that and not this");
TaggedString empty(u"\n\t\v \r \t\n", SectionOptions(1.0f, {}));
TaggedString empty(u"\n\t\v \r \t\n", SectionOptions(1.0f, {}, GlyphIDType::FontPBF, 0));
empty.trim();
EXPECT_EQ(empty.rawText(), u"");
TaggedString noTrim(u"no trim!", SectionOptions(1.0f, {}));
TaggedString noTrim(u"no trim!", SectionOptions(1.0f, {}, GlyphIDType::FontPBF, 0));
noTrim.trim();
EXPECT_EQ(noTrim.rawText(), u"no trim!");
}

View File

@@ -29,7 +29,7 @@ public:
: SymbolFeature(std::make_unique<StubGeometryTileFeature>(
std::move(id_), type_, std::move(geometry_), std::move(properties_))) {
if (text_) {
formattedText = TaggedString(*text_, SectionOptions(1.0, {}));
formattedText = TaggedString(*text_, SectionOptions(1.0, {}, GlyphIDType::FontPBF, 0));
}
icon = std::move(icon_);
index = index_;