Commit 15413548 by hoangtc Committed by Andrew Lewis

Support TTML font size using % correctly.

For TTML, if the font size is expressed in %, the font size should be relative
to the cellResolution of the document which we did not support before. This CL
adds support for handling this correctly.
Note that this still does not support font size using c unit.

-------------
Created by MOE: https://github.com/google/moe
MOE_MIGRATED_REVID=196985694
parent 5e1b4308
...@@ -25,6 +25,8 @@ ...@@ -25,6 +25,8 @@
time that can make text size of each region much smaller than defined. time that can make text size of each region much smaller than defined.
* Fix an issue when the caption line has no text (empty line or only line * Fix an issue when the caption line has no text (empty line or only line
break), and the line's background is still displayed. break), and the line's background is still displayed.
* Support TTML font size using % correctly (as percentage of document cell
resolution).
### 2.8.0 ### ### 2.8.0 ###
...@@ -101,7 +103,7 @@ ...@@ -101,7 +103,7 @@
* Allow multiple listeners for `DefaultDrmSessionManager`. * Allow multiple listeners for `DefaultDrmSessionManager`.
* Pass `DrmSessionManager` to `ExoPlayerFactory` instead of `RendererFactory`. * Pass `DrmSessionManager` to `ExoPlayerFactory` instead of `RendererFactory`.
* Change minimum API requirement for CBC and pattern encryption from 24 to 25 * Change minimum API requirement for CBC and pattern encryption from 24 to 25
([#4022][https://github.com/google/ExoPlayer/issues/4022]). ([#4022](https://github.com/google/ExoPlayer/issues/4022)).
* Fix handling of 307/308 redirects when making license requests * Fix handling of 307/308 redirects when making license requests
([#4108](https://github.com/google/ExoPlayer/issues/4108)). ([#4108](https://github.com/google/ExoPlayer/issues/4108)).
* HLS: * HLS:
......
...@@ -38,6 +38,7 @@ import org.xmlpull.v1.XmlPullParserFactory; ...@@ -38,6 +38,7 @@ import org.xmlpull.v1.XmlPullParserFactory;
/** /**
* A {@link SimpleSubtitleDecoder} for TTML supporting the DFXP presentation profile. Features * A {@link SimpleSubtitleDecoder} for TTML supporting the DFXP presentation profile. Features
* supported by this decoder are: * supported by this decoder are:
*
* <ul> * <ul>
* <li>content * <li>content
* <li>core * <li>core
...@@ -51,7 +52,9 @@ import org.xmlpull.v1.XmlPullParserFactory; ...@@ -51,7 +52,9 @@ import org.xmlpull.v1.XmlPullParserFactory;
* <li>time-clock * <li>time-clock
* <li>time-offset-with-frames * <li>time-offset-with-frames
* <li>time-offset-with-ticks * <li>time-offset-with-ticks
* <li>cell-resolution
* </ul> * </ul>
*
* @see <a href="http://www.w3.org/TR/ttaf1-dfxp/">TTML specification</a> * @see <a href="http://www.w3.org/TR/ttaf1-dfxp/">TTML specification</a>
*/ */
public final class TtmlDecoder extends SimpleSubtitleDecoder { public final class TtmlDecoder extends SimpleSubtitleDecoder {
...@@ -74,11 +77,14 @@ public final class TtmlDecoder extends SimpleSubtitleDecoder { ...@@ -74,11 +77,14 @@ public final class TtmlDecoder extends SimpleSubtitleDecoder {
private static final Pattern FONT_SIZE = Pattern.compile("^(([0-9]*.)?[0-9]+)(px|em|%)$"); private static final Pattern FONT_SIZE = Pattern.compile("^(([0-9]*.)?[0-9]+)(px|em|%)$");
private static final Pattern PERCENTAGE_COORDINATES = private static final Pattern PERCENTAGE_COORDINATES =
Pattern.compile("^(\\d+\\.?\\d*?)% (\\d+\\.?\\d*?)%$"); Pattern.compile("^(\\d+\\.?\\d*?)% (\\d+\\.?\\d*?)%$");
private static final Pattern CELL_RESOLUTION = Pattern.compile("^(\\d+) (\\d+)$");
private static final int DEFAULT_FRAME_RATE = 30; private static final int DEFAULT_FRAME_RATE = 30;
private static final FrameAndTickRate DEFAULT_FRAME_AND_TICK_RATE = private static final FrameAndTickRate DEFAULT_FRAME_AND_TICK_RATE =
new FrameAndTickRate(DEFAULT_FRAME_RATE, 1, 1); new FrameAndTickRate(DEFAULT_FRAME_RATE, 1, 1);
private static final CellResolution DEFAULT_CELL_RESOLUTION =
new CellResolution(/* columns= */ 32, /* rows= */ 15);
private final XmlPullParserFactory xmlParserFactory; private final XmlPullParserFactory xmlParserFactory;
...@@ -107,6 +113,7 @@ public final class TtmlDecoder extends SimpleSubtitleDecoder { ...@@ -107,6 +113,7 @@ public final class TtmlDecoder extends SimpleSubtitleDecoder {
int unsupportedNodeDepth = 0; int unsupportedNodeDepth = 0;
int eventType = xmlParser.getEventType(); int eventType = xmlParser.getEventType();
FrameAndTickRate frameAndTickRate = DEFAULT_FRAME_AND_TICK_RATE; FrameAndTickRate frameAndTickRate = DEFAULT_FRAME_AND_TICK_RATE;
CellResolution cellResolution = DEFAULT_CELL_RESOLUTION;
while (eventType != XmlPullParser.END_DOCUMENT) { while (eventType != XmlPullParser.END_DOCUMENT) {
TtmlNode parent = nodeStack.peekLast(); TtmlNode parent = nodeStack.peekLast();
if (unsupportedNodeDepth == 0) { if (unsupportedNodeDepth == 0) {
...@@ -114,12 +121,13 @@ public final class TtmlDecoder extends SimpleSubtitleDecoder { ...@@ -114,12 +121,13 @@ public final class TtmlDecoder extends SimpleSubtitleDecoder {
if (eventType == XmlPullParser.START_TAG) { if (eventType == XmlPullParser.START_TAG) {
if (TtmlNode.TAG_TT.equals(name)) { if (TtmlNode.TAG_TT.equals(name)) {
frameAndTickRate = parseFrameAndTickRates(xmlParser); frameAndTickRate = parseFrameAndTickRates(xmlParser);
cellResolution = parseCellResolution(xmlParser, DEFAULT_CELL_RESOLUTION);
} }
if (!isSupportedTag(name)) { if (!isSupportedTag(name)) {
Log.i(TAG, "Ignoring unsupported tag: " + xmlParser.getName()); Log.i(TAG, "Ignoring unsupported tag: " + xmlParser.getName());
unsupportedNodeDepth++; unsupportedNodeDepth++;
} else if (TtmlNode.TAG_HEAD.equals(name)) { } else if (TtmlNode.TAG_HEAD.equals(name)) {
parseHeader(xmlParser, globalStyles, regionMap); parseHeader(xmlParser, globalStyles, regionMap, cellResolution);
} else { } else {
try { try {
TtmlNode node = parseNode(xmlParser, parent, regionMap, frameAndTickRate); TtmlNode node = parseNode(xmlParser, parent, regionMap, frameAndTickRate);
...@@ -193,8 +201,36 @@ public final class TtmlDecoder extends SimpleSubtitleDecoder { ...@@ -193,8 +201,36 @@ public final class TtmlDecoder extends SimpleSubtitleDecoder {
return new FrameAndTickRate(frameRate * frameRateMultiplier, subFrameRate, tickRate); return new FrameAndTickRate(frameRate * frameRateMultiplier, subFrameRate, tickRate);
} }
private Map<String, TtmlStyle> parseHeader(XmlPullParser xmlParser, private CellResolution parseCellResolution(XmlPullParser xmlParser, CellResolution defaultValue)
Map<String, TtmlStyle> globalStyles, Map<String, TtmlRegion> globalRegions) throws SubtitleDecoderException {
String cellResolution = xmlParser.getAttributeValue(TTP, "cellResolution");
if (cellResolution == null) {
return defaultValue;
}
Matcher cellResolutionMatcher = CELL_RESOLUTION.matcher(cellResolution);
if (!cellResolutionMatcher.matches()) {
Log.w(TAG, "Ignoring malformed cell resolution: " + cellResolution);
return defaultValue;
}
try {
int columns = Integer.parseInt(cellResolutionMatcher.group(1));
int rows = Integer.parseInt(cellResolutionMatcher.group(2));
if (columns == 0 || rows == 0) {
throw new SubtitleDecoderException("Invalid cell resolution " + columns + " " + rows);
}
return new CellResolution(columns, rows);
} catch (NumberFormatException e) {
Log.w(TAG, "Ignoring malformed cell resolution: " + cellResolution);
return defaultValue;
}
}
private Map<String, TtmlStyle> parseHeader(
XmlPullParser xmlParser,
Map<String, TtmlStyle> globalStyles,
Map<String, TtmlRegion> globalRegions,
CellResolution cellResolution)
throws IOException, XmlPullParserException { throws IOException, XmlPullParserException {
do { do {
xmlParser.next(); xmlParser.next();
...@@ -210,7 +246,7 @@ public final class TtmlDecoder extends SimpleSubtitleDecoder { ...@@ -210,7 +246,7 @@ public final class TtmlDecoder extends SimpleSubtitleDecoder {
globalStyles.put(style.getId(), style); globalStyles.put(style.getId(), style);
} }
} else if (XmlPullParserUtil.isStartTag(xmlParser, TtmlNode.TAG_REGION)) { } else if (XmlPullParserUtil.isStartTag(xmlParser, TtmlNode.TAG_REGION)) {
TtmlRegion ttmlRegion = parseRegionAttributes(xmlParser); TtmlRegion ttmlRegion = parseRegionAttributes(xmlParser, cellResolution);
if (ttmlRegion != null) { if (ttmlRegion != null) {
globalRegions.put(ttmlRegion.id, ttmlRegion); globalRegions.put(ttmlRegion.id, ttmlRegion);
} }
...@@ -221,12 +257,12 @@ public final class TtmlDecoder extends SimpleSubtitleDecoder { ...@@ -221,12 +257,12 @@ public final class TtmlDecoder extends SimpleSubtitleDecoder {
/** /**
* Parses a region declaration. * Parses a region declaration.
* <p> *
* If the region defines an origin and extent, it is required that they're defined as percentages * <p>If the region defines an origin and extent, it is required that they're defined as
* of the viewport. Region declarations that define origin and extent in other formats are * percentages of the viewport. Region declarations that define origin and extent in other formats
* unsupported, and null is returned. * are unsupported, and null is returned.
*/ */
private TtmlRegion parseRegionAttributes(XmlPullParser xmlParser) { private TtmlRegion parseRegionAttributes(XmlPullParser xmlParser, CellResolution cellResolution) {
String regionId = XmlPullParserUtil.getAttributeValue(xmlParser, TtmlNode.ATTR_ID); String regionId = XmlPullParserUtil.getAttributeValue(xmlParser, TtmlNode.ATTR_ID);
if (regionId == null) { if (regionId == null) {
return null; return null;
...@@ -305,7 +341,16 @@ public final class TtmlDecoder extends SimpleSubtitleDecoder { ...@@ -305,7 +341,16 @@ public final class TtmlDecoder extends SimpleSubtitleDecoder {
} }
} }
return new TtmlRegion(regionId, position, line, Cue.LINE_TYPE_FRACTION, lineAnchor, width); float regionTextHeight = 1.0f / cellResolution.rows;
return new TtmlRegion(
regionId,
position,
line,
/* lineType= */ Cue.LINE_TYPE_FRACTION,
lineAnchor,
width,
/* textSizeType= */ Cue.TEXT_SIZE_TYPE_FRACTIONAL_IGNORE_PADDING,
/* textSize= */ regionTextHeight);
} }
private String[] parseStyleIds(String parentStyleIds) { private String[] parseStyleIds(String parentStyleIds) {
...@@ -594,4 +639,15 @@ public final class TtmlDecoder extends SimpleSubtitleDecoder { ...@@ -594,4 +639,15 @@ public final class TtmlDecoder extends SimpleSubtitleDecoder {
this.tickRate = tickRate; this.tickRate = tickRate;
} }
} }
/** Represents the cell resolution for a TTML file. */
private static final class CellResolution {
final int columns;
final int rows;
CellResolution(int columns, int rows) {
this.columns = columns;
this.rows = rows;
}
}
} }
...@@ -179,8 +179,18 @@ import java.util.TreeSet; ...@@ -179,8 +179,18 @@ import java.util.TreeSet;
List<Cue> cues = new ArrayList<>(); List<Cue> cues = new ArrayList<>();
for (Entry<String, SpannableStringBuilder> entry : regionOutputs.entrySet()) { for (Entry<String, SpannableStringBuilder> entry : regionOutputs.entrySet()) {
TtmlRegion region = regionMap.get(entry.getKey()); TtmlRegion region = regionMap.get(entry.getKey());
cues.add(new Cue(cleanUpText(entry.getValue()), null, region.line, region.lineType, cues.add(
region.lineAnchor, region.position, Cue.TYPE_UNSET, region.width)); new Cue(
cleanUpText(entry.getValue()),
/* textAlignment= */ null,
region.line,
region.lineType,
region.lineAnchor,
region.position,
/* positionAnchor= */ Cue.TYPE_UNSET,
region.width,
region.textSizeType,
region.textSize));
} }
return cues; return cues;
} }
......
...@@ -25,22 +25,41 @@ import com.google.android.exoplayer2.text.Cue; ...@@ -25,22 +25,41 @@ import com.google.android.exoplayer2.text.Cue;
public final String id; public final String id;
public final float position; public final float position;
public final float line; public final float line;
@Cue.LineType public final int lineType; public final @Cue.LineType int lineType;
@Cue.AnchorType public final int lineAnchor; public final @Cue.AnchorType int lineAnchor;
public final float width; public final float width;
public final @Cue.TextSizeType int textSizeType;
public final float textSize;
public TtmlRegion(String id) { public TtmlRegion(String id) {
this(id, Cue.DIMEN_UNSET, Cue.DIMEN_UNSET, Cue.TYPE_UNSET, Cue.TYPE_UNSET, Cue.DIMEN_UNSET); this(
id,
/* position= */ Cue.DIMEN_UNSET,
/* line= */ Cue.DIMEN_UNSET,
/* lineType= */ Cue.TYPE_UNSET,
/* lineAnchor= */ Cue.TYPE_UNSET,
/* width= */ Cue.DIMEN_UNSET,
/* textSizeType= */ Cue.TYPE_UNSET,
/* textSize= */ Cue.DIMEN_UNSET);
} }
public TtmlRegion(String id, float position, float line, @Cue.LineType int lineType, public TtmlRegion(
@Cue.AnchorType int lineAnchor, float width) { String id,
float position,
float line,
@Cue.LineType int lineType,
@Cue.AnchorType int lineAnchor,
float width,
int textSizeType,
float textSize) {
this.id = id; this.id = id;
this.position = position; this.position = position;
this.line = line; this.line = line;
this.lineType = lineType; this.lineType = lineType;
this.lineAnchor = lineAnchor; this.lineAnchor = lineAnchor;
this.width = width; this.width = width;
this.textSizeType = textSizeType;
this.textSize = textSize;
} }
} }
...@@ -51,14 +51,10 @@ public final class SubtitleView extends View implements TextOutput { ...@@ -51,14 +51,10 @@ public final class SubtitleView extends View implements TextOutput {
*/ */
public static final float DEFAULT_BOTTOM_PADDING_FRACTION = 0.08f; public static final float DEFAULT_BOTTOM_PADDING_FRACTION = 0.08f;
private static final int FRACTIONAL = 0;
private static final int FRACTIONAL_IGNORE_PADDING = 1;
private static final int ABSOLUTE = 2;
private final List<SubtitlePainter> painters; private final List<SubtitlePainter> painters;
private List<Cue> cues; private List<Cue> cues;
private int textSizeType; private @Cue.TextSizeType int textSizeType;
private float textSize; private float textSize;
private boolean applyEmbeddedStyles; private boolean applyEmbeddedStyles;
private boolean applyEmbeddedFontSizes; private boolean applyEmbeddedFontSizes;
...@@ -72,7 +68,7 @@ public final class SubtitleView extends View implements TextOutput { ...@@ -72,7 +68,7 @@ public final class SubtitleView extends View implements TextOutput {
public SubtitleView(Context context, AttributeSet attrs) { public SubtitleView(Context context, AttributeSet attrs) {
super(context, attrs); super(context, attrs);
painters = new ArrayList<>(); painters = new ArrayList<>();
textSizeType = FRACTIONAL; textSizeType = Cue.TEXT_SIZE_TYPE_FRACTIONAL;
textSize = DEFAULT_TEXT_SIZE_FRACTION; textSize = DEFAULT_TEXT_SIZE_FRACTION;
applyEmbeddedStyles = true; applyEmbeddedStyles = true;
applyEmbeddedFontSizes = true; applyEmbeddedFontSizes = true;
...@@ -120,7 +116,9 @@ public final class SubtitleView extends View implements TextOutput { ...@@ -120,7 +116,9 @@ public final class SubtitleView extends View implements TextOutput {
} else { } else {
resources = context.getResources(); resources = context.getResources();
} }
setTextSize(ABSOLUTE, TypedValue.applyDimension(unit, size, resources.getDisplayMetrics())); setTextSize(
Cue.TEXT_SIZE_TYPE_ABSOLUTE,
TypedValue.applyDimension(unit, size, resources.getDisplayMetrics()));
} }
/** /**
...@@ -154,10 +152,14 @@ public final class SubtitleView extends View implements TextOutput { ...@@ -154,10 +152,14 @@ public final class SubtitleView extends View implements TextOutput {
* height after the top and bottom padding has been subtracted. * height after the top and bottom padding has been subtracted.
*/ */
public void setFractionalTextSize(float fractionOfHeight, boolean ignorePadding) { public void setFractionalTextSize(float fractionOfHeight, boolean ignorePadding) {
setTextSize(ignorePadding ? FRACTIONAL_IGNORE_PADDING : FRACTIONAL, fractionOfHeight); setTextSize(
ignorePadding
? Cue.TEXT_SIZE_TYPE_FRACTIONAL_IGNORE_PADDING
: Cue.TEXT_SIZE_TYPE_FRACTIONAL,
fractionOfHeight);
} }
private void setTextSize(int textSizeType, float textSize) { private void setTextSize(@Cue.TextSizeType int textSizeType, float textSize) {
if (this.textSizeType == textSizeType && this.textSize == textSize) { if (this.textSizeType == textSizeType && this.textSize == textSize) {
return; return;
} }
...@@ -255,17 +257,61 @@ public final class SubtitleView extends View implements TextOutput { ...@@ -255,17 +257,61 @@ public final class SubtitleView extends View implements TextOutput {
// No space to draw subtitles. // No space to draw subtitles.
return; return;
} }
int rawViewHeight = rawBottom - rawTop;
int viewHeightMinusPadding = bottom - top;
float textSizePx = textSizeType == ABSOLUTE ? textSize float defaultViewTextSizePx =
: textSize * (textSizeType == FRACTIONAL ? (bottom - top) : (rawBottom - rawTop)); resolveTextSize(textSizeType, textSize, rawViewHeight, viewHeightMinusPadding);
if (textSizePx <= 0) { if (defaultViewTextSizePx <= 0) {
// Text has no height. // Text has no height.
return; return;
} }
for (int i = 0; i < cueCount; i++) { for (int i = 0; i < cueCount; i++) {
painters.get(i).draw(cues.get(i), applyEmbeddedStyles, applyEmbeddedFontSizes, style, Cue cue = cues.get(i);
textSizePx, bottomPaddingFraction, canvas, left, top, right, bottom); float textSizePx =
resolveTextSizeForCue(cue, rawViewHeight, viewHeightMinusPadding, defaultViewTextSizePx);
SubtitlePainter painter = painters.get(i);
painter.draw(
cue,
applyEmbeddedStyles,
applyEmbeddedFontSizes,
style,
textSizePx,
bottomPaddingFraction,
canvas,
left,
top,
right,
bottom);
}
}
private float resolveTextSizeForCue(
Cue cue, int rawViewHeight, int viewHeightMinusPadding, float defaultViewTextSizePx) {
if (cue.textSizeType == Cue.TYPE_UNSET || cue.textSize == Cue.DIMEN_UNSET) {
return defaultViewTextSizePx;
}
float defaultCueTextSizePx =
resolveTextSize(cue.textSizeType, cue.textSize, rawViewHeight, viewHeightMinusPadding);
return defaultCueTextSizePx > 0 ? defaultCueTextSizePx : defaultViewTextSizePx;
}
private float resolveTextSize(
@Cue.TextSizeType int textSizeType,
float textSize,
int rawViewHeight,
int viewHeightMinusPadding) {
switch (textSizeType) {
case Cue.TEXT_SIZE_TYPE_ABSOLUTE:
return textSize;
case Cue.TEXT_SIZE_TYPE_FRACTIONAL:
return textSize * viewHeightMinusPadding;
case Cue.TEXT_SIZE_TYPE_FRACTIONAL_IGNORE_PADDING:
return textSize * rawViewHeight;
case Cue.TYPE_UNSET:
default:
return Cue.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