Correct start- and end-point calculation.

Implement lengthAdjust and textLength attribute. Fix start and end rendering cutoff for text-anchor on a path. Refactor extractStroke. Improve docs.
This commit is contained in:
Mikael Sand
2017-07-27 00:06:46 +03:00
parent fab3afb03a
commit 36595c91e2
3 changed files with 241 additions and 55 deletions
@@ -80,7 +80,7 @@ class TSpanShadowNode extends TextShadowNode {
setupTextPath();
pushGlyphContext();
mCache = getLinePath(mContent, paint);
mCache = getLinePath(mContent, paint, canvas);
popGlyphContext();
mCache.computeBounds(new RectF(), true);
@@ -88,7 +88,7 @@ class TSpanShadowNode extends TextShadowNode {
return mCache;
}
private Path getLinePath(String line, Paint paint) {
private Path getLinePath(String line, Paint paint, Canvas canvas) {
final int length = line.length();
final Path path = new Path();
@@ -96,10 +96,22 @@ class TSpanShadowNode extends TextShadowNode {
return path;
}
double distance = 0;
PathMeasure pm = null;
final boolean hasTextPath = textPath != null;
if (hasTextPath) {
pm = new PathMeasure(textPath.getPath(), false);
distance = pm.getLength();
if (distance == 0) {
return path;
}
}
GlyphContext gc = getTextRootGlyphContext();
FontData font = gc.getFont();
applyTextPropertiesToPaint(paint, font);
GlyphPathBag bag = new GlyphPathBag(paint);
final char[] chars = line.toCharArray();
/*
Determine the startpoint-on-the-path for the first glyph using attribute startOffset
@@ -124,21 +136,80 @@ class TSpanShadowNode extends TextShadowNode {
is adjusted to take into account various horizontal alignment text properties and
attributes, such as a dx attribute value on a tspan element.
*/
final TextAnchor textAnchor = font.textAnchor;
final double textMeasure = paint.measureText(line);
double offset = getTextAnchorOffset(font.textAnchor, textMeasure);
double offset = getTextAnchorOffset(textAnchor, textMeasure);
final int side = textPath.getSide() == TextPathSide.right ? -1 : 1;
double distance = 0;
PathMeasure pm = null;
boolean sharpMidline = false;
if (textPath != null) {
sharpMidline = textPath.getMidLine() == sharp;
pm = new PathMeasure(textPath.getPath(), false);
distance = pm.getLength();
if (distance == 0) {
return path;
}
offset += getAbsoluteStartOffset(textPath.getStartOffset(), distance, gc.getFontSize());
int side = 1;
double endOfRendering = 0;
double startOfRendering = 0;
boolean sharpMidLine = false;
final double fontSize = gc.getFontSize();
if (hasTextPath) {
sharpMidLine = textPath.getMidLine() == sharp;
/*
Name
side
Value
left | right
initial value
left
Animatable
yes
Determines the side of the path the text is placed on
(relative to the path direction).
Specifying a value of right effectively reverses the path.
Added in SVG 2 to allow text either inside or outside closed subpaths
and basic shapes (e.g. rectangles, circles, and ellipses).
Adding 'side' was resolved at the Sydney (2015) meeting.
*/
side = textPath.getSide() == TextPathSide.right ? -1 : 1;
/*
Name
startOffset
Value
<length> | <percentage> | <number>
initial value
0
Animatable
yes
An offset from the start of the path for the initial current text position,
calculated using the user agent's distance along the path algorithm,
after converting the path to the textPath element's coordinate system.
If a <length> other than a percentage is given, then the startOffset
represents a distance along the path measured in the current user coordinate
system for the textPath element.
If a percentage is given, then the startOffset represents a percentage
distance along the entire path. Thus, startOffset="0%" indicates the start
point of the path and startOffset="100%" indicates the end point of the path.
Negative values and values larger than the path length (e.g. 150%) are allowed.
Any typographic characters with mid-points that are not on the path are not rendered
For paths consisting of a single closed subpath (including an equivalent path for a
basic shape), typographic characters are rendered along one complete circuit of the
path. The text is aligned as determined by the text-anchor property to a position
along the path set by the startOffset attribute.
For the start (end) value, the text is rendered from the start (end) of the line
until the initial position along the path is reached again.
For the middle, the text is rendered from the middle point in both directions until
a point on the path equal distance in both directions from the initial position on
the path is reached.
*/
final double halfPathDistance = distance / 2;
offset += getAbsoluteStartOffset(textPath.getStartOffset(), distance, fontSize);
startOfRendering = -offset + (textAnchor == TextAnchor.middle ? -halfPathDistance : 0);
endOfRendering = startOfRendering + distance;
/*
TextPathSpacing spacing = textPath.getSpacing();
if (spacing == TextPathSpacing.auto) {
@@ -148,8 +219,38 @@ class TSpanShadowNode extends TextShadowNode {
*/
}
double renderMethodScaling = getRenderMethodScaling(textMeasure, distance);
double scaledDirection = renderMethodScaling * side;
/*
Name
method
Value
align | stretch
initial value
align
Animatable
yes
Indicates the method by which text should be rendered along the path.
A value of align indicates that the typographic character should be rendered using
simple 2×3 matrix transformations such that there is no stretching/warping of the
typographic characters. Typically, supplemental rotation, scaling and translation
transformations are done for each typographic characters to be rendered.
As a result, with align, in fonts where the typographic characters are designed to be
connected (e.g., cursive fonts), the connections may not align properly when text is
rendered along a path.
A value of stretch indicates that the typographic character outlines will be converted
into paths, and then all end points and control points will be adjusted to be along the
perpendicular vectors from the path, thereby stretching and possibly warping the glyphs.
With this approach, connected typographic characters, such as in cursive scripts,
will maintain their connections. (Non-vertical straight path segments should be
converted to Bézier curves in such a way that horizontal straight paths have an
(approximately) constant offset from the path along which the typographic characters
are rendered.)
TODO implement stretch
*/
/*
*
@@ -170,13 +271,72 @@ class TSpanShadowNode extends TextShadowNode {
* or decrease the space between typographic character units in order to justify text.
*
* */
String previous = "";
double previousCharWidth = 0;
double kerning = font.kerning;
final double wordSpacing = font.wordSpacing;
double wordSpacing = font.wordSpacing;
final double letterSpacing = font.letterSpacing;
final boolean autoKerning = !font.manualKerning;
/*
Name Value Initial value Animatable
textLength <length> | <percentage> | <number> See below yes
The author's computation of the total sum of all of the advance values that correspond
to character data within this element, including the advance value on the glyph
(horizontal or vertical), the effect of properties letter-spacing and word-spacing and
adjustments due to attributes dx and dy on this text or tspan element or any
descendants. This value is used to calibrate the user agent's own calculations with
that of the author.
The purpose of this attribute is to allow the author to achieve exact alignment,
in visual rendering order after any bidirectional reordering, for the first and
last rendered glyphs that correspond to this element; thus, for the last rendered
character (in visual rendering order after any bidirectional reordering),
any supplemental inter-character spacing beyond normal glyph advances are ignored
(in most cases) when the user agent determines the appropriate amount to expand/compress
the text string to fit within a length of textLength.
If attribute textLength is specified on a given element and also specified on an
ancestor, the adjustments on all character data within this element are controlled by
the value of textLength on this element exclusively, with the possible side-effect
that the adjustment ratio for the contents of this element might be different than the
adjustment ratio used for other content that shares the same ancestor. The user agent
must assume that the total advance values for the other content within that ancestor is
the difference between the advance value on that ancestor and the advance value for
this element.
This attribute is not intended for use to obtain effects such as shrinking or
expanding text.
A negative value is an error (see Error processing).
The textLength attribute is only applied when the wrapping area is not defined by the
shape-inside or the inline-size properties. It is also not applied for any text or
tspan element that has forced line breaks (due to a white-space value of pre or
pre-line).
If the attribute is not specified anywhere within a text element, the effect is as if
the author's computation exactly matched the value calculated by the user agent;
thus, no advance adjustments are made.
*/
double scaleSpacingAndGlyphs = 1;
if (mTextLength != null) {
final double author = PropHelper.fromRelative(mTextLength, canvas.getWidth(), 0, mScale, fontSize);
if (author < 0) {
throw new IllegalArgumentException("Negative textLength value");
}
switch (mLengthAdjust) {
default:
case spacing:
int numSpaces = getNumSpaces(length, chars);
wordSpacing += (author - textMeasure) / numSpaces;
break;
case spacingAndGlyphs:
scaleSpacingAndGlyphs = author / textMeasure;
break;
}
}
final double scaledDirection = scaleSpacingAndGlyphs * side;
/*
https://developer.mozilla.org/en/docs/Web/CSS/vertical-align
https://developer.apple.com/fonts/TrueType-Reference-Manual/RM06/Chap6bsln.html
@@ -202,15 +362,16 @@ class TSpanShadowNode extends TextShadowNode {
Neither 'text-before-edge' nor 'text-after-edge' should be used with the vertical-align property.
*/
Paint.FontMetrics fm = paint.getFontMetrics();
double top = -fm.top;
double bottom = fm.bottom;
double ascenderHeight = -fm.ascent;
double descenderDepth = fm.descent;
double totalHeight = top + bottom;
final Paint.FontMetrics fm = paint.getFontMetrics();
final double top = -fm.top;
final double bottom = fm.bottom;
final double ascenderHeight = -fm.ascent;
final double descenderDepth = fm.descent;
final double totalHeight = top + bottom;
double baselineShift = 0;
if (mAlignmentBaseline != null) {
// TODO alignment-baseline, test / verify behavior
// TODO get per glyph baselines from font baseline table, for high-precision alignment
switch (mAlignmentBaseline) {
// https://wiki.apache.org/xmlgraphics-fop/LineLayout/AlignmentHandling
default:
@@ -296,14 +457,15 @@ class TSpanShadowNode extends TextShadowNode {
}
}
Matrix start = new Matrix();
Matrix mid = new Matrix();
Matrix end = new Matrix();
final Matrix start = new Matrix();
final Matrix mid = new Matrix();
final Matrix end = new Matrix();
float[] startPointMatrixData = new float[9];
float[] endPointMatrixData = new float[9];
final float[] startPointMatrixData = new float[9];
final float[] endPointMatrixData = new float[9];
final char[] chars = line.toCharArray();
String previous = "";
double previousCharWidth = 0;
for (int index = 0; index < length; index++) {
char currentChar = chars[index];
String current = String.valueOf(currentChar);
@@ -312,7 +474,7 @@ class TSpanShadowNode extends TextShadowNode {
Determine the glyph's charwidth (i.e., the amount which the current text position
advances horizontally when the glyph is drawn using horizontal text layout).
*/
double charWidth = paint.measureText(current) * renderMethodScaling;
double charWidth = paint.measureText(current) * scaleSpacingAndGlyphs;
/*
For each subsequent glyph, set a new startpoint-on-the-path as the previous
@@ -324,7 +486,7 @@ class TSpanShadowNode extends TextShadowNode {
using the user agent's distance along the path algorithm.
*/
if (autoKerning) {
double bothCharsWidth = paint.measureText(previous + current) * renderMethodScaling;
double bothCharsWidth = paint.measureText(previous + current) * scaleSpacingAndGlyphs;
kerning = bothCharsWidth - previousCharWidth - charWidth;
previousCharWidth = charWidth;
previous = current;
@@ -340,31 +502,32 @@ class TSpanShadowNode extends TextShadowNode {
double dy = gc.nextDeltaY();
double r = gc.nextRotation();
advance = advance * side;
charWidth = charWidth * side;
double cursor = offset + (x + dx) * side;
double startpoint = cursor - charWidth;
double startPoint = cursor - advance;
if (textPath != null) {
if (hasTextPath) {
/*
Determine the point on the curve which is charwidth distance along the path from
the startpoint-on-the-path for this glyph, calculated using the user agent's
distance along the path algorithm. This point is the endpoint-on-the-path for
the glyph.
*/
double endpoint = cursor;
double endPoint = startPoint + charWidth;
/*
Determine the midpoint-on-the-path, which is the point on the path which is
"halfway" (user agents can choose either a distance calculation or a parametric
calculation) between the startpoint-on-the-path and the endpoint-on-the-path.
*/
double halfway = charWidth / 2;
double midpoint = startpoint + halfway;
double halfWay = charWidth / 2;
double midPoint = startPoint + halfWay;
// Glyphs whose midpoint-on-the-path are off the path are not rendered.
if (midpoint > distance) {
break;
} else if (midpoint < 0) {
if (midPoint > endOfRendering) {
continue;
} else if (midPoint < startOfRendering) {
continue;
}
@@ -381,8 +544,7 @@ class TSpanShadowNode extends TextShadowNode {
which doesn't bend text smoothly along a right angle curve, (like Edge does)
but keeps the mid-line orthogonal to the mid-point tangent at all times instead.
*/
assert pm != null;
if (startpoint < 0 || endpoint > distance || sharpMidline) {
if (startPoint < 0 || endPoint > distance || sharpMidLine) {
/*
In the calculation above, if either the startpoint-on-the-path
or the endpoint-on-the-path is off the end of the path,
@@ -399,11 +561,11 @@ class TSpanShadowNode extends TextShadowNode {
endpoint-on-the-path can still be calculated.
*/
final int flags = POSITION_MATRIX_FLAG | TANGENT_MATRIX_FLAG;
pm.getMatrix((float) midpoint, mid, flags);
pm.getMatrix((float) midPoint, mid, flags);
} else {
pm.getMatrix((float) startpoint, start, POSITION_MATRIX_FLAG);
pm.getMatrix((float) midpoint, mid, POSITION_MATRIX_FLAG);
pm.getMatrix((float) endpoint, end, POSITION_MATRIX_FLAG);
pm.getMatrix((float) startPoint, start, POSITION_MATRIX_FLAG);
pm.getMatrix((float) midPoint, mid, POSITION_MATRIX_FLAG);
pm.getMatrix((float) endPoint, end, POSITION_MATRIX_FLAG);
start.getValues(startPointMatrixData);
end.getValues(endPointMatrixData);
@@ -428,11 +590,11 @@ class TSpanShadowNode extends TextShadowNode {
Align the glyph vertically relative to the midpoint-on-the-path based on property
alignment-baseline and any specified values for attribute dy on a tspan element.
*/
mid.preTranslate((float) -halfway, (float) (dy - baselineShift));
mid.preScale((float) scaledDirection, (float) scaledDirection);
mid.preTranslate((float) -halfWay, (float) (dy + baselineShift));
mid.preScale((float) scaledDirection, (float) side);
mid.postTranslate(0, (float) y);
} else {
mid.setTranslate((float) startpoint, (float) (y + dy));
mid.setTranslate((float) startPoint, (float) (y + dy));
}
mid.preRotate((float) r);
@@ -445,11 +607,14 @@ class TSpanShadowNode extends TextShadowNode {
return path;
}
private double getRenderMethodScaling(double textMeasure, double distance) {
if (textPath != null && textPath.getMethod() == TextPathMethod.stretch) {
return distance / textMeasure;
private int getNumSpaces(int length, char[] chars) {
int numSpaces = 0;
for (int index = 0; index < length; index++) {
if (chars[index] == ' ') {
numSpaces++;
}
}
return 1;
return numSpaces;
}
private double getAbsoluteStartOffset(String startOffset, double distance, double fontSize) {
@@ -0,0 +1,7 @@
package com.horcrux.svg;
enum TextLengthAdjust
{
spacing,
spacingAndGlyphs
}
@@ -24,6 +24,8 @@ import javax.annotation.Nullable;
*/
class TextShadowNode extends GroupShadowNode {
String mTextLength = null;
TextLengthAdjust mLengthAdjust = TextLengthAdjust.spacing;
AlignmentBaseline mAlignmentBaseline;
private @Nullable ReadableArray mPositionX;
private @Nullable ReadableArray mPositionY;
@@ -31,6 +33,18 @@ class TextShadowNode extends GroupShadowNode {
private @Nullable ReadableArray mDeltaX;
private @Nullable ReadableArray mDeltaY;
@ReactProp(name = "textLength")
public void setmTextLength(@Nullable String length) {
mTextLength = length;
markUpdated();
}
@ReactProp(name = "lengthAdjust")
public void setLengthAdjust(@Nullable String adjustment) {
mLengthAdjust = TextLengthAdjust.valueOf(adjustment);
markUpdated();
}
@ReactProp(name = "alignmentBaseline")
public void setMethod(@Nullable String alignment) {
mAlignmentBaseline = AlignmentBaseline.valueOf(alignment);