Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for SSA (V4+) PrimaryColour style #8490

Merged
merged 5 commits into from
Feb 1, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Original file line number Diff line number Diff line change
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,8 +303,17 @@ 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);

// Apply primary color.
if (style != null) {
if (style.primaryColor.isSet()) {
spannableText.setSpan(new ForegroundColorSpan(style.primaryColor.getColor()),
0, spannableText.length(), SpannableString.SPAN_EXCLUSIVE_EXCLUSIVE);
}
}
// Apply alignment.
@SsaStyle.SsaAlignment int alignment;
if (styleOverrides.alignment != SsaStyle.SSA_ALIGNMENT_UNKNOWN) {
alignment = styleOverrides.alignment;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,17 @@

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.ColorParser;
import com.google.android.exoplayer2.util.Log;
import com.google.android.exoplayer2.util.Util;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.util.NoSuchElementException;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

Expand Down Expand Up @@ -85,10 +88,12 @@

public final String name;
@SsaAlignment public final int alignment;
public SsaColor primaryColor;

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

@Nullable
Expand All @@ -105,7 +110,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]),
parsePrimaryColor(styleValues[format.primaryColorIndex]));
} catch (RuntimeException e) {
Log.w(TAG, "Skipping malformed 'Style:' line: '" + styleLine + "'", e);
return null;
Expand Down Expand Up @@ -144,6 +151,46 @@ private static boolean isValidAlignment(@SsaAlignment int alignment) {
}
}

private static SsaColor parsePrimaryColor(String primaryColorStr) {
try {
return SsaColor.from(ColorParser.parseSsaColor(primaryColorStr.trim()));
} catch (IllegalArgumentException ex) {
Log.w(TAG, "Failed parsing color value: " + primaryColorStr);
}
return SsaColor.UNSET;
}

/**
* Represents an SSA V4+ style color in ARGB format.
*/
/* package */ static final class SsaColor {

public static SsaColor UNSET = new SsaColor(0, false);

private final @ColorInt int color;
private final boolean isSet;

private SsaColor(@ColorInt int color, boolean isSet) {
this.color = color;
this.isSet = isSet;
}

public @ColorInt int getColor() {
if (!isSet) {
throw new NoSuchElementException("No color is present");
}
return color;
}

public boolean isSet() {
return isSet;
}

public static SsaColor from(@ColorInt int value) {
return new SsaColor(value, true);
}
}

/**
* 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;
icbaker marked this conversation as resolved.
Show resolved Hide resolved
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
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,45 @@ public static int parseCssColor(String colorExpression) {
return parseColorInternal(colorExpression, true);
}

/**
* Parses a SSA V4+ color expression.
*
* @param colorExpression The color expression.
* @return The parsed ARGB color.
*/
@ColorInt
public static int parseSsaColor(String colorExpression) {
// The SSA V4+ color can be represented in hex (&HAABBGGRR) or in decimal format (byte order
// AABBGGRR) and in both cases the alpha channel's value needs to be inverted as in case of SSA
// the 0xFF alpha value means transparent and 0x00 means opaque which is the opposite from the
// @ColorInt representation.
int abgr;
try {
// Parse color from hex format (&HAABBGGRR).
if (colorExpression.startsWith("&H")) {
StringBuilder rgbaStringBuilder = new StringBuilder(colorExpression);
if (colorExpression.length() < 10) {
// Add leading zeros if necessary.
while (rgbaStringBuilder.length() != 10) {
rgbaStringBuilder.insert(2, "0");
}
}
abgr = (int) Long.parseLong(rgbaStringBuilder.substring(2), 16);
} else {
// Parse color from decimal format (bytes order AABBGGRR).
abgr = (int) Long.parseLong(colorExpression);
}
} catch (NumberFormatException ex) {
throw new IllegalArgumentException(ex);
}
// Convert ABGR to ARGB.
int a = ((abgr >> 24) & 0xFF) ^ 0xFF; // Flip alpha.
int b = (abgr >> 16) & 0xFF;
int g = (abgr >> 8) & 0xFF;
int r = abgr & 0xff;
return Color.argb(a, r, g, b);
}

@ColorInt
private static int parseColorInternal(String colorExpression, boolean alphaHasFloatFormat) {
Assertions.checkArgument(!TextUtils.isEmpty(colorExpression));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,10 @@
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.SpannableString;
import android.text.style.ForegroundColorSpan;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.android.exoplayer2.testutil.TestUtil;
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,46 @@ 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(12);
// &H000000FF (AABBGGRR) -> #FFFF0000 (AARRGGBB)
Cue firstCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(0)));
ForegroundColorSpan firstSpan = getSpan(firstCue, ForegroundColorSpan.class);
assertThat(firstSpan.getForegroundColor()).isEqualTo(Color.RED);
// &H0000FFFF (AABBGGRR) -> #FFFFFF00 (AARRGGBB)
Cue secondCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(2)));
ForegroundColorSpan secondSpan = getSpan(secondCue, ForegroundColorSpan.class);
assertThat(secondSpan.getForegroundColor()).isEqualTo(Color.YELLOW);
// &HFF00 (GGRR) -> #FF00FF00 (AARRGGBB)
Cue thirdClue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(4)));
ForegroundColorSpan thirdSpan = getSpan(thirdClue, ForegroundColorSpan.class);
assertThat(thirdSpan.getForegroundColor()).isEqualTo(Color.GREEN);
// &H400000FF (AABBGGRR) -> #BFFF0000 (AARRGGBB) -> -1073807360
Cue forthClue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(6)));
ForegroundColorSpan forthSpan = getSpan(forthClue, ForegroundColorSpan.class);
assertThat(forthSpan.getForegroundColor()).isEqualTo(-1073807360);
// 16711680 (AABBGGRR) -> &H00FF0000 (AABBGGRR) -> #FF0000FF (AARRGGBB) -> -16776961
Cue fifthClue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(8)));
ForegroundColorSpan fifthSpan = getSpan(fifthClue, ForegroundColorSpan.class);
assertThat(fifthSpan.getForegroundColor()).isEqualTo(-16776961);
// 2164195328 (AABBGGRR) -> &H80FF0000 (AABBGGRR) -> #7F0000FF (AARRGGBB) -> 2130706687
Cue sixthClue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(10)));
ForegroundColorSpan sixthSpan = getSpan(sixthClue, ForegroundColorSpan.class);
assertThat(sixthSpan.getForegroundColor()).isEqualTo(2130706687);
}

private static <T> T getSpan(Cue cue, Class<T> clazz) {
return getSpan(cue, 0, cue.text.length(), clazz);
}

private static <T> T getSpan(Cue cue, int start, int end, Class<T> clazz) {
return SpannableString.valueOf(cue.text).getSpans(start, end, clazz)[0];
}

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
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,14 @@
package com.google.android.exoplayer2.util;

import static android.graphics.Color.BLACK;
import static android.graphics.Color.BLUE;
import static android.graphics.Color.GREEN;
import static android.graphics.Color.RED;
import static android.graphics.Color.WHITE;
import static android.graphics.Color.YELLOW;
import static android.graphics.Color.argb;
import static android.graphics.Color.parseColor;
import static com.google.android.exoplayer2.util.ColorParser.parseSsaColor;
import static com.google.android.exoplayer2.util.ColorParser.parseTtmlColor;
import static com.google.common.truth.Truth.assertThat;

Expand Down Expand Up @@ -66,6 +70,21 @@ public void hexCodeParsing() {
assertThat(parseTtmlColor("#12345678")).isEqualTo(parseColor("#78123456"));
}

@Test
public void ssaColorParsing() {
// Hex format (&HAABBGGRR).
assertThat(parseSsaColor("&H000000FF")).isEqualTo(RED);
assertThat(parseSsaColor("&H0000FFFF")).isEqualTo(YELLOW);
assertThat(parseSsaColor("&H400000FF")).isEqualTo(parseColor("#BFFF0000"));
// Leading zeros.
assertThat(parseSsaColor("&HFF")).isEqualTo(RED);
assertThat(parseSsaColor("&HFF00")).isEqualTo(GREEN);
assertThat(parseSsaColor("&HFF0000")).isEqualTo(BLUE);
// Decimal format (AABBGGRR byte order).
assertThat(parseSsaColor(/*#000000FF*/"255")).isEqualTo(parseColor("#FFFF0000"));
assertThat(parseSsaColor(/*#FF0000FF*/"4278190335")).isEqualTo(parseColor("#00FF0000"));
}

@Test
public void rgbColorParsing() {
assertThat(parseTtmlColor("rgb(255,255,255)")).isEqualTo(WHITE);
Expand Down
24 changes: 24 additions & 0 deletions testdata/src/test/assets/media/ssa/colors
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
[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,&H400000FF,&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


[Events]
Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text
Dialogue: 0,0:00:00.95,0:00:03.11,PrimaryColourStyleHexRed ,Arnold,0,0,0,,First line in RED (&H000000FF).
Dialogue: 0,0:00:04.50,0:00:07.50,PrimaryColourStyleHexYellow ,Arnold,0,0,0,,Second line in YELLOW (&H0000FFFF).
Dialogue: 0,0:00:08.50,0:00:11.50,PrimaryColourStyleHexGreen ,Arnold,0,0,0,,Third line in GREEN (leading zeros &HFF00).
Dialogue: 0,0:00:12.50,0:00:15.50,PrimaryColourStyleHexAlpha ,Arnold,0,0,0,,Fourth line in RED with alpha (&H400000FF).
Dialogue: 0,0:00:16.50,0:00:19.50,PrimaryColourStyleDecimal ,Arnold,0,0,0,,Fifth line in BLUE (16711680).
Dialogue: 0,0:00:20.70,0:00:23.00,PrimaryColourStyleDecimalAlpha ,Arnold,0,0,0,,Sixth line in BLUE with alpha (2164195328).