Skip to content

Commit

Permalink
Merge pull request #8490 from szaboa:dev-2-8435-ssa-color
Browse files Browse the repository at this point in the history
PiperOrigin-RevId: 354293679
  • Loading branch information
ojw28 committed Feb 1, 2021
2 parents ae51e2e + 28a3921 commit c9fce08
Show file tree
Hide file tree
Showing 5 changed files with 156 additions and 8 deletions.
3 changes: 3 additions & 0 deletions RELEASENOTES.md
Expand Up @@ -5,6 +5,9 @@
* Extractors:
* Fix Vorbis private codec data parsing in the Matroska extractor
([#8496](https://github.com/google/ExoPlayer/issues/8496)).
* Text:
* Add support for the SSA `primaryColour` style attribute
([#8435](https://github.com/google/ExoPlayer/issues/8435)).

### 2.13.0 (not yet released - targeted for 2021-02-TBD)

Expand Down
Expand Up @@ -19,6 +19,8 @@
import static com.google.android.exoplayer2.util.Util.castNonNull;

import android.text.Layout;
import android.text.SpannableString;
import android.text.style.ForegroundColorSpan;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.text.Cue;
Expand Down Expand Up @@ -301,7 +303,18 @@ private static Cue createCue(
SsaStyle.Overrides styleOverrides,
float screenWidth,
float screenHeight) {
Cue.Builder cue = new Cue.Builder().setText(text);
SpannableString spannableText = new SpannableString(text);
Cue.Builder cue = new Cue.Builder().setText(spannableText);

if (style != null) {
if (style.primaryColor != null) {
spannableText.setSpan(
new ForegroundColorSpan(style.primaryColor),
/* start= */ 0,
/* end= */ spannableText.length(),
SpannableString.SPAN_EXCLUSIVE_EXCLUSIVE);
}
}

@SsaStyle.SsaAlignment int alignment;
if (styleOverrides.alignment != SsaStyle.SSA_ALIGNMENT_UNKNOWN) {
Expand Down
Expand Up @@ -17,16 +17,20 @@
package com.google.android.exoplayer2.text.ssa;

import static com.google.android.exoplayer2.text.ssa.SsaDecoder.STYLE_LINE_PREFIX;
import static com.google.android.exoplayer2.util.Assertions.checkArgument;
import static java.lang.annotation.RetentionPolicy.SOURCE;

import android.graphics.Color;
import android.graphics.PointF;
import android.text.TextUtils;
import androidx.annotation.ColorInt;
import androidx.annotation.IntDef;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.Log;
import com.google.android.exoplayer2.util.Util;
import com.google.common.primitives.Ints;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.util.regex.Matcher;
Expand Down Expand Up @@ -85,15 +89,18 @@

public final String name;
@SsaAlignment public final int alignment;
@Nullable @ColorInt public final Integer primaryColor;

private SsaStyle(String name, @SsaAlignment int alignment) {
private SsaStyle(
String name, @SsaAlignment int alignment, @Nullable @ColorInt Integer primaryColor) {
this.name = name;
this.alignment = alignment;
this.primaryColor = primaryColor;
}

@Nullable
public static SsaStyle fromStyleLine(String styleLine, Format format) {
Assertions.checkArgument(styleLine.startsWith(STYLE_LINE_PREFIX));
checkArgument(styleLine.startsWith(STYLE_LINE_PREFIX));
String[] styleValues = TextUtils.split(styleLine.substring(STYLE_LINE_PREFIX.length()), ",");
if (styleValues.length != format.length) {
Log.w(
Expand All @@ -105,7 +112,9 @@ public static SsaStyle fromStyleLine(String styleLine, Format format) {
}
try {
return new SsaStyle(
styleValues[format.nameIndex].trim(), parseAlignment(styleValues[format.alignmentIndex]));
styleValues[format.nameIndex].trim(),
parseAlignment(styleValues[format.alignmentIndex].trim()),
parseColor(styleValues[format.primaryColorIndex].trim()));
} catch (RuntimeException e) {
Log.w(TAG, "Skipping malformed 'Style:' line: '" + styleLine + "'", e);
return null;
Expand Down Expand Up @@ -144,6 +153,44 @@ private static boolean isValidAlignment(@SsaAlignment int alignment) {
}
}

/**
* Parses a SSA V4+ color expression.
*
* <p>A SSA V4+ color can be represented in hex {@code ("&HAABBGGRR")} or in 64-bit decimal format
* (byte order AABBGGRR). In both cases the alpha channel's value needs to be inverted because in
* SSA the 0xFF alpha value means transparent and 0x00 means opaque which is the opposite from the
* Android {@link ColorInt} representation.
*
* @param ssaColorExpression A SSA V4+ color expression.
* @return The parsed color value, or null if parsing failed.
*/
@Nullable
@ColorInt
public static Integer parseColor(String ssaColorExpression) {
// We use a long because the value is an unsigned 32-bit number, so can be larger than
// Integer.MAX_VALUE.
long abgr;
try {
abgr =
ssaColorExpression.startsWith("&H")
// Parse color from hex format (&HAABBGGRR).
? Long.parseLong(ssaColorExpression.substring(2), /* radix= */ 16)
// Parse color from decimal format (bytes order AABBGGRR).
: Long.parseLong(ssaColorExpression);
// Ensure only the bottom 4 bytes of abgr are set.
checkArgument(abgr <= 0xFFFFFFFFL);
} catch (IllegalArgumentException e) {
Log.w(TAG, "Failed to parse color expression: '" + ssaColorExpression + "'", e);
return null;
}
// Convert ABGR to ARGB.
int a = Ints.checkedCast(((abgr >> 24) & 0xFF) ^ 0xFF); // Flip alpha.
int b = Ints.checkedCast((abgr >> 16) & 0xFF);
int g = Ints.checkedCast((abgr >> 8) & 0xFF);
int r = Ints.checkedCast(abgr & 0xFF);
return Color.argb(a, r, g, b);
}

/**
* Represents a {@code Format:} line from the {@code [V4+ Styles]} section
*
Expand All @@ -154,11 +201,13 @@ private static boolean isValidAlignment(@SsaAlignment int alignment) {

public final int nameIndex;
public final int alignmentIndex;
public final int primaryColorIndex;
public final int length;

private Format(int nameIndex, int alignmentIndex, int length) {
private Format(int nameIndex, int alignmentIndex, int primaryColorIndex, int length) {
this.nameIndex = nameIndex;
this.alignmentIndex = alignmentIndex;
this.primaryColorIndex = primaryColorIndex;
this.length = length;
}

Expand All @@ -171,6 +220,7 @@ private Format(int nameIndex, int alignmentIndex, int length) {
public static Format fromFormatLine(String styleFormatLine) {
int nameIndex = C.INDEX_UNSET;
int alignmentIndex = C.INDEX_UNSET;
int primaryColorIndex = C.INDEX_UNSET;
String[] keys =
TextUtils.split(styleFormatLine.substring(SsaDecoder.FORMAT_LINE_PREFIX.length()), ",");
for (int i = 0; i < keys.length; i++) {
Expand All @@ -181,9 +231,14 @@ public static Format fromFormatLine(String styleFormatLine) {
case "alignment":
alignmentIndex = i;
break;
case "primarycolour":
primaryColorIndex = i;
break;
}
}
return nameIndex != C.INDEX_UNSET ? new Format(nameIndex, alignmentIndex, keys.length) : null;
return nameIndex != C.INDEX_UNSET
? new Format(nameIndex, alignmentIndex, primaryColorIndex, keys.length)
: null;
}
}

Expand Down Expand Up @@ -237,8 +292,7 @@ public static Overrides parseFromDialogue(String text) {
// Ignore invalid \pos() or \move() function.
}
try {
@SsaAlignment
int parsedAlignment = parseAlignmentOverride(braceContents);
@SsaAlignment int parsedAlignment = parseAlignmentOverride(braceContents);
if (parsedAlignment != SSA_ALIGNMENT_UNKNOWN) {
alignment = parsedAlignment;
}
Expand Down
Expand Up @@ -18,10 +18,13 @@
import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.Truth.assertWithMessage;

import android.graphics.Color;
import android.text.Layout;
import android.text.Spanned;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.android.exoplayer2.testutil.TestUtil;
import com.google.android.exoplayer2.testutil.truth.SpannedSubject;
import com.google.android.exoplayer2.text.Cue;
import com.google.android.exoplayer2.text.Subtitle;
import com.google.common.collect.Iterables;
Expand All @@ -44,6 +47,7 @@ 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 COLORS = "media/ssa/colors";

@Test
public void decodeEmpty() throws IOException {
Expand Down Expand Up @@ -267,6 +271,54 @@ public void decodeInvalidTimecodes() throws IOException {
assertTypicalCue3(subtitle, 0);
}

@Test
public void decodeColors() throws IOException {
SsaDecoder decoder = new SsaDecoder();
byte[] bytes = TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), COLORS);
Subtitle subtitle = decoder.decode(bytes, bytes.length, false);
assertThat(subtitle.getEventTimeCount()).isEqualTo(14);
// &H000000FF (AABBGGRR) -> #FFFF0000 (AARRGGBB)
Spanned firstCueText =
(Spanned) Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(0))).text;
SpannedSubject.assertThat(firstCueText)
.hasForegroundColorSpanBetween(0, firstCueText.length())
.withColor(Color.RED);
// &H0000FFFF (AABBGGRR) -> #FFFFFF00 (AARRGGBB)
Spanned secondCueText =
(Spanned) Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(2))).text;
SpannedSubject.assertThat(secondCueText)
.hasForegroundColorSpanBetween(0, secondCueText.length())
.withColor(Color.YELLOW);
// &HFF00 (GGRR) -> #FF00FF00 (AARRGGBB)
Spanned thirdCueText =
(Spanned) Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(4))).text;
SpannedSubject.assertThat(thirdCueText)
.hasForegroundColorSpanBetween(0, thirdCueText.length())
.withColor(Color.GREEN);
// &HA00000FF (AABBGGRR) -> #5FFF0000 (AARRGGBB)
Spanned fourthCueText =
(Spanned) Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(6))).text;
SpannedSubject.assertThat(fourthCueText)
.hasForegroundColorSpanBetween(0, fourthCueText.length())
.withColor(0x5FFF0000);
// 16711680 (AABBGGRR) -> &H00FF0000 (AABBGGRR) -> #FF0000FF (AARRGGBB)
Spanned fifthCueText =
(Spanned) Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(8))).text;
SpannedSubject.assertThat(fifthCueText)
.hasForegroundColorSpanBetween(0, fifthCueText.length())
.withColor(0xFF0000FF);
// 2164195328 (AABBGGRR) -> &H80FF0000 (AABBGGRR) -> #7F0000FF (AARRGGBB)
Spanned sixthCueText =
(Spanned) Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(10))).text;
SpannedSubject.assertThat(sixthCueText)
.hasForegroundColorSpanBetween(0, sixthCueText.length())
.withColor(0x7F0000FF);
Spanned seventhCueText =
(Spanned) Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(12))).text;
SpannedSubject.assertThat(seventhCueText)
.hasNoForegroundColorSpanBetween(0, seventhCueText.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())
Expand Down
26 changes: 26 additions & 0 deletions testdata/src/test/assets/media/ssa/colors
@@ -0,0 +1,26 @@
[Script Info]
Title: Coloring
Script Type: V4.00+
PlayResX: 1280
PlayResY: 720

[V4+ Styles]
Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding
Style: PrimaryColourStyleHexRed ,Roboto,50,&H000000FF,&H000000FF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,3,0,2,50,50,70,1
Style: PrimaryColourStyleHexYellow ,Roboto,50,&H0000FFFF,&H000000FF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,3,0,2,50,50,70,1
Style: PrimaryColourStyleHexGreen ,Roboto,50,&HFF00 ,&H000000FF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,3,0,2,50,50,70,1
Style: PrimaryColourStyleHexAlpha ,Roboto,50,&HA00000FF,&H000000FF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,3,0,2,50,50,70,1
Style: PrimaryColourStyleDecimal ,Roboto,50,16711680 ,&H000000FF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,3,0,2,50,50,70,1
Style: PrimaryColourStyleDecimalAlpha ,Roboto,50,2164195328,&H000000FF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,3,0,2,50,50,70,1
Style: PrimaryColourStyleInvalid ,Roboto,50,blue ,&H000000FF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,3,0,2,50,50,70,1


[Events]
Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text
Dialogue: 0,0:00:01.00,0:00:02.00,PrimaryColourStyleHexRed ,Arnold,0,0,0,,First line in RED (&H000000FF).
Dialogue: 0,0:00:03.00,0:00:04.00,PrimaryColourStyleHexYellow ,Arnold,0,0,0,,Second line in YELLOW (&H0000FFFF).
Dialogue: 0,0:00:05.00,0:00:06.00,PrimaryColourStyleHexGreen ,Arnold,0,0,0,,Third line in GREEN (leading zeros &HFF00).
Dialogue: 0,0:00:07.00,0:00:08.00,PrimaryColourStyleHexAlpha ,Arnold,0,0,0,,Fourth line in RED with alpha (&H400000FF).
Dialogue: 0,0:00:09.00,0:00:10.00,PrimaryColourStyleDecimal ,Arnold,0,0,0,,Fifth line in BLUE (16711680).
Dialogue: 0,0:00:11.00,0:00:12.00,PrimaryColourStyleDecimalAlpha ,Arnold,0,0,0,,Sixth line in BLUE with alpha (2164195328).
Dialogue: 0,0:00:13.00,0:00:14.00,PrimaryColourInvalid ,Arnold,0,0,0,,Seventh line with invalid color .

0 comments on commit c9fce08

Please sign in to comment.