提交 3a74bcd0 编写于 作者: S Seigo Nonaka

Tune line breaking for justification

Add an "mJustified" for justification, and tune the line breaking to
produce good results. Major differences for fully justified text include:
- Space can be shrunk in justified text.
- Hyphenation should be more aggressive in justified text.

Also adds a penalty for the last line being very short. This is tuned
to be more aggressive for ragged right than for justified text.

This is based on a patch by Raph Levien (raph@google.com).

Bug: 31707212
Test: Manually tested with Icbfab2faa11a6a0b52e6f0a77a9c9b5ef6e191da
Change-Id: If366f82800831ccc247ec07b7bc28ca4c6ae0ed6
上级 32c12c0a
......@@ -147,6 +147,8 @@ class LineBreaker {
void setStrategy(BreakStrategy strategy) { mStrategy = strategy; }
void setJustified(bool justified) { mJustified = justified; }
HyphenationFrequency getHyphenationFrequency() const { return mHyphenationFrequency; }
void setHyphenationFrequency(HyphenationFrequency frequency) {
......@@ -194,19 +196,23 @@ class LineBreaker {
float penalty; // penalty of this break (for example, hyphen penalty)
float score; // best score found for this break
size_t lineNumber; // only updated for non-constant line widths
size_t preSpaceCount; // preceding space count before breaking
size_t postSpaceCount; // preceding space count after breaking
uint8_t hyphenEdit;
};
float currentLineWidth() const;
void addWordBreak(size_t offset, ParaWidth preBreak, ParaWidth postBreak, float penalty,
uint8_t hyph);
void addWordBreak(size_t offset, ParaWidth preBreak, ParaWidth postBreak,
size_t preSpaceCount, size_t postSpaceCount, float penalty, uint8_t hyph);
void addCandidate(Candidate cand);
// push an actual break to the output. Takes care of setting flags for tab
void pushBreak(int offset, float width, uint8_t hyph);
float getSpaceWidth() const;
void computeBreaksGreedy();
void computeBreaksOptimal(bool isRectangular);
......@@ -223,6 +229,7 @@ class LineBreaker {
// layout parameters
BreakStrategy mStrategy = kBreakStrategy_Greedy;
HyphenationFrequency mHyphenationFrequency = kHyphenationFrequency_Normal;
bool mJustified;
LineWidths mLineWidths;
TabStops mTabStops;
......@@ -241,6 +248,7 @@ class LineBreaker {
float mBestScore;
ParaWidth mPreBreak; // prebreak of last break
int mFirstTabIndex;
size_t mSpaceCount;
};
} // namespace minikin
......
......@@ -22,6 +22,7 @@
#include <android/log.h>
#include "LayoutUtils.h"
#include <minikin/Layout.h>
#include <minikin/LineBreaker.h>
......@@ -44,6 +45,9 @@ const float LAST_LINE_PENALTY_MULTIPLIER = 4.0f;
// probably not the most appropriate method.
const float LINE_PENALTY_MULTIPLIER = 2.0f;
// Penalty assigned to shrinking the whitepsace.
const float SHRINK_PENALTY_MULTIPLIER = 4.0f;
// 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
......@@ -54,6 +58,9 @@ const size_t LONGEST_HYPHENATED_WORD = 45;
// to avoid allocation.
const size_t MAX_TEXT_BUF_RETAIN = 32678;
// Maximum amount that spaces can shrink, in justified text.
const float SHRINKABILITY = 1.0 / 3.0;
void LineBreaker::setLocale(const icu::Locale& locale, Hyphenator* hyphenator) {
mWordBreaker.setLocale(locale);
......@@ -66,7 +73,7 @@ void LineBreaker::setText() {
// handle initial break here because addStyleRun may never be called
mWordBreaker.next();
mCandidates.clear();
Candidate cand = {0, 0, 0.0, 0.0, 0.0, 0.0, 0, 0};
Candidate cand = {0, 0, 0.0, 0.0, 0.0, 0.0, 0, 0, 0, 0};
mCandidates.push_back(cand);
// reset greedy breaker state
......@@ -78,6 +85,7 @@ void LineBreaker::setText() {
mBestScore = SCORE_INFTY;
mPreBreak = 0;
mFirstTabIndex = INT_MAX;
mSpaceCount = 0;
}
void LineBreaker::setLineWidths(float firstWidth, int firstWidthLineCount, float restWidth) {
......@@ -137,7 +145,14 @@ float LineBreaker::addStyleRun(MinikinPaint* paint, const FontCollection* typefa
hyphenPenalty *= 4.0; // TODO: Replace with a better value after some testing
}
mLinePenalty = std::max(mLinePenalty, hyphenPenalty * LINE_PENALTY_MULTIPLIER);
if (mJustified) {
// Make hyphenation more aggressive for fully justified text (so that "normal" in
// justified mode is the same as "full" in ragged-right).
hyphenPenalty *= 0.25;
} else {
// Line penalty is zero for justified text.
mLinePenalty = std::max(mLinePenalty, hyphenPenalty * LINE_PENALTY_MULTIPLIER);
}
}
size_t current = (size_t)mWordBreaker.current();
......@@ -145,6 +160,7 @@ float LineBreaker::addStyleRun(MinikinPaint* paint, const FontCollection* typefa
size_t lastBreak = start;
ParaWidth lastBreakWidth = mWidth;
ParaWidth postBreak = mWidth;
size_t postSpaceCount = mSpaceCount;
bool temporarilySkipHyphenation = false;
for (size_t i = start; i < end; i++) {
uint16_t c = mTextBuf[i];
......@@ -156,9 +172,11 @@ float LineBreaker::addStyleRun(MinikinPaint* paint, const FontCollection* typefa
// fall back to greedy; other modes don't know how to deal with tabs
mStrategy = kBreakStrategy_Greedy;
} else {
if (isWordSpace(c)) mSpaceCount += 1;
mWidth += mCharWidths[i];
if (!isLineEndSpace(c)) {
postBreak = mWidth;
postSpaceCount = mSpaceCount;
afterWord = i + 1;
}
}
......@@ -196,11 +214,12 @@ float LineBreaker::addStyleRun(MinikinPaint* paint, const FontCollection* typefa
ParaWidth hyphPostBreak = lastBreakWidth + firstPartWidth;
paint->hyphenEdit = 0;
const float secondPartWith = Layout::measureText(mTextBuf.data(), j,
const float secondPartWidth = Layout::measureText(mTextBuf.data(), j,
afterWord - j, mTextBuf.size(), bidiFlags, style, *paint,
typeface, nullptr);
ParaWidth hyphPreBreak = postBreak - secondPartWith;
addWordBreak(j, hyphPreBreak, hyphPostBreak, hyphenPenalty, hyph);
ParaWidth hyphPreBreak = postBreak - secondPartWidth;
addWordBreak(j, hyphPreBreak, hyphPostBreak, postSpaceCount, postSpaceCount,
hyphenPenalty, hyph);
}
}
}
......@@ -210,7 +229,7 @@ float LineBreaker::addStyleRun(MinikinPaint* paint, const FontCollection* typefa
// Skip break for zero-width characters inside replacement span
if (paint != nullptr || current == end || mCharWidths[current] > 0) {
float penalty = hyphenPenalty * mWordBreaker.breakBadness();
addWordBreak(current, mWidth, postBreak, penalty, 0);
addWordBreak(current, mWidth, postBreak, mSpaceCount, postSpaceCount, penalty, 0);
}
lastBreak = current;
lastBreakWidth = mWidth;
......@@ -224,7 +243,7 @@ float LineBreaker::addStyleRun(MinikinPaint* paint, const FontCollection* typefa
// 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,
float penalty, uint8_t hyph) {
size_t preSpaceCount, size_t postSpaceCount, float penalty, uint8_t hyph) {
Candidate cand;
ParaWidth width = mCandidates.back().preBreak;
if (postBreak - width > currentLineWidth()) {
......@@ -239,6 +258,9 @@ void LineBreaker::addWordBreak(size_t offset, ParaWidth preBreak, ParaWidth post
cand.offset = i;
cand.preBreak = width;
cand.postBreak = width;
// postSpaceCount doesn't include trailing spaces
cand.preSpaceCount = postSpaceCount;
cand.postSpaceCount = postSpaceCount;
cand.penalty = SCORE_DESPERATE;
cand.hyphenEdit = 0;
#if VERBOSE_DEBUG
......@@ -255,6 +277,8 @@ void LineBreaker::addWordBreak(size_t offset, ParaWidth preBreak, ParaWidth post
cand.preBreak = preBreak;
cand.postBreak = postBreak;
cand.penalty = penalty;
cand.preSpaceCount = preSpaceCount;
cand.postSpaceCount = postSpaceCount;
cand.hyphenEdit = hyph;
#if VERBOSE_DEBUG
ALOGD("cand: %zd %g:%g", mCandidates.size(), cand.postBreak, cand.preBreak);
......@@ -301,6 +325,18 @@ void LineBreaker::addReplacement(size_t start, size_t end, float width) {
addStyleRun(nullptr, nullptr, FontStyle(), start, end, false);
}
// Get the width of a space. May return 0 if there are no spaces.
// Note: if there are multiple different widths for spaces (for example, because of mixing of
// fonts), it's only guaranteed to pick one.
float LineBreaker::getSpaceWidth() const {
for (size_t i = 0; i < mTextBuf.size(); i++) {
if (isWordSpace(mTextBuf[i])) {
return mCharWidths[i];
}
}
return 0.0f;
}
float LineBreaker::currentLineWidth() const {
return mLineWidths.getLineWidth(mBreaks.size());
}
......@@ -340,6 +376,10 @@ void LineBreaker::computeBreaksOptimal(bool isRectangle) {
size_t active = 0;
size_t nCand = mCandidates.size();
float width = mLineWidths.getLineWidth(0);
float shortLineFactor = mJustified ? 0.75f : 0.5f;
float maxShrink = mJustified ? SHRINKABILITY * getSpaceWidth() : 0.0f;
// "i" iterates through candidates for the end of the line.
for (size_t i = 1; i < nCand; i++) {
bool atEnd = i == nCand - 1;
float best = SCORE_INFTY;
......@@ -353,6 +393,7 @@ void LineBreaker::computeBreaksOptimal(bool isRectangle) {
ParaWidth leftEdge = mCandidates[i].postBreak - width;
float bestHope = 0;
// "j" iterates through candidates for the beginning of the line.
for (size_t j = active; j < i; j++) {
if (!isRectangle) {
size_t lineNumber = mCandidates[j].lineNumber;
......@@ -377,13 +418,24 @@ void LineBreaker::computeBreaksOptimal(bool isRectangle) {
// breaks are considered.
float widthScore = 0.0f;
float additionalPenalty = 0.0f;
if (delta < 0) {
if ((atEnd || !mJustified) && delta < 0) {
widthScore = SCORE_OVERFULL;
} else if (atEnd && mStrategy != kBreakStrategy_Balanced) {
// increase penalty for hyphen on last line
additionalPenalty = LAST_LINE_PENALTY_MULTIPLIER * mCandidates[j].penalty;
// Penalize very short (< 1 - shortLineFactor of total width) lines.
float underfill = delta - shortLineFactor * width;
widthScore = underfill > 0 ? underfill * underfill : 0;
} else {
widthScore = delta * delta;
if (delta < 0) {
if (-delta < maxShrink *
(mCandidates[i].postSpaceCount - mCandidates[j].preSpaceCount)) {
widthScore *= SHRINK_PENALTY_MULTIPLIER;
} else {
widthScore = SCORE_OVERFULL;
}
}
}
if (delta < 0) {
......@@ -440,6 +492,7 @@ void LineBreaker::finish() {
mStrategy = kBreakStrategy_Greedy;
mHyphenationFrequency = kHyphenationFrequency_Normal;
mLinePenalty = 0.0f;
mJustified = false;
}
} // namespace minikin
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册