1 #include <mbgl/text/shaping.hpp>
2 #include <mbgl/util/i18n.hpp>
3 #include <mbgl/layout/symbol_feature.hpp>
4 #include <mbgl/math/minmax.hpp>
5 #include <mbgl/text/bidi.hpp>
6
7 #include <boost/algorithm/string.hpp>
8
9 #include <algorithm>
10 #include <cmath>
11
12 namespace mbgl {
13
14 struct AnchorAlignment {
AnchorAlignmentmbgl::AnchorAlignment15 AnchorAlignment(float horizontal_, float vertical_)
16 : horizontalAlign(horizontal_), verticalAlign(vertical_) {
17 }
18
19 float horizontalAlign;
20 float verticalAlign;
21 };
22
getAnchorAlignment(style::SymbolAnchorType anchor)23 AnchorAlignment getAnchorAlignment(style::SymbolAnchorType anchor) {
24 float horizontalAlign = 0.5;
25 float verticalAlign = 0.5;
26
27 switch (anchor) {
28 case style::SymbolAnchorType::Top:
29 case style::SymbolAnchorType::Bottom:
30 case style::SymbolAnchorType::Center:
31 break;
32 case style::SymbolAnchorType::Right:
33 case style::SymbolAnchorType::TopRight:
34 case style::SymbolAnchorType::BottomRight:
35 horizontalAlign = 1;
36 break;
37 case style::SymbolAnchorType::Left:
38 case style::SymbolAnchorType::TopLeft:
39 case style::SymbolAnchorType::BottomLeft:
40 horizontalAlign = 0;
41 break;
42 }
43
44 switch (anchor) {
45 case style::SymbolAnchorType::Left:
46 case style::SymbolAnchorType::Right:
47 case style::SymbolAnchorType::Center:
48 break;
49 case style::SymbolAnchorType::Bottom:
50 case style::SymbolAnchorType::BottomLeft:
51 case style::SymbolAnchorType::BottomRight:
52 verticalAlign = 1;
53 break;
54 case style::SymbolAnchorType::Top:
55 case style::SymbolAnchorType::TopLeft:
56 case style::SymbolAnchorType::TopRight:
57 verticalAlign = 0;
58 break;
59 }
60
61 return AnchorAlignment(horizontalAlign, verticalAlign);
62 }
63
shapeIcon(const ImagePosition & image,const std::array<float,2> & iconOffset,style::SymbolAnchorType iconAnchor,const float iconRotation)64 PositionedIcon PositionedIcon::shapeIcon(const ImagePosition& image, const std::array<float, 2>& iconOffset, style::SymbolAnchorType iconAnchor, const float iconRotation) {
65 AnchorAlignment anchorAlign = getAnchorAlignment(iconAnchor);
66 float dx = iconOffset[0];
67 float dy = iconOffset[1];
68 float x1 = dx - image.displaySize()[0] * anchorAlign.horizontalAlign;
69 float x2 = x1 + image.displaySize()[0];
70 float y1 = dy - image.displaySize()[1] * anchorAlign.verticalAlign;
71 float y2 = y1 + image.displaySize()[1];
72
73 return PositionedIcon { image, y1, y2, x1, x2, iconRotation };
74 }
75
align(Shaping & shaping,const float justify,const float horizontalAlign,const float verticalAlign,const float maxLineLength,const float lineHeight,const std::size_t lineCount)76 void align(Shaping& shaping,
77 const float justify,
78 const float horizontalAlign,
79 const float verticalAlign,
80 const float maxLineLength,
81 const float lineHeight,
82 const std::size_t lineCount) {
83 const float shiftX = (justify - horizontalAlign) * maxLineLength;
84 const float shiftY = (-verticalAlign * lineCount + 0.5) * lineHeight;
85
86 for (auto& glyph : shaping.positionedGlyphs) {
87 glyph.x += shiftX;
88 glyph.y += shiftY;
89 }
90 }
91
92 // justify left = 0, right = 1, center = .5
justifyLine(std::vector<PositionedGlyph> & positionedGlyphs,const Glyphs & glyphs,std::size_t start,std::size_t end,float justify)93 void justifyLine(std::vector<PositionedGlyph>& positionedGlyphs,
94 const Glyphs& glyphs,
95 std::size_t start,
96 std::size_t end,
97 float justify) {
98 if (!justify) {
99 return;
100 }
101
102 PositionedGlyph& glyph = positionedGlyphs[end];
103 auto it = glyphs.find(glyph.glyph);
104 if (it != glyphs.end() && it->second) {
105 const uint32_t lastAdvance = (*it->second)->metrics.advance;
106 const float lineIndent = float(glyph.x + lastAdvance) * justify;
107
108 for (std::size_t j = start; j <= end; j++) {
109 positionedGlyphs[j].x -= lineIndent;
110 }
111 }
112 }
113
determineAverageLineWidth(const std::u16string & logicalInput,const float spacing,float maxWidth,const Glyphs & glyphs)114 float determineAverageLineWidth(const std::u16string& logicalInput,
115 const float spacing,
116 float maxWidth,
117 const Glyphs& glyphs) {
118 float totalWidth = 0;
119
120 for (char16_t chr : logicalInput) {
121 auto it = glyphs.find(chr);
122 if (it != glyphs.end() && it->second) {
123 totalWidth += (*it->second)->metrics.advance + spacing;
124 }
125 }
126
127 int32_t targetLineCount = ::fmax(1, std::ceil(totalWidth / maxWidth));
128 return totalWidth / targetLineCount;
129 }
130
calculateBadness(const float lineWidth,const float targetWidth,const float penalty,const bool isLastBreak)131 float calculateBadness(const float lineWidth, const float targetWidth, const float penalty, const bool isLastBreak) {
132 const float raggedness = std::pow(lineWidth - targetWidth, 2);
133 if (isLastBreak) {
134 // Favor finals lines shorter than average over longer than average
135 if (lineWidth < targetWidth) {
136 return raggedness / 2;
137 } else {
138 return raggedness * 2;
139 }
140 }
141 if (penalty < 0) {
142 return raggedness - std::pow(penalty, 2);
143 }
144 return raggedness + std::pow(penalty, 2);
145 }
146
calculatePenalty(char16_t codePoint,char16_t nextCodePoint)147 float calculatePenalty(char16_t codePoint, char16_t nextCodePoint) {
148 float penalty = 0;
149 // Force break on newline
150 if (codePoint == 0x0a) {
151 penalty -= 10000;
152 }
153 // Penalize open parenthesis at end of line
154 if (codePoint == 0x28 || codePoint == 0xff08) {
155 penalty += 50;
156 }
157
158 // Penalize close parenthesis at beginning of line
159 if (nextCodePoint == 0x29 || nextCodePoint == 0xff09) {
160 penalty += 50;
161 }
162
163 return penalty;
164 }
165
166 struct PotentialBreak {
PotentialBreakmbgl::PotentialBreak167 PotentialBreak(const std::size_t p_index, const float p_x, const PotentialBreak* p_priorBreak, const float p_badness)
168 : index(p_index), x(p_x), priorBreak(p_priorBreak), badness(p_badness)
169 {}
170
171 const std::size_t index;
172 const float x;
173 const PotentialBreak* priorBreak;
174 const float badness;
175 };
176
177
evaluateBreak(const std::size_t breakIndex,const float breakX,const float targetWidth,const std::list<PotentialBreak> & potentialBreaks,const float penalty,const bool isLastBreak)178 PotentialBreak evaluateBreak(const std::size_t breakIndex, const float breakX, const float targetWidth, const std::list<PotentialBreak>& potentialBreaks, const float penalty, const bool isLastBreak) {
179 // We could skip evaluating breaks where the line length (breakX - priorBreak.x) > maxWidth
180 // ...but in fact we allow lines longer than maxWidth (if there's no break points)
181 // ...and when targetWidth and maxWidth are close, strictly enforcing maxWidth can give
182 // more lopsided results.
183
184 const PotentialBreak* bestPriorBreak = nullptr;
185 float bestBreakBadness = calculateBadness(breakX, targetWidth, penalty, isLastBreak);
186 for (const auto& potentialBreak : potentialBreaks) {
187 const float lineWidth = breakX - potentialBreak.x;
188 float breakBadness =
189 calculateBadness(lineWidth, targetWidth, penalty, isLastBreak) + potentialBreak.badness;
190 if (breakBadness <= bestBreakBadness) {
191 bestPriorBreak = &potentialBreak;
192 bestBreakBadness = breakBadness;
193 }
194 }
195
196 return PotentialBreak(breakIndex, breakX, bestPriorBreak, bestBreakBadness);
197 }
198
leastBadBreaks(const PotentialBreak & lastLineBreak)199 std::set<std::size_t> leastBadBreaks(const PotentialBreak& lastLineBreak) {
200 std::set<std::size_t> leastBadBreaks = { lastLineBreak.index };
201 const PotentialBreak* priorBreak = lastLineBreak.priorBreak;
202 while (priorBreak) {
203 leastBadBreaks.insert(priorBreak->index);
204 priorBreak = priorBreak->priorBreak;
205 }
206 return leastBadBreaks;
207 }
208
209
210 // We determine line breaks based on shaped text in logical order. Working in visual order would be
211 // more intuitive, but we can't do that because the visual order may be changed by line breaks!
determineLineBreaks(const std::u16string & logicalInput,const float spacing,float maxWidth,const WritingModeType writingMode,const Glyphs & glyphs)212 std::set<std::size_t> determineLineBreaks(const std::u16string& logicalInput,
213 const float spacing,
214 float maxWidth,
215 const WritingModeType writingMode,
216 const Glyphs& glyphs) {
217 if (!maxWidth || writingMode != WritingModeType::Horizontal) {
218 return {};
219 }
220
221 if (logicalInput.empty()) {
222 return {};
223 }
224
225 const float targetWidth = determineAverageLineWidth(logicalInput, spacing, maxWidth, glyphs);
226
227 std::list<PotentialBreak> potentialBreaks;
228 float currentX = 0;
229
230 for (std::size_t i = 0; i < logicalInput.size(); i++) {
231 const char16_t codePoint = logicalInput[i];
232 auto it = glyphs.find(codePoint);
233 if (it != glyphs.end() && it->second && !boost::algorithm::is_any_of(u" \t\n\v\f\r")(codePoint)) {
234 currentX += (*it->second)->metrics.advance + spacing;
235 }
236
237 // Ideographic characters, spaces, and word-breaking punctuation that often appear without
238 // surrounding spaces.
239 if ((i < logicalInput.size() - 1) &&
240 (util::i18n::allowsWordBreaking(codePoint) || util::i18n::allowsIdeographicBreaking(codePoint))) {
241 potentialBreaks.push_back(evaluateBreak(i+1, currentX, targetWidth, potentialBreaks,
242 calculatePenalty(codePoint, logicalInput[i+1]),
243 false));
244 }
245 }
246
247 return leastBadBreaks(evaluateBreak(logicalInput.size(), currentX, targetWidth, potentialBreaks, 0, true));
248 }
249
shapeLines(Shaping & shaping,const std::vector<std::u16string> & lines,const float spacing,const float lineHeight,const style::SymbolAnchorType textAnchor,const style::TextJustifyType textJustify,const float verticalHeight,const WritingModeType writingMode,const Glyphs & glyphs)250 void shapeLines(Shaping& shaping,
251 const std::vector<std::u16string>& lines,
252 const float spacing,
253 const float lineHeight,
254 const style::SymbolAnchorType textAnchor,
255 const style::TextJustifyType textJustify,
256 const float verticalHeight,
257 const WritingModeType writingMode,
258 const Glyphs& glyphs) {
259
260 // the y offset *should* be part of the font metadata
261 const int32_t yOffset = -17;
262
263 float x = 0;
264 float y = yOffset;
265
266 float maxLineLength = 0;
267
268 const float justify = textJustify == style::TextJustifyType::Right ? 1 :
269 textJustify == style::TextJustifyType::Left ? 0 :
270 0.5;
271
272 for (std::u16string line : lines) {
273 // Collapse whitespace so it doesn't throw off justification
274 boost::algorithm::trim_if(line, boost::algorithm::is_any_of(u" \t\n\v\f\r"));
275
276 if (line.empty()) {
277 y += lineHeight; // Still need a line feed after empty line
278 continue;
279 }
280
281 std::size_t lineStartIndex = shaping.positionedGlyphs.size();
282 for (char16_t chr : line) {
283 auto it = glyphs.find(chr);
284 if (it == glyphs.end() || !it->second) {
285 continue;
286 }
287
288 const Glyph& glyph = **it->second;
289
290 if (writingMode == WritingModeType::Horizontal || !util::i18n::hasUprightVerticalOrientation(chr)) {
291 shaping.positionedGlyphs.emplace_back(chr, x, y, false);
292 x += glyph.metrics.advance + spacing;
293 } else {
294 shaping.positionedGlyphs.emplace_back(chr, x, 0, true);
295 x += verticalHeight + spacing;
296 }
297 }
298
299 // Only justify if we placed at least one glyph
300 if (shaping.positionedGlyphs.size() != lineStartIndex) {
301 float lineLength = x - spacing; // Don't count trailing spacing
302 maxLineLength = util::max(lineLength, maxLineLength);
303
304 justifyLine(shaping.positionedGlyphs, glyphs, lineStartIndex,
305 shaping.positionedGlyphs.size() - 1, justify);
306 }
307
308 x = 0;
309 y += lineHeight;
310 }
311
312 auto anchorAlign = getAnchorAlignment(textAnchor);
313
314 align(shaping, justify, anchorAlign.horizontalAlign, anchorAlign.verticalAlign, maxLineLength,
315 lineHeight, lines.size());
316 const float height = lines.size() * lineHeight;
317
318 // Calculate the bounding box
319 shaping.top += -anchorAlign.verticalAlign * height;
320 shaping.bottom = shaping.top + height;
321 shaping.left += -anchorAlign.horizontalAlign * maxLineLength;
322 shaping.right = shaping.left + maxLineLength;
323 }
324
getShaping(const std::u16string & logicalInput,const float maxWidth,const float lineHeight,const style::SymbolAnchorType textAnchor,const style::TextJustifyType textJustify,const float spacing,const Point<float> & translate,const float verticalHeight,const WritingModeType writingMode,BiDi & bidi,const Glyphs & glyphs)325 const Shaping getShaping(const std::u16string& logicalInput,
326 const float maxWidth,
327 const float lineHeight,
328 const style::SymbolAnchorType textAnchor,
329 const style::TextJustifyType textJustify,
330 const float spacing,
331 const Point<float>& translate,
332 const float verticalHeight,
333 const WritingModeType writingMode,
334 BiDi& bidi,
335 const Glyphs& glyphs) {
336 Shaping shaping(translate.x, translate.y, writingMode);
337
338 std::vector<std::u16string> reorderedLines =
339 bidi.processText(logicalInput,
340 determineLineBreaks(logicalInput, spacing, maxWidth, writingMode, glyphs));
341
342 shapeLines(shaping, reorderedLines, spacing, lineHeight, textAnchor,
343 textJustify, verticalHeight, writingMode, glyphs);
344
345 return shaping;
346 }
347
348
349 } // namespace mbgl
350