diff --git a/library/extractor/src/main/java/com/google/android/exoplayer2/text/ssa/SsaDecoder.java b/library/extractor/src/main/java/com/google/android/exoplayer2/text/ssa/SsaDecoder.java index c6df62bb5f..ba6b70499e 100644 --- a/library/extractor/src/main/java/com/google/android/exoplayer2/text/ssa/SsaDecoder.java +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/text/ssa/SsaDecoder.java @@ -21,6 +21,7 @@ import static com.google.android.exoplayer2.util.Util.castNonNull; import android.graphics.Typeface; import android.text.Layout; import android.text.SpannableString; +import android.text.style.BackgroundColorSpan; import android.text.style.ForegroundColorSpan; import android.text.style.StrikethroughSpan; import android.text.style.StyleSpan; @@ -319,6 +320,13 @@ public final class SsaDecoder extends SimpleSubtitleDecoder { /* end= */ spannableText.length(), SpannableString.SPAN_EXCLUSIVE_EXCLUSIVE); } + if (style.borderStyle == SsaStyle.SSA_BORDER_STYLE_BOX && style.outlineColor != null) { + spannableText.setSpan( + new BackgroundColorSpan(style.outlineColor), + /* start= */ 0, + /* end= */ spannableText.length(), + SpannableString.SPAN_EXCLUSIVE_EXCLUSIVE); + } if (style.fontSize != Cue.DIMEN_UNSET && screenHeight != Cue.DIMEN_UNSET) { cue.setTextSize( style.fontSize / screenHeight, Cue.TEXT_SIZE_TYPE_FRACTIONAL_IGNORE_PADDING); diff --git a/library/extractor/src/main/java/com/google/android/exoplayer2/text/ssa/SsaStyle.java b/library/extractor/src/main/java/com/google/android/exoplayer2/text/ssa/SsaStyle.java index 8e09291312..1e6868bb29 100644 --- a/library/extractor/src/main/java/com/google/android/exoplayer2/text/ssa/SsaStyle.java +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/text/ssa/SsaStyle.java @@ -92,32 +92,64 @@ import java.util.regex.Pattern; public static final int SSA_ALIGNMENT_TOP_CENTER = 8; public static final int SSA_ALIGNMENT_TOP_RIGHT = 9; + /** + * The SSA/ASS BorderStyle. + * + *

Allowed values: + * + *

+ */ + @Target(TYPE_USE) + @IntDef({ + SSA_BORDER_STYLE_UNKNOWN, + SSA_BORDER_STYLE_OUTLINE, + SSA_BORDER_STYLE_BOX, + }) + @Documented + @Retention(SOURCE) + public @interface SsaBorderStyle {} + + // The numbering follows the ASS (v4+) spec. + public static final int SSA_BORDER_STYLE_UNKNOWN = -1; + public static final int SSA_BORDER_STYLE_OUTLINE = 1; + public static final int SSA_BORDER_STYLE_BOX = 3; + public final String name; public final @SsaAlignment int alignment; @Nullable @ColorInt public final Integer primaryColor; + @Nullable @ColorInt public final Integer outlineColor; public final float fontSize; public final boolean bold; public final boolean italic; public final boolean underline; public final boolean strikeout; + public final @SsaBorderStyle int borderStyle; private SsaStyle( String name, @SsaAlignment int alignment, @Nullable @ColorInt Integer primaryColor, + @Nullable @ColorInt Integer outlineColor, float fontSize, boolean bold, boolean italic, boolean underline, - boolean strikeout) { + boolean strikeout, + @SsaBorderStyle int borderStyle) { this.name = name; this.alignment = alignment; this.primaryColor = primaryColor; + this.outlineColor = outlineColor; this.fontSize = fontSize; this.bold = bold; this.italic = italic; this.underline = underline; this.strikeout = strikeout; + this.borderStyle = borderStyle; } @Nullable @@ -141,6 +173,9 @@ import java.util.regex.Pattern; format.primaryColorIndex != C.INDEX_UNSET ? parseColor(styleValues[format.primaryColorIndex].trim()) : null, + format.outlineColorIndex != C.INDEX_UNSET + ? parseColor(styleValues[format.outlineColorIndex].trim()) + : null, format.fontSizeIndex != C.INDEX_UNSET ? parseFontSize(styleValues[format.fontSizeIndex].trim()) : Cue.DIMEN_UNSET, @@ -151,7 +186,10 @@ import java.util.regex.Pattern; format.underlineIndex != C.INDEX_UNSET && parseBooleanValue(styleValues[format.underlineIndex].trim()), format.strikeoutIndex != C.INDEX_UNSET - && parseBooleanValue(styleValues[format.strikeoutIndex].trim())); + && parseBooleanValue(styleValues[format.strikeoutIndex].trim()), + format.borderStyleIndex != C.INDEX_UNSET + ? parseBorderStyle(styleValues[format.borderStyleIndex].trim()) + : SSA_BORDER_STYLE_UNKNOWN); } catch (RuntimeException e) { Log.w(TAG, "Skipping malformed 'Style:' line: '" + styleLine + "'", e); return null; @@ -189,6 +227,30 @@ import java.util.regex.Pattern; } } + private static @SsaBorderStyle int parseBorderStyle(String borderStyleStr) { + try { + @SsaBorderStyle int borderStyle = Integer.parseInt(borderStyleStr.trim()); + if (isValidBorderStyle(borderStyle)) { + return borderStyle; + } + } catch (NumberFormatException e) { + // Swallow the exception and return UNKNOWN below. + } + Log.w(TAG, "Ignoring unknown BorderStyle: " + borderStyleStr); + return SSA_BORDER_STYLE_UNKNOWN; + } + + private static boolean isValidBorderStyle(@SsaBorderStyle int alignment) { + switch (alignment) { + case SSA_BORDER_STYLE_OUTLINE: + case SSA_BORDER_STYLE_BOX: + return true; + case SSA_BORDER_STYLE_UNKNOWN: + default: + return false; + } + } + /** * Parses a SSA V4+ color expression. * @@ -257,31 +319,37 @@ import java.util.regex.Pattern; public final int nameIndex; public final int alignmentIndex; public final int primaryColorIndex; + public final int outlineColorIndex; public final int fontSizeIndex; public final int boldIndex; public final int italicIndex; public final int underlineIndex; public final int strikeoutIndex; + public final int borderStyleIndex; public final int length; private Format( int nameIndex, int alignmentIndex, int primaryColorIndex, + int outlineColorIndex, int fontSizeIndex, int boldIndex, int italicIndex, int underlineIndex, int strikeoutIndex, + int borderStyleIndex, int length) { this.nameIndex = nameIndex; this.alignmentIndex = alignmentIndex; this.primaryColorIndex = primaryColorIndex; + this.outlineColorIndex = outlineColorIndex; this.fontSizeIndex = fontSizeIndex; this.boldIndex = boldIndex; this.italicIndex = italicIndex; this.underlineIndex = underlineIndex; this.strikeoutIndex = strikeoutIndex; + this.borderStyleIndex = borderStyleIndex; this.length = length; } @@ -295,11 +363,13 @@ import java.util.regex.Pattern; int nameIndex = C.INDEX_UNSET; int alignmentIndex = C.INDEX_UNSET; int primaryColorIndex = C.INDEX_UNSET; + int outlineColorIndex = C.INDEX_UNSET; int fontSizeIndex = C.INDEX_UNSET; int boldIndex = C.INDEX_UNSET; int italicIndex = C.INDEX_UNSET; int underlineIndex = C.INDEX_UNSET; int strikeoutIndex = C.INDEX_UNSET; + int borderStyleIndex = C.INDEX_UNSET; String[] keys = TextUtils.split(styleFormatLine.substring(SsaDecoder.FORMAT_LINE_PREFIX.length()), ","); for (int i = 0; i < keys.length; i++) { @@ -313,6 +383,9 @@ import java.util.regex.Pattern; case "primarycolour": primaryColorIndex = i; break; + case "outlinecolour": + outlineColorIndex = i; + break; case "fontsize": fontSizeIndex = i; break; @@ -328,6 +401,9 @@ import java.util.regex.Pattern; case "strikeout": strikeoutIndex = i; break; + case "borderstyle": + borderStyleIndex = i; + break; } } return nameIndex != C.INDEX_UNSET @@ -335,11 +411,13 @@ import java.util.regex.Pattern; nameIndex, alignmentIndex, primaryColorIndex, + outlineColorIndex, fontSizeIndex, boldIndex, italicIndex, underlineIndex, strikeoutIndex, + borderStyleIndex, keys.length) : null; } diff --git a/library/extractor/src/test/java/com/google/android/exoplayer2/text/ssa/SsaDecoderTest.java b/library/extractor/src/test/java/com/google/android/exoplayer2/text/ssa/SsaDecoderTest.java index 4b3b8cef64..1e1ab6c80b 100644 --- a/library/extractor/src/test/java/com/google/android/exoplayer2/text/ssa/SsaDecoderTest.java +++ b/library/extractor/src/test/java/com/google/android/exoplayer2/text/ssa/SsaDecoderTest.java @@ -48,7 +48,8 @@ public final class SsaDecoderTest { private static final String INVALID_TIMECODES = "media/ssa/invalid_timecodes"; private static final String INVALID_POSITIONS = "media/ssa/invalid_positioning"; private static final String POSITIONS_WITHOUT_PLAYRES = "media/ssa/positioning_without_playres"; - private static final String STYLE_COLORS = "media/ssa/style_colors"; + private static final String STYLE_PRIMARY_COLOR = "media/ssa/style_primary_color"; + private static final String STYLE_OUTLINE_COLOR = "media/ssa/style_outline_color"; private static final String STYLE_FONT_SIZE = "media/ssa/style_font_size"; private static final String STYLE_BOLD_ITALIC = "media/ssa/style_bold_italic"; private static final String STYLE_UNDERLINE = "media/ssa/style_underline"; @@ -297,9 +298,10 @@ public final class SsaDecoderTest { } @Test - public void decodeColors() throws IOException { + public void decodePrimaryColor() throws IOException { SsaDecoder decoder = new SsaDecoder(); - byte[] bytes = TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), STYLE_COLORS); + byte[] bytes = + TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), STYLE_PRIMARY_COLOR); Subtitle subtitle = decoder.decode(bytes, bytes.length, false); assertThat(subtitle.getEventTimeCount()).isEqualTo(14); // &H000000FF (AABBGGRR) -> #FFFF0000 (AARRGGBB) @@ -344,6 +346,26 @@ public final class SsaDecoderTest { .hasNoForegroundColorSpanBetween(0, seventhCueText.length()); } + @Test + public void decodeOutlineColor() throws IOException { + SsaDecoder decoder = new SsaDecoder(); + byte[] bytes = + TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), STYLE_OUTLINE_COLOR); + Subtitle subtitle = decoder.decode(bytes, bytes.length, false); + assertThat(subtitle.getEventTimeCount()).isEqualTo(4); + Spanned firstCueText = + (Spanned) Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(0))).text; + SpannedSubject.assertThat(firstCueText) + .hasBackgroundColorSpanBetween(0, firstCueText.length()) + .withColor(Color.BLUE); + + // OutlineColour should be treated as background only when BorderStyle=3 + Spanned secondCueText = + (Spanned) Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(2))).text; + SpannedSubject.assertThat(secondCueText) + .hasNoBackgroundColorSpanBetween(0, secondCueText.length()); + } + @Test public void decodeFontSize() throws IOException { SsaDecoder decoder = new SsaDecoder(); diff --git a/testdata/src/test/assets/media/ssa/style_outline_color b/testdata/src/test/assets/media/ssa/style_outline_color new file mode 100644 index 0000000000..a16c2fb872 --- /dev/null +++ b/testdata/src/test/assets/media/ssa/style_outline_color @@ -0,0 +1,16 @@ +[Script Info] +Title: OutlineColour settings +Script Type: V4.00+ +PlayResX: 1280 +PlayResY: 720 + +[V4+ Styles] +Format: Name ,OutlineColour,BorderStyle +Style: OutlineColourStyleBlue ,&H00FF0000 ,3 +Style: OutlineColourStyleIgnored ,&H00FF0000 ,1 + + +[Events] +Format: Start ,End ,Style ,Text +Dialogue: 0:00:01.00,0:00:02.00,OutlineColourStyleBlue ,Line with BLUE (&H00FF0000) outline. +Dialogue: 0:00:03.00,0:00:04.00,OutlineColourStyleIgnored ,Line with ignored outline because BorderStyle is not 3. diff --git a/testdata/src/test/assets/media/ssa/style_colors b/testdata/src/test/assets/media/ssa/style_primary_color similarity index 97% rename from testdata/src/test/assets/media/ssa/style_colors rename to testdata/src/test/assets/media/ssa/style_primary_color index a224e7ed4d..aa1e4ca7ca 100644 --- a/testdata/src/test/assets/media/ssa/style_colors +++ b/testdata/src/test/assets/media/ssa/style_primary_color @@ -1,5 +1,5 @@ [Script Info] -Title: Coloring +Title: PrimaryColour settings Script Type: V4.00+ PlayResX: 1280 PlayResY: 720