Fix TTML positioning

Issue: #2824

-------------
Created by MOE: https://github.com/google/moe
MOE_MIGRATED_REVID=156781252
This commit is contained in:
olly 2017-05-22 13:52:51 -07:00 committed by Oliver Woodman
parent 78c4cb74ea
commit e892e3a5c7
4 changed files with 101 additions and 54 deletions

View File

@ -157,39 +157,39 @@ public final class TtmlDecoderTest extends InstrumentationTestCase {
assertEquals(2, output.size()); assertEquals(2, output.size());
Cue ttmlCue = output.get(0); Cue ttmlCue = output.get(0);
assertEquals("lorem", ttmlCue.text.toString()); assertEquals("lorem", ttmlCue.text.toString());
assertEquals(10.f / 100.f, ttmlCue.position); assertEquals(10f / 100f, ttmlCue.position);
assertEquals(10.f / 100.f, ttmlCue.line); assertEquals(10f / 100f, ttmlCue.line);
assertEquals(20.f / 100.f, ttmlCue.size); assertEquals(20f / 100f, ttmlCue.size);
ttmlCue = output.get(1); ttmlCue = output.get(1);
assertEquals("amet", ttmlCue.text.toString()); assertEquals("amet", ttmlCue.text.toString());
assertEquals(60.f / 100.f, ttmlCue.position); assertEquals(60f / 100f, ttmlCue.position);
assertEquals(10.f / 100.f, ttmlCue.line); assertEquals(10f / 100f, ttmlCue.line);
assertEquals(20.f / 100.f, ttmlCue.size); assertEquals(20f / 100f, ttmlCue.size);
output = subtitle.getCues(5000000); output = subtitle.getCues(5000000);
assertEquals(1, output.size()); assertEquals(1, output.size());
ttmlCue = output.get(0); ttmlCue = output.get(0);
assertEquals("ipsum", ttmlCue.text.toString()); assertEquals("ipsum", ttmlCue.text.toString());
assertEquals(40.f / 100.f, ttmlCue.position); assertEquals(40f / 100f, ttmlCue.position);
assertEquals(40.f / 100.f, ttmlCue.line); assertEquals(40f / 100f, ttmlCue.line);
assertEquals(20.f / 100.f, ttmlCue.size); assertEquals(20f / 100f, ttmlCue.size);
output = subtitle.getCues(9000000); output = subtitle.getCues(9000000);
assertEquals(1, output.size()); assertEquals(1, output.size());
ttmlCue = output.get(0); ttmlCue = output.get(0);
assertEquals("dolor", ttmlCue.text.toString()); assertEquals("dolor", ttmlCue.text.toString());
assertEquals(10.f / 100.f, ttmlCue.position); assertEquals(10f / 100f, ttmlCue.position);
assertEquals(80.f / 100.f, ttmlCue.line); assertEquals(80f / 100f, ttmlCue.line);
assertEquals(Cue.DIMEN_UNSET, ttmlCue.size); assertEquals(1f, ttmlCue.size);
output = subtitle.getCues(21000000); output = subtitle.getCues(21000000);
assertEquals(1, output.size()); assertEquals(1, output.size());
ttmlCue = output.get(0); ttmlCue = output.get(0);
assertEquals("She first said this", ttmlCue.text.toString()); assertEquals("She first said this", ttmlCue.text.toString());
assertEquals(45.f / 100.f, ttmlCue.position); assertEquals(45f / 100f, ttmlCue.position);
assertEquals(45.f / 100.f, ttmlCue.line); assertEquals(45f / 100f, ttmlCue.line);
assertEquals(35.f / 100.f, ttmlCue.size); assertEquals(35f / 100f, ttmlCue.size);
output = subtitle.getCues(25000000); output = subtitle.getCues(25000000);
ttmlCue = output.get(0); ttmlCue = output.get(0);
assertEquals("She first said this\nThen this", ttmlCue.text.toString()); assertEquals("She first said this\nThen this", ttmlCue.text.toString());
@ -197,8 +197,8 @@ public final class TtmlDecoderTest extends InstrumentationTestCase {
assertEquals(1, output.size()); assertEquals(1, output.size());
ttmlCue = output.get(0); ttmlCue = output.get(0);
assertEquals("She first said this\nThen this\nFinally this", ttmlCue.text.toString()); assertEquals("She first said this\nThen this\nFinally this", ttmlCue.text.toString());
assertEquals(45.f / 100.f, ttmlCue.position); assertEquals(45f / 100f, ttmlCue.position);
assertEquals(45.f / 100.f, ttmlCue.line); assertEquals(45f / 100f, ttmlCue.line);
} }
public void testEmptyStyleAttribute() throws IOException, SubtitleDecoderException { public void testEmptyStyleAttribute() throws IOException, SubtitleDecoderException {

View File

@ -17,7 +17,6 @@ package com.google.android.exoplayer2.text.ttml;
import android.text.Layout; import android.text.Layout;
import android.util.Log; import android.util.Log;
import android.util.Pair;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.text.Cue; import com.google.android.exoplayer2.text.Cue;
import com.google.android.exoplayer2.text.SimpleSubtitleDecoder; import com.google.android.exoplayer2.text.SimpleSubtitleDecoder;
@ -100,7 +99,7 @@ public final class TtmlDecoder extends SimpleSubtitleDecoder {
XmlPullParser xmlParser = xmlParserFactory.newPullParser(); XmlPullParser xmlParser = xmlParserFactory.newPullParser();
Map<String, TtmlStyle> globalStyles = new HashMap<>(); Map<String, TtmlStyle> globalStyles = new HashMap<>();
Map<String, TtmlRegion> regionMap = new HashMap<>(); Map<String, TtmlRegion> regionMap = new HashMap<>();
regionMap.put(TtmlNode.ANONYMOUS_REGION_ID, new TtmlRegion()); regionMap.put(TtmlNode.ANONYMOUS_REGION_ID, new TtmlRegion(null));
ByteArrayInputStream inputStream = new ByteArrayInputStream(bytes, 0, length); ByteArrayInputStream inputStream = new ByteArrayInputStream(bytes, 0, length);
xmlParser.setInput(inputStream, null); xmlParser.setInput(inputStream, null);
TtmlSubtitle ttmlSubtitle = null; TtmlSubtitle ttmlSubtitle = null;
@ -211,9 +210,9 @@ 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)) {
Pair<String, TtmlRegion> ttmlRegionInfo = parseRegionAttributes(xmlParser); TtmlRegion ttmlRegion = parseRegionAttributes(xmlParser);
if (ttmlRegionInfo != null) { if (ttmlRegion != null) {
globalRegions.put(ttmlRegionInfo.first, ttmlRegionInfo.second); globalRegions.put(ttmlRegion.id, ttmlRegion);
} }
} }
} while (!XmlPullParserUtil.isEndTag(xmlParser, TtmlNode.TAG_HEAD)); } while (!XmlPullParserUtil.isEndTag(xmlParser, TtmlNode.TAG_HEAD));
@ -221,41 +220,84 @@ public final class TtmlDecoder extends SimpleSubtitleDecoder {
} }
/** /**
* Parses a region declaration. Supports origin and extent definition but only when defined in * Parses a region declaration.
* terms of percentage of the viewport. Regions that do not correctly declare origin are ignored. * <p>
* If the region defines an origin and/or extent, it is required that they're defined as
* percentages of the viewport. Region declarations that define origin and/or extent in other
* formats are unsupported, and null is returned.
*/ */
private Pair<String, TtmlRegion> parseRegionAttributes(XmlPullParser xmlParser) { private TtmlRegion parseRegionAttributes(XmlPullParser xmlParser) {
String regionId = XmlPullParserUtil.getAttributeValue(xmlParser, TtmlNode.ATTR_ID); String regionId = XmlPullParserUtil.getAttributeValue(xmlParser, TtmlNode.ATTR_ID);
String regionOrigin = XmlPullParserUtil.getAttributeValue(xmlParser, TtmlNode.ATTR_TTS_ORIGIN); if (regionId == null) {
String regionExtent = XmlPullParserUtil.getAttributeValue(xmlParser, TtmlNode.ATTR_TTS_EXTENT);
if (regionOrigin == null || regionId == null) {
return null; return null;
} }
float position = Cue.DIMEN_UNSET;
float line = Cue.DIMEN_UNSET; float position;
Matcher originMatcher = PERCENTAGE_COORDINATES.matcher(regionOrigin); float line;
if (originMatcher.matches()) { String regionOrigin = XmlPullParserUtil.getAttributeValue(xmlParser, TtmlNode.ATTR_TTS_ORIGIN);
try { if (regionOrigin != null) {
position = Float.parseFloat(originMatcher.group(1)) / 100.f; Matcher originMatcher = PERCENTAGE_COORDINATES.matcher(regionOrigin);
line = Float.parseFloat(originMatcher.group(2)) / 100.f; if (originMatcher.matches()) {
} catch (NumberFormatException e) { try {
Log.w(TAG, "Ignoring region with malformed origin: '" + regionOrigin + "'", e); position = Float.parseFloat(originMatcher.group(1)) / 100f;
position = Cue.DIMEN_UNSET; line = Float.parseFloat(originMatcher.group(2)) / 100f;
} catch (NumberFormatException e) {
Log.w(TAG, "Ignoring region with malformed origin: " + regionOrigin);
return null;
}
} else {
Log.w(TAG, "Ignoring region with unsupported origin: " + regionOrigin);
return null;
} }
} else {
// Origin is omitted. Default to top left.
position = 0;
line = 0;
} }
float width = Cue.DIMEN_UNSET;
float width;
float height;
String regionExtent = XmlPullParserUtil.getAttributeValue(xmlParser, TtmlNode.ATTR_TTS_EXTENT);
if (regionExtent != null) { if (regionExtent != null) {
Matcher extentMatcher = PERCENTAGE_COORDINATES.matcher(regionExtent); Matcher extentMatcher = PERCENTAGE_COORDINATES.matcher(regionExtent);
if (extentMatcher.matches()) { if (extentMatcher.matches()) {
try { try {
width = Float.parseFloat(extentMatcher.group(1)) / 100.f; width = Float.parseFloat(extentMatcher.group(1)) / 100f;
height = Float.parseFloat(extentMatcher.group(2)) / 100f;
} catch (NumberFormatException e) { } catch (NumberFormatException e) {
Log.w(TAG, "Ignoring malformed region extent: '" + regionExtent + "'", e); Log.w(TAG, "Ignoring region with malformed extent: " + regionOrigin);
return null;
} }
} else {
Log.w(TAG, "Ignoring region with unsupported extent: " + regionOrigin);
return null;
}
} else {
// Extent is omitted. Default to extent of parent.
width = 1;
height = 1;
}
@Cue.AnchorType int lineAnchor = Cue.ANCHOR_TYPE_START;
String displayAlign = XmlPullParserUtil.getAttributeValue(xmlParser,
TtmlNode.ATTR_TTS_DISPLAY_ALIGN);
if (displayAlign != null) {
switch (displayAlign.toLowerCase()) {
case "center":
lineAnchor = Cue.ANCHOR_TYPE_MIDDLE;
line += height / 2;
break;
case "after":
lineAnchor = Cue.ANCHOR_TYPE_END;
line += height;
break;
default:
// Default "before" case. Do nothing.
break;
} }
} }
return position != Cue.DIMEN_UNSET ? new Pair<>(regionId, new TtmlRegion(position, line,
Cue.LINE_TYPE_FRACTION, width)) : null; return new TtmlRegion(regionId, position, line, Cue.LINE_TYPE_FRACTION, lineAnchor, width);
} }
private String[] parseStyleIds(String parentStyleIds) { private String[] parseStyleIds(String parentStyleIds) {
@ -277,7 +319,7 @@ public final class TtmlDecoder extends SimpleSubtitleDecoder {
try { try {
style.setBackgroundColor(ColorParser.parseTtmlColor(attributeValue)); style.setBackgroundColor(ColorParser.parseTtmlColor(attributeValue));
} catch (IllegalArgumentException e) { } catch (IllegalArgumentException e) {
Log.w(TAG, "failed parsing background value: '" + attributeValue + "'"); Log.w(TAG, "Failed parsing background value: " + attributeValue);
} }
break; break;
case TtmlNode.ATTR_TTS_COLOR: case TtmlNode.ATTR_TTS_COLOR:
@ -285,7 +327,7 @@ public final class TtmlDecoder extends SimpleSubtitleDecoder {
try { try {
style.setFontColor(ColorParser.parseTtmlColor(attributeValue)); style.setFontColor(ColorParser.parseTtmlColor(attributeValue));
} catch (IllegalArgumentException e) { } catch (IllegalArgumentException e) {
Log.w(TAG, "failed parsing color value: '" + attributeValue + "'"); Log.w(TAG, "Failed parsing color value: " + attributeValue);
} }
break; break;
case TtmlNode.ATTR_TTS_FONT_FAMILY: case TtmlNode.ATTR_TTS_FONT_FAMILY:
@ -296,7 +338,7 @@ public final class TtmlDecoder extends SimpleSubtitleDecoder {
style = createIfNull(style); style = createIfNull(style);
parseFontSize(attributeValue, style); parseFontSize(attributeValue, style);
} catch (SubtitleDecoderException e) { } catch (SubtitleDecoderException e) {
Log.w(TAG, "failed parsing fontSize value: '" + attributeValue + "'"); Log.w(TAG, "Failed parsing fontSize value: " + attributeValue);
} }
break; break;
case TtmlNode.ATTR_TTS_FONT_WEIGHT: case TtmlNode.ATTR_TTS_FONT_WEIGHT:

View File

@ -50,14 +50,15 @@ import java.util.TreeSet;
public static final String ANONYMOUS_REGION_ID = ""; public static final String ANONYMOUS_REGION_ID = "";
public static final String ATTR_ID = "id"; public static final String ATTR_ID = "id";
public static final String ATTR_TTS_BACKGROUND_COLOR = "backgroundColor"; public static final String ATTR_TTS_ORIGIN = "origin";
public static final String ATTR_TTS_EXTENT = "extent"; public static final String ATTR_TTS_EXTENT = "extent";
public static final String ATTR_TTS_DISPLAY_ALIGN = "displayAlign";
public static final String ATTR_TTS_BACKGROUND_COLOR = "backgroundColor";
public static final String ATTR_TTS_FONT_STYLE = "fontStyle"; public static final String ATTR_TTS_FONT_STYLE = "fontStyle";
public static final String ATTR_TTS_FONT_SIZE = "fontSize"; public static final String ATTR_TTS_FONT_SIZE = "fontSize";
public static final String ATTR_TTS_FONT_FAMILY = "fontFamily"; public static final String ATTR_TTS_FONT_FAMILY = "fontFamily";
public static final String ATTR_TTS_FONT_WEIGHT = "fontWeight"; public static final String ATTR_TTS_FONT_WEIGHT = "fontWeight";
public static final String ATTR_TTS_COLOR = "color"; public static final String ATTR_TTS_COLOR = "color";
public static final String ATTR_TTS_ORIGIN = "origin";
public static final String ATTR_TTS_TEXT_DECORATION = "textDecoration"; public static final String ATTR_TTS_TEXT_DECORATION = "textDecoration";
public static final String ATTR_TTS_TEXT_ALIGN = "textAlign"; public static final String ATTR_TTS_TEXT_ALIGN = "textAlign";
@ -179,7 +180,7 @@ import java.util.TreeSet;
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(new Cue(cleanUpText(entry.getValue()), null, region.line, region.lineType,
Cue.TYPE_UNSET, region.position, Cue.TYPE_UNSET, region.width)); region.lineAnchor, region.position, Cue.TYPE_UNSET, region.width));
} }
return cues; return cues;
} }

View File

@ -22,20 +22,24 @@ import com.google.android.exoplayer2.text.Cue;
*/ */
/* package */ final class TtmlRegion { /* package */ final class TtmlRegion {
public final String id;
public final float position; public final float position;
public final float line; public final float line;
@Cue.LineType @Cue.LineType public final int lineType;
public final int lineType; @Cue.AnchorType public final int lineAnchor;
public final float width; public final float width;
public TtmlRegion() { public TtmlRegion(String id) {
this(Cue.DIMEN_UNSET, Cue.DIMEN_UNSET, Cue.TYPE_UNSET, Cue.DIMEN_UNSET); this(id, Cue.DIMEN_UNSET, Cue.DIMEN_UNSET, Cue.TYPE_UNSET, Cue.TYPE_UNSET, Cue.DIMEN_UNSET);
} }
public TtmlRegion(float position, float line, @Cue.LineType int lineType, float width) { public TtmlRegion(String id, float position, float line, @Cue.LineType int lineType,
@Cue.AnchorType int lineAnchor, float width) {
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.width = width; this.width = width;
} }