Introduce full justification.

Adds a new get/setJustify API to TextView and StaticLayout.Builder for
justification, and fully justifies text when it's enabled.

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

Bug: 31707212
Test: Manually done and CTS will introduced with
      I0f3bbf39d60a66b71b30e1351f7c741208f05dce passes.
Change-Id: Icbfab2faa11a6a0b52e6f0a77a9c9b5ef6e191da
This commit is contained in:
Seigo Nonaka
2016-11-28 16:24:14 +09:00
parent ab156196f8
commit 09da71a6dc
9 changed files with 243 additions and 20 deletions

View File

@@ -39434,6 +39434,7 @@ package android.text {
method public android.text.StaticLayout.Builder setHyphenationFrequency(int);
method public android.text.StaticLayout.Builder setIncludePad(boolean);
method public android.text.StaticLayout.Builder setIndents(int[], int[]);
method public android.text.StaticLayout.Builder setJustify(boolean);
method public android.text.StaticLayout.Builder setLineSpacing(float, float);
method public android.text.StaticLayout.Builder setMaxLines(int);
method public android.text.StaticLayout.Builder setText(java.lang.CharSequence);
@@ -48958,6 +48959,7 @@ package android.widget {
method public boolean getIncludeFontPadding();
method public android.os.Bundle getInputExtras(boolean);
method public int getInputType();
method public boolean getJustify();
method public final android.text.method.KeyListener getKeyListener();
method public final android.text.Layout getLayout();
method public float getLetterSpacing();
@@ -49066,6 +49068,7 @@ package android.widget {
method public void setIncludeFontPadding(boolean);
method public void setInputExtras(int) throws java.io.IOException, org.xmlpull.v1.XmlPullParserException;
method public void setInputType(int);
method public void setJustify(boolean);
method public void setKeyListener(android.text.method.KeyListener);
method public void setLetterSpacing(float);
method public void setLineSpacing(float, float);

View File

@@ -42644,6 +42644,7 @@ package android.text {
method public android.text.StaticLayout.Builder setHyphenationFrequency(int);
method public android.text.StaticLayout.Builder setIncludePad(boolean);
method public android.text.StaticLayout.Builder setIndents(int[], int[]);
method public android.text.StaticLayout.Builder setJustify(boolean);
method public android.text.StaticLayout.Builder setLineSpacing(float, float);
method public android.text.StaticLayout.Builder setMaxLines(int);
method public android.text.StaticLayout.Builder setText(java.lang.CharSequence);
@@ -52528,6 +52529,7 @@ package android.widget {
method public boolean getIncludeFontPadding();
method public android.os.Bundle getInputExtras(boolean);
method public int getInputType();
method public boolean getJustify();
method public final android.text.method.KeyListener getKeyListener();
method public final android.text.Layout getLayout();
method public float getLetterSpacing();
@@ -52636,6 +52638,7 @@ package android.widget {
method public void setIncludeFontPadding(boolean);
method public void setInputExtras(int) throws java.io.IOException, org.xmlpull.v1.XmlPullParserException;
method public void setInputType(int);
method public void setJustify(boolean);
method public void setKeyListener(android.text.method.KeyListener);
method public void setLetterSpacing(float);
method public void setLineSpacing(float, float);

View File

@@ -39554,6 +39554,7 @@ package android.text {
method public android.text.StaticLayout.Builder setHyphenationFrequency(int);
method public android.text.StaticLayout.Builder setIncludePad(boolean);
method public android.text.StaticLayout.Builder setIndents(int[], int[]);
method public android.text.StaticLayout.Builder setJustify(boolean);
method public android.text.StaticLayout.Builder setLineSpacing(float, float);
method public android.text.StaticLayout.Builder setMaxLines(int);
method public android.text.StaticLayout.Builder setText(java.lang.CharSequence);
@@ -49260,6 +49261,7 @@ package android.widget {
method public boolean getIncludeFontPadding();
method public android.os.Bundle getInputExtras(boolean);
method public int getInputType();
method public boolean getJustify();
method public final android.text.method.KeyListener getKeyListener();
method public final android.text.Layout getLayout();
method public float getLetterSpacing();
@@ -49368,6 +49370,7 @@ package android.widget {
method public void setIncludeFontPadding(boolean);
method public void setInputExtras(int) throws java.io.IOException, org.xmlpull.v1.XmlPullParserException;
method public void setInputType(int);
method public void setJustify(boolean);
method public void setKeyListener(android.text.method.KeyListener);
method public void setLetterSpacing(float);
method public void setLineSpacing(float, float);

View File

@@ -85,7 +85,7 @@ public class DynamicLayout extends Layout
this(base, display, paint, width, align, TextDirectionHeuristics.FIRSTSTRONG_LTR,
spacingmult, spacingadd, includepad,
StaticLayout.BREAK_STRATEGY_SIMPLE, StaticLayout.HYPHENATION_FREQUENCY_NONE,
ellipsize, ellipsizedWidth);
false /* justify */, ellipsize, ellipsizedWidth);
}
/**
@@ -102,7 +102,8 @@ public class DynamicLayout extends Layout
int width, Alignment align, TextDirectionHeuristic textDir,
float spacingmult, float spacingadd,
boolean includepad, int breakStrategy, int hyphenationFrequency,
TextUtils.TruncateAt ellipsize, int ellipsizedWidth) {
boolean justify, TextUtils.TruncateAt ellipsize,
int ellipsizedWidth) {
super((ellipsize == null)
? display
: (display instanceof Spanned)
@@ -127,6 +128,7 @@ public class DynamicLayout extends Layout
mIncludePad = includepad;
mBreakStrategy = breakStrategy;
mJustify = justify;
mHyphenationFrequency = hyphenationFrequency;
/*
@@ -300,7 +302,8 @@ public class DynamicLayout extends Layout
.setEllipsizedWidth(mEllipsizedWidth)
.setEllipsize(mEllipsizeAt)
.setBreakStrategy(mBreakStrategy)
.setHyphenationFrequency(mHyphenationFrequency);
.setHyphenationFrequency(mHyphenationFrequency)
.setJustify(mJustify);
reflowed.generate(b, false, true);
int n = reflowed.getLineCount();
// If the new layout has a blank line at the end, but it is not
@@ -808,6 +811,7 @@ public class DynamicLayout extends Layout
private TextUtils.TruncateAt mEllipsizeAt;
private int mBreakStrategy;
private int mHyphenationFrequency;
private boolean mJustify;
private PackedIntVector mInts;
private PackedObjectVector<Directions> mObjects;

View File

@@ -218,6 +218,11 @@ public abstract class Layout {
mTextDir = textDir;
}
/** @hide */
protected void setJustify(boolean justify) {
mJustify = justify;
}
/**
* Replace constructor properties of this Layout with new ones. Be careful.
*/
@@ -266,6 +271,99 @@ public abstract class Layout {
drawText(canvas, firstLine, lastLine);
}
private boolean isJustificationRequired(int lineNum) {
if (!mJustify) return false;
final int lineEnd = getLineEnd(lineNum);
return lineEnd < mText.length() && mText.charAt(lineEnd - 1) != '\n';
}
private float getJustifyWidth(int lineNum) {
Alignment paraAlign = mAlignment;
TabStops tabStops = null;
boolean tabStopsIsInitialized = false;
int left = 0;
int right = mWidth;
final int dir = getParagraphDirection(lineNum);
ParagraphStyle[] spans = NO_PARA_SPANS;
if (mSpannedText) {
Spanned sp = (Spanned) mText;
final int start = getLineStart(lineNum);
final boolean isFirstParaLine = (start == 0 || mText.charAt(start - 1) == '\n');
if (isFirstParaLine) {
final int spanEnd = sp.nextSpanTransition(start, mText.length(),
ParagraphStyle.class);
spans = getParagraphSpans(sp, start, spanEnd, ParagraphStyle.class);
for (int n = spans.length - 1; n >= 0; n--) {
if (spans[n] instanceof AlignmentSpan) {
paraAlign = ((AlignmentSpan) spans[n]).getAlignment();
break;
}
}
}
final int length = spans.length;
boolean useFirstLineMargin = isFirstParaLine;
for (int n = 0; n < length; n++) {
if (spans[n] instanceof LeadingMarginSpan2) {
int count = ((LeadingMarginSpan2) spans[n]).getLeadingMarginLineCount();
int startLine = getLineForOffset(sp.getSpanStart(spans[n]));
if (lineNum < startLine + count) {
useFirstLineMargin = true;
break;
}
}
}
for (int n = 0; n < length; n++) {
if (spans[n] instanceof LeadingMarginSpan) {
LeadingMarginSpan margin = (LeadingMarginSpan) spans[n];
if (dir == DIR_RIGHT_TO_LEFT) {
right -= margin.getLeadingMargin(useFirstLineMargin);
} else {
left += margin.getLeadingMargin(useFirstLineMargin);
}
}
}
}
if (getLineContainsTab(lineNum)) {
tabStops = new TabStops(TAB_INCREMENT, spans);
}
final Alignment align;
if (paraAlign == Alignment.ALIGN_LEFT) {
align = (dir == DIR_LEFT_TO_RIGHT) ? Alignment.ALIGN_NORMAL : Alignment.ALIGN_OPPOSITE;
} else if (paraAlign == Alignment.ALIGN_RIGHT) {
align = (dir == DIR_LEFT_TO_RIGHT) ? Alignment.ALIGN_OPPOSITE : Alignment.ALIGN_NORMAL;
} else {
align = paraAlign;
}
final int indentWidth;
if (align == Alignment.ALIGN_NORMAL) {
if (dir == DIR_LEFT_TO_RIGHT) {
indentWidth = getIndentAdjust(lineNum, Alignment.ALIGN_LEFT);
} else {
indentWidth = -getIndentAdjust(lineNum, Alignment.ALIGN_RIGHT);
}
} else if (align == Alignment.ALIGN_OPPOSITE) {
if (dir == DIR_LEFT_TO_RIGHT) {
indentWidth = -getIndentAdjust(lineNum, Alignment.ALIGN_RIGHT);
} else {
indentWidth = getIndentAdjust(lineNum, Alignment.ALIGN_LEFT);
}
} else { // Alignment.ALIGN_CENTER
indentWidth = getIndentAdjust(lineNum, Alignment.ALIGN_CENTER);
}
return right - left - indentWidth;
}
/**
* @hide
*/
@@ -274,7 +372,7 @@ public abstract class Layout {
int previousLineEnd = getLineStart(firstLine);
ParagraphStyle[] spans = NO_PARA_SPANS;
int spanEnd = 0;
TextPaint paint = mPaint;
final TextPaint paint = mPaint;
CharSequence buf = mText;
Alignment paraAlign = mAlignment;
@@ -288,6 +386,7 @@ public abstract class Layout {
for (int lineNum = firstLine; lineNum <= lastLine; lineNum++) {
int start = previousLineEnd;
previousLineEnd = getLineStart(lineNum + 1);
final boolean justify = isJustificationRequired(lineNum);
int end = getLineVisibleEnd(lineNum, start, previousLineEnd);
int ltop = previousLineBottom;
@@ -386,34 +485,42 @@ public abstract class Layout {
}
int x;
final int indentWidth;
if (align == Alignment.ALIGN_NORMAL) {
if (dir == DIR_LEFT_TO_RIGHT) {
x = left + getIndentAdjust(lineNum, Alignment.ALIGN_LEFT);
indentWidth = getIndentAdjust(lineNum, Alignment.ALIGN_LEFT);
x = left + indentWidth;
} else {
x = right + getIndentAdjust(lineNum, Alignment.ALIGN_RIGHT);
indentWidth = -getIndentAdjust(lineNum, Alignment.ALIGN_RIGHT);
x = right - indentWidth;
}
} else {
int max = (int)getLineExtent(lineNum, tabStops, false);
if (align == Alignment.ALIGN_OPPOSITE) {
if (dir == DIR_LEFT_TO_RIGHT) {
x = right - max + getIndentAdjust(lineNum, Alignment.ALIGN_RIGHT);
indentWidth = -getIndentAdjust(lineNum, Alignment.ALIGN_RIGHT);
x = right - max - indentWidth;
} else {
x = left - max + getIndentAdjust(lineNum, Alignment.ALIGN_LEFT);
indentWidth = getIndentAdjust(lineNum, Alignment.ALIGN_LEFT);
x = left - max + indentWidth;
}
} else { // Alignment.ALIGN_CENTER
indentWidth = getIndentAdjust(lineNum, Alignment.ALIGN_CENTER);
max = max & ~1;
x = ((right + left - max) >> 1) +
getIndentAdjust(lineNum, Alignment.ALIGN_CENTER);
x = ((right + left - max) >> 1) + indentWidth;
}
}
paint.setHyphenEdit(getHyphen(lineNum));
Directions directions = getLineDirections(lineNum);
if (directions == DIRS_ALL_LEFT_TO_RIGHT && !mSpannedText && !hasTab) {
if (directions == DIRS_ALL_LEFT_TO_RIGHT && !mSpannedText && !hasTab && !justify) {
// XXX: assumes there's nothing additional to be done
canvas.drawText(buf, start, end, x, lbaseline, paint);
} else {
tl.set(paint, buf, start, end, dir, directions, hasTab, tabStops);
if (justify) {
tl.justify(right - left - indentWidth);
}
tl.draw(canvas, x, ltop, lbaseline, lbottom);
}
paint.setHyphenEdit(0);
@@ -1094,6 +1201,9 @@ public abstract class Layout {
TextLine tl = TextLine.obtain();
mPaint.setHyphenEdit(getHyphen(line));
tl.set(mPaint, mText, start, end, dir, directions, hasTabs, tabStops);
if (isJustificationRequired(line)) {
tl.justify(getJustifyWidth(line));
}
float width = tl.metrics(null);
mPaint.setHyphenEdit(0);
TextLine.recycle(tl);
@@ -1118,6 +1228,9 @@ public abstract class Layout {
TextLine tl = TextLine.obtain();
mPaint.setHyphenEdit(getHyphen(line));
tl.set(mPaint, mText, start, end, dir, directions, hasTabs, tabStops);
if (isJustificationRequired(line)) {
tl.justify(getJustifyWidth(line));
}
float width = tl.metrics(null);
mPaint.setHyphenEdit(0);
TextLine.recycle(tl);
@@ -1303,10 +1416,7 @@ public abstract class Layout {
return end - 1;
}
// Note: keep this in sync with Minikin LineBreaker::isLineEndSpace()
if (!(ch == ' ' || ch == '\t' || ch == 0x1680 ||
(0x2000 <= ch && ch <= 0x200A && ch != 0x2007) ||
ch == 0x205F || ch == 0x3000)) {
if (!TextLine.isLineEndSpace(ch)) {
break;
}
@@ -2086,6 +2196,7 @@ public abstract class Layout {
private boolean mSpannedText;
private TextDirectionHeuristic mTextDir;
private SpanSet<LineBackgroundSpan> mLineBackgroundSpans;
private boolean mJustify;
public static final int DIR_LEFT_TO_RIGHT = 1;
public static final int DIR_RIGHT_TO_LEFT = -1;

View File

@@ -94,6 +94,7 @@ public class StaticLayout extends Layout {
b.mMaxLines = Integer.MAX_VALUE;
b.mBreakStrategy = Layout.BREAK_STRATEGY_SIMPLE;
b.mHyphenationFrequency = Layout.HYPHENATION_FREQUENCY_NONE;
b.mJustify = false;
b.mMeasuredText = MeasuredText.obtain();
return b;
@@ -319,6 +320,17 @@ public class StaticLayout extends Layout {
return this;
}
/**
* Enables or disables paragraph justification. The default value is disabled (false).
*
* @param justify true for enabling and false for disabling paragraph justification.
* @return this builder, useful for chaining.
*/
public Builder setJustify(boolean justify) {
mJustify = justify;
return this;
}
/**
* Measurement and break iteration is done in native code. The protocol for using
* the native code is as follows.
@@ -404,6 +416,7 @@ public class StaticLayout extends Layout {
int mHyphenationFrequency;
int[] mLeftIndents;
int[] mRightIndents;
boolean mJustify;
Paint.FontMetricsInt mFontMetricsInt = new Paint.FontMetricsInt();
@@ -557,6 +570,7 @@ public class StaticLayout extends Layout {
mLeftIndents = b.mLeftIndents;
mRightIndents = b.mRightIndents;
setJustify(b.mJustify);
generate(b, b.mIncludePad, b.mIncludePad);
}
@@ -676,7 +690,8 @@ public class StaticLayout extends Layout {
nSetupParagraph(b.mNativePtr, chs, paraEnd - paraStart,
firstWidth, firstWidthLineCount, restWidth,
variableTabStops, TAB_INCREMENT, b.mBreakStrategy, b.mHyphenationFrequency);
variableTabStops, TAB_INCREMENT, b.mBreakStrategy, b.mHyphenationFrequency,
b.mJustify);
if (mLeftIndents != null || mRightIndents != null) {
// TODO(raph) performance: it would be better to do this once per layout rather
// than once per paragraph, but that would require a change to the native
@@ -1284,7 +1299,8 @@ public class StaticLayout extends Layout {
// Set up paragraph text and settings; done as one big method to minimize jni crossings
private static native void nSetupParagraph(long nativePtr, char[] text, int length,
float firstWidth, int firstWidthLineCount, float restWidth,
int[] variableTabStops, int defaultTabStop, int breakStrategy, int hyphenationFrequency);
int[] variableTabStops, int defaultTabStop, int breakStrategy, int hyphenationFrequency,
boolean isJustified);
private static native float nAddStyleRun(long nativePtr, long nativePaint,
long nativeTypeface, int start, int end, boolean isRtl);

View File

@@ -54,6 +54,10 @@ class TextLine {
private char[] mChars;
private boolean mCharsValid;
private Spanned mSpanned;
// Additional width of whitespace for justification. This value is per whitespace, thus
// the line width will increase by mAddedWidth x (number of stretchable whitespaces).
private float mAddedWidth;
private final TextPaint mWorkPaint = new TextPaint();
private final SpanSet<MetricAffectingSpan> mMetricAffectingSpanSpanSet =
new SpanSet<MetricAffectingSpan>(MetricAffectingSpan.class);
@@ -177,6 +181,25 @@ class TextLine {
}
}
mTabs = tabStops;
mAddedWidth = 0;
}
/**
* Justify the line to the given width.
*/
void justify(float justifyWidth) {
int end = mLen;
while (end > 0 && isLineEndSpace(mText.charAt(mStart + end - 1))) {
end--;
}
final int spaces = countStretchableSpaces(0, end);
if (spaces == 0) {
// There are no stretchable spaces, so we can't help the justification by adding any
// width.
return;
}
final float width = Math.abs(measure(end, false, null));
mAddedWidth = (justifyWidth - width) / spaces;
}
/**
@@ -595,6 +618,7 @@ class TextLine {
TextPaint wp = mWorkPaint;
wp.set(mPaint);
wp.setWordSpacing(mAddedWidth);
int spanStart = runStart;
int spanLimit;
@@ -695,6 +719,7 @@ class TextLine {
Canvas c, float x, int top, int y, int bottom,
FontMetricsInt fmi, boolean needWidth, int offset) {
wp.setWordSpacing(mAddedWidth);
// Get metrics first (even for empty strings or "0" width runs)
if (fmi != null) {
expandMetricsFromPaint(fmi, wp);
@@ -981,5 +1006,34 @@ class TextLine {
return TabStops.nextDefaultStop(h, TAB_INCREMENT);
}
private boolean isStretchableWhitespace(int ch) {
// TODO: Support other stretchable whitespace. (Bug: 34013491)
return ch == 0x0020 || ch == 0x00A0;
}
private int nextStretchableSpace(int start, int end) {
for (int i = start; i < end; i++) {
final char c = mCharsValid ? mChars[i] : mText.charAt(i + mStart);
if (isStretchableWhitespace(c)) return i;
}
return end;
}
/* Return the number of spaces in the text line, for the purpose of justification */
private int countStretchableSpaces(int start, int end) {
int count = 0;
for (int i = start; i < end; i = nextStretchableSpace(i + 1, end)) {
count++;
}
return count;
}
// Note: keep this in sync with Minikin LineBreaker::isLineEndSpace()
public static boolean isLineEndSpace(char ch) {
return ch == ' ' || ch == '\t' || ch == 0x1680
|| (0x2000 <= ch && ch <= 0x200A && ch != 0x2007)
|| ch == 0x205F || ch == 0x3000;
}
private static final int TAB_INCREMENT = 20;
}

View File

@@ -596,6 +596,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
private int mBreakStrategy;
private int mHyphenationFrequency;
private boolean mJustify;
private int mMaximum = Integer.MAX_VALUE;
private int mMaxMode = LINES;
@@ -769,6 +770,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
String fontFeatureSettings = null;
mBreakStrategy = Layout.BREAK_STRATEGY_SIMPLE;
mHyphenationFrequency = Layout.HYPHENATION_FREQUENCY_NONE;
mJustify = false;
final Resources.Theme theme = context.getTheme();
@@ -3297,6 +3299,29 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
return mHyphenationFrequency;
}
/**
* Enables or disables full justification. The default value is false.
*
* @see #getJustify()
*/
public void setJustify(boolean justify) {
mJustify = justify;
if (mLayout != null) {
nullLayouts();
requestLayout();
invalidate();
}
}
/**
* @return true if currently paragraph justification is enabled.
*
* @see #setJustify(boolean)
*/
public boolean getJustify() {
return mJustify;
}
/**
* Sets font feature settings. The format is the same as the CSS
* font-feature-settings attribute:
@@ -7170,6 +7195,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
.setIncludePad(mIncludePad)
.setBreakStrategy(mBreakStrategy)
.setHyphenationFrequency(mHyphenationFrequency)
.setJustify(mJustify)
.setMaxLines(mMaxMode == LINES ? mMaximum : Integer.MAX_VALUE);
if (shouldEllipsize) {
builder.setEllipsize(mEllipsize)
@@ -7211,7 +7237,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
if (mText instanceof Spannable) {
result = new DynamicLayout(mText, mTransformed, mTextPaint, wantWidth,
alignment, mTextDir, mSpacingMult, mSpacingAdd, mIncludePad,
mBreakStrategy, mHyphenationFrequency,
mBreakStrategy, mHyphenationFrequency, mJustify,
getKeyListener() == null ? effectiveEllipsize : null, ellipsisWidth);
} else {
if (boring == UNKNOWN_BORING) {
@@ -7261,6 +7287,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
.setIncludePad(mIncludePad)
.setBreakStrategy(mBreakStrategy)
.setHyphenationFrequency(mHyphenationFrequency)
.setJustify(mJustify)
.setMaxLines(mMaxMode == LINES ? mMaximum : Integer.MAX_VALUE);
if (shouldEllipsize) {
builder.setEllipsize(effectiveEllipsize)

View File

@@ -54,7 +54,8 @@ static JLineBreaksID gLineBreaks_fieldID;
// hyphenFrequency)
static void nSetupParagraph(JNIEnv* env, jclass, jlong nativePtr, jcharArray text, jint length,
jfloat firstWidth, jint firstWidthLineLimit, jfloat restWidth,
jintArray variableTabStops, jint defaultTabStop, jint strategy, jint hyphenFrequency) {
jintArray variableTabStops, jint defaultTabStop, jint strategy, jint hyphenFrequency,
jboolean isJustified) {
minikin::LineBreaker* b = reinterpret_cast<minikin::LineBreaker*>(nativePtr);
b->resize(length);
env->GetCharArrayRegion(text, 0, length, b->buffer());
@@ -68,6 +69,7 @@ static void nSetupParagraph(JNIEnv* env, jclass, jlong nativePtr, jcharArray tex
}
b->setStrategy(static_cast<minikin::BreakStrategy>(strategy));
b->setHyphenationFrequency(static_cast<minikin::HyphenationFrequency>(hyphenFrequency));
b->setJustified(isJustified);
}
static void recycleCopy(JNIEnv* env, jobject recycle, jintArray recycleBreaks,
@@ -190,7 +192,7 @@ static const JNINativeMethod gMethods[] = {
{"nFinishBuilder", "(J)V", (void*) nFinishBuilder},
{"nLoadHyphenator", "(Ljava/nio/ByteBuffer;I)J", (void*) nLoadHyphenator},
{"nSetLocale", "(JLjava/lang/String;J)V", (void*) nSetLocale},
{"nSetupParagraph", "(J[CIFIF[IIII)V", (void*) nSetupParagraph},
{"nSetupParagraph", "(J[CIFIF[IIIIZ)V", (void*) nSetupParagraph},
{"nSetIndents", "(J[I)V", (void*) nSetIndents},
{"nAddStyleRun", "(JJJIIZ)F", (void*) nAddStyleRun},
{"nAddMeasuredRun", "(JII[F)V", (void*) nAddMeasuredRun},