keep digits and short codes upright in CJK line labels (#4566)

This commit is contained in:
Taiyu Yoshizawa
2026-09-08 06:17:43 +09:00
committed by GitHub
parent 042f21da5c
commit 57d1981497
24 changed files with 1438 additions and 9 deletions

View File

@@ -29,6 +29,7 @@
### 🐞 Bug fixes ### 🐞 Bug fixes
- *...Add new stuff here...* - *...Add new stuff here...*
- [core] Draw numbers and short uppercase codes upright in vertical CJK line labels ([#4565](https://github.com/maplibre/maplibre-native/issues/4565)), compat to [maplibre-gl-js#8205](https://github.com/maplibre/maplibre-gl-js/pull/8205).
- [core] Fix `ImageSource` not rendering across world copies ([#4508](https://github.com/maplibre/maplibre-native/issues/4508)). - [core] Fix `ImageSource` not rendering across world copies ([#4508](https://github.com/maplibre/maplibre-native/issues/4508)).
- [core] Repaint data-driven symbol paint properties after feature-state updates. - [core] Repaint data-driven symbol paint properties after feature-state updates.
- [core] Accept alpha values in legacy comma-separated `hsl()` colors ([#4434](https://github.com/maplibre/maplibre-native/issues/4434)). - [core] Accept alpha values in legacy comma-separated `hsl()` colors ([#4434](https://github.com/maplibre/maplibre-native/issues/4434)).

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

View File

@@ -0,0 +1,514 @@
{
"version": 8,
"metadata": {
"test": {
"height": 400,
"width": 900
}
},
"zoom": 3.5,
"center": [
1,
3
],
"sources": {
"maplibre": {
"type": "geojson",
"data": {
"type": "FeatureCollection",
"features": [
{
"type": "Feature",
"properties": {
"name": "札幌21タワー"
},
"geometry": {
"type": "LineString",
"coordinates": [
[
-17.5,
3
],
[
-9.5,
3
]
]
}
},
{
"type": "Feature",
"properties": {
"name": "札幌21タワー"
},
"geometry": {
"type": "LineString",
"coordinates": [
[
-17.6464,
3.3536
],
[
-9.8682,
11.1318
]
]
}
},
{
"type": "Feature",
"properties": {
"name": "札幌21タワー"
},
"geometry": {
"type": "LineString",
"coordinates": [
[
-18,
3.5
],
[
-18,
11.5
]
]
}
},
{
"type": "Feature",
"properties": {
"name": "札幌21タワー"
},
"geometry": {
"type": "LineString",
"coordinates": [
[
-18.3536,
3.3536
],
[
-26.1318,
11.1318
]
]
}
},
{
"type": "Feature",
"properties": {
"name": "札幌21タワー"
},
"geometry": {
"type": "LineString",
"coordinates": [
[
-18.5,
3
],
[
-26.5,
3
]
]
}
},
{
"type": "Feature",
"properties": {
"name": "札幌21タワー"
},
"geometry": {
"type": "LineString",
"coordinates": [
[
-18.3536,
2.6464
],
[
-26.1318,
-5.1318
]
]
}
},
{
"type": "Feature",
"properties": {
"name": "東京21タワー"
},
"geometry": {
"type": "LineString",
"coordinates": [
[
-18,
2.5
],
[
-18,
-5.5
]
]
}
},
{
"type": "Feature",
"properties": {
"name": "東京21タワー"
},
"geometry": {
"type": "LineString",
"coordinates": [
[
-17.6464,
2.6464
],
[
-9.8682,
-5.1318
]
]
}
},
{
"type": "Feature",
"properties": {
"name": "南京路21号"
},
"geometry": {
"type": "LineString",
"coordinates": [
[
1.5,
3
],
[
9.5,
3
]
]
}
},
{
"type": "Feature",
"properties": {
"name": "南京路21号"
},
"geometry": {
"type": "LineString",
"coordinates": [
[
1.3536,
3.3536
],
[
9.1318,
11.1318
]
]
}
},
{
"type": "Feature",
"properties": {
"name": "南京路21号"
},
"geometry": {
"type": "LineString",
"coordinates": [
[
1,
3.5
],
[
1,
11.5
]
]
}
},
{
"type": "Feature",
"properties": {
"name": "南京路21号"
},
"geometry": {
"type": "LineString",
"coordinates": [
[
0.6464,
3.3536
],
[
-7.1318,
11.1318
]
]
}
},
{
"type": "Feature",
"properties": {
"name": "南京路21号"
},
"geometry": {
"type": "LineString",
"coordinates": [
[
0.5,
3
],
[
-7.5,
3
]
]
}
},
{
"type": "Feature",
"properties": {
"name": "南京路21号"
},
"geometry": {
"type": "LineString",
"coordinates": [
[
0.6464,
2.6464
],
[
-7.1318,
-5.1318
]
]
}
},
{
"type": "Feature",
"properties": {
"name": "南京路21号"
},
"geometry": {
"type": "LineString",
"coordinates": [
[
1,
2.5
],
[
1,
-5.5
]
]
}
},
{
"type": "Feature",
"properties": {
"name": "南京路21号"
},
"geometry": {
"type": "LineString",
"coordinates": [
[
1.3536,
2.6464
],
[
9.1318,
-5.1318
]
]
}
},
{
"type": "Feature",
"properties": {
"name": "Street 123"
},
"geometry": {
"type": "LineString",
"coordinates": [
[
20.5,
3
],
[
28.5,
3
]
]
}
},
{
"type": "Feature",
"properties": {
"name": "Street 123"
},
"geometry": {
"type": "LineString",
"coordinates": [
[
20.3536,
3.3536
],
[
28.1318,
11.1318
]
]
}
},
{
"type": "Feature",
"properties": {
"name": "Street 123"
},
"geometry": {
"type": "LineString",
"coordinates": [
[
20,
3.5
],
[
20,
11.5
]
]
}
},
{
"type": "Feature",
"properties": {
"name": "Street 123"
},
"geometry": {
"type": "LineString",
"coordinates": [
[
19.6464,
3.3536
],
[
11.8682,
11.1318
]
]
}
},
{
"type": "Feature",
"properties": {
"name": "Street 123"
},
"geometry": {
"type": "LineString",
"coordinates": [
[
19.5,
3
],
[
11.5,
3
]
]
}
},
{
"type": "Feature",
"properties": {
"name": "Street 123"
},
"geometry": {
"type": "LineString",
"coordinates": [
[
19.6464,
2.6464
],
[
11.8682,
-5.1318
]
]
}
},
{
"type": "Feature",
"properties": {
"name": "Street 123"
},
"geometry": {
"type": "LineString",
"coordinates": [
[
20,
2.5
],
[
20,
-5.5
]
]
}
},
{
"type": "Feature",
"properties": {
"name": "Street 123"
},
"geometry": {
"type": "LineString",
"coordinates": [
[
20.3536,
2.6464
],
[
28.1318,
-5.1318
]
]
}
}
]
}
}
},
"glyphs": "local://glyphs/{fontstack}/{range}.pbf",
"layers": [
{
"id": "background",
"type": "background",
"paint": {
"background-color": "white"
}
},
{
"id": "lines-symbol",
"type": "symbol",
"source": "maplibre",
"layout": {
"text-field": "{name}",
"text-size": 12,
"symbol-placement": "line-center",
"symbol-spacing": 100,
"text-allow-overlap": true,
"text-ignore-placement": true,
"text-font": [
"NotoCJK"
]
}
},
{
"id": "lines",
"type": "line",
"source": "maplibre",
"paint": {
"line-opacity": 0.25
}
}
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

View File

@@ -0,0 +1,208 @@
{
"version": 8,
"metadata": {
"test": {
"height": 512,
"width": 900
}
},
"zoom": 2,
"center": [
0,
57
],
"sources": {
"maplibre": {
"type": "geojson",
"data": {
"type": "FeatureCollection",
"features": [
{
"type": "Feature",
"properties": {
"name": "身什戰1-2号"
},
"geometry": {
"type": "LineString",
"coordinates": [
[
-70,
39
],
[
-70,
75
]
]
}
},
{
"type": "Feature",
"properties": {
"name": "身什戰3.5号"
},
"geometry": {
"type": "LineString",
"coordinates": [
[
-50,
39
],
[
-50,
75
]
]
}
},
{
"type": "Feature",
"properties": {
"name": "身什戰1〜2号"
},
"geometry": {
"type": "LineString",
"coordinates": [
[
-30,
39
],
[
-30,
75
]
]
}
},
{
"type": "Feature",
"properties": {
"name": "身什戰A16号"
},
"geometry": {
"type": "LineString",
"coordinates": [
[
-10,
39
],
[
-10,
75
]
]
}
},
{
"type": "Feature",
"properties": {
"name": "身什戰ABC号"
},
"geometry": {
"type": "LineString",
"coordinates": [
[
10,
39
],
[
10,
75
]
]
}
},
{
"type": "Feature",
"properties": {
"name": "身什戰ABCD号"
},
"geometry": {
"type": "LineString",
"coordinates": [
[
30,
39
],
[
30,
75
]
]
}
},
{
"type": "Feature",
"properties": {
"name": "身什戰what号"
},
"geometry": {
"type": "LineString",
"coordinates": [
[
50,
39
],
[
50,
75
]
]
}
},
{
"type": "Feature",
"properties": {
"name": "ヴィラ ISHIKAWA"
},
"geometry": {
"type": "LineString",
"coordinates": [
[
70,
39
],
[
70,
75
]
]
}
}
]
}
}
},
"glyphs": "local://glyphs/{fontstack}/{range}.pbf",
"layers": [
{
"id": "background",
"type": "background",
"paint": {
"background-color": "white"
}
},
{
"id": "lines-symbol",
"type": "symbol",
"source": "maplibre",
"layout": {
"text-field": "{name}",
"symbol-placement": "line",
"symbol-spacing": 400,
"text-allow-overlap": true,
"text-font": [
"NotoCJK"
]
}
},
{
"id": "lines",
"type": "line",
"source": "maplibre",
"paint": {
"line-opacity": 0.25
}
}
]
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 KiB

After

Width:  |  Height:  |  Size: 28 KiB

View File

@@ -70,6 +70,7 @@ target_sources(
${PROJECT_SOURCE_DIR}/platform/default/src/mln/text/bidi.cpp ${PROJECT_SOURCE_DIR}/platform/default/src/mln/text/bidi.cpp
${PROJECT_SOURCE_DIR}/platform/default/src/mln/util/compression.cpp ${PROJECT_SOURCE_DIR}/platform/default/src/mln/util/compression.cpp
${PROJECT_SOURCE_DIR}/platform/default/src/mln/util/filesystem.cpp ${PROJECT_SOURCE_DIR}/platform/default/src/mln/util/filesystem.cpp
${PROJECT_SOURCE_DIR}/platform/default/src/mln/util/i18n.cpp
${PROJECT_SOURCE_DIR}/platform/default/src/mln/util/monotonic_timer.cpp ${PROJECT_SOURCE_DIR}/platform/default/src/mln/util/monotonic_timer.cpp
${PROJECT_SOURCE_DIR}/platform/default/src/mln/util/png_writer.cpp ${PROJECT_SOURCE_DIR}/platform/default/src/mln/util/png_writer.cpp
${PROJECT_SOURCE_DIR}/platform/default/src/mln/util/thread_local.cpp ${PROJECT_SOURCE_DIR}/platform/default/src/mln/util/thread_local.cpp

View File

@@ -80,6 +80,7 @@ target_sources(
${PROJECT_SOURCE_DIR}/platform/default/src/mln/text/bidi.cpp ${PROJECT_SOURCE_DIR}/platform/default/src/mln/text/bidi.cpp
${PROJECT_SOURCE_DIR}/platform/default/src/mln/util/compression.cpp ${PROJECT_SOURCE_DIR}/platform/default/src/mln/util/compression.cpp
${PROJECT_SOURCE_DIR}/platform/default/src/mln/util/filesystem.cpp ${PROJECT_SOURCE_DIR}/platform/default/src/mln/util/filesystem.cpp
${PROJECT_SOURCE_DIR}/platform/default/src/mln/util/i18n.cpp
${PROJECT_SOURCE_DIR}/platform/default/src/mln/util/monotonic_timer.cpp ${PROJECT_SOURCE_DIR}/platform/default/src/mln/util/monotonic_timer.cpp
${PROJECT_SOURCE_DIR}/platform/default/src/mln/util/png_writer.cpp ${PROJECT_SOURCE_DIR}/platform/default/src/mln/util/png_writer.cpp
${PROJECT_SOURCE_DIR}/platform/default/src/mln/util/thread_local.cpp ${PROJECT_SOURCE_DIR}/platform/default/src/mln/util/thread_local.cpp

View File

@@ -63,6 +63,7 @@ cc_library(
"src/mln/text/bidi.cpp", "src/mln/text/bidi.cpp",
"src/mln/util/compression.cpp", "src/mln/util/compression.cpp",
"src/mln/util/filesystem.cpp", "src/mln/util/filesystem.cpp",
"src/mln/util/i18n.cpp",
"src/mln/util/monotonic_timer.cpp", "src/mln/util/monotonic_timer.cpp",
"src/mln/util/png_writer.cpp", "src/mln/util/png_writer.cpp",
"src/mln/util/thread_local.cpp", "src/mln/util/thread_local.cpp",

View File

@@ -0,0 +1,23 @@
#include <mln/util/i18n.hpp>
#include <unicode/uchar.h>
namespace mln {
namespace util {
namespace i18n {
bool isDigit(char16_t chr) {
return u_charType(chr) == U_DECIMAL_DIGIT_NUMBER;
}
bool isUppercase(char16_t chr) {
return u_charType(chr) == U_UPPERCASE_LETTER;
}
bool isPunctuationOrSymbol(char16_t chr) {
return (U_GET_GC_MASK(chr) & (U_GC_P_MASK | U_GC_S_MASK)) != 0;
}
} // namespace i18n
} // namespace util
} // namespace mln

View File

@@ -57,6 +57,7 @@ target_sources(
${PROJECT_SOURCE_DIR}/platform/default/src/mln/util/async_task.cpp ${PROJECT_SOURCE_DIR}/platform/default/src/mln/util/async_task.cpp
${PROJECT_SOURCE_DIR}/platform/default/src/mln/util/compression.cpp ${PROJECT_SOURCE_DIR}/platform/default/src/mln/util/compression.cpp
${PROJECT_SOURCE_DIR}/platform/default/src/mln/util/filesystem.cpp ${PROJECT_SOURCE_DIR}/platform/default/src/mln/util/filesystem.cpp
${PROJECT_SOURCE_DIR}/platform/default/src/mln/util/i18n.cpp
${PROJECT_SOURCE_DIR}/platform/default/src/mln/util/image.cpp ${PROJECT_SOURCE_DIR}/platform/default/src/mln/util/image.cpp
${PROJECT_SOURCE_DIR}/platform/default/src/mln/util/jpeg_reader.cpp ${PROJECT_SOURCE_DIR}/platform/default/src/mln/util/jpeg_reader.cpp
${PROJECT_SOURCE_DIR}/platform/default/src/mln/util/webp_reader.cpp ${PROJECT_SOURCE_DIR}/platform/default/src/mln/util/webp_reader.cpp

View File

@@ -112,6 +112,7 @@ target_sources(
${PROJECT_SOURCE_DIR}/platform/qt/src/mln/http_file_source.hpp ${PROJECT_SOURCE_DIR}/platform/qt/src/mln/http_file_source.hpp
${PROJECT_SOURCE_DIR}/platform/qt/src/mln/http_request.cpp ${PROJECT_SOURCE_DIR}/platform/qt/src/mln/http_request.cpp
${PROJECT_SOURCE_DIR}/platform/qt/src/mln/http_request.hpp ${PROJECT_SOURCE_DIR}/platform/qt/src/mln/http_request.hpp
${PROJECT_SOURCE_DIR}/platform/qt/src/mln/i18n.cpp
${PROJECT_SOURCE_DIR}/platform/qt/src/mln/image.cpp ${PROJECT_SOURCE_DIR}/platform/qt/src/mln/image.cpp
${PROJECT_SOURCE_DIR}/platform/qt/src/mln/number_format.cpp ${PROJECT_SOURCE_DIR}/platform/qt/src/mln/number_format.cpp
${PROJECT_SOURCE_DIR}/platform/qt/src/mln/local_glyph_rasterizer.cpp ${PROJECT_SOURCE_DIR}/platform/qt/src/mln/local_glyph_rasterizer.cpp

View File

@@ -0,0 +1,23 @@
#include <mln/util/i18n.hpp>
#include <QChar>
namespace mln {
namespace util {
namespace i18n {
bool isDigit(char16_t chr) {
return QChar(chr).isDigit();
}
bool isUppercase(char16_t chr) {
return QChar(chr).isUpper();
}
bool isPunctuationOrSymbol(char16_t chr) {
return QChar(chr).isPunct() || QChar(chr).isSymbol();
}
} // namespace i18n
} // namespace util
} // namespace mln

View File

@@ -78,6 +78,7 @@ target_sources(
${PROJECT_SOURCE_DIR}/platform/default/src/mln/util/async_task.cpp ${PROJECT_SOURCE_DIR}/platform/default/src/mln/util/async_task.cpp
${PROJECT_SOURCE_DIR}/platform/default/src/mln/util/compression.cpp ${PROJECT_SOURCE_DIR}/platform/default/src/mln/util/compression.cpp
${PROJECT_SOURCE_DIR}/platform/default/src/mln/util/filesystem.cpp ${PROJECT_SOURCE_DIR}/platform/default/src/mln/util/filesystem.cpp
${PROJECT_SOURCE_DIR}/platform/default/src/mln/util/i18n.cpp
${PROJECT_SOURCE_DIR}/platform/default/src/mln/util/image.cpp ${PROJECT_SOURCE_DIR}/platform/default/src/mln/util/image.cpp
${PROJECT_SOURCE_DIR}/platform/default/src/mln/util/jpeg_reader.cpp ${PROJECT_SOURCE_DIR}/platform/default/src/mln/util/jpeg_reader.cpp
${PROJECT_SOURCE_DIR}/platform/default/src/mln/util/webp_reader.cpp ${PROJECT_SOURCE_DIR}/platform/default/src/mln/util/webp_reader.cpp

View File

@@ -405,6 +405,74 @@ std::set<std::size_t> determineLineBreaks(const TaggedString& logicalInput,
return leastBadBreaks(evaluateBreak(logicalInput.length(), currentX, targetWidth, potentialBreaks, 0, true)); return leastBadBreaks(evaluateBreak(logicalInput.length(), currentX, targetWidth, potentialBreaks, 0, true));
} }
namespace {
constexpr std::size_t maxUprightLetterRun = 3;
// Longer uppercase runs are words, while short runs such as "JR" and "A1" are codes.
bool runIsUpright(const TaggedString& line, std::size_t start, std::size_t end) {
bool hasDigit = false;
bool isNumber = true;
bool isShortUppercaseCode = end - start <= maxUprightLetterRun;
for (std::size_t i = start; i < end; ++i) {
const auto chr = line.getCharCodeAt(i);
const bool isDigit = util::i18n::isDigit(chr);
hasDigit |= isDigit;
isNumber &= isDigit || util::i18n::isPunctuationOrSymbol(chr);
isShortUppercaseCode &= isDigit || util::i18n::isUppercase(chr);
}
return (hasDigit && isNumber) || isShortUppercaseCode;
}
// Punctuation such as the hyphen in "1-2" needs a vertical presentation form
// now that its neighbors are upright. The glyph is replaced during shaping.
void markSurroundedPunctuationUpright(const TaggedString& line, std::vector<bool>& verticals) {
const auto isText = [&](std::size_t i) {
const auto& section = line.getSection(i);
return section.type == GlyphIDType::FontPBF && !section.imageID;
};
for (std::size_t i = 0; i < line.length(); ++i) {
if (!verticals[i] && isText(i) && util::i18n::verticalizePunctuation(line.getCharCodeAt(i)) &&
(i == 0 || verticals[i - 1]) && (i + 1 == line.length() || verticals[i + 1])) {
verticals[i] = true;
}
}
}
// Keep words and combining marks together; images and shaped glyph IDs delimit runs.
std::vector<bool> determineLineVerticals(const TaggedString& line) {
std::vector<bool> verticals(line.length(), false);
const auto isText = [&](std::size_t i) {
const auto& section = line.getSection(i);
return section.type == GlyphIDType::FontPBF && !section.imageID;
};
for (std::size_t i = 0; i < line.length(); ++i) {
verticals[i] = isText(i) && util::i18n::hasUprightVerticalOrientation(line.getCharCodeAt(i));
}
const auto isRunCharacter = [&](std::size_t i) {
return isText(i) && !verticals[i] && !util::i18n::isWhitespace(line.getCharCodeAt(i));
};
for (std::size_t start = 0; start < line.length(); ++start) {
if (!isRunCharacter(start)) continue;
std::size_t end = start + 1;
while (end < line.length() && isRunCharacter(end)) ++end;
if (runIsUpright(line, start, end)) {
for (std::size_t i = start; i < end; ++i) {
const auto chr = line.getCharCodeAt(i);
verticals[i] = (util::i18n::isUppercase(chr) || util::i18n::isDigit(chr)) &&
!util::i18n::isCharInComplexShapingScript(chr);
}
}
start = end - 1;
}
markSurroundedPunctuationUpright(line, verticals);
return verticals;
}
} // namespace
void shapeLines(Shaping& shaping, void shapeLines(Shaping& shaping,
std::vector<TaggedString>& lines, std::vector<TaggedString>& lines,
const float spacing, const float spacing,
@@ -443,6 +511,10 @@ void shapeLines(Shaping& shaping,
continue; continue;
} }
const auto lineVerticals = writingMode == WritingModeType::Vertical && !allowVerticalPlacement
? determineLineVerticals(line)
: std::vector<bool>{};
for (std::size_t i = 0; i < line.length(); i++) { for (std::size_t i = 0; i < line.length(); i++) {
const std::size_t sectionIndex = line.getSectionIndex(i); const std::size_t sectionIndex = line.getSectionIndex(i);
const SectionOptions& section = line.sectionAt(sectionIndex); const SectionOptions& section = line.sectionAt(sectionIndex);
@@ -465,15 +537,19 @@ void shapeLines(Shaping& shaping,
double sectionScale = section.scale; double sectionScale = section.scale;
assert(sectionScale); assert(sectionScale);
const bool vertical = !( const bool vertical = !lineVerticals.empty()
writingMode == WritingModeType::Horizontal || codePoint.complex.type != GlyphIDType::FontPBF || ? lineVerticals[i]
// Don't verticalize glyphs that have no upright orientation : writingMode != WritingModeType::Horizontal &&
// if vertical placement is disabled. codePoint.complex.type == GlyphIDType::FontPBF &&
(!allowVerticalPlacement && !util::i18n::hasUprightVerticalOrientation(codePoint)) || (allowVerticalPlacement
// If vertical placement is ebabled, don't verticalize glyphs ? !util::i18n::isWhitespace(codePoint) &&
// that are from complex text layout script, or whitespaces. !util::i18n::isCharInComplexShapingScript(codePoint)
(allowVerticalPlacement && : util::i18n::hasUprightVerticalOrientation(codePoint));
(util::i18n::isWhitespace(codePoint) || util::i18n::isCharInComplexShapingScript(codePoint)))); if (!lineVerticals.empty() && vertical && !util::i18n::hasUprightVerticalOrientation(codePoint)) {
if (const char16_t replacement = util::i18n::verticalizePunctuation(codePoint)) {
codePoint = GlyphID(replacement, section.type);
}
}
if (!section.imageID) { if (!section.imageID) {
auto glyphPositionMap = glyphPositions.find(section.fontStackHash); auto glyphPositionMap = glyphPositions.find(section.fontStackHash);

View File

@@ -87,6 +87,12 @@ bool isCharInComplexShapingScript(char16_t chr);
bool isWhitespace(char16_t chr); bool isWhitespace(char16_t chr);
bool isDigit(char16_t chr);
bool isUppercase(char16_t chr);
bool isPunctuationOrSymbol(char16_t chr);
bool isVariationSelector1(char16_t chr); bool isVariationSelector1(char16_t chr);
} // namespace i18n } // namespace i18n

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.6 KiB

After

Width:  |  Height:  |  Size: 7.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.6 KiB

After

Width:  |  Height:  |  Size: 7.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.4 KiB

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 18 KiB

View File

@@ -5,10 +5,103 @@
#include <mln/text/tagged_string.hpp> #include <mln/text/tagged_string.hpp>
#include <mln/text/shaping.hpp> #include <mln/text/shaping.hpp>
#include <mln/util/constants.hpp> #include <mln/util/constants.hpp>
#include <mln/util/i18n.hpp>
#include <optional>
using namespace mln; using namespace mln;
using namespace util; using namespace util;
namespace {
void addStubGlyph(GlyphMap& glyphs,
GlyphPositions& glyphPositions,
const FontStackHash fontStackHash,
const char16_t codePoint,
const GlyphIDType type) {
const GlyphID glyphID(codePoint, type);
GlyphMetrics metrics;
metrics.width = 10;
metrics.height = 18;
metrics.left = 1;
metrics.top = -8;
metrics.advance = 12;
Glyph glyph;
glyph.id = glyphID;
glyph.metrics = metrics;
glyphs[fontStackHash][glyphID] = Immutable<Glyph>(makeMutable<Glyph>(std::move(glyph)));
GlyphPosition position;
position.metrics = metrics;
glyphPositions[fontStackHash][glyphID] = position;
}
void addStubGlyphs(const TaggedString& string, GlyphMap& glyphs, GlyphPositions& glyphPositions) {
for (std::size_t i = 0; i < string.length(); ++i) {
const auto codePoint = string.getCharCodeAt(i);
const auto& section = string.getSection(i);
if (section.imageID) continue;
addStubGlyph(glyphs, glyphPositions, section.fontStackHash, codePoint, section.type);
if (section.type == GlyphIDType::FontPBF) {
if (const auto verticalCodePoint = util::i18n::verticalizePunctuation(codePoint)) {
addStubGlyph(glyphs, glyphPositions, section.fontStackHash, verticalCodePoint, section.type);
}
}
}
}
Shaping shapeTaggedText(const TaggedString& string,
const WritingModeType writingMode = WritingModeType::Vertical,
const bool allowVerticalPlacement = false,
ImagePositions imagePositions = {}) {
GlyphMap glyphs;
GlyphPositions glyphPositions;
addStubGlyphs(string, glyphs, glyphPositions);
BiDi bidi;
return getShaping(string,
4096.0f,
ONE_EM,
style::SymbolAnchorType::TopLeft,
style::TextJustifyType::Left,
0.0f,
{{0.0f, 0.0f}},
writingMode,
bidi,
glyphs,
glyphPositions,
imagePositions,
ONE_EM,
ONE_EM,
allowVerticalPlacement);
}
Shaping shapeLineLabel(const std::u16string& text,
const WritingModeType writingMode = WritingModeType::Vertical,
const bool allowVerticalPlacement = false) {
const FontStack fontStack{{"font-stack"}};
return shapeTaggedText(TaggedString(text, SectionOptions(1.0, fontStack, GlyphIDType::FontPBF, 0)),
writingMode,
allowVerticalPlacement);
}
using GlyphOrientation = std::pair<char16_t, std::string>;
std::vector<GlyphOrientation> getGlyphOrientations(const Shaping& shaping) {
std::vector<GlyphOrientation> result;
for (const auto& line : shaping.positionedLines) {
for (const auto& glyph : line.positionedGlyphs) {
result.emplace_back(glyph.glyph.complex.code, glyph.vertical ? "upright" : "along-line");
}
}
return result;
}
} // namespace
TEST(Shaping, ZWSP) { TEST(Shaping, ZWSP) {
GlyphPosition glyphPosition; GlyphPosition glyphPosition;
glyphPosition.metrics.width = 18; glyphPosition.metrics.width = 18;
@@ -104,6 +197,485 @@ TEST(Shaping, ZWSP) {
} }
} }
// Regression cases ported from maplibre-gl-js#8205 for maplibre-native#4565.
TEST(Shaping, VerticalDigitsBetweenCJKCharacters) {
const auto shapedLineLabel = shapeLineLabel(u"반포대로21길");
const auto orientations = getGlyphOrientations(shapedLineLabel);
const std::vector<GlyphOrientation> expected = {
{u'반', "upright"},
{u'포', "upright"},
{u'대', "upright"},
{u'로', "upright"},
{u'2', "upright"},
{u'1', "upright"},
{u'길', "upright"},
};
EXPECT_EQ(orientations, expected);
}
TEST(Shaping, VerticalTrailingDigits) {
const auto shapedLineLabel = shapeLineLabel(u"身什戰33");
const auto orientations = getGlyphOrientations(shapedLineLabel);
const std::vector<GlyphOrientation> expected = {
{u'身', "upright"},
{u'什', "upright"},
{u'戰', "upright"},
{u'3', "upright"},
{u'3', "upright"},
};
EXPECT_EQ(orientations, expected);
}
TEST(Shaping, VerticalDigitsOfAnyLength) {
const auto shapedLineLabel = shapeLineLabel(u"国道1234号");
const auto orientations = getGlyphOrientations(shapedLineLabel);
const std::vector<GlyphOrientation> expected = {
{u'国', "upright"},
{u'道', "upright"},
{u'1', "upright"},
{u'2', "upright"},
{u'3', "upright"},
{u'4', "upright"},
{u'号', "upright"},
};
EXPECT_EQ(orientations, expected);
}
TEST(Shaping, VerticalWhitespaceSeparatedDigit) {
const auto shapedLineLabel = shapeLineLabel(u"身什戰 1");
const auto orientations = getGlyphOrientations(shapedLineLabel);
const std::vector<GlyphOrientation> expected = {
{u'身', "upright"},
{u'什', "upright"},
{u'戰', "upright"},
{u' ', "along-line"},
{u'1', "upright"},
};
EXPECT_EQ(orientations, expected);
}
TEST(Shaping, VerticalSingleUppercaseLetter) {
const auto shapedLineLabel = shapeLineLabel(u"国道A号");
const auto orientations = getGlyphOrientations(shapedLineLabel);
const std::vector<GlyphOrientation> expected = {
{u'国', "upright"},
{u'道', "upright"},
{u'A', "upright"},
{u'号', "upright"},
};
EXPECT_EQ(orientations, expected);
}
TEST(Shaping, VerticalDecomposedLatinLetter) {
const auto shapedLineLabel = shapeLineLabel(u"国道e\u0301号");
const auto orientations = getGlyphOrientations(shapedLineLabel);
const std::vector<GlyphOrientation> expected = {
{u'国', "upright"},
{u'道', "upright"},
{u'e', "along-line"},
{u'\u0301', "along-line"},
{u'号', "upright"},
};
EXPECT_EQ(orientations, expected);
}
TEST(Shaping, VerticalLowercaseGreekLetter) {
const auto shapedLineLabel = shapeLineLabel(u"国道α号");
const auto orientations = getGlyphOrientations(shapedLineLabel);
const std::vector<GlyphOrientation> expected = {
{u'国', "upright"},
{u'道', "upright"},
{u'α', "along-line"},
{u'号', "upright"},
};
EXPECT_EQ(orientations, expected);
}
TEST(Shaping, VerticalNonLatinUppercaseCode) {
const auto shapedLineLabel = shapeLineLabel(u"国道\u041C1号");
const auto orientations = getGlyphOrientations(shapedLineLabel);
const std::vector<GlyphOrientation> expected = {
{u'国', "upright"},
{u'道', "upright"},
{u'\u041C', "upright"},
{u'1', "upright"},
{u'号', "upright"},
};
EXPECT_EQ(orientations, expected);
}
TEST(Shaping, VerticalComplexScript) {
const auto shapedLineLabel = shapeLineLabel(u"国道ب号");
const auto orientations = getGlyphOrientations(shapedLineLabel);
const std::vector<GlyphOrientation> expected = {
{u'国', "upright"},
{u'道', "upright"},
{u'ب', "along-line"},
{u'号', "upright"},
};
EXPECT_EQ(orientations, expected);
}
TEST(Shaping, VerticalProlongedSoundMark) {
const auto shapedLineLabel = shapeLineLabel(u"札幌タワー");
const auto orientations = getGlyphOrientations(shapedLineLabel);
const std::vector<GlyphOrientation> expected = {
{u'札', "upright"},
{u'幌', "upright"},
{u'タ', "upright"},
{u'ワ', "upright"},
{u'ー', "along-line"},
};
EXPECT_EQ(orientations, expected);
}
TEST(Shaping, VerticalWaveDashBetweenDigits) {
const auto shapedLineLabel = shapeLineLabel(u"身什戰1〜2");
const auto orientations = getGlyphOrientations(shapedLineLabel);
const std::vector<GlyphOrientation> expected = {
{u'身', "upright"},
{u'什', "upright"},
{u'戰', "upright"},
{u'1', "upright"},
{u'〜', "along-line"},
{u'2', "upright"},
};
EXPECT_EQ(orientations, expected);
}
TEST(Shaping, VerticalLowercaseLatinWord) {
const auto shapedLineLabel = shapeLineLabel(u"two 身什戰");
const auto orientations = getGlyphOrientations(shapedLineLabel);
const std::vector<GlyphOrientation> expected = {
{u't', "along-line"},
{u'w', "along-line"},
{u'o', "along-line"},
{u' ', "along-line"},
{u'身', "upright"},
{u'什', "upright"},
{u'戰', "upright"},
};
EXPECT_EQ(orientations, expected);
}
TEST(Shaping, VerticalMixedAlphanumericCode) {
const auto shapedLineLabel = shapeLineLabel(u"身什戰A1");
const auto orientations = getGlyphOrientations(shapedLineLabel);
const std::vector<GlyphOrientation> expected = {
{u'身', "upright"},
{u'什', "upright"},
{u'戰', "upright"},
{u'A', "upright"},
{u'1', "upright"},
};
EXPECT_EQ(orientations, expected);
}
TEST(Shaping, VerticalLatinWordAdjoiningCJK) {
const auto shapedLineLabel = shapeLineLabel(u"銀座Ginza通り");
const auto orientations = getGlyphOrientations(shapedLineLabel);
const std::vector<GlyphOrientation> expected = {
{u'銀', "upright"},
{u'座', "upright"},
{u'G', "along-line"},
{u'i', "along-line"},
{u'n', "along-line"},
{u'z', "along-line"},
{u'a', "along-line"},
{u'通', "upright"},
{u'り', "upright"},
};
EXPECT_EQ(orientations, expected);
}
TEST(Shaping, VerticalLongUppercaseWord) {
const auto shapedLineLabel = shapeLineLabel(u"ヴィラ ISHIKAWA");
const auto orientations = getGlyphOrientations(shapedLineLabel);
const std::vector<GlyphOrientation> expected = {
{u'ヴ', "upright"},
{u'ィ', "upright"},
{u'ラ', "upright"},
{u' ', "along-line"},
{u'I', "along-line"},
{u'S', "along-line"},
{u'H', "along-line"},
{u'I', "along-line"},
{u'K', "along-line"},
{u'A', "along-line"},
{u'W', "along-line"},
{u'A', "along-line"},
};
EXPECT_EQ(orientations, expected);
}
TEST(Shaping, VerticalShortUppercaseCode) {
const auto shapedLineLabel = shapeLineLabel(u"JR山手線");
const auto orientations = getGlyphOrientations(shapedLineLabel);
const std::vector<GlyphOrientation> expected = {
{u'J', "upright"},
{u'R', "upright"},
{u'山', "upright"},
{u'手', "upright"},
{u'線', "upright"},
};
EXPECT_EQ(orientations, expected);
}
TEST(Shaping, VerticalPunctuationBetweenDigits) {
const auto shapedLineLabel = shapeLineLabel(u"国道1-2号");
const auto orientations = getGlyphOrientations(shapedLineLabel);
const std::vector<GlyphOrientation> expected = {
{u'国', "upright"},
{u'道', "upright"},
{u'1', "upright"},
{u'︲', "upright"},
{u'2', "upright"},
{u'号', "upright"},
};
EXPECT_EQ(orientations, expected);
}
TEST(Shaping, VerticalPunctuationInRotatedRun) {
const auto shapedLineLabel = shapeLineLabel(u"国道3.5km");
const auto orientations = getGlyphOrientations(shapedLineLabel);
const std::vector<GlyphOrientation> expected = {
{u'国', "upright"},
{u'道', "upright"},
{u'3', "along-line"},
{u'.', "along-line"},
{u'5', "along-line"},
{u'k', "along-line"},
{u'm', "along-line"},
};
EXPECT_EQ(orientations, expected);
}
TEST(Shaping, VerticalMissingGlyphPreservesIndices) {
const FontStack fontStack{{"font-stack"}};
const auto fontStackHash = FontStackHasher()(fontStack);
const TaggedString text(u"what 国21号", SectionOptions(1.0, fontStack, GlyphIDType::FontPBF, 0));
GlyphMap glyphs;
GlyphPositions glyphPositions;
addStubGlyphs(text, glyphs, glyphPositions);
// Missing "w" must not shift the character index used for the remaining glyphs.
glyphs.at(fontStackHash).erase(u'w');
glyphPositions.at(fontStackHash).erase(u'w');
BiDi bidi;
const auto shapedLineLabel = getShaping(text,
4096.0f,
ONE_EM,
style::SymbolAnchorType::TopLeft,
style::TextJustifyType::Left,
0.0f,
{{0.0f, 0.0f}},
WritingModeType::Vertical,
bidi,
glyphs,
glyphPositions,
{},
ONE_EM,
ONE_EM,
false);
const auto orientations = getGlyphOrientations(shapedLineLabel);
const std::vector<GlyphOrientation> expected = {
{u'h', "along-line"},
{u'a', "along-line"},
{u't', "along-line"},
{u' ', "along-line"},
{u'国', "upright"},
{u'2', "upright"},
{u'1', "upright"},
{u'号', "upright"},
};
EXPECT_EQ(orientations, expected);
}
TEST(Shaping, HorizontalGlyphOrientation) {
const auto shapedLineLabel = shapeLineLabel(u"身什戰33", WritingModeType::Horizontal);
const auto orientations = getGlyphOrientations(shapedLineLabel);
const std::vector<GlyphOrientation> expected = {
{u'身', "along-line"},
{u'什', "along-line"},
{u'戰', "along-line"},
{u'3', "along-line"},
{u'3', "along-line"},
};
EXPECT_EQ(orientations, expected);
}
TEST(Shaping, AllowedVerticalGlyphOrientation) {
const auto shapedLineLabel = shapeLineLabel(u"two 身什戰", WritingModeType::Vertical, true);
const auto orientations = getGlyphOrientations(shapedLineLabel);
const std::vector<GlyphOrientation> expected = {
{u't', "upright"},
{u'w', "upright"},
{u'o', "upright"},
{u' ', "along-line"},
{u'身', "upright"},
{u'什', "upright"},
{u'戰', "upright"},
};
EXPECT_EQ(orientations, expected);
}
TEST(Shaping, VerticalThreeCharacterUppercaseCode) {
const auto shapedLineLabel = shapeLineLabel(u"国道ABC号");
const auto orientations = getGlyphOrientations(shapedLineLabel);
const std::vector<GlyphOrientation> expected = {
{u'国', "upright"},
{u'道', "upright"},
{u'A', "upright"},
{u'B', "upright"},
{u'C', "upright"},
{u'号', "upright"},
};
EXPECT_EQ(orientations, expected);
}
TEST(Shaping, VerticalFourCharacterUppercaseWord) {
const auto shapedLineLabel = shapeLineLabel(u"国道ABCD号");
const auto orientations = getGlyphOrientations(shapedLineLabel);
const std::vector<GlyphOrientation> expected = {
{u'国', "upright"},
{u'道', "upright"},
{u'A', "along-line"},
{u'B', "along-line"},
{u'C', "along-line"},
{u'D', "along-line"},
{u'号', "upright"},
};
EXPECT_EQ(orientations, expected);
}
TEST(Shaping, VerticalBengaliDigits) {
const auto shapedLineLabel = shapeLineLabel(u"国道১২号");
const auto orientations = getGlyphOrientations(shapedLineLabel);
const std::vector<GlyphOrientation> expected = {
{u'国', "upright"},
{u'道', "upright"},
{u'১', "upright"},
{u'২', "upright"},
{u'号', "upright"},
};
EXPECT_EQ(orientations, expected);
}
TEST(Shaping, VerticalArabicIndicDigits) {
const auto shapedLineLabel = shapeLineLabel(u"国道١٢号");
const auto orientations = getGlyphOrientations(shapedLineLabel);
const std::vector<GlyphOrientation> expected = {
{u'国', "upright"},
{u'道', "upright"},
{u'١', "along-line"},
{u'٢', "along-line"},
{u'号', "upright"},
};
EXPECT_EQ(orientations, expected);
}
TEST(Shaping, VerticalDigitWithCombiningMark) {
const auto shapedLineLabel = shapeLineLabel(u"国道1\u0301号");
const auto orientations = getGlyphOrientations(shapedLineLabel);
const std::vector<GlyphOrientation> expected = {
{u'国', "upright"},
{u'道', "upright"},
{u'1', "along-line"},
{u'\u0301', "along-line"},
{u'号', "upright"},
};
EXPECT_EQ(orientations, expected);
}
TEST(Shaping, VerticalGlyphOrientationRespectsFormattedSections) {
const FontStack fontStack{{"font-stack"}};
TaggedString text;
text.addTextSection(u"AB", 1.0, fontStack, GlyphIDType::FontPBF);
text.addTextSection(u"CD", 0.5, fontStack, GlyphIDType::FontPBF);
const auto shapedLineLabel = shapeTaggedText(text);
const auto orientations = getGlyphOrientations(shapedLineLabel);
const std::vector<GlyphOrientation> expected = {
{u'A', "along-line"},
{u'B', "along-line"},
{u'C', "along-line"},
{u'D', "along-line"},
};
EXPECT_EQ(orientations, expected);
}
TEST(Shaping, VerticalGlyphOrientationDelimitsInlineImages) {
const FontStack fontStack{{"font-stack"}};
TaggedString text;
text.addTextSection(u"AB", 1.0, fontStack, GlyphIDType::FontPBF);
text.addImageSection("image");
text.addTextSection(u"CD", 1.0, fontStack, GlyphIDType::FontPBF);
const style::Image::Impl image("image", PremultipliedImage({12, 24}), 1.0f);
ImagePositions imagePositions;
imagePositions.emplace("image", ImagePosition(Rect<uint16_t>(0, 0, 14, 26), image));
const auto shapedLineLabel = shapeTaggedText(text, WritingModeType::Vertical, false, std::move(imagePositions));
ASSERT_EQ(shapedLineLabel.positionedLines.size(), 1u);
ASSERT_EQ(shapedLineLabel.positionedLines.front().positionedGlyphs.size(), 5u);
const auto orientations = getGlyphOrientations(shapedLineLabel);
const std::vector<GlyphOrientation> expected = {
{u'A', "upright"},
{u'B', "upright"},
{u'\uE000', "along-line"}, // Inline image placeholder.
{u'C', "upright"},
{u'D', "upright"},
};
EXPECT_EQ(orientations, expected);
EXPECT_EQ(shapedLineLabel.positionedLines.front().positionedGlyphs[2].imageID, std::optional<std::string>("image"));
EXPECT_TRUE(shapedLineLabel.iconsInText);
}
TEST(Shaping, VerticalGlyphOrientationDelimitsNativeGlyphSections) {
const FontStack fontStack{{"font-stack"}};
const auto nativeType = genNewGlyphIDType("shaping-test-native", fontStack, {{0, 0xffff}});
TaggedString text;
text.addTextSection(u"AB", 1.0, fontStack, GlyphIDType::FontPBF);
text.addTextSection(u"C", 1.0, fontStack, nativeType);
text.addTextSection(u"D", 1.0, fontStack, GlyphIDType::FontPBF);
const auto shapedLineLabel = shapeTaggedText(text);
const auto orientations = getGlyphOrientations(shapedLineLabel);
const std::vector<GlyphOrientation> expected = {
{u'A', "upright"},
{u'B', "upright"},
{u'C', "along-line"},
{u'D', "upright"},
};
EXPECT_EQ(orientations, expected);
}
void setupShapedText(Shaping& shapedText, float textSize) { void setupShapedText(Shaping& shapedText, float textSize) {
const auto glyph = PositionedGlyph(32, const auto glyph = PositionedGlyph(32,
0.0f, 0.0f,