mirror of
https://github.com/androidx/media.git
synced 2025-05-21 23:56:32 +08:00
Add support for TTML regions.
------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=121564959
This commit is contained in:
parent
d978398faf
commit
1fea3fe3c0
@ -1,7 +1,8 @@
|
||||
<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">
|
||||
<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"
|
||||
|
@ -1,7 +1,8 @@
|
||||
<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">
|
||||
<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"
|
||||
|
@ -1,7 +1,8 @@
|
||||
<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">
|
||||
<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"
|
||||
|
29
library/src/androidTest/assets/ttml/multiple_regions.xml
Normal file
29
library/src/androidTest/assets/ttml/multiple_regions.xml
Normal file
@ -0,0 +1,29 @@
|
||||
<tt xmlns="http://www.w3.org/ns/ttml"
|
||||
xmlns="http://www.w3.org/2006/10/ttaf1"
|
||||
xmlns:id="http://www.w3.org/XML/1998/namespace"
|
||||
xmlns:ttp="http://www.w3.org/ns/ttml#parameter"
|
||||
xmlns:tts="http://www.w3.org/ns/ttml#styling"
|
||||
xmlns:ttm="http://www.w3.org/ns/ttml#metadata">
|
||||
<head>
|
||||
<layout>
|
||||
<region xml:id="region1" ttm:origin="10% 10%" extent="20% 20%"/>
|
||||
<region xml:id="region2" ttm:origin="40% 40%" extent="20% 20%"/>
|
||||
<region xml:id="region3" ttm:origin="10% 80%" extent="10% 10%"/>
|
||||
<region xml:id="region4" ttm:origin="60% 10%" extent="20% 20%"/>
|
||||
<region xml:id="ultimate" ttm:origin="45% 45%" extent="35% 35%"/>
|
||||
</layout>
|
||||
</head>
|
||||
<body>
|
||||
<div>
|
||||
<p begin="1s" end="4s" region="region1">lorem</p>
|
||||
<p begin="5s" end="8s" region="region2">ipsum</p>
|
||||
<p begin="9s" end="18s" region="region3">dolor</p>
|
||||
<p begin="1s" end="4s" region="region4">amet</p>
|
||||
</div>
|
||||
<div region="ultimate">
|
||||
<p begin="21s" end="34s">She first said this</p>
|
||||
<p begin="25s" end="34s">Then this</p>
|
||||
<p begin="29s" end="34s">Finally this</p>
|
||||
</div>
|
||||
</body>
|
||||
</tt>
|
@ -1,17 +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">
|
||||
<body>
|
||||
<div>
|
||||
<p begin="10s" end="18s"
|
||||
tts:backgroundColor="black"
|
||||
abc:fontFamily="sansSerif"
|
||||
def:fontStyle="italic"
|
||||
ghi:textDecoration="lineThrough"
|
||||
jkl:color="yellow">text 1</p>
|
||||
</div>
|
||||
</body>
|
||||
</tt>
|
||||
|
@ -1,13 +0,0 @@
|
||||
<tt>
|
||||
<body>
|
||||
<div>
|
||||
<p begin="10s" end="18s"
|
||||
tts:backgroundColor="black"
|
||||
abc:fontFamily="sansSerif"
|
||||
def:fontStyle="italic"
|
||||
ghi:textDecoration="lineThrough"
|
||||
jkl:color="yellow">text 1</p>
|
||||
</div>
|
||||
</body>
|
||||
</tt>
|
||||
|
@ -1,115 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2014 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.graphics.Color;
|
||||
import android.test.InstrumentationTestCase;
|
||||
|
||||
/**
|
||||
* Unit test for <code>TtmlColorParser</code>.
|
||||
*/
|
||||
public class TtmlColorParserTest extends InstrumentationTestCase {
|
||||
|
||||
public void testHexCodeParsing() {
|
||||
assertEquals(Color.WHITE, TtmlColorParser.parseColor("#FFFFFF"));
|
||||
assertEquals(Color.WHITE, TtmlColorParser.parseColor("#FFFFFFFF"));
|
||||
assertEquals(Color.parseColor("#FF123456"), TtmlColorParser.parseColor("#123456"));
|
||||
// Hex colors in TTML are RGBA, where-as {@link Color#parseColor} takes ARGB.
|
||||
assertEquals(Color.parseColor("#00FFFFFF"), TtmlColorParser.parseColor("#FFFFFF00"));
|
||||
assertEquals(Color.parseColor("#78123456"), TtmlColorParser.parseColor("#12345678"));
|
||||
}
|
||||
|
||||
public void testColorNameParsing() {
|
||||
assertEquals(TtmlColorParser.TRANSPARENT, TtmlColorParser.parseColor("transparent"));
|
||||
assertEquals(TtmlColorParser.BLACK, TtmlColorParser.parseColor("black"));
|
||||
assertEquals(TtmlColorParser.GRAY, TtmlColorParser.parseColor("gray"));
|
||||
assertEquals(TtmlColorParser.SILVER, TtmlColorParser.parseColor("silver"));
|
||||
assertEquals(TtmlColorParser.WHITE, TtmlColorParser.parseColor("white"));
|
||||
assertEquals(TtmlColorParser.MAROON, TtmlColorParser.parseColor("maroon"));
|
||||
assertEquals(TtmlColorParser.RED, TtmlColorParser.parseColor("red"));
|
||||
assertEquals(TtmlColorParser.PURPLE, TtmlColorParser.parseColor("purple"));
|
||||
assertEquals(TtmlColorParser.FUCHSIA, TtmlColorParser.parseColor("fuchsia"));
|
||||
assertEquals(TtmlColorParser.MAGENTA, TtmlColorParser.parseColor("magenta"));
|
||||
assertEquals(TtmlColorParser.GREEN, TtmlColorParser.parseColor("green"));
|
||||
assertEquals(TtmlColorParser.LIME, TtmlColorParser.parseColor("lime"));
|
||||
assertEquals(TtmlColorParser.OLIVE, TtmlColorParser.parseColor("olive"));
|
||||
assertEquals(TtmlColorParser.YELLOW, TtmlColorParser.parseColor("yellow"));
|
||||
assertEquals(TtmlColorParser.NAVY, TtmlColorParser.parseColor("navy"));
|
||||
assertEquals(TtmlColorParser.BLUE, TtmlColorParser.parseColor("blue"));
|
||||
assertEquals(TtmlColorParser.TEAL, TtmlColorParser.parseColor("teal"));
|
||||
assertEquals(TtmlColorParser.AQUA, TtmlColorParser.parseColor("aqua"));
|
||||
assertEquals(TtmlColorParser.CYAN, TtmlColorParser.parseColor("cyan"));
|
||||
}
|
||||
|
||||
public void testParseUnknownColor() {
|
||||
try {
|
||||
TtmlColorParser.parseColor("colorOfAnElectron");
|
||||
fail();
|
||||
} catch (IllegalArgumentException e) {
|
||||
// expected
|
||||
}
|
||||
}
|
||||
|
||||
public void testParseNull() {
|
||||
try {
|
||||
TtmlColorParser.parseColor(null);
|
||||
fail();
|
||||
} catch (IllegalArgumentException e) {
|
||||
// expected
|
||||
}
|
||||
}
|
||||
|
||||
public void testParseEmpty() {
|
||||
try {
|
||||
TtmlColorParser.parseColor("");
|
||||
fail();
|
||||
} catch (IllegalArgumentException e) {
|
||||
// expected
|
||||
}
|
||||
}
|
||||
|
||||
public void testRgbColorParsing() {
|
||||
assertEquals(Color.WHITE, TtmlColorParser.parseColor("rgb(255,255,255)"));
|
||||
// spaces do not matter
|
||||
assertEquals(Color.WHITE, TtmlColorParser.parseColor(" rgb ( 255, 255, 255)"));
|
||||
}
|
||||
|
||||
public void testRgbColorParsing_rgbValuesOutOfBounds() {
|
||||
int outOfBounds = TtmlColorParser.parseColor("rgb(999, 999, 999)");
|
||||
int color = Color.rgb(999, 999, 999);
|
||||
// behave like framework Color behaves
|
||||
assertEquals(color, outOfBounds);
|
||||
}
|
||||
|
||||
public void testRgbColorParsing_rgbValuesNegative() {
|
||||
try {
|
||||
TtmlColorParser.parseColor("rgb(-4, 55, 209)");
|
||||
fail();
|
||||
} catch (IllegalArgumentException e) {
|
||||
// expected
|
||||
}
|
||||
}
|
||||
|
||||
public void testRgbaColorParsing() {
|
||||
assertEquals(Color.WHITE, TtmlColorParser.parseColor("rgba(255,255,255,0)"));
|
||||
assertEquals(Color.argb(0, 255, 255, 255), TtmlColorParser.parseColor("rgba(255,255,255,255)"));
|
||||
assertEquals(Color.BLACK, TtmlColorParser.parseColor("rgba(0, 0, 0, 0)"));
|
||||
assertEquals(Color.argb(0, 0, 0, 0), TtmlColorParser.parseColor("rgba(0, 0, 0, 255)"));
|
||||
assertEquals(Color.RED, TtmlColorParser.parseColor("rgba(255, 0, 0, 0)"));
|
||||
assertEquals(Color.argb(0, 255, 0, 0), TtmlColorParser.parseColor("rgba(255, 0, 0, 255)"));
|
||||
assertEquals(Color.argb(205, 255, 0, 0), TtmlColorParser.parseColor("rgba(255, 0, 0, 50)"));
|
||||
}
|
||||
}
|
@ -51,10 +51,9 @@ public final class TtmlParserTest extends InstrumentationTestCase {
|
||||
private static final String INHERIT_MULTIPLE_STYLES_TTML_FILE =
|
||||
"ttml/inherit_multiple_styles.xml";
|
||||
private static final String CHAIN_MULTIPLE_STYLES_TTML_FILE = "ttml/chain_multiple_styles.xml";
|
||||
private static final String MULTIPLE_REGIONS_TTML_FILE = "ttml/multiple_regions.xml";
|
||||
private static final String NO_UNDERLINE_LINETHROUGH_TTML_FILE =
|
||||
"ttml/no_underline_linethrough.xml";
|
||||
private static final String NAMESPACE_CONFUSION_TTML_FILE = "ttml/namespace_confusion.xml";
|
||||
private static final String NAMESPACE_NOT_DECLARED_TTML_FILE = "ttml/namespace_not_declared.xml";
|
||||
private static final String FONT_SIZE_TTML_FILE = "ttml/font_size.xml";
|
||||
private static final String FONT_SIZE_MISSING_UNIT_TTML_FILE = "ttml/font_size_no_unit.xml";
|
||||
private static final String FONT_SIZE_INVALID_TTML_FILE = "ttml/font_size_invalid.xml";
|
||||
@ -69,7 +68,7 @@ public final class TtmlParserTest extends InstrumentationTestCase {
|
||||
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(ColorParser.parseTtmlColor("yellow"), firstPStyle.getColor());
|
||||
assertEquals(ColorParser.parseTtmlColor("yellow"), firstPStyle.getFontColor());
|
||||
assertEquals(ColorParser.parseTtmlColor("blue"), firstPStyle.getBackgroundColor());
|
||||
assertEquals("serif", firstPStyle.getFontFamily());
|
||||
assertEquals(TtmlStyle.STYLE_BOLD_ITALIC, firstPStyle.getStyle());
|
||||
@ -84,15 +83,14 @@ public final class TtmlParserTest extends InstrumentationTestCase {
|
||||
}
|
||||
|
||||
/**
|
||||
* regression test for devices on JellyBean where some named colors are not correctly defined
|
||||
* Regression test for devices on JellyBean where some named colors are not correctly defined
|
||||
* on framework level. Tests that <i>lime</i> resolves to <code>#FF00FF00</code> not
|
||||
* <code>#00FF00</code>.
|
||||
*
|
||||
* See: https://github.com/android/platform_frameworks_base/blob/jb-mr2-release/
|
||||
* graphics/java/android/graphics/Color.java#L414
|
||||
* https://github.com/android/platform_frameworks_base/blob/kitkat-mr2.2-release/
|
||||
* graphics/java/android/graphics/Color.java#L414
|
||||
*
|
||||
* @see <a href="https://github.com/android/platform_frameworks_base/blob/jb-mr2-release/graphics/java/android/graphics/Color.java#L414">
|
||||
* JellyBean Color</a>
|
||||
* <a href="https://github.com/android/platform_frameworks_base/blob/kitkat-mr2.2-release/graphics/java/android/graphics/Color.java#L414">
|
||||
* Kitkat Color</a>
|
||||
* @throws IOException thrown if reading subtitle file fails.
|
||||
*/
|
||||
public void testLime() throws IOException {
|
||||
@ -132,7 +130,6 @@ public final class TtmlParserTest extends InstrumentationTestCase {
|
||||
public void testInheritMultipleStyles() throws IOException {
|
||||
TtmlSubtitle subtitle = getSubtitle(INHERIT_MULTIPLE_STYLES_TTML_FILE);
|
||||
assertEquals(12, subtitle.getEventTimeCount());
|
||||
|
||||
assertSpans(subtitle, 10, "text 1", "sansSerif", TtmlStyle.STYLE_BOLD_ITALIC, 0xFF0000FF,
|
||||
0xFFFFFF00, false, true, null);
|
||||
}
|
||||
@ -140,7 +137,6 @@ public final class TtmlParserTest extends InstrumentationTestCase {
|
||||
public void testInheritMultipleStylesWithoutLocalAttributes() throws IOException {
|
||||
TtmlSubtitle subtitle = getSubtitle(INHERIT_MULTIPLE_STYLES_TTML_FILE);
|
||||
assertEquals(12, subtitle.getEventTimeCount());
|
||||
|
||||
assertSpans(subtitle, 20, "text 2", "sansSerif", TtmlStyle.STYLE_BOLD_ITALIC, 0xFF0000FF,
|
||||
0xFF000000, false, true, null);
|
||||
}
|
||||
@ -148,11 +144,54 @@ public final class TtmlParserTest extends InstrumentationTestCase {
|
||||
public void testMergeMultipleStylesWithParentStyle() throws IOException {
|
||||
TtmlSubtitle subtitle = getSubtitle(INHERIT_MULTIPLE_STYLES_TTML_FILE);
|
||||
assertEquals(12, subtitle.getEventTimeCount());
|
||||
|
||||
assertSpans(subtitle, 30, "text 2.5", "sansSerifInline", TtmlStyle.STYLE_ITALIC, 0xFFFF0000,
|
||||
0xFFFFFF00, true, true, null);
|
||||
}
|
||||
|
||||
public void testMultipleRegions() throws IOException {
|
||||
TtmlSubtitle subtitle = getSubtitle(MULTIPLE_REGIONS_TTML_FILE);
|
||||
List<Cue> output = subtitle.getCues(1000000);
|
||||
assertEquals(2, output.size());
|
||||
Cue ttmlCue = output.get(0);
|
||||
assertEquals("lorem", ttmlCue.text.toString());
|
||||
assertEquals(10.f / 100.f, ttmlCue.position);
|
||||
assertEquals(10.f / 100.f, ttmlCue.line);
|
||||
ttmlCue = output.get(1);
|
||||
assertEquals("amet", ttmlCue.text.toString());
|
||||
assertEquals(60.f / 100.f, ttmlCue.position);
|
||||
assertEquals(10.f / 100.f, ttmlCue.line);
|
||||
|
||||
output = subtitle.getCues(5000000);
|
||||
assertEquals(1, output.size());
|
||||
ttmlCue = output.get(0);
|
||||
assertEquals("ipsum", ttmlCue.text.toString());
|
||||
assertEquals(40.f / 100.f, ttmlCue.position);
|
||||
assertEquals(40.f / 100.f, ttmlCue.line);
|
||||
|
||||
output = subtitle.getCues(9000000);
|
||||
assertEquals(1, output.size());
|
||||
ttmlCue = output.get(0);
|
||||
assertEquals("dolor", ttmlCue.text.toString());
|
||||
assertEquals(10.f / 100.f, ttmlCue.position);
|
||||
assertEquals(80.f / 100.f, ttmlCue.line);
|
||||
|
||||
output = subtitle.getCues(21000000);
|
||||
assertEquals(1, output.size());
|
||||
ttmlCue = output.get(0);
|
||||
assertEquals("She first said this", ttmlCue.text.toString());
|
||||
assertEquals(45.f / 100.f, ttmlCue.position);
|
||||
assertEquals(45.f / 100.f, ttmlCue.line);
|
||||
output = subtitle.getCues(25000000);
|
||||
ttmlCue = output.get(0);
|
||||
assertEquals("She first said this\nThen this", ttmlCue.text.toString());
|
||||
output = subtitle.getCues(29000000);
|
||||
assertEquals(1, output.size());
|
||||
ttmlCue = output.get(0);
|
||||
assertEquals("She first said this\nThen this\nFinally this", ttmlCue.text.toString());
|
||||
assertEquals(45.f / 100.f, ttmlCue.position);
|
||||
assertEquals(45.f / 100.f, ttmlCue.line);
|
||||
}
|
||||
|
||||
public void testEmptyStyleAttribute() throws IOException {
|
||||
TtmlSubtitle subtitle = getSubtitle(INHERIT_MULTIPLE_STYLES_TTML_FILE);
|
||||
assertEquals(12, subtitle.getEventTimeCount());
|
||||
@ -196,13 +235,13 @@ public final class TtmlParserTest extends InstrumentationTestCase {
|
||||
TtmlStyle style = globalStyles.get("s2");
|
||||
assertEquals("serif", style.getFontFamily());
|
||||
assertEquals(0xFFFF0000, style.getBackgroundColor());
|
||||
assertEquals(0xFF000000, style.getColor());
|
||||
assertEquals(0xFF000000, style.getFontColor());
|
||||
assertEquals(TtmlStyle.STYLE_BOLD_ITALIC, style.getStyle());
|
||||
assertTrue(style.isLinethrough());
|
||||
|
||||
style = globalStyles.get("s3");
|
||||
// only difference: color must be RED
|
||||
assertEquals(0xFFFF0000, style.getColor());
|
||||
assertEquals(0xFFFF0000, style.getFontColor());
|
||||
assertEquals("serif", style.getFontFamily());
|
||||
assertEquals(0xFFFF0000, style.getBackgroundColor());
|
||||
assertEquals(TtmlStyle.STYLE_BOLD_ITALIC, style.getStyle());
|
||||
@ -234,43 +273,6 @@ public final class TtmlParserTest extends InstrumentationTestCase {
|
||||
style.isLinethrough());
|
||||
}
|
||||
|
||||
public void testNamspaceConfusionDoesNotHurt() throws IOException {
|
||||
TtmlSubtitle subtitle = getSubtitle(NAMESPACE_CONFUSION_TTML_FILE);
|
||||
assertEquals(2, subtitle.getEventTimeCount());
|
||||
|
||||
TtmlNode root = subtitle.getRoot();
|
||||
TtmlNode body = queryChildrenForTag(root, TtmlNode.TAG_BODY, 0);
|
||||
TtmlNode div = queryChildrenForTag(body, TtmlNode.TAG_DIV, 0);
|
||||
TtmlStyle style = queryChildrenForTag(div, TtmlNode.TAG_P, 0).style;
|
||||
|
||||
assertNotNull(style);
|
||||
assertEquals(0xFF000000, style.getBackgroundColor());
|
||||
assertEquals(0xFFFFFF00, style.getColor());
|
||||
assertEquals(TtmlStyle.STYLE_ITALIC, style.getStyle());
|
||||
assertEquals("sansSerif", style.getFontFamily());
|
||||
assertFalse(style.isUnderline());
|
||||
assertTrue(style.isLinethrough());
|
||||
|
||||
}
|
||||
|
||||
public void testNamespaceNotDeclared() throws IOException {
|
||||
TtmlSubtitle subtitle = getSubtitle(NAMESPACE_NOT_DECLARED_TTML_FILE);
|
||||
assertEquals(2, subtitle.getEventTimeCount());
|
||||
|
||||
TtmlNode root = subtitle.getRoot();
|
||||
TtmlNode body = queryChildrenForTag(root, TtmlNode.TAG_BODY, 0);
|
||||
TtmlNode div = queryChildrenForTag(body, TtmlNode.TAG_DIV, 0);
|
||||
TtmlStyle style = queryChildrenForTag(div, TtmlNode.TAG_P, 0).style;
|
||||
|
||||
assertNotNull(style);
|
||||
assertEquals(0xFF000000, style.getBackgroundColor());
|
||||
assertEquals(0xFFFFFF00, style.getColor());
|
||||
assertEquals(TtmlStyle.STYLE_ITALIC, style.getStyle());
|
||||
assertEquals("sansSerif", style.getFontFamily());
|
||||
assertFalse(style.isUnderline());
|
||||
assertTrue(style.isLinethrough());
|
||||
}
|
||||
|
||||
public void testFontSizeSpans() throws IOException {
|
||||
TtmlSubtitle subtitle = getSubtitle(FONT_SIZE_TTML_FILE);
|
||||
assertEquals(10, subtitle.getEventTimeCount());
|
||||
|
@ -48,7 +48,7 @@ public class TtmlRenderUtilTest extends InstrumentationTestCase {
|
||||
// inherited from s0
|
||||
assertEquals(Color.BLACK, resolved.getBackgroundColor());
|
||||
// inherited from s1
|
||||
assertEquals(Color.RED, resolved.getColor());
|
||||
assertEquals(Color.RED, resolved.getFontColor());
|
||||
// merged from s0 and s1
|
||||
assertEquals(TtmlStyle.STYLE_BOLD_ITALIC, resolved.getStyle());
|
||||
}
|
||||
@ -101,7 +101,7 @@ public class TtmlRenderUtilTest extends InstrumentationTestCase {
|
||||
TtmlStyle s1 = new TtmlStyle();
|
||||
s1.setId("s1");
|
||||
s1.setBackgroundColor(Color.RED);
|
||||
s1.setColor(Color.RED);
|
||||
s1.setFontColor(Color.RED);
|
||||
s1.setItalic(true);
|
||||
globalStyles.put(s1.getId(), s1);
|
||||
|
||||
|
@ -42,8 +42,8 @@ public final class TtmlStyleTest extends InstrumentationTestCase {
|
||||
assertTrue(style.isLinethrough());
|
||||
assertEquals(TtmlStyle.STYLE_BOLD_ITALIC, style.getStyle());
|
||||
assertEquals(FONT_FAMILY, style.getFontFamily());
|
||||
assertEquals(Color.WHITE, style.getColor());
|
||||
assertFalse("do not inherit backgroundColor", style.hasBackgroundColorSpecified());
|
||||
assertEquals(Color.WHITE, style.getFontColor());
|
||||
assertFalse("do not inherit backgroundColor", style.hasBackgroundColor());
|
||||
}
|
||||
|
||||
public void testChainStyle() {
|
||||
@ -53,7 +53,7 @@ public final class TtmlStyleTest extends InstrumentationTestCase {
|
||||
assertTrue(style.isLinethrough());
|
||||
assertEquals(TtmlStyle.STYLE_BOLD_ITALIC, style.getStyle());
|
||||
assertEquals(FONT_FAMILY, style.getFontFamily());
|
||||
assertEquals(FOREGROUND_COLOR, style.getColor());
|
||||
assertEquals(FOREGROUND_COLOR, style.getFontColor());
|
||||
// do inherit backgroundColor when chaining
|
||||
assertEquals("do not inherit backgroundColor when chaining",
|
||||
BACKGROUND_COLOR, style.getBackgroundColor());
|
||||
@ -65,7 +65,7 @@ public final class TtmlStyleTest extends InstrumentationTestCase {
|
||||
ancestor.setItalic(true);
|
||||
ancestor.setBold(true);
|
||||
ancestor.setBackgroundColor(BACKGROUND_COLOR);
|
||||
ancestor.setColor(FOREGROUND_COLOR);
|
||||
ancestor.setFontColor(FOREGROUND_COLOR);
|
||||
ancestor.setLinethrough(true);
|
||||
ancestor.setUnderline(true);
|
||||
ancestor.setFontFamily(FONT_FAMILY);
|
||||
@ -109,17 +109,17 @@ public final class TtmlStyleTest extends InstrumentationTestCase {
|
||||
}
|
||||
|
||||
public void testColor() {
|
||||
assertFalse(style.hasColorSpecified());
|
||||
style.setColor(Color.BLACK);
|
||||
assertEquals(Color.BLACK, style.getColor());
|
||||
assertTrue(style.hasColorSpecified());
|
||||
assertFalse(style.hasFontColor());
|
||||
style.setFontColor(Color.BLACK);
|
||||
assertEquals(Color.BLACK, style.getFontColor());
|
||||
assertTrue(style.hasFontColor());
|
||||
}
|
||||
|
||||
public void testBackgroundColor() {
|
||||
assertFalse(style.hasBackgroundColorSpecified());
|
||||
assertFalse(style.hasBackgroundColor());
|
||||
style.setBackgroundColor(Color.BLACK);
|
||||
assertEquals(Color.BLACK, style.getBackgroundColor());
|
||||
assertTrue(style.hasBackgroundColorSpecified());
|
||||
assertTrue(style.hasBackgroundColor());
|
||||
}
|
||||
|
||||
public void testId() {
|
||||
|
@ -1,140 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2014 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 com.google.android.exoplayer.util.Assertions;
|
||||
import com.google.android.exoplayer.util.Util;
|
||||
|
||||
import android.text.TextUtils;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* Parser to parse ttml color value expression
|
||||
* (http://www.w3.org/TR/ttml1/#style-value-color)
|
||||
*/
|
||||
/*package*/ final class TtmlColorParser {
|
||||
|
||||
private static final String RGB = "rgb";
|
||||
private static final String RGBA = "rgba";
|
||||
|
||||
private static final Pattern RGB_PATTERN = Pattern.compile(
|
||||
"^rgb\\((\\d{1,3}),(\\d{1,3}),(\\d{1,3})\\)$");
|
||||
|
||||
private static final Pattern RGBA_PATTERN = Pattern.compile(
|
||||
"^rgba\\((\\d{1,3}),(\\d{1,3}),(\\d{1,3}),(\\d{1,3})\\)$");
|
||||
|
||||
static final int TRANSPARENT = 0x00000000;
|
||||
static final int BLACK = 0xFF000000;
|
||||
static final int SILVER = 0xFFC0C0C0;
|
||||
static final int GRAY = 0xFF808080;
|
||||
static final int WHITE = 0xFFFFFFFF;
|
||||
static final int MAROON = 0xFF800000;
|
||||
static final int RED = 0xFFFF0000;
|
||||
static final int PURPLE = 0xFF800080;
|
||||
static final int FUCHSIA = 0xFFFF00FF;
|
||||
static final int MAGENTA = FUCHSIA;
|
||||
static final int GREEN = 0xFF008000;
|
||||
static final int LIME = 0xFF00FF00;
|
||||
static final int OLIVE = 0xFF808000;
|
||||
static final int YELLOW = 0xFFFFFF00;
|
||||
static final int NAVY = 0xFF000080;
|
||||
static final int BLUE = 0xFF0000FF;
|
||||
static final int TEAL = 0xFF008080;
|
||||
static final int AQUA = 0x00FFFFFF;
|
||||
static final int CYAN = 0xFF00FFFF;
|
||||
|
||||
private static final Map<String, Integer> COLOR_NAME_MAP;
|
||||
static {
|
||||
COLOR_NAME_MAP = new HashMap<>();
|
||||
COLOR_NAME_MAP.put("transparent", TRANSPARENT);
|
||||
COLOR_NAME_MAP.put("black", BLACK);
|
||||
COLOR_NAME_MAP.put("silver", SILVER);
|
||||
COLOR_NAME_MAP.put("gray", GRAY);
|
||||
COLOR_NAME_MAP.put("white", WHITE);
|
||||
COLOR_NAME_MAP.put("maroon", MAROON);
|
||||
COLOR_NAME_MAP.put("red", RED);
|
||||
COLOR_NAME_MAP.put("purple", PURPLE);
|
||||
COLOR_NAME_MAP.put("fuchsia", FUCHSIA);
|
||||
COLOR_NAME_MAP.put("magenta", MAGENTA);
|
||||
COLOR_NAME_MAP.put("green", GREEN);
|
||||
COLOR_NAME_MAP.put("lime", LIME);
|
||||
COLOR_NAME_MAP.put("olive", OLIVE);
|
||||
COLOR_NAME_MAP.put("yellow", YELLOW);
|
||||
COLOR_NAME_MAP.put("navy", NAVY);
|
||||
COLOR_NAME_MAP.put("blue", BLUE);
|
||||
COLOR_NAME_MAP.put("teal", TEAL);
|
||||
COLOR_NAME_MAP.put("aqua", AQUA);
|
||||
COLOR_NAME_MAP.put("cyan", CYAN);
|
||||
}
|
||||
|
||||
public static int parseColor(String colorExpression) {
|
||||
Assertions.checkArgument(!TextUtils.isEmpty(colorExpression));
|
||||
colorExpression = colorExpression.replace(" ", "");
|
||||
if (colorExpression.charAt(0) == '#') {
|
||||
// Parse using Long to avoid failure when colorExpression is greater than #7FFFFFFF.
|
||||
int color = (int) Long.parseLong(colorExpression.substring(1), 16);
|
||||
if (colorExpression.length() == 7) {
|
||||
// Set the alpha value
|
||||
color |= 0xFF000000;
|
||||
} else if (colorExpression.length() == 9) {
|
||||
// We have #RRGGBBAA, but we need #AARRGGBB
|
||||
color = ((color & 0xFF) << 24) | (color >>> 8);
|
||||
} else {
|
||||
throw new IllegalArgumentException();
|
||||
}
|
||||
return color;
|
||||
} else if (colorExpression.startsWith(RGBA)) {
|
||||
Matcher matcher = RGBA_PATTERN.matcher(colorExpression);
|
||||
if (matcher.matches()) {
|
||||
return argb(
|
||||
255 - Integer.parseInt(matcher.group(4), 10),
|
||||
Integer.parseInt(matcher.group(1), 10),
|
||||
Integer.parseInt(matcher.group(2), 10),
|
||||
Integer.parseInt(matcher.group(3), 10)
|
||||
);
|
||||
}
|
||||
} else if (colorExpression.startsWith(RGB)) {
|
||||
Matcher matcher = RGB_PATTERN.matcher(colorExpression);
|
||||
if (matcher.matches()) {
|
||||
return rgb(
|
||||
Integer.parseInt(matcher.group(1), 10),
|
||||
Integer.parseInt(matcher.group(2), 10),
|
||||
Integer.parseInt(matcher.group(3), 10)
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// we use our own color map
|
||||
Integer color = COLOR_NAME_MAP.get(Util.toLowerInvariant(colorExpression));
|
||||
if (color != null) {
|
||||
return color;
|
||||
}
|
||||
}
|
||||
throw new IllegalArgumentException();
|
||||
}
|
||||
|
||||
private static int argb(int alpha, int red, int green, int blue) {
|
||||
return (alpha << 24) | (red << 16) | (green << 8) | blue;
|
||||
}
|
||||
|
||||
private static int rgb(int red, int green, int blue) {
|
||||
return argb(0xFF, red, green, blue);
|
||||
}
|
||||
|
||||
}
|
@ -15,12 +15,17 @@
|
||||
*/
|
||||
package com.google.android.exoplayer.text.ttml;
|
||||
|
||||
import com.google.android.exoplayer.text.Cue;
|
||||
import com.google.android.exoplayer.util.Assertions;
|
||||
|
||||
import android.text.SpannableStringBuilder;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Iterator;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Map.Entry;
|
||||
import java.util.TreeMap;
|
||||
import java.util.TreeSet;
|
||||
|
||||
/**
|
||||
@ -45,6 +50,7 @@ import java.util.TreeSet;
|
||||
public static final String TAG_SMPTE_DATA = "smpte:data";
|
||||
public static final String TAG_SMPTE_INFORMATION = "smpte:information";
|
||||
|
||||
public static final String ANONYMOUS_REGION_ID = "";
|
||||
public static final String ATTR_ID = "id";
|
||||
public static final String ATTR_TTS_BACKGROUND_COLOR = "backgroundColor";
|
||||
public static final String ATTR_TTS_FONT_STYLE = "fontStyle";
|
||||
@ -52,6 +58,7 @@ import java.util.TreeSet;
|
||||
public static final String ATTR_TTS_FONT_FAMILY = "fontFamily";
|
||||
public static final String ATTR_TTS_FONT_WEIGHT = "fontWeight";
|
||||
public static final String ATTR_TTS_COLOR = "color";
|
||||
public static final String ATTR_TTS_ORIGIN = "origin";
|
||||
public static final String ATTR_TTS_TEXT_DECORATION = "textDecoration";
|
||||
public static final String ATTR_TTS_TEXT_ALIGN = "textAlign";
|
||||
|
||||
@ -74,24 +81,26 @@ import java.util.TreeSet;
|
||||
public final long startTimeUs;
|
||||
public final long endTimeUs;
|
||||
public final TtmlStyle style;
|
||||
public final String regionId;
|
||||
|
||||
private final String[] styleIds;
|
||||
private final HashMap<String, Integer> nodeStartsByRegion;
|
||||
private final HashMap<String, Integer> nodeEndsByRegion;
|
||||
|
||||
private List<TtmlNode> children;
|
||||
private int start;
|
||||
private int end;
|
||||
|
||||
public static TtmlNode buildTextNode(String text) {
|
||||
return new TtmlNode(null, TtmlRenderUtil.applyTextElementSpacePolicy(text), UNDEFINED_TIME,
|
||||
UNDEFINED_TIME, null, null);
|
||||
UNDEFINED_TIME, null, null, ANONYMOUS_REGION_ID);
|
||||
}
|
||||
|
||||
public static TtmlNode buildNode(String tag, long startTimeUs, long endTimeUs,
|
||||
TtmlStyle style, String[] styleIds) {
|
||||
return new TtmlNode(tag, null, startTimeUs, endTimeUs, style, styleIds);
|
||||
TtmlStyle style, String[] styleIds, String regionId) {
|
||||
return new TtmlNode(tag, null, startTimeUs, endTimeUs, style, styleIds, regionId);
|
||||
}
|
||||
|
||||
private TtmlNode(String tag, String text, long startTimeUs, long endTimeUs,
|
||||
TtmlStyle style, String[] styleIds) {
|
||||
TtmlStyle style, String[] styleIds, String regionId) {
|
||||
this.tag = tag;
|
||||
this.text = text;
|
||||
this.style = style;
|
||||
@ -99,6 +108,9 @@ import java.util.TreeSet;
|
||||
this.isTextNode = text != null;
|
||||
this.startTimeUs = startTimeUs;
|
||||
this.endTimeUs = endTimeUs;
|
||||
this.regionId = Assertions.checkNotNull(regionId);
|
||||
nodeStartsByRegion = new HashMap<>();
|
||||
nodeEndsByRegion = new HashMap<>();
|
||||
}
|
||||
|
||||
public boolean isActive(long timeUs) {
|
||||
@ -130,10 +142,8 @@ import java.util.TreeSet;
|
||||
TreeSet<Long> eventTimeSet = new TreeSet<>();
|
||||
getEventTimes(eventTimeSet, false);
|
||||
long[] eventTimes = new long[eventTimeSet.size()];
|
||||
Iterator<Long> eventTimeIterator = eventTimeSet.iterator();
|
||||
int i = 0;
|
||||
while (eventTimeIterator.hasNext()) {
|
||||
long eventTimeUs = eventTimeIterator.next();
|
||||
for (long eventTimeUs : eventTimeSet) {
|
||||
eventTimes[i++] = eventTimeUs;
|
||||
}
|
||||
return eventTimes;
|
||||
@ -161,10 +171,83 @@ import java.util.TreeSet;
|
||||
return styleIds;
|
||||
}
|
||||
|
||||
public CharSequence getText(long timeUs, Map<String, TtmlStyle> globalStyles) {
|
||||
SpannableStringBuilder builder = new SpannableStringBuilder();
|
||||
traverseForText(timeUs, builder, false);
|
||||
traverseForStyle(builder, globalStyles);
|
||||
public List<Cue> getCues(long timeUs, Map<String, TtmlStyle> globalStyles,
|
||||
Map<String, TtmlRegion> regionMap) {
|
||||
TreeMap<String, SpannableStringBuilder> regionOutputs = new TreeMap<>();
|
||||
traverseForText(timeUs, false, regionId, regionOutputs);
|
||||
traverseForStyle(globalStyles, regionOutputs);
|
||||
List<Cue> cues = new ArrayList<>();
|
||||
for (Entry<String, SpannableStringBuilder> entry : regionOutputs.entrySet()) {
|
||||
TtmlRegion region = regionMap.get(entry.getKey());
|
||||
cues.add(new Cue(cleanUpText(entry.getValue()), null, region.line, Cue.TYPE_UNSET,
|
||||
Cue.TYPE_UNSET, region.position, Cue.TYPE_UNSET, Cue.DIMEN_UNSET));
|
||||
}
|
||||
return cues;
|
||||
}
|
||||
|
||||
private void traverseForText(long timeUs, boolean descendsPNode,
|
||||
String inheritedRegion, Map<String, SpannableStringBuilder> regionOutputs) {
|
||||
nodeStartsByRegion.clear();
|
||||
nodeEndsByRegion.clear();
|
||||
String resolvedRegionId = regionId;
|
||||
if (ANONYMOUS_REGION_ID.equals(resolvedRegionId)) {
|
||||
resolvedRegionId = inheritedRegion;
|
||||
}
|
||||
if (isTextNode && descendsPNode) {
|
||||
getRegionOutput(resolvedRegionId, regionOutputs).append(text);
|
||||
} else if (TAG_BR.equals(tag) && descendsPNode) {
|
||||
getRegionOutput(resolvedRegionId, regionOutputs).append('\n');
|
||||
} else if (TAG_METADATA.equals(tag)) {
|
||||
// Do nothing.
|
||||
} else if (isActive(timeUs)) {
|
||||
boolean isPNode = TAG_P.equals(tag);
|
||||
for (Entry<String, SpannableStringBuilder> entry : regionOutputs.entrySet()) {
|
||||
nodeStartsByRegion.put(entry.getKey(), entry.getValue().length());
|
||||
}
|
||||
for (int i = 0; i < getChildCount(); i++) {
|
||||
getChild(i).traverseForText(timeUs, descendsPNode || isPNode, resolvedRegionId,
|
||||
regionOutputs);
|
||||
}
|
||||
if (isPNode) {
|
||||
TtmlRenderUtil.endParagraph(getRegionOutput(resolvedRegionId, regionOutputs));
|
||||
}
|
||||
for (Entry<String, SpannableStringBuilder> entry : regionOutputs.entrySet()) {
|
||||
nodeEndsByRegion.put(entry.getKey(), entry.getValue().length());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static SpannableStringBuilder getRegionOutput(String resolvedRegionId,
|
||||
Map<String, SpannableStringBuilder> regionOutputs) {
|
||||
if (!regionOutputs.containsKey(resolvedRegionId)) {
|
||||
regionOutputs.put(resolvedRegionId, new SpannableStringBuilder());
|
||||
}
|
||||
return regionOutputs.get(resolvedRegionId);
|
||||
}
|
||||
|
||||
private void traverseForStyle(Map<String, TtmlStyle> globalStyles,
|
||||
Map<String, SpannableStringBuilder> regionOutputs) {
|
||||
for (Entry<String, Integer> entry : nodeEndsByRegion.entrySet()) {
|
||||
String regionId = entry.getKey();
|
||||
int start = nodeStartsByRegion.containsKey(regionId) ? nodeStartsByRegion.get(regionId) : 0;
|
||||
applyStyleToOutput(globalStyles, regionOutputs.get(regionId), start, entry.getValue());
|
||||
for (int i = 0; i < getChildCount(); ++i) {
|
||||
getChild(i).traverseForStyle(globalStyles, regionOutputs);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void applyStyleToOutput(Map<String, TtmlStyle> globalStyles,
|
||||
SpannableStringBuilder regionOutput, int start, int end) {
|
||||
if (start != end) {
|
||||
TtmlStyle resolvedStyle = TtmlRenderUtil.resolveStyle(style, styleIds, globalStyles);
|
||||
if (resolvedStyle != null) {
|
||||
TtmlRenderUtil.applyStylesToSpan(regionOutput, start, end, resolvedStyle);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private SpannableStringBuilder cleanUpText(SpannableStringBuilder builder) {
|
||||
// Having joined the text elements, we need to do some final cleanup on the result.
|
||||
// 1. Collapse multiple consecutive spaces into a single space.
|
||||
int builderLength = builder.length();
|
||||
@ -208,44 +291,7 @@ import java.util.TreeSet;
|
||||
builder.delete(builderLength - 1, builderLength);
|
||||
/*builderLength--;*/
|
||||
}
|
||||
|
||||
return builder;
|
||||
}
|
||||
|
||||
private SpannableStringBuilder traverseForText(long timeUs, SpannableStringBuilder builder,
|
||||
boolean descendsPNode) {
|
||||
start = builder.length();
|
||||
end = start;
|
||||
if (isTextNode && descendsPNode) {
|
||||
builder.append(text);
|
||||
} else if (TAG_BR.equals(tag) && descendsPNode) {
|
||||
builder.append('\n');
|
||||
} else if (TAG_METADATA.equals(tag)) {
|
||||
// Do nothing.
|
||||
} else if (isActive(timeUs)) {
|
||||
boolean isPNode = TAG_P.equals(tag);
|
||||
for (int i = 0; i < getChildCount(); ++i) {
|
||||
getChild(i).traverseForText(timeUs, builder, descendsPNode || isPNode);
|
||||
}
|
||||
if (isPNode) {
|
||||
TtmlRenderUtil.endParagraph(builder);
|
||||
}
|
||||
end = builder.length();
|
||||
}
|
||||
return builder;
|
||||
}
|
||||
|
||||
private void traverseForStyle(SpannableStringBuilder builder,
|
||||
Map<String, TtmlStyle> globalStyles) {
|
||||
if (start != end) {
|
||||
TtmlStyle resolvedStyle = TtmlRenderUtil.resolveStyle(style, styleIds, globalStyles);
|
||||
if (resolvedStyle != null) {
|
||||
TtmlRenderUtil.applyStylesToSpan(builder, start, end, resolvedStyle);
|
||||
}
|
||||
for (int i = 0; i < getChildCount(); ++i) {
|
||||
getChild(i).traverseForStyle(builder, globalStyles);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -24,6 +24,7 @@ import com.google.android.exoplayer.util.Util;
|
||||
|
||||
import android.text.Layout;
|
||||
import android.util.Log;
|
||||
import android.util.Pair;
|
||||
|
||||
import org.xmlpull.v1.XmlPullParser;
|
||||
import org.xmlpull.v1.XmlPullParserException;
|
||||
@ -66,6 +67,7 @@ public final class TtmlParser extends SimpleSubtitleParser {
|
||||
private static final String ATTR_DURATION = "dur";
|
||||
private static final String ATTR_END = "end";
|
||||
private static final String ATTR_STYLE = "style";
|
||||
private static final String ATTR_REGION = "region";
|
||||
|
||||
private static final Pattern CLOCK_TIME =
|
||||
Pattern.compile("^([0-9][0-9]+):([0-9][0-9]):([0-9][0-9])"
|
||||
@ -74,6 +76,8 @@ public final class TtmlParser extends SimpleSubtitleParser {
|
||||
Pattern.compile("^([0-9]+(?:\\.[0-9]+)?)(h|m|s|ms|f|t)$");
|
||||
private static final Pattern FONT_SIZE =
|
||||
Pattern.compile("^(([0-9]*.)?[0-9]+)(px|em|%)$");
|
||||
private static final Pattern ORIGIN_COORDINATES =
|
||||
Pattern.compile("^(\\d+\\.?\\d*?)% (\\d+\\.?\\d*?)%$");
|
||||
|
||||
// TODO: read and apply the following attributes if specified.
|
||||
private static final int DEFAULT_FRAMERATE = 30;
|
||||
@ -85,6 +89,7 @@ public final class TtmlParser extends SimpleSubtitleParser {
|
||||
public TtmlParser() {
|
||||
try {
|
||||
xmlParserFactory = XmlPullParserFactory.newInstance();
|
||||
xmlParserFactory.setNamespaceAware(true);
|
||||
} catch (XmlPullParserException e) {
|
||||
throw new RuntimeException("Couldn't create XmlPullParserFactory instance", e);
|
||||
}
|
||||
@ -95,6 +100,8 @@ public final class TtmlParser extends SimpleSubtitleParser {
|
||||
try {
|
||||
XmlPullParser xmlParser = xmlParserFactory.newPullParser();
|
||||
Map<String, TtmlStyle> globalStyles = new HashMap<>();
|
||||
Map<String, TtmlRegion> regionMap = new HashMap<>();
|
||||
regionMap.put(TtmlNode.ANONYMOUS_REGION_ID, new TtmlRegion());
|
||||
ByteArrayInputStream inputStream = new ByteArrayInputStream(bytes, 0, length);
|
||||
xmlParser.setInput(inputStream, null);
|
||||
TtmlSubtitle ttmlSubtitle = null;
|
||||
@ -110,10 +117,10 @@ public final class TtmlParser extends SimpleSubtitleParser {
|
||||
Log.i(TAG, "Ignoring unsupported tag: " + xmlParser.getName());
|
||||
unsupportedNodeDepth++;
|
||||
} else if (TtmlNode.TAG_HEAD.equals(name)) {
|
||||
parseHeader(xmlParser, globalStyles);
|
||||
parseHeader(xmlParser, globalStyles, regionMap);
|
||||
} else {
|
||||
try {
|
||||
TtmlNode node = parseNode(xmlParser, parent);
|
||||
TtmlNode node = parseNode(xmlParser, parent, regionMap);
|
||||
nodeStack.addLast(node);
|
||||
if (parent != null) {
|
||||
parent.addChild(node);
|
||||
@ -128,7 +135,7 @@ public final class TtmlParser extends SimpleSubtitleParser {
|
||||
parent.addChild(TtmlNode.buildTextNode(xmlParser.getText()));
|
||||
} else if (eventType == XmlPullParser.END_TAG) {
|
||||
if (xmlParser.getName().equals(TtmlNode.TAG_TT)) {
|
||||
ttmlSubtitle = new TtmlSubtitle(nodeStack.getLast(), globalStyles);
|
||||
ttmlSubtitle = new TtmlSubtitle(nodeStack.getLast(), globalStyles, regionMap);
|
||||
}
|
||||
nodeStack.removeLast();
|
||||
}
|
||||
@ -151,28 +158,50 @@ public final class TtmlParser extends SimpleSubtitleParser {
|
||||
}
|
||||
|
||||
private Map<String, TtmlStyle> parseHeader(XmlPullParser xmlParser,
|
||||
Map<String, TtmlStyle> globalStyles)
|
||||
Map<String, TtmlStyle> globalStyles, Map<String, TtmlRegion> globalRegions)
|
||||
throws IOException, XmlPullParserException {
|
||||
|
||||
do {
|
||||
xmlParser.next();
|
||||
if (ParserUtil.isStartTag(xmlParser, TtmlNode.TAG_STYLE)) {
|
||||
String parentStyleId = xmlParser.getAttributeValue(null, ATTR_STYLE);
|
||||
String parentStyleId = ParserUtil.getAttributeValue(xmlParser, ATTR_STYLE);
|
||||
TtmlStyle style = parseStyleAttributes(xmlParser, new TtmlStyle());
|
||||
if (parentStyleId != null) {
|
||||
String[] ids = parseStyleIds(parentStyleId);
|
||||
for (String id : ids) {
|
||||
for (String id : parseStyleIds(parentStyleId)) {
|
||||
style.chain(globalStyles.get(id));
|
||||
}
|
||||
}
|
||||
if (style.getId() != null) {
|
||||
globalStyles.put(style.getId(), style);
|
||||
}
|
||||
} else if (ParserUtil.isStartTag(xmlParser, TtmlNode.TAG_REGION)) {
|
||||
Pair<String, TtmlRegion> ttmlRegionInfo = parseRegionAttributes(xmlParser);
|
||||
if (ttmlRegionInfo != null) {
|
||||
globalRegions.put(ttmlRegionInfo.first, ttmlRegionInfo.second);
|
||||
}
|
||||
}
|
||||
} while (!ParserUtil.isEndTag(xmlParser, TtmlNode.TAG_HEAD));
|
||||
return globalStyles;
|
||||
}
|
||||
|
||||
private Pair<String, TtmlRegion> parseRegionAttributes(XmlPullParser xmlParser) {
|
||||
String regionId = ParserUtil.getAttributeValue(xmlParser, TtmlNode.ATTR_ID);
|
||||
String regionOrigin = ParserUtil.getAttributeValue(xmlParser, TtmlNode.ATTR_TTS_ORIGIN);
|
||||
if (regionOrigin == null || regionId == null) {
|
||||
return null;
|
||||
}
|
||||
Matcher originMatcher = ORIGIN_COORDINATES.matcher(regionOrigin);
|
||||
if (originMatcher.matches()) {
|
||||
try {
|
||||
float position = Float.parseFloat(originMatcher.group(1)) / 100.f;
|
||||
float line = Float.parseFloat(originMatcher.group(2)) / 100.f;
|
||||
return new Pair<>(regionId, new TtmlRegion(position, line));
|
||||
} catch (NumberFormatException e) {
|
||||
Log.w(TAG, "Ignoring malformed region declaration: '" + regionOrigin + "'", e);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private String[] parseStyleIds(String parentStyleIds) {
|
||||
return parentStyleIds.split("\\s+");
|
||||
}
|
||||
@ -180,9 +209,8 @@ public final class TtmlParser extends SimpleSubtitleParser {
|
||||
private TtmlStyle parseStyleAttributes(XmlPullParser parser, TtmlStyle style) {
|
||||
int attributeCount = parser.getAttributeCount();
|
||||
for (int i = 0; i < attributeCount; i++) {
|
||||
String attributeName = parser.getAttributeName(i);
|
||||
String attributeValue = parser.getAttributeValue(i);
|
||||
switch (ParserUtil.removeNamespacePrefix(attributeName)) {
|
||||
switch (parser.getAttributeName(i)) {
|
||||
case TtmlNode.ATTR_ID:
|
||||
if (TtmlNode.TAG_STYLE.equals(parser.getName())) {
|
||||
style = createIfNull(style).setId(attributeValue);
|
||||
@ -199,7 +227,7 @@ public final class TtmlParser extends SimpleSubtitleParser {
|
||||
case TtmlNode.ATTR_TTS_COLOR:
|
||||
style = createIfNull(style);
|
||||
try {
|
||||
style.setColor(ColorParser.parseTtmlColor(attributeValue));
|
||||
style.setFontColor(ColorParser.parseTtmlColor(attributeValue));
|
||||
} catch (IllegalArgumentException e) {
|
||||
Log.w(TAG, "failed parsing color value: '" + attributeValue + "'");
|
||||
}
|
||||
@ -270,15 +298,17 @@ public final class TtmlParser extends SimpleSubtitleParser {
|
||||
return style == null ? new TtmlStyle() : style;
|
||||
}
|
||||
|
||||
private TtmlNode parseNode(XmlPullParser parser, TtmlNode parent) throws ParserException {
|
||||
private TtmlNode parseNode(XmlPullParser parser, TtmlNode parent,
|
||||
Map<String, TtmlRegion> regionMap) throws ParserException {
|
||||
long duration = 0;
|
||||
long startTime = TtmlNode.UNDEFINED_TIME;
|
||||
long endTime = TtmlNode.UNDEFINED_TIME;
|
||||
String regionId = TtmlNode.ANONYMOUS_REGION_ID;
|
||||
String[] styleIds = null;
|
||||
int attributeCount = parser.getAttributeCount();
|
||||
TtmlStyle style = parseStyleAttributes(parser, null);
|
||||
for (int i = 0; i < attributeCount; i++) {
|
||||
String attr = ParserUtil.removeNamespacePrefix(parser.getAttributeName(i));
|
||||
String attr = parser.getAttributeName(i);
|
||||
String value = parser.getAttributeValue(i);
|
||||
switch (attr) {
|
||||
case ATTR_BEGIN:
|
||||
@ -300,6 +330,13 @@ public final class TtmlParser extends SimpleSubtitleParser {
|
||||
styleIds = ids;
|
||||
}
|
||||
break;
|
||||
case ATTR_REGION:
|
||||
if (regionMap.containsKey(value)) {
|
||||
// If the region has not been correctly declared or does not define a position, we use
|
||||
// the anonymous region.
|
||||
regionId = value;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
// Do nothing.
|
||||
break;
|
||||
@ -322,7 +359,7 @@ public final class TtmlParser extends SimpleSubtitleParser {
|
||||
endTime = parent.endTimeUs;
|
||||
}
|
||||
}
|
||||
return TtmlNode.buildNode(parser.getName(), startTime, endTime, style, styleIds);
|
||||
return TtmlNode.buildNode(parser.getName(), startTime, endTime, style, styleIds, regionId);
|
||||
}
|
||||
|
||||
private static boolean isSupportedTag(String tag) {
|
||||
|
@ -0,0 +1,37 @@
|
||||
/*
|
||||
* Copyright (C) 2016 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 com.google.android.exoplayer.text.Cue;
|
||||
|
||||
/**
|
||||
* Represents a TTML Region.
|
||||
*/
|
||||
/* package */ final class TtmlRegion {
|
||||
|
||||
public final float position;
|
||||
public final float line;
|
||||
|
||||
public TtmlRegion() {
|
||||
this(Cue.DIMEN_UNSET, Cue.DIMEN_UNSET);
|
||||
}
|
||||
|
||||
public TtmlRegion(float position, float line) {
|
||||
this.position = position;
|
||||
this.line = line;
|
||||
}
|
||||
|
||||
}
|
@ -38,29 +38,29 @@ import java.util.Map;
|
||||
public static TtmlStyle resolveStyle(TtmlStyle style, String[] styleIds,
|
||||
Map<String, TtmlStyle> globalStyles) {
|
||||
if (style == null && styleIds == null) {
|
||||
// no styles at all
|
||||
// No styles at all.
|
||||
return null;
|
||||
} else if (style == null && styleIds.length == 1) {
|
||||
// only one single referential style present
|
||||
// Only one single referential style present.
|
||||
return globalStyles.get(styleIds[0]);
|
||||
} else if (style == null && styleIds.length > 1) {
|
||||
// only multiple referential styles present
|
||||
// Only multiple referential styles present.
|
||||
TtmlStyle chainedStyle = new TtmlStyle();
|
||||
for (String styleId : styleIds) {
|
||||
chainedStyle.chain(globalStyles.get(styleId));
|
||||
for (String id : styleIds) {
|
||||
chainedStyle.chain(globalStyles.get(id));
|
||||
}
|
||||
return chainedStyle;
|
||||
} else if (style != null && styleIds != null && styleIds.length == 1) {
|
||||
// merge a single referential style into inline style
|
||||
// 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 (String styleId : styleIds) {
|
||||
style.chain(globalStyles.get(styleId));
|
||||
// Merge multiple referential styles into inline style.
|
||||
for (String id : styleIds) {
|
||||
style.chain(globalStyles.get(id));
|
||||
}
|
||||
return style;
|
||||
}
|
||||
// only inline styles available
|
||||
// Only inline styles available.
|
||||
return style;
|
||||
}
|
||||
|
||||
@ -77,11 +77,11 @@ import java.util.Map;
|
||||
if (style.isUnderline()) {
|
||||
builder.setSpan(new UnderlineSpan(), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
}
|
||||
if (style.hasColorSpecified()) {
|
||||
builder.setSpan(new ForegroundColorSpan(style.getColor()), start, end,
|
||||
if (style.hasFontColor()) {
|
||||
builder.setSpan(new ForegroundColorSpan(style.getFontColor()), start, end,
|
||||
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
}
|
||||
if (style.hasBackgroundColorSpecified()) {
|
||||
if (style.hasBackgroundColor()) {
|
||||
builder.setSpan(new BackgroundColorSpan(style.getBackgroundColor()), start, end,
|
||||
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
}
|
||||
|
@ -25,53 +25,55 @@ import android.text.Layout;
|
||||
*/
|
||||
/* package */ final class TtmlStyle {
|
||||
|
||||
public static final short UNSPECIFIED = -1;
|
||||
public static final int UNSPECIFIED = -1;
|
||||
|
||||
public static final short STYLE_NORMAL = Typeface.NORMAL;
|
||||
public static final short STYLE_BOLD = Typeface.BOLD;
|
||||
public static final short STYLE_ITALIC = Typeface.ITALIC;
|
||||
public static final short STYLE_BOLD_ITALIC = Typeface.BOLD_ITALIC;
|
||||
public static final int STYLE_NORMAL = Typeface.NORMAL;
|
||||
public static final int STYLE_BOLD = Typeface.BOLD;
|
||||
public static final int STYLE_ITALIC = Typeface.ITALIC;
|
||||
public static final int STYLE_BOLD_ITALIC = Typeface.BOLD_ITALIC;
|
||||
|
||||
public static final short FONT_SIZE_UNIT_PIXEL = 1;
|
||||
public static final short FONT_SIZE_UNIT_EM = 2;
|
||||
public static final short FONT_SIZE_UNIT_PERCENT = 3;
|
||||
public static final int FONT_SIZE_UNIT_PIXEL = 1;
|
||||
public static final int FONT_SIZE_UNIT_EM = 2;
|
||||
public static final int FONT_SIZE_UNIT_PERCENT = 3;
|
||||
|
||||
private static final short OFF = 0;
|
||||
private static final short ON = 1;
|
||||
private static final int OFF = 0;
|
||||
private static final int ON = 1;
|
||||
|
||||
private String fontFamily;
|
||||
private int color;
|
||||
private boolean colorSpecified;
|
||||
private int fontColor;
|
||||
private boolean hasFontColor;
|
||||
private int backgroundColor;
|
||||
private boolean backgroundColorSpecified;
|
||||
private short linethrough = UNSPECIFIED;
|
||||
private short underline = UNSPECIFIED;
|
||||
private short bold = UNSPECIFIED;
|
||||
private short italic = UNSPECIFIED;
|
||||
private short fontSizeUnit = UNSPECIFIED;
|
||||
private boolean hasBackgroundColor;
|
||||
private int linethrough;
|
||||
private int underline;
|
||||
private int bold;
|
||||
private int italic;
|
||||
private int fontSizeUnit;
|
||||
private float fontSize;
|
||||
private String id;
|
||||
private TtmlStyle inheritableStyle;
|
||||
private Layout.Alignment textAlign;
|
||||
|
||||
public TtmlStyle() {
|
||||
linethrough = UNSPECIFIED;
|
||||
underline = UNSPECIFIED;
|
||||
bold = UNSPECIFIED;
|
||||
italic = UNSPECIFIED;
|
||||
fontSizeUnit = UNSPECIFIED;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the style or <code>UNSPECIFIED</code> when no style information is given.
|
||||
* Returns the style or {@link #UNSPECIFIED} when no style information is given.
|
||||
*
|
||||
* @return UNSPECIFIED, STYLE_NORMAL, STYLE_BOLD, STYLE_BOLD or STYLE_BOLD_ITALIC
|
||||
* @return {@link #UNSPECIFIED}, {@link #STYLE_NORMAL}, {@link #STYLE_BOLD}, {@link #STYLE_BOLD}
|
||||
* or {@link #STYLE_BOLD_ITALIC}.
|
||||
*/
|
||||
public short getStyle() {
|
||||
public int getStyle() {
|
||||
if (bold == UNSPECIFIED && italic == UNSPECIFIED) {
|
||||
return UNSPECIFIED;
|
||||
}
|
||||
|
||||
short style = STYLE_NORMAL;
|
||||
if (bold != UNSPECIFIED) {
|
||||
style += bold;
|
||||
}
|
||||
if (italic != UNSPECIFIED){
|
||||
style += italic;
|
||||
}
|
||||
return style;
|
||||
return (bold != UNSPECIFIED ? bold : STYLE_NORMAL)
|
||||
| (italic != UNSPECIFIED ? italic : STYLE_NORMAL);
|
||||
}
|
||||
|
||||
public boolean isLinethrough() {
|
||||
@ -104,33 +106,39 @@ import android.text.Layout;
|
||||
return this;
|
||||
}
|
||||
|
||||
public int getColor() {
|
||||
return color;
|
||||
public int getFontColor() {
|
||||
if (!hasFontColor) {
|
||||
throw new IllegalStateException("Font color has not been defined.");
|
||||
}
|
||||
return fontColor;
|
||||
}
|
||||
|
||||
public TtmlStyle setColor(int color) {
|
||||
public TtmlStyle setFontColor(int fontColor) {
|
||||
Assertions.checkState(inheritableStyle == null);
|
||||
this.color = color;
|
||||
colorSpecified = true;
|
||||
this.fontColor = fontColor;
|
||||
hasFontColor = true;
|
||||
return this;
|
||||
}
|
||||
|
||||
public boolean hasColorSpecified() {
|
||||
return colorSpecified;
|
||||
public boolean hasFontColor() {
|
||||
return hasFontColor;
|
||||
}
|
||||
|
||||
public int getBackgroundColor() {
|
||||
if (!hasBackgroundColor) {
|
||||
throw new IllegalStateException("Background color has not been defined.");
|
||||
}
|
||||
return backgroundColor;
|
||||
}
|
||||
|
||||
public TtmlStyle setBackgroundColor(int backgroundColor) {
|
||||
this.backgroundColor = backgroundColor;
|
||||
backgroundColorSpecified = true;
|
||||
hasBackgroundColor = true;
|
||||
return this;
|
||||
}
|
||||
|
||||
public boolean hasBackgroundColorSpecified() {
|
||||
return backgroundColorSpecified;
|
||||
public boolean hasBackgroundColor() {
|
||||
return hasBackgroundColor;
|
||||
}
|
||||
|
||||
public TtmlStyle setBold(boolean isBold) {
|
||||
@ -168,8 +176,8 @@ import android.text.Layout;
|
||||
|
||||
private TtmlStyle inherit(TtmlStyle ancestor, boolean chaining) {
|
||||
if (ancestor != null) {
|
||||
if (!colorSpecified && ancestor.colorSpecified) {
|
||||
setColor(ancestor.color);
|
||||
if (!hasFontColor && ancestor.hasFontColor) {
|
||||
setFontColor(ancestor.fontColor);
|
||||
}
|
||||
if (bold == UNSPECIFIED) {
|
||||
bold = ancestor.bold;
|
||||
@ -194,7 +202,7 @@ import android.text.Layout;
|
||||
fontSize = ancestor.fontSize;
|
||||
}
|
||||
// attributes not inherited as of http://www.w3.org/TR/ttml1/
|
||||
if (chaining && !backgroundColorSpecified && ancestor.backgroundColorSpecified) {
|
||||
if (chaining && !hasBackgroundColor && ancestor.hasBackgroundColor) {
|
||||
setBackgroundColor(ancestor.backgroundColor);
|
||||
}
|
||||
}
|
||||
@ -224,12 +232,12 @@ import android.text.Layout;
|
||||
return this;
|
||||
}
|
||||
|
||||
public TtmlStyle setFontSizeUnit(short unit) {
|
||||
this.fontSizeUnit = unit;
|
||||
public TtmlStyle setFontSizeUnit(int fontSizeUnit) {
|
||||
this.fontSizeUnit = fontSizeUnit;
|
||||
return this;
|
||||
}
|
||||
|
||||
public short getFontSizeUnit() {
|
||||
public int getFontSizeUnit() {
|
||||
return fontSizeUnit;
|
||||
}
|
||||
|
||||
|
@ -26,14 +26,17 @@ import java.util.Map;
|
||||
/**
|
||||
* A representation of a TTML subtitle.
|
||||
*/
|
||||
public final class TtmlSubtitle implements Subtitle {
|
||||
/* package */ final class TtmlSubtitle implements Subtitle {
|
||||
|
||||
private final TtmlNode root;
|
||||
private final long[] eventTimesUs;
|
||||
private final Map<String, TtmlStyle> globalStyles;
|
||||
private final Map<String, TtmlRegion> regionMap;
|
||||
|
||||
public TtmlSubtitle(TtmlNode root, Map<String, TtmlStyle> globalStyles) {
|
||||
public TtmlSubtitle(TtmlNode root, Map<String, TtmlStyle> globalStyles,
|
||||
Map<String, TtmlRegion> regionMap) {
|
||||
this.root = root;
|
||||
this.regionMap = regionMap;
|
||||
this.globalStyles = globalStyles != null
|
||||
? Collections.unmodifiableMap(globalStyles) : Collections.<String, TtmlStyle>emptyMap();
|
||||
this.eventTimesUs = root.getEventTimesUs();
|
||||
@ -62,13 +65,7 @@ public final class TtmlSubtitle implements Subtitle {
|
||||
|
||||
@Override
|
||||
public List<Cue> getCues(long timeUs) {
|
||||
CharSequence cueText = root.getText(timeUs, globalStyles);
|
||||
if (cueText == null) {
|
||||
return Collections.emptyList();
|
||||
} else {
|
||||
Cue cue = new Cue(cueText);
|
||||
return Collections.singletonList(cue);
|
||||
}
|
||||
return root.getCues(timeUs, globalStyles, regionMap);
|
||||
}
|
||||
|
||||
/* @VisibleForTesting */
|
||||
|
@ -26,26 +26,30 @@ public final class ParserUtil {
|
||||
private ParserUtil() {}
|
||||
|
||||
public static boolean isEndTag(XmlPullParser xpp, String name) throws XmlPullParserException {
|
||||
return xpp.getEventType() == XmlPullParser.END_TAG && name.equals(xpp.getName());
|
||||
return isEndTag(xpp) && xpp.getName().equals(name);
|
||||
}
|
||||
|
||||
public static boolean isEndTag(XmlPullParser xpp) throws XmlPullParserException {
|
||||
return xpp.getEventType() == XmlPullParser.END_TAG;
|
||||
}
|
||||
|
||||
public static boolean isStartTag(XmlPullParser xpp, String name)
|
||||
throws XmlPullParserException {
|
||||
return xpp.getEventType() == XmlPullParser.START_TAG && name.equals(xpp.getName());
|
||||
return isStartTag(xpp) && xpp.getName().equals(name);
|
||||
}
|
||||
|
||||
public static boolean isStartTag(XmlPullParser xpp) throws XmlPullParserException {
|
||||
return xpp.getEventType() == XmlPullParser.START_TAG;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the namespace part ('^.*:') of the attributeName.
|
||||
*
|
||||
* @param attributeName the string to remove the namespace prefix from
|
||||
* @return the name of the attribute without the prefix
|
||||
*/
|
||||
public static String removeNamespacePrefix(String attributeName) {
|
||||
return attributeName.replaceFirst("^.*:", "");
|
||||
public static String getAttributeValue(XmlPullParser xpp, String attributeName) {
|
||||
int attributeCount = xpp.getAttributeCount();
|
||||
for (int i = 0; i < attributeCount; i++) {
|
||||
if (attributeName.equals(xpp.getAttributeName(i))) {
|
||||
return xpp.getAttributeValue(i);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user