Commit e4e02f91 by Oliver Woodman

Further improve WebVTT parser according to WebVTT spec

parent 71f542f7
WEBVTT
00:00.000 --> 00:01.234
This is the first subtitle.
00:02.345 --> 00:03.456
This is the second subtitle.
......@@ -8,7 +8,9 @@ with multiple lines
00:00.000 --> 00:01.234
This is the first subtitle.
NOTE Single line comment
NOTE Single line comment with a space
NOTE Single line comment with a tab
2
00:02.345 --> 00:03.456
......
......@@ -16,7 +16,13 @@ NOTE Line as percentage and line alignment
00:04.000 --> 00:05.000 line:45%,end align:middle size:35%
This is the third subtitle.
NOTE Line as absolute negative number and without line alignment
NOTE Line as absolute negative number and without line alignment.
00:06.000 --> 00:07.000 line:-10 align:middle
This is the fourth subtitle.
NOTE The positioning alignment should be inherited from align.
00:07.000 --> 00:08.000 position:10% align:end size:10%
This is the fifth subtitle.
00:06.000 --> 00:07.000 line:-10 align:middle size:35%
This is the forth subtitle.
......@@ -23,30 +23,117 @@ import android.text.Layout.Alignment;
public class Cue {
/**
* Used by some methods to indicate that no value is set.
* An unset position or width.
*/
public static final int UNSET_VALUE = -1;
public static final float DIMEN_UNSET = Float.MIN_VALUE;
/**
* An unset anchor or line type value.
*/
public static final int TYPE_UNSET = Integer.MIN_VALUE;
/**
* Anchors the left (for horizontal positions) or top (for vertical positions) edge of the cue
* box.
*/
public static final int ANCHOR_TYPE_START = 0;
/**
* Anchors the middle of the cue box.
*/
public static final int ANCHOR_TYPE_MIDDLE = 1;
/**
* Anchors the right (for horizontal positions) or bottom (for vertical positions) edge of the cue
* box.
*/
public static final int ANCHOR_TYPE_END = 2;
/**
* Value for {@link #lineType} when {@link #line} is a fractional position.
*/
public static final int LINE_TYPE_FRACTION = 0;
/**
* Value for {@link #lineType} when {@link #line} is a line number.
*/
public static final int LINE_TYPE_NUMBER = 1;
/**
* The cue text. Note the {@link CharSequence} may be decorated with styling spans.
*/
public final CharSequence text;
public final int line;
public final int position;
public final Alignment alignment;
public final int size;
/**
* The alignment of the cue text within the cue box.
*/
public final Alignment textAlignment;
/**
* The position of the {@link #lineAnchor} of the cue box within the viewport in the direction
* orthogonal to the writing direction, or {@link #DIMEN_UNSET}. When set, the interpretation of
* the value depends on the value of {@link #lineType}.
* <p>
* For horizontal text and {@link #lineType} equal to {@link #LINE_TYPE_FRACTION}, this is the
* fractional vertical position relative to the top of the viewport.
*/
public final float line;
/**
* The type of the {@link #line} value.
* <p>
* {@link #LINE_TYPE_FRACTION} indicates that {@link #line} is a fractional position within the
* viewport.
* <p>
* {@link #LINE_TYPE_NUMBER} indicates that {@link #line} is a line number, where the size of each
* line is taken to be the size of the first line of the cue. When {@link #line} is greater than
* or equal to 0, lines count from the start of the viewport (the first line is numbered 0). When
* {@link #line} is negative, lines count from the end of the viewport (the last line is numbered
* -1). For horizontal text the size of the first line of the cue is its height, and the start
* and end of the viewport are the top and bottom respectively.
*/
public final int lineType;
/**
* The cue box anchor positioned by {@link #line}. One of {@link #ANCHOR_TYPE_START},
* {@link #ANCHOR_TYPE_MIDDLE}, {@link #ANCHOR_TYPE_END} and {@link #TYPE_UNSET}.
* <p>
* For the normal case of horizontal text, {@link #ANCHOR_TYPE_START}, {@link #ANCHOR_TYPE_MIDDLE}
* and {@link #ANCHOR_TYPE_END} correspond to the top, middle and bottom of the cue box
* respectively.
*/
public final int lineAnchor;
/**
* The fractional position of the {@link #positionAnchor} of the cue box within the viewport in
* the direction orthogonal to {@link #line}, or {@link #DIMEN_UNSET}.
* <p>
* For horizontal text, this is the horizontal position relative to the left of the viewport. Note
* that positioning is relative to the left of the viewport even in the case of right-to-left
* text.
*/
public final float position;
/**
* The cue box anchor positioned by {@link #position}. One of {@link #ANCHOR_TYPE_START},
* {@link #ANCHOR_TYPE_MIDDLE}, {@link #ANCHOR_TYPE_END} and {@link #TYPE_UNSET}.
* <p>
* For the normal case of horizontal text, {@link #ANCHOR_TYPE_START}, {@link #ANCHOR_TYPE_MIDDLE}
* and {@link #ANCHOR_TYPE_END} correspond to the left, middle and right of the cue box
* respectively.
*/
public final int positionAnchor;
/**
* The size of the cue box in the writing direction specified as a fraction of the viewport size
* in that direction, or {@link #DIMEN_UNSET}.
*/
public final float size;
public Cue() {
this(null);
}
public Cue(CharSequence text) {
this(text, UNSET_VALUE, UNSET_VALUE, null, UNSET_VALUE);
this(text, null, DIMEN_UNSET, TYPE_UNSET, TYPE_UNSET, DIMEN_UNSET, TYPE_UNSET, DIMEN_UNSET);
}
public Cue(CharSequence text, int line, int position, Alignment alignment, int size) {
public Cue(CharSequence text, Alignment textAlignment, float line, int lineType,
int lineAnchor, float position, int positionAnchor, float size) {
this.text = text;
this.textAlignment = textAlignment;
this.line = line;
this.lineType = lineType;
this.lineAnchor = lineAnchor;
this.position = position;
this.alignment = alignment;
this.positionAnchor = positionAnchor;
this.size = size;
}
......
......@@ -63,8 +63,13 @@ import android.util.Log;
// Previous input variables.
private CharSequence cueText;
private int cuePosition;
private Alignment cueAlignment;
private Alignment cueTextAlignment;
private float cueLine;
private int cueLineType;
private int cueLineAnchor;
private float cuePosition;
private int cuePositionAnchor;
private float cueSize;
private boolean applyEmbeddedStyles;
private int foregroundColor;
private int backgroundColor;
......@@ -120,7 +125,7 @@ import android.util.Log;
* @param style The style to use when drawing the cue text.
* @param textSizePx The text size to use when drawing the cue text, in pixels.
* @param bottomPaddingFraction The bottom padding fraction to apply when {@link Cue#line} is
* {@link Cue#UNSET_VALUE}, as a fraction of the viewport height
* {@link Cue#DIMEN_UNSET}, as a fraction of the viewport height
* @param canvas The canvas into which to draw.
* @param cueBoxLeft The left position of the enclosing cue box.
* @param cueBoxTop The top position of the enclosing cue box.
......@@ -140,8 +145,13 @@ import android.util.Log;
cueText = cueText.toString();
}
if (areCharSequencesEqual(this.cueText, cueText)
&& Util.areEqual(this.cueTextAlignment, cue.textAlignment)
&& this.cueLine == cue.line
&& this.cueLineType == cue.lineType
&& Util.areEqual(this.cueLineAnchor, cue.lineAnchor)
&& this.cuePosition == cue.position
&& Util.areEqual(this.cueAlignment, cue.alignment)
&& Util.areEqual(this.cuePositionAnchor, cue.positionAnchor)
&& this.cueSize == cue.size
&& this.applyEmbeddedStyles == applyEmbeddedStyles
&& this.foregroundColor == style.foregroundColor
&& this.backgroundColor == style.backgroundColor
......@@ -161,8 +171,13 @@ import android.util.Log;
}
this.cueText = cueText;
this.cueTextAlignment = cue.textAlignment;
this.cueLine = cue.line;
this.cueLineType = cue.lineType;
this.cueLineAnchor = cue.lineAnchor;
this.cuePosition = cue.position;
this.cueAlignment = cue.alignment;
this.cuePositionAnchor = cue.positionAnchor;
this.cueSize = cue.size;
this.applyEmbeddedStyles = applyEmbeddedStyles;
this.foregroundColor = style.foregroundColor;
this.backgroundColor = style.backgroundColor;
......@@ -182,16 +197,19 @@ import android.util.Log;
textPaint.setTextSize(textSizePx);
int textPaddingX = (int) (textSizePx * INNER_PADDING_RATIO + 0.5f);
int availableWidth = parentWidth - textPaddingX * 2;
if (cueSize != Cue.DIMEN_UNSET) {
availableWidth = (int) (availableWidth * cueSize);
}
if (availableWidth <= 0) {
Log.w(TAG, "Skipped drawing subtitle cue (insufficient space)");
return;
}
Alignment layoutAlignment = cueAlignment == null ? Alignment.ALIGN_CENTER : cueAlignment;
textLayout = new StaticLayout(cueText, textPaint, availableWidth, layoutAlignment, spacingMult,
Alignment textAlignment = cueTextAlignment == null ? Alignment.ALIGN_CENTER : cueTextAlignment;
textLayout = new StaticLayout(cueText, textPaint, availableWidth, textAlignment, spacingMult,
spacingAdd, true);
int textHeight = textLayout.getHeight();
int textWidth = 0;
int lineCount = textLayout.getLineCount();
......@@ -202,14 +220,13 @@ import android.util.Log;
int textLeft;
int textRight;
if (cue.position != Cue.UNSET_VALUE) {
if (cue.alignment == Alignment.ALIGN_OPPOSITE) {
textRight = (parentWidth * cue.position) / 100 + parentLeft;
textLeft = Math.max(textRight - textWidth, parentLeft);
} else {
textLeft = (parentWidth * cue.position) / 100 + parentLeft;
textRight = Math.min(textLeft + textWidth, parentRight);
}
if (cuePosition != Cue.DIMEN_UNSET) {
int anchorPosition = Math.round(parentWidth * cuePosition) + parentLeft;
textLeft = cuePositionAnchor == Cue.ANCHOR_TYPE_END ? anchorPosition - textWidth
: cuePositionAnchor == Cue.ANCHOR_TYPE_MIDDLE ? (anchorPosition * 2 - textWidth) / 2
: anchorPosition;
textLeft = Math.max(textLeft, parentLeft);
textRight = Math.min(textLeft + textWidth, parentRight);
} else {
textLeft = (parentWidth - textWidth) / 2;
textRight = textLeft + textWidth;
......@@ -217,12 +234,29 @@ import android.util.Log;
int textTop;
int textBottom;
if (cue.line != Cue.UNSET_VALUE) {
textTop = (parentHeight * cue.line) / 100 + parentTop;
if (cueLine != Cue.DIMEN_UNSET) {
int anchorPosition;
if (cueLineType == Cue.LINE_TYPE_FRACTION) {
anchorPosition = Math.round(parentHeight * cueLine) + parentTop;
} else {
// cueLineType == Cue.LINE_TYPE_NUMBER
int firstLineHeight = textLayout.getLineBottom(0) - textLayout.getLineTop(0);
if (cueLine >= 0) {
anchorPosition = Math.round(cueLine * firstLineHeight) + parentTop;
} else {
anchorPosition = Math.round(cueLine * firstLineHeight) + parentBottom;
}
}
textTop = cueLineAnchor == Cue.ANCHOR_TYPE_END ? anchorPosition - textHeight
: cueLineAnchor == Cue.ANCHOR_TYPE_MIDDLE ? (anchorPosition * 2 - textHeight) / 2
: anchorPosition;
textBottom = textTop + textHeight;
if (textBottom > parentBottom) {
textTop = parentBottom - textHeight;
textBottom = parentBottom;
} else if (textTop < parentTop) {
textTop = parentTop;
textBottom = parentTop + textHeight;
}
} else {
textTop = parentBottom - textHeight - (int) (parentHeight * bottomPaddingFraction);
......@@ -232,7 +266,7 @@ import android.util.Log;
textWidth = textRight - textLeft;
// Update the derived drawing variables.
this.textLayout = new StaticLayout(cueText, textPaint, textWidth, layoutAlignment, spacingMult,
this.textLayout = new StaticLayout(cueText, textPaint, textWidth, textAlignment, spacingMult,
spacingAdd, true);
this.textLeft = textLeft;
this.textTop = textTop;
......
......@@ -38,7 +38,7 @@ public final class SubtitleLayout extends View {
public static final float DEFAULT_TEXT_SIZE_FRACTION = 0.0533f;
/**
* The default bottom padding to apply when {@link Cue#line} is {@link Cue#UNSET_VALUE}, as a
* The default bottom padding to apply when {@link Cue#line} is {@link Cue#DIMEN_UNSET}, as a
* fraction of the viewport height.
*
* @see #setBottomPaddingFraction(float)
......@@ -174,7 +174,7 @@ public final class SubtitleLayout extends View {
}
/**
* Sets the bottom padding fraction to apply when {@link Cue#line} is {@link Cue#UNSET_VALUE},
* Sets the bottom padding fraction to apply when {@link Cue#line} is {@link Cue#DIMEN_UNSET},
* as a fraction of the view's remaining height after its top and bottom padding have been
* subtracted.
* <p>
......
......@@ -28,16 +28,17 @@ import android.text.Layout.Alignment;
public final long endTime;
public WebvttCue(CharSequence text) {
this(Cue.UNSET_VALUE, Cue.UNSET_VALUE, text);
this(0, 0, text);
}
public WebvttCue(long startTime, long endTime, CharSequence text) {
this(startTime, endTime, text, Cue.UNSET_VALUE, Cue.UNSET_VALUE, null, Cue.UNSET_VALUE);
this(startTime, endTime, text, null, Cue.DIMEN_UNSET, Cue.TYPE_UNSET, Cue.TYPE_UNSET,
Cue.DIMEN_UNSET, Cue.TYPE_UNSET, Cue.DIMEN_UNSET);
}
public WebvttCue(long startTime, long endTime, CharSequence text, int line, int position,
Alignment alignment, int size) {
super(text, line, position, alignment, size);
public WebvttCue(long startTime, long endTime, CharSequence text, Alignment textAlignment,
float line, int lineType, int lineAnchor, float position, int positionAnchor, float width) {
super(text, textAlignment, line, lineType, lineAnchor, position, positionAnchor, width);
this.startTime = startTime;
this.endTime = endTime;
}
......@@ -49,7 +50,7 @@ import android.text.Layout.Alignment;
* @return True if this cue should be placed in the default position; false otherwise.
*/
public boolean isNormalCue() {
return (line == UNSET_VALUE && position == UNSET_VALUE);
return (line == DIMEN_UNSET && position == DIMEN_UNSET);
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment