[core] Full support for line breaking bidirectional text using ICU bidi functionality.
- Trim whitespace from labels before determining their max-width for alignment. - Fix crash on labels that contain lines with only a single character of whitespace.
This commit is contained in:
@@ -24,7 +24,7 @@
|
||||
"lodash": "^4.16.4",
|
||||
"mapbox-gl-shaders": "mapbox/mapbox-gl-shaders#597115a1e1bd982944b068f8accde34eada74fc2",
|
||||
"mapbox-gl-style-spec": "mapbox/mapbox-gl-style-spec#7f62a4fc9f21e619824d68abbc4b03cbc1685572",
|
||||
"mapbox-gl-test-suite": "mapbox/mapbox-gl-test-suite#c32d0c5ac80e3b7393bc17b8944e64fa5cffd90a",
|
||||
"mapbox-gl-test-suite": "mapbox/mapbox-gl-test-suite#d85534f77e1b06fbfe7aa610c98a363be86fceb4",
|
||||
"mkdirp": "^0.5.1",
|
||||
"node-cmake": "^1.2.1",
|
||||
"request": "^2.72.0",
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
#pragma once
|
||||
|
||||
#include <mbgl/text/bidi.hpp>
|
||||
#include <mbgl/tile/geometry_tile_data.hpp>
|
||||
#include <mbgl/util/optional.hpp>
|
||||
|
||||
@@ -12,7 +11,6 @@ class SymbolFeature {
|
||||
public:
|
||||
GeometryCollection geometry;
|
||||
optional<std::u16string> text;
|
||||
optional<WritingDirection> writingDirection;
|
||||
optional<std::string> icon;
|
||||
std::size_t index;
|
||||
};
|
||||
|
||||
@@ -20,8 +20,6 @@
|
||||
#include <mbgl/util/platform.hpp>
|
||||
#include <mbgl/util/logging.hpp>
|
||||
|
||||
#include <mbgl/text/bidi.hpp>
|
||||
|
||||
namespace mbgl {
|
||||
|
||||
using namespace style;
|
||||
@@ -92,9 +90,7 @@ SymbolLayout::SymbolLayout(std::string bucketName_,
|
||||
u8string = platform::lowercase(u8string);
|
||||
}
|
||||
|
||||
std::u16string u16string = util::utf8_to_utf16::convert(u8string);
|
||||
ft.text = bidi.bidiTransform(u16string);
|
||||
ft.writingDirection = bidi.baseWritingDirection(u16string);
|
||||
ft.text = applyArabicShaping(util::utf8_to_utf16::convert(u8string));
|
||||
|
||||
// Loop through all characters of this text and collect unique codepoints.
|
||||
for (char16_t chr : *ft.text) {
|
||||
@@ -198,7 +194,6 @@ void SymbolLayout::prepare(uintptr_t tileUID,
|
||||
if (feature.text) {
|
||||
shapedText = glyphSet->getShaping(
|
||||
/* string */ *feature.text,
|
||||
/* base direction of text */ *feature.writingDirection,
|
||||
/* maxWidth: ems */ layout.get<SymbolPlacement>() != SymbolPlacementType::Line ?
|
||||
layout.get<TextMaxWidth>() * 24 : 0,
|
||||
/* lineHeight: ems */ layout.get<TextLineHeight>() * 24,
|
||||
@@ -206,7 +201,8 @@ void SymbolLayout::prepare(uintptr_t tileUID,
|
||||
/* verticalAlign */ verticalAlign,
|
||||
/* justify */ justify,
|
||||
/* spacing: ems */ layout.get<TextLetterSpacing>() * 24,
|
||||
/* translate */ Point<float>(layout.get<TextOffset>()[0], layout.get<TextOffset>()[1]));
|
||||
/* translate */ Point<float>(layout.get<TextOffset>()[0], layout.get<TextOffset>()[1]),
|
||||
/* bidirectional algorithm object */ bidi);
|
||||
|
||||
// Add the glyphs we need for this label to the glyph atlas.
|
||||
if (shapedText) {
|
||||
|
||||
@@ -2,41 +2,30 @@
|
||||
|
||||
#include <mbgl/text/bidi.hpp>
|
||||
#include <unicode/ubidi.h>
|
||||
#include <unicode/ubiditransform.h>
|
||||
#include <unicode/ushape.h>
|
||||
|
||||
namespace mbgl {
|
||||
|
||||
BiDi::BiDi() {
|
||||
UErrorCode errorCode = U_ZERO_ERROR;
|
||||
transform = ubiditransform_open(&errorCode); // Only error is failure to allocate memory, in
|
||||
// that case ubidi_transform would fall back to
|
||||
// creating transform object on the fly
|
||||
}
|
||||
|
||||
BiDi::~BiDi() {
|
||||
if (transform)
|
||||
ubiditransform_close(transform);
|
||||
}
|
||||
|
||||
std::u16string BiDi::bidiTransform(const std::u16string& input) {
|
||||
// Takes UTF16 input in logical order and applies Arabic shaping to the input while maintaining
|
||||
// logical order
|
||||
// Output won't be intelligible until the bidirectional algorithm is applied
|
||||
std::u16string applyArabicShaping(const std::u16string& input) {
|
||||
UErrorCode errorCode = U_ZERO_ERROR;
|
||||
|
||||
std::unique_ptr<UChar[]> outputText =
|
||||
std::make_unique<UChar[]>(input.size() * 2); // Maximum output of ubidi_transform is twice
|
||||
// the size of input according to
|
||||
// ubidi_transform.h
|
||||
uint32_t outputLength = ubiditransform_transform(
|
||||
transform, input.c_str(), static_cast<int32_t>(input.size()), outputText.get(),
|
||||
static_cast<int32_t>(input.size()) * 2,
|
||||
UBIDI_DEFAULT_LTR, // Assume input is LTR unless strong RTL characters are found
|
||||
UBIDI_LOGICAL, // Input is in logical order
|
||||
UBIDI_LTR, // Output is in "visual LTR" order
|
||||
UBIDI_VISUAL, // ''
|
||||
UBIDI_MIRRORING_ON, // Use mirroring lookups for things like parentheses that need mirroring
|
||||
// in RTL text
|
||||
U_SHAPE_LETTERS_SHAPE, // Add options here for handling numbers in bidirectional text
|
||||
&errorCode);
|
||||
int32_t outputLength =
|
||||
u_shapeArabic(input.c_str(), static_cast<int32_t>(input.size()), NULL, 0,
|
||||
(U_SHAPE_LETTERS_SHAPE & U_SHAPE_LETTERS_MASK) |
|
||||
(U_SHAPE_TEXT_DIRECTION_LOGICAL & U_SHAPE_TEXT_DIRECTION_MASK),
|
||||
&errorCode);
|
||||
|
||||
// Pre-flighting will always set U_BUFFER_OVERFLOW_ERROR
|
||||
errorCode = U_ZERO_ERROR;
|
||||
|
||||
std::unique_ptr<UChar[]> outputText = std::make_unique<UChar[]>(outputLength);
|
||||
u_shapeArabic(input.c_str(), static_cast<int32_t>(input.size()), outputText.get(), outputLength,
|
||||
(U_SHAPE_LETTERS_SHAPE & U_SHAPE_LETTERS_MASK) |
|
||||
(U_SHAPE_TEXT_DIRECTION_LOGICAL & U_SHAPE_TEXT_DIRECTION_MASK),
|
||||
&errorCode);
|
||||
|
||||
// If the algorithm fails for any reason, fall back to non-transformed text
|
||||
if (U_FAILURE(errorCode))
|
||||
@@ -45,12 +34,92 @@ std::u16string BiDi::bidiTransform(const std::u16string& input) {
|
||||
return std::u16string(outputText.get(), outputLength);
|
||||
}
|
||||
|
||||
WritingDirection BiDi::baseWritingDirection(const std::u16string& input) {
|
||||
// This just looks for the first character with a strong direction property, it does not perform
|
||||
// the BiDi algorithm
|
||||
return ubidi_getBaseDirection(input.c_str(), static_cast<int32_t>(input.size())) == UBIDI_RTL
|
||||
? WritingDirection::RightToLeft
|
||||
: WritingDirection::LeftToRight;
|
||||
ProcessedBiDiText::ProcessedBiDiText(BiDi& p_bidi) : bidi(p_bidi) {
|
||||
}
|
||||
|
||||
void ProcessedBiDiText::mergeParagraphLineBreaks(std::set<int32_t>& lineBreakPoints) {
|
||||
int32_t paragraphCount = ubidi_countParagraphs(bidi.bidiText);
|
||||
for (int32_t i = 0; i < paragraphCount; i++) {
|
||||
UErrorCode errorCode = U_ZERO_ERROR;
|
||||
int32_t paragraphEndIndex;
|
||||
ubidi_getParagraphByIndex(bidi.bidiText, i, NULL, ¶graphEndIndex, NULL, &errorCode);
|
||||
|
||||
if (U_FAILURE(errorCode))
|
||||
throw std::runtime_error(std::string("ProcessedBiDiText::mergeParagraphLineBreaks: ") +
|
||||
u_errorName(errorCode));
|
||||
|
||||
lineBreakPoints.insert(paragraphEndIndex);
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<std::u16string>
|
||||
ProcessedBiDiText::applyLineBreaking(std::set<int32_t> lineBreakPoints) {
|
||||
// BiDi::getLine will error if called across a paragraph boundary, so we need to ensure that all
|
||||
// paragraph
|
||||
// boundaries are included in the set of line break points. The calling code might not include
|
||||
// the line break because it
|
||||
// didn't need to wrap at that point, or because the text was separated with a more exotic code
|
||||
// point such as (U+001C)
|
||||
mergeParagraphLineBreaks(lineBreakPoints);
|
||||
|
||||
std::vector<std::u16string> transformedLines;
|
||||
int32_t start = 0;
|
||||
for (int32_t lineBreakPoint : lineBreakPoints) {
|
||||
transformedLines.push_back(bidi.getLine(start, lineBreakPoint));
|
||||
start = lineBreakPoint;
|
||||
}
|
||||
|
||||
return transformedLines;
|
||||
}
|
||||
|
||||
BiDi::BiDi() {
|
||||
bidiText = ubidi_open();
|
||||
bidiLine = ubidi_open();
|
||||
}
|
||||
|
||||
BiDi::~BiDi() {
|
||||
if (bidiText)
|
||||
ubidi_close(bidiText);
|
||||
|
||||
if (bidiLine)
|
||||
ubidi_close(bidiLine);
|
||||
}
|
||||
|
||||
ProcessedBiDiText BiDi::processText(const std::u16string& input) {
|
||||
UErrorCode errorCode = U_ZERO_ERROR;
|
||||
|
||||
ubidi_setPara(bidiText, input.c_str(), static_cast<int32_t>(input.size()), UBIDI_DEFAULT_LTR,
|
||||
NULL, &errorCode);
|
||||
|
||||
if (U_FAILURE(errorCode))
|
||||
throw std::runtime_error(std::string("BiDi::processText: ") + u_errorName(errorCode));
|
||||
|
||||
return ProcessedBiDiText(*this);
|
||||
}
|
||||
|
||||
std::u16string BiDi::getLine(int32_t start, int32_t end) {
|
||||
UErrorCode errorCode = U_ZERO_ERROR;
|
||||
ubidi_setLine(bidiText, start, end, bidiLine, &errorCode);
|
||||
|
||||
if (U_FAILURE(errorCode))
|
||||
throw std::runtime_error(std::string("BiDi::getLine (setLine): ") + u_errorName(errorCode));
|
||||
|
||||
// Because we set UBIDI_REMOVE_BIDI_CONTROLS, the output may be smaller than what we reserve
|
||||
// Setting UBIDI_INSERT_LRM_FOR_NUMERIC would require
|
||||
// ubidi_getLength(pBiDi)+2*ubidi_countRuns(pBiDi)
|
||||
int32_t outputLength = ubidi_getProcessedLength(bidiLine);
|
||||
std::unique_ptr<UChar[]> outputText = std::make_unique<UChar[]>(outputLength);
|
||||
|
||||
// UBIDI_DO_MIRRORING: Apply unicode mirroring of characters like parentheses
|
||||
// UBIDI_REMOVE_BIDI_CONTROLS: Now that all the lines are set, remove control characters so that
|
||||
// they don't show up on screen (some fonts have glyphs representing them)
|
||||
ubidi_writeReordered(bidiLine, outputText.get(), outputLength,
|
||||
UBIDI_DO_MIRRORING | UBIDI_REMOVE_BIDI_CONTROLS, &errorCode);
|
||||
|
||||
if (U_FAILURE(errorCode))
|
||||
throw std::runtime_error(std::string("BiDi::getLine (writeReordered): ") + u_errorName(errorCode));
|
||||
|
||||
return std::u16string(outputText.get(), outputLength);
|
||||
}
|
||||
|
||||
} // end namespace mbgl
|
||||
|
||||
@@ -1,25 +1,47 @@
|
||||
#pragma once
|
||||
|
||||
#include <set>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include <mbgl/util/noncopyable.hpp>
|
||||
|
||||
struct UBiDiTransform;
|
||||
struct UBiDi;
|
||||
|
||||
namespace mbgl {
|
||||
|
||||
enum class WritingDirection : bool { LeftToRight, RightToLeft };
|
||||
|
||||
|
||||
class BiDi;
|
||||
|
||||
std::u16string applyArabicShaping(const std::u16string&);
|
||||
|
||||
class ProcessedBiDiText {
|
||||
public:
|
||||
ProcessedBiDiText(BiDi&);
|
||||
|
||||
std::vector<std::u16string> applyLineBreaking(std::set<int32_t>);
|
||||
|
||||
private:
|
||||
void mergeParagraphLineBreaks(std::set<int32_t>&);
|
||||
|
||||
BiDi& bidi;
|
||||
};
|
||||
|
||||
class BiDi : private util::noncopyable {
|
||||
public:
|
||||
BiDi();
|
||||
~BiDi();
|
||||
|
||||
std::u16string bidiTransform(const std::u16string&);
|
||||
WritingDirection baseWritingDirection(const std::u16string&);
|
||||
// Calling processText resets internal state, invalidating any existing ProcessedBiDiText
|
||||
// objects
|
||||
ProcessedBiDiText processText(const std::u16string&);
|
||||
|
||||
friend class ProcessedBiDiText;
|
||||
|
||||
private:
|
||||
UBiDiTransform* transform;
|
||||
std::u16string getLine(int32_t start, int32_t end);
|
||||
|
||||
UBiDi* bidiText;
|
||||
UBiDi* bidiLine;
|
||||
};
|
||||
|
||||
} // end namespace mbgl
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
#include <mbgl/text/glyph_set.hpp>
|
||||
#include <mbgl/util/logging.hpp>
|
||||
#include <mbgl/math/minmax.hpp>
|
||||
#include <mbgl/text/glyph_set.hpp>
|
||||
#include <mbgl/util/i18n.hpp>
|
||||
#include <mbgl/text/bidi.hpp>
|
||||
#include <mbgl/util/logging.hpp>
|
||||
|
||||
#include <boost/algorithm/string.hpp>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cassert>
|
||||
|
||||
namespace mbgl {
|
||||
@@ -28,45 +30,47 @@ void GlyphSet::insert(uint32_t id, SDFGlyph&& glyph) {
|
||||
}
|
||||
}
|
||||
|
||||
const std::map<uint32_t, SDFGlyph> &GlyphSet::getSDFs() const {
|
||||
const std::map<uint32_t, SDFGlyph>& GlyphSet::getSDFs() const {
|
||||
return sdfs;
|
||||
}
|
||||
|
||||
const Shaping GlyphSet::getShaping(const std::u16string &string, const WritingDirection writingDirection, const float maxWidth,
|
||||
const float lineHeight, const float horizontalAlign,
|
||||
const float verticalAlign, const float justify,
|
||||
const float spacing, const Point<float> &translate) const {
|
||||
Shaping shaping(translate.x * 24, translate.y * 24, string);
|
||||
const Shaping GlyphSet::getShaping(const std::u16string& logicalInput,
|
||||
const float maxWidth,
|
||||
const float lineHeight,
|
||||
const float horizontalAlign,
|
||||
const float verticalAlign,
|
||||
const float justify,
|
||||
const float spacing,
|
||||
const Point<float>& translate,
|
||||
BiDi& bidi) const {
|
||||
|
||||
// the y offset *should* be part of the font metadata
|
||||
const int32_t yOffset = -17;
|
||||
// The string stored in shaping.text is used for finding duplicates, but may end up quite
|
||||
// different from the glyphs that get shown
|
||||
Shaping shaping(translate.x * 24, translate.y * 24, logicalInput);
|
||||
|
||||
float x = 0;
|
||||
const float y = yOffset;
|
||||
ProcessedBiDiText processedText = bidi.processText(logicalInput);
|
||||
|
||||
// Loop through all characters of this label and shape.
|
||||
for (char16_t chr : string) {
|
||||
auto it = sdfs.find(chr);
|
||||
if (it != sdfs.end()) {
|
||||
shaping.positionedGlyphs.emplace_back(chr, x, y);
|
||||
x += it->second.metrics.advance + spacing;
|
||||
}
|
||||
}
|
||||
std::vector<std::u16string> reorderedLines =
|
||||
processedText.applyLineBreaking(determineLineBreaks(logicalInput, spacing, maxWidth));
|
||||
|
||||
if (shaping.positionedGlyphs.empty())
|
||||
return shaping;
|
||||
|
||||
lineWrap(shaping, lineHeight, maxWidth, horizontalAlign, verticalAlign, justify, translate,
|
||||
util::i18n::allowsIdeographicBreaking(string), writingDirection);
|
||||
shapeLines(shaping, reorderedLines, spacing, lineHeight, horizontalAlign, verticalAlign,
|
||||
justify, translate);
|
||||
|
||||
return shaping;
|
||||
}
|
||||
|
||||
void align(Shaping &shaping, const float justify, const float horizontalAlign,
|
||||
const float verticalAlign, const uint32_t maxLineLength, const float lineHeight,
|
||||
const uint32_t line, const Point<float> &translate) {
|
||||
const float shiftX = (justify - horizontalAlign) * maxLineLength + ::round(translate.x * 24/* one em */);
|
||||
const float shiftY = (-verticalAlign * (line + 1) + 0.5) * lineHeight + ::round(translate.y * 24/* one em */);
|
||||
void align(Shaping& shaping,
|
||||
const float justify,
|
||||
const float horizontalAlign,
|
||||
const float verticalAlign,
|
||||
const float maxLineLength,
|
||||
const float lineHeight,
|
||||
const uint32_t lineCount,
|
||||
const Point<float>& translate) {
|
||||
const float shiftX =
|
||||
(justify - horizontalAlign) * maxLineLength + ::round(translate.x * 24 /* one em */);
|
||||
const float shiftY =
|
||||
(-verticalAlign * lineCount + 0.5) * lineHeight + ::round(translate.y * 24 /* one em */);
|
||||
|
||||
for (auto& glyph : shaping.positionedGlyphs) {
|
||||
glyph.x += shiftX;
|
||||
@@ -74,9 +78,16 @@ void align(Shaping &shaping, const float justify, const float horizontalAlign,
|
||||
}
|
||||
}
|
||||
|
||||
void justifyLine(std::vector<PositionedGlyph> &positionedGlyphs, const std::map<uint32_t, SDFGlyph> &sdfs, uint32_t start,
|
||||
uint32_t end, float justify) {
|
||||
PositionedGlyph &glyph = positionedGlyphs[end];
|
||||
// justify left = 0, right = 1, center = .5
|
||||
void justifyLine(std::vector<PositionedGlyph>& positionedGlyphs,
|
||||
const std::map<uint32_t, SDFGlyph>& sdfs,
|
||||
uint32_t start,
|
||||
uint32_t end,
|
||||
float justify) {
|
||||
if (!justify)
|
||||
return;
|
||||
|
||||
PositionedGlyph& glyph = positionedGlyphs[end];
|
||||
auto it = sdfs.find(glyph.glyph);
|
||||
if (it != sdfs.end()) {
|
||||
const uint32_t lastAdvance = it->second.metrics.advance;
|
||||
@@ -88,81 +99,122 @@ void justifyLine(std::vector<PositionedGlyph> &positionedGlyphs, const std::map<
|
||||
}
|
||||
}
|
||||
|
||||
void GlyphSet::lineWrap(Shaping &shaping, const float lineHeight, float maxWidth,
|
||||
const float horizontalAlign, const float verticalAlign,
|
||||
const float justify, const Point<float> &translate,
|
||||
bool useBalancedIdeographicBreaking, const WritingDirection writingDirection) const {
|
||||
float lineFeedOffset = writingDirection == WritingDirection::RightToLeft ? -lineHeight : lineHeight;
|
||||
|
||||
uint32_t lastSafeBreak = 0;
|
||||
float GlyphSet::determineIdeographicLineWidth(const std::u16string& logicalInput,
|
||||
const float spacing,
|
||||
float maxWidth) const {
|
||||
float totalWidth = 0;
|
||||
|
||||
uint32_t lengthBeforeCurrentLine = 0;
|
||||
uint32_t lineStartIndex = 0;
|
||||
uint32_t line = 0;
|
||||
|
||||
uint32_t maxLineLength = 0;
|
||||
|
||||
std::vector<PositionedGlyph> &positionedGlyphs = shaping.positionedGlyphs;
|
||||
|
||||
if (maxWidth) {
|
||||
if (useBalancedIdeographicBreaking) {
|
||||
auto lastPositionedGlyph = positionedGlyphs[positionedGlyphs.size() - 1];
|
||||
uint32_t estimatedLineCount = std::fmax(1, std::ceil(lastPositionedGlyph.x / maxWidth));
|
||||
maxWidth = lastPositionedGlyph.x / estimatedLineCount;
|
||||
}
|
||||
|
||||
for (uint32_t i = 0; i < positionedGlyphs.size(); i++) {
|
||||
PositionedGlyph &shape = positionedGlyphs[i];
|
||||
|
||||
shape.x -= lengthBeforeCurrentLine;
|
||||
shape.y += lineFeedOffset * line;
|
||||
|
||||
if (shape.x > maxWidth && lastSafeBreak > 0) {
|
||||
|
||||
uint32_t lineLength = positionedGlyphs[lastSafeBreak + 1].x;
|
||||
maxLineLength = util::max(lineLength, maxLineLength);
|
||||
|
||||
for (uint32_t k = lastSafeBreak + 1; k <= i; k++) {
|
||||
positionedGlyphs[k].y += lineFeedOffset;
|
||||
positionedGlyphs[k].x -= lineLength;
|
||||
}
|
||||
|
||||
if (justify) {
|
||||
// Collapse invisible characters.
|
||||
uint32_t breakGlyph = positionedGlyphs[lastSafeBreak].glyph;
|
||||
uint32_t lineEnd = lastSafeBreak;
|
||||
if (util::i18n::isVisible(breakGlyph)) {
|
||||
lineEnd--;
|
||||
}
|
||||
|
||||
justifyLine(positionedGlyphs, sdfs, lineStartIndex, lineEnd, justify);
|
||||
}
|
||||
|
||||
lineStartIndex = lastSafeBreak + 1;
|
||||
lastSafeBreak = 0;
|
||||
lengthBeforeCurrentLine += lineLength;
|
||||
line++;
|
||||
}
|
||||
|
||||
// Ideographic characters, spaces, and word-breaking punctuation that often appear without surrounding spaces.
|
||||
if (useBalancedIdeographicBreaking
|
||||
|| util::i18n::allowsWordBreaking(shape.glyph)
|
||||
|| util::i18n::allowsIdeographicBreaking(shape.glyph)) {
|
||||
lastSafeBreak = i;
|
||||
}
|
||||
}
|
||||
// totalWidth doesn't include the last character for magical tuning reasons. This makes the
|
||||
// algorithm a little
|
||||
// more agressive about trying to fit the text into fewer lines, taking advantage of the
|
||||
// tolerance for going a little
|
||||
// over maxWidth
|
||||
for (uint32_t i = 0; i < logicalInput.size() - 1; i++) {
|
||||
auto it = sdfs.find(logicalInput[i]);
|
||||
if (it != sdfs.end())
|
||||
totalWidth += it->second.metrics.advance + spacing;
|
||||
}
|
||||
|
||||
const PositionedGlyph& lastPositionedGlyph = positionedGlyphs.back();
|
||||
const auto lastGlyphIt = sdfs.find(lastPositionedGlyph.glyph);
|
||||
assert(lastGlyphIt != sdfs.end());
|
||||
const uint32_t lastLineLength = lastPositionedGlyph.x + lastGlyphIt->second.metrics.advance;
|
||||
maxLineLength = std::max(maxLineLength, lastLineLength);
|
||||
int32_t lineCount = std::fmax(1, std::ceil(totalWidth / maxWidth));
|
||||
return totalWidth / lineCount;
|
||||
}
|
||||
|
||||
const uint32_t height = (line + 1) * lineHeight;
|
||||
// We determine line breaks based on shaped text in logical order. Working in visual order would be
|
||||
// more intuitive, but we can't do that because the visual order may be changed by line breaks!
|
||||
std::set<int32_t> GlyphSet::determineLineBreaks(const std::u16string& logicalInput,
|
||||
const float spacing,
|
||||
float maxWidth) const {
|
||||
if (!maxWidth)
|
||||
return {};
|
||||
|
||||
justifyLine(positionedGlyphs, sdfs, lineStartIndex, uint32_t(positionedGlyphs.size()) - 1, justify);
|
||||
align(shaping, justify, horizontalAlign, verticalAlign, maxLineLength, lineHeight, line, translate);
|
||||
if (logicalInput.empty())
|
||||
return {};
|
||||
|
||||
if (util::i18n::allowsIdeographicBreaking(logicalInput))
|
||||
maxWidth = determineIdeographicLineWidth(logicalInput, spacing, maxWidth);
|
||||
|
||||
std::set<int32_t> lineBreakPoints;
|
||||
float currentX = 0;
|
||||
uint32_t lastSafeBreak = 0;
|
||||
float lastSafeBreakX = 0;
|
||||
|
||||
for (uint32_t i = 0; i < logicalInput.size(); i++) {
|
||||
auto it = sdfs.find(logicalInput[i]);
|
||||
if (it == sdfs.end())
|
||||
continue;
|
||||
|
||||
const SDFGlyph& glyph = it->second;
|
||||
|
||||
// Ideographic characters, spaces, and word-breaking punctuation that often appear without
|
||||
// surrounding spaces.
|
||||
if (util::i18n::allowsWordBreaking(glyph.id) ||
|
||||
util::i18n::allowsIdeographicBreaking(glyph.id)) {
|
||||
lastSafeBreak = i;
|
||||
lastSafeBreakX = currentX;
|
||||
}
|
||||
|
||||
if (currentX > maxWidth && lastSafeBreak > 0) {
|
||||
lineBreakPoints.insert(lastSafeBreak);
|
||||
currentX -= lastSafeBreakX;
|
||||
lastSafeBreakX = 0;
|
||||
}
|
||||
|
||||
currentX += glyph.metrics.advance + spacing;
|
||||
}
|
||||
|
||||
return lineBreakPoints;
|
||||
}
|
||||
|
||||
void GlyphSet::shapeLines(Shaping& shaping,
|
||||
const std::vector<std::u16string>& lines,
|
||||
const float spacing,
|
||||
const float lineHeight,
|
||||
const float horizontalAlign,
|
||||
const float verticalAlign,
|
||||
const float justify,
|
||||
const Point<float>& translate) const {
|
||||
|
||||
// the y offset *should* be part of the font metadata
|
||||
const int32_t yOffset = -17;
|
||||
|
||||
float x = 0;
|
||||
float y = yOffset;
|
||||
|
||||
float maxLineLength = 0;
|
||||
|
||||
for (std::u16string line : lines) {
|
||||
// Collapse whitespace so it doesn't throw off justification
|
||||
boost::algorithm::trim_if(line, boost::algorithm::is_any_of(u" \t\n\v\f\r"));
|
||||
|
||||
if (line.empty())
|
||||
continue;
|
||||
|
||||
uint32_t lineStartIndex = static_cast<uint32_t>(shaping.positionedGlyphs.size());
|
||||
for (char16_t chr : line) {
|
||||
auto it = sdfs.find(chr);
|
||||
if (it == sdfs.end())
|
||||
continue;
|
||||
|
||||
const SDFGlyph& glyph = it->second;
|
||||
shaping.positionedGlyphs.emplace_back(chr, x, y);
|
||||
x += glyph.metrics.advance + spacing;
|
||||
}
|
||||
|
||||
if (static_cast<uint32_t>(shaping.positionedGlyphs.size()) == lineStartIndex)
|
||||
continue;
|
||||
|
||||
maxLineLength = util::max(x, maxLineLength);
|
||||
|
||||
justifyLine(shaping.positionedGlyphs, sdfs, lineStartIndex,
|
||||
static_cast<uint32_t>(shaping.positionedGlyphs.size()) - 1, justify);
|
||||
|
||||
x = 0;
|
||||
y += lineHeight; // Move to next line
|
||||
}
|
||||
|
||||
align(shaping, justify, horizontalAlign, verticalAlign, maxLineLength, lineHeight,
|
||||
static_cast<uint32_t>(lines.size()), translate);
|
||||
const uint32_t height = lines.size() * lineHeight;
|
||||
|
||||
// Calculate the bounding box
|
||||
shaping.top += -verticalAlign * height;
|
||||
|
||||
@@ -9,15 +9,34 @@ namespace mbgl {
|
||||
class GlyphSet {
|
||||
public:
|
||||
void insert(uint32_t id, SDFGlyph&&);
|
||||
const std::map<uint32_t, SDFGlyph> &getSDFs() const;
|
||||
const Shaping getShaping(const std::u16string &string, const WritingDirection writingDirection, float maxWidth, float lineHeight,
|
||||
float horizontalAlign, float verticalAlign, float justify,
|
||||
float spacing, const Point<float> &translate) const;
|
||||
void lineWrap(Shaping &shaping, float lineHeight, float maxWidth, float horizontalAlign,
|
||||
float verticalAlign, float justify, const Point<float> &translate,
|
||||
bool useBalancedIdeographicBreaking, const WritingDirection writingDirection) const;
|
||||
const std::map<uint32_t, SDFGlyph>& getSDFs() const;
|
||||
const Shaping getShaping(const std::u16string& string,
|
||||
float maxWidth,
|
||||
float lineHeight,
|
||||
float horizontalAlign,
|
||||
float verticalAlign,
|
||||
float justify,
|
||||
float spacing,
|
||||
const Point<float>& translate,
|
||||
BiDi& bidi) const;
|
||||
|
||||
private:
|
||||
float determineIdeographicLineWidth(const std::u16string& logicalInput,
|
||||
const float spacing,
|
||||
float maxWidth) const;
|
||||
std::set<int32_t> determineLineBreaks(const std::u16string& logicalInput,
|
||||
const float spacing,
|
||||
float maxWidth) const;
|
||||
|
||||
void shapeLines(Shaping& shaping,
|
||||
const std::vector<std::u16string>& lines,
|
||||
const float spacing,
|
||||
float lineHeight,
|
||||
float horizontalAlign,
|
||||
float verticalAlign,
|
||||
float justify,
|
||||
const Point<float>& translate) const;
|
||||
|
||||
std::map<uint32_t, SDFGlyph> sdfs;
|
||||
};
|
||||
|
||||
|
||||
@@ -294,12 +294,6 @@ namespace mbgl {
|
||||
namespace util {
|
||||
namespace i18n {
|
||||
|
||||
bool isVisible(uint16_t chr) {
|
||||
return (chr == 0x0a /* newline */
|
||||
|| chr == 0x20 /* space */
|
||||
|| chr == 0x200b /* zero-width space */);
|
||||
}
|
||||
|
||||
bool allowsWordBreaking(uint16_t chr) {
|
||||
return (chr == 0x0a /* newline */
|
||||
|| chr == 0x20 /* space */
|
||||
@@ -324,6 +318,10 @@ bool allowsIdeographicBreaking(const std::u16string& string) {
|
||||
}
|
||||
|
||||
bool allowsIdeographicBreaking(uint16_t chr) {
|
||||
// Allow U+2027 "Interpunct" for hyphenation of Chinese words
|
||||
if (chr == 0x2027)
|
||||
return true;
|
||||
|
||||
// Return early for characters outside all ideographic ranges.
|
||||
if (chr < 0x2E80)
|
||||
return false;
|
||||
|
||||
@@ -6,9 +6,6 @@ namespace mbgl {
|
||||
namespace util {
|
||||
namespace i18n {
|
||||
|
||||
/** Returns whether a character is a visible character. */
|
||||
bool isVisible(uint16_t chr);
|
||||
|
||||
/** Returns whether a line break can be inserted after the character indicated
|
||||
by the given Unicode codepoint due to word breaking. */
|
||||
bool allowsWordBreaking(uint16_t chr);
|
||||
|
||||
@@ -9,21 +9,21 @@ const std::u16string bbb = u"b";
|
||||
TEST(MergeLines, SameText) {
|
||||
// merges lines with the same text
|
||||
std::vector<mbgl::SymbolFeature> input1 = {
|
||||
{ {{{0, 0}, {1, 0}, {2, 0}}}, aaa, mbgl::WritingDirection::LeftToRight, {}, 0 },
|
||||
{ {{{4, 0}, {5, 0}, {6, 0}}}, bbb, mbgl::WritingDirection::LeftToRight, {}, 0 },
|
||||
{ {{{8, 0}, {9, 0}}}, aaa, mbgl::WritingDirection::LeftToRight, {}, 0 },
|
||||
{ {{{2, 0}, {3, 0}, {4, 0}}}, aaa, mbgl::WritingDirection::LeftToRight, {}, 0 },
|
||||
{ {{{6, 0}, {7, 0}, {8, 0}}}, aaa, mbgl::WritingDirection::LeftToRight, {}, 0 },
|
||||
{ {{{5, 0}, {6, 0}}}, aaa, mbgl::WritingDirection::LeftToRight, {}, 0 }
|
||||
{ {{{0, 0}, {1, 0}, {2, 0}}}, aaa, {}, 0 },
|
||||
{ {{{4, 0}, {5, 0}, {6, 0}}}, bbb, {}, 0 },
|
||||
{ {{{8, 0}, {9, 0}}}, aaa, {}, 0 },
|
||||
{ {{{2, 0}, {3, 0}, {4, 0}}}, aaa, {}, 0 },
|
||||
{ {{{6, 0}, {7, 0}, {8, 0}}}, aaa, {}, 0 },
|
||||
{ {{{5, 0}, {6, 0}}}, aaa, {}, 0 }
|
||||
};
|
||||
|
||||
const std::vector<mbgl::SymbolFeature> expected1 = {
|
||||
{ {{{0, 0}, {1, 0}, {2, 0}, {3, 0}, {4, 0}}}, aaa, mbgl::WritingDirection::LeftToRight, {}, 0 },
|
||||
{ {{{4, 0}, {5, 0}, {6, 0}}}, bbb, mbgl::WritingDirection::LeftToRight, {}, 0 },
|
||||
{ {{{5, 0}, {6, 0}, {7, 0}, {8, 0}, {9, 0}}}, aaa, mbgl::WritingDirection::LeftToRight, {}, 0 },
|
||||
{ {{}}, aaa, mbgl::WritingDirection::LeftToRight, {}, 0 },
|
||||
{ {{}}, aaa, mbgl::WritingDirection::LeftToRight, {}, 0 },
|
||||
{ {{}}, aaa, mbgl::WritingDirection::LeftToRight, {}, 0 }
|
||||
{ {{{0, 0}, {1, 0}, {2, 0}, {3, 0}, {4, 0}}}, aaa, {}, 0 },
|
||||
{ {{{4, 0}, {5, 0}, {6, 0}}}, bbb, {}, 0 },
|
||||
{ {{{5, 0}, {6, 0}, {7, 0}, {8, 0}, {9, 0}}}, aaa, {}, 0 },
|
||||
{ {{}}, aaa, {}, 0 },
|
||||
{ {{}}, aaa, {}, 0 },
|
||||
{ {{}}, aaa, {}, 0 }
|
||||
};
|
||||
|
||||
mbgl::util::mergeLines(input1);
|
||||
@@ -36,15 +36,15 @@ TEST(MergeLines, SameText) {
|
||||
TEST(MergeLines, BothEnds) {
|
||||
// mergeLines handles merge from both ends
|
||||
std::vector<mbgl::SymbolFeature> input2 = {
|
||||
{ {{{0, 0}, {1, 0}, {2, 0}}}, aaa, mbgl::WritingDirection::LeftToRight, {}, 0 },
|
||||
{ {{{4, 0}, {5, 0}, {6, 0}}}, aaa, mbgl::WritingDirection::LeftToRight, {}, 0 },
|
||||
{ {{{2, 0}, {3, 0}, {4, 0}}}, aaa, mbgl::WritingDirection::LeftToRight, {}, 0 }
|
||||
{ {{{0, 0}, {1, 0}, {2, 0}}}, aaa, {}, 0 },
|
||||
{ {{{4, 0}, {5, 0}, {6, 0}}}, aaa, {}, 0 },
|
||||
{ {{{2, 0}, {3, 0}, {4, 0}}}, aaa, {}, 0 }
|
||||
};
|
||||
|
||||
const std::vector<mbgl::SymbolFeature> expected2 = {
|
||||
{ {{{0, 0}, {1, 0}, {2, 0}, {3, 0}, {4, 0}, {5, 0}, {6, 0}}}, aaa, mbgl::WritingDirection::LeftToRight, {}, 0 },
|
||||
{ {{}}, aaa, mbgl::WritingDirection::LeftToRight, {}, 0 },
|
||||
{ {{}}, aaa, mbgl::WritingDirection::LeftToRight, {}, 0 }
|
||||
{ {{{0, 0}, {1, 0}, {2, 0}, {3, 0}, {4, 0}, {5, 0}, {6, 0}}}, aaa, {}, 0 },
|
||||
{ {{}}, aaa, {}, 0 },
|
||||
{ {{}}, aaa, {}, 0 }
|
||||
};
|
||||
|
||||
mbgl::util::mergeLines(input2);
|
||||
@@ -57,15 +57,15 @@ TEST(MergeLines, BothEnds) {
|
||||
TEST(MergeLines, CircularLines) {
|
||||
// mergeLines handles circular lines
|
||||
std::vector<mbgl::SymbolFeature> input3 = {
|
||||
{ {{{0, 0}, {1, 0}, {2, 0}}}, aaa, mbgl::WritingDirection::LeftToRight, {}, 0 },
|
||||
{ {{{2, 0}, {3, 0}, {4, 0}}}, aaa, mbgl::WritingDirection::LeftToRight, {}, 0 },
|
||||
{ {{{4, 0}, {0, 0}}}, aaa, mbgl::WritingDirection::LeftToRight, {}, 0 }
|
||||
{ {{{0, 0}, {1, 0}, {2, 0}}}, aaa, {}, 0 },
|
||||
{ {{{2, 0}, {3, 0}, {4, 0}}}, aaa, {}, 0 },
|
||||
{ {{{4, 0}, {0, 0}}}, aaa, {}, 0 }
|
||||
};
|
||||
|
||||
const std::vector<mbgl::SymbolFeature> expected3 = {
|
||||
{ {{{0, 0}, {1, 0}, {2, 0}, {3, 0}, {4, 0}, {0, 0}}}, aaa, mbgl::WritingDirection::LeftToRight, {}, 0 },
|
||||
{ {{}}, aaa, mbgl::WritingDirection::LeftToRight, {}, 0 },
|
||||
{ {{}}, aaa, mbgl::WritingDirection::LeftToRight, {}, 0 }
|
||||
{ {{{0, 0}, {1, 0}, {2, 0}, {3, 0}, {4, 0}, {0, 0}}}, aaa, {}, 0 },
|
||||
{ {{}}, aaa, {}, 0 },
|
||||
{ {{}}, aaa, {}, 0 }
|
||||
};
|
||||
|
||||
mbgl::util::mergeLines(input3);
|
||||
|
||||
Reference in New Issue
Block a user