LineBreaker.cpp 17.2 KB
Newer Older
R
Raph Levien 已提交
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
/*
 * Copyright (C) 2015 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

#define VERBOSE_DEBUG 0

#include <limits>

#define LOG_TAG "Minikin"
#include <cutils/log.h>

#include <minikin/Layout.h>
#include <minikin/LineBreaker.h>

using std::vector;

namespace android {

const int CHAR_TAB = 0x0009;
32
const uint16_t CHAR_SOFT_HYPHEN = 0x00AD;
R
Raph Levien 已提交
33 34 35 36 37 38 39

// Large scores in a hierarchy; we prefer desperate breaks to an overfull line. All these
// constants are larger than any reasonable actual width score.
const float SCORE_INFTY = std::numeric_limits<float>::max();
const float SCORE_OVERFULL = 1e12f;
const float SCORE_DESPERATE = 1e10f;

40 41 42 43 44 45 46
// Multiplier for hyphen penalty on last line.
const float LAST_LINE_PENALTY_MULTIPLIER = 4.0f;
// Penalty assigned to each line break (to try to minimize number of lines)
// TODO: when we implement full justification (so spaces can shrink and stretch), this is
// probably not the most appropriate method.
const float LINE_PENALTY_MULTIPLIER = 2.0f;

47 48 49 50 51 52
// Very long words trigger O(n^2) behavior in hyphenation, so we disable hyphenation for
// unreasonably long words. This is somewhat of a heuristic because extremely long words
// are possible in some languages. This does mean that very long real words can get
// broken by desperate breaks, with no hyphens.
const size_t LONGEST_HYPHENATED_WORD = 45;

R
Raph Levien 已提交
53 54 55 56
// When the text buffer is within this limit, capacity of vectors is retained at finish(),
// to avoid allocation.
const size_t MAX_TEXT_BUF_RETAIN = 32678;

57 58 59 60 61 62 63 64 65 66
void LineBreaker::setLocale(const icu::Locale& locale, Hyphenator* hyphenator) {
    delete mBreakIterator;
    UErrorCode status = U_ZERO_ERROR;
    mBreakIterator = icu::BreakIterator::createLineInstance(locale, status);
    // TODO: check status

    // TODO: load actual resource dependent on locale; letting Minikin do it is a hack
    mHyphenator = hyphenator;
}

R
Raph Levien 已提交
67 68 69 70 71 72 73 74 75
void LineBreaker::setText() {
    UErrorCode status = U_ZERO_ERROR;
    utext_openUChars(&mUText, mTextBuf.data(), mTextBuf.size(), &status);
    mBreakIterator->setText(&mUText, status);
    mBreakIterator->first();

    // handle initial break here because addStyleRun may never be called
    mBreakIterator->next();
    mCandidates.clear();
76
    Candidate cand = {0, 0, 0.0, 0.0, 0.0, 0.0, 0, 0};
R
Raph Levien 已提交
77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93
    mCandidates.push_back(cand);

    // reset greedy breaker state
    mBreaks.clear();
    mWidths.clear();
    mFlags.clear();
    mLastBreak = 0;
    mBestBreak = 0;
    mBestScore = SCORE_INFTY;
    mPreBreak = 0;
    mFirstTabIndex = INT_MAX;
}

void LineBreaker::setLineWidths(float firstWidth, int firstWidthLineCount, float restWidth) {
    mLineWidths.setWidths(firstWidth, firstWidthLineCount, restWidth);
}

94

R
Raph Levien 已提交
95 96
void LineBreaker::setIndents(const std::vector<float>& indents) {
    mLineWidths.setIndents(indents);
97 98
}

R
Raph Levien 已提交
99
// This function determines whether a character is a space that disappears at end of line.
100 101
// It is the Unicode set: [[:General_Category=Space_Separator:]-[:Line_Break=Glue:]],
// plus '\n'.
R
Raph Levien 已提交
102 103
// Note: all such characters are in the BMP, so it's ok to use code units for this.
static bool isLineEndSpace(uint16_t c) {
104 105
    return c == '\n' || c == ' ' || c == 0x1680 || (0x2000 <= c && c <= 0x200A && c != 0x2007) ||
            c == 0x205F || c == 0x3000;
R
Raph Levien 已提交
106 107
}

108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125
// This function determines whether a character is like U+2010 HYPHEN in
// line breaking and usage: a character immediately after which line breaks
// are allowed, but words containing it should not be automatically
// hyphenated. This is a curated set, created by manually inspecting all
// the characters that have the Unicode line breaking property of BA or HY
// and seeing which ones are hyphens.
static bool isLineBreakingHyphen(uint16_t c) {
    return (c == 0x002D || // HYPHEN-MINUS
            c == 0x058A || // ARMENIAN HYPHEN
            c == 0x05BE || // HEBREW PUNCTUATION MAQAF
            c == 0x1400 || // CANADIAN SYLLABICS HYPHEN
            c == 0x2010 || // HYPHEN
            c == 0x2013 || // EN DASH
            c == 0x2027 || // HYPHENATION POINT
            c == 0x2E17 || // DOUBLE OBLIQUE HYPHEN
            c == 0x2E40);  // DOUBLE HYPHEN
}

R
Raph Levien 已提交
126 127 128 129 130
// Ordinarily, this method measures the text in the range given. However, when paint
// is nullptr, it assumes the widths have already been calculated and stored in the
// width buffer.
// This method finds the candidate word breaks (using the ICU break iterator) and sends them
// to addCandidate.
131
float LineBreaker::addStyleRun(MinikinPaint* paint, const FontCollection* typeface,
R
Raph Levien 已提交
132 133 134 135 136
        FontStyle style, size_t start, size_t end, bool isRtl) {
    Layout layout;  // performance TODO: move layout to self object to reduce allocation cost?
    float width = 0.0f;
    int bidiFlags = isRtl ? kBidi_Force_RTL : kBidi_Force_LTR;

137
    float hyphenPenalty = 0.0;
R
Raph Levien 已提交
138 139 140 141 142 143
    if (paint != nullptr) {
        layout.setFontCollection(typeface);
        layout.doLayout(mTextBuf.data(), start, end - start, mTextBuf.size(), bidiFlags, style,
                *paint);
        layout.getAdvances(mCharWidths.data() + start);
        width = layout.getAdvance();
144 145 146

        // a heuristic that seems to perform well
        hyphenPenalty = 0.5 * paint->size * paint->scaleX * mLineWidths.getLineWidth(0);
147 148 149
        if (mHyphenationFrequency == kHyphenationFrequency_Normal) {
            hyphenPenalty *= 4.0; // TODO: Replace with a better value after some testing
        }
150 151

        mLinePenalty = std::max(mLinePenalty, hyphenPenalty * LINE_PENALTY_MULTIPLIER);
R
Raph Levien 已提交
152 153
    }

154
    size_t current = (size_t)mBreakIterator->current();
155 156 157 158
    size_t wordEnd = start;
    size_t lastBreak = start;
    ParaWidth lastBreakWidth = mWidth;
    ParaWidth postBreak = mWidth;
159
    bool temporarilySkipHyphenation = false;
R
Raph Levien 已提交
160 161 162 163 164 165 166 167 168 169 170 171 172
    for (size_t i = start; i < end; i++) {
        uint16_t c = mTextBuf[i];
        if (c == CHAR_TAB) {
            mWidth = mPreBreak + mTabStops.nextTab(mWidth - mPreBreak);
            if (mFirstTabIndex == INT_MAX) {
                mFirstTabIndex = (int)i;
            }
            // fall back to greedy; other modes don't know how to deal with tabs
            mStrategy = kBreakStrategy_Greedy;
        } else {
            mWidth += mCharWidths[i];
            if (!isLineEndSpace(c)) {
                postBreak = mWidth;
173
                wordEnd = i + 1;
R
Raph Levien 已提交
174 175 176
            }
        }
        if (i + 1 == current) {
177 178 179
            // Override ICU's treatment of soft hyphen as a break opportunity, because we want it
            // to be a hyphen break, with penalty and drawing behavior.
            if (c != CHAR_SOFT_HYPHEN) {
180 181
                // TODO: Add a new type of HyphenEdit for breaks whose hyphen already exists, so
                // we can pass the whole word down to Hyphenator like the soft hyphen case.
182
                bool wordEndsInHyphen = isLineBreakingHyphen(c);
183 184
                if (paint != nullptr && mHyphenator != nullptr &&
                        mHyphenationFrequency != kHyphenationFrequency_None &&
185
                        !wordEndsInHyphen && !temporarilySkipHyphenation &&
186
                        wordEnd > lastBreak && wordEnd - lastBreak <= LONGEST_HYPHENATED_WORD) {
187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213
                    mHyphenator->hyphenate(&mHyphBuf, &mTextBuf[lastBreak], wordEnd - lastBreak);
    #if VERBOSE_DEBUG
                    std::string hyphenatedString;
                    for (size_t j = lastBreak; j < wordEnd; j++) {
                        if (mHyphBuf[j - lastBreak]) hyphenatedString.push_back('-');
                        // Note: only works with ASCII, should do UTF-8 conversion here
                        hyphenatedString.push_back(buffer()[j]);
                    }
                    ALOGD("hyphenated string: %s", hyphenatedString.c_str());
    #endif

                    // measure hyphenated substrings
                    for (size_t j = lastBreak; j < wordEnd; j++) {
                        uint8_t hyph = mHyphBuf[j - lastBreak];
                        if (hyph) {
                            paint->hyphenEdit = hyph;
                            layout.doLayout(mTextBuf.data(), lastBreak, j - lastBreak,
                                    mTextBuf.size(), bidiFlags, style, *paint);
                            ParaWidth hyphPostBreak = lastBreakWidth + layout.getAdvance();
                            paint->hyphenEdit = 0;
                            layout.doLayout(mTextBuf.data(), j, wordEnd - j,
                                    mTextBuf.size(), bidiFlags, style, *paint);
                            ParaWidth hyphPreBreak = postBreak - layout.getAdvance();
                            addWordBreak(j, hyphPreBreak, hyphPostBreak, hyphenPenalty, hyph);
                        }
                    }
                }
214 215
                // Skip hyphenating the next word if and only if the present word ends in a hyphen
                temporarilySkipHyphenation = wordEndsInHyphen;
216

217 218
                // Skip break for zero-width characters inside replacement span
                if (paint != nullptr || current == end || mCharWidths[current] > 0) {
219 220 221 222
                    addWordBreak(current, mWidth, postBreak, 0.0, 0);
                }
                lastBreak = current;
                lastBreakWidth = mWidth;
R
Raph Levien 已提交
223 224 225 226 227 228 229 230 231 232 233
            }
            current = (size_t)mBreakIterator->next();
        }
    }

    return width;
}

// add a word break (possibly for a hyphenated fragment), and add desperate breaks if
// needed (ie when word exceeds current line width)
void LineBreaker::addWordBreak(size_t offset, ParaWidth preBreak, ParaWidth postBreak,
234
        float penalty, uint8_t hyph) {
R
Raph Levien 已提交
235 236 237 238 239 240 241 242 243 244 245 246 247 248 249
    Candidate cand;
    ParaWidth width = mCandidates.back().preBreak;
    if (postBreak - width > currentLineWidth()) {
        // Add desperate breaks.
        // Note: these breaks are based on the shaping of the (non-broken) original text; they
        // are imprecise especially in the presence of kerning, ligatures, and Arabic shaping.
        size_t i = mCandidates.back().offset;
        width += mCharWidths[i++];
        for (; i < offset; i++) {
            float w = mCharWidths[i];
            if (w > 0) {
                cand.offset = i;
                cand.preBreak = width;
                cand.postBreak = width;
                cand.penalty = SCORE_DESPERATE;
250
                cand.hyphenEdit = 0;
R
Raph Levien 已提交
251
#if VERBOSE_DEBUG
252
                ALOGD("desperate cand: %zd %g:%g",
R
Raph Levien 已提交
253 254 255 256 257 258 259 260 261 262 263 264
                        mCandidates.size(), cand.postBreak, cand.preBreak);
#endif
                addCandidate(cand);
                width += w;
            }
        }
    }

    cand.offset = offset;
    cand.preBreak = preBreak;
    cand.postBreak = postBreak;
    cand.penalty = penalty;
265
    cand.hyphenEdit = hyph;
R
Raph Levien 已提交
266
#if VERBOSE_DEBUG
267
    ALOGD("cand: %zd %g:%g", mCandidates.size(), cand.postBreak, cand.preBreak);
R
Raph Levien 已提交
268 269 270 271 272 273 274 275 276 277 278 279 280
#endif
    addCandidate(cand);
}

// TODO performance: could avoid populating mCandidates if greedy only
void LineBreaker::addCandidate(Candidate cand) {
    size_t candIndex = mCandidates.size();
    mCandidates.push_back(cand);
    if (cand.postBreak - mPreBreak > currentLineWidth()) {
        // This break would create an overfull line, pick the best break and break there (greedy)
        if (mBestBreak == mLastBreak) {
            mBestBreak = candIndex;
        }
281 282
        pushBreak(mCandidates[mBestBreak].offset, mCandidates[mBestBreak].postBreak - mPreBreak,
                mCandidates[mBestBreak].hyphenEdit);
R
Raph Levien 已提交
283 284 285 286 287 288 289 290 291 292 293 294 295
        mBestScore = SCORE_INFTY;
#if VERBOSE_DEBUG
        ALOGD("break: %d %g", mBreaks.back(), mWidths.back());
#endif
        mLastBreak = mBestBreak;
        mPreBreak = mCandidates[mBestBreak].preBreak;
    }
    if (cand.penalty <= mBestScore) {
        mBestBreak = candIndex;
        mBestScore = cand.penalty;
    }
}

296 297 298 299 300 301 302 303 304
void LineBreaker::pushBreak(int offset, float width, uint8_t hyph) {
    mBreaks.push_back(offset);
    mWidths.push_back(width);
    int flags = (mFirstTabIndex < mBreaks.back()) << kTab_Shift;
    flags |= hyph;
    mFlags.push_back(flags);
    mFirstTabIndex = INT_MAX;
}

R
Raph Levien 已提交
305 306 307 308 309 310 311 312 313 314 315 316 317 318
void LineBreaker::addReplacement(size_t start, size_t end, float width) {
    mCharWidths[start] = width;
    std::fill(&mCharWidths[start + 1], &mCharWidths[end], 0.0f);
    addStyleRun(nullptr, nullptr, FontStyle(), start, end, false);
}

float LineBreaker::currentLineWidth() const {
    return mLineWidths.getLineWidth(mBreaks.size());
}

void LineBreaker::computeBreaksGreedy() {
    // All breaks but the last have been added in addCandidate already.
    size_t nCand = mCandidates.size();
    if (nCand == 1 || mLastBreak != nCand - 1) {
319 320
        pushBreak(mCandidates[nCand - 1].offset, mCandidates[nCand - 1].postBreak - mPreBreak, 0);
        // don't need to update mBestScore, because we're done
R
Raph Levien 已提交
321 322 323 324 325 326
#if VERBOSE_DEBUG
        ALOGD("final break: %d %g", mBreaks.back(), mWidths.back());
#endif
    }
}

327 328
// Follow "prev" links in mCandidates array, and copy to result arrays.
void LineBreaker::finishBreaksOptimal() {
R
Raph Levien 已提交
329 330 331 332
    // clear existing greedy break result
    mBreaks.clear();
    mWidths.clear();
    mFlags.clear();
333 334 335 336 337 338 339 340 341 342 343 344 345
    size_t nCand = mCandidates.size();
    size_t prev;
    for (size_t i = nCand - 1; i > 0; i = prev) {
        prev = mCandidates[i].prev;
        mBreaks.push_back(mCandidates[i].offset);
        mWidths.push_back(mCandidates[i].postBreak - mCandidates[prev].preBreak);
        mFlags.push_back(mCandidates[i].hyphenEdit);
    }
    std::reverse(mBreaks.begin(), mBreaks.end());
    std::reverse(mWidths.begin(), mWidths.end());
    std::reverse(mFlags.begin(), mFlags.end());
}

346
void LineBreaker::computeBreaksOptimal(bool isRectangle) {
347 348
    size_t active = 0;
    size_t nCand = mCandidates.size();
349
    float width = mLineWidths.getLineWidth(0);
350 351 352 353
    for (size_t i = 1; i < nCand; i++) {
        bool atEnd = i == nCand - 1;
        float best = SCORE_INFTY;
        size_t bestPrev = 0;
354
        size_t lineNumberLast = 0;
355

356 357 358 359
        if (!isRectangle) {
            size_t lineNumberLast = mCandidates[active].lineNumber;
            width = mLineWidths.getLineWidth(lineNumberLast);
        }
360 361 362 363
        ParaWidth leftEdge = mCandidates[i].postBreak - width;
        float bestHope = 0;

        for (size_t j = active; j < i; j++) {
364 365 366 367 368 369 370 371 372 373
            if (!isRectangle) {
                size_t lineNumber = mCandidates[j].lineNumber;
                if (lineNumber != lineNumberLast) {
                    float widthNew = mLineWidths.getLineWidth(lineNumber);
                    if (widthNew != width) {
                        leftEdge = mCandidates[i].postBreak - width;
                        bestHope = 0;
                        width = widthNew;
                    }
                    lineNumberLast = lineNumber;
374 375 376 377 378 379
                }
            }
            float jScore = mCandidates[j].score;
            if (jScore + bestHope >= best) continue;
            float delta = mCandidates[j].preBreak - leftEdge;

380
            // compute width score for line
381 382 383 384

            // Note: the "bestHope" optimization makes the assumption that, when delta is
            // non-negative, widthScore will increase monotonically as successive candidate
            // breaks are considered.
385
            float widthScore = 0.0f;
386
            float additionalPenalty = 0.0f;
387
            if (delta < 0) {
388 389 390
                widthScore = SCORE_OVERFULL;
            } else if (atEnd && mStrategy != kBreakStrategy_Balanced) {
                // increase penalty for hyphen on last line
391
                additionalPenalty = LAST_LINE_PENALTY_MULTIPLIER * mCandidates[j].penalty;
392
            } else {
393
                widthScore = delta * delta;
394
            }
R
Raph Levien 已提交
395 396 397 398 399 400 401

            if (delta < 0) {
                active = j + 1;
            } else {
                bestHope = widthScore;
            }

402
            float score = jScore + widthScore + additionalPenalty;
R
Raph Levien 已提交
403 404 405 406 407
            if (score <= best) {
                best = score;
                bestPrev = j;
            }
        }
408
        mCandidates[i].score = best + mCandidates[i].penalty + mLinePenalty;
R
Raph Levien 已提交
409
        mCandidates[i].prev = bestPrev;
410
        mCandidates[i].lineNumber = mCandidates[bestPrev].lineNumber + 1;
411
#if VERBOSE_DEBUG
412
        ALOGD("break %zd: score=%g, prev=%zd", i, mCandidates[i].score, mCandidates[i].prev);
413
#endif
R
Raph Levien 已提交
414
    }
415
    finishBreaksOptimal();
R
Raph Levien 已提交
416 417 418 419 420 421
}

size_t LineBreaker::computeBreaks() {
    if (mStrategy == kBreakStrategy_Greedy) {
        computeBreaksGreedy();
    } else {
422
        computeBreaksOptimal(mLineWidths.isConstant());
R
Raph Levien 已提交
423 424 425 426 427 428 429 430 431 432 433 434 435 436 437
    }
    return mBreaks.size();
}

void LineBreaker::finish() {
    mWidth = 0;
    mCandidates.clear();
    mBreaks.clear();
    mWidths.clear();
    mFlags.clear();
    if (mTextBuf.size() > MAX_TEXT_BUF_RETAIN) {
        mTextBuf.clear();
        mTextBuf.shrink_to_fit();
        mCharWidths.clear();
        mCharWidths.shrink_to_fit();
438 439
        mHyphBuf.clear();
        mHyphBuf.shrink_to_fit();
R
Raph Levien 已提交
440 441 442 443 444 445
        mCandidates.shrink_to_fit();
        mBreaks.shrink_to_fit();
        mWidths.shrink_to_fit();
        mFlags.shrink_to_fit();
    }
    mStrategy = kBreakStrategy_Greedy;
446
    mHyphenationFrequency = kHyphenationFrequency_Normal;
447
    mLinePenalty = 0.0f;
R
Raph Levien 已提交
448 449 450
}

}  // namespace android