diff --git a/library/src/androidTest/java/com/google/android/exoplayer/text/ttml/TtmlColorParserTest.java b/library/src/androidTest/java/com/google/android/exoplayer/text/ttml/TtmlColorParserTest.java new file mode 100644 index 0000000000..922b054011 --- /dev/null +++ b/library/src/androidTest/java/com/google/android/exoplayer/text/ttml/TtmlColorParserTest.java @@ -0,0 +1,113 @@ +/* + * 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 TtmlColorParser. + */ +public class TtmlColorParserTest extends InstrumentationTestCase { + + public void testHexCodeParsing() { + assertEquals(Color.WHITE, TtmlColorParser.parseColor("#ffffff")); + assertEquals(Color.WHITE, TtmlColorParser.parseColor("#ffffffff")); + assertEquals(Color.parseColor("#00ffffff"), TtmlColorParser.parseColor("#00ffffff")); + assertEquals(Color.parseColor("#12341234"), TtmlColorParser.parseColor("#12341234")); + } + + 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)")); + } +} diff --git a/library/src/androidTest/java/com/google/android/exoplayer/text/ttml/TtmlParserTest.java b/library/src/androidTest/java/com/google/android/exoplayer/text/ttml/TtmlParserTest.java index 9d5d953977..4133c3ce3d 100644 --- a/library/src/androidTest/java/com/google/android/exoplayer/text/ttml/TtmlParserTest.java +++ b/library/src/androidTest/java/com/google/android/exoplayer/text/ttml/TtmlParserTest.java @@ -17,7 +17,6 @@ package com.google.android.exoplayer.text.ttml; import com.google.android.exoplayer.text.Cue; -import android.graphics.Color; import android.test.InstrumentationTestCase; import android.text.Layout; import android.text.SpannableStringBuilder; @@ -67,8 +66,8 @@ 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(Color.parseColor("yellow"), firstPStyle.getColor()); - assertEquals(Color.parseColor("blue"), firstPStyle.getBackgroundColor()); + assertEquals(TtmlColorParser.parseColor("yellow"), firstPStyle.getColor()); + assertEquals(TtmlColorParser.parseColor("blue"), firstPStyle.getBackgroundColor()); assertEquals("serif", firstPStyle.getFontFamily()); assertEquals(TtmlStyle.STYLE_BOLD_ITALIC, firstPStyle.getStyle()); assertTrue(firstPStyle.isUnderline()); @@ -78,24 +77,43 @@ public final class TtmlParserTest extends InstrumentationTestCase { TtmlSubtitle subtitle = getSubtitle(INLINE_ATTRIBUTES_TTML_FILE); assertEquals(4, subtitle.getEventTimeCount()); assertSpans(subtitle, 20, "text 2", "sansSerif", TtmlStyle.STYLE_ITALIC, - Color.CYAN, Color.parseColor("lime"), false, true, null); + TtmlColorParser.CYAN, TtmlColorParser.parseColor("lime"), false, true, null); + } + + /** + * regression test for devices on JellyBean where some named colors are not correctly defined + * on framework level. Tests that lime resolves to #FF00FF00 not + * #00FF00. + * + * 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 + * + * @throws IOException thrown if reading subtitle file fails. + */ + public void testLime() throws IOException { + TtmlSubtitle subtitle = getSubtitle(INLINE_ATTRIBUTES_TTML_FILE); + assertEquals(4, subtitle.getEventTimeCount()); + assertSpans(subtitle, 20, "text 2", "sansSerif", TtmlStyle.STYLE_ITALIC, + TtmlColorParser.CYAN, TtmlColorParser.LIME, false, true, null); } public void testInheritGlobalStyle() throws IOException { TtmlSubtitle subtitle = getSubtitle(INHERIT_STYLE_TTML_FILE); assertEquals(2, subtitle.getEventTimeCount()); assertSpans(subtitle, 10, "text 1", "serif", TtmlStyle.STYLE_BOLD_ITALIC, - Color.BLUE, Color.YELLOW, true, false, null); + TtmlColorParser.BLUE, TtmlColorParser.YELLOW, true, false, null); } public void testInheritGlobalStyleOverriddenByInlineAttributes() throws IOException { TtmlSubtitle subtitle = getSubtitle(INHERIT_STYLE_OVERRIDE_TTML_FILE); assertEquals(4, subtitle.getEventTimeCount()); - assertSpans(subtitle, 10, "text 1", "serif", TtmlStyle.STYLE_BOLD_ITALIC, Color.BLUE, - Color.YELLOW, true, false, null); - assertSpans(subtitle, 20, "text 2", "sansSerif", TtmlStyle.STYLE_ITALIC, Color.RED, - Color.YELLOW, true, false, null); + assertSpans(subtitle, 10, "text 1", "serif", TtmlStyle.STYLE_BOLD_ITALIC, TtmlColorParser.BLUE, + TtmlColorParser.YELLOW, true, false, null); + assertSpans(subtitle, 20, "text 2", "sansSerif", TtmlStyle.STYLE_ITALIC, TtmlColorParser.RED, + TtmlColorParser.YELLOW, true, false, null); } public void testInheritGlobalAndParent() throws IOException { @@ -103,9 +121,10 @@ public final class TtmlParserTest extends InstrumentationTestCase { assertEquals(4, subtitle.getEventTimeCount()); assertSpans(subtitle, 10, "text 1", "sansSerif", TtmlStyle.STYLE_NORMAL, - Color.RED, Color.parseColor("lime"), false, true, Layout.Alignment.ALIGN_CENTER); + TtmlColorParser.RED, TtmlColorParser.parseColor("lime"), false, true, + Layout.Alignment.ALIGN_CENTER); assertSpans(subtitle, 20, "text 2", "serif", TtmlStyle.STYLE_BOLD_ITALIC, - Color.BLUE, Color.YELLOW, true, true, Layout.Alignment.ALIGN_CENTER); + TtmlColorParser.BLUE, TtmlColorParser.YELLOW, true, true, Layout.Alignment.ALIGN_CENTER); } public void testInheritMultipleStyles() throws IOException { @@ -113,7 +132,7 @@ public final class TtmlParserTest extends InstrumentationTestCase { assertEquals(12, subtitle.getEventTimeCount()); assertSpans(subtitle, 10, "text 1", "sansSerif", TtmlStyle.STYLE_BOLD_ITALIC, - Color.BLUE, Color.YELLOW, false, true, null); + TtmlColorParser.BLUE, TtmlColorParser.YELLOW, false, true, null); } public void testInheritMultipleStylesWithoutLocalAttributes() throws IOException { @@ -121,7 +140,7 @@ public final class TtmlParserTest extends InstrumentationTestCase { assertEquals(12, subtitle.getEventTimeCount()); assertSpans(subtitle, 20, "text 2", "sansSerif", TtmlStyle.STYLE_BOLD_ITALIC, - Color.BLUE, Color.BLACK, false, true, null); + TtmlColorParser.BLUE, TtmlColorParser.BLACK, false, true, null); } @@ -130,7 +149,7 @@ public final class TtmlParserTest extends InstrumentationTestCase { assertEquals(12, subtitle.getEventTimeCount()); assertSpans(subtitle, 30, "text 2.5", "sansSerifInline", TtmlStyle.STYLE_ITALIC, - Color.RED, Color.YELLOW, true, true, null); + TtmlColorParser.RED, TtmlColorParser.YELLOW, true, true, null); } public void testEmptyStyleAttribute() throws IOException { @@ -175,16 +194,16 @@ public final class TtmlParserTest extends InstrumentationTestCase { TtmlStyle style = globalStyles.get("s2"); assertEquals("serif", style.getFontFamily()); - assertEquals(Color.RED, style.getBackgroundColor()); - assertEquals(Color.BLACK, style.getColor()); + assertEquals(TtmlColorParser.RED, style.getBackgroundColor()); + assertEquals(TtmlColorParser.BLACK, style.getColor()); assertEquals(TtmlStyle.STYLE_BOLD_ITALIC, style.getStyle()); assertTrue(style.isLinethrough()); style = globalStyles.get("s3"); // only difference: color must be RED - assertEquals(Color.RED, style.getColor()); + assertEquals(TtmlColorParser.RED, style.getColor()); assertEquals("serif", style.getFontFamily()); - assertEquals(Color.RED, style.getBackgroundColor()); + assertEquals(TtmlColorParser.RED, style.getBackgroundColor()); assertEquals(TtmlStyle.STYLE_BOLD_ITALIC, style.getStyle()); assertTrue(style.isLinethrough()); } @@ -224,8 +243,8 @@ public final class TtmlParserTest extends InstrumentationTestCase { TtmlStyle style = queryChildrenForTag(div, TtmlNode.TAG_P, 0).style; assertNotNull(style); - assertEquals(Color.BLACK, style.getBackgroundColor()); - assertEquals(Color.YELLOW, style.getColor()); + assertEquals(TtmlColorParser.BLACK, style.getBackgroundColor()); + assertEquals(TtmlColorParser.YELLOW, style.getColor()); assertEquals(TtmlStyle.STYLE_ITALIC, style.getStyle()); assertEquals("sansSerif", style.getFontFamily()); assertFalse(style.isUnderline()); @@ -243,8 +262,8 @@ public final class TtmlParserTest extends InstrumentationTestCase { TtmlStyle style = queryChildrenForTag(div, TtmlNode.TAG_P, 0).style; assertNotNull(style); - assertEquals(Color.BLACK, style.getBackgroundColor()); - assertEquals(Color.YELLOW, style.getColor()); + assertEquals(TtmlColorParser.BLACK, style.getBackgroundColor()); + assertEquals(TtmlColorParser.YELLOW, style.getColor()); assertEquals(TtmlStyle.STYLE_ITALIC, style.getStyle()); assertEquals("sansSerif", style.getFontFamily()); assertFalse(style.isUnderline()); diff --git a/library/src/main/java/com/google/android/exoplayer/text/ttml/TtmlColorParser.java b/library/src/main/java/com/google/android/exoplayer/text/ttml/TtmlColorParser.java new file mode 100644 index 0000000000..4a519dae7e --- /dev/null +++ b/library/src/main/java/com/google/android/exoplayer/text/ttml/TtmlColorParser.java @@ -0,0 +1,138 @@ +/* + * 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 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) == '#') { + // Use a long to avoid rollovers on #ffXXXXXX + long color = Long.parseLong(colorExpression.substring(1), 16); + if (colorExpression.length() == 7) { + // Set the alpha value + color |= 0x00000000ff000000; + } else if (colorExpression.length() != 9) { + throw new IllegalArgumentException(); + } + return (int) 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); + } + +} diff --git a/library/src/main/java/com/google/android/exoplayer/text/ttml/TtmlParser.java b/library/src/main/java/com/google/android/exoplayer/text/ttml/TtmlParser.java index a9e6c1b99a..87670e7f8b 100644 --- a/library/src/main/java/com/google/android/exoplayer/text/ttml/TtmlParser.java +++ b/library/src/main/java/com/google/android/exoplayer/text/ttml/TtmlParser.java @@ -23,7 +23,6 @@ import com.google.android.exoplayer.util.MimeTypes; import com.google.android.exoplayer.util.ParserUtil; import com.google.android.exoplayer.util.Util; -import android.graphics.Color; import android.text.Layout; import android.util.Log; @@ -207,7 +206,7 @@ public final class TtmlParser implements SubtitleParser { case TtmlNode.ATTR_TTS_BACKGROUND_COLOR: style = createIfNull(style); try { - style.setBackgroundColor(Color.parseColor(attributeValue)); + style.setBackgroundColor(TtmlColorParser.parseColor(attributeValue)); } catch (IllegalArgumentException e) { Log.w(TAG, "failed parsing background value: '" + attributeValue + "'"); } @@ -215,7 +214,7 @@ public final class TtmlParser implements SubtitleParser { case TtmlNode.ATTR_TTS_COLOR: style = createIfNull(style); try { - style.setColor(Color.parseColor(attributeValue)); + style.setColor(TtmlColorParser.parseColor(attributeValue)); } catch (IllegalArgumentException e) { Log.w(TAG, "failed parsing color value: '" + attributeValue + "'"); }