diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 223602671b..032ecb29a0 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -46,6 +46,9 @@ * UI * Fix `StyledPlayerView` scrubber not reappearing correctly in some cases ([#8646](https://github.com/google/ExoPlayer/issues/8646)). +* Text + * Parse SSA/ASS bold & italic info in `Style:` lines + ([#8435](https://github.com/google/ExoPlayer/issues/8435)). * MediaSession extension: Remove dependency to core module and rely on common only. The `TimelineQueueEditor` uses a new `MediaDescriptionConverter` for this purpose and does not rely on the `ConcatenatingMediaSource` anymore. diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/ssa/SsaDecoder.java b/library/core/src/main/java/com/google/android/exoplayer2/text/ssa/SsaDecoder.java index 3f7a7c1dd6..89366ca28c 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/ssa/SsaDecoder.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/ssa/SsaDecoder.java @@ -18,9 +18,11 @@ package com.google.android.exoplayer2.text.ssa; import static com.google.android.exoplayer2.text.Cue.LINE_TYPE_FRACTION; 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.ForegroundColorSpan; +import android.text.style.StyleSpan; import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.text.Cue; @@ -318,6 +320,25 @@ public final class SsaDecoder extends SimpleSubtitleDecoder { cue.setTextSize( style.fontSize / screenHeight, Cue.TEXT_SIZE_TYPE_FRACTIONAL_IGNORE_PADDING); } + if (style.bold && style.italic) { + spannableText.setSpan( + new StyleSpan(Typeface.BOLD_ITALIC), + /* start= */ 0, + /* end= */ spannableText.length(), + SpannableString.SPAN_EXCLUSIVE_EXCLUSIVE); + } else if (style.bold) { + spannableText.setSpan( + new StyleSpan(Typeface.BOLD), + /* start= */ 0, + /* end= */ spannableText.length(), + SpannableString.SPAN_EXCLUSIVE_EXCLUSIVE); + } else if (style.italic) { + spannableText.setSpan( + new StyleSpan(Typeface.ITALIC), + /* start= */ 0, + /* end= */ spannableText.length(), + SpannableString.SPAN_EXCLUSIVE_EXCLUSIVE); + } } @SsaStyle.SsaAlignment int alignment; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/ssa/SsaStyle.java b/library/core/src/main/java/com/google/android/exoplayer2/text/ssa/SsaStyle.java index bcfa514841..192838ad0d 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/ssa/SsaStyle.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/ssa/SsaStyle.java @@ -92,16 +92,22 @@ import java.util.regex.Pattern; @SsaAlignment public final int alignment; @Nullable @ColorInt public final Integer primaryColor; public final float fontSize; + public final boolean bold; + public final boolean italic; private SsaStyle( String name, @SsaAlignment int alignment, @Nullable @ColorInt Integer primaryColor, - float fontSize) { + float fontSize, + boolean bold, + boolean italic) { this.name = name; this.alignment = alignment; this.primaryColor = primaryColor; this.fontSize = fontSize; + this.bold = bold; + this.italic = italic; } @Nullable @@ -127,7 +133,13 @@ import java.util.regex.Pattern; : null, format.fontSizeIndex != C.INDEX_UNSET ? parseFontSize(styleValues[format.fontSizeIndex].trim()) - : Cue.DIMEN_UNSET); + : Cue.DIMEN_UNSET, + format.boldIndex != C.INDEX_UNSET + ? parseBoldOrItalic(styleValues[format.boldIndex].trim()) + : false, + format.italicIndex != C.INDEX_UNSET + ? parseBoldOrItalic(styleValues[format.italicIndex].trim()) + : false); } catch (RuntimeException e) { Log.w(TAG, "Skipping malformed 'Style:' line: '" + styleLine + "'", e); return null; @@ -213,6 +225,16 @@ import java.util.regex.Pattern; } } + private static boolean parseBoldOrItalic(String boldOrItalic) { + try { + int value = Integer.parseInt(boldOrItalic); + return value == 1 || value == -1; + } catch (NumberFormatException e) { + Log.w(TAG, "Failed to parse bold/italic: '" + boldOrItalic + "'", e); + return false; + } + } + /** * Represents a {@code Format:} line from the {@code [V4+ Styles]} section * @@ -225,14 +247,24 @@ import java.util.regex.Pattern; public final int alignmentIndex; public final int primaryColorIndex; public final int fontSizeIndex; + public final int boldIndex; + public final int italicIndex; public final int length; private Format( - int nameIndex, int alignmentIndex, int primaryColorIndex, int fontSizeIndex, int length) { + int nameIndex, + int alignmentIndex, + int primaryColorIndex, + int fontSizeIndex, + int boldIndex, + int italicIndex, + int length) { this.nameIndex = nameIndex; this.alignmentIndex = alignmentIndex; this.primaryColorIndex = primaryColorIndex; this.fontSizeIndex = fontSizeIndex; + this.boldIndex = boldIndex; + this.italicIndex = italicIndex; this.length = length; } @@ -247,6 +279,8 @@ import java.util.regex.Pattern; int alignmentIndex = C.INDEX_UNSET; int primaryColorIndex = C.INDEX_UNSET; int fontSizeIndex = C.INDEX_UNSET; + int boldIndex = C.INDEX_UNSET; + int italicIndex = C.INDEX_UNSET; String[] keys = TextUtils.split(styleFormatLine.substring(SsaDecoder.FORMAT_LINE_PREFIX.length()), ","); for (int i = 0; i < keys.length; i++) { @@ -263,10 +297,23 @@ import java.util.regex.Pattern; case "fontsize": fontSizeIndex = i; break; + case "bold": + boldIndex = i; + break; + case "italic": + italicIndex = i; + break; } } return nameIndex != C.INDEX_UNSET - ? new Format(nameIndex, alignmentIndex, primaryColorIndex, fontSizeIndex, keys.length) + ? new Format( + nameIndex, + alignmentIndex, + primaryColorIndex, + fontSizeIndex, + boldIndex, + italicIndex, + keys.length) : null; } } diff --git a/library/core/src/test/java/com/google/android/exoplayer2/text/ssa/SsaDecoderTest.java b/library/core/src/test/java/com/google/android/exoplayer2/text/ssa/SsaDecoderTest.java index 24a9188022..46192e4618 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/text/ssa/SsaDecoderTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/text/ssa/SsaDecoderTest.java @@ -49,6 +49,7 @@ public final class SsaDecoderTest { 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_FONT_SIZE = "media/ssa/style_font_size"; + private static final String STYLE_BOLD_ITALIC = "media/ssa/style_bold_italic"; @Test public void decodeEmpty() throws IOException { @@ -336,6 +337,25 @@ public final class SsaDecoderTest { assertThat(secondCue.textSizeType).isEqualTo(Cue.TEXT_SIZE_TYPE_FRACTIONAL_IGNORE_PADDING); } + @Test + public void decodeBoldItalic() throws IOException { + SsaDecoder decoder = new SsaDecoder(); + byte[] bytes = + TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), STYLE_BOLD_ITALIC); + Subtitle subtitle = decoder.decode(bytes, bytes.length, false); + assertThat(subtitle.getEventTimeCount()).isEqualTo(6); + + Spanned firstCueText = + (Spanned) Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(0))).text; + SpannedSubject.assertThat(firstCueText).hasBoldSpanBetween(0, firstCueText.length()); + Spanned secondCueText = + (Spanned) Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(2))).text; + SpannedSubject.assertThat(secondCueText).hasItalicSpanBetween(0, secondCueText.length()); + Spanned thirdCueText = + (Spanned) Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(4))).text; + SpannedSubject.assertThat(thirdCueText).hasBoldItalicSpanBetween(0, thirdCueText.length()); + } + private static void assertTypicalCue1(Subtitle subtitle, int eventIndex) { assertThat(subtitle.getEventTime(eventIndex)).isEqualTo(0); assertThat(subtitle.getCues(subtitle.getEventTime(eventIndex)).get(0).text.toString()) diff --git a/testdata/src/test/assets/media/ssa/style_bold_italic b/testdata/src/test/assets/media/ssa/style_bold_italic new file mode 100644 index 0000000000..4d8d110efc --- /dev/null +++ b/testdata/src/test/assets/media/ssa/style_bold_italic @@ -0,0 +1,18 @@ +[Script Info] +Title: SSA/ASS Test +Original Script: Abel +Script Type: V4.00+ +PlayResX: 1280 +PlayResY: 720 + +[V4+ Styles] +Format: Name ,Bold,Italic +Style: FontBold ,-1 ,0 +Style: FontItalic ,0 ,-1 +Style: FontBoldItalic ,1 ,1 + +[Events] +Format: Start ,End ,Style ,Text +Dialogue: 0:00:01.00,0:00:03.00,FontBold ,First line with Bold. +Dialogue: 0:00:05.00,0:00:07.00,FontItalic ,Second line with Italic. +Dialogue: 0:00:09.00,0:00:11.00,FontBoldItalic,Third line with Bold Italic.