LineBreaker.cpp revision 4cf0e3763c5087db642757933de1fcae3074c76c
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 <limits>
22
23#include <log/log.h>
24
25#include "LayoutUtils.h"
26#include "WordBreaker.h"
27#include <minikin/Layout.h>
28#include <minikin/LineBreaker.h>
29
30using std::vector;
31
32namespace minikin {
33
34constexpr uint16_t CHAR_TAB = 0x0009;
35constexpr uint16_t CHAR_NBSP = 0x00A0;
36
37// Large scores in a hierarchy; we prefer desperate breaks to an overfull line. All these
38// constants are larger than any reasonable actual width score.
39const float SCORE_INFTY = std::numeric_limits<float>::max();
40const float SCORE_OVERFULL = 1e12f;
41const float SCORE_DESPERATE = 1e10f;
42
43// Multiplier for hyphen penalty on last line.
44const float LAST_LINE_PENALTY_MULTIPLIER = 4.0f;
45// Penalty assigned to each line break (to try to minimize number of lines)
46// TODO: when we implement full justification (so spaces can shrink and stretch), this is
47// probably not the most appropriate method.
48const float LINE_PENALTY_MULTIPLIER = 2.0f;
49
50// Penalty assigned to shrinking the whitepsace.
51const float SHRINK_PENALTY_MULTIPLIER = 4.0f;
52
53// Very long words trigger O(n^2) behavior in hyphenation, so we disable hyphenation for
54// unreasonably long words. This is somewhat of a heuristic because extremely long words
55// are possible in some languages. This does mean that very long real words can get
56// broken by desperate breaks, with no hyphens.
57const size_t LONGEST_HYPHENATED_WORD = 45;
58
59// When the text buffer is within this limit, capacity of vectors is retained at finish(),
60// to avoid allocation.
61const size_t MAX_TEXT_BUF_RETAIN = 32678;
62
63// Maximum amount that spaces can shrink, in justified text.
64const float SHRINKABILITY = 1.0 / 3.0;
65
66LineBreaker::LineBreaker() : mWordBreaker(std::make_unique<WordBreaker>()) { }
67
68LineBreaker::LineBreaker(std::unique_ptr<WordBreaker>&& breaker)
69        : mWordBreaker(std::move(breaker)) { }
70
71LineBreaker::~LineBreaker() {}
72
73void LineBreaker::setLocales(const char* locales, const std::vector<Hyphenator*>& hyphenators,
74        size_t restartFrom) {
75    bool goodLocaleFound = false;
76    const ssize_t numLocales = hyphenators.size();
77    // For now, we ignore all locales except the first valid one.
78    // TODO: Support selecting the locale based on the script of the text.
79    const char* localeStart = locales;
80    icu::Locale locale;
81    for (ssize_t i = 0; i < numLocales - 1; i++) { // Loop over all locales, except the last one.
82        const char* localeEnd = strchr(localeStart, ',');
83        const size_t localeNameLength = localeEnd - localeStart;
84        char localeName[localeNameLength + 1];
85        strncpy(localeName, localeStart, localeNameLength);
86        localeName[localeNameLength] = '\0';
87        locale = icu::Locale::createFromName(localeName);
88        goodLocaleFound = !locale.isBogus();
89        if (goodLocaleFound) {
90            mHyphenator = hyphenators[i];
91            break;
92        } else {
93            localeStart = localeEnd + 1;
94        }
95    }
96    if (!goodLocaleFound) { // Try the last locale.
97        locale = icu::Locale::createFromName(localeStart);
98        if (locale.isBogus()) {
99            // No good locale.
100            locale = icu::Locale::getRoot();
101            mHyphenator = nullptr;
102        } else {
103            mHyphenator = numLocales == 0 ? nullptr : hyphenators[numLocales - 1];
104        }
105    }
106    mWordBreaker->followingWithLocale(locale, restartFrom);
107}
108
109void LineBreaker::setText() {
110    mWordBreaker->setText(mTextBuf.data(), mTextBuf.size());
111
112    // handle initial break here because addStyleRun may never be called
113    mCandidates.clear();
114    Candidate cand = {
115            0, 0, 0.0, 0.0, 0.0, 0.0, 0, 0, {0.0, 0.0, 0.0}, HyphenationType::DONT_BREAK};
116    mCandidates.push_back(cand);
117
118    // reset greedy breaker state
119    mBreaks.clear();
120    mWidths.clear();
121    mAscents.clear();
122    mDescents.clear();
123    mFlags.clear();
124    mLastBreak = 0;
125    mBestBreak = 0;
126    mBestScore = SCORE_INFTY;
127    mPreBreak = 0;
128    mLastHyphenation = HyphenEdit::NO_EDIT;
129    mFirstTabIndex = INT_MAX;
130    mSpaceCount = 0;
131}
132
133// This function determines whether a character is a space that disappears at end of line.
134// It is the Unicode set: [[:General_Category=Space_Separator:]-[:Line_Break=Glue:]],
135// plus '\n'.
136// Note: all such characters are in the BMP, so it's ok to use code units for this.
137static bool isLineEndSpace(uint16_t c) {
138    return c == '\n' || c == ' ' || c == 0x1680 || (0x2000 <= c && c <= 0x200A && c != 0x2007) ||
139            c == 0x205F || c == 0x3000;
140}
141
142// Hyphenates a string potentially containing non-breaking spaces.
143std::vector<HyphenationType> LineBreaker::hyphenate(const uint16_t* str, size_t len) {
144    std::vector<HyphenationType> out;
145    out.reserve(len);
146
147    // A word here is any consecutive string of non-NBSP characters.
148    bool inWord = false;
149    size_t wordStart = 0; // The initial value will never be accessed, but just in case.
150    for (size_t i = 0; i <= len; i++) {
151        if (i == len || str[i] == CHAR_NBSP) {
152            if (inWord) {
153                // A word just ended. Hyphenate it.
154                const size_t wordLen = i - wordStart;
155                if (wordLen <= LONGEST_HYPHENATED_WORD) {
156                    if (wordStart == 0) {
157                        // The string starts with a word. Use out directly.
158                        mHyphenator->hyphenate(&out, str, wordLen);
159                    } else {
160                        std::vector<HyphenationType> wordVec;
161                        mHyphenator->hyphenate(&wordVec, str + wordStart, wordLen);
162                        out.insert(out.end(), wordVec.cbegin(), wordVec.cend());
163                    }
164                } else { // Word is too long. Inefficient to hyphenate.
165                    out.insert(out.end(), wordLen, HyphenationType::DONT_BREAK);
166                }
167                inWord = false;
168            }
169            if (i < len) {
170                // Insert one DONT_BREAK for the NBSP.
171                out.push_back(HyphenationType::DONT_BREAK);
172            }
173        } else if (!inWord) {
174            inWord = true;
175            wordStart = i;
176        }
177    }
178    return out;
179}
180
181// Compute the total overhang of text based on per-cluster advances and overhangs.
182// The two input vectors are expected to be of the same size.
183/* static */ LayoutOverhang LineBreaker::computeOverhang(float totalAdvance,
184        const std::vector<float>& advances, const std::vector<LayoutOverhang>& overhangs,
185        bool isRtl) {
186    ParaWidth left = 0.0;
187    ParaWidth right = 0.0;
188    ParaWidth seenAdvance = 0.0;
189    const size_t len = advances.size();
190    if (isRtl) {
191        for (size_t i = 0; i < len; i++) {
192            right = std::max(right, overhangs[i].right - seenAdvance);
193            seenAdvance += advances[i];
194            left = std::max(left, overhangs[i].left - (totalAdvance - seenAdvance));
195        }
196    } else {
197        for (size_t i = 0; i < len; i++) {
198            left = std::max(left, overhangs[i].left - seenAdvance);
199            seenAdvance += advances[i];
200            right = std::max(right, overhangs[i].right - (totalAdvance - seenAdvance));
201        }
202    }
203    return {static_cast<float>(left), static_cast<float>(right)};
204}
205
206// This adds all the hyphenation candidates for a given word by first finding all the hyphenation
207// points and then calling addWordBreak for each.
208//
209// paint and typeface will be used for measuring the text and paint must not be null.
210// runStart is the beginning of the whole run, and is used for making sure we don't hyphenated words
211//     that go outside the run.
212// afterWord is the index of last code unit seen that's not a line-ending space, plus one. In other
213//     words, the index of the first code unit after a word.
214// lastBreak is the index of the previous break point and lastBreakWidth is the width seen until
215//     that point.
216// extent is a pointer to the variable that keeps track of the maximum vertical extents seen since
217//     the last time addWordBreak() was called. It will be reset to 0 after every call.
218// bidiFlags keep the bidi flags to determine the direction of text for layout and other
219//     calculations. It may only be kBidi_Force_RTL or kBidi_Force_LTR.
220//
221// The following parameters are needed to be passed to addWordBreak:
222// postBreak is the width that would be seen if we decide to break at the end of the word (so it
223//     doesn't count any line-ending space after the word).
224// postSpaceCount is the number of spaces that would be seen if we decide to break at the end of the
225//     word (and like postBreak it doesn't count any line-ending space after the word).
226// hyphenPenalty is the amount of penalty for hyphenation.
227void LineBreaker::addHyphenationCandidates(MinikinPaint* paint,
228        const std::shared_ptr<FontCollection>& typeface, FontStyle style, size_t runStart,
229        size_t afterWord, size_t lastBreak, ParaWidth lastBreakWidth, ParaWidth postBreak,
230        size_t postSpaceCount, MinikinExtent* extent, float hyphenPenalty, int bidiFlags) {
231    const bool isRtl = (bidiFlags == kBidi_Force_RTL);
232    const size_t wordStart = mWordBreaker->wordStart();
233    const size_t wordEnd = mWordBreaker->wordEnd();
234    if (wordStart < runStart || wordEnd <= wordStart) {
235        return;
236    }
237    std::vector<HyphenationType> hyphenResult =
238            hyphenate(&mTextBuf[wordStart], wordEnd - wordStart);
239
240    std::vector<float> advances;
241    std::vector<LayoutOverhang> overhangs;
242    const size_t bufferSize = std::max(wordEnd - 1 - lastBreak, afterWord - wordStart);
243    advances.reserve(bufferSize);
244    overhangs.reserve(bufferSize);
245    // measure hyphenated substrings
246    for (size_t j = wordStart; j < wordEnd; j++) {
247        HyphenationType hyph = hyphenResult[j - wordStart];
248        if (hyph == HyphenationType::DONT_BREAK) {
249            continue;
250        }
251
252        paint->hyphenEdit = HyphenEdit::editForThisLine(hyph);
253        const size_t firstPartLen = j - lastBreak;
254        advances.resize(firstPartLen);
255        overhangs.resize(firstPartLen);
256        const float firstPartWidth = Layout::measureText(mTextBuf.data(), lastBreak, firstPartLen,
257                mTextBuf.size(), bidiFlags, style, *paint, typeface, advances.data(),
258                nullptr /* extent */, overhangs.data());
259        const LayoutOverhang overhang = computeOverhang(firstPartWidth, advances, overhangs, isRtl);
260        const ParaWidth hyphPostBreak = lastBreakWidth + firstPartWidth
261                + (isRtl ? overhang.left : overhang.right);
262
263        paint->hyphenEdit = HyphenEdit::editForNextLine(hyph); // XXX may need to be NO_EDIT.
264        const size_t secondPartLen = afterWord - j;
265        const float secondPartWidth = Layout::measureText(mTextBuf.data(), j, secondPartLen,
266                mTextBuf.size(), bidiFlags, style, *paint, typeface, nullptr /* advances */,
267                nullptr /* extent */, nullptr /* overhangs */);
268        const ParaWidth hyphPreBreak = postBreak - secondPartWidth;
269
270        addWordBreak(j, hyphPreBreak, hyphPostBreak, postSpaceCount,
271                postSpaceCount, *extent, hyphenPenalty, hyph);
272        extent->reset();
273
274        paint->hyphenEdit = HyphenEdit::NO_EDIT;
275    }
276}
277
278// Ordinarily, this method measures the text in the range given. However, when paint is nullptr, it
279// assumes the character widths, extents, and overhangs have already been calculated and stored in
280// the mCharWidths, mCharExtents, and mCharOverhangs buffers.
281//
282// This method finds the candidate word breaks (using the ICU break iterator) and sends them
283// to addCandidate.
284float LineBreaker::addStyleRun(MinikinPaint* paint, const std::shared_ptr<FontCollection>& typeface,
285        FontStyle style, size_t start, size_t end, bool isRtl, const char* langTags,
286        const std::vector<Hyphenator*>& hyphenators) {
287    float width = 0.0f;
288    const int bidiFlags = isRtl ? kBidi_Force_RTL : kBidi_Force_LTR;
289
290    float hyphenPenalty = 0.0;
291    if (paint != nullptr) {
292        width = Layout::measureText(mTextBuf.data(), start, end - start, mTextBuf.size(), bidiFlags,
293                style, *paint, typeface, mCharWidths.data() + start, mCharExtents.data() + start,
294                mCharOverhangs.data() + start);
295
296        // a heuristic that seems to perform well
297        hyphenPenalty = 0.5 * paint->size * paint->scaleX * mLineWidthDelegate->getLineWidth(0);
298        if (mHyphenationFrequency == kHyphenationFrequency_Normal) {
299            hyphenPenalty *= 4.0; // TODO: Replace with a better value after some testing
300        }
301
302        if (mJustified) {
303            // Make hyphenation more aggressive for fully justified text (so that "normal" in
304            // justified mode is the same as "full" in ragged-right).
305            hyphenPenalty *= 0.25;
306        } else {
307            // Line penalty is zero for justified text.
308            mLinePenalty = std::max(mLinePenalty, hyphenPenalty * LINE_PENALTY_MULTIPLIER);
309        }
310    }
311
312    // Caller passes nullptr for langTag if language is not changed.
313    if (langTags != nullptr) {
314        setLocales(langTags, hyphenators, start);
315    }
316    size_t current = (size_t) mWordBreaker->current();
317    // This will keep the index of last code unit seen that's not a line-ending space, plus one.
318    // In other words, the index of the first code unit after a word.
319    size_t afterWord = start;
320
321    size_t lastBreak = start; // The index of the previous break point.
322    ParaWidth lastBreakWidth = mWidth; // The width of the text as of the previous break point.
323    ParaWidth postBreak = mWidth; // The width of text seen if we decide to break here
324    size_t postSpaceCount = mSpaceCount;
325    MinikinExtent extent = {0.0, 0.0, 0.0};
326    for (size_t i = start; i < end; i++) {
327        const uint16_t c = mTextBuf[i];
328        if (c == CHAR_TAB) {
329            mWidth = mPreBreak + mTabStops.nextTab(mWidth - mPreBreak);
330            if (mFirstTabIndex == INT_MAX) {
331                mFirstTabIndex = (int)i;
332            }
333            // fall back to greedy; other modes don't know how to deal with tabs
334            mStrategy = kBreakStrategy_Greedy;
335        } else {
336            if (isWordSpace(c)) {
337                mSpaceCount += 1;
338            }
339            mWidth += mCharWidths[i];
340            extent.extendBy(mCharExtents[i]);
341            if (isLineEndSpace(c)) {
342                // If we break a line on a line-ending space, that space goes away. So postBreak
343                // and postSpaceCount, which keep the width and number of spaces if we decide to
344                // break at this point, don't need to get adjusted.
345            } else {
346                postBreak = mWidth;
347                postSpaceCount = mSpaceCount;
348                afterWord = i + 1;
349            }
350        }
351        if (i + 1 == current) { // We are at the end of a word.
352            if (paint != nullptr && mHyphenator != nullptr &&
353                    mHyphenationFrequency != kHyphenationFrequency_None) {
354                addHyphenationCandidates(paint, typeface, style, start, afterWord, lastBreak,
355                        lastBreakWidth, postBreak, postSpaceCount, &extent, hyphenPenalty,
356                        bidiFlags);
357            }
358
359            // Skip break for zero-width characters inside replacement span
360            if (paint != nullptr || current == end || mCharWidths[current] > 0) {
361                const float penalty = hyphenPenalty * mWordBreaker->breakBadness();
362                addWordBreak(current, mWidth, postBreak, mSpaceCount, postSpaceCount, extent,
363                        penalty, HyphenationType::DONT_BREAK);
364                extent.reset();
365            }
366            lastBreak = current;
367            lastBreakWidth = mWidth;
368            current = (size_t)mWordBreaker->next();
369        }
370    }
371
372    return width;
373}
374
375// add a word break (possibly for a hyphenated fragment), and add desperate breaks if
376// needed (ie when word exceeds current line width)
377void LineBreaker::addWordBreak(size_t offset, ParaWidth preBreak, ParaWidth postBreak,
378        size_t preSpaceCount, size_t postSpaceCount, MinikinExtent extent,
379        float penalty, HyphenationType hyph) {
380    Candidate cand;
381    ParaWidth width = mCandidates.back().preBreak;
382    if (postBreak - width > currentLineWidth()) {
383        // Add desperate breaks.
384        // Note: these breaks are based on the shaping of the (non-broken) original text; they
385        // are imprecise especially in the presence of kerning, ligatures, and Arabic shaping.
386        size_t i = mCandidates.back().offset;
387        width += mCharWidths[i++];
388        for (; i < offset; i++) {
389            float w = mCharWidths[i];
390            if (w > 0) {
391                cand.offset = i;
392                cand.preBreak = width;
393                cand.postBreak = width;
394                // postSpaceCount doesn't include trailing spaces
395                cand.preSpaceCount = postSpaceCount;
396                cand.postSpaceCount = postSpaceCount;
397                cand.extent = mCharExtents[i];
398                cand.penalty = SCORE_DESPERATE;
399                cand.hyphenType = HyphenationType::BREAK_AND_DONT_INSERT_HYPHEN;
400#if VERBOSE_DEBUG
401                ALOGD("desperate cand: %zd %g:%g",
402                        mCandidates.size(), cand.postBreak, cand.preBreak);
403#endif
404                addCandidate(cand);
405                width += w;
406            }
407        }
408    }
409
410    cand.offset = offset;
411    cand.preBreak = preBreak;
412    cand.postBreak = postBreak;
413    cand.penalty = penalty;
414    cand.preSpaceCount = preSpaceCount;
415    cand.postSpaceCount = postSpaceCount;
416    cand.extent = extent;
417    cand.hyphenType = hyph;
418#if VERBOSE_DEBUG
419    ALOGD("cand: %zd %g:%g", mCandidates.size(), cand.postBreak, cand.preBreak);
420#endif
421    addCandidate(cand);
422}
423
424// Find the needed extent between the start and end ranges. start and end are inclusive.
425MinikinExtent LineBreaker::computeMaxExtent(size_t start, size_t end) const {
426    MinikinExtent res = mCandidates[end].extent;
427    for (size_t j = start; j < end; j++) {
428        res.extendBy(mCandidates[j].extent);
429    }
430    return res;
431}
432
433// Helper method for addCandidate()
434void LineBreaker::pushGreedyBreak() {
435    const Candidate& bestCandidate = mCandidates[mBestBreak];
436    pushBreak(bestCandidate.offset, bestCandidate.postBreak - mPreBreak,
437            computeMaxExtent(mLastBreak + 1, mBestBreak),
438            mLastHyphenation | HyphenEdit::editForThisLine(bestCandidate.hyphenType));
439    mBestScore = SCORE_INFTY;
440#if VERBOSE_DEBUG
441    ALOGD("break: %d %g", mBreaks.back(), mWidths.back());
442#endif
443    mLastBreak = mBestBreak;
444    mPreBreak = bestCandidate.preBreak;
445    mLastHyphenation = HyphenEdit::editForNextLine(bestCandidate.hyphenType);
446}
447
448// TODO performance: could avoid populating mCandidates if greedy only
449void LineBreaker::addCandidate(Candidate cand) {
450    const size_t candIndex = mCandidates.size();
451    mCandidates.push_back(cand);
452
453    // mLastBreak is the index of the last line break we decided to do in mCandidates,
454    // and mPreBreak is its preBreak value. mBestBreak is the index of the best line breaking
455    // candidate we have found since then, and mBestScore is its penalty.
456    if (cand.postBreak - mPreBreak > currentLineWidth()) {
457        // This break would create an overfull line, pick the best break and break there (greedy)
458        if (mBestBreak == mLastBreak) {
459            // No good break has been found since last break. Break here.
460            mBestBreak = candIndex;
461        }
462        pushGreedyBreak();
463    }
464
465    while (mLastBreak != candIndex && cand.postBreak - mPreBreak > currentLineWidth()) {
466        // We should rarely come here. But if we are here, we have broken the line, but the
467        // remaining part still doesn't fit. We now need to break at the second best place after the
468        // last break, but we have not kept that information, so we need to go back and find it.
469        //
470        // In some really rare cases, postBreak - preBreak of a candidate itself may be over the
471        // current line width. We protect ourselves against an infinite loop in that case by
472        // checking that we have not broken the line at this candidate already.
473        for (size_t i = mLastBreak + 1; i < candIndex; i++) {
474            const float penalty = mCandidates[i].penalty;
475            if (penalty <= mBestScore) {
476                mBestBreak = i;
477                mBestScore = penalty;
478            }
479        }
480        if (mBestBreak == mLastBreak) {
481            // We didn't find anything good. Break here.
482            mBestBreak = candIndex;
483        }
484        pushGreedyBreak();
485    }
486
487    if (cand.penalty <= mBestScore) {
488        mBestBreak = candIndex;
489        mBestScore = cand.penalty;
490    }
491}
492
493void LineBreaker::pushBreak(int offset, float width, MinikinExtent extent, uint8_t hyphenEdit) {
494    mBreaks.push_back(offset);
495    mWidths.push_back(width);
496    mAscents.push_back(extent.ascent);
497    mDescents.push_back(extent.descent);
498    int flags = (mFirstTabIndex < mBreaks.back()) << kTab_Shift;
499    flags |= hyphenEdit;
500    mFlags.push_back(flags);
501    mFirstTabIndex = INT_MAX;
502}
503
504void LineBreaker::addReplacement(size_t start, size_t end, float width, const char* langTags,
505        const std::vector<Hyphenator*>& hyphenators) {
506    mCharWidths[start] = width;
507    std::fill(&mCharWidths[start + 1], &mCharWidths[end], 0.0f);
508    // TODO: Get the extents information from the caller.
509    std::fill(&mCharExtents[start], &mCharExtents[end], (MinikinExtent) {0.0f, 0.0f, 0.0f});
510    addStyleRun(nullptr, nullptr, FontStyle(), start, end, false, langTags, hyphenators);
511}
512
513// Get the width of a space. May return 0 if there are no spaces.
514// Note: if there are multiple different widths for spaces (for example, because of mixing of
515// fonts), it's only guaranteed to pick one.
516float LineBreaker::getSpaceWidth() const {
517    for (size_t i = 0; i < mTextBuf.size(); i++) {
518        if (isWordSpace(mTextBuf[i])) {
519            return mCharWidths[i];
520        }
521    }
522    return 0.0f;
523}
524
525float LineBreaker::currentLineWidth() const {
526    return mLineWidthDelegate->getLineWidth(mBreaks.size());
527}
528
529void LineBreaker::computeBreaksGreedy() {
530    // All breaks but the last have been added in addCandidate already.
531    size_t nCand = mCandidates.size();
532    if (nCand == 1 || mLastBreak != nCand - 1) {
533        pushBreak(mCandidates[nCand - 1].offset, mCandidates[nCand - 1].postBreak - mPreBreak,
534                computeMaxExtent(mLastBreak + 1, nCand - 1),
535                mLastHyphenation);
536        // don't need to update mBestScore, because we're done
537#if VERBOSE_DEBUG
538        ALOGD("final break: %d %g", mBreaks.back(), mWidths.back());
539#endif
540    }
541}
542
543// Follow "prev" links in mCandidates array, and copy to result arrays.
544void LineBreaker::finishBreaksOptimal() {
545    // clear existing greedy break result
546    mBreaks.clear();
547    mWidths.clear();
548    mAscents.clear();
549    mDescents.clear();
550    mFlags.clear();
551
552    size_t nCand = mCandidates.size();
553    size_t prev;
554    for (size_t i = nCand - 1; i > 0; i = prev) {
555        prev = mCandidates[i].prev;
556        mBreaks.push_back(mCandidates[i].offset);
557        mWidths.push_back(mCandidates[i].postBreak - mCandidates[prev].preBreak);
558        MinikinExtent extent = computeMaxExtent(prev + 1, i);
559        mAscents.push_back(extent.ascent);
560        mDescents.push_back(extent.descent);
561        int flags = HyphenEdit::editForThisLine(mCandidates[i].hyphenType);
562        if (prev > 0) {
563            flags |= HyphenEdit::editForNextLine(mCandidates[prev].hyphenType);
564        }
565        mFlags.push_back(flags);
566    }
567    std::reverse(mBreaks.begin(), mBreaks.end());
568    std::reverse(mWidths.begin(), mWidths.end());
569    std::reverse(mFlags.begin(), mFlags.end());
570}
571
572void LineBreaker::computeBreaksOptimal() {
573    size_t active = 0;
574    size_t nCand = mCandidates.size();
575    float width = mLineWidthDelegate->getLineWidth(0);
576    float maxShrink = mJustified ? SHRINKABILITY * getSpaceWidth() : 0.0f;
577    std::vector<size_t> lineNumbers;
578    lineNumbers.reserve(nCand);
579    lineNumbers.push_back(0);  // The first candidate is always at the first line.
580
581    // "i" iterates through candidates for the end of the line.
582    for (size_t i = 1; i < nCand; i++) {
583        bool atEnd = i == nCand - 1;
584        float best = SCORE_INFTY;
585        size_t bestPrev = 0;
586
587        size_t lineNumberLast = lineNumbers[active];
588        width = mLineWidthDelegate->getLineWidth(lineNumberLast);
589
590        ParaWidth leftEdge = mCandidates[i].postBreak - width;
591        float bestHope = 0;
592
593        // "j" iterates through candidates for the beginning of the line.
594        for (size_t j = active; j < i; j++) {
595            size_t lineNumber = lineNumbers[j];
596            if (lineNumber != lineNumberLast) {
597                float widthNew = mLineWidthDelegate->getLineWidth(lineNumber);
598                if (widthNew != width) {
599                    leftEdge = mCandidates[i].postBreak - width;
600                    bestHope = 0;
601                    width = widthNew;
602                }
603                lineNumberLast = lineNumber;
604            }
605            float jScore = mCandidates[j].score;
606            if (jScore + bestHope >= best) continue;
607            float delta = mCandidates[j].preBreak - leftEdge;
608
609            // compute width score for line
610
611            // Note: the "bestHope" optimization makes the assumption that, when delta is
612            // non-negative, widthScore will increase monotonically as successive candidate
613            // breaks are considered.
614            float widthScore = 0.0f;
615            float additionalPenalty = 0.0f;
616            if ((atEnd || !mJustified) && delta < 0) {
617                widthScore = SCORE_OVERFULL;
618            } else if (atEnd && mStrategy != kBreakStrategy_Balanced) {
619                // increase penalty for hyphen on last line
620                additionalPenalty = LAST_LINE_PENALTY_MULTIPLIER * mCandidates[j].penalty;
621            } else {
622                widthScore = delta * delta;
623                if (delta < 0) {
624                    if (-delta < maxShrink *
625                            (mCandidates[i].postSpaceCount - mCandidates[j].preSpaceCount)) {
626                        widthScore *= SHRINK_PENALTY_MULTIPLIER;
627                    } else {
628                        widthScore = SCORE_OVERFULL;
629                    }
630                }
631            }
632
633            if (delta < 0) {
634                active = j + 1;
635            } else {
636                bestHope = widthScore;
637            }
638
639            float score = jScore + widthScore + additionalPenalty;
640            if (score <= best) {
641                best = score;
642                bestPrev = j;
643            }
644        }
645        mCandidates[i].score = best + mCandidates[i].penalty + mLinePenalty;
646        mCandidates[i].prev = bestPrev;
647        lineNumbers.push_back(lineNumbers[bestPrev] + 1);
648#if VERBOSE_DEBUG
649        ALOGD("break %zd: score=%g, prev=%zd", i, mCandidates[i].score, mCandidates[i].prev);
650#endif
651    }
652    finishBreaksOptimal();
653}
654
655size_t LineBreaker::computeBreaks() {
656    if (mStrategy == kBreakStrategy_Greedy) {
657        computeBreaksGreedy();
658    } else {
659        computeBreaksOptimal();
660    }
661    return mBreaks.size();
662}
663
664void LineBreaker::finish() {
665    mWordBreaker->finish();
666    mWidth = 0;
667    mCandidates.clear();
668    mBreaks.clear();
669    mWidths.clear();
670    mAscents.clear();
671    mDescents.clear();
672    mFlags.clear();
673    if (mTextBuf.size() > MAX_TEXT_BUF_RETAIN) {
674        mTextBuf.clear();
675        mTextBuf.shrink_to_fit();
676        mCharWidths.clear();
677        mCharWidths.shrink_to_fit();
678        mCharExtents.clear();
679        mCharExtents.shrink_to_fit();
680        mCandidates.shrink_to_fit();
681        mBreaks.shrink_to_fit();
682        mWidths.shrink_to_fit();
683        mAscents.shrink_to_fit();
684        mDescents.shrink_to_fit();
685        mFlags.shrink_to_fit();
686    }
687    mStrategy = kBreakStrategy_Greedy;
688    mHyphenationFrequency = kHyphenationFrequency_Normal;
689    mLinePenalty = 0.0f;
690    mJustified = false;
691    mLineWidthDelegate.reset();
692}
693
694}  // namespace minikin
695