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