TTML improvements.

- do not denormalize styles at parsing time but only put normalized style info
into TtmlNode tree. Resolve styles on demand when Cues are requested for a
given timeUs.
- create TtmlRenderUtil to have static render functions separate
- added unit test for TtmlRenderUtil
- adjusted testing strategy for unit test to check resolved style on Spannables after rendering
This commit is contained in:
Oliver Woodman 2015-09-15 13:44:52 +01:00
parent 908e4dfd5d
commit b6f15a17e0
10 changed files with 421 additions and 341 deletions

View File

@ -18,6 +18,9 @@
<style style="s0 s1" id="s2" <style style="s0 s1" id="s2"
tts:fontFamily="serif" tts:fontFamily="serif"
tts:backgroundColor="red" /> tts:backgroundColor="red" />
<style style="s1 s0" id="s3"
tts:fontFamily="serif"
tts:backgroundColor="red" />
</styling> </styling>
</head> </head>
<body> <body>

View File

@ -26,20 +26,20 @@
<p style="s0 s1" begin="10s" end="18s" tts:color="yellow">text 1</p> <p style="s0 s1" begin="10s" end="18s" tts:color="yellow">text 1</p>
</div> </div>
<div> <div>
<p style="s0 s1" begin="20s" end="28s">text 1</p> <p style="s0 s1" begin="20s" end="28s">text 2</p>
</div> </div>
<div tts:color="yellow" tts:textDecoration="underline" tts:fontStyle="italic" tts:fontFamily="sansSerifInline"> <div tts:color="yellow" tts:textDecoration="underline" tts:fontStyle="italic" tts:fontFamily="sansSerifInline">
<p style="s2 s3" begin="30s" end="38s">text 1</p> <p style="s2 s3" begin="30s" end="38s">text 2.5</p>
</div> </div>
<div> <div>
<!-- empty style attribute --> <!-- empty style attribute -->
<p style=" " begin="40s" end="48s">text 1</p> <p style=" " begin="40s" end="48s">text 3</p>
</div> </div>
<div> <div>
<p style="not_existing" begin="50s" end="58s">text 1</p> <p style="not_existing" begin="50s" end="58s">text 4</p>
</div> </div>
<div> <div>
<p style="not_existing s2" begin="60s" end="68s">text 1</p> <p style="not_existing s2" begin="60s" end="68s">text 5</p>
</div> </div>
</body> </body>
</tt> </tt>

View File

@ -1,24 +0,0 @@
<tt xmlns:ttm="http://www.w3.org/2006/10/ttaf1#metadata" xmlns:ttp="http://www.w3.org/2006/10/ttaf1#parameter"
xmlns:tts="http://www.w3.org/2006/10/ttaf1#style"
xmlns="http://www.w3.org/ns/ttml"
xmlns="http://www.w3.org/2006/10/ttaf1">
<head>
<styling>
<style id="s0"
tts:color="blue"/>
<style id="s1"
tts:backgroundColor="red"/>
</styling>
</head>
<body style="s0">
<div>
<p style="s0" begin="10s" end="18s">text 1</p>
</div>
<div>
<p style="s0" begin="20s" end="28s">text <span style="s0">2</span></p>
</div>
<div>
<p style="s1" begin="20s" end="28s">text <span style="s1">3</span></p>
</div>
</body>
</tt>

View File

@ -1,19 +0,0 @@
<tt xmlns="http://www.w3.org/ns/ttml"
xmlns="http://www.w3.org/2006/10/ttaf1"
xmlns:ttp="http://www.w3.org/2006/10/ttaf1#parameter"
xmlns:tts="http://www.w3.org/2006/10/ttaf1#style"
xmlns:ttm="http://www.w3.org/2006/10/ttaf1#metadata">
<body>
<div>
<p begin="10s" end="18s"
tts:fontWeight="bold"
tts:fontStyle="italic"
tts:fontFamily="serif"
tts:textDecoration="underline"
tts:backgroundColor="blue"
tts:color="yellow">
<span>text 1</span>
</p>
</div>
</body>
</tt>

View File

@ -15,12 +15,24 @@
*/ */
package com.google.android.exoplayer.text.ttml; package com.google.android.exoplayer.text.ttml;
import com.google.android.exoplayer.text.Cue;
import android.graphics.Color; import android.graphics.Color;
import android.test.InstrumentationTestCase; import android.test.InstrumentationTestCase;
import android.text.Layout; import android.text.Layout;
import android.text.SpannableStringBuilder;
import android.text.style.AlignmentSpan;
import android.text.style.BackgroundColorSpan;
import android.text.style.ForegroundColorSpan;
import android.text.style.StrikethroughSpan;
import android.text.style.StyleSpan;
import android.text.style.TypefaceSpan;
import android.text.style.UnderlineSpan;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.util.List;
import java.util.Map;
/** /**
* Unit test for {@link TtmlParser}. * Unit test for {@link TtmlParser}.
@ -35,16 +47,12 @@ public final class TtmlParserTest extends InstrumentationTestCase {
"ttml/inherit_and_override_style.xml"; "ttml/inherit_and_override_style.xml";
private static final String INHERIT_GLOBAL_AND_PARENT_TTML_FILE = private static final String INHERIT_GLOBAL_AND_PARENT_TTML_FILE =
"ttml/inherit_global_and_parent.xml"; "ttml/inherit_global_and_parent.xml";
private static final String NON_INHERTABLE_PROPERTIES_TTML_FILE =
"ttml/non_inheritable_properties.xml";
private static final String INHERIT_MULTIPLE_STYLES_TTML_FILE = private static final String INHERIT_MULTIPLE_STYLES_TTML_FILE =
"ttml/inherit_multiple_styles.xml"; "ttml/inherit_multiple_styles.xml";
private static final String CHAIN_MULTIPLE_STYLES_TTML_FILE = private static final String CHAIN_MULTIPLE_STYLES_TTML_FILE =
"ttml/chain_multiple_styles.xml"; "ttml/chain_multiple_styles.xml";
private static final String NO_UNDERLINE_LINETHROUGH_TTML_FILE = private static final String NO_UNDERLINE_LINETHROUGH_TTML_FILE =
"ttml/no_underline_linethrough.xml"; "ttml/no_underline_linethrough.xml";
private static final String INSTANCE_CREATION_TTML_FILE =
"ttml/instance_creation.xml";
private static final String NAMESPACE_CONFUSION_TTML_FILE = private static final String NAMESPACE_CONFUSION_TTML_FILE =
"ttml/namespace_confusion.xml"; "ttml/namespace_confusion.xml";
private static final String NAMESPACE_NOT_DECLARED_TTML_FILE = private static final String NAMESPACE_NOT_DECLARED_TTML_FILE =
@ -67,143 +75,53 @@ public final class TtmlParserTest extends InstrumentationTestCase {
} }
public void testInheritInlineAttributes() throws IOException { public void testInheritInlineAttributes() throws IOException {
TtmlSubtitle subtitle = getSubtitle(INLINE_ATTRIBUTES_TTML_FILE); TtmlSubtitle subtitle = getSubtitle(INLINE_ATTRIBUTES_TTML_FILE);
assertEquals(4, subtitle.getEventTimeCount()); assertEquals(4, subtitle.getEventTimeCount());
assertSpans(subtitle, 20, "text 2", "sansSerif", TtmlStyle.STYLE_ITALIC,
TtmlNode root = subtitle.getRoot(); Color.CYAN, Color.parseColor("lime"), false, true, null);
// inherite inline attributes
TtmlNode body = queryChildrenForTag(root, TtmlNode.TAG_BODY, 0);
TtmlNode secondDiv = queryChildrenForTag(body, TtmlNode.TAG_DIV, 1);
TtmlStyle secondPStyle = queryChildrenForTag(secondDiv, TtmlNode.TAG_P, 0).style;
assertEquals(Color.parseColor("lime"), secondPStyle.getColor());
assertFalse(secondPStyle.hasBackgroundColorSpecified());
assertEquals(0, secondPStyle.getBackgroundColor());
assertEquals("sansSerif", secondPStyle.getFontFamily());
assertEquals(TtmlStyle.STYLE_ITALIC, secondPStyle.getStyle());
assertTrue(secondPStyle.isLinethrough());
} }
public void testInheritGlobalStyle() throws IOException { public void testInheritGlobalStyle() throws IOException {
TtmlSubtitle subtitle = getSubtitle(INHERIT_STYLE_TTML_FILE); TtmlSubtitle subtitle = getSubtitle(INHERIT_STYLE_TTML_FILE);
assertEquals(2, subtitle.getEventTimeCount()); assertEquals(2, subtitle.getEventTimeCount());
assertSpans(subtitle, 10, "text 1", "serif", TtmlStyle.STYLE_BOLD_ITALIC,
TtmlNode root = subtitle.getRoot(); Color.BLUE, Color.YELLOW, true, false, null);
TtmlNode body = queryChildrenForTag(root, TtmlNode.TAG_BODY, 0);
TtmlNode firstDiv = queryChildrenForTag(body, TtmlNode.TAG_DIV, 0);
TtmlStyle firstPStyle = queryChildrenForTag(firstDiv, TtmlNode.TAG_P, 0).style;
assertEquals(Color.parseColor("yellow"), firstPStyle.getColor());
assertEquals(Color.parseColor("blue"), firstPStyle.getBackgroundColor());
assertEquals("serif", firstPStyle.getFontFamily());
assertEquals(TtmlStyle.STYLE_BOLD_ITALIC, firstPStyle.getStyle());
assertTrue(firstPStyle.isUnderline());
} }
public void testInheritGlobalStyleOverriddenByInlineAttributes() throws IOException { public void testInheritGlobalStyleOverriddenByInlineAttributes() throws IOException {
TtmlSubtitle subtitle = getSubtitle(INHERIT_STYLE_OVERRIDE_TTML_FILE); TtmlSubtitle subtitle = getSubtitle(INHERIT_STYLE_OVERRIDE_TTML_FILE);
assertEquals(4, subtitle.getEventTimeCount()); assertEquals(4, subtitle.getEventTimeCount());
TtmlNode root = subtitle.getRoot(); assertSpans(subtitle, 10, "text 1", "serif", TtmlStyle.STYLE_BOLD_ITALIC, Color.BLUE,
Color.YELLOW, true, false, null);
// first pNode inherits global style assertSpans(subtitle, 20, "text 2", "sansSerif", TtmlStyle.STYLE_ITALIC, Color.RED,
TtmlNode body = queryChildrenForTag(root, TtmlNode.TAG_BODY, 0); Color.YELLOW, true, false, null);
TtmlNode firstDiv = queryChildrenForTag(body, TtmlNode.TAG_DIV, 0);
TtmlStyle firstPStyle = queryChildrenForTag(firstDiv, TtmlNode.TAG_P, 0).style;
assertEquals(Color.parseColor("yellow"), firstPStyle.getColor());
assertEquals(Color.parseColor("blue"), firstPStyle.getBackgroundColor());
assertEquals("serif", firstPStyle.getFontFamily());
assertEquals(TtmlStyle.STYLE_BOLD_ITALIC, firstPStyle.getStyle());
assertTrue(firstPStyle.isUnderline());
// second pNode inherits global style and overrides with attribute
TtmlNode secondDiv = queryChildrenForTag(body, TtmlNode.TAG_DIV, 1);
TtmlStyle secondPStyle = queryChildrenForTag(secondDiv, TtmlNode.TAG_P, 0).style;
assertEquals(Color.parseColor("yellow"), secondPStyle.getColor());
assertEquals(Color.parseColor("red"), secondPStyle.getBackgroundColor());
assertEquals("sansSerif", secondPStyle.getFontFamily());
assertEquals(TtmlStyle.STYLE_ITALIC, secondPStyle.getStyle());
assertTrue(secondPStyle.isUnderline());
} }
public void testInheritGlobalAndParent() throws IOException { public void testInheritGlobalAndParent() throws IOException {
TtmlSubtitle subtitle = getSubtitle(INHERIT_GLOBAL_AND_PARENT_TTML_FILE); TtmlSubtitle subtitle = getSubtitle(INHERIT_GLOBAL_AND_PARENT_TTML_FILE);
assertEquals(4, subtitle.getEventTimeCount()); assertEquals(4, subtitle.getEventTimeCount());
TtmlNode root = subtitle.getRoot(); assertSpans(subtitle, 10, "text 1", "sansSerif", TtmlStyle.STYLE_NORMAL,
Color.RED, Color.parseColor("lime"), false, true, Layout.Alignment.ALIGN_CENTER);
// first pNode inherits parent style assertSpans(subtitle, 20, "text 2", "serif", TtmlStyle.STYLE_BOLD_ITALIC,
TtmlNode body = queryChildrenForTag(root, TtmlNode.TAG_BODY, 0); Color.BLUE, Color.YELLOW, true, true, Layout.Alignment.ALIGN_CENTER);
TtmlNode firstDiv = queryChildrenForTag(body, TtmlNode.TAG_DIV, 0);
TtmlStyle firstPStyle = queryChildrenForTag(firstDiv, TtmlNode.TAG_P, 0).style;
assertFalse(firstPStyle.hasBackgroundColorSpecified());
assertEquals(0, firstPStyle.getBackgroundColor());
assertEquals(Color.parseColor("lime"), firstPStyle.getColor());
assertEquals("sansSerif", firstPStyle.getFontFamily());
assertEquals(TtmlStyle.STYLE_NORMAL, firstPStyle.getStyle());
assertTrue(firstPStyle.isLinethrough());
// second pNode inherits parent style and overrides with global style
TtmlNode secondDiv = queryChildrenForTag(body, TtmlNode.TAG_DIV, 1);
TtmlStyle secondPStyle = queryChildrenForTag(secondDiv, TtmlNode.TAG_P, 0).style;
// attributes overridden by global style
assertEquals(Color.parseColor("blue"), secondPStyle.getBackgroundColor());
assertEquals(Color.parseColor("yellow"), secondPStyle.getColor());
assertEquals(TtmlStyle.STYLE_BOLD_ITALIC, secondPStyle.getStyle());
assertEquals("serif", secondPStyle.getFontFamily());
assertTrue(secondPStyle.isUnderline());
assertEquals(Layout.Alignment.ALIGN_CENTER, secondPStyle.getTextAlign());
}
public void testNonInheritablePropertiesAreNotInherited() throws IOException {
TtmlSubtitle subtitle = getSubtitle(NON_INHERTABLE_PROPERTIES_TTML_FILE);
assertEquals(2, subtitle.getEventTimeCount());
TtmlNode root = subtitle.getRoot();
TtmlNode body = queryChildrenForTag(root, TtmlNode.TAG_BODY, 0);
TtmlNode firstDiv = queryChildrenForTag(body, TtmlNode.TAG_DIV, 0);
TtmlNode firstPStyle = queryChildrenForTag(firstDiv, TtmlNode.TAG_P, 0);
TtmlStyle spanStyle = queryChildrenForTag(firstPStyle, TtmlNode.TAG_SPAN, 0).style;
assertFalse("background color must not be inherited from a context node",
spanStyle.hasBackgroundColorSpecified());
} }
public void testInheritMultipleStyles() throws IOException { public void testInheritMultipleStyles() throws IOException {
TtmlSubtitle subtitle = getSubtitle(INHERIT_MULTIPLE_STYLES_TTML_FILE); TtmlSubtitle subtitle = getSubtitle(INHERIT_MULTIPLE_STYLES_TTML_FILE);
assertEquals(12, subtitle.getEventTimeCount()); assertEquals(12, subtitle.getEventTimeCount());
TtmlNode root = subtitle.getRoot(); assertSpans(subtitle, 10, "text 1", "sansSerif", TtmlStyle.STYLE_BOLD_ITALIC,
Color.BLUE, Color.YELLOW, false, true, null);
TtmlNode body = queryChildrenForTag(root, TtmlNode.TAG_BODY, 0);
TtmlNode firstDiv = queryChildrenForTag(body, TtmlNode.TAG_DIV, 0);
TtmlStyle firstPStyle = queryChildrenForTag(firstDiv, TtmlNode.TAG_P, 0).style;
assertEquals(Color.parseColor("blue"), firstPStyle.getBackgroundColor());
assertEquals(Color.parseColor("yellow"), firstPStyle.getColor());
assertEquals("sansSerif", firstPStyle.getFontFamily());
assertEquals(TtmlStyle.STYLE_BOLD_ITALIC, firstPStyle.getStyle());
assertTrue(firstPStyle.isLinethrough());
} }
public void testInheritMultipleStylesWithoutLocalAttributes() throws IOException { public void testInheritMultipleStylesWithoutLocalAttributes() throws IOException {
TtmlSubtitle subtitle = getSubtitle(INHERIT_MULTIPLE_STYLES_TTML_FILE); TtmlSubtitle subtitle = getSubtitle(INHERIT_MULTIPLE_STYLES_TTML_FILE);
assertEquals(12, subtitle.getEventTimeCount()); assertEquals(12, subtitle.getEventTimeCount());
TtmlNode root = subtitle.getRoot(); assertSpans(subtitle, 20, "text 2", "sansSerif", TtmlStyle.STYLE_BOLD_ITALIC,
Color.BLUE, Color.BLACK, false, true, null);
TtmlNode body = queryChildrenForTag(root, TtmlNode.TAG_BODY, 0);
TtmlNode secondDiv = queryChildrenForTag(body, TtmlNode.TAG_DIV, 1);
TtmlStyle firstPStyle = queryChildrenForTag(secondDiv, TtmlNode.TAG_P, 0).style;
assertEquals(Color.parseColor("blue"), firstPStyle.getBackgroundColor());
assertEquals(Color.parseColor("black"), firstPStyle.getColor());
assertEquals("sansSerif", firstPStyle.getFontFamily());
assertEquals(TtmlStyle.STYLE_BOLD_ITALIC, firstPStyle.getStyle());
assertTrue(firstPStyle.isLinethrough());
} }
@ -211,21 +129,8 @@ public final class TtmlParserTest extends InstrumentationTestCase {
TtmlSubtitle subtitle = getSubtitle(INHERIT_MULTIPLE_STYLES_TTML_FILE); TtmlSubtitle subtitle = getSubtitle(INHERIT_MULTIPLE_STYLES_TTML_FILE);
assertEquals(12, subtitle.getEventTimeCount()); assertEquals(12, subtitle.getEventTimeCount());
TtmlNode root = subtitle.getRoot(); assertSpans(subtitle, 30, "text 2.5", "sansSerifInline", TtmlStyle.STYLE_ITALIC,
Color.RED, Color.YELLOW, true, true, null);
TtmlNode body = queryChildrenForTag(root, TtmlNode.TAG_BODY, 0);
TtmlNode thirdDiv = queryChildrenForTag(body, TtmlNode.TAG_DIV, 2);
TtmlStyle firstPStyle = queryChildrenForTag(thirdDiv, TtmlNode.TAG_P, 0).style;
// inherit from first global style
assertEquals(Color.parseColor("red"), firstPStyle.getBackgroundColor());
// inherit from second global style
assertTrue(firstPStyle.isLinethrough());
// inherited from parent node
assertEquals("sansSerifInline", firstPStyle.getFontFamily());
assertEquals(TtmlStyle.STYLE_ITALIC, firstPStyle.getStyle());
assertTrue(firstPStyle.isUnderline());
assertEquals(Color.parseColor("yellow"), firstPStyle.getColor());
} }
public void testEmptyStyleAttribute() throws IOException { public void testEmptyStyleAttribute() throws IOException {
@ -236,8 +141,7 @@ public final class TtmlParserTest extends InstrumentationTestCase {
TtmlNode body = queryChildrenForTag(root, TtmlNode.TAG_BODY, 0); TtmlNode body = queryChildrenForTag(root, TtmlNode.TAG_BODY, 0);
TtmlNode fourthDiv = queryChildrenForTag(body, TtmlNode.TAG_DIV, 3); TtmlNode fourthDiv = queryChildrenForTag(body, TtmlNode.TAG_DIV, 3);
// no styles specified assertNull(queryChildrenForTag(fourthDiv, TtmlNode.TAG_P, 0).getStyleIds());
assertNull(queryChildrenForTag(fourthDiv, TtmlNode.TAG_P, 0).style);
} }
public void testNonexistingStyleId() throws IOException { public void testNonexistingStyleId() throws IOException {
@ -248,11 +152,10 @@ public final class TtmlParserTest extends InstrumentationTestCase {
TtmlNode body = queryChildrenForTag(root, TtmlNode.TAG_BODY, 0); TtmlNode body = queryChildrenForTag(root, TtmlNode.TAG_BODY, 0);
TtmlNode fifthDiv = queryChildrenForTag(body, TtmlNode.TAG_DIV, 4); TtmlNode fifthDiv = queryChildrenForTag(body, TtmlNode.TAG_DIV, 4);
// no styles specified assertEquals(1, queryChildrenForTag(fifthDiv, TtmlNode.TAG_P, 0).getStyleIds().length);
assertNull(queryChildrenForTag(fifthDiv, TtmlNode.TAG_P, 0).style);
} }
public void testNonExistingAndExistingStyleId() throws IOException { public void testNonExistingAndExistingStyleIdWithRedundantSpaces() throws IOException {
TtmlSubtitle subtitle = getSubtitle(INHERIT_MULTIPLE_STYLES_TTML_FILE); TtmlSubtitle subtitle = getSubtitle(INHERIT_MULTIPLE_STYLES_TTML_FILE);
assertEquals(12, subtitle.getEventTimeCount()); assertEquals(12, subtitle.getEventTimeCount());
@ -260,28 +163,30 @@ public final class TtmlParserTest extends InstrumentationTestCase {
TtmlNode body = queryChildrenForTag(root, TtmlNode.TAG_BODY, 0); TtmlNode body = queryChildrenForTag(root, TtmlNode.TAG_BODY, 0);
TtmlNode sixthDiv = queryChildrenForTag(body, TtmlNode.TAG_DIV, 5); TtmlNode sixthDiv = queryChildrenForTag(body, TtmlNode.TAG_DIV, 5);
// no styles specified String[] styleIds = queryChildrenForTag(sixthDiv, TtmlNode.TAG_P, 0).getStyleIds();
TtmlStyle style = queryChildrenForTag(sixthDiv, TtmlNode.TAG_P, 0).style; assertEquals(2, styleIds.length);
assertNotNull(style);
assertEquals(Color.RED, style.getBackgroundColor());
} }
public void testMultipleChaining() throws IOException { public void testMultipleChaining() throws IOException {
TtmlSubtitle subtitle = getSubtitle(CHAIN_MULTIPLE_STYLES_TTML_FILE); TtmlSubtitle subtitle = getSubtitle(CHAIN_MULTIPLE_STYLES_TTML_FILE);
assertEquals(2, subtitle.getEventTimeCount()); assertEquals(2, subtitle.getEventTimeCount());
TtmlNode root = subtitle.getRoot(); Map<String, TtmlStyle> globalStyles = subtitle.getGlobalStyles();
TtmlNode body = queryChildrenForTag(root, TtmlNode.TAG_BODY, 0);
TtmlNode div = queryChildrenForTag(body, TtmlNode.TAG_DIV, 0);
// no styles specified TtmlStyle style = globalStyles.get("s2");
TtmlStyle style = queryChildrenForTag(div, TtmlNode.TAG_P, 0).style;
assertEquals("serif", style.getFontFamily()); assertEquals("serif", style.getFontFamily());
assertEquals(Color.RED, style.getBackgroundColor()); assertEquals(Color.RED, style.getBackgroundColor());
assertEquals(Color.BLACK, style.getColor()); assertEquals(Color.BLACK, style.getColor());
assertEquals(TtmlStyle.STYLE_BOLD_ITALIC, style.getStyle()); assertEquals(TtmlStyle.STYLE_BOLD_ITALIC, style.getStyle());
assertTrue(style.isLinethrough()); assertTrue(style.isLinethrough());
style = globalStyles.get("s3");
// only difference: color must be RED
assertEquals(Color.RED, style.getColor());
assertEquals("serif", style.getFontFamily());
assertEquals(Color.RED, style.getBackgroundColor());
assertEquals(TtmlStyle.STYLE_BOLD_ITALIC, style.getStyle());
assertTrue(style.isLinethrough());
} }
public void testNoUnderline() throws IOException { public void testNoUnderline() throws IOException {
@ -309,32 +214,6 @@ public final class TtmlParserTest extends InstrumentationTestCase {
style.isLinethrough()); style.isLinethrough());
} }
public void testOnlySingleInstance() throws IOException {
TtmlSubtitle subtitle = getSubtitle(INSTANCE_CREATION_TTML_FILE);
assertEquals(4, subtitle.getEventTimeCount());
TtmlNode root = subtitle.getRoot();
TtmlNode body = queryChildrenForTag(root, TtmlNode.TAG_BODY, 0);
TtmlNode firstDiv = queryChildrenForTag(body, TtmlNode.TAG_DIV, 0);
TtmlNode secondDiv = queryChildrenForTag(body, TtmlNode.TAG_DIV, 1);
TtmlNode thirdDiv = queryChildrenForTag(body, TtmlNode.TAG_DIV, 2);
TtmlNode firstP = queryChildrenForTag(firstDiv, TtmlNode.TAG_P, 0);
TtmlNode secondP = queryChildrenForTag(secondDiv, TtmlNode.TAG_P, 0);
TtmlNode secondSpan = queryChildrenForTag(secondP, TtmlNode.TAG_SPAN, 0);
TtmlNode thirdP = queryChildrenForTag(thirdDiv, TtmlNode.TAG_P, 0);
TtmlNode thirdSpan = queryChildrenForTag(secondP, TtmlNode.TAG_SPAN, 0);
// inherit the same instance down the tree if possible
assertSame(body.style, firstP.style);
assertSame(firstP.style, secondP.style);
assertSame(secondP.style, secondSpan.style);
// if a backgroundColor is involved it does not help
assertNotSame(thirdP.style.getInheritableStyle(), thirdSpan.style);
}
public void testNamspaceConfusionDoesNotHurt() throws IOException { public void testNamspaceConfusionDoesNotHurt() throws IOException {
TtmlSubtitle subtitle = getSubtitle(NAMESPACE_CONFUSION_TTML_FILE); TtmlSubtitle subtitle = getSubtitle(NAMESPACE_CONFUSION_TTML_FILE);
assertEquals(2, subtitle.getEventTimeCount()); assertEquals(2, subtitle.getEventTimeCount());
@ -373,6 +252,60 @@ public final class TtmlParserTest extends InstrumentationTestCase {
} }
private void assertSpans(TtmlSubtitle subtitle, int second,
String text, String font, int fontStyle,
int backgroundColor, int color, boolean isUnderline,
boolean isLinethrough, Layout.Alignment alignment) {
long timeUs = second * 1000000;
List<Cue> cues = subtitle.getCues(timeUs);
assertEquals(1, cues.size());
assertEquals(text, String.valueOf(cues.get(0).text));
assertEquals("single cue expected for timeUs: " + timeUs, 1, cues.size());
SpannableStringBuilder spannable = (SpannableStringBuilder) cues.get(0).text;
TypefaceSpan[] typefaceSpans = spannable.getSpans(0, spannable.length(), TypefaceSpan.class);
assertEquals(font, typefaceSpans[typefaceSpans.length - 1].getFamily());
StyleSpan[] styleSpans = spannable.getSpans(0, spannable.length(), StyleSpan.class);
assertEquals(fontStyle, styleSpans[styleSpans.length - 1].getStyle());
UnderlineSpan[] underlineSpans = spannable.getSpans(0, spannable.length(),
UnderlineSpan.class);
assertEquals(isUnderline ? "must be underlined" : "must not be underlined",
isUnderline ? 1 : 0, underlineSpans.length);
StrikethroughSpan[] striketroughSpans = spannable.getSpans(0, spannable.length(),
StrikethroughSpan.class);
assertEquals(isLinethrough ? "must be strikethrough" : "must not be strikethrough",
isLinethrough ? 1 : 0, striketroughSpans.length);
BackgroundColorSpan[] backgroundColorSpans =
spannable.getSpans(0, spannable.length(), BackgroundColorSpan.class);
if (backgroundColor != 0) {
assertEquals(backgroundColor, backgroundColorSpans[backgroundColorSpans.length - 1]
.getBackgroundColor());
} else {
assertEquals(0, backgroundColorSpans.length);
}
ForegroundColorSpan[] foregroundColorSpans =
spannable.getSpans(0, spannable.length(), ForegroundColorSpan.class);
assertEquals(color, foregroundColorSpans[foregroundColorSpans.length - 1].getForegroundColor());
if (alignment != null) {
AlignmentSpan.Standard[] alignmentSpans =
spannable.getSpans(0, spannable.length(), AlignmentSpan.Standard.class);
assertEquals(1, alignmentSpans.length);
assertEquals(alignment, alignmentSpans[0].getAlignment());
} else {
assertEquals(0, spannable.getSpans
(0, spannable.length(), AlignmentSpan.Standard.class).length);
}
}
private TtmlNode queryChildrenForTag(TtmlNode node, String tag, int pos) { private TtmlNode queryChildrenForTag(TtmlNode node, String tag, int pos) {
int count = 0; int count = 0;
for (int i = 0; i < node.getChildCount(); i++) { for (int i = 0; i < node.getChildCount(); i++) {

View File

@ -0,0 +1,111 @@
package com.google.android.exoplayer.text.ttml;
/*
* Copyright (C) 2015 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import android.graphics.Color;
import android.test.InstrumentationTestCase;
import java.util.HashMap;
import java.util.Map;
/**
* Unit test for <code>TtmlRenderUtil</code>
*/
public class TtmlRenderUtilTest extends InstrumentationTestCase {
public void testResolveStyleNoStyleAtAll() {
assertNull(TtmlRenderUtil.resolveStyle(null, null, null));
}
public void testResolveStyleSingleReferentialStyle() {
Map<String, TtmlStyle> globalStyles = getGlobalStyles();
String[] styleIds = {"s0"};
assertSame(globalStyles.get("s0"),
TtmlRenderUtil.resolveStyle(null, styleIds, globalStyles));
}
public void testResolveStyleMultipleReferentialStyles() {
Map<String, TtmlStyle> globalStyles = getGlobalStyles();
String[] styleIds = {"s0", "s1"};
TtmlStyle resolved = TtmlRenderUtil.resolveStyle(null, styleIds, globalStyles);
assertNotSame(globalStyles.get("s0"), resolved);
assertNotSame(globalStyles.get("s1"), resolved);
assertNull(resolved.getId());
// inherited from s0
assertEquals(Color.BLACK, resolved.getBackgroundColor());
// inherited from s1
assertEquals(Color.RED, resolved.getColor());
// merged from s0 and s1
assertEquals(TtmlStyle.STYLE_BOLD_ITALIC, resolved.getStyle());
}
public void testResolveMergeSingleReferentialStyleIntoInlineStyle() {
Map<String, TtmlStyle> globalStyles = getGlobalStyles();
String[] styleIds = {"s0"};
TtmlStyle style = new TtmlStyle();
style.setBackgroundColor(Color.YELLOW);
TtmlStyle resolved = TtmlRenderUtil.resolveStyle(style, styleIds, globalStyles);
assertSame(style, resolved);
// inline attribute not overridden
assertEquals(Color.YELLOW, resolved.getBackgroundColor());
// inherited from referential style
assertEquals(TtmlStyle.STYLE_BOLD, resolved.getStyle());
}
public void testResolveMergeMultipleReferentialStylesIntoInlineStyle() {
Map<String, TtmlStyle> globalStyles = getGlobalStyles();
String[] styleIds = {"s0", "s1"};
TtmlStyle style = new TtmlStyle();
style.setBackgroundColor(Color.YELLOW);
TtmlStyle resolved = TtmlRenderUtil.resolveStyle(style, styleIds, globalStyles);
assertSame(style, resolved);
// inline attribute not overridden
assertEquals(Color.YELLOW, resolved.getBackgroundColor());
// inherited from both referential style
assertEquals(TtmlStyle.STYLE_BOLD_ITALIC, resolved.getStyle());
}
public void testResolveStyleOnlyInlineStyle() {
TtmlStyle inlineStyle = new TtmlStyle();
assertSame(inlineStyle, TtmlRenderUtil.resolveStyle(inlineStyle, null, null));
}
private Map<String, TtmlStyle> getGlobalStyles() {
Map<String, TtmlStyle> globalStyles = new HashMap<>();
TtmlStyle s0 = new TtmlStyle();
s0.setId("s0");
s0.setBackgroundColor(Color.BLACK);
s0.setBold(true);
globalStyles.put(s0.getId(), s0);
TtmlStyle s1 = new TtmlStyle();
s1.setId("s1");
s1.setBackgroundColor(Color.RED);
s1.setColor(Color.RED);
s1.setItalic(true);
globalStyles.put(s1.getId(), s1);
return globalStyles;
}
}

View File

@ -15,20 +15,12 @@
*/ */
package com.google.android.exoplayer.text.ttml; package com.google.android.exoplayer.text.ttml;
import android.text.Spannable;
import android.text.SpannableStringBuilder; import android.text.SpannableStringBuilder;
import android.text.Spanned;
import android.text.style.AlignmentSpan;
import android.text.style.BackgroundColorSpan;
import android.text.style.ForegroundColorSpan;
import android.text.style.StrikethroughSpan;
import android.text.style.StyleSpan;
import android.text.style.TypefaceSpan;
import android.text.style.UnderlineSpan;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Iterator; import java.util.Iterator;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.TreeSet; import java.util.TreeSet;
/** /**
@ -82,22 +74,28 @@ import java.util.TreeSet;
public final long startTimeUs; public final long startTimeUs;
public final long endTimeUs; public final long endTimeUs;
public final TtmlStyle style; public final TtmlStyle style;
private String[] styleIds;
private List<TtmlNode> children; private List<TtmlNode> children;
private int start;
private int end;
public static TtmlNode buildTextNode(String text, TtmlStyle style) { public static TtmlNode buildTextNode(String text) {
return new TtmlNode(null, applyTextElementSpacePolicy(text), UNDEFINED_TIME, return new TtmlNode(null, TtmlRenderUtil.applyTextElementSpacePolicy(text), UNDEFINED_TIME,
UNDEFINED_TIME, style); UNDEFINED_TIME, null, null);
} }
public static TtmlNode buildNode(String tag, long startTimeUs, long endTimeUs, TtmlStyle style) { public static TtmlNode buildNode(String tag, long startTimeUs, long endTimeUs,
return new TtmlNode(tag, null, startTimeUs, endTimeUs, style); TtmlStyle style, String[] styleIds) {
return new TtmlNode(tag, null, startTimeUs, endTimeUs, style, styleIds);
} }
private TtmlNode(String tag, String text, long startTimeUs, long endTimeUs, TtmlStyle style) { private TtmlNode(String tag, String text, long startTimeUs, long endTimeUs,
TtmlStyle style, String[] styleIds) {
this.tag = tag; this.tag = tag;
this.text = text; this.text = text;
this.style = style; this.style = style;
this.styleIds = styleIds;
this.isTextNode = text != null; this.isTextNode = text != null;
this.startTimeUs = startTimeUs; this.startTimeUs = startTimeUs;
this.endTimeUs = endTimeUs; this.endTimeUs = endTimeUs;
@ -159,8 +157,14 @@ import java.util.TreeSet;
} }
} }
public CharSequence getText(long timeUs) { public String[] getStyleIds() {
SpannableStringBuilder builder = getText(timeUs, new SpannableStringBuilder(), false); return styleIds;
}
public CharSequence getText(long timeUs, Map<String, TtmlStyle> globalStyles) {
SpannableStringBuilder builder = new SpannableStringBuilder();
traverseForText(timeUs, builder, false);
traverseForStyle(builder, globalStyles);
// Having joined the text elements, we need to do some final cleanup on the result. // Having joined the text elements, we need to do some final cleanup on the result.
// 1. Collapse multiple consecutive spaces into a single space. // 1. Collapse multiple consecutive spaces into a single space.
int builderLength = builder.length(); int builderLength = builder.length();
@ -208,12 +212,12 @@ import java.util.TreeSet;
return builder; return builder;
} }
private SpannableStringBuilder getText(long timeUs, SpannableStringBuilder builder, private SpannableStringBuilder traverseForText(long timeUs, SpannableStringBuilder builder,
boolean descendsPNode) { boolean descendsPNode) {
start = builder.length();
end = start;
if (isTextNode && descendsPNode) { if (isTextNode && descendsPNode) {
int start = builder.length();
builder.append(text); builder.append(text);
applyStylesToSpan(builder, start, builder.length(), style);
} else if (TAG_BR.equals(tag) && descendsPNode) { } else if (TAG_BR.equals(tag) && descendsPNode) {
builder.append('\n'); builder.append('\n');
} else if (TAG_METADATA.equals(tag)) { } else if (TAG_METADATA.equals(tag)) {
@ -221,79 +225,27 @@ import java.util.TreeSet;
} else if (isActive(timeUs)) { } else if (isActive(timeUs)) {
boolean isPNode = TAG_P.equals(tag); boolean isPNode = TAG_P.equals(tag);
for (int i = 0; i < getChildCount(); ++i) { for (int i = 0; i < getChildCount(); ++i) {
getChild(i).getText(timeUs, builder, descendsPNode || isPNode); getChild(i).traverseForText(timeUs, builder, descendsPNode || isPNode);
} }
if (isPNode) { if (isPNode) {
endParagraph(builder); TtmlRenderUtil.endParagraph(builder);
} }
end = builder.length();
} }
return builder; return builder;
} }
private static void applyStylesToSpan(SpannableStringBuilder builder, private void traverseForStyle(SpannableStringBuilder builder,
int start, int end, TtmlStyle style) { Map<String, TtmlStyle> globalStyles) {
if (start != end) {
if (style.getStyle() != TtmlStyle.UNSPECIFIED) { TtmlStyle resolvedStyle = TtmlRenderUtil.resolveStyle(style, styleIds, globalStyles);
builder.setSpan(new StyleSpan(style.getStyle()), start, end, if (resolvedStyle != null) {
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); TtmlRenderUtil.applyStylesToSpan(builder, start, end, resolvedStyle);
} }
if (style.isLinethrough()) { for (int i = 0; i < getChildCount(); ++i) {
builder.setSpan(new StrikethroughSpan(), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); getChild(i).traverseForStyle(builder, globalStyles);
}
if (style.isUnderline()) {
builder.setSpan(new UnderlineSpan(), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
}
if (style.hasColorSpecified()) {
builder.setSpan(new ForegroundColorSpan(style.getColor()), start, end,
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
}
if (style.hasBackgroundColorSpecified()) {
builder.setSpan(new BackgroundColorSpan(style.getBackgroundColor()), start, end,
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
}
if (style.getFontFamily() != null) {
builder.setSpan(new TypefaceSpan(style.getFontFamily()), start, end,
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
}
if (style.getTextAlign() != null) {
builder.setSpan(new AlignmentSpan.Standard(style.getTextAlign()), start, end,
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
} }
} }
/**
* Invoked when the end of a paragraph is encountered. Adds a newline if there are one or more
* non-space characters since the previous newline.
*
* @param builder The builder.
*/
private static void endParagraph(SpannableStringBuilder builder) {
int position = builder.length() - 1;
while (position >= 0 && builder.charAt(position) == ' ') {
position--;
}
if (position >= 0 && builder.charAt(position) != '\n') {
builder.append('\n');
}
}
/**
* Applies the appropriate space policy to the given text element.
*
* @param in The text element to which the policy should be applied.
* @return The result of applying the policy to the text element.
*/
private static String applyTextElementSpacePolicy(String in) {
// Removes carriage return followed by line feed. See: http://www.w3.org/TR/xml/#sec-line-ends
String out = in.replaceAll("\r\n", "\n");
// Apply suppress-at-line-break="auto" and
// white-space-treatment="ignore-if-surrounding-linefeed"
out = out.replaceAll(" *\n *", "\n");
// Apply linefeed-treatment="treat-as-space"
out = out.replaceAll("\n", " ");
// Apply white-space-collapse="true"
out = out.replaceAll("[ \t\\x0B\f\r]+", " ");
return out;
} }
} }

View File

@ -127,7 +127,7 @@ public final class TtmlParser implements SubtitleParser {
parseHeader(xmlParser, globalStyles); parseHeader(xmlParser, globalStyles);
} else { } else {
try { try {
TtmlNode node = parseNode(xmlParser, parent, globalStyles); TtmlNode node = parseNode(xmlParser, parent);
nodeStack.addLast(node); nodeStack.addLast(node);
if (parent != null) { if (parent != null) {
parent.addChild(node); parent.addChild(node);
@ -143,10 +143,10 @@ public final class TtmlParser implements SubtitleParser {
} }
} }
} else if (eventType == XmlPullParser.TEXT) { } else if (eventType == XmlPullParser.TEXT) {
parent.addChild(TtmlNode.buildTextNode(xmlParser.getText(), parent.style)); parent.addChild(TtmlNode.buildTextNode(xmlParser.getText()));
} else if (eventType == XmlPullParser.END_TAG) { } else if (eventType == XmlPullParser.END_TAG) {
if (xmlParser.getName().equals(TtmlNode.TAG_TT)) { if (xmlParser.getName().equals(TtmlNode.TAG_TT)) {
ttmlSubtitle = new TtmlSubtitle(nodeStack.getLast()); ttmlSubtitle = new TtmlSubtitle(nodeStack.getLast(), globalStyles);
} }
nodeStack.removeLast(); nodeStack.removeLast();
} }
@ -176,7 +176,7 @@ public final class TtmlParser implements SubtitleParser {
String parentStyleId = xmlParser.getAttributeValue(null, ATTR_STYLE); String parentStyleId = xmlParser.getAttributeValue(null, ATTR_STYLE);
TtmlStyle style = parseStyleAttributes(xmlParser, new TtmlStyle()); TtmlStyle style = parseStyleAttributes(xmlParser, new TtmlStyle());
if (parentStyleId != null) { if (parentStyleId != null) {
String[] ids = parentStyleId.split(" "); String[] ids = parseStyleIds(parentStyleId);
for (int i = 0; i < ids.length; i++) { for (int i = 0; i < ids.length; i++) {
style.chain(globalStyles.get(ids[i])); style.chain(globalStyles.get(ids[i]));
} }
@ -189,6 +189,10 @@ public final class TtmlParser implements SubtitleParser {
return globalStyles; return globalStyles;
} }
private String[] parseStyleIds(String parentStyleIds) {
return parentStyleIds.split("\\s+");
}
private TtmlStyle parseStyleAttributes(XmlPullParser parser, TtmlStyle style) { private TtmlStyle parseStyleAttributes(XmlPullParser parser, TtmlStyle style) {
int attributeCount = parser.getAttributeCount(); int attributeCount = parser.getAttributeCount();
for (int i = 0; i < attributeCount; i++) { for (int i = 0; i < attributeCount; i++) {
@ -282,23 +286,14 @@ public final class TtmlParser implements SubtitleParser {
return MimeTypes.APPLICATION_TTML.equals(mimeType); return MimeTypes.APPLICATION_TTML.equals(mimeType);
} }
private TtmlNode parseNode(XmlPullParser parser, TtmlNode parent, private TtmlNode parseNode(XmlPullParser parser, TtmlNode parent) throws ParserException {
Map<String, TtmlStyle> globalStyles) throws ParserException {
long duration = 0; long duration = 0;
long startTime = TtmlNode.UNDEFINED_TIME; long startTime = TtmlNode.UNDEFINED_TIME;
long endTime = TtmlNode.UNDEFINED_TIME; long endTime = TtmlNode.UNDEFINED_TIME;
String[] styleIds = null;
int attributeCount = parser.getAttributeCount(); int attributeCount = parser.getAttributeCount();
TtmlStyle style = parseStyleAttributes(parser, null); TtmlStyle style = parseStyleAttributes(parser, null);
boolean hasInlineStyles = style != null;
if (parent != null && parent.style != null) {
if (hasInlineStyles) {
style.inherit(parent.style);
} else {
style = parent.style.getInheritableStyle();
}
}
for (int i = 0; i < attributeCount; i++) { for (int i = 0; i < attributeCount; i++) {
// TODO: check if it is safe to remove the namespace prefix
String attr = ParserUtil.removeNamespacePrefix(parser.getAttributeName(i)); String attr = ParserUtil.removeNamespacePrefix(parser.getAttributeName(i));
String value = parser.getAttributeValue(i); String value = parser.getAttributeValue(i);
if (attr.equals(ATTR_BEGIN)) { if (attr.equals(ATTR_BEGIN)) {
@ -312,32 +307,10 @@ public final class TtmlParser implements SubtitleParser {
DEFAULT_FRAMERATE, DEFAULT_SUBFRAMERATE, DEFAULT_TICKRATE); DEFAULT_FRAMERATE, DEFAULT_SUBFRAMERATE, DEFAULT_TICKRATE);
} else if (attr.equals(ATTR_STYLE)) { } else if (attr.equals(ATTR_STYLE)) {
// IDREFS: potentially multiple space delimited ids // IDREFS: potentially multiple space delimited ids
String[] ids = value.split(" "); String[] ids = parseStyleIds(value);
if (style == null) { if (ids.length > 0) {
// use global style without overriding styleIds = ids;
if (ids.length == 1) {
style = globalStyles.get(value);
} else if (ids.length > 1){
style = new TtmlStyle();
for (int j = 0; j < ids.length; j++) {
style.chain(globalStyles.get(ids[j]));
} }
}
} else if (hasInlineStyles) {
// local attributes inherits from global style
for (int j = 0; j < ids.length; j++) {
style.chain(globalStyles.get(ids[j]));
}
} else if (ids.length > 1 || (ids.length == 1 && style != globalStyles.get(ids[0]))) {
// merge global style and parent styles
TtmlStyle inheritedStyles = style;
style = new TtmlStyle();
for (int j = 0; j < ids.length; j++) {
style.chain(globalStyles.get(ids[j]));
}
style.inherit(inheritedStyles);
}
} else { } else {
// Do nothing. // Do nothing.
} }
@ -359,7 +332,7 @@ public final class TtmlParser implements SubtitleParser {
endTime = parent.endTimeUs; endTime = parent.endTimeUs;
} }
} }
return TtmlNode.buildNode(parser.getName(), startTime, endTime, style); return TtmlNode.buildNode(parser.getName(), startTime, endTime, style, styleIds);
} }
private static boolean isSupportedTag(String tag) { private static boolean isSupportedTag(String tag) {

View File

@ -0,0 +1,143 @@
/*
* Copyright (C) 2015 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer.text.ttml;
import android.text.Spannable;
import android.text.SpannableStringBuilder;
import android.text.Spanned;
import android.text.style.AlignmentSpan;
import android.text.style.BackgroundColorSpan;
import android.text.style.ForegroundColorSpan;
import android.text.style.StrikethroughSpan;
import android.text.style.StyleSpan;
import android.text.style.TypefaceSpan;
import android.text.style.UnderlineSpan;
import java.util.Map;
/**
* Package internal utility class to render styled <code>TtmlNode</code>s.
*/
/* package */ final class TtmlRenderUtil {
/* spans which are always the same can be reused to avoid object creation */
private static final StrikethroughSpan STRIKETHROUGH_SPAN = new StrikethroughSpan();
private static final UnderlineSpan UNDERLINE_SPAN = new UnderlineSpan();
private static final StyleSpan[] STYLE_SPANS = new StyleSpan[] {
new StyleSpan(TtmlStyle.STYLE_NORMAL),
new StyleSpan(TtmlStyle.STYLE_BOLD),
new StyleSpan(TtmlStyle.STYLE_ITALIC),
new StyleSpan(TtmlStyle.STYLE_BOLD_ITALIC),
};
public static TtmlStyle resolveStyle(TtmlStyle style, String[] styleIds,
Map<String, TtmlStyle> globalStyles) {
if (style == null && styleIds == null) {
// no styles at all
return null;
} else if (style == null && styleIds.length == 1) {
// only one single referential style present
return globalStyles.get(styleIds[0]);
} else if (style == null && styleIds.length > 1) {
// only multiple referential styles present
TtmlStyle chainedStyle = new TtmlStyle();
for (int i = 0; i < styleIds.length; i++) {
chainedStyle.chain(globalStyles.get(styleIds[i]));
}
return chainedStyle;
} else if (style != null && styleIds != null && styleIds.length == 1) {
// merge a single referential style into inline style
return style.chain(globalStyles.get(styleIds[0]));
} else if (style != null && styleIds != null && styleIds.length > 1) {
// merge multiple referential styles into inline style
for (int i = 0; i < styleIds.length; i++) {
style.chain(globalStyles.get(styleIds[i]));
}
return style;
}
// only inline styles available
return style;
}
public static void applyStylesToSpan(SpannableStringBuilder builder,
int start, int end, TtmlStyle style) {
if (style.getStyle() != TtmlStyle.UNSPECIFIED) {
builder.setSpan(STYLE_SPANS[style.getStyle()], start, end,
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
}
if (style.isLinethrough()) {
builder.setSpan(STRIKETHROUGH_SPAN, start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
}
if (style.isUnderline()) {
builder.setSpan(UNDERLINE_SPAN, start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
}
if (style.hasColorSpecified()) {
builder.setSpan(new ForegroundColorSpan(style.getColor()), start, end,
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
}
if (style.hasBackgroundColorSpecified()) {
builder.setSpan(new BackgroundColorSpan(style.getBackgroundColor()), start, end,
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
}
if (style.getFontFamily() != null) {
builder.setSpan(new TypefaceSpan(style.getFontFamily()), start, end,
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
}
if (style.getTextAlign() != null) {
builder.setSpan(new AlignmentSpan.Standard(style.getTextAlign()), start, end,
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
}
}
/**
* Invoked when the end of a paragraph is encountered. Adds a newline if there are one or more
* non-space characters since the previous newline.
*
* @param builder The builder.
*/
/* package */ static void endParagraph(SpannableStringBuilder builder) {
int position = builder.length() - 1;
while (position >= 0 && builder.charAt(position) == ' ') {
position--;
}
if (position >= 0 && builder.charAt(position) != '\n') {
builder.append('\n');
}
}
/**
* Applies the appropriate space policy to the given text element.
*
* @param in The text element to which the policy should be applied.
* @return The result of applying the policy to the text element.
*/
/* package */ static String applyTextElementSpacePolicy(String in) {
// Removes carriage return followed by line feed. See: http://www.w3.org/TR/xml/#sec-line-ends
String out = in.replaceAll("\r\n", "\n");
// Apply suppress-at-line-break="auto" and
// white-space-treatment="ignore-if-surrounding-linefeed"
out = out.replaceAll(" *\n *", "\n");
// Apply linefeed-treatment="treat-as-space"
out = out.replaceAll("\n", " ");
// Apply white-space-collapse="true"
out = out.replaceAll("[ \t\\x0B\f\r]+", " ");
return out;
}
private TtmlRenderUtil() {}
}

View File

@ -21,6 +21,7 @@ import com.google.android.exoplayer.util.Util;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Map;
/** /**
* A representation of a TTML subtitle. * A representation of a TTML subtitle.
@ -29,9 +30,12 @@ public final class TtmlSubtitle implements Subtitle {
private final TtmlNode root; private final TtmlNode root;
private final long[] eventTimesUs; private final long[] eventTimesUs;
private final Map<String, TtmlStyle> globalStyles;
public TtmlSubtitle(TtmlNode root) { public TtmlSubtitle(TtmlNode root, Map<String, TtmlStyle> globalStyles) {
this.root = root; this.root = root;
this.globalStyles = globalStyles != null
? Collections.unmodifiableMap(globalStyles) : Collections.<String, TtmlStyle>emptyMap();
this.eventTimesUs = root.getEventTimesUs(); this.eventTimesUs = root.getEventTimesUs();
} }
@ -63,7 +67,7 @@ public final class TtmlSubtitle implements Subtitle {
@Override @Override
public List<Cue> getCues(long timeUs) { public List<Cue> getCues(long timeUs) {
CharSequence cueText = root.getText(timeUs); CharSequence cueText = root.getText(timeUs, globalStyles);
if (cueText == null) { if (cueText == null) {
return Collections.<Cue>emptyList(); return Collections.<Cue>emptyList();
} else { } else {
@ -72,4 +76,8 @@ public final class TtmlSubtitle implements Subtitle {
} }
} }
/* @VisibleForTesting */
/* package */ Map<String, TtmlStyle> getGlobalStyles() {
return globalStyles;
}
} }