Raph Levien abae97a39c Increase hyphenation penalty for short last line
Tuning for hyphenation parameters. We discourage hyphenation on the
last line, but offset this penalty by also applying a penalty for
each line, which optimizes for minimizing the number of lines. Thus,
when hyphenation can reduce the number of lines, it increases the
chance they're used.

There's probably more tuning and refinement that can be done, but
testing suggests that the tunable parameters are appropriate.

Bug: 20883322

Change-Id: Ida7eaf8aced109e426694f5a386924a842d29c4b
2015-06-08 15:23:20 -07:00

251 lines
8.2 KiB
C++

/*
* 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.
*/
/**
* A module for breaking paragraphs into lines, supporting high quality
* hyphenation and justification.
*/
#ifndef MINIKIN_LINE_BREAKER_H
#define MINIKIN_LINE_BREAKER_H
#include "unicode/brkiter.h"
#include "unicode/locid.h"
#include <cmath>
#include <vector>
#include "minikin/Hyphenator.h"
namespace android {
enum BreakStrategy {
kBreakStrategy_Greedy = 0,
kBreakStrategy_HighQuality = 1,
kBreakStrategy_Balanced = 2
};
enum HyphenationFrequency {
kHyphenationFrequency_None = 0,
kHyphenationFrequency_Normal = 1,
kHyphenationFrequency_Full = 2
};
// TODO: want to generalize to be able to handle array of line widths
class LineWidths {
public:
void setWidths(float firstWidth, int firstWidthLineCount, float restWidth) {
mFirstWidth = firstWidth;
mFirstWidthLineCount = firstWidthLineCount;
mRestWidth = restWidth;
}
void setIndents(const std::vector<float>& indents) {
mIndents = indents;
}
bool isConstant() const {
// technically mFirstWidthLineCount == 0 would count too, but doesn't actually happen
return mRestWidth == mFirstWidth && mIndents.empty();
}
float getLineWidth(int line) const {
float width = (line < mFirstWidthLineCount) ? mFirstWidth : mRestWidth;
if (!mIndents.empty()) {
if ((size_t)line < mIndents.size()) {
width -= mIndents[line];
} else {
width -= mIndents.back();
}
}
return width;
}
private:
float mFirstWidth;
int mFirstWidthLineCount;
float mRestWidth;
std::vector<float> mIndents;
};
class TabStops {
public:
void set(const int* stops, size_t nStops, int tabWidth) {
if (stops != nullptr) {
mStops.assign(stops, stops + nStops);
} else {
mStops.clear();
}
mTabWidth = tabWidth;
}
float nextTab(float widthSoFar) const {
for (size_t i = 0; i < mStops.size(); i++) {
if (mStops[i] > widthSoFar) {
return mStops[i];
}
}
return floor(widthSoFar / mTabWidth + 1) * mTabWidth;
}
private:
std::vector<int> mStops;
int mTabWidth;
};
class LineBreaker {
public:
const static int kTab_Shift = 29; // keep synchronized with TAB_MASK in StaticLayout.java
~LineBreaker() {
utext_close(&mUText);
delete mBreakIterator;
}
// Note: Locale persists across multiple invocations (it is not cleaned up by finish()),
// explicitly to avoid the cost of creating ICU BreakIterator objects. It should always
// be set on the first invocation, but callers are encouraged not to call again unless
// locale has actually changed.
// That logic could be here but it's better for performance that it's upstream because of
// the cost of constructing and comparing the ICU Locale object.
// Note: caller is responsible for managing lifetime of hyphenator
void setLocale(const icu::Locale& locale, Hyphenator* hyphenator);
void resize(size_t size) {
mTextBuf.resize(size);
mCharWidths.resize(size);
}
size_t size() const {
return mTextBuf.size();
}
uint16_t* buffer() {
return mTextBuf.data();
}
float* charWidths() {
return mCharWidths.data();
}
// set text to current contents of buffer
void setText();
void setLineWidths(float firstWidth, int firstWidthLineCount, float restWidth);
void setIndents(const std::vector<float>& indents);
void setTabStops(const int* stops, size_t nStops, int tabWidth) {
mTabStops.set(stops, nStops, tabWidth);
}
BreakStrategy getStrategy() const { return mStrategy; }
void setStrategy(BreakStrategy strategy) { mStrategy = strategy; }
HyphenationFrequency getHyphenationFrequency() const { return mHyphenationFrequency; }
void setHyphenationFrequency(HyphenationFrequency frequency) {
mHyphenationFrequency = frequency;
}
// TODO: this class is actually fairly close to being general and not tied to using
// Minikin to do the shaping of the strings. The main thing that would need to be changed
// is having some kind of callback (or virtual class, or maybe even template), which could
// easily be instantiated with Minikin's Layout. Future work for when needed.
float addStyleRun(MinikinPaint* paint, const FontCollection* typeface, FontStyle style,
size_t start, size_t end, bool isRtl);
void addReplacement(size_t start, size_t end, float width);
size_t computeBreaks();
const int* getBreaks() const {
return mBreaks.data();
}
const float* getWidths() const {
return mWidths.data();
}
const int* getFlags() const {
return mFlags.data();
}
void finish();
private:
// ParaWidth is used to hold cumulative width from beginning of paragraph. Note that for
// very large paragraphs, accuracy could degrade using only 32-bit float. Note however
// that float is used extensively on the Java side for this. This is a typedef so that
// we can easily change it based on performance/accuracy tradeoff.
typedef double ParaWidth;
// A single candidate break
struct Candidate {
size_t offset; // offset to text buffer, in code units
size_t prev; // index to previous break
ParaWidth preBreak;
ParaWidth postBreak;
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
uint8_t hyphenEdit;
};
float currentLineWidth() const;
void addWordBreak(size_t offset, ParaWidth preBreak, ParaWidth postBreak, 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);
void computeBreaksGreedy();
void computeBreaksOptimal(bool isRectangular);
void finishBreaksOptimal();
icu::BreakIterator* mBreakIterator = nullptr;
UText mUText = UTEXT_INITIALIZER;
std::vector<uint16_t>mTextBuf;
std::vector<float>mCharWidths;
Hyphenator* mHyphenator;
std::vector<uint8_t> mHyphBuf;
// layout parameters
BreakStrategy mStrategy = kBreakStrategy_Greedy;
HyphenationFrequency mHyphenationFrequency = kHyphenationFrequency_Normal;
LineWidths mLineWidths;
TabStops mTabStops;
// result of line breaking
std::vector<int> mBreaks;
std::vector<float> mWidths;
std::vector<int> mFlags;
ParaWidth mWidth = 0;
std::vector<Candidate> mCandidates;
float mLinePenalty = 0.0f;
// the following are state for greedy breaker (updated while adding style runs)
size_t mLastBreak;
size_t mBestBreak;
float mBestScore;
ParaWidth mPreBreak; // prebreak of last break
int mFirstTabIndex;
};
} // namespace android
#endif // MINIKIN_LINE_BREAKER_H