• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2015 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 #define VERBOSE_DEBUG 0
18 
19 #define LOG_TAG "Minikin"
20 
21 #include <algorithm>
22 #include <climits>
23 #include <limits>
24 
25 #include <log/log.h>
26 
27 #include <minikin/Layout.h>
28 #include <minikin/LineBreaker.h>
29 #include <utils/WindowsUtils.h>
30 #include "LayoutUtils.h"
31 
32 using std::vector;
33 
34 namespace minikin {
35 
36 // Large scores in a hierarchy; we prefer desperate breaks to an overfull line.
37 // All these constants are larger than any reasonable actual width score.
38 const float SCORE_INFTY = std::numeric_limits<float>::max();
39 const float SCORE_OVERFULL = 1e12f;
40 const float SCORE_DESPERATE = 1e10f;
41 
42 // Multiplier for hyphen penalty on last line.
43 const float LAST_LINE_PENALTY_MULTIPLIER = 4.0f;
44 // Penalty assigned to each line break (to try to minimize number of lines)
45 // TODO: when we implement full justification (so spaces can shrink and
46 // stretch), this is probably not the most appropriate method.
47 const float LINE_PENALTY_MULTIPLIER = 2.0f;
48 
49 // Penalty assigned to shrinking the whitepsace.
50 const float SHRINK_PENALTY_MULTIPLIER = 4.0f;
51 
52 // Very long words trigger O(n^2) behavior in hyphenation, so we disable
53 // hyphenation for unreasonably long words. This is somewhat of a heuristic
54 // because extremely long words are possible in some languages. This does mean
55 // that very long real words can get broken by desperate breaks, with no
56 // hyphens.
57 const size_t LONGEST_HYPHENATED_WORD = 45;
58 
59 // When the text buffer is within this limit, capacity of vectors is retained at
60 // finish(), to avoid allocation.
61 const size_t MAX_TEXT_BUF_RETAIN = 32678;
62 
63 // Maximum amount that spaces can shrink, in justified text.
64 const float SHRINKABILITY = 1.0 / 3.0;
65 
66 // libtxt: Add a fudge factor to comparisons between currentLineWidth and
67 // postBreak width.  The currentLineWidth passed by the Flutter framework
68 // is based on maxIntrinsicWidth/Layout::measureText calculations that may
69 // not precisely match the postBreak width.
70 const float LIBTXT_WIDTH_ADJUST = 0.00001;
71 
setLocale()72 void LineBreaker::setLocale() {
73   mWordBreaker.setLocale();
74   mLocale = icu::Locale();
75   mHyphenator = nullptr;
76 }
77 
setText()78 void LineBreaker::setText() {
79   mWordBreaker.setText(mTextBuf.data(), mTextBuf.size());
80 
81   // handle initial break here because addStyleRun may never be called
82   mWordBreaker.next();
83   mCandidates.clear();
84   Candidate cand = {0,   0, 0.0, 0.0, 0.0,
85                     0.0, 0, 0,   0,   HyphenationType::DONT_BREAK};
86   mCandidates.push_back(cand);
87 
88   // reset greedy breaker state
89   mBreaks.clear();
90   mWidths.clear();
91   mFlags.clear();
92   mLastBreak = 0;
93   mBestBreak = 0;
94   mBestScore = SCORE_INFTY;
95   mPreBreak = 0;
96   mLastHyphenation = HyphenEdit::NO_EDIT;
97   mFirstTabIndex = INT_MAX;
98   mSpaceCount = 0;
99 }
100 
setLineWidths(float firstWidth,int firstWidthLineCount,float restWidth)101 void LineBreaker::setLineWidths(float firstWidth,
102                                 int firstWidthLineCount,
103                                 float restWidth) {
104   mLineWidths.setWidths(firstWidth, firstWidthLineCount, restWidth);
105 }
106 
setIndents(const std::vector<float> & indents)107 void LineBreaker::setIndents(const std::vector<float>& indents) {
108   mLineWidths.setIndents(indents);
109 }
110 
111 // This function determines whether a character is a space that disappears at
112 // end of line. It is the Unicode set:
113 // [[:General_Category=Space_Separator:]-[:Line_Break=Glue:]], plus '\n'. Note:
114 // all such characters are in the BMP, so it's ok to use code units for this.
isLineEndSpace(uint16_t c)115 bool isLineEndSpace(uint16_t c) {
116   return c == '\n' || c == ' ' || c == 0x1680 ||
117          (0x2000 <= c && c <= 0x200A && c != 0x2007) || c == 0x205F ||
118          c == 0x3000;
119 }
120 
121 // Ordinarily, this method measures the text in the range given. However, when
122 // paint is nullptr, it assumes the widths have already been calculated and
123 // stored in the width buffer. This method finds the candidate word breaks
124 // (using the ICU break iterator) and sends them to addCandidate.
addStyleRun(MinikinPaint * paint,const std::shared_ptr<FontCollection> & typeface,FontStyle style,size_t start,size_t end,bool isRtl)125 float LineBreaker::addStyleRun(MinikinPaint* paint,
126                                const std::shared_ptr<FontCollection>& typeface,
127                                FontStyle style,
128                                size_t start,
129                                size_t end,
130                                bool isRtl) {
131   float width = 0.0f;
132 
133   float hyphenPenalty = 0.0;
134   if (paint != nullptr) {
135     width = Layout::measureText(mTextBuf.data(), start, end - start,
136                                 mTextBuf.size(), isRtl, style, *paint, typeface,
137                                 mCharWidths.data() + start);
138 
139     // a heuristic that seems to perform well
140     hyphenPenalty =
141         0.5 * paint->size * paint->scaleX * mLineWidths.getLineWidth(0);
142     if (mHyphenationFrequency == kHyphenationFrequency_Normal) {
143       hyphenPenalty *=
144           4.0;  // TODO: Replace with a better value after some testing
145     }
146 
147     if (mJustified) {
148       // Make hyphenation more aggressive for fully justified text (so that
149       // "normal" in justified mode is the same as "full" in ragged-right).
150       hyphenPenalty *= 0.25;
151     } else {
152       // Line penalty is zero for justified text.
153       mLinePenalty =
154           std::max(mLinePenalty, hyphenPenalty * LINE_PENALTY_MULTIPLIER);
155     }
156   }
157 
158   size_t current = (size_t)mWordBreaker.current();
159   size_t afterWord = start;
160   size_t lastBreak = start;
161   ParaWidth lastBreakWidth = mWidth;
162   ParaWidth postBreak = mWidth;
163   size_t postSpaceCount = mSpaceCount;
164   for (size_t i = start; i < end; i++) {
165     uint16_t c = mTextBuf[i];
166     // libtxt: Tab handling was removed here.
167     if (isWordSpace(c))
168       mSpaceCount += 1;
169     mWidth += mCharWidths[i];
170     if (!isLineEndSpace(c)) {
171       postBreak = mWidth;
172       postSpaceCount = mSpaceCount;
173       afterWord = i + 1;
174     }
175     if (i + 1 == current) {
176       size_t wordStart = mWordBreaker.wordStart();
177       size_t wordEnd = mWordBreaker.wordEnd();
178       if (paint != nullptr && mHyphenator != nullptr &&
179           mHyphenationFrequency != kHyphenationFrequency_None &&
180           wordStart >= start && wordEnd > wordStart &&
181           wordEnd - wordStart <= LONGEST_HYPHENATED_WORD) {
182         mHyphenator->hyphenate(&mHyphBuf, &mTextBuf[wordStart],
183                                wordEnd - wordStart, mLocale);
184 #if VERBOSE_DEBUG
185         std::string hyphenatedString;
186         for (size_t j = wordStart; j < wordEnd; j++) {
187           if (mHyphBuf[j - wordStart] ==
188               HyphenationType::BREAK_AND_INSERT_HYPHEN) {
189             hyphenatedString.push_back('-');
190           }
191           // Note: only works with ASCII, should do UTF-8 conversion here
192           hyphenatedString.push_back(buffer()[j]);
193         }
194         ALOGD("hyphenated string: %s", hyphenatedString.c_str());
195 #endif
196 
197         // measure hyphenated substrings
198         for (size_t j = wordStart; j < wordEnd; j++) {
199           HyphenationType hyph = mHyphBuf[j - wordStart];
200           if (hyph != HyphenationType::DONT_BREAK) {
201             paint->hyphenEdit = HyphenEdit::editForThisLine(hyph);
202             const float firstPartWidth = Layout::measureText(
203                 mTextBuf.data(), lastBreak, j - lastBreak, mTextBuf.size(),
204                 isRtl, style, *paint, typeface, nullptr);
205             ParaWidth hyphPostBreak = lastBreakWidth + firstPartWidth;
206 
207             paint->hyphenEdit = HyphenEdit::editForNextLine(hyph);
208             const float secondPartWidth = Layout::measureText(
209                 mTextBuf.data(), j, afterWord - j, mTextBuf.size(), isRtl,
210                 style, *paint, typeface, nullptr);
211             ParaWidth hyphPreBreak = postBreak - secondPartWidth;
212 
213             addWordBreak(j, hyphPreBreak, hyphPostBreak, postSpaceCount,
214                          postSpaceCount, hyphenPenalty, hyph);
215 
216             paint->hyphenEdit = HyphenEdit::NO_EDIT;
217           }
218         }
219       }
220 
221       // Skip break for zero-width characters inside replacement span
222       if (paint != nullptr || current == end || mCharWidths[current] > 0) {
223         float penalty = hyphenPenalty * mWordBreaker.breakBadness();
224         addWordBreak(current, mWidth, postBreak, mSpaceCount, postSpaceCount,
225                      penalty, HyphenationType::DONT_BREAK);
226       }
227       lastBreak = current;
228       lastBreakWidth = mWidth;
229       current = (size_t)mWordBreaker.next();
230     }
231   }
232 
233   return width;
234 }
235 
236 // add a word break (possibly for a hyphenated fragment), and add desperate
237 // breaks if needed (ie when word exceeds current line width)
addWordBreak(size_t offset,ParaWidth preBreak,ParaWidth postBreak,size_t preSpaceCount,size_t postSpaceCount,float penalty,HyphenationType hyph)238 void LineBreaker::addWordBreak(size_t offset,
239                                ParaWidth preBreak,
240                                ParaWidth postBreak,
241                                size_t preSpaceCount,
242                                size_t postSpaceCount,
243                                float penalty,
244                                HyphenationType hyph) {
245   Candidate cand;
246   ParaWidth width = mCandidates.back().preBreak;
247   // libtxt: add a fudge factor to this comparison.  The currentLineWidth passed
248   // by the framework is based on maxIntrinsicWidth/Layout::measureText
249   // calculations that may not precisely match the postBreak width.
250   if (postBreak - width > currentLineWidth() + LIBTXT_WIDTH_ADJUST) {
251     // Add desperate breaks.
252     // Note: these breaks are based on the shaping of the (non-broken) original
253     // text; they are imprecise especially in the presence of kerning,
254     // ligatures, and Arabic shaping.
255     size_t i = mCandidates.back().offset;
256     width += mCharWidths[i++];
257     for (; i < offset; i++) {
258       float w = mCharWidths[i];
259       if (w > 0) {
260         cand.offset = i;
261         cand.preBreak = width;
262         cand.postBreak = width;
263         // postSpaceCount doesn't include trailing spaces
264         cand.preSpaceCount = postSpaceCount;
265         cand.postSpaceCount = postSpaceCount;
266         cand.penalty = SCORE_DESPERATE;
267         cand.hyphenType = HyphenationType::BREAK_AND_DONT_INSERT_HYPHEN;
268 #if VERBOSE_DEBUG
269         ALOGD("desperate cand: %zd %g:%g", mCandidates.size(), cand.postBreak,
270               cand.preBreak);
271 #endif
272         addCandidate(cand);
273         width += w;
274       }
275     }
276   }
277 
278   cand.offset = offset;
279   cand.preBreak = preBreak;
280   cand.postBreak = postBreak;
281   cand.penalty = penalty;
282   cand.preSpaceCount = preSpaceCount;
283   cand.postSpaceCount = postSpaceCount;
284   cand.hyphenType = hyph;
285 #if VERBOSE_DEBUG
286   ALOGD("cand: %zd %g:%g", mCandidates.size(), cand.postBreak, cand.preBreak);
287 #endif
288   addCandidate(cand);
289 }
290 
291 // Helper method for addCandidate()
pushGreedyBreak()292 void LineBreaker::pushGreedyBreak() {
293   const Candidate& bestCandidate = mCandidates[mBestBreak];
294   pushBreak(
295       bestCandidate.offset, bestCandidate.postBreak - mPreBreak,
296       mLastHyphenation | HyphenEdit::editForThisLine(bestCandidate.hyphenType));
297   mBestScore = SCORE_INFTY;
298 #if VERBOSE_DEBUG
299   ALOGD("break: %d %g", mBreaks.back(), mWidths.back());
300 #endif
301   mLastBreak = mBestBreak;
302   mPreBreak = bestCandidate.preBreak;
303   mLastHyphenation = HyphenEdit::editForNextLine(bestCandidate.hyphenType);
304 }
305 
306 // TODO performance: could avoid populating mCandidates if greedy only
addCandidate(Candidate cand)307 void LineBreaker::addCandidate(Candidate cand) {
308   const size_t candIndex = mCandidates.size();
309   mCandidates.push_back(cand);
310 
311   // mLastBreak is the index of the last line break we decided to do in
312   // mCandidates, and mPreBreak is its preBreak value. mBestBreak is the index
313   // of the best line breaking candidate we have found since then, and
314   // mBestScore is its penalty.
315   if (cand.postBreak - mPreBreak > currentLineWidth() + LIBTXT_WIDTH_ADJUST) {
316     // This break would create an overfull line, pick the best break and break
317     // there (greedy)
318     if (mBestBreak == mLastBreak) {
319       // No good break has been found since last break. Break here.
320       mBestBreak = candIndex;
321     }
322     pushGreedyBreak();
323   }
324 
325   while (mLastBreak != candIndex &&
326          cand.postBreak - mPreBreak >
327              currentLineWidth() + LIBTXT_WIDTH_ADJUST) {
328     // We should rarely come here. But if we are here, we have broken the line,
329     // but the remaining part still doesn't fit. We now need to break at the
330     // second best place after the last break, but we have not kept that
331     // information, so we need to go back and find it.
332     //
333     // In some really rare cases, postBreak - preBreak of a candidate itself may
334     // be over the current line width. We protect ourselves against an infinite
335     // loop in that case by checking that we have not broken the line at this
336     // candidate already.
337     for (size_t i = mLastBreak + 1; i < candIndex; i++) {
338       const float penalty = mCandidates[i].penalty;
339       if (penalty <= mBestScore) {
340         mBestBreak = i;
341         mBestScore = penalty;
342       }
343     }
344     if (mBestBreak == mLastBreak) {
345       // We didn't find anything good. Break here.
346       mBestBreak = candIndex;
347     }
348     pushGreedyBreak();
349   }
350 
351   if (cand.penalty <= mBestScore) {
352     mBestBreak = candIndex;
353     mBestScore = cand.penalty;
354   }
355 }
356 
pushBreak(int offset,float width,uint8_t hyphenEdit)357 void LineBreaker::pushBreak(int offset, float width, uint8_t hyphenEdit) {
358   mBreaks.push_back(offset);
359   mWidths.push_back(width);
360   int flags = (mFirstTabIndex < mBreaks.back()) << kTab_Shift;
361   flags |= hyphenEdit;
362   mFlags.push_back(flags);
363   mFirstTabIndex = INT_MAX;
364 }
365 
366 // libtxt: Add ability to set custom char widths. This allows manual definition
367 // of the widths of arbitrary glyphs. To linebreak properly, call addStyleRun
368 // with nullptr as the paint property, which will lead it to assume the width
369 // has already been calculated. Used for properly breaking inline widgets.
setCustomCharWidth(size_t offset,float width)370 void LineBreaker::setCustomCharWidth(size_t offset, float width) {
371   mCharWidths[offset] = (width);
372 }
373 
addReplacement(size_t start,size_t end,float width)374 void LineBreaker::addReplacement(size_t start, size_t end, float width) {
375   mCharWidths[start] = width;
376   std::fill(&mCharWidths[start + 1], &mCharWidths[end], 0.0f);
377   addStyleRun(nullptr, nullptr, FontStyle(), start, end, false);
378 }
379 
380 // Get the width of a space. May return 0 if there are no spaces.
381 // Note: if there are multiple different widths for spaces (for example, because
382 // of mixing of fonts), it's only guaranteed to pick one.
getSpaceWidth() const383 float LineBreaker::getSpaceWidth() const {
384   for (size_t i = 0; i < mTextBuf.size(); i++) {
385     if (isWordSpace(mTextBuf[i])) {
386       return mCharWidths[i];
387     }
388   }
389   return 0.0f;
390 }
391 
currentLineWidth() const392 float LineBreaker::currentLineWidth() const {
393   return mLineWidths.getLineWidth(mBreaks.size());
394 }
395 
computeBreaksGreedy()396 void LineBreaker::computeBreaksGreedy() {
397   // All breaks but the last have been added in addCandidate already.
398   size_t nCand = mCandidates.size();
399   if (nCand > 0 && (nCand == 1 || mLastBreak != nCand - 1)) {
400     pushBreak(mCandidates[nCand - 1].offset,
401               mCandidates[nCand - 1].postBreak - mPreBreak, mLastHyphenation);
402     // don't need to update mBestScore, because we're done
403 #if VERBOSE_DEBUG
404     ALOGD("final break: %d %g", mBreaks.back(), mWidths.back());
405 #endif
406   }
407 }
408 
409 // Follow "prev" links in mCandidates array, and copy to result arrays.
finishBreaksOptimal()410 void LineBreaker::finishBreaksOptimal() {
411   // clear existing greedy break result
412   mBreaks.clear();
413   mWidths.clear();
414   mFlags.clear();
415   size_t nCand = mCandidates.size();
416   size_t prev;
417   for (size_t i = nCand - 1; i > 0; i = prev) {
418     prev = mCandidates[i].prev;
419     mBreaks.push_back(mCandidates[i].offset);
420     mWidths.push_back(mCandidates[i].postBreak - mCandidates[prev].preBreak);
421     int flags = HyphenEdit::editForThisLine(mCandidates[i].hyphenType);
422     if (prev > 0) {
423       flags |= HyphenEdit::editForNextLine(mCandidates[prev].hyphenType);
424     }
425     mFlags.push_back(flags);
426   }
427   std::reverse(mBreaks.begin(), mBreaks.end());
428   std::reverse(mWidths.begin(), mWidths.end());
429   std::reverse(mFlags.begin(), mFlags.end());
430 }
431 
computeBreaksOptimal(bool isRectangle)432 void LineBreaker::computeBreaksOptimal(bool isRectangle) {
433   size_t active = 0;
434   size_t nCand = mCandidates.size();
435   float width = mLineWidths.getLineWidth(0);
436   float shortLineFactor = mJustified ? 0.75f : 0.5f;
437   float maxShrink = mJustified ? SHRINKABILITY * getSpaceWidth() : 0.0f;
438 
439   // "i" iterates through candidates for the end of the line.
440   for (size_t i = 1; i < nCand; i++) {
441     bool atEnd = i == nCand - 1;
442     float best = SCORE_INFTY;
443     size_t bestPrev = 0;
444     size_t lineNumberLast = 0;
445 
446     if (!isRectangle) {
447       size_t lineNumberLast = mCandidates[active].lineNumber;
448       width = mLineWidths.getLineWidth(lineNumberLast);
449     }
450     ParaWidth leftEdge = mCandidates[i].postBreak - width;
451     float bestHope = 0;
452 
453     // "j" iterates through candidates for the beginning of the line.
454     for (size_t j = active; j < i; j++) {
455       if (!isRectangle) {
456         size_t lineNumber = mCandidates[j].lineNumber;
457         if (lineNumber != lineNumberLast) {
458           float widthNew = mLineWidths.getLineWidth(lineNumber);
459           if (widthNew != width) {
460             leftEdge = mCandidates[i].postBreak - width;
461             bestHope = 0;
462             width = widthNew;
463           }
464           lineNumberLast = lineNumber;
465         }
466       }
467       float jScore = mCandidates[j].score;
468       if (jScore + bestHope >= best)
469         continue;
470       float delta = mCandidates[j].preBreak - leftEdge;
471 
472       // compute width score for line
473 
474       // Note: the "bestHope" optimization makes the assumption that, when delta
475       // is non-negative, widthScore will increase monotonically as successive
476       // candidate breaks are considered.
477       float widthScore = 0.0f;
478       float additionalPenalty = 0.0f;
479       if ((atEnd || !mJustified) && delta < 0) {
480         widthScore = SCORE_OVERFULL;
481       } else if (atEnd && mStrategy != kBreakStrategy_Balanced) {
482         // increase penalty for hyphen on last line
483         additionalPenalty =
484             LAST_LINE_PENALTY_MULTIPLIER * mCandidates[j].penalty;
485         // Penalize very short (< 1 - shortLineFactor of total width) lines.
486         float underfill = delta - shortLineFactor * width;
487         widthScore = underfill > 0 ? underfill * underfill : 0;
488       } else {
489         widthScore = delta * delta;
490         if (delta < 0) {
491           if (-delta < maxShrink * (mCandidates[i].postSpaceCount -
492                                     mCandidates[j].preSpaceCount)) {
493             widthScore *= SHRINK_PENALTY_MULTIPLIER;
494           } else {
495             widthScore = SCORE_OVERFULL;
496           }
497         }
498       }
499 
500       if (delta < 0) {
501         active = j + 1;
502       } else {
503         bestHope = widthScore;
504       }
505 
506       float score = jScore + widthScore + additionalPenalty;
507       if (score <= best) {
508         best = score;
509         bestPrev = j;
510       }
511     }
512     mCandidates[i].score = best + mCandidates[i].penalty + mLinePenalty;
513     mCandidates[i].prev = bestPrev;
514     mCandidates[i].lineNumber = mCandidates[bestPrev].lineNumber + 1;
515 #if VERBOSE_DEBUG
516     ALOGD("break %zd: score=%g, prev=%zd", i, mCandidates[i].score,
517           mCandidates[i].prev);
518 #endif
519   }
520   finishBreaksOptimal();
521 }
522 
computeBreaks()523 size_t LineBreaker::computeBreaks() {
524   if (mStrategy == kBreakStrategy_Greedy) {
525     computeBreaksGreedy();
526   } else {
527     computeBreaksOptimal(mLineWidths.isConstant());
528   }
529   return mBreaks.size();
530 }
531 
finish()532 void LineBreaker::finish() {
533   mWordBreaker.finish();
534   mWidth = 0;
535   mLineWidths.clear();
536   mCandidates.clear();
537   mBreaks.clear();
538   mWidths.clear();
539   mFlags.clear();
540   if (mTextBuf.size() > MAX_TEXT_BUF_RETAIN) {
541     mTextBuf.clear();
542     mTextBuf.shrink_to_fit();
543     mCharWidths.clear();
544     mCharWidths.shrink_to_fit();
545     mHyphBuf.clear();
546     mHyphBuf.shrink_to_fit();
547     mCandidates.shrink_to_fit();
548     mBreaks.shrink_to_fit();
549     mWidths.shrink_to_fit();
550     mFlags.shrink_to_fit();
551   }
552   mStrategy = kBreakStrategy_Greedy;
553   mHyphenationFrequency = kHyphenationFrequency_Normal;
554   mLinePenalty = 0.0f;
555   mJustified = false;
556 }
557 
558 }  // namespace minikin
559