diff --git a/CHANGELOG.md b/CHANGELOG.md index e83f6b6e6..09779a2a4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -29,6 +29,7 @@ ### 🐞 Bug fixes - *...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] 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)). diff --git a/metrics/integration/render-tests/text-writing-mode/line_label/mixed-upright-digits/expected.png b/metrics/integration/render-tests/text-writing-mode/line_label/mixed-upright-digits/expected.png new file mode 100644 index 000000000..ebd0126fc Binary files /dev/null and b/metrics/integration/render-tests/text-writing-mode/line_label/mixed-upright-digits/expected.png differ diff --git a/metrics/integration/render-tests/text-writing-mode/line_label/mixed-upright-digits/style.json b/metrics/integration/render-tests/text-writing-mode/line_label/mixed-upright-digits/style.json new file mode 100644 index 000000000..bfa33e35a --- /dev/null +++ b/metrics/integration/render-tests/text-writing-mode/line_label/mixed-upright-digits/style.json @@ -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 + } + } + ] +} diff --git a/metrics/integration/render-tests/text-writing-mode/line_label/mixed-upright-edge-cases/expected.png b/metrics/integration/render-tests/text-writing-mode/line_label/mixed-upright-edge-cases/expected.png new file mode 100644 index 000000000..23b8879de Binary files /dev/null and b/metrics/integration/render-tests/text-writing-mode/line_label/mixed-upright-edge-cases/expected.png differ diff --git a/metrics/integration/render-tests/text-writing-mode/line_label/mixed-upright-edge-cases/style.json b/metrics/integration/render-tests/text-writing-mode/line_label/mixed-upright-edge-cases/style.json new file mode 100644 index 000000000..c1340bddb --- /dev/null +++ b/metrics/integration/render-tests/text-writing-mode/line_label/mixed-upright-edge-cases/style.json @@ -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 + } + } + ] +} diff --git a/metrics/integration/render-tests/text-writing-mode/line_label/mixed/expected.png b/metrics/integration/render-tests/text-writing-mode/line_label/mixed/expected.png index ea816c34b..e2cc7a2d0 100644 Binary files a/metrics/integration/render-tests/text-writing-mode/line_label/mixed/expected.png and b/metrics/integration/render-tests/text-writing-mode/line_label/mixed/expected.png differ diff --git a/platform/android/android.cmake b/platform/android/android.cmake index abf959dfa..910bdb920 100644 --- a/platform/android/android.cmake +++ b/platform/android/android.cmake @@ -70,6 +70,7 @@ target_sources( ${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/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/png_writer.cpp ${PROJECT_SOURCE_DIR}/platform/default/src/mln/util/thread_local.cpp diff --git a/platform/darwin/darwin.cmake b/platform/darwin/darwin.cmake index d19b20827..e33b69ac4 100644 --- a/platform/darwin/darwin.cmake +++ b/platform/darwin/darwin.cmake @@ -80,6 +80,7 @@ target_sources( ${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/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/png_writer.cpp ${PROJECT_SOURCE_DIR}/platform/default/src/mln/util/thread_local.cpp diff --git a/platform/default/BUILD.bazel b/platform/default/BUILD.bazel index af461b2c6..f96cb1635 100644 --- a/platform/default/BUILD.bazel +++ b/platform/default/BUILD.bazel @@ -63,6 +63,7 @@ cc_library( "src/mln/text/bidi.cpp", "src/mln/util/compression.cpp", "src/mln/util/filesystem.cpp", + "src/mln/util/i18n.cpp", "src/mln/util/monotonic_timer.cpp", "src/mln/util/png_writer.cpp", "src/mln/util/thread_local.cpp", diff --git a/platform/default/src/mln/util/i18n.cpp b/platform/default/src/mln/util/i18n.cpp new file mode 100644 index 000000000..9d8c351fe --- /dev/null +++ b/platform/default/src/mln/util/i18n.cpp @@ -0,0 +1,23 @@ +#include + +#include + +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 diff --git a/platform/linux/linux.cmake b/platform/linux/linux.cmake index 0164feb8d..1ea05c3c8 100644 --- a/platform/linux/linux.cmake +++ b/platform/linux/linux.cmake @@ -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/compression.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/jpeg_reader.cpp ${PROJECT_SOURCE_DIR}/platform/default/src/mln/util/webp_reader.cpp diff --git a/platform/qt/qt.cmake b/platform/qt/qt.cmake index e67f8cd0e..c9afad4de 100644 --- a/platform/qt/qt.cmake +++ b/platform/qt/qt.cmake @@ -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_request.cpp ${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/number_format.cpp ${PROJECT_SOURCE_DIR}/platform/qt/src/mln/local_glyph_rasterizer.cpp diff --git a/platform/qt/src/mln/i18n.cpp b/platform/qt/src/mln/i18n.cpp new file mode 100644 index 000000000..7082af131 --- /dev/null +++ b/platform/qt/src/mln/i18n.cpp @@ -0,0 +1,23 @@ +#include + +#include + +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 diff --git a/platform/windows/windows.cmake b/platform/windows/windows.cmake index d28403607..820dad5a5 100644 --- a/platform/windows/windows.cmake +++ b/platform/windows/windows.cmake @@ -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/compression.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/jpeg_reader.cpp ${PROJECT_SOURCE_DIR}/platform/default/src/mln/util/webp_reader.cpp diff --git a/src/mln/text/shaping.cpp b/src/mln/text/shaping.cpp index 6bad45c05..5db03e6d3 100644 --- a/src/mln/text/shaping.cpp +++ b/src/mln/text/shaping.cpp @@ -405,6 +405,74 @@ std::set determineLineBreaks(const TaggedString& logicalInput, 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& 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 determineLineVerticals(const TaggedString& line) { + std::vector 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, std::vector& lines, const float spacing, @@ -443,6 +511,10 @@ void shapeLines(Shaping& shaping, continue; } + const auto lineVerticals = writingMode == WritingModeType::Vertical && !allowVerticalPlacement + ? determineLineVerticals(line) + : std::vector{}; + for (std::size_t i = 0; i < line.length(); i++) { const std::size_t sectionIndex = line.getSectionIndex(i); const SectionOptions& section = line.sectionAt(sectionIndex); @@ -465,15 +537,19 @@ void shapeLines(Shaping& shaping, double sectionScale = section.scale; assert(sectionScale); - const bool vertical = !( - writingMode == WritingModeType::Horizontal || codePoint.complex.type != GlyphIDType::FontPBF || - // Don't verticalize glyphs that have no upright orientation - // if vertical placement is disabled. - (!allowVerticalPlacement && !util::i18n::hasUprightVerticalOrientation(codePoint)) || - // If vertical placement is ebabled, don't verticalize glyphs - // that are from complex text layout script, or whitespaces. - (allowVerticalPlacement && - (util::i18n::isWhitespace(codePoint) || util::i18n::isCharInComplexShapingScript(codePoint)))); + const bool vertical = !lineVerticals.empty() + ? lineVerticals[i] + : writingMode != WritingModeType::Horizontal && + codePoint.complex.type == GlyphIDType::FontPBF && + (allowVerticalPlacement + ? !util::i18n::isWhitespace(codePoint) && + !util::i18n::isCharInComplexShapingScript(codePoint) + : util::i18n::hasUprightVerticalOrientation(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) { auto glyphPositionMap = glyphPositions.find(section.fontStackHash); diff --git a/src/mln/util/i18n.hpp b/src/mln/util/i18n.hpp index 9f306ef8e..5c351d6cf 100644 --- a/src/mln/util/i18n.hpp +++ b/src/mln/util/i18n.hpp @@ -87,6 +87,12 @@ bool isCharInComplexShapingScript(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); } // namespace i18n diff --git a/test/fixtures/local_glyphs/no_local/expected.png b/test/fixtures/local_glyphs/no_local/expected.png index 7e2049518..e0b7a0fd6 100644 Binary files a/test/fixtures/local_glyphs/no_local/expected.png and b/test/fixtures/local_glyphs/no_local/expected.png differ diff --git a/test/fixtures/local_glyphs/no_local_with_content_insets/expected.png b/test/fixtures/local_glyphs/no_local_with_content_insets/expected.png index da5e6957d..81d75e67c 100644 Binary files a/test/fixtures/local_glyphs/no_local_with_content_insets/expected.png and b/test/fixtures/local_glyphs/no_local_with_content_insets/expected.png differ diff --git a/test/fixtures/local_glyphs/no_local_with_content_insets_and_pitch/expected.png b/test/fixtures/local_glyphs/no_local_with_content_insets_and_pitch/expected.png index 250147156..4c3a29bf0 100644 Binary files a/test/fixtures/local_glyphs/no_local_with_content_insets_and_pitch/expected.png and b/test/fixtures/local_glyphs/no_local_with_content_insets_and_pitch/expected.png differ diff --git a/test/fixtures/local_glyphs/noto_sans_cjk_kr_regular_qt/expected.png b/test/fixtures/local_glyphs/noto_sans_cjk_kr_regular_qt/expected.png index 34b176881..eb56dd174 100644 Binary files a/test/fixtures/local_glyphs/noto_sans_cjk_kr_regular_qt/expected.png and b/test/fixtures/local_glyphs/noto_sans_cjk_kr_regular_qt/expected.png differ diff --git a/test/fixtures/local_glyphs/ping_fang/expected.png b/test/fixtures/local_glyphs/ping_fang/expected.png index a4e34459b..350b15b42 100644 Binary files a/test/fixtures/local_glyphs/ping_fang/expected.png and b/test/fixtures/local_glyphs/ping_fang/expected.png differ diff --git a/test/fixtures/local_glyphs/ping_fang_semibold/expected.png b/test/fixtures/local_glyphs/ping_fang_semibold/expected.png index f52fdf0fa..743c3f0d9 100644 Binary files a/test/fixtures/local_glyphs/ping_fang_semibold/expected.png and b/test/fixtures/local_glyphs/ping_fang_semibold/expected.png differ diff --git a/test/fixtures/local_glyphs/ping_fang_with_bold_in_style/expected.png b/test/fixtures/local_glyphs/ping_fang_with_bold_in_style/expected.png index 11b1fee9b..ce72111e6 100644 Binary files a/test/fixtures/local_glyphs/ping_fang_with_bold_in_style/expected.png and b/test/fixtures/local_glyphs/ping_fang_with_bold_in_style/expected.png differ diff --git a/test/text/shaping.test.cpp b/test/text/shaping.test.cpp index cd0e09605..c031493c3 100644 --- a/test/text/shaping.test.cpp +++ b/test/text/shaping.test.cpp @@ -5,10 +5,103 @@ #include #include #include +#include + +#include using namespace mln; 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(makeMutable(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; + +std::vector getGlyphOrientations(const Shaping& shaping) { + std::vector 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) { GlyphPosition glyphPosition; 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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(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 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("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 expected = { + {u'A', "upright"}, + {u'B', "upright"}, + {u'C', "along-line"}, + {u'D', "upright"}, + }; + EXPECT_EQ(orientations, expected); +} + void setupShapedText(Shaping& shapedText, float textSize) { const auto glyph = PositionedGlyph(32, 0.0f,