From ae520a8c2cfac9702f3cc1c87d504faf5232d95f Mon Sep 17 00:00:00 2001 From: Arnold Szabo Date: Wed, 25 Jul 2018 01:29:07 +0300 Subject: [PATCH 1/5] #4306 - Extract tags from SubRip subtitles, add support for alignment tags based on SSA v4+ --- .../exoplayer2/text/subrip/SubripDecoder.java | 201 +++++++++++++++++- .../src/test/assets/subrip/typical_with_tags | 20 ++ .../text/subrip/SubripDecoderTest.java | 22 ++ 3 files changed, 241 insertions(+), 2 deletions(-) create mode 100644 library/core/src/test/assets/subrip/typical_with_tags diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/subrip/SubripDecoder.java b/library/core/src/main/java/com/google/android/exoplayer2/text/subrip/SubripDecoder.java index 6cce902e87..96c065973e 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/subrip/SubripDecoder.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/subrip/SubripDecoder.java @@ -15,7 +15,9 @@ */ package com.google.android.exoplayer2.text.subrip; +import android.support.annotation.StringDef; import android.text.Html; +import android.text.Layout; import android.text.Spanned; import android.text.TextUtils; import android.util.Log; @@ -23,7 +25,11 @@ import com.google.android.exoplayer2.text.Cue; import com.google.android.exoplayer2.text.SimpleSubtitleDecoder; 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.List; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -38,6 +44,33 @@ public final class SubripDecoder extends SimpleSubtitleDecoder { private static final Pattern SUBRIP_TIMING_LINE = Pattern.compile("\\s*(" + SUBRIP_TIMECODE + ")\\s*-->\\s*(" + SUBRIP_TIMECODE + ")?\\s*"); + 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}"; + 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 final StringBuilder textBuilder; public SubripDecoder() { @@ -95,8 +128,36 @@ public final class SubripDecoder extends SimpleSubtitleDecoder { textBuilder.append(currentLine.trim()); } - Spanned text = Html.fromHtml(textBuilder.toString()); - cues.add(new Cue(text)); + // Extract tags + SubtitleTagResult tagResult = extractTags(textBuilder); + Spanned text = Html.fromHtml(tagResult.cue); + + Cue cue = null; + + // Check if tags are present + if (tagResult.tags.length > 0) { + + boolean alignTagFound = false; + + // At end of this loop the clue must be created with the applied tags + for (String tag : tagResult.tags) { + + // Check if the tag is an alignment tag + if (tag.matches(SUBRIP_ALIGNMENT_TAG)) { + + // Based on the specs, in case of the alignment tags only the first appearance counts + if (alignTagFound) continue; + alignTagFound = true; + + AlignmentResult alignmentResult = getAlignmentValues(tag); + cue = new Cue(text, Layout.Alignment.ALIGN_NORMAL, alignmentResult.line, Cue.LINE_TYPE_FRACTION, + alignmentResult.lineAnchor, alignmentResult.position, alignmentResult.positionAnchor, Cue.DIMEN_UNSET); + } + } + } + + cues.add(cue == null ? new Cue(text) : cue); + if (haveEndTimecode) { cues.add(null); } @@ -108,6 +169,111 @@ public final class SubripDecoder extends SimpleSubtitleDecoder { return new SubripSubtitle(cuesArray, cueTimesUsArray); } + /** + * Extracts the tags from the given {@code cue} + * The pattern that is used to extract the tags is specified in SSA v4+ specs and + * has the following form: "{\...}". + *

+ * "All override codes appear within braces {}" + * "All override codes are always preceded by a backslash \" + * + * @param cue Cue text + * @return {@link SubtitleTagResult} that holds new cue and also the extracted tags + */ + private SubtitleTagResult extractTags(StringBuilder cue) { + StringBuilder cueCopy = new StringBuilder(cue.toString()); + List tags = new ArrayList<>(); + + int replacedCharacters = 0; + + Matcher matcher = SUBRIP_TAG_PATTERN.matcher(cue.toString()); + while (matcher.find()) { + String tag = matcher.group(); + tags.add(tag); + cueCopy.replace(matcher.start() - replacedCharacters, matcher.end() - replacedCharacters, ""); + replacedCharacters += tag.length(); + } + + return new SubtitleTagResult(tags.toArray(new String[tags.size()]), cueCopy.toString()); + } + + /** + * Match the alignment tag and calculate the line, position, position anchor accordingly + * + * 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 + * + * @param tag Alignment tag + * @return {@link AlignmentResult} that holds the line, position, position anchor values + */ + private AlignmentResult getAlignmentValues(String tag) { + // Default values used for positioning the subtitle in case of align tags + float line = DEFAULT_END_FRACTION, position = DEFAULT_MID_FRACTION; + @Cue.AnchorType int positionAnchor = Cue.ANCHOR_TYPE_MIDDLE; + @Cue.AnchorType int lineAnchor = Cue.ANCHOR_TYPE_END; + + switch (tag) { + case ALIGN_BOTTOM_LEFT: + line = DEFAULT_END_FRACTION; + position = DEFAULT_START_FRACTION; + positionAnchor = Cue.ANCHOR_TYPE_START; + lineAnchor = Cue.ANCHOR_TYPE_END; + break; + case ALIGN_BOTTOM_MID: + line = DEFAULT_END_FRACTION; + position = DEFAULT_MID_FRACTION; + positionAnchor = Cue.ANCHOR_TYPE_MIDDLE; + lineAnchor = Cue.ANCHOR_TYPE_END; + break; + case ALIGN_BOTTOM_RIGHT: + line = DEFAULT_END_FRACTION; + position = DEFAULT_END_FRACTION; + positionAnchor = Cue.ANCHOR_TYPE_END; + lineAnchor = Cue.ANCHOR_TYPE_END; + break; + case ALIGN_MID_LEFT: + line = DEFAULT_MID_FRACTION; + position = DEFAULT_START_FRACTION; + positionAnchor = Cue.ANCHOR_TYPE_START; + lineAnchor = Cue.ANCHOR_TYPE_MIDDLE; + break; + case ALIGN_MID_MID: + line = DEFAULT_MID_FRACTION; + position = DEFAULT_MID_FRACTION; + positionAnchor = Cue.ANCHOR_TYPE_MIDDLE; + lineAnchor = Cue.ANCHOR_TYPE_MIDDLE; + break; + case ALIGN_MID_RIGHT: + line = DEFAULT_MID_FRACTION; + position = DEFAULT_END_FRACTION; + positionAnchor = Cue.ANCHOR_TYPE_END; + lineAnchor = Cue.ANCHOR_TYPE_MIDDLE; + break; + case ALIGN_TOP_LEFT: + line = DEFAULT_START_FRACTION; + position = DEFAULT_START_FRACTION; + positionAnchor = Cue.ANCHOR_TYPE_START; + lineAnchor = Cue.ANCHOR_TYPE_START; + break; + case ALIGN_TOP_MID: + line = DEFAULT_START_FRACTION; + position = DEFAULT_MID_FRACTION; + positionAnchor = Cue.ANCHOR_TYPE_MIDDLE; + lineAnchor = Cue.ANCHOR_TYPE_START; + break; + case ALIGN_TOP_RIGHT: + line = DEFAULT_START_FRACTION; + position = DEFAULT_END_FRACTION; + positionAnchor = Cue.ANCHOR_TYPE_END; + lineAnchor = Cue.ANCHOR_TYPE_START; + break; + } + + return new AlignmentResult(positionAnchor, position, lineAnchor, line); + } + private static long parseTimecode(Matcher matcher, int groupOffset) { long timestampMs = Long.parseLong(matcher.group(groupOffset + 1)) * 60 * 60 * 1000; timestampMs += Long.parseLong(matcher.group(groupOffset + 2)) * 60 * 1000; @@ -116,4 +282,35 @@ public final class SubripDecoder extends SimpleSubtitleDecoder { return timestampMs * 1000; } + /** + * Class that holds the tags, new clue after the tag extraction + */ + private static final class SubtitleTagResult { + public final String[] tags; + public final String cue; + + public SubtitleTagResult(String[] tags, String cue) { + this.tags = tags; + this.cue = cue; + } + } + + /** + * Class that holds the parsed and mapped alignment values (such as line, + * position and anchor type of line) + */ + private static final class AlignmentResult { + + public @Cue.AnchorType int positionAnchor; + public @Cue.AnchorType int lineAnchor; + public float position, line; + + public AlignmentResult(@Cue.AnchorType int positionAnchor, float position, @Cue.AnchorType int lineAnchor, float line) { + this.positionAnchor = positionAnchor; + this.position = position; + this.line = line; + this.lineAnchor = lineAnchor; + } + } + } diff --git a/library/core/src/test/assets/subrip/typical_with_tags b/library/core/src/test/assets/subrip/typical_with_tags new file mode 100644 index 0000000000..02e1ffbcd9 --- /dev/null +++ b/library/core/src/test/assets/subrip/typical_with_tags @@ -0,0 +1,20 @@ +1 +00:00:00,000 --> 00:00:01,234 +This is {\an1} the first subtitle. + +2 +00:00:02,345 --> 00:00:03,456 +This is the second subtitle. +Second {\ an 2} subtitle with second line. + +3 +00:00:04,567 --> 00:00:08,901 +This {\an2} is the third {\ tag} subtitle. + +4 +00:00:09,567 --> 00:00:12,901 +This { \an2} is the fourth subtitle. + +5 +00:00:013,567 --> 00:00:14,901 +This {\an2} is the fifth subtitle with multiple {\xyz} valid {\qwe} tags. diff --git a/library/core/src/test/java/com/google/android/exoplayer2/text/subrip/SubripDecoderTest.java b/library/core/src/test/java/com/google/android/exoplayer2/text/subrip/SubripDecoderTest.java index e9abaca075..a9d69076c2 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/text/subrip/SubripDecoderTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/text/subrip/SubripDecoderTest.java @@ -36,6 +36,7 @@ public final class SubripDecoderTest { private static final String TYPICAL_MISSING_SEQUENCE = "subrip/typical_missing_sequence"; private static final String TYPICAL_NEGATIVE_TIMESTAMPS = "subrip/typical_negative_timestamps"; private static final String TYPICAL_UNEXPECTED_END = "subrip/typical_unexpected_end"; + private static final String TYPICAL_WITH_TAGS = "subrip/typical_with_tags"; private static final String NO_END_TIMECODES_FILE = "subrip/no_end_timecodes"; @Test @@ -154,6 +155,27 @@ public final class SubripDecoderTest { .isEqualTo("Or to the end of the media."); } + @Test + public void testDecodeCueWithTag() throws IOException{ + SubripDecoder decoder = new SubripDecoder(); + byte[] bytes = TestUtil.getByteArray(RuntimeEnvironment.application, TYPICAL_WITH_TAGS); + SubripSubtitle subtitle = decoder.decode(bytes, bytes.length, false); + assertThat(subtitle.getCues(subtitle.getEventTime(0)).get(0).text.toString()) + .isEqualTo("This is the first subtitle."); + assertThat(subtitle.getCues(subtitle.getEventTime(2)).get(0).text.toString()) + .isEqualTo("This is the second subtitle.\nSecond subtitle with second line."); + assertThat(subtitle.getCues(subtitle.getEventTime(4)).get(0).text.toString()) + .isEqualTo("This is the third subtitle."); + + // Based on the SSA v4+ specs the curly bracket must be followed by a backslash, so this is + // not a valid tag (won't be parsed / replaced) + assertThat(subtitle.getCues(subtitle.getEventTime(6)).get(0).text.toString()) + .isEqualTo("This { \\an2} is the fourth subtitle."); + + assertThat(subtitle.getCues(subtitle.getEventTime(8)).get(0).text.toString()) + .isEqualTo("This is the fifth subtitle with multiple valid tags."); + } + private static void assertTypicalCue1(SubripSubtitle subtitle, int eventIndex) { assertThat(subtitle.getEventTime(eventIndex)).isEqualTo(0); assertThat(subtitle.getCues(subtitle.getEventTime(eventIndex)).get(0).text.toString()) From 876080ed1a70762d06ab97fbafda7ac7575487bc Mon Sep 17 00:00:00 2001 From: Arnold Szabo Date: Fri, 21 Sep 2018 15:05:43 +0300 Subject: [PATCH 2/5] #4306 - code review fixes --- .../exoplayer2/text/subrip/SubripDecoder.java | 101 ++++++------------ 1 file changed, 33 insertions(+), 68 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/subrip/SubripDecoder.java b/library/core/src/main/java/com/google/android/exoplayer2/text/subrip/SubripDecoder.java index 96c065973e..492eb60bfc 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/subrip/SubripDecoder.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/subrip/SubripDecoder.java @@ -29,7 +29,6 @@ import com.google.android.exoplayer2.util.ParsableByteArray; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; -import java.util.List; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -81,6 +80,7 @@ public final class SubripDecoder extends SimpleSubtitleDecoder { @Override protected SubripSubtitle decode(byte[] bytes, int length, boolean reset) { ArrayList cues = new ArrayList<>(); + ArrayList tags = new ArrayList<>(); LongArray cueTimesUs = new LongArray(); ParsableByteArray subripData = new ParsableByteArray(bytes, length); String currentLine; @@ -125,34 +125,25 @@ public final class SubripDecoder extends SimpleSubtitleDecoder { if (textBuilder.length() > 0) { textBuilder.append("
"); } - textBuilder.append(currentLine.trim()); + textBuilder.append(processLine(currentLine, tags)); } - // Extract tags - SubtitleTagResult tagResult = extractTags(textBuilder); - Spanned text = Html.fromHtml(tagResult.cue); - + Spanned text = Html.fromHtml(textBuilder.toString()); Cue cue = null; - // Check if tags are present - if (tagResult.tags.length > 0) { + boolean alignTagFound = false; - boolean alignTagFound = false; + // At end of this loop the clue must be created with the applied tags + for (String tag : tags) { - // At end of this loop the clue must be created with the applied tags - for (String tag : tagResult.tags) { + // Check if the tag is an alignment tag + if (tag.matches(SUBRIP_ALIGNMENT_TAG)) { - // Check if the tag is an alignment tag - if (tag.matches(SUBRIP_ALIGNMENT_TAG)) { + // Based on the specs, in case of the alignment tags only the first appearance counts + if (alignTagFound) continue; + alignTagFound = true; - // Based on the specs, in case of the alignment tags only the first appearance counts - if (alignTagFound) continue; - alignTagFound = true; - - AlignmentResult alignmentResult = getAlignmentValues(tag); - cue = new Cue(text, Layout.Alignment.ALIGN_NORMAL, alignmentResult.line, Cue.LINE_TYPE_FRACTION, - alignmentResult.lineAnchor, alignmentResult.position, alignmentResult.positionAnchor, Cue.DIMEN_UNSET); - } + cue = buildCue(text, tag); } } @@ -170,51 +161,57 @@ public final class SubripDecoder extends SimpleSubtitleDecoder { } /** - * Extracts the tags from the given {@code cue} + * Process the given line by first trimming it then extracting the tags from it + *

* The pattern that is used to extract the tags is specified in SSA v4+ specs and * has the following form: "{\...}". *

* "All override codes appear within braces {}" * "All override codes are always preceded by a backslash \" * - * @param cue Cue text - * @return {@link SubtitleTagResult} that holds new cue and also the extracted tags + * @param currentLine Current line + * @param tags Extracted tags will be stored in this array list + * @return Processed line */ - private SubtitleTagResult extractTags(StringBuilder cue) { - StringBuilder cueCopy = new StringBuilder(cue.toString()); - List tags = new ArrayList<>(); + private String processLine(String currentLine, ArrayList tags) { + // Trim line + String trimmedLine = currentLine.trim(); + // Extract tags int replacedCharacters = 0; + StringBuilder processedLine = new StringBuilder(trimmedLine); + Matcher matcher = SUBRIP_TAG_PATTERN.matcher(processedLine); - Matcher matcher = SUBRIP_TAG_PATTERN.matcher(cue.toString()); while (matcher.find()) { String tag = matcher.group(); tags.add(tag); - cueCopy.replace(matcher.start() - replacedCharacters, matcher.end() - replacedCharacters, ""); + processedLine.replace(matcher.start() - replacedCharacters, matcher.end() - replacedCharacters, ""); replacedCharacters += tag.length(); } - return new SubtitleTagResult(tags.toArray(new String[tags.size()]), cueCopy.toString()); + return processedLine.toString(); } /** + * Build a {@link Cue} based on the given text and tag + *

* Match the alignment tag and calculate the line, position, position anchor accordingly - * + *

* 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 * - * @param tag Alignment tag - * @return {@link AlignmentResult} that holds the line, position, position anchor values + * @param alignmentTag Alignment tag + * @return Built cue */ - private AlignmentResult getAlignmentValues(String tag) { + private Cue buildCue(Spanned text, String alignmentTag) { // Default values used for positioning the subtitle in case of align tags float line = DEFAULT_END_FRACTION, position = DEFAULT_MID_FRACTION; @Cue.AnchorType int positionAnchor = Cue.ANCHOR_TYPE_MIDDLE; @Cue.AnchorType int lineAnchor = Cue.ANCHOR_TYPE_END; - switch (tag) { + switch (alignmentTag) { case ALIGN_BOTTOM_LEFT: line = DEFAULT_END_FRACTION; position = DEFAULT_START_FRACTION; @@ -271,7 +268,7 @@ public final class SubripDecoder extends SimpleSubtitleDecoder { break; } - return new AlignmentResult(positionAnchor, position, lineAnchor, line); + return new Cue(text, Layout.Alignment.ALIGN_NORMAL, line, Cue.LINE_TYPE_FRACTION, lineAnchor, position, positionAnchor, Cue.DIMEN_UNSET); } private static long parseTimecode(Matcher matcher, int groupOffset) { @@ -281,36 +278,4 @@ public final class SubripDecoder extends SimpleSubtitleDecoder { timestampMs += Long.parseLong(matcher.group(groupOffset + 4)); return timestampMs * 1000; } - - /** - * Class that holds the tags, new clue after the tag extraction - */ - private static final class SubtitleTagResult { - public final String[] tags; - public final String cue; - - public SubtitleTagResult(String[] tags, String cue) { - this.tags = tags; - this.cue = cue; - } - } - - /** - * Class that holds the parsed and mapped alignment values (such as line, - * position and anchor type of line) - */ - private static final class AlignmentResult { - - public @Cue.AnchorType int positionAnchor; - public @Cue.AnchorType int lineAnchor; - public float position, line; - - public AlignmentResult(@Cue.AnchorType int positionAnchor, float position, @Cue.AnchorType int lineAnchor, float line) { - this.positionAnchor = positionAnchor; - this.position = position; - this.line = line; - this.lineAnchor = lineAnchor; - } - } - } From fc5eb12e7955e1272f52a90ec25a1a385429a47c Mon Sep 17 00:00:00 2001 From: Arnold Szabo Date: Mon, 1 Oct 2018 22:45:15 +0300 Subject: [PATCH 3/5] #4306 - breaking after the first alignment tag is found --- .../exoplayer2/text/subrip/SubripDecoder.java | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/subrip/SubripDecoder.java b/library/core/src/main/java/com/google/android/exoplayer2/text/subrip/SubripDecoder.java index 492eb60bfc..e50184e403 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/subrip/SubripDecoder.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/subrip/SubripDecoder.java @@ -80,7 +80,6 @@ public final class SubripDecoder extends SimpleSubtitleDecoder { @Override protected SubripSubtitle decode(byte[] bytes, int length, boolean reset) { ArrayList cues = new ArrayList<>(); - ArrayList tags = new ArrayList<>(); LongArray cueTimesUs = new LongArray(); ParsableByteArray subripData = new ParsableByteArray(bytes, length); String currentLine; @@ -120,6 +119,7 @@ public final class SubripDecoder extends SimpleSubtitleDecoder { } // Read and parse the text. + ArrayList tags = new ArrayList<>(); textBuilder.setLength(0); while (!TextUtils.isEmpty(currentLine = subripData.readLine())) { if (textBuilder.length() > 0) { @@ -131,19 +131,15 @@ public final class SubripDecoder extends SimpleSubtitleDecoder { Spanned text = Html.fromHtml(textBuilder.toString()); Cue cue = null; - boolean alignTagFound = false; - // 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 if (tag.matches(SUBRIP_ALIGNMENT_TAG)) { - - // Based on the specs, in case of the alignment tags only the first appearance counts - if (alignTagFound) continue; - alignTagFound = true; - cue = buildCue(text, tag); + + // Based on the specs, in case of alignment tags only the first appearance counts, so break + break; } } From 75a7385bbb4a2e6940f6a97ba8d5814e2dcd6049 Mon Sep 17 00:00:00 2001 From: Arnold Szabo Date: Mon, 1 Oct 2018 23:16:48 +0300 Subject: [PATCH 4/5] #4306 - extends test case with line and position anchor verifications --- .../exoplayer2/text/subrip/SubripDecoder.java | 2 +- .../src/test/assets/subrip/typical_with_tags | 36 ++++++++++ .../text/subrip/SubripDecoderTest.java | 67 +++++++++++++++++++ 3 files changed, 104 insertions(+), 1 deletion(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/subrip/SubripDecoder.java b/library/core/src/main/java/com/google/android/exoplayer2/text/subrip/SubripDecoder.java index e50184e403..63887906c6 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/subrip/SubripDecoder.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/subrip/SubripDecoder.java @@ -176,7 +176,7 @@ public final class SubripDecoder extends SimpleSubtitleDecoder { // Extract tags int replacedCharacters = 0; StringBuilder processedLine = new StringBuilder(trimmedLine); - Matcher matcher = SUBRIP_TAG_PATTERN.matcher(processedLine); + Matcher matcher = SUBRIP_TAG_PATTERN.matcher(trimmedLine); while (matcher.find()) { String tag = matcher.group(); diff --git a/library/core/src/test/assets/subrip/typical_with_tags b/library/core/src/test/assets/subrip/typical_with_tags index 02e1ffbcd9..af196f8a04 100644 --- a/library/core/src/test/assets/subrip/typical_with_tags +++ b/library/core/src/test/assets/subrip/typical_with_tags @@ -18,3 +18,39 @@ This { \an2} is the fourth subtitle. 5 00:00:013,567 --> 00:00:14,901 This {\an2} is the fifth subtitle with multiple {\xyz} valid {\qwe} tags. + +6 +00:00:015,567 --> 00:00:15,901 +This {\an1} is a lines. + +7 +00:00:016,567 --> 00:00:16,901 +This {\an2} is a line. + +8 +00:00:017,567 --> 00:00:17,901 +This {\an3} is a line. + +9 +00:00:018,567 --> 00:00:18,901 +This {\an4} is a line. + +10 +00:00:019,567 --> 00:00:19,901 +This {\an5} is a line. + +11 +00:00:020,567 --> 00:00:20,901 +This {\an6} is a line. + +12 +00:00:021,567 --> 00:00:22,901 +This {\an7} is a line. + +13 +00:00:023,567 --> 00:00:23,901 +This {\an8} is a line. + +14 +00:00:024,567 --> 00:00:24,901 +This {\an9} is a line. \ No newline at end of file diff --git a/library/core/src/test/java/com/google/android/exoplayer2/text/subrip/SubripDecoderTest.java b/library/core/src/test/java/com/google/android/exoplayer2/text/subrip/SubripDecoderTest.java index a9d69076c2..554184da5d 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/text/subrip/SubripDecoderTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/text/subrip/SubripDecoderTest.java @@ -18,6 +18,8 @@ package com.google.android.exoplayer2.text.subrip; import static com.google.common.truth.Truth.assertThat; import com.google.android.exoplayer2.testutil.TestUtil; +import com.google.android.exoplayer2.text.Cue; + import java.io.IOException; import org.junit.Test; import org.junit.runner.RunWith; @@ -174,6 +176,71 @@ public final class SubripDecoderTest { assertThat(subtitle.getCues(subtitle.getEventTime(8)).get(0).text.toString()) .isEqualTo("This is the fifth subtitle with multiple valid tags."); + + // Verify positions + + // {/an1} + assertThat(subtitle.getCues(subtitle.getEventTime(10)).get(0).positionAnchor) + .isEqualTo(Cue.ANCHOR_TYPE_START); + + assertThat(subtitle.getCues(subtitle.getEventTime(10)).get(0).lineAnchor) + .isEqualTo(Cue.ANCHOR_TYPE_END); + + // {/an2} + assertThat(subtitle.getCues(subtitle.getEventTime(12)).get(0).positionAnchor) + .isEqualTo(Cue.ANCHOR_TYPE_MIDDLE); + + assertThat(subtitle.getCues(subtitle.getEventTime(12)).get(0).lineAnchor) + .isEqualTo(Cue.ANCHOR_TYPE_END); + + // {/an3} + assertThat(subtitle.getCues(subtitle.getEventTime(14)).get(0).positionAnchor) + .isEqualTo(Cue.ANCHOR_TYPE_END); + + assertThat(subtitle.getCues(subtitle.getEventTime(14)).get(0).lineAnchor) + .isEqualTo(Cue.ANCHOR_TYPE_END); + + // {/an4} + assertThat(subtitle.getCues(subtitle.getEventTime(16)).get(0).positionAnchor) + .isEqualTo(Cue.ANCHOR_TYPE_START); + + assertThat(subtitle.getCues(subtitle.getEventTime(16)).get(0).lineAnchor) + .isEqualTo(Cue.ANCHOR_TYPE_MIDDLE); + + // {/an5} + assertThat(subtitle.getCues(subtitle.getEventTime(18)).get(0).positionAnchor) + .isEqualTo(Cue.ANCHOR_TYPE_MIDDLE); + + assertThat(subtitle.getCues(subtitle.getEventTime(18)).get(0).lineAnchor) + .isEqualTo(Cue.ANCHOR_TYPE_MIDDLE); + + // {/an6} + assertThat(subtitle.getCues(subtitle.getEventTime(20)).get(0).positionAnchor) + .isEqualTo(Cue.ANCHOR_TYPE_END); + + assertThat(subtitle.getCues(subtitle.getEventTime(20)).get(0).lineAnchor) + .isEqualTo(Cue.ANCHOR_TYPE_MIDDLE); + + // {/an7} + assertThat(subtitle.getCues(subtitle.getEventTime(22)).get(0).positionAnchor) + .isEqualTo(Cue.ANCHOR_TYPE_START); + + assertThat(subtitle.getCues(subtitle.getEventTime(22)).get(0).lineAnchor) + .isEqualTo(Cue.ANCHOR_TYPE_START); + + // {/an8} + assertThat(subtitle.getCues(subtitle.getEventTime(24)).get(0).positionAnchor) + .isEqualTo(Cue.ANCHOR_TYPE_MIDDLE); + + assertThat(subtitle.getCues(subtitle.getEventTime(24)).get(0).lineAnchor) + .isEqualTo(Cue.ANCHOR_TYPE_START); + + // {/an9} + assertThat(subtitle.getCues(subtitle.getEventTime(26)).get(0).positionAnchor) + .isEqualTo(Cue.ANCHOR_TYPE_END); + + assertThat(subtitle.getCues(subtitle.getEventTime(26)).get(0).lineAnchor) + .isEqualTo(Cue.ANCHOR_TYPE_START); } private static void assertTypicalCue1(SubripSubtitle subtitle, int eventIndex) { From 56c7e1ff475ea6b9a7fb06ad9dd14303b1104e26 Mon Sep 17 00:00:00 2001 From: Arnold Szabo Date: Tue, 2 Oct 2018 18:45:06 +0300 Subject: [PATCH 5/5] #4306 - grouping line/lineAnchor and position/positionAnchor assignments, setting Cue's textAlignment to def value - null --- .../exoplayer2/text/subrip/SubripDecoder.java | 80 +++++++++---------- 1 file changed, 37 insertions(+), 43 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/subrip/SubripDecoder.java b/library/core/src/main/java/com/google/android/exoplayer2/text/subrip/SubripDecoder.java index 63887906c6..cf2d3c11bc 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/subrip/SubripDecoder.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/subrip/SubripDecoder.java @@ -202,69 +202,63 @@ public final class SubripDecoder extends SimpleSubtitleDecoder { * @return Built cue */ private Cue buildCue(Spanned text, String alignmentTag) { - // Default values used for positioning the subtitle in case of align tags - float line = DEFAULT_END_FRACTION, position = DEFAULT_MID_FRACTION; - @Cue.AnchorType int positionAnchor = Cue.ANCHOR_TYPE_MIDDLE; - @Cue.AnchorType int lineAnchor = Cue.ANCHOR_TYPE_END; + float line, position; + @Cue.AnchorType int positionAnchor; + @Cue.AnchorType int lineAnchor; + // Set position and position anchor (horizontal alignment) switch (alignmentTag) { case ALIGN_BOTTOM_LEFT: - line = DEFAULT_END_FRACTION; + case ALIGN_MID_LEFT: + case ALIGN_TOP_LEFT: position = DEFAULT_START_FRACTION; positionAnchor = Cue.ANCHOR_TYPE_START; - lineAnchor = Cue.ANCHOR_TYPE_END; break; case ALIGN_BOTTOM_MID: - line = DEFAULT_END_FRACTION; + case ALIGN_MID_MID: + case ALIGN_TOP_MID: position = DEFAULT_MID_FRACTION; positionAnchor = Cue.ANCHOR_TYPE_MIDDLE; - lineAnchor = Cue.ANCHOR_TYPE_END; break; case ALIGN_BOTTOM_RIGHT: - line = DEFAULT_END_FRACTION; - position = DEFAULT_END_FRACTION; - positionAnchor = Cue.ANCHOR_TYPE_END; - lineAnchor = Cue.ANCHOR_TYPE_END; - break; - case ALIGN_MID_LEFT: - line = DEFAULT_MID_FRACTION; - position = DEFAULT_START_FRACTION; - positionAnchor = Cue.ANCHOR_TYPE_START; - lineAnchor = Cue.ANCHOR_TYPE_MIDDLE; - break; - case ALIGN_MID_MID: - line = DEFAULT_MID_FRACTION; - position = DEFAULT_MID_FRACTION; - positionAnchor = Cue.ANCHOR_TYPE_MIDDLE; - lineAnchor = Cue.ANCHOR_TYPE_MIDDLE; - break; case ALIGN_MID_RIGHT: - line = DEFAULT_MID_FRACTION; + case ALIGN_TOP_RIGHT: position = DEFAULT_END_FRACTION; positionAnchor = Cue.ANCHOR_TYPE_END; - lineAnchor = Cue.ANCHOR_TYPE_MIDDLE; break; - case ALIGN_TOP_LEFT: - line = DEFAULT_START_FRACTION; - position = DEFAULT_START_FRACTION; - positionAnchor = Cue.ANCHOR_TYPE_START; - lineAnchor = Cue.ANCHOR_TYPE_START; - break; - case ALIGN_TOP_MID: - line = DEFAULT_START_FRACTION; + default: position = DEFAULT_MID_FRACTION; positionAnchor = Cue.ANCHOR_TYPE_MIDDLE; - lineAnchor = Cue.ANCHOR_TYPE_START; - break; - case ALIGN_TOP_RIGHT: - line = DEFAULT_START_FRACTION; - position = DEFAULT_END_FRACTION; - positionAnchor = Cue.ANCHOR_TYPE_END; - lineAnchor = Cue.ANCHOR_TYPE_START; break; } - return new Cue(text, Layout.Alignment.ALIGN_NORMAL, line, Cue.LINE_TYPE_FRACTION, lineAnchor, position, positionAnchor, Cue.DIMEN_UNSET); + // Set line and line anchor (vertical alignment) + 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; + default: + line = DEFAULT_END_FRACTION; + lineAnchor = Cue.ANCHOR_TYPE_END; + break; + } + + return new Cue(text, null, line, Cue.LINE_TYPE_FRACTION, lineAnchor, position, positionAnchor, Cue.DIMEN_UNSET); } private static long parseTimecode(Matcher matcher, int groupOffset) {