Skip to content

Commit

Permalink
Subrip cleanup
Browse files Browse the repository at this point in the history
  • Loading branch information
ojw28 committed Oct 3, 2018
1 parent 16fe67b commit 7849a5e
Show file tree
Hide file tree
Showing 4 changed files with 111 additions and 171 deletions.
2 changes: 2 additions & 0 deletions RELEASENOTES.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@
* Fix issue where subtitles have a wrong position if SubtitleView has a non-zero
offset to its parent
([#4788](https://github.com/google/ExoPlayer/issues/4788)).
* SubRip: Add support for alignment tags, and remove tags from the displayed
captions ([#4306](https://github.com/google/ExoPlayer/issues/4306)).

### 2.9.0 ###

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,19 +15,15 @@
*/
package com.google.android.exoplayer2.text.subrip;

import android.support.annotation.StringDef;
import android.support.annotation.Nullable;
import android.text.Html;
import android.text.Layout;
import android.text.Spanned;
import android.text.TextUtils;
import com.google.android.exoplayer2.text.Cue;
import com.google.android.exoplayer2.text.SimpleSubtitleDecoder;
import com.google.android.exoplayer2.util.Log;
import com.google.android.exoplayer2.util.LongArray;
import com.google.android.exoplayer2.util.ParsableByteArray;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
Expand All @@ -37,6 +33,11 @@
*/
public final class SubripDecoder extends SimpleSubtitleDecoder {

// Fractional positions for use when alignment tags are present.
/* package */ static final float START_FRACTION = 0.08f;
/* package */ static final float END_FRACTION = 1 - START_FRACTION;
/* package */ static final float MID_FRACTION = 0.5f;

private static final String TAG = "SubripDecoder";

private static final String SUBRIP_TIMECODE = "(?:(\\d+):)?(\\d+):(\\d+),(\\d+)";
Expand All @@ -46,35 +47,24 @@ public final class SubripDecoder extends SimpleSubtitleDecoder {
private static final Pattern SUBRIP_TAG_PATTERN = Pattern.compile("\\{\\\\.*?\\}");
private static final String SUBRIP_ALIGNMENT_TAG = "\\{\\\\an[1-9]\\}";

private static final float DEFAULT_START_FRACTION = 0.08f;
private static final float DEFAULT_END_FRACTION = 1 - DEFAULT_START_FRACTION;
private static final float DEFAULT_MID_FRACTION = 0.5f;

@Retention(RetentionPolicy.SOURCE)
@StringDef({
ALIGN_BOTTOM_LEFT, ALIGN_BOTTOM_MID, ALIGN_BOTTOM_RIGHT,
ALIGN_MID_LEFT, ALIGN_MID_MID, ALIGN_MID_RIGHT,
ALIGN_TOP_LEFT, ALIGN_TOP_MID, ALIGN_TOP_RIGHT
})

private @interface SubRipTag {}

// Possible valid alignment tags based on SSA v4+ specs
private static final String ALIGN_BOTTOM_LEFT = "{\\an1}";
private static final String ALIGN_BOTTOM_MID = "{\\an2}";
// Alignment tags for SSA V4+.
private static final String ALIGN_BOTTOM_LEFT = "{\\an1}";
private static final String ALIGN_BOTTOM_MID = "{\\an2}";
private static final String ALIGN_BOTTOM_RIGHT = "{\\an3}";
private static final String ALIGN_MID_LEFT = "{\\an4}";
private static final String ALIGN_MID_MID = "{\\an5}";
private static final String ALIGN_MID_RIGHT = "{\\an6}";
private static final String ALIGN_TOP_LEFT = "{\\an7}";
private static final String ALIGN_TOP_MID = "{\\an8}";
private static final String ALIGN_TOP_RIGHT = "{\\an9}";
private static final String ALIGN_MID_LEFT = "{\\an4}";
private static final String ALIGN_MID_MID = "{\\an5}";
private static final String ALIGN_MID_RIGHT = "{\\an6}";
private static final String ALIGN_TOP_LEFT = "{\\an7}";
private static final String ALIGN_TOP_MID = "{\\an8}";
private static final String ALIGN_TOP_RIGHT = "{\\an9}";

private final StringBuilder textBuilder;
private final ArrayList<String> tags;

public SubripDecoder() {
super("SubripDecoder");
textBuilder = new StringBuilder();
tags = new ArrayList<>();
}

@Override
Expand Down Expand Up @@ -118,9 +108,9 @@ protected SubripSubtitle decode(byte[] bytes, int length, boolean reset) {
continue;
}

// Read and parse the text.
ArrayList<String> tags = new ArrayList<>();
// Read and parse the text and tags.
textBuilder.setLength(0);
tags.clear();
while (!TextUtils.isEmpty(currentLine = subripData.readLine())) {
if (textBuilder.length() > 0) {
textBuilder.append("<br>");
Expand All @@ -129,21 +119,17 @@ protected SubripSubtitle decode(byte[] bytes, int length, boolean reset) {
}

Spanned text = Html.fromHtml(textBuilder.toString());
Cue cue = null;

// At end of this loop the clue must be created with the applied tags
for (String tag : tags) {

// Check if the tag is an alignment tag
String alignmentTag = null;
for (int i = 0; i < tags.size(); i++) {
String tag = tags.get(i);
if (tag.matches(SUBRIP_ALIGNMENT_TAG)) {
cue = buildCue(text, tag);

// Based on the specs, in case of alignment tags only the first appearance counts, so break
alignmentTag = tag;
// Subsequent alignment tags should be ignored.
break;
}
}

cues.add(cue == null ? new Cue(text) : cue);
cues.add(buildCue(text, alignmentTag));

if (haveEndTimecode) {
cues.add(null);
Expand All @@ -157,108 +143,93 @@ protected SubripSubtitle decode(byte[] bytes, int length, boolean reset) {
}

/**
* Process the given line by first trimming it then extracting the tags from it
* <p>
* The pattern that is used to extract the tags is specified in SSA v4+ specs and
* has the following form: "{\...}".
* <p>
* "All override codes appear within braces {}"
* "All override codes are always preceded by a backslash \"
* Trims and removes tags from the given line. The removed tags are added to {@code tags}.
*
* @param currentLine Current line
* @param tags Extracted tags will be stored in this array list
* @return Processed line
* @param line The line to process.
* @param tags A list to which removed tags will be added.
* @return The processed line.
*/
private String processLine(String currentLine, ArrayList<String> tags) {
// Trim line
String trimmedLine = currentLine.trim();

// Extract tags
int replacedCharacters = 0;
StringBuilder processedLine = new StringBuilder(trimmedLine);
Matcher matcher = SUBRIP_TAG_PATTERN.matcher(trimmedLine);
private String processLine(String line, ArrayList<String> tags) {
line = line.trim();

int removedCharacterCount = 0;
StringBuilder processedLine = new StringBuilder(line);
Matcher matcher = SUBRIP_TAG_PATTERN.matcher(line);
while (matcher.find()) {
String tag = matcher.group();
tags.add(tag);
processedLine.replace(matcher.start() - replacedCharacters, matcher.end() - replacedCharacters, "");
replacedCharacters += tag.length();
int start = matcher.start() - removedCharacterCount;
int tagLength = tag.length();
processedLine.replace(start, /* end= */ start + tagLength, /* str= */ "");
removedCharacterCount += tagLength;
}

return processedLine.toString();
}

/**
* Build a {@link Cue} based on the given text and tag
* <p>
* Match the alignment tag and calculate the line, position, position anchor accordingly
* <p>
* Based on SSA v4+ specs the alignment tag can have the following form: {\an[1-9},
* where the number specifies the direction (based on the numpad layout).
* Note. older SSA scripts may contain tags like {\a1[1-9]} but these are based on
* other direction rules, but multiple sources says that these are deprecated, so no support here either
* Build a {@link Cue} based on the given text and alignment tag.
*
* @param alignmentTag Alignment tag
* @param text The text.
* @param alignmentTag The alignment tag, or {@code null} if no alignment tag is available.
* @return Built cue
*/
private Cue buildCue(Spanned text, String alignmentTag) {
float line, position;
@Cue.AnchorType int positionAnchor;
@Cue.AnchorType int lineAnchor;
private Cue buildCue(Spanned text, @Nullable String alignmentTag) {
if (alignmentTag == null) {
return new Cue(text);
}

// Set position and position anchor (horizontal alignment)
// Horizontal alignment.
@Cue.AnchorType int positionAnchor;
switch (alignmentTag) {
case ALIGN_BOTTOM_LEFT:
case ALIGN_MID_LEFT:
case ALIGN_TOP_LEFT:
position = DEFAULT_START_FRACTION;
positionAnchor = Cue.ANCHOR_TYPE_START;
break;
case ALIGN_BOTTOM_MID:
case ALIGN_MID_MID:
case ALIGN_TOP_MID:
position = DEFAULT_MID_FRACTION;
positionAnchor = Cue.ANCHOR_TYPE_MIDDLE;
break;
case ALIGN_BOTTOM_RIGHT:
case ALIGN_MID_RIGHT:
case ALIGN_TOP_RIGHT:
position = DEFAULT_END_FRACTION;
positionAnchor = Cue.ANCHOR_TYPE_END;
break;
case ALIGN_BOTTOM_MID:
case ALIGN_MID_MID:
case ALIGN_TOP_MID:
default:
position = DEFAULT_MID_FRACTION;
positionAnchor = Cue.ANCHOR_TYPE_MIDDLE;
break;
}

// Set line and line anchor (vertical alignment)
// Vertical alignment.
@Cue.AnchorType int lineAnchor;
switch (alignmentTag) {
case ALIGN_BOTTOM_LEFT:
case ALIGN_BOTTOM_MID:
case ALIGN_BOTTOM_RIGHT:
line = DEFAULT_END_FRACTION;
lineAnchor = Cue.ANCHOR_TYPE_END;
break;
case ALIGN_MID_LEFT:
case ALIGN_MID_MID:
case ALIGN_MID_RIGHT:
line = DEFAULT_MID_FRACTION;
lineAnchor = Cue.ANCHOR_TYPE_MIDDLE;
break;
case ALIGN_TOP_LEFT:
case ALIGN_TOP_MID:
case ALIGN_TOP_RIGHT:
line = DEFAULT_START_FRACTION;
lineAnchor = Cue.ANCHOR_TYPE_START;
break;
case ALIGN_MID_LEFT:
case ALIGN_MID_MID:
case ALIGN_MID_RIGHT:
default:
line = DEFAULT_END_FRACTION;
lineAnchor = Cue.ANCHOR_TYPE_END;
lineAnchor = Cue.ANCHOR_TYPE_MIDDLE;
break;
}

return new Cue(text, null, line, Cue.LINE_TYPE_FRACTION, lineAnchor, position, positionAnchor, Cue.DIMEN_UNSET);
return new Cue(
text,
/* textAlignment= */ null,
getFractionalPositionForAnchorType(lineAnchor),
Cue.LINE_TYPE_FRACTION,
lineAnchor,
getFractionalPositionForAnchorType(positionAnchor),
positionAnchor,
Cue.DIMEN_UNSET);
}

private static long parseTimecode(Matcher matcher, int groupOffset) {
Expand All @@ -268,4 +239,16 @@ private static long parseTimecode(Matcher matcher, int groupOffset) {
timestampMs += Long.parseLong(matcher.group(groupOffset + 4));
return timestampMs * 1000;
}

/* package */ static float getFractionalPositionForAnchorType(@Cue.AnchorType int anchorType) {
switch (anchorType) {
case Cue.ANCHOR_TYPE_START:
return SubripDecoder.START_FRACTION;
case Cue.ANCHOR_TYPE_MIDDLE:
return SubripDecoder.MID_FRACTION;
case Cue.ANCHOR_TYPE_END:
default:
return SubripDecoder.END_FRACTION;
}
}
}
4 changes: 2 additions & 2 deletions library/core/src/test/assets/subrip/typical_with_tags
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ This {\an2} is the third {\ tag} subtitle.

4
00:00:09,567 --> 00:00:12,901
This { \an2} is the fourth subtitle.
This { \an2} is not a valid tag due to the space after the opening bracket.

5
00:00:013,567 --> 00:00:14,901
Expand Down Expand Up @@ -53,4 +53,4 @@ This {\an8} is a line.

14
00:00:024,567 --> 00:00:24,901
This {\an9} is a line.
This {\an9} is a line.
Loading

0 comments on commit 7849a5e

Please sign in to comment.