diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/span/TextEmphasisSpan.java b/library/core/src/main/java/com/google/android/exoplayer2/text/span/TextEmphasisSpan.java new file mode 100644 index 0000000000..6d87ad521c --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/span/TextEmphasisSpan.java @@ -0,0 +1,134 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package com.google.android.exoplayer2.text.span; + +import static java.lang.annotation.RetentionPolicy.SOURCE; + +import androidx.annotation.IntDef; +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; + +public final class TextEmphasisSpan { + /** + * Text Emphasis Position + */ + + /** + * The text emphasis position is unknown. If an implementation does not recognize or otherwise + * distinguish an annotation position value, then it must be interpreted as if a position of + * before were specified; as such, an implementation that supports text annotation marks must + * minimally support the before value. + */ + public static final int POSITION_UNKNOWN = -1; + + /** + * The emphasis marks should be positioned above the base text in horizontal writing mode The + * emphasis marks should be positioned to the right of the base text in vertical writing mode + */ + public static final int POSITION_BEFORE = 1; + + /** + * The emphasis marks should be positioned below the base text in horizontal writing mode The + * emphasis marks should be positioned to the left of the base text in vertical writing mode + */ + public static final int POSITION_AFTER = 2; + + /** + * The text emphasis should be positioned in following way: + * + *
One of: + * + *
One of: + * + *
#FF00FF00 not #00FF00
* .
*
- * @see
- * JellyBean Color
- * Kitkat Color
* @throws IOException thrown if reading subtitle file fails.
+ * @see
+ * JellyBean Color
+ * Kitkat Color
*/
@Test
public void lime() throws IOException, SubtitleDecoderException {
@@ -674,6 +676,105 @@ public final class TtmlDecoderTest {
assertThat(sixthCue).hasNoRubySpanBetween(0, sixthCue.length());
}
+ @Test
+ public void textEmphasis() throws IOException, SubtitleDecoderException {
+ TtmlSubtitle subtitle = getSubtitle(TEXT_EMPHASIS_FILE);
+
+ Spanned firstCue = getOnlyCueTextAtTimeUs(subtitle, 10_000_000);
+ assertThat(firstCue)
+ .hasNoTextEmphasisSpanBetween("None ".length(), "None おはよ".length());
+
+ Spanned secondCue = getOnlyCueTextAtTimeUs(subtitle, 20_000_000);
+ assertThat(secondCue)
+ .hasTextEmphasisSpanBetween("Auto ".length(), "Auto ございます".length())
+ .withMarkAndPosition(TextEmphasisSpan.MARK_FILLED_CIRCLE,
+ TextEmphasisSpan.POSITION_UNKNOWN);
+
+ Spanned thirdCue = getOnlyCueTextAtTimeUs(subtitle, 30_000_000);
+ assertThat(thirdCue)
+ .hasTextEmphasisSpanBetween("Filled circle ".length(), "Filled circle こんばんは".length())
+ .withMarkAndPosition(TextEmphasisSpan.MARK_FILLED_CIRCLE,
+ TextEmphasisSpan.POSITION_UNKNOWN);
+
+ Spanned fourthCue = getOnlyCueTextAtTimeUs(subtitle, 40_000_000);
+ assertThat(fourthCue)
+ .hasTextEmphasisSpanBetween("Filled dot ".length(), "Filled dot ございます".length())
+ .withMarkAndPosition(TextEmphasisSpan.MARK_FILLED_DOT, TextEmphasisSpan.POSITION_UNKNOWN);
+
+ Spanned fifthCue = getOnlyCueTextAtTimeUs(subtitle, 50_000_000);
+ assertThat(fifthCue)
+ .hasTextEmphasisSpanBetween("Filled sesame ".length(), "Filled sesame おはよ".length())
+ .withMarkAndPosition(TextEmphasisSpan.MARK_FILLED_SESAME,
+ TextEmphasisSpan.POSITION_UNKNOWN);
+
+ Spanned sixthCue = getOnlyCueTextAtTimeUs(subtitle, 60_000_000);
+ assertThat(sixthCue)
+ .hasTextEmphasisSpanBetween("Open circle before ".length(),
+ "Open circle before ございます".length())
+ .withMarkAndPosition(TextEmphasisSpan.MARK_OPEN_CIRCLE, TextEmphasisSpan.POSITION_BEFORE);
+
+ Spanned seventhCue = getOnlyCueTextAtTimeUs(subtitle, 70_000_000);
+ assertThat(seventhCue)
+ .hasTextEmphasisSpanBetween("Open dot after ".length(), "Open dot after おはよ".length())
+ .withMarkAndPosition(TextEmphasisSpan.MARK_OPEN_DOT, TextEmphasisSpan.POSITION_AFTER);
+
+ Spanned eighthCue = getOnlyCueTextAtTimeUs(subtitle, 80_000_000);
+ assertThat(eighthCue)
+ .hasTextEmphasisSpanBetween("Open sesame outside ".length(),
+ "Open sesame outside ございます".length())
+ .withMarkAndPosition(TextEmphasisSpan.MARK_OPEN_SESAME, TextEmphasisSpan.POSITION_OUTSIDE);
+
+ Spanned ninthCue = getOnlyCueTextAtTimeUs(subtitle, 90_000_000);
+ assertThat(ninthCue)
+ .hasTextEmphasisSpanBetween("Auto outside ".length(), "Auto outside おはよ".length())
+ .withMarkAndPosition(TextEmphasisSpan.MARK_FILLED_CIRCLE,
+ TextEmphasisSpan.POSITION_OUTSIDE);
+
+ Spanned tenthCue = getOnlyCueTextAtTimeUs(subtitle, 100_000_000);
+ assertThat(tenthCue)
+ .hasTextEmphasisSpanBetween("Circle before ".length(), "Circle before ございます".length())
+ .withMarkAndPosition(TextEmphasisSpan.MARK_FILLED_CIRCLE, TextEmphasisSpan.POSITION_BEFORE);
+
+ Spanned eleventhCue = getOnlyCueTextAtTimeUs(subtitle, 110_000_000);
+ assertThat(eleventhCue)
+ .hasTextEmphasisSpanBetween("Sesame after ".length(), "Sesame after おはよ".length())
+ .withMarkAndPosition(TextEmphasisSpan.MARK_FILLED_SESAME, TextEmphasisSpan.POSITION_AFTER);
+
+ Spanned twelfthCue = getOnlyCueTextAtTimeUs(subtitle, 120_000_000);
+ assertThat(twelfthCue)
+ .hasTextEmphasisSpanBetween("Dot outside ".length(), "Dot outside ございます".length())
+ .withMarkAndPosition(TextEmphasisSpan.MARK_FILLED_DOT, TextEmphasisSpan.POSITION_OUTSIDE);
+
+ Spanned thirteenthCue = getOnlyCueTextAtTimeUs(subtitle, 130_000_000);
+ assertThat(thirteenthCue)
+ .hasNoTextEmphasisSpanBetween("No textEmphasis property ".length(),
+ "No textEmphasis property おはよ".length());
+
+ Spanned fourteenthCue = getOnlyCueTextAtTimeUs(subtitle, 140_000_000);
+ assertThat(fourteenthCue)
+ .hasTextEmphasisSpanBetween("Auto (TBLR) ".length(), "Auto (TBLR) ございます".length())
+ .withMarkAndPosition(TextEmphasisSpan.MARK_FILLED_SESAME,
+ TextEmphasisSpan.POSITION_UNKNOWN);
+
+ Spanned fifteenthCue = getOnlyCueTextAtTimeUs(subtitle, 150_000_000);
+ assertThat(fifteenthCue)
+ .hasTextEmphasisSpanBetween("Auto (TBRL) ".length(), "Auto (TBRL) おはよ".length())
+ .withMarkAndPosition(TextEmphasisSpan.MARK_FILLED_SESAME,
+ TextEmphasisSpan.POSITION_UNKNOWN);
+
+ Spanned sixteenthCue = getOnlyCueTextAtTimeUs(subtitle, 160_000_000);
+ assertThat(sixteenthCue)
+ .hasTextEmphasisSpanBetween("Auto (TB) ".length(), "Auto (TB) ございます".length())
+ .withMarkAndPosition(TextEmphasisSpan.MARK_FILLED_SESAME,
+ TextEmphasisSpan.POSITION_UNKNOWN);
+
+ Spanned seventeenthCue = getOnlyCueTextAtTimeUs(subtitle, 170_000_000);
+ assertThat(seventeenthCue)
+ .hasTextEmphasisSpanBetween("Auto (LR) ".length(), "Auto (LR) おはよ".length())
+ .withMarkAndPosition(TextEmphasisSpan.MARK_FILLED_CIRCLE,
+ TextEmphasisSpan.POSITION_UNKNOWN);
+ }
+
private static Spanned getOnlyCueTextAtTimeUs(Subtitle subtitle, long timeUs) {
Cue cue = getOnlyCueAtTimeUs(subtitle, timeUs);
assertThat(cue.text).isInstanceOf(Spanned.class);
diff --git a/library/core/src/test/java/com/google/android/exoplayer2/text/ttml/TtmlStyleTest.java b/library/core/src/test/java/com/google/android/exoplayer2/text/ttml/TtmlStyleTest.java
index a3ad1ba599..60409315ef 100644
--- a/library/core/src/test/java/com/google/android/exoplayer2/text/ttml/TtmlStyleTest.java
+++ b/library/core/src/test/java/com/google/android/exoplayer2/text/ttml/TtmlStyleTest.java
@@ -16,6 +16,10 @@
package com.google.android.exoplayer2.text.ttml;
import static android.graphics.Color.BLACK;
+import static com.google.android.exoplayer2.text.span.TextEmphasisSpan.MARK_FILLED_DOT;
+import static com.google.android.exoplayer2.text.span.TextEmphasisSpan.MARK_OPEN_SESAME;
+import static com.google.android.exoplayer2.text.span.TextEmphasisSpan.POSITION_AFTER;
+import static com.google.android.exoplayer2.text.span.TextEmphasisSpan.POSITION_BEFORE;
import static com.google.android.exoplayer2.text.ttml.TtmlStyle.STYLE_BOLD;
import static com.google.android.exoplayer2.text.ttml.TtmlStyle.STYLE_BOLD_ITALIC;
import static com.google.android.exoplayer2.text.ttml.TtmlStyle.STYLE_ITALIC;
@@ -46,6 +50,7 @@ public final class TtmlStyleTest {
private static final int RUBY_POSITION = RubySpan.POSITION_UNDER;
private static final Layout.Alignment TEXT_ALIGN = Layout.Alignment.ALIGN_CENTER;
private static final boolean TEXT_COMBINE = true;
+ public static final String TEXT_EMPHASIS_STYLE="dot before";
private final TtmlStyle populatedStyle =
new TtmlStyle()
@@ -62,7 +67,8 @@ public final class TtmlStyleTest {
.setRubyType(RUBY_TYPE)
.setRubyPosition(RUBY_POSITION)
.setTextAlign(TEXT_ALIGN)
- .setTextCombine(TEXT_COMBINE);
+ .setTextCombine(TEXT_COMBINE)
+ .setTextEmphasis(TextEmphasis.createTextEmphasis(TEXT_EMPHASIS_STYLE));
@Test
public void inheritStyle() {
@@ -86,6 +92,9 @@ public final class TtmlStyleTest {
assertWithMessage("backgroundColor should not be inherited")
.that(style.hasBackgroundColor())
.isFalse();
+ assertThat(style.getTextEmphasis()).isNotNull();
+ assertThat(style.getTextEmphasis().mark).isEqualTo(MARK_FILLED_DOT);
+ assertThat(style.getTextEmphasis().position).isEqualTo(POSITION_BEFORE);
}
@Test
@@ -109,6 +118,9 @@ public final class TtmlStyleTest {
.that(style.getBackgroundColor())
.isEqualTo(BACKGROUND_COLOR);
assertWithMessage("rubyType should be chained").that(style.getRubyType()).isEqualTo(RUBY_TYPE);
+ assertThat(style.getTextEmphasis()).isNotNull();
+ assertThat(style.getTextEmphasis().mark).isEqualTo(MARK_FILLED_DOT);
+ assertThat(style.getTextEmphasis().position).isEqualTo(POSITION_BEFORE);
}
@Test
@@ -245,4 +257,13 @@ public final class TtmlStyleTest {
style.setTextCombine(true);
assertThat(style.getTextCombine()).isTrue();
}
+
+ @Test
+ public void textEmphasis() {
+ TtmlStyle style = new TtmlStyle();
+ assertThat(style.getTextEmphasis()).isNull();
+ style.setTextEmphasis(TextEmphasis.createTextEmphasis("open sesame after"));
+ assertThat(style.getTextEmphasis().mark).isEqualTo(MARK_OPEN_SESAME);
+ assertThat(style.getTextEmphasis().position).isEqualTo(POSITION_AFTER);
+ }
}
diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/SpannedToHtmlConverter.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/SpannedToHtmlConverter.java
index 7ea2b55cf4..95c6562599 100644
--- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/SpannedToHtmlConverter.java
+++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/SpannedToHtmlConverter.java
@@ -31,6 +31,7 @@ import android.util.SparseArray;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.text.span.HorizontalTextInVerticalContextSpan;
import com.google.android.exoplayer2.text.span.RubySpan;
+import com.google.android.exoplayer2.text.span.TextEmphasisSpan;
import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.Util;
import com.google.common.collect.ImmutableMap;
@@ -197,6 +198,14 @@ import java.util.regex.Pattern;
}
} else if (span instanceof UnderlineSpan) {
return "";
+ } else if (span instanceof TextEmphasisSpan) {
+ TextEmphasisSpan textEmphasisSpan = (TextEmphasisSpan) span;
+ String style = getTextEmphasisStyle(textEmphasisSpan.mark);
+ String position = getTextEmphasisPosition(textEmphasisSpan.position);
+ return Util
+ .formatInvariant("",
+ style, style, position, position);
} else {
return null;
}
@@ -209,7 +218,8 @@ import java.util.regex.Pattern;
|| span instanceof BackgroundColorSpan
|| span instanceof HorizontalTextInVerticalContextSpan
|| span instanceof AbsoluteSizeSpan
- || span instanceof RelativeSizeSpan) {
+ || span instanceof RelativeSizeSpan
+ || span instanceof TextEmphasisSpan) {
return "";
} else if (span instanceof TypefaceSpan) {
@Nullable String fontFamily = ((TypefaceSpan) span).getFamily();
@@ -232,6 +242,47 @@ import java.util.regex.Pattern;
return null;
}
+ private static String getTextEmphasisStyle(@TextEmphasisSpan.Mark int mark) {
+ switch (mark) {
+ case TextEmphasisSpan.MARK_FILLED_CIRCLE:
+ return "filled circle";
+ case TextEmphasisSpan.MARK_FILLED_DOT:
+ return "filled dot";
+ case TextEmphasisSpan.MARK_FILLED_SESAME:
+ return "filled sesame";
+ case TextEmphasisSpan.MARK_OPEN_CIRCLE:
+ return "open circle";
+ case TextEmphasisSpan.MARK_OPEN_DOT:
+ return "open dot";
+ case TextEmphasisSpan.MARK_OPEN_SESAME:
+ return "open sesame";
+ case TextEmphasisSpan.MARK_AUTO: // TODO
+ // https://www.w3.org/TR/ttml2/#style-value-emphasis-style
+ // If a vertical writing mode applies, then equivalent to filled sesame; otherwise,
+ // equivalent to filled circle.
+ case TextEmphasisSpan.MARK_UNKNOWN:
+ default:
+ return "unset";
+ }
+ }
+
+ private static String getTextEmphasisPosition(@TextEmphasisSpan.Position int position){
+ switch (position) {
+ case TextEmphasisSpan.POSITION_AFTER:
+ return "under left";
+ case TextEmphasisSpan.POSITION_UNKNOWN:
+ case TextEmphasisSpan.POSITION_BEFORE:
+ case TextEmphasisSpan.POSITION_OUTSIDE: /* Not supported, fallback to "before" */
+ default:
+ // https://www.w3.org/TR/ttml2/#style-value-annotation-position
+ // If an implementation does not recognize or otherwise distinguish an annotation position
+ // value, then it must be interpreted as if a position of before were specified; as such,
+ // an implementation that supports text annotation marks must minimally support the before
+ // value.
+ return "over right";
+ }
+ }
+
private static Transition getOrCreate(SparseArrayNone おはよ
+Auto ございます
+Filled circle こんばんは
+Filled dot ございます
+Filled sesame おはよ
+Open circle before ございます
+Open dot after おはよ
+Open sesame outside ございます
+Auto outside おはよ
+Circle before ございます
+Sesame after おはよ
+Dot outside ございます
+No textEmphasis property おはよ
+Auto (TBLR) ございます
+Auto (TBRL) おはよ
+Auto (TB) ございます
+Auto (LR) おはよ
+This fails even if the start and end indexes don't exactly match.
+ *
+ * @param start The start index to start searching for spans.
+ * @param end The end index to stop searching for spans.
+ */
+ public void hasNoTextEmphasisSpanBetween(int start, int end) {
+ hasNoSpansOfTypeBetween(TextEmphasisSpan.class, start, end);
+ }
+
+
/**
* Checks that the subject has no {@link HorizontalTextInVerticalContextSpan}s on any of the text
* between {@code start} and {@code end}.
@@ -1110,4 +1152,95 @@ public final class SpannedSubject extends Subject {
}
}
}
+
+ /** Allows assertions about a span's textEmphasis mark and its position. */
+ public interface TextEmphasisDescription {
+
+ /**
+ * Checks that at least one of the matched spans has the expected {@code mark} and {@code position}.
+ *
+ * @param mark The expected mark
+ * @param position The expected position of the mark
+ * @return A {@link WithSpanFlags} object for optional additional assertions on the flags.
+ */
+ AndSpanFlags withMarkAndPosition(@TextEmphasisSpan.Mark int mark,
+ @TextEmphasisSpan.Position int position);
+ }
+
+ private static final TextEmphasisDescription ALREADY_FAILED_WITH_MARK =
+ (mark, position) -> ALREADY_FAILED_AND_FLAGS;
+
+ private static Factory