mirror of
https://github.com/androidx/media.git
synced 2025-05-10 00:59:51 +08:00
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:
parent
908e4dfd5d
commit
b6f15a17e0
@ -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>
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
|
@ -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>
|
|
@ -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++) {
|
||||||
|
@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -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);
|
||||||
|
}
|
||||||
|
for (int i = 0; i < getChildCount(); ++i) {
|
||||||
|
getChild(i).traverseForStyle(builder, globalStyles);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (style.isLinethrough()) {
|
|
||||||
builder.setSpan(new StrikethroughSpan(), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
|
||||||
}
|
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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) {
|
||||||
|
@ -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() {}
|
||||||
|
|
||||||
|
}
|
@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user