From fcda8d47ff5606b8a36b3ec44d12946979fc0612 Mon Sep 17 00:00:00 2001 From: Denise LaFayette Date: Thu, 4 Feb 2021 11:56:30 -0800 Subject: [PATCH 1/6] Support tts:textEmphasis in TTML parser and WebView output --- .../text/span/TextEmphasisSpan.java | 134 +++++++ .../exoplayer2/text/ttml/TextEmphasis.java | 201 ++++++++++ .../exoplayer2/text/ttml/TtmlDecoder.java | 4 + .../exoplayer2/text/ttml/TtmlNode.java | 33 +- .../exoplayer2/text/ttml/TtmlRenderUtil.java | 26 +- .../exoplayer2/text/ttml/TtmlStyle.java | 15 + .../text/ttml/TextEmphasisTest.java | 369 ++++++++++++++++++ .../exoplayer2/text/ttml/TtmlDecoderTest.java | 113 +++++- .../exoplayer2/text/ttml/TtmlStyleTest.java | 23 +- .../exoplayer2/ui/SpannedToHtmlConverter.java | 53 ++- .../ui/SpannedToHtmlConverterTest.java | 32 ++ .../test/assets/media/ttml/text_emphasis.xml | 65 +++ .../testutil/truth/SpannedSubject.java | 133 +++++++ .../testutil/truth/SpannedSubjectTest.java | 100 +++++ 14 files changed, 1286 insertions(+), 15 deletions(-) create mode 100644 library/core/src/main/java/com/google/android/exoplayer2/text/span/TextEmphasisSpan.java create mode 100644 library/core/src/main/java/com/google/android/exoplayer2/text/ttml/TextEmphasis.java create mode 100644 library/core/src/test/java/com/google/android/exoplayer2/text/ttml/TextEmphasisTest.java create mode 100644 testdata/src/test/assets/media/ttml/text_emphasis.xml 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: + * + * + */ + public static final int POSITION_OUTSIDE = 3; + + /** + * The possible positions of the emphasis marks relative to the base text. + * + *

One of: + * + *

+ */ + @Documented + @Retention(SOURCE) + @IntDef({POSITION_UNKNOWN, POSITION_BEFORE, POSITION_AFTER, POSITION_OUTSIDE}) + public @interface Position { + + } + + /** + * The text emphasis position is unknown. + */ + public static final int MARK_UNKNOWN = -1; + + public static final int MARK_AUTO = 1; + public static final int MARK_FILLED_CIRCLE = 2; + public static final int MARK_FILLED_DOT = 3; + public static final int MARK_FILLED_SESAME = 4; + public static final int MARK_OPEN_CIRCLE = 5; + public static final int MARK_OPEN_DOT = 6; + public static final int MARK_OPEN_SESAME = 7; + + /** + * The possible types of annotations used. + * + *

One of: + * + *

+ */ + @Documented + @Retention(SOURCE) + @IntDef({MARK_UNKNOWN, MARK_AUTO, MARK_FILLED_CIRCLE, MARK_FILLED_DOT, MARK_FILLED_SESAME, + MARK_OPEN_CIRCLE, MARK_OPEN_DOT, MARK_OPEN_SESAME}) + public @interface Mark { + + } + + /** + * The position of the text emphasis relative to the base text + */ + @TextEmphasisSpan.Position + public final int position; + + /** + * The text emphasis mark + */ + @TextEmphasisSpan.Mark + public final int mark; + + public TextEmphasisSpan(@TextEmphasisSpan.Mark int mark, + @TextEmphasisSpan.Position int position) { + this.mark = mark; + this.position = position; + } +} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/ttml/TextEmphasis.java b/library/core/src/main/java/com/google/android/exoplayer2/text/ttml/TextEmphasis.java new file mode 100644 index 0000000000..6d1be2e4cb --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/ttml/TextEmphasis.java @@ -0,0 +1,201 @@ +/* + * 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.ttml; + +import static com.google.android.exoplayer2.text.span.TextEmphasisSpan.MARK_AUTO; +import static com.google.android.exoplayer2.text.span.TextEmphasisSpan.MARK_FILLED_CIRCLE; +import static com.google.android.exoplayer2.text.span.TextEmphasisSpan.MARK_FILLED_DOT; +import static com.google.android.exoplayer2.text.span.TextEmphasisSpan.MARK_FILLED_SESAME; +import static com.google.android.exoplayer2.text.span.TextEmphasisSpan.MARK_OPEN_CIRCLE; +import static com.google.android.exoplayer2.text.span.TextEmphasisSpan.MARK_OPEN_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.span.TextEmphasisSpan.POSITION_OUTSIDE; +import static com.google.android.exoplayer2.text.span.TextEmphasisSpan.POSITION_UNKNOWN; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import com.google.android.exoplayer2.text.span.TextEmphasisSpan; + +/** + * This class is used to emphasize text using markers above or below the text. For example, markers + * known as boutens are commonly used in Japanese texts. Boutens are dots placed above or below a + * word or phrase that act as literal points of emphasis, equivalent to the use of italics in + * English. Boutens can help express implied meanings which provide a richer and more dynamic + * translation. + */ +/* package */ final class TextEmphasis { + + /** + * The position of the text emphasis relative to the base text. + */ + @TextEmphasisSpan.Position + public final int position; + + /** + * The desired emphasis mark + */ + @TextEmphasisSpan.Mark + public final int mark; + + private TextEmphasis(@TextEmphasisSpan.Mark int mark, @TextEmphasisSpan.Position int position) { + this.mark = mark; + this.position = position; + } + + @Override + public String toString() { + return "TextEmphasis{" + + "position=" + position + + ", mark=" + mark + + '}'; + } + + public static TextEmphasis createTextEmphasis(@Nullable String value) { + if (value == null) { + return null; + } + + String parsingValue = value.toLowerCase().trim(); + if ("".equals(parsingValue)) { + return null; + } + + String[] nodes = parsingValue.split("\\s+"); + + switch (nodes.length) { + case 0: + return null; + case 1: + return handleOneNode(nodes[0]); + case 2: + return handleTwoNodes(nodes[0], nodes[1]); + default: + // We ignore anything after third entry in value + return handleThreeNodes(nodes[0], nodes[1], nodes[2]); + } + } + + private static @Nullable + TextEmphasis handleOneNode(@NonNull String value) { + + if (TtmlNode.TEXT_EMPHASIS_NONE.equals(value)) { + return null; + } + + // Handle "auto" or unknown value + // If an implementation does not recognize or otherwise distinguish an emphasis style value, + // then it must be interpreted as if a style of auto were specified; as such, an implementation + // that supports text emphasis marks must minimally support the auto value. + return new TextEmphasis(MARK_AUTO, POSITION_UNKNOWN); + } + + private static @Nullable + TextEmphasis handleTwoNodes(@NonNull String mark, @NonNull String position) { + + @TextEmphasisSpan.Position int positionEntry = getPosition(position); + @TextEmphasisSpan.Mark int markEntry; + switch (mark) { + case TtmlNode.TEXT_EMPHASIS_AUTO: + markEntry = MARK_AUTO; + break; + // If only circle, dot, or sesame is specified, then it is equivalent to filled circle, + // filled dot, and filled sesame, respectively. + case TtmlNode.TEXT_EMPHASIS_MARK_DOT: + markEntry = MARK_FILLED_DOT; + break; + case TtmlNode.TEXT_EMPHASIS_MARK_SESAME: + markEntry = MARK_FILLED_SESAME; + break; + case TtmlNode.TEXT_EMPHASIS_MARK_CIRCLE: + markEntry = MARK_FILLED_CIRCLE; + break; + default: + // This is use case for: "filled dot" when position is not specified. + return handleWithPosition(mark, position, POSITION_UNKNOWN); + } + + return new TextEmphasis(markEntry, positionEntry); + } + + private static @Nullable + TextEmphasis handleWithPosition(@NonNull String markStyle, @Nullable String mark, + @TextEmphasisSpan.Position int position) { + + switch (mark) { + + case TtmlNode.TEXT_EMPHASIS_MARK_DOT: + if (TtmlNode.TEXT_EMPHASIS_MARK_FILLED.equals(markStyle)) { + return new TextEmphasis(MARK_FILLED_DOT, position); + } else if (TtmlNode.TEXT_EMPHASIS_MARK_OPEN.equals(markStyle)) { + return new TextEmphasis(MARK_OPEN_DOT, position); + } else { + return new TextEmphasis(MARK_FILLED_DOT, position); + } + + case TtmlNode.TEXT_EMPHASIS_MARK_SESAME: + if (TtmlNode.TEXT_EMPHASIS_MARK_FILLED.equals(markStyle)) { + return new TextEmphasis(MARK_FILLED_SESAME, position); + } else if (TtmlNode.TEXT_EMPHASIS_MARK_OPEN.equals(markStyle)) { + return new TextEmphasis(MARK_OPEN_SESAME, position); + } else { + return new TextEmphasis(MARK_FILLED_SESAME, position); + } + + case TtmlNode.TEXT_EMPHASIS_MARK_CIRCLE: + if (TtmlNode.TEXT_EMPHASIS_MARK_FILLED.equals(markStyle)) { + return new TextEmphasis(MARK_FILLED_CIRCLE, position); + } else if (TtmlNode.TEXT_EMPHASIS_MARK_OPEN.equals(markStyle)) { + return new TextEmphasis(MARK_OPEN_CIRCLE, position); + } else { + return new TextEmphasis(MARK_FILLED_CIRCLE, position); + } + + default: + // Not supported, default to AUTO. + break; + } + + return new TextEmphasis(MARK_AUTO, POSITION_UNKNOWN); + } + + private static @Nullable + TextEmphasis handleThreeNodes(@NonNull String markStyle, @NonNull String mark, + @NonNull String position) { + + @TextEmphasisSpan.Position int positionEntry = getPosition(position); + return handleWithPosition(markStyle, mark, positionEntry); + } + + private static @TextEmphasisSpan.Position + int getPosition(@NonNull String value) { + + switch (value) { + case TtmlNode.TEXT_EMPHASIS_POSITION_AFTER: + return POSITION_AFTER; + case TtmlNode.TEXT_EMPHASIS_POSITION_BEFORE: + return POSITION_BEFORE; + case TtmlNode.TEXT_EMPHASIS_POSITION_OUTSIDE: + return POSITION_OUTSIDE; + default: + // ignore + break; + } + return POSITION_UNKNOWN; + } +} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/ttml/TtmlDecoder.java b/library/core/src/main/java/com/google/android/exoplayer2/text/ttml/TtmlDecoder.java index 611eb7ff2f..8e13ea0c8d 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/ttml/TtmlDecoder.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/ttml/TtmlDecoder.java @@ -609,6 +609,10 @@ public final class TtmlDecoder extends SimpleSubtitleDecoder { break; } break; + case TtmlNode.ATTR_TTS_TEXT_EMPHASIS: + style = createIfNull(style).setTextEmphasis( + TextEmphasis.createTextEmphasis(Util.toLowerInvariant(attributeValue))); + break; default: // ignore break; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/ttml/TtmlNode.java b/library/core/src/main/java/com/google/android/exoplayer2/text/ttml/TtmlNode.java index 8e516dedf1..0a22c76575 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/ttml/TtmlNode.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/ttml/TtmlNode.java @@ -69,6 +69,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; public static final String ATTR_TTS_TEXT_DECORATION = "textDecoration"; public static final String ATTR_TTS_TEXT_ALIGN = "textAlign"; public static final String ATTR_TTS_TEXT_COMBINE = "textCombine"; + public static final String ATTR_TTS_TEXT_EMPHASIS = "textEmphasis"; public static final String ATTR_TTS_WRITING_MODE = "writingMode"; // Values for ruby @@ -106,6 +107,19 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; public static final String VERTICAL_LR = "tblr"; public static final String VERTICAL_RL = "tbrl"; + // Values for textEmphasis + public static final String TEXT_EMPHASIS_NONE = "none"; + public static final String TEXT_EMPHASIS_AUTO = "auto"; + public static final String TEXT_EMPHASIS_MARK_DOT = "dot"; + public static final String TEXT_EMPHASIS_MARK_SESAME = "sesame"; + public static final String TEXT_EMPHASIS_MARK_CIRCLE = "circle"; + public static final String TEXT_EMPHASIS_MARK_FILLED = "filled"; + public static final String TEXT_EMPHASIS_MARK_OPEN = "open"; + + public static final String TEXT_EMPHASIS_POSITION_AFTER = "after"; + public static final String TEXT_EMPHASIS_POSITION_BEFORE = "before"; + public static final String TEXT_EMPHASIS_POSITION_OUTSIDE = "outside"; + @Nullable public final String tag; @Nullable public final String text; public final boolean isTextNode; @@ -243,7 +257,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; TreeMap regionTextOutputs = new TreeMap<>(); traverseForText(timeUs, false, regionId, regionTextOutputs); - traverseForStyle(timeUs, globalStyles, regionTextOutputs); + traverseForStyle(timeUs, globalStyles, regionMap, regionId, regionTextOutputs); List cues = new ArrayList<>(); @@ -354,26 +368,32 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; } private void traverseForStyle( - long timeUs, Map globalStyles, Map regionOutputs) { + long timeUs, Map globalStyles, Map regionMaps, + String inheritedRegion, Map regionOutputs) { if (!isActive(timeUs)) { return; } + String resolvedRegionId = ANONYMOUS_REGION_ID.equals(regionId) ? inheritedRegion : regionId; + for (Map.Entry entry : nodeEndsByRegion.entrySet()) { String regionId = entry.getKey(); int start = nodeStartsByRegion.containsKey(regionId) ? nodeStartsByRegion.get(regionId) : 0; int end = entry.getValue(); if (start != end) { Cue.Builder regionOutput = Assertions.checkNotNull(regionOutputs.get(regionId)); - applyStyleToOutput(globalStyles, regionOutput, start, end); + @Cue.VerticalType int verticalType = Assertions + .checkNotNull(regionMaps.get(resolvedRegionId)).verticalType; + applyStyleToOutput(globalStyles, regionOutput, start, end, verticalType); } } for (int i = 0; i < getChildCount(); ++i) { - getChild(i).traverseForStyle(timeUs, globalStyles, regionOutputs); + getChild(i).traverseForStyle(timeUs, globalStyles, regionMaps, resolvedRegionId, regionOutputs); } } private void applyStyleToOutput( - Map globalStyles, Cue.Builder regionOutput, int start, int end) { + Map globalStyles, Cue.Builder regionOutput, int start, int end, + @Cue.VerticalType int verticalType) { @Nullable TtmlStyle resolvedStyle = TtmlRenderUtil.resolveStyle(style, styleIds, globalStyles); @Nullable SpannableStringBuilder text = (SpannableStringBuilder) regionOutput.getText(); if (text == null) { @@ -381,7 +401,8 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; regionOutput.setText(text); } if (resolvedStyle != null) { - TtmlRenderUtil.applyStylesToSpan(text, start, end, resolvedStyle, parent, globalStyles); + TtmlRenderUtil + .applyStylesToSpan(text, start, end, resolvedStyle, parent, globalStyles, verticalType); regionOutput.setTextAlignment(resolvedStyle.getTextAlign()); } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/ttml/TtmlRenderUtil.java b/library/core/src/main/java/com/google/android/exoplayer2/text/ttml/TtmlRenderUtil.java index 13f3fe2b16..b7c03e3053 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/ttml/TtmlRenderUtil.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/ttml/TtmlRenderUtil.java @@ -27,9 +27,11 @@ import android.text.style.StyleSpan; import android.text.style.TypefaceSpan; import android.text.style.UnderlineSpan; import androidx.annotation.Nullable; +import com.google.android.exoplayer2.text.Cue; import com.google.android.exoplayer2.text.span.HorizontalTextInVerticalContextSpan; import com.google.android.exoplayer2.text.span.RubySpan; import com.google.android.exoplayer2.text.span.SpanUtil; +import com.google.android.exoplayer2.text.span.TextEmphasisSpan; import com.google.android.exoplayer2.util.Log; import com.google.android.exoplayer2.util.Util; import java.util.ArrayDeque; @@ -83,7 +85,8 @@ import java.util.Map; int end, TtmlStyle style, @Nullable TtmlNode parent, - Map globalStyles) { + Map globalStyles, + @Cue.VerticalType int verticalType) { if (style.getStyle() != TtmlStyle.UNSPECIFIED) { builder.setSpan(new StyleSpan(style.getStyle()), start, end, @@ -119,6 +122,27 @@ import java.util.Map; end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); } + if (style.getTextEmphasis() != null) { + TextEmphasis textEmphasis = style.getTextEmphasis(); + // https://www.w3.org/TR/ttml2/#style-value-emphasis-style + // If an implementation does not recognize or otherwise distinguish an emphasis style value, + // then it must be interpreted as if a style of auto were specified; as such, an + // implementation that supports text emphasis marks must minimally support the auto value. + // If a vertical writing mode applies, then equivalent to filled sesame; otherwise, equivalent + // to filled circle. + @TextEmphasisSpan.Mark int mark = textEmphasis.mark; + if (textEmphasis.mark == TextEmphasisSpan.MARK_AUTO + || textEmphasis.mark == TextEmphasisSpan.MARK_UNKNOWN) { + mark = (verticalType == Cue.VERTICAL_TYPE_LR || verticalType == Cue.VERTICAL_TYPE_RL) ? + TextEmphasisSpan.MARK_FILLED_SESAME : TextEmphasisSpan.MARK_FILLED_CIRCLE; + } + SpanUtil.addOrReplaceSpan( + builder, + new TextEmphasisSpan(mark, textEmphasis.position), + start, + end, + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + } switch (style.getRubyType()) { case TtmlStyle.RUBY_TYPE_BASE: // look for the sibling RUBY_TEXT and add it as span between start & end. diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/ttml/TtmlStyle.java b/library/core/src/main/java/com/google/android/exoplayer2/text/ttml/TtmlStyle.java index 3ca519660d..f25defbb8b 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/ttml/TtmlStyle.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/ttml/TtmlStyle.java @@ -87,6 +87,8 @@ import java.lang.annotation.RetentionPolicy; @Nullable private Layout.Alignment textAlign; @OptionalBoolean private int textCombine; + private TextEmphasis textEmphasis; + public TtmlStyle() { linethrough = UNSPECIFIED; underline = UNSPECIFIED; @@ -238,6 +240,9 @@ import java.lang.annotation.RetentionPolicy; fontSizeUnit = ancestor.fontSizeUnit; fontSize = ancestor.fontSize; } + if (textEmphasis == null) { + textEmphasis = ancestor.textEmphasis; + } // attributes not inherited as of http://www.w3.org/TR/ttml1/ if (chaining && !hasBackgroundColor && ancestor.hasBackgroundColor) { setBackgroundColor(ancestor.backgroundColor); @@ -299,6 +304,16 @@ import java.lang.annotation.RetentionPolicy; return this; } + @Nullable + public TextEmphasis getTextEmphasis() { + return textEmphasis; + } + + public TtmlStyle setTextEmphasis(@Nullable TextEmphasis textEmphasis) { + this.textEmphasis = textEmphasis; + return this; + } + public TtmlStyle setFontSize(float fontSize) { this.fontSize = fontSize; return this; diff --git a/library/core/src/test/java/com/google/android/exoplayer2/text/ttml/TextEmphasisTest.java b/library/core/src/test/java/com/google/android/exoplayer2/text/ttml/TextEmphasisTest.java new file mode 100644 index 0000000000..6d50cdf08b --- /dev/null +++ b/library/core/src/test/java/com/google/android/exoplayer2/text/ttml/TextEmphasisTest.java @@ -0,0 +1,369 @@ +package com.google.android.exoplayer2.text.ttml; + +import static com.google.android.exoplayer2.text.ttml.TextEmphasis.createTextEmphasis; +import static com.google.common.truth.Truth.assertThat; +import static com.google.common.truth.Truth.assertWithMessage; + +import androidx.test.ext.junit.runners.AndroidJUnit4; +import com.google.android.exoplayer2.text.span.TextEmphasisSpan; +import com.google.android.exoplayer2.util.Log; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** Unit test for {@link TextEmphasis}. */ +@RunWith(AndroidJUnit4.class) +public class TextEmphasisTest { + + public final String TAG = "TextEmphasisTest"; + + @Test + public void testNull() { + String value = null; + TextEmphasis textEmphasis = createTextEmphasis(value); + + Log.d(TAG, "textEmphasis: " + textEmphasis); + assertWithMessage("Text Emphasis must be null").that(textEmphasis).isNull(); + } + + @Test + public void testEmpty() { + String value = ""; + TextEmphasis textEmphasis = createTextEmphasis(value); + + Log.d(TAG, "textEmphasis: " + textEmphasis); + assertWithMessage("Text Emphasis must be null").that(textEmphasis).isNull(); + } + + @Test + public void testNone() { + String value = "none"; + TextEmphasis textEmphasis = createTextEmphasis(value); + + Log.d(TAG, "textEmphasis: " + textEmphasis); + assertWithMessage("Text Emphasis must be null").that(textEmphasis).isNull(); + } + + @Test + public void testAuto() { + String value = "auto"; + TextEmphasis textEmphasis = createTextEmphasis(value); + + Log.d(TAG, "textEmphasis: " + textEmphasis); + assertWithMessage("Text Emphasis must exist").that(textEmphasis).isNotNull(); + assertThat(textEmphasis.mark).isEqualTo(TextEmphasisSpan.MARK_AUTO); + assertThat(textEmphasis.position).isEqualTo(TextEmphasisSpan.POSITION_UNKNOWN); + } + + @Test + public void testAutoOutside() { + String value = "auto outside"; + TextEmphasis textEmphasis = createTextEmphasis(value); + + Log.d(TAG, "textEmphasis: " + textEmphasis); + assertWithMessage("Text Emphasis must exist").that(textEmphasis).isNotNull(); + assertThat(textEmphasis.mark).isEqualTo(TextEmphasisSpan.MARK_AUTO); + assertThat(textEmphasis.position).isEqualTo(TextEmphasisSpan.POSITION_OUTSIDE); + } + + /** + * If only circle, dot, or sesame is specified, then it is equivalent to filled circle, filled dot, + * and filled sesame, respectively. + */ + + @Test + public void testDotBefore() { + String value = "dot before"; + TextEmphasis textEmphasis = createTextEmphasis(value); + + Log.d(TAG, "textEmphasis: " + textEmphasis); + assertWithMessage("Text Emphasis must exist").that(textEmphasis).isNotNull(); + assertThat(textEmphasis.mark).isEqualTo(TextEmphasisSpan.MARK_FILLED_DOT); + assertThat(textEmphasis.position).isEqualTo(TextEmphasisSpan.POSITION_BEFORE); + } + + @Test + public void testCircleBefore() { + String value = "circle before"; + TextEmphasis textEmphasis = createTextEmphasis(value); + + Log.d(TAG, "textEmphasis: " + textEmphasis); + assertWithMessage("Text Emphasis must exist").that(textEmphasis).isNotNull(); + assertThat(textEmphasis.mark).isEqualTo(TextEmphasisSpan.MARK_FILLED_CIRCLE); + assertThat(textEmphasis.position).isEqualTo(TextEmphasisSpan.POSITION_BEFORE); + } + + @Test + public void testSesameBefore() { + String value = "sesame before"; + TextEmphasis textEmphasis = createTextEmphasis(value); + + Log.d(TAG, "textEmphasis: " + textEmphasis); + assertWithMessage("Text Emphasis must exist").that(textEmphasis).isNotNull(); + assertThat(textEmphasis.mark).isEqualTo(TextEmphasisSpan.MARK_FILLED_SESAME); + assertThat(textEmphasis.position).isEqualTo(TextEmphasisSpan.POSITION_BEFORE); + } + + @Test + public void testDotAfter() { + String value = "dot AFTER"; + TextEmphasis textEmphasis = createTextEmphasis(value); + + Log.d(TAG, "textEmphasis: " + textEmphasis); + assertWithMessage("Text Emphasis must exist").that(textEmphasis).isNotNull(); + assertThat(textEmphasis.mark).isEqualTo(TextEmphasisSpan.MARK_FILLED_DOT); + assertThat(textEmphasis.position).isEqualTo(TextEmphasisSpan.POSITION_AFTER); + } + + @Test + public void testCircleAfter() { + String value = "circle after"; + TextEmphasis textEmphasis = createTextEmphasis(value); + + Log.d(TAG, "textEmphasis: " + textEmphasis); + assertWithMessage("Text Emphasis must exist").that(textEmphasis).isNotNull(); + assertThat(textEmphasis.mark).isEqualTo(TextEmphasisSpan.MARK_FILLED_CIRCLE); + assertThat(textEmphasis.position).isEqualTo(TextEmphasisSpan.POSITION_AFTER); + } + + @Test + public void testSesameAfter() { + String value = "sesame aFter"; + TextEmphasis textEmphasis = createTextEmphasis(value); + + Log.d(TAG, "textEmphasis: " + textEmphasis); + assertWithMessage("Text Emphasis must exist").that(textEmphasis).isNotNull(); + assertThat(textEmphasis.mark).isEqualTo(TextEmphasisSpan.MARK_FILLED_SESAME); + assertThat(textEmphasis.position).isEqualTo(TextEmphasisSpan.POSITION_AFTER); + } + + @Test + public void testDotOutside() { + String value = "dot outside"; + TextEmphasis textEmphasis = createTextEmphasis(value); + + Log.d(TAG, "textEmphasis: " + textEmphasis); + assertWithMessage("Text Emphasis must exist").that(textEmphasis).isNotNull(); + assertThat(textEmphasis.mark).isEqualTo(TextEmphasisSpan.MARK_FILLED_DOT); + assertThat(textEmphasis.position).isEqualTo(TextEmphasisSpan.POSITION_OUTSIDE); + } + + @Test + public void testCircleOutside() { + String value = "circle outside"; + TextEmphasis textEmphasis = createTextEmphasis(value); + + Log.d(TAG, "textEmphasis: " + textEmphasis); + assertWithMessage("Text Emphasis must exist").that(textEmphasis).isNotNull(); + assertThat(textEmphasis.mark).isEqualTo(TextEmphasisSpan.MARK_FILLED_CIRCLE); + assertThat(textEmphasis.position).isEqualTo(TextEmphasisSpan.POSITION_OUTSIDE); + } + + @Test + public void testSesameOutside() { + String value = "sesame outside"; + TextEmphasis textEmphasis = createTextEmphasis(value); + + Log.d(TAG, "textEmphasis: " + textEmphasis); + assertWithMessage("Text Emphasis must exist").that(textEmphasis).isNotNull(); + assertThat(textEmphasis.mark).isEqualTo(TextEmphasisSpan.MARK_FILLED_SESAME); + assertThat(textEmphasis.position).isEqualTo(TextEmphasisSpan.POSITION_OUTSIDE); + } + + @Test + public void testOpenDotAfter() { + String value = "open dot AFTER"; + TextEmphasis textEmphasis = createTextEmphasis(value); + + Log.d(TAG, "textEmphasis: " + textEmphasis); + assertWithMessage("Text Emphasis must exist").that(textEmphasis).isNotNull(); + assertThat(textEmphasis.mark).isEqualTo(TextEmphasisSpan.MARK_OPEN_DOT); + assertThat(textEmphasis.position).isEqualTo(TextEmphasisSpan.POSITION_AFTER); + } + + @Test + public void testOpenCircleAfter() { + String value = "Open circle after"; + TextEmphasis textEmphasis = createTextEmphasis(value); + + Log.d(TAG, "textEmphasis: " + textEmphasis); + assertWithMessage("Text Emphasis must exist").that(textEmphasis).isNotNull(); + assertThat(textEmphasis.mark).isEqualTo(TextEmphasisSpan.MARK_OPEN_CIRCLE); + assertThat(textEmphasis.position).isEqualTo(TextEmphasisSpan.POSITION_AFTER); + } + + @Test + public void testOpenSesameAfter() { + String value = "open sesame aFter"; + TextEmphasis textEmphasis = createTextEmphasis(value); + + Log.d(TAG, "textEmphasis: " + textEmphasis); + assertWithMessage("Text Emphasis must exist").that(textEmphasis).isNotNull(); + assertThat(textEmphasis.mark).isEqualTo(TextEmphasisSpan.MARK_OPEN_SESAME); + assertThat(textEmphasis.position).isEqualTo(TextEmphasisSpan.POSITION_AFTER); + } + + @Test + public void testOpenDotBefore() { + String value = "open dot before"; + TextEmphasis textEmphasis = createTextEmphasis(value); + + Log.d(TAG, "textEmphasis: " + textEmphasis); + assertWithMessage("Text Emphasis must exist").that(textEmphasis).isNotNull(); + assertThat(textEmphasis.mark).isEqualTo(TextEmphasisSpan.MARK_OPEN_DOT); + assertThat(textEmphasis.position).isEqualTo(TextEmphasisSpan.POSITION_BEFORE); + } + + @Test + public void testOpenCircleBefore() { + String value = "Open circle Before"; + TextEmphasis textEmphasis = createTextEmphasis(value); + + Log.d(TAG, "textEmphasis: " + textEmphasis); + assertWithMessage("Text Emphasis must exist").that(textEmphasis).isNotNull(); + assertThat(textEmphasis.mark).isEqualTo(TextEmphasisSpan.MARK_OPEN_CIRCLE); + assertThat(textEmphasis.position).isEqualTo(TextEmphasisSpan.POSITION_BEFORE); + } + + @Test + public void testOpenSesameBefore() { + String value = "open sesame Before"; + TextEmphasis textEmphasis = createTextEmphasis(value); + + Log.d(TAG, "textEmphasis: " + textEmphasis); + assertWithMessage("Text Emphasis must exist").that(textEmphasis).isNotNull(); + assertThat(textEmphasis.mark).isEqualTo(TextEmphasisSpan.MARK_OPEN_SESAME); + assertThat(textEmphasis.position).isEqualTo(TextEmphasisSpan.POSITION_BEFORE); + } + + @Test + public void testOpenDotOutside() { + String value = "open dot Outside"; + TextEmphasis textEmphasis = createTextEmphasis(value); + + Log.d(TAG, "textEmphasis: " + textEmphasis); + assertWithMessage("Text Emphasis must exist").that(textEmphasis).isNotNull(); + assertThat(textEmphasis.mark).isEqualTo(TextEmphasisSpan.MARK_OPEN_DOT); + assertThat(textEmphasis.position).isEqualTo(TextEmphasisSpan.POSITION_OUTSIDE); + } + + @Test + public void testOpenCircleOutside() { + String value = "Open circle Outside"; + TextEmphasis textEmphasis = createTextEmphasis(value); + + Log.d(TAG, "textEmphasis: " + textEmphasis); + assertWithMessage("Text Emphasis must exist").that(textEmphasis).isNotNull(); + assertThat(textEmphasis.mark).isEqualTo(TextEmphasisSpan.MARK_OPEN_CIRCLE); + assertThat(textEmphasis.position).isEqualTo(TextEmphasisSpan.POSITION_OUTSIDE); + } + + @Test + public void testOpenSesameOutside() { + String value = "open sesame outside"; + TextEmphasis textEmphasis = createTextEmphasis(value); + + Log.d(TAG, "textEmphasis: " + textEmphasis); + assertWithMessage("Text Emphasis must exist").that(textEmphasis).isNotNull(); + assertThat(textEmphasis.mark).isEqualTo(TextEmphasisSpan.MARK_OPEN_SESAME); + assertThat(textEmphasis.position).isEqualTo(TextEmphasisSpan.POSITION_OUTSIDE); + } + + @Test + public void testFilledDotOutside() { + String value = "filled dot outside"; + TextEmphasis textEmphasis = createTextEmphasis(value); + + Log.d(TAG, "textEmphasis: " + textEmphasis); + assertWithMessage("Text Emphasis must exist").that(textEmphasis).isNotNull(); + assertThat(textEmphasis.mark).isEqualTo(TextEmphasisSpan.MARK_FILLED_DOT); + assertThat(textEmphasis.position).isEqualTo(TextEmphasisSpan.POSITION_OUTSIDE); + } + + @Test + public void testFilledCircleOutside() { + String value = "filled circle outside"; + TextEmphasis textEmphasis = createTextEmphasis(value); + + Log.d(TAG, "textEmphasis: " + textEmphasis); + assertWithMessage("Text Emphasis must exist").that(textEmphasis).isNotNull(); + assertThat(textEmphasis.mark).isEqualTo(TextEmphasisSpan.MARK_FILLED_CIRCLE); + assertThat(textEmphasis.position).isEqualTo(TextEmphasisSpan.POSITION_OUTSIDE); + } + + @Test + public void testFilledSesameOutside() { + String value = "filled sesame outside"; + TextEmphasis textEmphasis = createTextEmphasis(value); + + Log.d(TAG, "textEmphasis: " + textEmphasis); + assertWithMessage("Text Emphasis must exist").that(textEmphasis).isNotNull(); + assertThat(textEmphasis.mark).isEqualTo(TextEmphasisSpan.MARK_FILLED_SESAME); + assertThat(textEmphasis.position).isEqualTo(TextEmphasisSpan.POSITION_OUTSIDE); + } + + @Test + public void testFilledDotAfter() { + String value = "filled dot After"; + TextEmphasis textEmphasis = createTextEmphasis(value); + + Log.d(TAG, "textEmphasis: " + textEmphasis); + assertWithMessage("Text Emphasis must exist").that(textEmphasis).isNotNull(); + assertThat(textEmphasis.mark).isEqualTo(TextEmphasisSpan.MARK_FILLED_DOT); + assertThat(textEmphasis.position).isEqualTo(TextEmphasisSpan.POSITION_AFTER); + } + + @Test + public void testFilledCircleAfter() { + String value = "filled circle after"; + TextEmphasis textEmphasis = createTextEmphasis(value); + + Log.d(TAG, "textEmphasis: " + textEmphasis); + assertWithMessage("Text Emphasis must exist").that(textEmphasis).isNotNull(); + assertThat(textEmphasis.mark).isEqualTo(TextEmphasisSpan.MARK_FILLED_CIRCLE); + assertThat(textEmphasis.position).isEqualTo(TextEmphasisSpan.POSITION_AFTER); + } + + @Test + public void testFilledSesameAfter() { + String value = "filled sesame After"; + TextEmphasis textEmphasis = createTextEmphasis(value); + + Log.d(TAG, "textEmphasis: " + textEmphasis); + assertWithMessage("Text Emphasis must exist").that(textEmphasis).isNotNull(); + assertThat(textEmphasis.mark).isEqualTo(TextEmphasisSpan.MARK_FILLED_SESAME); + assertThat(textEmphasis.position).isEqualTo(TextEmphasisSpan.POSITION_AFTER); + } + + @Test + public void testFilledDotBefore() { + String value = "filled dot before"; + TextEmphasis textEmphasis = createTextEmphasis(value); + + Log.d(TAG, "textEmphasis: " + textEmphasis); + assertWithMessage("Text Emphasis must exist").that(textEmphasis).isNotNull(); + assertThat(textEmphasis.mark).isEqualTo(TextEmphasisSpan.MARK_FILLED_DOT); + assertThat(textEmphasis.position).isEqualTo(TextEmphasisSpan.POSITION_BEFORE); + } + + @Test + public void testFilledCircleBefore() { + String value = "filled circle Before"; + TextEmphasis textEmphasis = createTextEmphasis(value); + + Log.d(TAG, "textEmphasis: " + textEmphasis); + assertWithMessage("Text Emphasis must exist").that(textEmphasis).isNotNull(); + assertThat(textEmphasis.mark).isEqualTo(TextEmphasisSpan.MARK_FILLED_CIRCLE); + assertThat(textEmphasis.position).isEqualTo(TextEmphasisSpan.POSITION_BEFORE); + } + + @Test + public void testFilledSesameBefore() { + String value = "filled sesame Before"; + TextEmphasis textEmphasis = createTextEmphasis(value); + + Log.d(TAG, "textEmphasis: " + textEmphasis); + assertWithMessage("Text Emphasis must exist").that(textEmphasis).isNotNull(); + assertThat(textEmphasis.mark).isEqualTo(TextEmphasisSpan.MARK_FILLED_SESAME); + assertThat(textEmphasis.position).isEqualTo(TextEmphasisSpan.POSITION_BEFORE); + } +} diff --git a/library/core/src/test/java/com/google/android/exoplayer2/text/ttml/TtmlDecoderTest.java b/library/core/src/test/java/com/google/android/exoplayer2/text/ttml/TtmlDecoderTest.java index dac21f3628..fa4c28aaf1 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/text/ttml/TtmlDecoderTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/text/ttml/TtmlDecoderTest.java @@ -28,6 +28,7 @@ import com.google.android.exoplayer2.text.Cue; import com.google.android.exoplayer2.text.Subtitle; import com.google.android.exoplayer2.text.SubtitleDecoderException; 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.ColorParser; import java.io.IOException; @@ -36,7 +37,9 @@ import java.util.Map; import org.junit.Test; import org.junit.runner.RunWith; -/** Unit test for {@link TtmlDecoder}. */ +/** + * Unit test for {@link TtmlDecoder}. + */ @RunWith(AndroidJUnit4.class) public final class TtmlDecoderTest { @@ -67,6 +70,7 @@ public final class TtmlDecoderTest { private static final String VERTICAL_TEXT_FILE = "media/ttml/vertical_text.xml"; private static final String TEXT_COMBINE_FILE = "media/ttml/text_combine.xml"; private static final String RUBIES_FILE = "media/ttml/rubies.xml"; + private static final String TEXT_EMPHASIS_FILE = "media/ttml/text_emphasis.xml"; @Test public void inlineAttributes() throws IOException, SubtitleDecoderException { @@ -109,12 +113,10 @@ public final class TtmlDecoderTest { * framework level. Tests that lime resolves to #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(SparseArray transitions, int key) { @Nullable Transition transition = transitions.get(key); if (transition == null) { diff --git a/library/ui/src/test/java/com/google/android/exoplayer2/ui/SpannedToHtmlConverterTest.java b/library/ui/src/test/java/com/google/android/exoplayer2/ui/SpannedToHtmlConverterTest.java index b9eb6d8e6a..b5d60416ff 100644 --- a/library/ui/src/test/java/com/google/android/exoplayer2/ui/SpannedToHtmlConverterTest.java +++ b/library/ui/src/test/java/com/google/android/exoplayer2/ui/SpannedToHtmlConverterTest.java @@ -34,6 +34,7 @@ import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; 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 org.junit.Test; import org.junit.runner.RunWith; import org.robolectric.annotation.Config; @@ -279,6 +280,37 @@ public class SpannedToHtmlConverterTest { + "section"); } + @Test + public void convert_supportsTextEmphasisSpan() { + SpannableString spanned = new SpannableString("Text emphasis おはよ ございます "); + spanned.setSpan( + new TextEmphasisSpan(TextEmphasisSpan.MARK_FILLED_CIRCLE, TextEmphasisSpan.POSITION_BEFORE), + "Text emphasis ".length(), + "Text emphasis おはよ".length(), + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + + spanned.setSpan( + new TextEmphasisSpan(TextEmphasisSpan.MARK_OPEN_SESAME, TextEmphasisSpan.POSITION_AFTER), + "Text emphasis おはよ ".length(), + "Text emphasis おはよ ございます ".length(), + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + + SpannedToHtmlConverter.HtmlAndCss htmlAndCss = + SpannedToHtmlConverter.convert(spanned, displayDensity); + + assertThat(htmlAndCss.cssRuleSets).isEmpty(); + assertThat(htmlAndCss.html) + .isEqualTo( + "Text emphasis おはよ " + + "ございます "); + } + @Test public void convert_supportsUnderlineSpan() { SpannableString spanned = new SpannableString("String with underlined section."); diff --git a/testdata/src/test/assets/media/ttml/text_emphasis.xml b/testdata/src/test/assets/media/ttml/text_emphasis.xml new file mode 100644 index 0000000000..90e85b931a --- /dev/null +++ b/testdata/src/test/assets/media/ttml/text_emphasis.xml @@ -0,0 +1,65 @@ + + + + + + + + +
+

None おはよ

+
+
+

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) おはよ

+
+ + +
diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/truth/SpannedSubject.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/truth/SpannedSubject.java index a980254277..546c417394 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/truth/SpannedSubject.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/truth/SpannedSubject.java @@ -38,6 +38,7 @@ import androidx.annotation.ColorInt; 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.Util; import com.google.common.truth.Fact; import com.google.common.truth.FailureMetadata; @@ -578,6 +579,47 @@ public final class SpannedSubject extends Subject { return ALREADY_FAILED_WITH_FLAGS; } + /** + * Checks that the subject has an {@link TextEmphasisSpan} from {@code start} + * to {@code end}. + * + * @param start The start of the expected span. + * @param end The end of the expected span. + * @return A {@link WithSpanFlags} object for optional additional assertions on the flags. + */ + public TextEmphasisDescription hasTextEmphasisSpanBetween(int start, int end) { + if (actual == null) { + failWithoutActual(simpleFact("Spanned must not be null")); + return ALREADY_FAILED_WITH_MARK; + } + + List textEmphasisSpans = + findMatchingSpans(start, end, TextEmphasisSpan.class); + if (textEmphasisSpans.size() == 1) { + return check("TextEmphasisSpan (start=%s,end=%s)", start, end).about(textEmphasisSubjects(actual)).that(textEmphasisSpans); + } + failWithExpectedSpan( + start, + end, + TextEmphasisSpan.class, + actual.toString().substring(start, end)); + return ALREADY_FAILED_WITH_MARK; + } + + /** + * Checks that the subject has no {@link TextEmphasisSpan}s on any of the text between + * {@code start} and {@code end}. + * + *

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> textEmphasisSubjects(Spanned actualSpanned) { + return (FailureMetadata metadata, List spans) -> + new TextEmphasisSubject(metadata, spans, actualSpanned); + } + + private static final class TextEmphasisSubject extends Subject implements TextEmphasisDescription { + + private final List actualSpans; + private final Spanned actualSpanned; + + private TextEmphasisSubject( + FailureMetadata metadata, List actualSpans, Spanned actualSpanned) { + super(metadata, actualSpans); + this.actualSpans = actualSpans; + this.actualSpanned = actualSpanned; + } + + @Override + public AndSpanFlags withMarkAndPosition(@TextEmphasisSpan.Mark int mark, + @TextEmphasisSpan.Position int position) { + List matchingSpanFlags = new ArrayList<>(); + List textEmphasisMarksAndPositions = new ArrayList<>(); + for (TextEmphasisSpan span : actualSpans) { + textEmphasisMarksAndPositions.add(new MarkAndPosition(span.mark, span.position)); + if (span.mark == mark && span.position == position) { + matchingSpanFlags.add(actualSpanned.getSpanFlags(span)); + } + } + check("textEmphasisMarkAndPosition") + .that(textEmphasisMarksAndPositions) + .containsExactly(new MarkAndPosition(mark, position)); + return check("flags").about(spanFlags()).that(matchingSpanFlags); + } + + private static final class MarkAndPosition { + + @TextEmphasisSpan.Mark + private final int mark; + @TextEmphasisSpan.Position + private final int position; + + private MarkAndPosition(@TextEmphasisSpan.Mark int mark, + @TextEmphasisSpan.Position int position) { + this.mark = mark; + this.position = position; + } + + @Override + public boolean equals(@Nullable Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + TextEmphasisSubject.MarkAndPosition that = (TextEmphasisSubject.MarkAndPosition) o; + return (position == that.position) && (mark == that.mark); + } + + @Override + public int hashCode() { + int result = 34613 * mark + position; + return result; + } + + @Override + public String toString() { + return String.format("{mark=%s,position=%s}", mark, position); + } + } + } + } diff --git a/testutils/src/test/java/com/google/android/exoplayer2/testutil/truth/SpannedSubjectTest.java b/testutils/src/test/java/com/google/android/exoplayer2/testutil/truth/SpannedSubjectTest.java index 75495a4293..23ee8d3dbf 100644 --- a/testutils/src/test/java/com/google/android/exoplayer2/testutil/truth/SpannedSubjectTest.java +++ b/testutils/src/test/java/com/google/android/exoplayer2/testutil/truth/SpannedSubjectTest.java @@ -41,6 +41,8 @@ import com.google.android.exoplayer2.testutil.truth.SpannedSubject.AndSpanFlags; import com.google.android.exoplayer2.testutil.truth.SpannedSubject.WithSpanFlags; 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.Util; import com.google.common.truth.ExpectFailure; import org.junit.Test; import org.junit.runner.RunWith; @@ -679,6 +681,104 @@ public class SpannedSubjectTest { new RubySpan("ruby text", RubySpan.POSITION_OVER), SpannedSubject::hasNoRubySpanBetween); } + @Test + public void textEmphasis_success() { + SpannableString spannable = + createSpannable( + new TextEmphasisSpan(TextEmphasisSpan.MARK_FILLED_CIRCLE, TextEmphasisSpan.POSITION_AFTER)); + + assertThat(spannable) + .hasTextEmphasisSpanBetween(SPAN_START, SPAN_END) + .withMarkAndPosition(TextEmphasisSpan.MARK_FILLED_CIRCLE, TextEmphasisSpan.POSITION_AFTER) + .andFlags(Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + } + + @Test + public void textEmphasis_wrongIndex() { + checkHasSpanFailsDueToIndexMismatch( + new TextEmphasisSpan(TextEmphasisSpan.MARK_FILLED_CIRCLE, TextEmphasisSpan.POSITION_AFTER), + SpannedSubject::hasTextEmphasisSpanBetween); + } + + + @Test + public void textEmphasis_wrongMark() { + SpannableString spannable = + createSpannable( + new TextEmphasisSpan(TextEmphasisSpan.MARK_FILLED_CIRCLE, TextEmphasisSpan.POSITION_AFTER)); + + AssertionError expected = + expectFailure( + whenTesting -> + whenTesting + .that(spannable) + .hasTextEmphasisSpanBetween(SPAN_START, SPAN_END) + .withMarkAndPosition(TextEmphasisSpan.MARK_OPEN_DOT, + TextEmphasisSpan.POSITION_AFTER)); + + assertThat(expected).factValue("value of").contains("textEmphasisMarkAndPosition"); + assertThat(expected).factValue("expected").contains(Util.formatInvariant( + "{mark=%d,position=%d}", TextEmphasisSpan.MARK_OPEN_DOT, + TextEmphasisSpan.POSITION_AFTER)); + assertThat(expected).factValue("but was").contains(Util.formatInvariant( + "{mark=%d,position=%d}", TextEmphasisSpan.MARK_FILLED_CIRCLE, + TextEmphasisSpan.POSITION_AFTER)); + + } + + @Test + public void textEmphasis_wrongPosition() { + SpannableString spannable = + createSpannable( + new TextEmphasisSpan(TextEmphasisSpan.MARK_OPEN_SESAME, TextEmphasisSpan.POSITION_BEFORE)); + + AssertionError expected = + expectFailure( + whenTesting -> + whenTesting + .that(spannable) + .hasTextEmphasisSpanBetween(SPAN_START, SPAN_END) + .withMarkAndPosition(TextEmphasisSpan.MARK_OPEN_SESAME, + TextEmphasisSpan.POSITION_AFTER)); + + assertThat(expected).factValue("value of").contains("textEmphasisMarkAndPosition"); + assertThat(expected).factValue("expected").contains(Util.formatInvariant( + "{mark=%d,position=%d}", TextEmphasisSpan.MARK_OPEN_SESAME, + TextEmphasisSpan.POSITION_AFTER)); + assertThat(expected).factValue("but was").contains(Util.formatInvariant( + "{mark=%d,position=%d}", TextEmphasisSpan.MARK_OPEN_SESAME, + TextEmphasisSpan.POSITION_BEFORE)); + } + + @Test + public void textEmphasis_wrongFlags() { + checkHasSpanFailsDueToFlagMismatch( + new TextEmphasisSpan(TextEmphasisSpan.MARK_OPEN_SESAME, TextEmphasisSpan.POSITION_BEFORE), + (subject, start, end) -> + subject + .hasTextEmphasisSpanBetween(start, end) + .withMarkAndPosition(TextEmphasisSpan.MARK_OPEN_SESAME, + TextEmphasisSpan.POSITION_BEFORE)); + } + + + @Test + public void noTextEmphasis_success() { + SpannableString spannable = + createSpannableWithUnrelatedSpanAnd( + new TextEmphasisSpan(TextEmphasisSpan.MARK_FILLED_CIRCLE, + TextEmphasisSpan.POSITION_AFTER)); + + assertThat(spannable).hasNoTextEmphasisSpanBetween(UNRELATED_SPAN_START, UNRELATED_SPAN_END); + } + + @Test + public void noTextEmphasis_failure() { + checkHasNoSpanFails( + new TextEmphasisSpan(TextEmphasisSpan.MARK_FILLED_CIRCLE, TextEmphasisSpan.POSITION_AFTER), + SpannedSubject::hasNoTextEmphasisSpanBetween); + } + @Test public void horizontalTextInVerticalContextSpan_success() { SpannableString spannable = From 4ad6454713a11628a39fe89591c4ea4c0c9d32c0 Mon Sep 17 00:00:00 2001 From: Denise LaFayette Date: Thu, 4 Mar 2021 12:21:45 -0800 Subject: [PATCH 2/6] Address code review comments - Refactor TextEmphasis class to support different ordering of styles - Merge RubySpan.Position and TextEmphasisSpan.Position - Remove TTML constructs from Spanned classes --- .../exoplayer2/text/span/RubySpan.java | 46 +--- .../exoplayer2/text/span/TextAnnotation.java | 47 ++++ .../text/span/TextEmphasisSpan.java | 100 ++----- .../exoplayer2/text/ttml/TextEmphasis.java | 252 +++++++++--------- .../exoplayer2/text/ttml/TtmlDecoder.java | 6 +- .../exoplayer2/text/ttml/TtmlRenderUtil.java | 40 ++- .../exoplayer2/text/ttml/TtmlStyle.java | 11 +- .../exoplayer2/text/webvtt/CssParser.java | 5 +- .../text/webvtt/WebvttCssStyle.java | 9 +- .../text/webvtt/WebvttCueParser.java | 27 +- .../text/ttml/TextEmphasisTest.java | 151 ++++++----- .../exoplayer2/text/ttml/TtmlDecoderTest.java | 40 +-- .../exoplayer2/text/ttml/TtmlStyleTest.java | 15 +- .../text/webvtt/WebvttDecoderTest.java | 13 +- .../exoplayer2/ui/SpannedToHtmlConverter.java | 21 +- .../ui/SpannedToHtmlConverterTest.java | 9 +- .../test/assets/media/ttml/text_emphasis.xml | 16 +- .../testutil/truth/SpannedSubject.java | 35 +-- .../testutil/truth/SpannedSubjectTest.java | 26 +- 19 files changed, 417 insertions(+), 452 deletions(-) create mode 100644 library/core/src/main/java/com/google/android/exoplayer2/text/span/TextAnnotation.java diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/span/RubySpan.java b/library/core/src/main/java/com/google/android/exoplayer2/text/span/RubySpan.java index 8ed84d6f6b..3491d68853 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/span/RubySpan.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/span/RubySpan.java @@ -16,12 +16,6 @@ */ 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; - /** * A styling span for ruby text. * @@ -38,48 +32,14 @@ import java.lang.annotation.Retention; // rubies (e.g. HTML tag). public final class RubySpan { - /** The ruby position is unknown. */ - public static final int POSITION_UNKNOWN = -1; - - /** - * The ruby text should be positioned above the base text. - * - *

For vertical text it should be positioned to the right, same as CSS's ruby-position. - */ - public static final int POSITION_OVER = 1; - - /** - * The ruby text should be positioned below the base text. - * - *

For vertical text it should be positioned to the left, same as CSS's ruby-position. - */ - public static final int POSITION_UNDER = 2; - - /** - * The possible positions of the ruby text relative to the base text. - * - *

One of: - * - *

    - *
  • {@link #POSITION_UNKNOWN} - *
  • {@link #POSITION_OVER} - *
  • {@link #POSITION_UNDER} - *
- */ - @Documented - @Retention(SOURCE) - @IntDef({POSITION_UNKNOWN, POSITION_OVER, POSITION_UNDER}) - public @interface Position {} - /** The ruby text, i.e. the smaller explanatory characters. */ public final String rubyText; /** The position of the ruby text relative to the base text. */ - @Position public final int position; + @TextAnnotation.Position + public final int position; - public RubySpan(String rubyText, @Position int position) { + public RubySpan(String rubyText, @TextAnnotation.Position int position) { this.rubyText = rubyText; this.position = position; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/span/TextAnnotation.java b/library/core/src/main/java/com/google/android/exoplayer2/text/span/TextAnnotation.java new file mode 100644 index 0000000000..65a14b61c2 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/span/TextAnnotation.java @@ -0,0 +1,47 @@ +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; + +/** + * Class used to describe properties of a text annotation (i.e. ruby, text emphasis marks) + */ +public class TextAnnotation { + /** The text annotation position is unknown. */ + public static final int POSITION_UNKNOWN = -1; + + /** + * For horizontal text, the text annotation should be positioned above the base text. + * + *

For vertical text it should be positioned to the right, same as CSS's ruby-position. + */ + public static final int POSITION_BEFORE = 1; + + /** + * For horizontal text, the text annotation should be positioned below the base text. + * + *

For vertical text it should be positioned to the left, same as CSS's ruby-position. + */ + public static final int POSITION_AFTER = 2; + + /** + * The possible positions of the annotation text relative to the base text. + * + *

One of: + * + *

    + *
  • {@link #POSITION_UNKNOWN} + *
  • {@link #POSITION_BEFORE} + *
  • {@link #POSITION_AFTER} + *
+ */ + @Documented + @Retention(SOURCE) + @IntDef({POSITION_UNKNOWN, POSITION_BEFORE, POSITION_AFTER}) + public @interface Position {} +} 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 index 6d87ad521c..d247ad9364 100644 --- 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 @@ -23,72 +23,22 @@ import java.lang.annotation.Documented; import java.lang.annotation.Retention; public final class TextEmphasisSpan { - /** - * Text Emphasis Position - */ + // Bits [1:0] are used for typical mark types + public static final int MARK_FLAG_CIRCLE = 1; + public static final int MARK_FLAG_DOT = 2; + public static final int MARK_FLAG_SESAME = 3; - /** - * 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; + // Bit 2 is used for filled/open + public static final int MARK_FLAG_FILLED = 0; + public static final int MARK_FLAG_OPEN = 4; - /** - * 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: - * - *
    - *
  • Equivalent to {@link #POSITION_BEFORE} for (1) the only line area of or (2) the first line - * area of the last block area generated by a p element which contains annotated text - *
  • otherwise, equivalent to {@link #POSITION_AFTER} - *
- */ - public static final int POSITION_OUTSIDE = 3; - - /** - * The possible positions of the emphasis marks relative to the base text. - * - *

One of: - * - *

    - *
  • {@link #POSITION_UNKNOWN} - *
  • {@link #POSITION_BEFORE} - *
  • {@link #POSITION_AFTER} - *
  • {@link #POSITION_OUTSIDE} - *
- */ - @Documented - @Retention(SOURCE) - @IntDef({POSITION_UNKNOWN, POSITION_BEFORE, POSITION_AFTER, POSITION_OUTSIDE}) - public @interface Position { - - } - - /** - * The text emphasis position is unknown. - */ - public static final int MARK_UNKNOWN = -1; - - public static final int MARK_AUTO = 1; - public static final int MARK_FILLED_CIRCLE = 2; - public static final int MARK_FILLED_DOT = 3; - public static final int MARK_FILLED_SESAME = 4; - public static final int MARK_OPEN_CIRCLE = 5; - public static final int MARK_OPEN_DOT = 6; - public static final int MARK_OPEN_SESAME = 7; + // Below are the mark style constants + public static final int MARK_FILLED_CIRCLE = MARK_FLAG_CIRCLE | MARK_FLAG_FILLED; + public static final int MARK_FILLED_DOT = MARK_FLAG_DOT | MARK_FLAG_FILLED; + public static final int MARK_FILLED_SESAME = MARK_FLAG_SESAME | MARK_FLAG_FILLED; + public static final int MARK_OPEN_CIRCLE = MARK_FLAG_CIRCLE | MARK_FLAG_OPEN; + public static final int MARK_OPEN_DOT = MARK_FLAG_DOT | MARK_FLAG_OPEN; + public static final int MARK_OPEN_SESAME = MARK_FLAG_SESAME | MARK_FLAG_OPEN; /** * The possible types of annotations used. @@ -96,8 +46,6 @@ public final class TextEmphasisSpan { *

One of: * *

    - *
  • {@link #MARK_UNKNOWN} - *
  • {@link #MARK_AUTO} *
  • {@link #MARK_FILLED_CIRCLE} *
  • {@link #MARK_FILLED_DOT} *
  • {@link #MARK_FILLED_SESAME} @@ -105,29 +53,31 @@ public final class TextEmphasisSpan { *
  • {@link #MARK_OPEN_DOT} *
  • {@link #MARK_OPEN_SESAME} *
+ * + * Note: We are intentionally excluding MARK_AUTO here since the auto value should + * be resolved */ @Documented @Retention(SOURCE) - @IntDef({MARK_UNKNOWN, MARK_AUTO, MARK_FILLED_CIRCLE, MARK_FILLED_DOT, MARK_FILLED_SESAME, + @IntDef({MARK_FILLED_CIRCLE, MARK_FILLED_DOT, MARK_FILLED_SESAME, MARK_OPEN_CIRCLE, MARK_OPEN_DOT, MARK_OPEN_SESAME}) public @interface Mark { - } + /** + * The mark used to emphasis text + */ + public @TextEmphasisSpan.Mark int mark; + /** * The position of the text emphasis relative to the base text */ - @TextEmphasisSpan.Position + @TextAnnotation.Position public final int position; - /** - * The text emphasis mark - */ - @TextEmphasisSpan.Mark - public final int mark; public TextEmphasisSpan(@TextEmphasisSpan.Mark int mark, - @TextEmphasisSpan.Position int position) { + @TextAnnotation.Position int position) { this.mark = mark; this.position = position; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/ttml/TextEmphasis.java b/library/core/src/main/java/com/google/android/exoplayer2/text/ttml/TextEmphasis.java index 6d1be2e4cb..d7b874d511 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/ttml/TextEmphasis.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/ttml/TextEmphasis.java @@ -16,21 +16,17 @@ */ package com.google.android.exoplayer2.text.ttml; -import static com.google.android.exoplayer2.text.span.TextEmphasisSpan.MARK_AUTO; -import static com.google.android.exoplayer2.text.span.TextEmphasisSpan.MARK_FILLED_CIRCLE; -import static com.google.android.exoplayer2.text.span.TextEmphasisSpan.MARK_FILLED_DOT; -import static com.google.android.exoplayer2.text.span.TextEmphasisSpan.MARK_FILLED_SESAME; -import static com.google.android.exoplayer2.text.span.TextEmphasisSpan.MARK_OPEN_CIRCLE; -import static com.google.android.exoplayer2.text.span.TextEmphasisSpan.MARK_OPEN_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.span.TextEmphasisSpan.POSITION_OUTSIDE; -import static com.google.android.exoplayer2.text.span.TextEmphasisSpan.POSITION_UNKNOWN; +import static java.lang.annotation.RetentionPolicy.SOURCE; -import androidx.annotation.NonNull; +import androidx.annotation.IntDef; import androidx.annotation.Nullable; +import com.google.android.exoplayer2.text.span.TextAnnotation; import com.google.android.exoplayer2.text.span.TextEmphasisSpan; +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Sets; +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.util.Set; /** * This class is used to emphasize text using markers above or below the text. For example, markers @@ -42,18 +38,73 @@ import com.google.android.exoplayer2.text.span.TextEmphasisSpan; /* package */ final class TextEmphasis { /** - * The position of the text emphasis relative to the base text. + * Mark style to be resolved at rendering time. Hence, it is not defined in + * {@link TextEmphasisSpan.Mark} */ - @TextEmphasisSpan.Position - public final int position; + public static final int MARK_AUTO = 1 << 8; + + @Documented + @Retention(SOURCE) + @IntDef({TextEmphasisSpan.MARK_FILLED_CIRCLE, + TextEmphasisSpan.MARK_FILLED_DOT, + TextEmphasisSpan.MARK_FILLED_SESAME, + TextEmphasisSpan.MARK_OPEN_CIRCLE, + TextEmphasisSpan.MARK_OPEN_DOT, + TextEmphasisSpan.MARK_OPEN_SESAME, + // Extending the definition in TextEmphasisSpan for intermediate values + MARK_AUTO + }) + + /* package */ @interface Mark { + } /** - * The desired emphasis mark + * The mark style of the text emphasis. */ - @TextEmphasisSpan.Mark - public final int mark; + /* package */@Mark + final int mark; - private TextEmphasis(@TextEmphasisSpan.Mark int mark, @TextEmphasisSpan.Position int position) { + /** + * Position to be resolved at rendering time. Hence, it is not defined in + * {@link TextAnnotation.Position} + */ + public static final int POSITION_OUTSIDE = 1 << 8; + + @Documented + @Retention(SOURCE) + @IntDef({TextAnnotation.POSITION_UNKNOWN, + TextAnnotation.POSITION_BEFORE, + TextAnnotation.POSITION_AFTER, + // Extending the definition in TextAnnotation.Position for intermediate values + POSITION_OUTSIDE + }) + public @interface Position {} + + /** + * The position of the text emphasis relative to the base text. + */ + @Position + public final int position; + + private static Set markValues = ImmutableSet.of( + TtmlNode.TEXT_EMPHASIS_AUTO, + TtmlNode.TEXT_EMPHASIS_MARK_DOT, + TtmlNode.TEXT_EMPHASIS_MARK_SESAME, + TtmlNode.TEXT_EMPHASIS_MARK_CIRCLE + ); + + private static Set markStyles = ImmutableSet.of( + TtmlNode.TEXT_EMPHASIS_MARK_FILLED, + TtmlNode.TEXT_EMPHASIS_MARK_OPEN + ); + + private static Set positionValues = ImmutableSet.of( + TtmlNode.TEXT_EMPHASIS_POSITION_AFTER, + TtmlNode.TEXT_EMPHASIS_POSITION_BEFORE, + TtmlNode.TEXT_EMPHASIS_POSITION_OUTSIDE + ); + + private TextEmphasis(@Mark int mark, @TextAnnotation.Position int position) { this.mark = mark; this.position = position; } @@ -76,126 +127,65 @@ import com.google.android.exoplayer2.text.span.TextEmphasisSpan; return null; } - String[] nodes = parsingValue.split("\\s+"); + Set nodes = Sets.newHashSet(parsingValue.split("\\s+")); - switch (nodes.length) { - case 0: - return null; - case 1: - return handleOneNode(nodes[0]); - case 2: - return handleTwoNodes(nodes[0], nodes[1]); - default: - // We ignore anything after third entry in value - return handleThreeNodes(nodes[0], nodes[1], nodes[2]); - } - } - - private static @Nullable - TextEmphasis handleOneNode(@NonNull String value) { - - if (TtmlNode.TEXT_EMPHASIS_NONE.equals(value)) { + if (nodes.size() == 0 || TtmlNode.TEXT_EMPHASIS_NONE.equals(nodes.iterator().next())) { return null; } - - // Handle "auto" or unknown value - // If an implementation does not recognize or otherwise distinguish an emphasis style value, - // then it must be interpreted as if a style of auto were specified; as such, an implementation - // that supports text emphasis marks must minimally support the auto value. - return new TextEmphasis(MARK_AUTO, POSITION_UNKNOWN); + return parseNodes(nodes); } - private static @Nullable - TextEmphasis handleTwoNodes(@NonNull String mark, @NonNull String position) { + private static @Nullable TextEmphasis parseNodes(Set nodes) { + Set styleSet = Sets.intersection(markStyles, nodes).immutableCopy(); + Set markSet = Sets.intersection(markValues, nodes).immutableCopy(); + Set positionSet = Sets.intersection(positionValues, nodes).immutableCopy(); - @TextEmphasisSpan.Position int positionEntry = getPosition(position); - @TextEmphasisSpan.Mark int markEntry; - switch (mark) { - case TtmlNode.TEXT_EMPHASIS_AUTO: - markEntry = MARK_AUTO; - break; - // If only circle, dot, or sesame is specified, then it is equivalent to filled circle, - // filled dot, and filled sesame, respectively. - case TtmlNode.TEXT_EMPHASIS_MARK_DOT: - markEntry = MARK_FILLED_DOT; - break; - case TtmlNode.TEXT_EMPHASIS_MARK_SESAME: - markEntry = MARK_FILLED_SESAME; - break; - case TtmlNode.TEXT_EMPHASIS_MARK_CIRCLE: - markEntry = MARK_FILLED_CIRCLE; - break; - default: - // This is use case for: "filled dot" when position is not specified. - return handleWithPosition(mark, position, POSITION_UNKNOWN); + @Mark int mark = 0; + if (styleSet.size() == 1) { + mark |= TtmlNode.TEXT_EMPHASIS_MARK_OPEN.equals(styleSet.iterator().next()) + ? TextEmphasisSpan.MARK_FLAG_OPEN + : TextEmphasisSpan.MARK_FLAG_FILLED; + } + if (markSet.size() == 1) { + switch ((String) markSet.iterator().next()) { + case TtmlNode.TEXT_EMPHASIS_AUTO: + mark |= MARK_AUTO; + break; + case TtmlNode.TEXT_EMPHASIS_MARK_DOT: + mark |= TextEmphasisSpan.MARK_FLAG_DOT; + break; + case TtmlNode.TEXT_EMPHASIS_MARK_SESAME: + mark |= TextEmphasisSpan.MARK_FLAG_SESAME; + break; + case TtmlNode.TEXT_EMPHASIS_MARK_CIRCLE: + default: + mark |= TextEmphasisSpan.MARK_FLAG_CIRCLE; + } + } else { + mark |= TextEmphasisSpan.MARK_FLAG_CIRCLE; } - return new TextEmphasis(markEntry, positionEntry); - } - - private static @Nullable - TextEmphasis handleWithPosition(@NonNull String markStyle, @Nullable String mark, - @TextEmphasisSpan.Position int position) { - - switch (mark) { - - case TtmlNode.TEXT_EMPHASIS_MARK_DOT: - if (TtmlNode.TEXT_EMPHASIS_MARK_FILLED.equals(markStyle)) { - return new TextEmphasis(MARK_FILLED_DOT, position); - } else if (TtmlNode.TEXT_EMPHASIS_MARK_OPEN.equals(markStyle)) { - return new TextEmphasis(MARK_OPEN_DOT, position); - } else { - return new TextEmphasis(MARK_FILLED_DOT, position); - } - - case TtmlNode.TEXT_EMPHASIS_MARK_SESAME: - if (TtmlNode.TEXT_EMPHASIS_MARK_FILLED.equals(markStyle)) { - return new TextEmphasis(MARK_FILLED_SESAME, position); - } else if (TtmlNode.TEXT_EMPHASIS_MARK_OPEN.equals(markStyle)) { - return new TextEmphasis(MARK_OPEN_SESAME, position); - } else { - return new TextEmphasis(MARK_FILLED_SESAME, position); - } - - case TtmlNode.TEXT_EMPHASIS_MARK_CIRCLE: - if (TtmlNode.TEXT_EMPHASIS_MARK_FILLED.equals(markStyle)) { - return new TextEmphasis(MARK_FILLED_CIRCLE, position); - } else if (TtmlNode.TEXT_EMPHASIS_MARK_OPEN.equals(markStyle)) { - return new TextEmphasis(MARK_OPEN_CIRCLE, position); - } else { - return new TextEmphasis(MARK_FILLED_CIRCLE, position); - } - - default: - // Not supported, default to AUTO. - break; + /** + * If no emphasis position is specified, then the emphasis position must be interpreted as if + * a position of outside were specified. + *

+ * More information on + * tts:textEmphasis + */ + @Position int position = POSITION_OUTSIDE; + if (positionSet.size() == 1) { + switch ((String) positionSet.iterator().next()) { + case TtmlNode.TEXT_EMPHASIS_POSITION_AFTER: + position = TextAnnotation.POSITION_AFTER; + break; + case TtmlNode.TEXT_EMPHASIS_POSITION_OUTSIDE: + position = POSITION_OUTSIDE; + break; + case TtmlNode.TEXT_EMPHASIS_POSITION_BEFORE: + default: + position = TextAnnotation.POSITION_BEFORE; + } } - - return new TextEmphasis(MARK_AUTO, POSITION_UNKNOWN); - } - - private static @Nullable - TextEmphasis handleThreeNodes(@NonNull String markStyle, @NonNull String mark, - @NonNull String position) { - - @TextEmphasisSpan.Position int positionEntry = getPosition(position); - return handleWithPosition(markStyle, mark, positionEntry); - } - - private static @TextEmphasisSpan.Position - int getPosition(@NonNull String value) { - - switch (value) { - case TtmlNode.TEXT_EMPHASIS_POSITION_AFTER: - return POSITION_AFTER; - case TtmlNode.TEXT_EMPHASIS_POSITION_BEFORE: - return POSITION_BEFORE; - case TtmlNode.TEXT_EMPHASIS_POSITION_OUTSIDE: - return POSITION_OUTSIDE; - default: - // ignore - break; - } - return POSITION_UNKNOWN; + return new TextEmphasis(mark, position); } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/ttml/TtmlDecoder.java b/library/core/src/main/java/com/google/android/exoplayer2/text/ttml/TtmlDecoder.java index 8e13ea0c8d..873105b9c7 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/ttml/TtmlDecoder.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/ttml/TtmlDecoder.java @@ -22,7 +22,7 @@ import com.google.android.exoplayer2.text.Cue; import com.google.android.exoplayer2.text.SimpleSubtitleDecoder; import com.google.android.exoplayer2.text.Subtitle; import com.google.android.exoplayer2.text.SubtitleDecoderException; -import com.google.android.exoplayer2.text.span.RubySpan; +import com.google.android.exoplayer2.text.span.TextAnnotation; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.ColorParser; import com.google.android.exoplayer2.util.Log; @@ -583,10 +583,10 @@ public final class TtmlDecoder extends SimpleSubtitleDecoder { case TtmlNode.ATTR_TTS_RUBY_POSITION: switch (Util.toLowerInvariant(attributeValue)) { case TtmlNode.RUBY_BEFORE: - style = createIfNull(style).setRubyPosition(RubySpan.POSITION_OVER); + style = createIfNull(style).setRubyPosition(TextAnnotation.POSITION_BEFORE); break; case TtmlNode.RUBY_AFTER: - style = createIfNull(style).setRubyPosition(RubySpan.POSITION_UNDER); + style = createIfNull(style).setRubyPosition(TextAnnotation.POSITION_AFTER); break; default: // ignore diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/ttml/TtmlRenderUtil.java b/library/core/src/main/java/com/google/android/exoplayer2/text/ttml/TtmlRenderUtil.java index b7c03e3053..72b1f63f15 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/ttml/TtmlRenderUtil.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/ttml/TtmlRenderUtil.java @@ -31,6 +31,7 @@ import com.google.android.exoplayer2.text.Cue; import com.google.android.exoplayer2.text.span.HorizontalTextInVerticalContextSpan; import com.google.android.exoplayer2.text.span.RubySpan; import com.google.android.exoplayer2.text.span.SpanUtil; +import com.google.android.exoplayer2.text.span.TextAnnotation; import com.google.android.exoplayer2.text.span.TextEmphasisSpan; import com.google.android.exoplayer2.util.Log; import com.google.android.exoplayer2.util.Util; @@ -124,21 +125,36 @@ import java.util.Map; } if (style.getTextEmphasis() != null) { TextEmphasis textEmphasis = style.getTextEmphasis(); - // https://www.w3.org/TR/ttml2/#style-value-emphasis-style - // If an implementation does not recognize or otherwise distinguish an emphasis style value, - // then it must be interpreted as if a style of auto were specified; as such, an - // implementation that supports text emphasis marks must minimally support the auto value. - // If a vertical writing mode applies, then equivalent to filled sesame; otherwise, equivalent - // to filled circle. - @TextEmphasisSpan.Mark int mark = textEmphasis.mark; - if (textEmphasis.mark == TextEmphasisSpan.MARK_AUTO - || textEmphasis.mark == TextEmphasisSpan.MARK_UNKNOWN) { + /** + * If an implementation does not recognize or otherwise distinguish an emphasis style value, + * then it must be interpreted as if a style of auto were specified; as such, an + * implementation that supports text emphasis marks must minimally support the auto value. + * If a vertical writing mode applies, then equivalent to filled sesame; otherwise, equivalent + * to filled circle. + * See https://www.w3.org/TR/ttml2/#style-value-emphasis-style + */ + @TextEmphasis.Mark int mark = textEmphasis.mark; + if (textEmphasis.mark == TextEmphasis.MARK_AUTO) { mark = (verticalType == Cue.VERTICAL_TYPE_LR || verticalType == Cue.VERTICAL_TYPE_RL) ? TextEmphasisSpan.MARK_FILLED_SESAME : TextEmphasisSpan.MARK_FILLED_CIRCLE; } + + @TextEmphasis.Position int position = textEmphasis.position; + if (textEmphasis.position == TextEmphasis.POSITION_OUTSIDE) { + /** + * Not supported in current implementation + * 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. + * See https://www.w3.org/TR/ttml2/#style-value-annotation-position + */ + position = TextAnnotation.POSITION_BEFORE; + } + SpanUtil.addOrReplaceSpan( builder, - new TextEmphasisSpan(mark, textEmphasis.position), + new TextEmphasisSpan(mark, position), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); @@ -165,11 +181,11 @@ import java.util.Map; } // TODO: Get rubyPosition from `textNode` when TTML inheritance is implemented. - @RubySpan.Position + @TextAnnotation.Position int rubyPosition = containerNode.style != null ? containerNode.style.getRubyPosition() - : RubySpan.POSITION_UNKNOWN; + : TextAnnotation.POSITION_UNKNOWN; builder.setSpan( new RubySpan(rubyText, rubyPosition), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); break; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/ttml/TtmlStyle.java b/library/core/src/main/java/com/google/android/exoplayer2/text/ttml/TtmlStyle.java index f25defbb8b..b9a9fba98a 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/ttml/TtmlStyle.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/ttml/TtmlStyle.java @@ -20,6 +20,7 @@ import android.text.Layout; import androidx.annotation.IntDef; import androidx.annotation.Nullable; import com.google.android.exoplayer2.text.span.RubySpan; +import com.google.android.exoplayer2.text.span.TextAnnotation; import java.lang.annotation.Documented; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -83,7 +84,7 @@ import java.lang.annotation.RetentionPolicy; private float fontSize; @Nullable private String id; @RubyType private int rubyType; - @RubySpan.Position private int rubyPosition; + @TextAnnotation.Position private int rubyPosition; @Nullable private Layout.Alignment textAlign; @OptionalBoolean private int textCombine; @@ -96,7 +97,7 @@ import java.lang.annotation.RetentionPolicy; italic = UNSPECIFIED; fontSizeUnit = UNSPECIFIED; rubyType = UNSPECIFIED; - rubyPosition = RubySpan.POSITION_UNKNOWN; + rubyPosition = TextAnnotation.POSITION_UNKNOWN; textCombine = UNSPECIFIED; } @@ -227,7 +228,7 @@ import java.lang.annotation.RetentionPolicy; if (underline == UNSPECIFIED) { underline = ancestor.underline; } - if (rubyPosition == RubySpan.POSITION_UNKNOWN) { + if (rubyPosition == TextAnnotation.POSITION_UNKNOWN) { rubyPosition = ancestor.rubyPosition; } if (textAlign == null && ancestor.textAlign != null) { @@ -274,12 +275,12 @@ import java.lang.annotation.RetentionPolicy; return rubyType; } - public TtmlStyle setRubyPosition(@RubySpan.Position int position) { + public TtmlStyle setRubyPosition(@TextAnnotation.Position int position) { this.rubyPosition = position; return this; } - @RubySpan.Position + @TextAnnotation.Position public int getRubyPosition() { return rubyPosition; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/CssParser.java b/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/CssParser.java index 40fb1fcbb2..a9ecdc683b 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/CssParser.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/CssParser.java @@ -18,6 +18,7 @@ package com.google.android.exoplayer2.text.webvtt; import android.text.TextUtils; import androidx.annotation.Nullable; import com.google.android.exoplayer2.text.span.RubySpan; +import com.google.android.exoplayer2.text.span.TextAnnotation; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.ColorParser; import com.google.android.exoplayer2.util.ParsableByteArray; @@ -195,9 +196,9 @@ import java.util.regex.Pattern; style.setBackgroundColor(ColorParser.parseCssColor(value)); } else if (PROPERTY_RUBY_POSITION.equals(property)) { if (VALUE_OVER.equals(value)) { - style.setRubyPosition(RubySpan.POSITION_OVER); + style.setRubyPosition(TextAnnotation.POSITION_BEFORE); } else if (VALUE_UNDER.equals(value)) { - style.setRubyPosition(RubySpan.POSITION_UNDER); + style.setRubyPosition(TextAnnotation.POSITION_AFTER); } } else if (PROPERTY_TEXT_COMBINE_UPRIGHT.equals(property)) { style.setCombineUpright(VALUE_ALL.equals(value) || value.startsWith(VALUE_DIGITS)); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttCssStyle.java b/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttCssStyle.java index eeb3392e54..232c50130c 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttCssStyle.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttCssStyle.java @@ -21,6 +21,7 @@ import androidx.annotation.ColorInt; import androidx.annotation.IntDef; import androidx.annotation.Nullable; import com.google.android.exoplayer2.text.span.RubySpan; +import com.google.android.exoplayer2.text.span.TextAnnotation; import com.google.android.exoplayer2.util.Util; import java.lang.annotation.Documented; import java.lang.annotation.Retention; @@ -95,7 +96,7 @@ public final class WebvttCssStyle { @OptionalBoolean private int italic; @FontSizeUnit private int fontSizeUnit; private float fontSize; - @RubySpan.Position private int rubyPosition; + @TextAnnotation.Position private int rubyPosition; private boolean combineUpright; public WebvttCssStyle() { @@ -111,7 +112,7 @@ public final class WebvttCssStyle { bold = UNSPECIFIED; italic = UNSPECIFIED; fontSizeUnit = UNSPECIFIED; - rubyPosition = RubySpan.POSITION_UNKNOWN; + rubyPosition = TextAnnotation.POSITION_UNKNOWN; combineUpright = false; } @@ -272,12 +273,12 @@ public final class WebvttCssStyle { return fontSize; } - public WebvttCssStyle setRubyPosition(@RubySpan.Position int rubyPosition) { + public WebvttCssStyle setRubyPosition(@TextAnnotation.Position int rubyPosition) { this.rubyPosition = rubyPosition; return this; } - @RubySpan.Position + @TextAnnotation.Position public int getRubyPosition() { return rubyPosition; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttCueParser.java b/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttCueParser.java index ed95f6b4e0..a040a3acf3 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttCueParser.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttCueParser.java @@ -39,6 +39,7 @@ import androidx.annotation.Nullable; import com.google.android.exoplayer2.text.Cue; import com.google.android.exoplayer2.text.span.HorizontalTextInVerticalContextSpan; import com.google.android.exoplayer2.text.span.RubySpan; +import com.google.android.exoplayer2.text.span.TextAnnotation; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Log; import com.google.android.exoplayer2.util.ParsableByteArray; @@ -572,7 +573,7 @@ public final class WebvttCueParser { StartTag startTag, List nestedElements, List styles) { - @RubySpan.Position int rubyTagPosition = getRubyPosition(styles, cueId, startTag); + @TextAnnotation.Position int rubyTagPosition = getRubyPosition(styles, cueId, startTag); List sortedNestedElements = new ArrayList<>(nestedElements.size()); sortedNestedElements.addAll(nestedElements); Collections.sort(sortedNestedElements, Element.BY_START_POSITION_ASC); @@ -585,12 +586,12 @@ public final class WebvttCueParser { Element rubyTextElement = sortedNestedElements.get(i); // Use the element's ruby-position if set, otherwise the element's and otherwise // default to OVER. - @RubySpan.Position + @TextAnnotation.Position int rubyPosition = firstKnownRubyPosition( getRubyPosition(styles, cueId, rubyTextElement.startTag), rubyTagPosition, - RubySpan.POSITION_OVER); + TextAnnotation.POSITION_BEFORE); // Move the rubyText from spannedText into the RubySpan. int adjustedRubyTextStart = rubyTextElement.startTag.position - deletedCharCount; int adjustedRubyTextEnd = rubyTextElement.endPosition - deletedCharCount; @@ -607,31 +608,31 @@ public final class WebvttCueParser { } } - @RubySpan.Position + @TextAnnotation.Position private static int getRubyPosition( List styles, @Nullable String cueId, StartTag startTag) { List styleMatches = getApplicableStyles(styles, cueId, startTag); for (int i = 0; i < styleMatches.size(); i++) { WebvttCssStyle style = styleMatches.get(i).style; - if (style.getRubyPosition() != RubySpan.POSITION_UNKNOWN) { + if (style.getRubyPosition() != TextAnnotation.POSITION_UNKNOWN) { return style.getRubyPosition(); } } - return RubySpan.POSITION_UNKNOWN; + return TextAnnotation.POSITION_UNKNOWN; } - @RubySpan.Position + @TextAnnotation.Position private static int firstKnownRubyPosition( - @RubySpan.Position int position1, - @RubySpan.Position int position2, - @RubySpan.Position int position3) { - if (position1 != RubySpan.POSITION_UNKNOWN) { + @TextAnnotation.Position int position1, + @TextAnnotation.Position int position2, + @TextAnnotation.Position int position3) { + if (position1 != TextAnnotation.POSITION_UNKNOWN) { return position1; } - if (position2 != RubySpan.POSITION_UNKNOWN) { + if (position2 != TextAnnotation.POSITION_UNKNOWN) { return position2; } - if (position3 != RubySpan.POSITION_UNKNOWN) { + if (position3 != TextAnnotation.POSITION_UNKNOWN) { return position3; } throw new IllegalArgumentException(); diff --git a/library/core/src/test/java/com/google/android/exoplayer2/text/ttml/TextEmphasisTest.java b/library/core/src/test/java/com/google/android/exoplayer2/text/ttml/TextEmphasisTest.java index 6d50cdf08b..e273a0d049 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/text/ttml/TextEmphasisTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/text/ttml/TextEmphasisTest.java @@ -5,8 +5,8 @@ import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assertWithMessage; import androidx.test.ext.junit.runners.AndroidJUnit4; +import com.google.android.exoplayer2.text.span.TextAnnotation; import com.google.android.exoplayer2.text.span.TextEmphasisSpan; -import com.google.android.exoplayer2.util.Log; import org.junit.Test; import org.junit.runner.RunWith; @@ -21,7 +21,6 @@ public class TextEmphasisTest { String value = null; TextEmphasis textEmphasis = createTextEmphasis(value); - Log.d(TAG, "textEmphasis: " + textEmphasis); assertWithMessage("Text Emphasis must be null").that(textEmphasis).isNull(); } @@ -30,7 +29,6 @@ public class TextEmphasisTest { String value = ""; TextEmphasis textEmphasis = createTextEmphasis(value); - Log.d(TAG, "textEmphasis: " + textEmphasis); assertWithMessage("Text Emphasis must be null").that(textEmphasis).isNull(); } @@ -39,7 +37,6 @@ public class TextEmphasisTest { String value = "none"; TextEmphasis textEmphasis = createTextEmphasis(value); - Log.d(TAG, "textEmphasis: " + textEmphasis); assertWithMessage("Text Emphasis must be null").that(textEmphasis).isNull(); } @@ -48,10 +45,9 @@ public class TextEmphasisTest { String value = "auto"; TextEmphasis textEmphasis = createTextEmphasis(value); - Log.d(TAG, "textEmphasis: " + textEmphasis); assertWithMessage("Text Emphasis must exist").that(textEmphasis).isNotNull(); - assertThat(textEmphasis.mark).isEqualTo(TextEmphasisSpan.MARK_AUTO); - assertThat(textEmphasis.position).isEqualTo(TextEmphasisSpan.POSITION_UNKNOWN); + assertThat(textEmphasis.mark).isEqualTo(TextEmphasis.MARK_AUTO); + assertThat(textEmphasis.position).isEqualTo(TextEmphasis.POSITION_OUTSIDE); } @Test @@ -59,26 +55,57 @@ public class TextEmphasisTest { String value = "auto outside"; TextEmphasis textEmphasis = createTextEmphasis(value); - Log.d(TAG, "textEmphasis: " + textEmphasis); assertWithMessage("Text Emphasis must exist").that(textEmphasis).isNotNull(); - assertThat(textEmphasis.mark).isEqualTo(TextEmphasisSpan.MARK_AUTO); - assertThat(textEmphasis.position).isEqualTo(TextEmphasisSpan.POSITION_OUTSIDE); + assertThat(textEmphasis.mark).isEqualTo(TextEmphasis.MARK_AUTO); + assertThat(textEmphasis.position).isEqualTo(TextEmphasis.POSITION_OUTSIDE); + } + + /** + * If only filled or open is specified, then it is equivalent to filled circle and open circle, + * respectively. + */ + @Test + public void testFilled() { + String value = "filled"; + TextEmphasis textEmphasis = createTextEmphasis(value); + + assertWithMessage("Text Emphasis must exist").that(textEmphasis).isNotNull(); + assertThat(textEmphasis.mark).isEqualTo(TextEmphasisSpan.MARK_FILLED_CIRCLE); + assertThat(textEmphasis.position).isEqualTo(TextEmphasis.POSITION_OUTSIDE); + } + + @Test + public void testOpen() { + String value = "open"; + TextEmphasis textEmphasis = createTextEmphasis(value); + + assertWithMessage("Text Emphasis must exist").that(textEmphasis).isNotNull(); + assertThat(textEmphasis.mark).isEqualTo(TextEmphasisSpan.MARK_OPEN_CIRCLE); + assertThat(textEmphasis.position).isEqualTo(TextEmphasis.POSITION_OUTSIDE); + } + + @Test + public void testOpenAfter() { + String value = "open after"; + TextEmphasis textEmphasis = createTextEmphasis(value); + + assertWithMessage("Text Emphasis must exist").that(textEmphasis).isNotNull(); + assertThat(textEmphasis.mark).isEqualTo(TextEmphasisSpan.MARK_OPEN_CIRCLE); + assertThat(textEmphasis.position).isEqualTo(TextAnnotation.POSITION_AFTER); } /** * If only circle, dot, or sesame is specified, then it is equivalent to filled circle, filled dot, * and filled sesame, respectively. */ - @Test public void testDotBefore() { String value = "dot before"; TextEmphasis textEmphasis = createTextEmphasis(value); - Log.d(TAG, "textEmphasis: " + textEmphasis); assertWithMessage("Text Emphasis must exist").that(textEmphasis).isNotNull(); assertThat(textEmphasis.mark).isEqualTo(TextEmphasisSpan.MARK_FILLED_DOT); - assertThat(textEmphasis.position).isEqualTo(TextEmphasisSpan.POSITION_BEFORE); + assertThat(textEmphasis.position).isEqualTo(TextAnnotation.POSITION_BEFORE); } @Test @@ -86,10 +113,9 @@ public class TextEmphasisTest { String value = "circle before"; TextEmphasis textEmphasis = createTextEmphasis(value); - Log.d(TAG, "textEmphasis: " + textEmphasis); assertWithMessage("Text Emphasis must exist").that(textEmphasis).isNotNull(); assertThat(textEmphasis.mark).isEqualTo(TextEmphasisSpan.MARK_FILLED_CIRCLE); - assertThat(textEmphasis.position).isEqualTo(TextEmphasisSpan.POSITION_BEFORE); + assertThat(textEmphasis.position).isEqualTo(TextAnnotation.POSITION_BEFORE); } @Test @@ -97,10 +123,9 @@ public class TextEmphasisTest { String value = "sesame before"; TextEmphasis textEmphasis = createTextEmphasis(value); - Log.d(TAG, "textEmphasis: " + textEmphasis); assertWithMessage("Text Emphasis must exist").that(textEmphasis).isNotNull(); assertThat(textEmphasis.mark).isEqualTo(TextEmphasisSpan.MARK_FILLED_SESAME); - assertThat(textEmphasis.position).isEqualTo(TextEmphasisSpan.POSITION_BEFORE); + assertThat(textEmphasis.position).isEqualTo(TextAnnotation.POSITION_BEFORE); } @Test @@ -108,10 +133,9 @@ public class TextEmphasisTest { String value = "dot AFTER"; TextEmphasis textEmphasis = createTextEmphasis(value); - Log.d(TAG, "textEmphasis: " + textEmphasis); assertWithMessage("Text Emphasis must exist").that(textEmphasis).isNotNull(); assertThat(textEmphasis.mark).isEqualTo(TextEmphasisSpan.MARK_FILLED_DOT); - assertThat(textEmphasis.position).isEqualTo(TextEmphasisSpan.POSITION_AFTER); + assertThat(textEmphasis.position).isEqualTo(TextAnnotation.POSITION_AFTER); } @Test @@ -119,10 +143,9 @@ public class TextEmphasisTest { String value = "circle after"; TextEmphasis textEmphasis = createTextEmphasis(value); - Log.d(TAG, "textEmphasis: " + textEmphasis); assertWithMessage("Text Emphasis must exist").that(textEmphasis).isNotNull(); assertThat(textEmphasis.mark).isEqualTo(TextEmphasisSpan.MARK_FILLED_CIRCLE); - assertThat(textEmphasis.position).isEqualTo(TextEmphasisSpan.POSITION_AFTER); + assertThat(textEmphasis.position).isEqualTo(TextAnnotation.POSITION_AFTER); } @Test @@ -130,10 +153,9 @@ public class TextEmphasisTest { String value = "sesame aFter"; TextEmphasis textEmphasis = createTextEmphasis(value); - Log.d(TAG, "textEmphasis: " + textEmphasis); assertWithMessage("Text Emphasis must exist").that(textEmphasis).isNotNull(); assertThat(textEmphasis.mark).isEqualTo(TextEmphasisSpan.MARK_FILLED_SESAME); - assertThat(textEmphasis.position).isEqualTo(TextEmphasisSpan.POSITION_AFTER); + assertThat(textEmphasis.position).isEqualTo(TextAnnotation.POSITION_AFTER); } @Test @@ -141,10 +163,9 @@ public class TextEmphasisTest { String value = "dot outside"; TextEmphasis textEmphasis = createTextEmphasis(value); - Log.d(TAG, "textEmphasis: " + textEmphasis); assertWithMessage("Text Emphasis must exist").that(textEmphasis).isNotNull(); assertThat(textEmphasis.mark).isEqualTo(TextEmphasisSpan.MARK_FILLED_DOT); - assertThat(textEmphasis.position).isEqualTo(TextEmphasisSpan.POSITION_OUTSIDE); + assertThat(textEmphasis.position).isEqualTo(TextEmphasis.POSITION_OUTSIDE); } @Test @@ -152,10 +173,9 @@ public class TextEmphasisTest { String value = "circle outside"; TextEmphasis textEmphasis = createTextEmphasis(value); - Log.d(TAG, "textEmphasis: " + textEmphasis); assertWithMessage("Text Emphasis must exist").that(textEmphasis).isNotNull(); assertThat(textEmphasis.mark).isEqualTo(TextEmphasisSpan.MARK_FILLED_CIRCLE); - assertThat(textEmphasis.position).isEqualTo(TextEmphasisSpan.POSITION_OUTSIDE); + assertThat(textEmphasis.position).isEqualTo(TextEmphasis.POSITION_OUTSIDE); } @Test @@ -163,10 +183,9 @@ public class TextEmphasisTest { String value = "sesame outside"; TextEmphasis textEmphasis = createTextEmphasis(value); - Log.d(TAG, "textEmphasis: " + textEmphasis); assertWithMessage("Text Emphasis must exist").that(textEmphasis).isNotNull(); assertThat(textEmphasis.mark).isEqualTo(TextEmphasisSpan.MARK_FILLED_SESAME); - assertThat(textEmphasis.position).isEqualTo(TextEmphasisSpan.POSITION_OUTSIDE); + assertThat(textEmphasis.position).isEqualTo(TextEmphasis.POSITION_OUTSIDE); } @Test @@ -174,10 +193,9 @@ public class TextEmphasisTest { String value = "open dot AFTER"; TextEmphasis textEmphasis = createTextEmphasis(value); - Log.d(TAG, "textEmphasis: " + textEmphasis); assertWithMessage("Text Emphasis must exist").that(textEmphasis).isNotNull(); assertThat(textEmphasis.mark).isEqualTo(TextEmphasisSpan.MARK_OPEN_DOT); - assertThat(textEmphasis.position).isEqualTo(TextEmphasisSpan.POSITION_AFTER); + assertThat(textEmphasis.position).isEqualTo(TextAnnotation.POSITION_AFTER); } @Test @@ -185,10 +203,9 @@ public class TextEmphasisTest { String value = "Open circle after"; TextEmphasis textEmphasis = createTextEmphasis(value); - Log.d(TAG, "textEmphasis: " + textEmphasis); assertWithMessage("Text Emphasis must exist").that(textEmphasis).isNotNull(); assertThat(textEmphasis.mark).isEqualTo(TextEmphasisSpan.MARK_OPEN_CIRCLE); - assertThat(textEmphasis.position).isEqualTo(TextEmphasisSpan.POSITION_AFTER); + assertThat(textEmphasis.position).isEqualTo(TextAnnotation.POSITION_AFTER); } @Test @@ -196,10 +213,9 @@ public class TextEmphasisTest { String value = "open sesame aFter"; TextEmphasis textEmphasis = createTextEmphasis(value); - Log.d(TAG, "textEmphasis: " + textEmphasis); assertWithMessage("Text Emphasis must exist").that(textEmphasis).isNotNull(); assertThat(textEmphasis.mark).isEqualTo(TextEmphasisSpan.MARK_OPEN_SESAME); - assertThat(textEmphasis.position).isEqualTo(TextEmphasisSpan.POSITION_AFTER); + assertThat(textEmphasis.position).isEqualTo(TextAnnotation.POSITION_AFTER); } @Test @@ -207,10 +223,9 @@ public class TextEmphasisTest { String value = "open dot before"; TextEmphasis textEmphasis = createTextEmphasis(value); - Log.d(TAG, "textEmphasis: " + textEmphasis); assertWithMessage("Text Emphasis must exist").that(textEmphasis).isNotNull(); assertThat(textEmphasis.mark).isEqualTo(TextEmphasisSpan.MARK_OPEN_DOT); - assertThat(textEmphasis.position).isEqualTo(TextEmphasisSpan.POSITION_BEFORE); + assertThat(textEmphasis.position).isEqualTo(TextAnnotation.POSITION_BEFORE); } @Test @@ -218,10 +233,9 @@ public class TextEmphasisTest { String value = "Open circle Before"; TextEmphasis textEmphasis = createTextEmphasis(value); - Log.d(TAG, "textEmphasis: " + textEmphasis); assertWithMessage("Text Emphasis must exist").that(textEmphasis).isNotNull(); assertThat(textEmphasis.mark).isEqualTo(TextEmphasisSpan.MARK_OPEN_CIRCLE); - assertThat(textEmphasis.position).isEqualTo(TextEmphasisSpan.POSITION_BEFORE); + assertThat(textEmphasis.position).isEqualTo(TextAnnotation.POSITION_BEFORE); } @Test @@ -229,10 +243,9 @@ public class TextEmphasisTest { String value = "open sesame Before"; TextEmphasis textEmphasis = createTextEmphasis(value); - Log.d(TAG, "textEmphasis: " + textEmphasis); assertWithMessage("Text Emphasis must exist").that(textEmphasis).isNotNull(); assertThat(textEmphasis.mark).isEqualTo(TextEmphasisSpan.MARK_OPEN_SESAME); - assertThat(textEmphasis.position).isEqualTo(TextEmphasisSpan.POSITION_BEFORE); + assertThat(textEmphasis.position).isEqualTo(TextAnnotation.POSITION_BEFORE); } @Test @@ -240,10 +253,9 @@ public class TextEmphasisTest { String value = "open dot Outside"; TextEmphasis textEmphasis = createTextEmphasis(value); - Log.d(TAG, "textEmphasis: " + textEmphasis); assertWithMessage("Text Emphasis must exist").that(textEmphasis).isNotNull(); assertThat(textEmphasis.mark).isEqualTo(TextEmphasisSpan.MARK_OPEN_DOT); - assertThat(textEmphasis.position).isEqualTo(TextEmphasisSpan.POSITION_OUTSIDE); + assertThat(textEmphasis.position).isEqualTo(TextEmphasis.POSITION_OUTSIDE); } @Test @@ -251,10 +263,9 @@ public class TextEmphasisTest { String value = "Open circle Outside"; TextEmphasis textEmphasis = createTextEmphasis(value); - Log.d(TAG, "textEmphasis: " + textEmphasis); assertWithMessage("Text Emphasis must exist").that(textEmphasis).isNotNull(); assertThat(textEmphasis.mark).isEqualTo(TextEmphasisSpan.MARK_OPEN_CIRCLE); - assertThat(textEmphasis.position).isEqualTo(TextEmphasisSpan.POSITION_OUTSIDE); + assertThat(textEmphasis.position).isEqualTo(TextEmphasis.POSITION_OUTSIDE); } @Test @@ -262,10 +273,9 @@ public class TextEmphasisTest { String value = "open sesame outside"; TextEmphasis textEmphasis = createTextEmphasis(value); - Log.d(TAG, "textEmphasis: " + textEmphasis); assertWithMessage("Text Emphasis must exist").that(textEmphasis).isNotNull(); assertThat(textEmphasis.mark).isEqualTo(TextEmphasisSpan.MARK_OPEN_SESAME); - assertThat(textEmphasis.position).isEqualTo(TextEmphasisSpan.POSITION_OUTSIDE); + assertThat(textEmphasis.position).isEqualTo(TextEmphasis.POSITION_OUTSIDE); } @Test @@ -273,10 +283,9 @@ public class TextEmphasisTest { String value = "filled dot outside"; TextEmphasis textEmphasis = createTextEmphasis(value); - Log.d(TAG, "textEmphasis: " + textEmphasis); assertWithMessage("Text Emphasis must exist").that(textEmphasis).isNotNull(); assertThat(textEmphasis.mark).isEqualTo(TextEmphasisSpan.MARK_FILLED_DOT); - assertThat(textEmphasis.position).isEqualTo(TextEmphasisSpan.POSITION_OUTSIDE); + assertThat(textEmphasis.position).isEqualTo(TextEmphasis.POSITION_OUTSIDE); } @Test @@ -284,10 +293,9 @@ public class TextEmphasisTest { String value = "filled circle outside"; TextEmphasis textEmphasis = createTextEmphasis(value); - Log.d(TAG, "textEmphasis: " + textEmphasis); assertWithMessage("Text Emphasis must exist").that(textEmphasis).isNotNull(); assertThat(textEmphasis.mark).isEqualTo(TextEmphasisSpan.MARK_FILLED_CIRCLE); - assertThat(textEmphasis.position).isEqualTo(TextEmphasisSpan.POSITION_OUTSIDE); + assertThat(textEmphasis.position).isEqualTo(TextEmphasis.POSITION_OUTSIDE); } @Test @@ -295,10 +303,9 @@ public class TextEmphasisTest { String value = "filled sesame outside"; TextEmphasis textEmphasis = createTextEmphasis(value); - Log.d(TAG, "textEmphasis: " + textEmphasis); assertWithMessage("Text Emphasis must exist").that(textEmphasis).isNotNull(); assertThat(textEmphasis.mark).isEqualTo(TextEmphasisSpan.MARK_FILLED_SESAME); - assertThat(textEmphasis.position).isEqualTo(TextEmphasisSpan.POSITION_OUTSIDE); + assertThat(textEmphasis.position).isEqualTo(TextEmphasis.POSITION_OUTSIDE); } @Test @@ -306,10 +313,9 @@ public class TextEmphasisTest { String value = "filled dot After"; TextEmphasis textEmphasis = createTextEmphasis(value); - Log.d(TAG, "textEmphasis: " + textEmphasis); assertWithMessage("Text Emphasis must exist").that(textEmphasis).isNotNull(); assertThat(textEmphasis.mark).isEqualTo(TextEmphasisSpan.MARK_FILLED_DOT); - assertThat(textEmphasis.position).isEqualTo(TextEmphasisSpan.POSITION_AFTER); + assertThat(textEmphasis.position).isEqualTo(TextAnnotation.POSITION_AFTER); } @Test @@ -317,10 +323,9 @@ public class TextEmphasisTest { String value = "filled circle after"; TextEmphasis textEmphasis = createTextEmphasis(value); - Log.d(TAG, "textEmphasis: " + textEmphasis); assertWithMessage("Text Emphasis must exist").that(textEmphasis).isNotNull(); assertThat(textEmphasis.mark).isEqualTo(TextEmphasisSpan.MARK_FILLED_CIRCLE); - assertThat(textEmphasis.position).isEqualTo(TextEmphasisSpan.POSITION_AFTER); + assertThat(textEmphasis.position).isEqualTo(TextAnnotation.POSITION_AFTER); } @Test @@ -328,10 +333,9 @@ public class TextEmphasisTest { String value = "filled sesame After"; TextEmphasis textEmphasis = createTextEmphasis(value); - Log.d(TAG, "textEmphasis: " + textEmphasis); assertWithMessage("Text Emphasis must exist").that(textEmphasis).isNotNull(); assertThat(textEmphasis.mark).isEqualTo(TextEmphasisSpan.MARK_FILLED_SESAME); - assertThat(textEmphasis.position).isEqualTo(TextEmphasisSpan.POSITION_AFTER); + assertThat(textEmphasis.position).isEqualTo(TextAnnotation.POSITION_AFTER); } @Test @@ -339,10 +343,9 @@ public class TextEmphasisTest { String value = "filled dot before"; TextEmphasis textEmphasis = createTextEmphasis(value); - Log.d(TAG, "textEmphasis: " + textEmphasis); assertWithMessage("Text Emphasis must exist").that(textEmphasis).isNotNull(); assertThat(textEmphasis.mark).isEqualTo(TextEmphasisSpan.MARK_FILLED_DOT); - assertThat(textEmphasis.position).isEqualTo(TextEmphasisSpan.POSITION_BEFORE); + assertThat(textEmphasis.position).isEqualTo(TextAnnotation.POSITION_BEFORE); } @Test @@ -350,10 +353,9 @@ public class TextEmphasisTest { String value = "filled circle Before"; TextEmphasis textEmphasis = createTextEmphasis(value); - Log.d(TAG, "textEmphasis: " + textEmphasis); assertWithMessage("Text Emphasis must exist").that(textEmphasis).isNotNull(); assertThat(textEmphasis.mark).isEqualTo(TextEmphasisSpan.MARK_FILLED_CIRCLE); - assertThat(textEmphasis.position).isEqualTo(TextEmphasisSpan.POSITION_BEFORE); + assertThat(textEmphasis.position).isEqualTo(TextAnnotation.POSITION_BEFORE); } @Test @@ -361,9 +363,28 @@ public class TextEmphasisTest { String value = "filled sesame Before"; TextEmphasis textEmphasis = createTextEmphasis(value); - Log.d(TAG, "textEmphasis: " + textEmphasis); assertWithMessage("Text Emphasis must exist").that(textEmphasis).isNotNull(); assertThat(textEmphasis.mark).isEqualTo(TextEmphasisSpan.MARK_FILLED_SESAME); - assertThat(textEmphasis.position).isEqualTo(TextEmphasisSpan.POSITION_BEFORE); + assertThat(textEmphasis.position).isEqualTo(TextAnnotation.POSITION_BEFORE); + } + + @Test + public void testBeforeFilledSesame() { + String value = "before filled sesame"; + TextEmphasis textEmphasis = createTextEmphasis(value); + + assertWithMessage("Text Emphasis must exist").that(textEmphasis).isNotNull(); + assertThat(textEmphasis.mark).isEqualTo(TextEmphasisSpan.MARK_FILLED_SESAME); + assertThat(textEmphasis.position).isEqualTo(TextAnnotation.POSITION_BEFORE); + } + + @Test + public void testBeforeSesameFilled() { + String value = "before sesame filled"; + TextEmphasis textEmphasis = createTextEmphasis(value); + + assertWithMessage("Text Emphasis must exist").that(textEmphasis).isNotNull(); + assertThat(textEmphasis.mark).isEqualTo(TextEmphasisSpan.MARK_FILLED_SESAME); + assertThat(textEmphasis.position).isEqualTo(TextAnnotation.POSITION_BEFORE); } } diff --git a/library/core/src/test/java/com/google/android/exoplayer2/text/ttml/TtmlDecoderTest.java b/library/core/src/test/java/com/google/android/exoplayer2/text/ttml/TtmlDecoderTest.java index fa4c28aaf1..c4245572be 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/text/ttml/TtmlDecoderTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/text/ttml/TtmlDecoderTest.java @@ -28,6 +28,7 @@ import com.google.android.exoplayer2.text.Cue; import com.google.android.exoplayer2.text.Subtitle; import com.google.android.exoplayer2.text.SubtitleDecoderException; import com.google.android.exoplayer2.text.span.RubySpan; +import com.google.android.exoplayer2.text.span.TextAnnotation; import com.google.android.exoplayer2.text.span.TextEmphasisSpan; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.ColorParser; @@ -648,16 +649,16 @@ public final class TtmlDecoderTest { assertThat(firstCue.toString()).isEqualTo("Cue with annotated text."); assertThat(firstCue) .hasRubySpanBetween("Cue with ".length(), "Cue with annotated".length()) - .withTextAndPosition("1st rubies", RubySpan.POSITION_OVER); + .withTextAndPosition("1st rubies", TextAnnotation.POSITION_BEFORE); assertThat(firstCue) .hasRubySpanBetween("Cue with annotated ".length(), "Cue with annotated text".length()) - .withTextAndPosition("2nd rubies", RubySpan.POSITION_UNKNOWN); + .withTextAndPosition("2nd rubies", TextAnnotation.POSITION_UNKNOWN); Spanned secondCue = getOnlyCueTextAtTimeUs(subtitle, 20_000_000); assertThat(secondCue.toString()).isEqualTo("Cue with annotated text."); assertThat(secondCue) .hasRubySpanBetween("Cue with ".length(), "Cue with annotated".length()) - .withTextAndPosition("rubies", RubySpan.POSITION_UNKNOWN); + .withTextAndPosition("rubies", TextAnnotation.POSITION_UNKNOWN); Spanned thirdCue = getOnlyCueTextAtTimeUs(subtitle, 30_000_000); assertThat(thirdCue.toString()).isEqualTo("Cue with annotated text."); @@ -681,69 +682,68 @@ public final class TtmlDecoderTest { TtmlSubtitle subtitle = getSubtitle(TEXT_EMPHASIS_FILE); Spanned firstCue = getOnlyCueTextAtTimeUs(subtitle, 10_000_000); - assertThat(firstCue) - .hasNoTextEmphasisSpanBetween("None ".length(), "None おはよ".length()); + assertThat(firstCue).hasNoSpans(); Spanned secondCue = getOnlyCueTextAtTimeUs(subtitle, 20_000_000); assertThat(secondCue) .hasTextEmphasisSpanBetween("Auto ".length(), "Auto ございます".length()) .withMarkAndPosition(TextEmphasisSpan.MARK_FILLED_CIRCLE, - TextEmphasisSpan.POSITION_UNKNOWN); + TextAnnotation.POSITION_BEFORE); Spanned thirdCue = getOnlyCueTextAtTimeUs(subtitle, 30_000_000); assertThat(thirdCue) .hasTextEmphasisSpanBetween("Filled circle ".length(), "Filled circle こんばんは".length()) .withMarkAndPosition(TextEmphasisSpan.MARK_FILLED_CIRCLE, - TextEmphasisSpan.POSITION_UNKNOWN); + TextAnnotation.POSITION_BEFORE); Spanned fourthCue = getOnlyCueTextAtTimeUs(subtitle, 40_000_000); assertThat(fourthCue) .hasTextEmphasisSpanBetween("Filled dot ".length(), "Filled dot ございます".length()) - .withMarkAndPosition(TextEmphasisSpan.MARK_FILLED_DOT, TextEmphasisSpan.POSITION_UNKNOWN); + .withMarkAndPosition(TextEmphasisSpan.MARK_FILLED_DOT, TextAnnotation.POSITION_BEFORE); Spanned fifthCue = getOnlyCueTextAtTimeUs(subtitle, 50_000_000); assertThat(fifthCue) .hasTextEmphasisSpanBetween("Filled sesame ".length(), "Filled sesame おはよ".length()) .withMarkAndPosition(TextEmphasisSpan.MARK_FILLED_SESAME, - TextEmphasisSpan.POSITION_UNKNOWN); + TextAnnotation.POSITION_BEFORE); 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); + .withMarkAndPosition(TextEmphasisSpan.MARK_OPEN_CIRCLE, TextAnnotation.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); + .withMarkAndPosition(TextEmphasisSpan.MARK_OPEN_DOT, TextAnnotation.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); + .withMarkAndPosition(TextEmphasisSpan.MARK_OPEN_SESAME, TextAnnotation.POSITION_BEFORE); Spanned ninthCue = getOnlyCueTextAtTimeUs(subtitle, 90_000_000); assertThat(ninthCue) .hasTextEmphasisSpanBetween("Auto outside ".length(), "Auto outside おはよ".length()) .withMarkAndPosition(TextEmphasisSpan.MARK_FILLED_CIRCLE, - TextEmphasisSpan.POSITION_OUTSIDE); + TextAnnotation.POSITION_BEFORE); Spanned tenthCue = getOnlyCueTextAtTimeUs(subtitle, 100_000_000); assertThat(tenthCue) .hasTextEmphasisSpanBetween("Circle before ".length(), "Circle before ございます".length()) - .withMarkAndPosition(TextEmphasisSpan.MARK_FILLED_CIRCLE, TextEmphasisSpan.POSITION_BEFORE); + .withMarkAndPosition(TextEmphasisSpan.MARK_FILLED_CIRCLE, TextAnnotation.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); + .withMarkAndPosition(TextEmphasisSpan.MARK_FILLED_SESAME, TextAnnotation.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); + .withMarkAndPosition(TextEmphasisSpan.MARK_FILLED_DOT, TextAnnotation.POSITION_BEFORE); Spanned thirteenthCue = getOnlyCueTextAtTimeUs(subtitle, 130_000_000); assertThat(thirteenthCue) @@ -754,25 +754,25 @@ public final class TtmlDecoderTest { assertThat(fourteenthCue) .hasTextEmphasisSpanBetween("Auto (TBLR) ".length(), "Auto (TBLR) ございます".length()) .withMarkAndPosition(TextEmphasisSpan.MARK_FILLED_SESAME, - TextEmphasisSpan.POSITION_UNKNOWN); + TextAnnotation.POSITION_BEFORE); Spanned fifteenthCue = getOnlyCueTextAtTimeUs(subtitle, 150_000_000); assertThat(fifteenthCue) .hasTextEmphasisSpanBetween("Auto (TBRL) ".length(), "Auto (TBRL) おはよ".length()) .withMarkAndPosition(TextEmphasisSpan.MARK_FILLED_SESAME, - TextEmphasisSpan.POSITION_UNKNOWN); + TextAnnotation.POSITION_BEFORE); Spanned sixteenthCue = getOnlyCueTextAtTimeUs(subtitle, 160_000_000); assertThat(sixteenthCue) .hasTextEmphasisSpanBetween("Auto (TB) ".length(), "Auto (TB) ございます".length()) .withMarkAndPosition(TextEmphasisSpan.MARK_FILLED_SESAME, - TextEmphasisSpan.POSITION_UNKNOWN); + TextAnnotation.POSITION_BEFORE); Spanned seventeenthCue = getOnlyCueTextAtTimeUs(subtitle, 170_000_000); assertThat(seventeenthCue) .hasTextEmphasisSpanBetween("Auto (LR) ".length(), "Auto (LR) おはよ".length()) .withMarkAndPosition(TextEmphasisSpan.MARK_FILLED_CIRCLE, - TextEmphasisSpan.POSITION_UNKNOWN); + TextAnnotation.POSITION_BEFORE); } private static Spanned getOnlyCueTextAtTimeUs(Subtitle subtitle, long timeUs) { 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 60409315ef..8f6556c463 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,10 +16,9 @@ package com.google.android.exoplayer2.text.ttml; import static android.graphics.Color.BLACK; +import static com.google.android.exoplayer2.text.span.TextAnnotation.POSITION_BEFORE; 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; @@ -32,7 +31,7 @@ import android.graphics.Color; import android.text.Layout; import androidx.annotation.ColorInt; import androidx.test.ext.junit.runners.AndroidJUnit4; -import com.google.android.exoplayer2.text.span.RubySpan; +import com.google.android.exoplayer2.text.span.TextAnnotation; import org.junit.Test; import org.junit.runner.RunWith; @@ -47,7 +46,7 @@ public final class TtmlStyleTest { @TtmlStyle.FontSizeUnit private static final int FONT_SIZE_UNIT = TtmlStyle.FONT_SIZE_UNIT_EM; @ColorInt private static final int BACKGROUND_COLOR = Color.BLACK; private static final int RUBY_TYPE = TtmlStyle.RUBY_TYPE_TEXT; - private static final int RUBY_POSITION = RubySpan.POSITION_UNDER; + private static final int RUBY_POSITION = TextAnnotation.POSITION_AFTER; 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"; @@ -233,9 +232,9 @@ public final class TtmlStyleTest { public void rubyPosition() { TtmlStyle style = new TtmlStyle(); - assertThat(style.getRubyPosition()).isEqualTo(RubySpan.POSITION_UNKNOWN); - style.setRubyPosition(RubySpan.POSITION_OVER); - assertThat(style.getRubyPosition()).isEqualTo(RubySpan.POSITION_OVER); + assertThat(style.getRubyPosition()).isEqualTo(TextAnnotation.POSITION_UNKNOWN); + style.setRubyPosition(POSITION_BEFORE); + assertThat(style.getRubyPosition()).isEqualTo(POSITION_BEFORE); } @Test @@ -264,6 +263,6 @@ public final class TtmlStyleTest { 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); + assertThat(style.getTextEmphasis().position).isEqualTo(TextAnnotation.POSITION_AFTER); } } diff --git a/library/core/src/test/java/com/google/android/exoplayer2/text/webvtt/WebvttDecoderTest.java b/library/core/src/test/java/com/google/android/exoplayer2/text/webvtt/WebvttDecoderTest.java index 9b7db097a7..b04827bdfd 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/text/webvtt/WebvttDecoderTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/text/webvtt/WebvttDecoderTest.java @@ -27,6 +27,7 @@ import com.google.android.exoplayer2.testutil.TestUtil; import com.google.android.exoplayer2.text.Cue; import com.google.android.exoplayer2.text.SubtitleDecoderException; import com.google.android.exoplayer2.text.span.RubySpan; +import com.google.android.exoplayer2.text.span.TextAnnotation; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.ColorParser; import com.google.common.collect.Iterables; @@ -349,7 +350,7 @@ public class WebvttDecoderTest { assertThat(firstCue.text.toString()).isEqualTo("Some text with over-ruby."); assertThat((Spanned) firstCue.text) .hasRubySpanBetween("Some ".length(), "Some text with over-ruby".length()) - .withTextAndPosition("over", RubySpan.POSITION_OVER); + .withTextAndPosition("over", TextAnnotation.POSITION_BEFORE); // Check that `under` is read from CSS and unspecified defaults to `over`. Cue secondCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(2))); @@ -357,25 +358,25 @@ public class WebvttDecoderTest { .isEqualTo("Some text with under-ruby and over-ruby (default)."); assertThat((Spanned) secondCue.text) .hasRubySpanBetween("Some ".length(), "Some text with under-ruby".length()) - .withTextAndPosition("under", RubySpan.POSITION_UNDER); + .withTextAndPosition("under", TextAnnotation.POSITION_AFTER); assertThat((Spanned) secondCue.text) .hasRubySpanBetween( "Some text with under-ruby and ".length(), "Some text with under-ruby and over-ruby (default)".length()) - .withTextAndPosition("over", RubySpan.POSITION_OVER); + .withTextAndPosition("over", TextAnnotation.POSITION_BEFORE); // Check many tags with different positions nested in a single span. Cue thirdCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(4))); assertThat(thirdCue.text.toString()).isEqualTo("base1base2base3."); assertThat((Spanned) thirdCue.text) .hasRubySpanBetween(/* start= */ 0, "base1".length()) - .withTextAndPosition("over1", RubySpan.POSITION_OVER); + .withTextAndPosition("over1", TextAnnotation.POSITION_BEFORE); assertThat((Spanned) thirdCue.text) .hasRubySpanBetween("base1".length(), "base1base2".length()) - .withTextAndPosition("under2", RubySpan.POSITION_UNDER); + .withTextAndPosition("under2", TextAnnotation.POSITION_AFTER); assertThat((Spanned) thirdCue.text) .hasRubySpanBetween("base1base2".length(), "base1base2base3".length()) - .withTextAndPosition("under3", RubySpan.POSITION_UNDER); + .withTextAndPosition("under3", TextAnnotation.POSITION_AFTER); // Check a span with no tags. Cue fourthCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(6))); 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 95c6562599..0c81ac1931 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.TextAnnotation; import com.google.android.exoplayer2.text.span.TextEmphasisSpan; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Util; @@ -187,11 +188,11 @@ import java.util.regex.Pattern; } else if (span instanceof RubySpan) { RubySpan rubySpan = (RubySpan) span; switch (rubySpan.position) { - case RubySpan.POSITION_OVER: + case TextAnnotation.POSITION_BEFORE: return ""; - case RubySpan.POSITION_UNDER: + case TextAnnotation.POSITION_AFTER: return ""; - case RubySpan.POSITION_UNKNOWN: + case TextAnnotation.POSITION_UNKNOWN: return ""; default: return null; @@ -256,23 +257,17 @@ import java.util.regex.Pattern; 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){ + private static String getTextEmphasisPosition(@TextAnnotation.Position int position){ switch (position) { - case TextEmphasisSpan.POSITION_AFTER: + case TextAnnotation.POSITION_AFTER: return "under left"; - case TextEmphasisSpan.POSITION_UNKNOWN: - case TextEmphasisSpan.POSITION_BEFORE: - case TextEmphasisSpan.POSITION_OUTSIDE: /* Not supported, fallback to "before" */ + case TextAnnotation.POSITION_UNKNOWN: + case TextAnnotation.POSITION_BEFORE: default: // https://www.w3.org/TR/ttml2/#style-value-annotation-position // If an implementation does not recognize or otherwise distinguish an annotation position diff --git a/library/ui/src/test/java/com/google/android/exoplayer2/ui/SpannedToHtmlConverterTest.java b/library/ui/src/test/java/com/google/android/exoplayer2/ui/SpannedToHtmlConverterTest.java index b5d60416ff..f3b95e9d42 100644 --- a/library/ui/src/test/java/com/google/android/exoplayer2/ui/SpannedToHtmlConverterTest.java +++ b/library/ui/src/test/java/com/google/android/exoplayer2/ui/SpannedToHtmlConverterTest.java @@ -34,6 +34,7 @@ import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; import com.google.android.exoplayer2.text.span.HorizontalTextInVerticalContextSpan; import com.google.android.exoplayer2.text.span.RubySpan; +import com.google.android.exoplayer2.text.span.TextAnnotation; import com.google.android.exoplayer2.text.span.TextEmphasisSpan; import org.junit.Test; import org.junit.runner.RunWith; @@ -251,12 +252,12 @@ public class SpannedToHtmlConverterTest { SpannableString spanned = new SpannableString("String with over-annotated and under-annotated section"); spanned.setSpan( - new RubySpan("ruby-text", RubySpan.POSITION_OVER), + new RubySpan("ruby-text", TextAnnotation.POSITION_BEFORE), "String with ".length(), "String with over-annotated".length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); spanned.setSpan( - new RubySpan("non-àscìì-text", RubySpan.POSITION_UNDER), + new RubySpan("non-àscìì-text", TextAnnotation.POSITION_AFTER), "String with over-annotated and ".length(), "String with over-annotated and under-annotated".length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); @@ -284,13 +285,13 @@ public class SpannedToHtmlConverterTest { public void convert_supportsTextEmphasisSpan() { SpannableString spanned = new SpannableString("Text emphasis おはよ ございます "); spanned.setSpan( - new TextEmphasisSpan(TextEmphasisSpan.MARK_FILLED_CIRCLE, TextEmphasisSpan.POSITION_BEFORE), + new TextEmphasisSpan(TextEmphasisSpan.MARK_FILLED_CIRCLE, TextAnnotation.POSITION_BEFORE), "Text emphasis ".length(), "Text emphasis おはよ".length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); spanned.setSpan( - new TextEmphasisSpan(TextEmphasisSpan.MARK_OPEN_SESAME, TextEmphasisSpan.POSITION_AFTER), + new TextEmphasisSpan(TextEmphasisSpan.MARK_OPEN_SESAME, TextAnnotation.POSITION_AFTER), "Text emphasis おはよ ".length(), "Text emphasis おはよ ございます ".length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); diff --git a/testdata/src/test/assets/media/ttml/text_emphasis.xml b/testdata/src/test/assets/media/ttml/text_emphasis.xml index 90e85b931a..3f56704a37 100644 --- a/testdata/src/test/assets/media/ttml/text_emphasis.xml +++ b/testdata/src/test/assets/media/ttml/text_emphasis.xml @@ -3,10 +3,10 @@ xmlns:tts="http://www.w3.org/2006/10/ttaf1#style" xmlns="http://www.w3.org/ns/ttml"> - - - - + + + +

@@ -49,16 +49,16 @@

No textEmphasis property おはよ

-

Auto (TBLR) ございます

+

Auto (TBLR) ございます

-

Auto (TBRL) おはよ

+

Auto (TBRL) おはよ

-

Auto (TB) ございます

+

Auto (TB) ございます

-

Auto (LR) おはよ

+

Auto (LR) おはよ

diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/truth/SpannedSubject.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/truth/SpannedSubject.java index 546c417394..fd0b484fcf 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/truth/SpannedSubject.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/truth/SpannedSubject.java @@ -38,6 +38,7 @@ import androidx.annotation.ColorInt; 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.TextAnnotation; import com.google.android.exoplayer2.text.span.TextEmphasisSpan; import com.google.android.exoplayer2.util.Util; import com.google.common.truth.Fact; @@ -1075,7 +1076,7 @@ public final class SpannedSubject extends Subject { * @param position The expected position of the text. * @return A {@link WithSpanFlags} object for optional additional assertions on the flags. */ - AndSpanFlags withTextAndPosition(String text, @RubySpan.Position int position); + AndSpanFlags withTextAndPosition(String text, @TextAnnotation.Position int position); } private static final RubyText ALREADY_FAILED_WITH_TEXT = @@ -1099,7 +1100,7 @@ public final class SpannedSubject extends Subject { } @Override - public AndSpanFlags withTextAndPosition(String text, @RubySpan.Position int position) { + public AndSpanFlags withTextAndPosition(String text, @TextAnnotation.Position int position) { List matchingSpanFlags = new ArrayList<>(); List spanTextsAndPositions = new ArrayList<>(); for (RubySpan span : actualSpans) { @@ -1116,7 +1117,7 @@ public final class SpannedSubject extends Subject { private static final class TextAndPosition { private final String text; - @RubySpan.Position private final int position; + @TextAnnotation.Position private final int position; private TextAndPosition(String text, int position) { this.text = text; @@ -1164,7 +1165,7 @@ public final class SpannedSubject extends Subject { * @return A {@link WithSpanFlags} object for optional additional assertions on the flags. */ AndSpanFlags withMarkAndPosition(@TextEmphasisSpan.Mark int mark, - @TextEmphasisSpan.Position int position); + @TextAnnotation.Position int position); } private static final TextEmphasisDescription ALREADY_FAILED_WITH_MARK = @@ -1189,7 +1190,7 @@ public final class SpannedSubject extends Subject { @Override public AndSpanFlags withMarkAndPosition(@TextEmphasisSpan.Mark int mark, - @TextEmphasisSpan.Position int position) { + @TextAnnotation.Position int position) { List matchingSpanFlags = new ArrayList<>(); List textEmphasisMarksAndPositions = new ArrayList<>(); for (TextEmphasisSpan span : actualSpans) { @@ -1208,39 +1209,19 @@ public final class SpannedSubject extends Subject { @TextEmphasisSpan.Mark private final int mark; - @TextEmphasisSpan.Position + @TextAnnotation.Position private final int position; private MarkAndPosition(@TextEmphasisSpan.Mark int mark, - @TextEmphasisSpan.Position int position) { + @TextAnnotation.Position int position) { this.mark = mark; this.position = position; } - @Override - public boolean equals(@Nullable Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - - TextEmphasisSubject.MarkAndPosition that = (TextEmphasisSubject.MarkAndPosition) o; - return (position == that.position) && (mark == that.mark); - } - - @Override - public int hashCode() { - int result = 34613 * mark + position; - return result; - } - @Override public String toString() { return String.format("{mark=%s,position=%s}", mark, position); } } } - } diff --git a/testutils/src/test/java/com/google/android/exoplayer2/testutil/truth/SpannedSubjectTest.java b/testutils/src/test/java/com/google/android/exoplayer2/testutil/truth/SpannedSubjectTest.java index 23ee8d3dbf..5625e1eb8c 100644 --- a/testutils/src/test/java/com/google/android/exoplayer2/testutil/truth/SpannedSubjectTest.java +++ b/testutils/src/test/java/com/google/android/exoplayer2/testutil/truth/SpannedSubjectTest.java @@ -609,23 +609,23 @@ public class SpannedSubjectTest { public void rubySpan_success() { SpannableString spannable = createSpannable( - new RubySpan("ruby text", RubySpan.POSITION_OVER), Spanned.SPAN_INCLUSIVE_EXCLUSIVE); + new RubySpan("ruby text", TextAnnotation.POSITION_BEFORE), Spanned.SPAN_INCLUSIVE_EXCLUSIVE); assertThat(spannable) .hasRubySpanBetween(SPAN_START, SPAN_END) - .withTextAndPosition("ruby text", RubySpan.POSITION_OVER) + .withTextAndPosition("ruby text", TextAnnotation.POSITION_BEFORE) .andFlags(Spanned.SPAN_INCLUSIVE_EXCLUSIVE); } @Test public void rubySpan_wrongEndIndex() { checkHasSpanFailsDueToIndexMismatch( - new RubySpan("ruby text", RubySpan.POSITION_OVER), SpannedSubject::hasRubySpanBetween); + new RubySpan("ruby text", TextAnnotation.POSITION_BEFORE), SpannedSubject::hasRubySpanBetween); } @Test public void rubySpan_wrongText() { - SpannableString spannable = createSpannable(new RubySpan("ruby text", RubySpan.POSITION_OVER)); + SpannableString spannable = createSpannable(new RubySpan("ruby text", TextAnnotation.POSITION_BEFORE)); AssertionError expected = expectFailure( @@ -633,7 +633,7 @@ public class SpannedSubjectTest { whenTesting .that(spannable) .hasRubySpanBetween(SPAN_START, SPAN_END) - .withTextAndPosition("incorrect text", RubySpan.POSITION_OVER)); + .withTextAndPosition("incorrect text", TextAnnotation.POSITION_BEFORE)); assertThat(expected).factValue("value of").contains("rubyTextAndPosition"); assertThat(expected).factValue("expected").contains("text='incorrect text'"); @@ -642,7 +642,7 @@ public class SpannedSubjectTest { @Test public void rubySpan_wrongPosition() { - SpannableString spannable = createSpannable(new RubySpan("ruby text", RubySpan.POSITION_OVER)); + SpannableString spannable = createSpannable(new RubySpan("ruby text", TextAnnotation.POSITION_BEFORE)); AssertionError expected = expectFailure( @@ -650,27 +650,27 @@ public class SpannedSubjectTest { whenTesting .that(spannable) .hasRubySpanBetween(SPAN_START, SPAN_END) - .withTextAndPosition("ruby text", RubySpan.POSITION_UNDER)); + .withTextAndPosition("ruby text", TextAnnotation.POSITION_AFTER)); assertThat(expected).factValue("value of").contains("rubyTextAndPosition"); - assertThat(expected).factValue("expected").contains("position=" + RubySpan.POSITION_UNDER); - assertThat(expected).factValue("but was").contains("position=" + RubySpan.POSITION_OVER); + assertThat(expected).factValue("expected").contains("position=" + TextAnnotation.POSITION_AFTER); + assertThat(expected).factValue("but was").contains("position=" + TextAnnotation.POSITION_BEFORE); } @Test public void rubySpan_wrongFlags() { checkHasSpanFailsDueToFlagMismatch( - new RubySpan("ruby text", RubySpan.POSITION_OVER), + new RubySpan("ruby text", TextAnnotation.POSITION_BEFORE), (subject, start, end) -> subject .hasRubySpanBetween(start, end) - .withTextAndPosition("ruby text", RubySpan.POSITION_OVER)); + .withTextAndPosition("ruby text", TextAnnotation.POSITION_BEFORE)); } @Test public void noRubySpan_success() { SpannableString spannable = - createSpannableWithUnrelatedSpanAnd(new RubySpan("ruby text", RubySpan.POSITION_OVER)); + createSpannableWithUnrelatedSpanAnd(new RubySpan("ruby text", TextAnnotation.POSITION_BEFORE)); assertThat(spannable).hasNoRubySpanBetween(UNRELATED_SPAN_START, UNRELATED_SPAN_END); } @@ -678,7 +678,7 @@ public class SpannedSubjectTest { @Test public void noRubySpan_failure() { checkHasNoSpanFails( - new RubySpan("ruby text", RubySpan.POSITION_OVER), SpannedSubject::hasNoRubySpanBetween); + new RubySpan("ruby text", TextAnnotation.POSITION_BEFORE), SpannedSubject::hasNoRubySpanBetween); } @Test From 60a3af8d7758becdba6b7b05a01fa6209bb3ebb5 Mon Sep 17 00:00:00 2001 From: Denise LaFayette Date: Thu, 4 Mar 2021 16:06:56 -0800 Subject: [PATCH 3/6] Fix broken unit test in TtmlDecoder.textEmphasis() - Merge RUBY_POSITION with TEXT_EMPHASIS_POSITION in TtmlNode --- library/core/jacoco.exec | Bin 0 -> 49787 bytes .../exoplayer2/text/ttml/TextEmphasis.java | 24 ++++++++---------- .../exoplayer2/text/ttml/TtmlDecoder.java | 4 +-- .../exoplayer2/text/ttml/TtmlNode.java | 12 ++++----- .../testutil/truth/SpannedSubject.java | 19 ++++++++++++++ 5 files changed, 37 insertions(+), 22 deletions(-) create mode 100644 library/core/jacoco.exec diff --git a/library/core/jacoco.exec b/library/core/jacoco.exec new file mode 100644 index 0000000000000000000000000000000000000000..874c67f35901780ac1685fbebd3a1a5dafc17078 GIT binary patch literal 49787 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 Date: Fri, 5 Mar 2021 12:23:34 -0800 Subject: [PATCH 4/6] Address code review comments - Split text emphasis mark and style into two IntDefs - Represent textEmphasis="none" with a span rather than null - Fixed bugs in the style parsing logic --- .../text/span/TextEmphasisSpan.java | 76 +-- .../exoplayer2/text/ttml/TextEmphasis.java | 145 +++--- .../exoplayer2/text/ttml/TtmlRenderUtil.java | 16 +- .../text/ttml/TextEmphasisTest.java | 471 ++++++++++++------ .../exoplayer2/text/ttml/TtmlDecoderTest.java | 48 +- .../exoplayer2/text/ttml/TtmlStyleTest.java | 12 +- .../exoplayer2/ui/SpannedToHtmlConverter.java | 55 +- .../ui/SpannedToHtmlConverterTest.java | 6 +- .../testutil/truth/SpannedSubject.java | 39 +- 9 files changed, 560 insertions(+), 308 deletions(-) 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 index d247ad9364..221e0305d6 100644 --- 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 @@ -23,51 +23,60 @@ import java.lang.annotation.Documented; import java.lang.annotation.Retention; public final class TextEmphasisSpan { - // Bits [1:0] are used for typical mark types - public static final int MARK_FLAG_CIRCLE = 1; - public static final int MARK_FLAG_DOT = 2; - public static final int MARK_FLAG_SESAME = 3; - - // Bit 2 is used for filled/open - public static final int MARK_FLAG_FILLED = 0; - public static final int MARK_FLAG_OPEN = 4; - - // Below are the mark style constants - public static final int MARK_FILLED_CIRCLE = MARK_FLAG_CIRCLE | MARK_FLAG_FILLED; - public static final int MARK_FILLED_DOT = MARK_FLAG_DOT | MARK_FLAG_FILLED; - public static final int MARK_FILLED_SESAME = MARK_FLAG_SESAME | MARK_FLAG_FILLED; - public static final int MARK_OPEN_CIRCLE = MARK_FLAG_CIRCLE | MARK_FLAG_OPEN; - public static final int MARK_OPEN_DOT = MARK_FLAG_DOT | MARK_FLAG_OPEN; - public static final int MARK_OPEN_SESAME = MARK_FLAG_SESAME | MARK_FLAG_OPEN; + public static final int MARK_SHAPE_NONE = 0; + public static final int MARK_SHAPE_CIRCLE = 1; + public static final int MARK_SHAPE_DOT = 2; + public static final int MARK_SHAPE_SESAME = 3; /** - * The possible types of annotations used. + * The possible mark shapes that can be used. * *

One of: * *

    - *
  • {@link #MARK_FILLED_CIRCLE} - *
  • {@link #MARK_FILLED_DOT} - *
  • {@link #MARK_FILLED_SESAME} - *
  • {@link #MARK_OPEN_CIRCLE} - *
  • {@link #MARK_OPEN_DOT} - *
  • {@link #MARK_OPEN_SESAME} + *
  • {@link #MARK_SHAPE_NONE} + *
  • {@link #MARK_SHAPE_CIRCLE} + *
  • {@link #MARK_SHAPE_DOT} + *
  • {@link #MARK_SHAPE_SESAME} *
- * - * Note: We are intentionally excluding MARK_AUTO here since the auto value should - * be resolved */ @Documented @Retention(SOURCE) - @IntDef({MARK_FILLED_CIRCLE, MARK_FILLED_DOT, MARK_FILLED_SESAME, - MARK_OPEN_CIRCLE, MARK_OPEN_DOT, MARK_OPEN_SESAME}) - public @interface Mark { + @IntDef({MARK_SHAPE_NONE, MARK_SHAPE_CIRCLE, MARK_SHAPE_DOT, MARK_SHAPE_SESAME}) + public @interface MarkShape { } + public static final int MARK_FILL_UNSPECIFIED = 0; + public static final int MARK_FILL_FILLED = 1; + public static final int MARK_FILL_OPEN = 2; /** - * The mark used to emphasis text + * The possible mark fills that can be used. + * + *

One of: + * + *

    + *
  • {@link #MARK_FILL_UNSPECIFIED} + *
  • {@link #MARK_FILL_FILLED} + *
  • {@link #MARK_FILL_OPEN} + *
*/ - public @TextEmphasisSpan.Mark int mark; + @Documented + @Retention(SOURCE) + @IntDef({MARK_FILL_UNSPECIFIED, MARK_FILL_FILLED, MARK_FILL_OPEN}) + public @interface MarkFill { + } + + + /** + * The mark shape used for text emphasis + */ + public @MarkShape int markShape; + + /** + * The mark fill for the text emphasis mark + */ + public @MarkShape int markFill; + /** * The position of the text emphasis relative to the base text @@ -76,9 +85,10 @@ public final class TextEmphasisSpan { public final int position; - public TextEmphasisSpan(@TextEmphasisSpan.Mark int mark, + public TextEmphasisSpan(@MarkShape int shape, @MarkFill int fill, @TextAnnotation.Position int position) { - this.mark = mark; + this.markShape = shape; + this.markFill = fill; this.position = position; } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/ttml/TextEmphasis.java b/library/core/src/main/java/com/google/android/exoplayer2/text/ttml/TextEmphasis.java index 8b03cc12df..5069f80e4c 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/ttml/TextEmphasis.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/ttml/TextEmphasis.java @@ -38,33 +38,35 @@ import java.util.Set; /* package */ final class TextEmphasis { /** - * Mark style to be resolved at rendering time. Hence, it is not defined in - * {@link TextEmphasisSpan.Mark} + * Mark shape AUTO is to be resolved at rendering time. Hence, it is not defined in + * {@link TextEmphasisSpan.MarkShape} */ - public static final int MARK_AUTO = 1 << 8; + public static final int MARK_SHAPE_AUTO = 1 << 8; @Documented @Retention(SOURCE) - @IntDef({TextEmphasisSpan.MARK_FILLED_CIRCLE, - TextEmphasisSpan.MARK_FILLED_DOT, - TextEmphasisSpan.MARK_FILLED_SESAME, - TextEmphasisSpan.MARK_OPEN_CIRCLE, - TextEmphasisSpan.MARK_OPEN_DOT, - TextEmphasisSpan.MARK_OPEN_SESAME, - // Extending the definition in TextEmphasisSpan for intermediate values - MARK_AUTO + @IntDef({TextEmphasisSpan.MARK_SHAPE_NONE, + TextEmphasisSpan.MARK_SHAPE_CIRCLE, + TextEmphasisSpan.MARK_SHAPE_DOT, + TextEmphasisSpan.MARK_SHAPE_SESAME, + // Extending the definition in TextEmphasisSpan.MarkShape for intermediate values + MARK_SHAPE_AUTO }) - @interface Mark { + @interface MarkShape { } + @MarkShape + final int markShape; + /** * The mark style of the text emphasis. */ - @Mark final int mark; + @TextEmphasisSpan.MarkFill + final int markFill; /** - * Position to be resolved at rendering time. Hence, it is not defined in + * Position OUTSIDE is to be resolved at rendering time. Hence, it is not defined in * {@link TextAnnotation.Position} */ static final int POSITION_OUTSIDE = 1 << 8; @@ -84,26 +86,32 @@ import java.util.Set; */ @Position final int position; - private static Set markValues = ImmutableSet.of( + private static final Set singleStyleValues = ImmutableSet.of( TtmlNode.TEXT_EMPHASIS_AUTO, + TtmlNode.TEXT_EMPHASIS_NONE + ); + + private static final Set markShapeValues = ImmutableSet.of( TtmlNode.TEXT_EMPHASIS_MARK_DOT, TtmlNode.TEXT_EMPHASIS_MARK_SESAME, TtmlNode.TEXT_EMPHASIS_MARK_CIRCLE - ); + ); - private static Set markStyles = ImmutableSet.of( + private static final Set markFillValues = ImmutableSet.of( TtmlNode.TEXT_EMPHASIS_MARK_FILLED, TtmlNode.TEXT_EMPHASIS_MARK_OPEN ); - private static Set positionValues = ImmutableSet.of( + private static final Set positionValues = ImmutableSet.of( TtmlNode.ANNOTATION_POSITION_AFTER, TtmlNode.ANNOTATION_POSITION_BEFORE, TtmlNode.ANNOTATION_POSITION_OUTSIDE ); - private TextEmphasis(@Mark int mark, @TextAnnotation.Position int position) { - this.mark = mark; + private TextEmphasis(@MarkShape int shape, @TextEmphasisSpan.MarkFill int fill, + @TextAnnotation.Position int position) { + this.markShape = shape; + this.markFill = fill; this.position = position; } @@ -111,68 +119,83 @@ import java.util.Set; public String toString() { return "TextEmphasis{" + "position=" + position + - ", mark=" + mark + + ", markShape=" + markShape + + ", markFill=" + markFill + '}'; } - public static TextEmphasis createTextEmphasis(@Nullable String value) { + @Nullable public static TextEmphasis createTextEmphasis(@Nullable String value) { if (value == null) { return null; } String parsingValue = value.toLowerCase().trim(); - if ("".equals(parsingValue)) { + if (parsingValue.isEmpty()) { return null; } Set nodes = Sets.newHashSet(parsingValue.split("\\s+")); - - if (nodes.size() == 0 || TtmlNode.TEXT_EMPHASIS_NONE.equals(nodes.iterator().next())) { + if (nodes.size() == 0) { return null; } return parseNodes(nodes); } private static @Nullable TextEmphasis parseNodes(Set nodes) { - Set styleSet = Sets.intersection(markStyles, nodes).immutableCopy(); - Set markSet = Sets.intersection(markValues, nodes).immutableCopy(); - Set positionSet = Sets.intersection(positionValues, nodes).immutableCopy(); - - @Mark int mark = 0; - if (styleSet.size() == 1) { - mark |= TtmlNode.TEXT_EMPHASIS_MARK_OPEN.equals(styleSet.iterator().next()) - ? TextEmphasisSpan.MARK_FLAG_OPEN - : TextEmphasisSpan.MARK_FLAG_FILLED; - } - if (markSet.size() == 1) { - switch ((String) markSet.iterator().next()) { - case TtmlNode.TEXT_EMPHASIS_AUTO: - mark |= MARK_AUTO; - break; - case TtmlNode.TEXT_EMPHASIS_MARK_DOT: - mark |= TextEmphasisSpan.MARK_FLAG_DOT; - break; - case TtmlNode.TEXT_EMPHASIS_MARK_SESAME: - mark |= TextEmphasisSpan.MARK_FLAG_SESAME; - break; - case TtmlNode.TEXT_EMPHASIS_MARK_CIRCLE: - default: - mark |= TextEmphasisSpan.MARK_FLAG_CIRCLE; - } + @MarkShape int markShape; + @TextEmphasisSpan.MarkFill int markFill = TextEmphasisSpan.MARK_FILL_UNSPECIFIED; + Set styleSet = Sets.intersection(singleStyleValues, nodes).immutableCopy(); + if (styleSet.size() > 0) { + // If "none" or "auto" are found in the description, ignore the other style (fill, shape) + // attributes. + markShape = TtmlNode.TEXT_EMPHASIS_NONE.equals(styleSet.iterator().next()) + ? TextEmphasisSpan.MARK_SHAPE_NONE : MARK_SHAPE_AUTO; + // markFill is ignored when markShape is NONE or AUTO } else { - mark |= TextEmphasisSpan.MARK_FLAG_CIRCLE; + Set fillSet = Sets.intersection(markFillValues, nodes).immutableCopy(); + Set shapeSet = Sets.intersection(markShapeValues, nodes).immutableCopy(); + + if (fillSet.size() == 0 && shapeSet.size() == 0) { + // If an implementation does not recognize or otherwise distinguish an emphasis style value, + // then it must be interpreted as if a style of auto were specified; as such, an + // implementation that supports text emphasis marks must minimally support the auto value. + // https://www.w3.org/TR/ttml2/#style-value-emphasis-style + markShape = MARK_SHAPE_AUTO; + } else { + if (fillSet.size() > 0) { + markFill = TtmlNode.TEXT_EMPHASIS_MARK_OPEN.equals(fillSet.iterator().next()) + ? TextEmphasisSpan.MARK_FILL_OPEN + : TextEmphasisSpan.MARK_FILL_FILLED; + } else { + markFill = TextEmphasisSpan.MARK_FILL_FILLED; + } + + if (shapeSet.size() > 0) { + switch (shapeSet.iterator().next()) { + case TtmlNode.TEXT_EMPHASIS_MARK_DOT: + markShape = TextEmphasisSpan.MARK_SHAPE_DOT; + break; + case TtmlNode.TEXT_EMPHASIS_MARK_SESAME: + markShape = TextEmphasisSpan.MARK_SHAPE_SESAME; + break; + case TtmlNode.TEXT_EMPHASIS_MARK_CIRCLE: + default: + markShape = TextEmphasisSpan.MARK_SHAPE_CIRCLE; + } + } else { + markShape = TextEmphasisSpan.MARK_SHAPE_CIRCLE; + } + } } - /** - * If no emphasis position is specified, then the emphasis position must be interpreted as if - * a position of outside were specified. - *

- * More information on - * tts:textEmphasis - */ + Set positionSet = Sets.intersection(positionValues, nodes).immutableCopy(); + + // If no emphasis position is specified, then the emphasis position must be interpreted as if + // a position of outside were specified. + // https://www.w3.org/TR/2018/REC-ttml2-20181108/#style-attribute-textEmphasis @Position int position = POSITION_OUTSIDE; - if (positionSet.size() == 1) { - switch ((String) positionSet.iterator().next()) { + if (positionSet.size() > 0) { + switch (positionSet.iterator().next()) { case TtmlNode.ANNOTATION_POSITION_AFTER: position = TextAnnotation.POSITION_AFTER; break; @@ -184,6 +207,6 @@ import java.util.Set; position = TextAnnotation.POSITION_BEFORE; } } - return new TextEmphasis(mark, position); + return new TextEmphasis(markShape, markFill, position); } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/ttml/TtmlRenderUtil.java b/library/core/src/main/java/com/google/android/exoplayer2/text/ttml/TtmlRenderUtil.java index 72b1f63f15..3231a66eee 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/ttml/TtmlRenderUtil.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/ttml/TtmlRenderUtil.java @@ -129,14 +129,14 @@ import java.util.Map; * If an implementation does not recognize or otherwise distinguish an emphasis style value, * then it must be interpreted as if a style of auto were specified; as such, an * implementation that supports text emphasis marks must minimally support the auto value. - * If a vertical writing mode applies, then equivalent to filled sesame; otherwise, equivalent - * to filled circle. - * See 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. + * See https://www.w3.org/TR/ttml2/#style-value-emphasis-style */ - @TextEmphasis.Mark int mark = textEmphasis.mark; - if (textEmphasis.mark == TextEmphasis.MARK_AUTO) { - mark = (verticalType == Cue.VERTICAL_TYPE_LR || verticalType == Cue.VERTICAL_TYPE_RL) ? - TextEmphasisSpan.MARK_FILLED_SESAME : TextEmphasisSpan.MARK_FILLED_CIRCLE; + @TextEmphasis.MarkShape int markShape = textEmphasis.markShape; + if (textEmphasis.markShape == TextEmphasis.MARK_SHAPE_AUTO) { + markShape = (verticalType == Cue.VERTICAL_TYPE_LR || verticalType == Cue.VERTICAL_TYPE_RL) ? + TextEmphasisSpan.MARK_SHAPE_SESAME : TextEmphasisSpan.MARK_SHAPE_CIRCLE; } @TextEmphasis.Position int position = textEmphasis.position; @@ -154,7 +154,7 @@ import java.util.Map; SpanUtil.addOrReplaceSpan( builder, - new TextEmphasisSpan(mark, position), + new TextEmphasisSpan(markShape, textEmphasis.markFill, position), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); diff --git a/library/core/src/test/java/com/google/android/exoplayer2/text/ttml/TextEmphasisTest.java b/library/core/src/test/java/com/google/android/exoplayer2/text/ttml/TextEmphasisTest.java index e273a0d049..128a2b2ea0 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/text/ttml/TextEmphasisTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/text/ttml/TextEmphasisTest.java @@ -1,16 +1,20 @@ package com.google.android.exoplayer2.text.ttml; +import static com.google.android.exoplayer2.text.ttml.TextEmphasis.MARK_SHAPE_AUTO; +import static com.google.android.exoplayer2.text.ttml.TextEmphasis.POSITION_OUTSIDE; import static com.google.android.exoplayer2.text.ttml.TextEmphasis.createTextEmphasis; -import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assertWithMessage; +import androidx.annotation.Nullable; import androidx.test.ext.junit.runners.AndroidJUnit4; import com.google.android.exoplayer2.text.span.TextAnnotation; import com.google.android.exoplayer2.text.span.TextEmphasisSpan; import org.junit.Test; import org.junit.runner.RunWith; -/** Unit test for {@link TextEmphasis}. */ +/** + * Unit test for {@link TextEmphasis}. + */ @RunWith(AndroidJUnit4.class) public class TextEmphasisTest { @@ -18,373 +22,542 @@ public class TextEmphasisTest { @Test public void testNull() { - String value = null; - TextEmphasis textEmphasis = createTextEmphasis(value); - + @Nullable TextEmphasis textEmphasis = createTextEmphasis(null); assertWithMessage("Text Emphasis must be null").that(textEmphasis).isNull(); } @Test public void testEmpty() { - String value = ""; - TextEmphasis textEmphasis = createTextEmphasis(value); + @Nullable TextEmphasis textEmphasis = createTextEmphasis(""); + assertWithMessage("Text Emphasis must be null").that(textEmphasis).isNull(); + } + @Test + public void testEmptyWithWhitespace() { + @Nullable TextEmphasis textEmphasis = createTextEmphasis(" "); assertWithMessage("Text Emphasis must be null").that(textEmphasis).isNull(); } @Test public void testNone() { String value = "none"; - TextEmphasis textEmphasis = createTextEmphasis(value); - - assertWithMessage("Text Emphasis must be null").that(textEmphasis).isNull(); + @Nullable TextEmphasis textEmphasis = createTextEmphasis(value); + assertWithMessage("Text Emphasis must exist").that(textEmphasis).isNotNull(); + assertWithMessage("markShape").that(textEmphasis.markShape) + .isEqualTo(TextEmphasisSpan.MARK_SHAPE_NONE); + assertWithMessage("position").that(textEmphasis.position) + .isEqualTo(TextEmphasis.POSITION_OUTSIDE); } @Test public void testAuto() { String value = "auto"; - TextEmphasis textEmphasis = createTextEmphasis(value); - + @Nullable TextEmphasis textEmphasis = createTextEmphasis(value); assertWithMessage("Text Emphasis must exist").that(textEmphasis).isNotNull(); - assertThat(textEmphasis.mark).isEqualTo(TextEmphasis.MARK_AUTO); - assertThat(textEmphasis.position).isEqualTo(TextEmphasis.POSITION_OUTSIDE); + assertWithMessage("markShape").that(textEmphasis.markShape).isEqualTo(MARK_SHAPE_AUTO); + assertWithMessage("markFill").that(textEmphasis.markFill) + .isEqualTo(TextEmphasisSpan.MARK_FILL_UNSPECIFIED); + assertWithMessage("position").that(textEmphasis.position) + .isEqualTo(TextEmphasis.POSITION_OUTSIDE); } + @Test + public void testInvalid() { + String value = "invalid"; + @Nullable TextEmphasis textEmphasis = createTextEmphasis(value); + assertWithMessage("Text Emphasis must exist").that(textEmphasis).isNotNull(); + assertWithMessage("markShape").that(textEmphasis.markShape).isEqualTo(MARK_SHAPE_AUTO); + assertWithMessage("markFill").that(textEmphasis.markFill) + .isEqualTo(TextEmphasisSpan.MARK_FILL_UNSPECIFIED); + assertWithMessage("position").that(textEmphasis.position) + .isEqualTo(TextEmphasis.POSITION_OUTSIDE); + } + + @Test public void testAutoOutside() { String value = "auto outside"; - TextEmphasis textEmphasis = createTextEmphasis(value); - + @Nullable TextEmphasis textEmphasis = createTextEmphasis(value); assertWithMessage("Text Emphasis must exist").that(textEmphasis).isNotNull(); - assertThat(textEmphasis.mark).isEqualTo(TextEmphasis.MARK_AUTO); - assertThat(textEmphasis.position).isEqualTo(TextEmphasis.POSITION_OUTSIDE); + assertWithMessage("markShape").that(textEmphasis.markShape).isEqualTo(MARK_SHAPE_AUTO); + assertWithMessage("markFill").that(textEmphasis.markFill) + .isEqualTo(TextEmphasisSpan.MARK_FILL_UNSPECIFIED); + assertWithMessage("position").that(textEmphasis.position) + .isEqualTo(TextEmphasis.POSITION_OUTSIDE); + } + + @Test + public void testAutoAfter() { + String value = "auto after"; + @Nullable TextEmphasis textEmphasis = createTextEmphasis(value); + assertWithMessage("Text Emphasis must exist").that(textEmphasis).isNotNull(); + assertWithMessage("markShape").that(textEmphasis.markShape).isEqualTo(MARK_SHAPE_AUTO); + assertWithMessage("markFill").that(textEmphasis.markFill) + .isEqualTo(TextEmphasisSpan.MARK_FILL_UNSPECIFIED); + assertWithMessage("position").that(textEmphasis.position) + .isEqualTo(TextAnnotation.POSITION_AFTER); } /** - * If only filled or open is specified, then it is equivalent to filled circle and open circle, - * respectively. + * If only filled or open is specified, then it is equivalent to filled circle and open circle, + * respectively. */ @Test public void testFilled() { String value = "filled"; - TextEmphasis textEmphasis = createTextEmphasis(value); - + @Nullable TextEmphasis textEmphasis = createTextEmphasis(value); assertWithMessage("Text Emphasis must exist").that(textEmphasis).isNotNull(); - assertThat(textEmphasis.mark).isEqualTo(TextEmphasisSpan.MARK_FILLED_CIRCLE); - assertThat(textEmphasis.position).isEqualTo(TextEmphasis.POSITION_OUTSIDE); + assertWithMessage("markShape").that(textEmphasis.markShape) + .isEqualTo(TextEmphasisSpan.MARK_SHAPE_CIRCLE); + assertWithMessage("markFill").that(textEmphasis.markFill) + .isEqualTo(TextEmphasisSpan.MARK_FILL_FILLED); + assertWithMessage("position").that(textEmphasis.position) + .isEqualTo(TextEmphasis.POSITION_OUTSIDE); } @Test public void testOpen() { String value = "open"; - TextEmphasis textEmphasis = createTextEmphasis(value); - + @Nullable TextEmphasis textEmphasis = createTextEmphasis(value); assertWithMessage("Text Emphasis must exist").that(textEmphasis).isNotNull(); - assertThat(textEmphasis.mark).isEqualTo(TextEmphasisSpan.MARK_OPEN_CIRCLE); - assertThat(textEmphasis.position).isEqualTo(TextEmphasis.POSITION_OUTSIDE); + assertWithMessage("markShape").that(textEmphasis.markShape) + .isEqualTo(TextEmphasisSpan.MARK_SHAPE_CIRCLE); + assertWithMessage("markFill").that(textEmphasis.markFill) + .isEqualTo(TextEmphasisSpan.MARK_FILL_OPEN); + assertWithMessage("position").that(textEmphasis.position) + .isEqualTo(TextEmphasis.POSITION_OUTSIDE); } @Test public void testOpenAfter() { String value = "open after"; - TextEmphasis textEmphasis = createTextEmphasis(value); - + @Nullable TextEmphasis textEmphasis = createTextEmphasis(value); assertWithMessage("Text Emphasis must exist").that(textEmphasis).isNotNull(); - assertThat(textEmphasis.mark).isEqualTo(TextEmphasisSpan.MARK_OPEN_CIRCLE); - assertThat(textEmphasis.position).isEqualTo(TextAnnotation.POSITION_AFTER); + assertWithMessage("markShape").that(textEmphasis.markShape) + .isEqualTo(TextEmphasisSpan.MARK_SHAPE_CIRCLE); + assertWithMessage("markFill").that(textEmphasis.markFill) + .isEqualTo(TextEmphasisSpan.MARK_FILL_OPEN); + assertWithMessage("position").that(textEmphasis.position) + .isEqualTo(TextAnnotation.POSITION_AFTER); } /** - * If only circle, dot, or sesame is specified, then it is equivalent to filled circle, filled dot, - * and filled sesame, respectively. + * If only circle, dot, or sesame is specified, then it is equivalent to filled circle, filled + * dot, and filled sesame, respectively. */ @Test public void testDotBefore() { String value = "dot before"; - TextEmphasis textEmphasis = createTextEmphasis(value); - + @Nullable TextEmphasis textEmphasis = createTextEmphasis(value); assertWithMessage("Text Emphasis must exist").that(textEmphasis).isNotNull(); - assertThat(textEmphasis.mark).isEqualTo(TextEmphasisSpan.MARK_FILLED_DOT); - assertThat(textEmphasis.position).isEqualTo(TextAnnotation.POSITION_BEFORE); + assertWithMessage("markShape").that(textEmphasis.markShape) + .isEqualTo(TextEmphasisSpan.MARK_SHAPE_DOT); + assertWithMessage("markFill").that(textEmphasis.markFill) + .isEqualTo(TextEmphasisSpan.MARK_FILL_FILLED); + assertWithMessage("position").that(textEmphasis.position) + .isEqualTo(TextAnnotation.POSITION_BEFORE); } @Test public void testCircleBefore() { String value = "circle before"; - TextEmphasis textEmphasis = createTextEmphasis(value); - + @Nullable TextEmphasis textEmphasis = createTextEmphasis(value); assertWithMessage("Text Emphasis must exist").that(textEmphasis).isNotNull(); - assertThat(textEmphasis.mark).isEqualTo(TextEmphasisSpan.MARK_FILLED_CIRCLE); - assertThat(textEmphasis.position).isEqualTo(TextAnnotation.POSITION_BEFORE); + assertWithMessage("markShape").that(textEmphasis.markShape) + .isEqualTo(TextEmphasisSpan.MARK_SHAPE_CIRCLE); + assertWithMessage("markFill").that(textEmphasis.markFill) + .isEqualTo(TextEmphasisSpan.MARK_FILL_FILLED); + assertWithMessage("position").that(textEmphasis.position) + .isEqualTo(TextAnnotation.POSITION_BEFORE); } @Test public void testSesameBefore() { String value = "sesame before"; - TextEmphasis textEmphasis = createTextEmphasis(value); - + @Nullable TextEmphasis textEmphasis = createTextEmphasis(value); assertWithMessage("Text Emphasis must exist").that(textEmphasis).isNotNull(); - assertThat(textEmphasis.mark).isEqualTo(TextEmphasisSpan.MARK_FILLED_SESAME); - assertThat(textEmphasis.position).isEqualTo(TextAnnotation.POSITION_BEFORE); + assertWithMessage("markShape").that(textEmphasis.markShape) + .isEqualTo(TextEmphasisSpan.MARK_SHAPE_SESAME); + assertWithMessage("markFill").that(textEmphasis.markFill) + .isEqualTo(TextEmphasisSpan.MARK_FILL_FILLED); + assertWithMessage("position").that(textEmphasis.position) + .isEqualTo(TextAnnotation.POSITION_BEFORE); } @Test public void testDotAfter() { String value = "dot AFTER"; - TextEmphasis textEmphasis = createTextEmphasis(value); - + @Nullable TextEmphasis textEmphasis = createTextEmphasis(value); assertWithMessage("Text Emphasis must exist").that(textEmphasis).isNotNull(); - assertThat(textEmphasis.mark).isEqualTo(TextEmphasisSpan.MARK_FILLED_DOT); - assertThat(textEmphasis.position).isEqualTo(TextAnnotation.POSITION_AFTER); + assertWithMessage("markShape").that(textEmphasis.markShape) + .isEqualTo(TextEmphasisSpan.MARK_SHAPE_DOT); + assertWithMessage("markFill").that(textEmphasis.markFill) + .isEqualTo(TextEmphasisSpan.MARK_FILL_FILLED); + assertWithMessage("position").that(textEmphasis.position) + .isEqualTo(TextAnnotation.POSITION_AFTER); } @Test public void testCircleAfter() { String value = "circle after"; - TextEmphasis textEmphasis = createTextEmphasis(value); - + @Nullable TextEmphasis textEmphasis = createTextEmphasis(value); assertWithMessage("Text Emphasis must exist").that(textEmphasis).isNotNull(); - assertThat(textEmphasis.mark).isEqualTo(TextEmphasisSpan.MARK_FILLED_CIRCLE); - assertThat(textEmphasis.position).isEqualTo(TextAnnotation.POSITION_AFTER); + assertWithMessage("markShape").that(textEmphasis.markShape) + .isEqualTo(TextEmphasisSpan.MARK_SHAPE_CIRCLE); + assertWithMessage("markFill").that(textEmphasis.markFill) + .isEqualTo(TextEmphasisSpan.MARK_FILL_FILLED); + assertWithMessage("position").that(textEmphasis.position) + .isEqualTo(TextAnnotation.POSITION_AFTER); } @Test public void testSesameAfter() { String value = "sesame aFter"; - TextEmphasis textEmphasis = createTextEmphasis(value); - + @Nullable TextEmphasis textEmphasis = createTextEmphasis(value); assertWithMessage("Text Emphasis must exist").that(textEmphasis).isNotNull(); - assertThat(textEmphasis.mark).isEqualTo(TextEmphasisSpan.MARK_FILLED_SESAME); - assertThat(textEmphasis.position).isEqualTo(TextAnnotation.POSITION_AFTER); + assertWithMessage("markShape").that(textEmphasis.markShape) + .isEqualTo(TextEmphasisSpan.MARK_SHAPE_SESAME); + assertWithMessage("markFill").that(textEmphasis.markFill) + .isEqualTo(TextEmphasisSpan.MARK_FILL_FILLED); + assertWithMessage("position").that(textEmphasis.position) + .isEqualTo(TextAnnotation.POSITION_AFTER); } @Test public void testDotOutside() { String value = "dot outside"; - TextEmphasis textEmphasis = createTextEmphasis(value); - + @Nullable TextEmphasis textEmphasis = createTextEmphasis(value); assertWithMessage("Text Emphasis must exist").that(textEmphasis).isNotNull(); - assertThat(textEmphasis.mark).isEqualTo(TextEmphasisSpan.MARK_FILLED_DOT); - assertThat(textEmphasis.position).isEqualTo(TextEmphasis.POSITION_OUTSIDE); + assertWithMessage("markShape").that(textEmphasis.markShape) + .isEqualTo(TextEmphasisSpan.MARK_SHAPE_DOT); + assertWithMessage("markFill").that(textEmphasis.markFill) + .isEqualTo(TextEmphasisSpan.MARK_FILL_FILLED); + assertWithMessage("position").that(textEmphasis.position) + .isEqualTo(TextEmphasis.POSITION_OUTSIDE); } @Test public void testCircleOutside() { String value = "circle outside"; - TextEmphasis textEmphasis = createTextEmphasis(value); - + @Nullable TextEmphasis textEmphasis = createTextEmphasis(value); assertWithMessage("Text Emphasis must exist").that(textEmphasis).isNotNull(); - assertThat(textEmphasis.mark).isEqualTo(TextEmphasisSpan.MARK_FILLED_CIRCLE); - assertThat(textEmphasis.position).isEqualTo(TextEmphasis.POSITION_OUTSIDE); + assertWithMessage("markShape").that(textEmphasis.markShape) + .isEqualTo(TextEmphasisSpan.MARK_SHAPE_CIRCLE); + assertWithMessage("markFill").that(textEmphasis.markFill) + .isEqualTo(TextEmphasisSpan.MARK_FILL_FILLED); + assertWithMessage("position").that(textEmphasis.position) + .isEqualTo(TextEmphasis.POSITION_OUTSIDE); } @Test public void testSesameOutside() { String value = "sesame outside"; - TextEmphasis textEmphasis = createTextEmphasis(value); - + @Nullable TextEmphasis textEmphasis = createTextEmphasis(value); assertWithMessage("Text Emphasis must exist").that(textEmphasis).isNotNull(); - assertThat(textEmphasis.mark).isEqualTo(TextEmphasisSpan.MARK_FILLED_SESAME); - assertThat(textEmphasis.position).isEqualTo(TextEmphasis.POSITION_OUTSIDE); + assertWithMessage("markShape").that(textEmphasis.markShape) + .isEqualTo(TextEmphasisSpan.MARK_SHAPE_SESAME); + assertWithMessage("markFill").that(textEmphasis.markFill) + .isEqualTo(TextEmphasisSpan.MARK_FILL_FILLED); + assertWithMessage("position").that(textEmphasis.position) + .isEqualTo(TextEmphasis.POSITION_OUTSIDE); } @Test public void testOpenDotAfter() { String value = "open dot AFTER"; - TextEmphasis textEmphasis = createTextEmphasis(value); - + @Nullable TextEmphasis textEmphasis = createTextEmphasis(value); assertWithMessage("Text Emphasis must exist").that(textEmphasis).isNotNull(); - assertThat(textEmphasis.mark).isEqualTo(TextEmphasisSpan.MARK_OPEN_DOT); - assertThat(textEmphasis.position).isEqualTo(TextAnnotation.POSITION_AFTER); + assertWithMessage("markShape").that(textEmphasis.markShape) + .isEqualTo(TextEmphasisSpan.MARK_SHAPE_DOT); + assertWithMessage("markFill").that(textEmphasis.markFill) + .isEqualTo(TextEmphasisSpan.MARK_FILL_OPEN); + assertWithMessage("position").that(textEmphasis.position) + .isEqualTo(TextAnnotation.POSITION_AFTER); } @Test public void testOpenCircleAfter() { String value = "Open circle after"; - TextEmphasis textEmphasis = createTextEmphasis(value); - + @Nullable TextEmphasis textEmphasis = createTextEmphasis(value); assertWithMessage("Text Emphasis must exist").that(textEmphasis).isNotNull(); - assertThat(textEmphasis.mark).isEqualTo(TextEmphasisSpan.MARK_OPEN_CIRCLE); - assertThat(textEmphasis.position).isEqualTo(TextAnnotation.POSITION_AFTER); + assertWithMessage("markShape").that(textEmphasis.markShape) + .isEqualTo(TextEmphasisSpan.MARK_SHAPE_CIRCLE); + assertWithMessage("markFill").that(textEmphasis.markFill) + .isEqualTo(TextEmphasisSpan.MARK_FILL_OPEN); + assertWithMessage("position").that(textEmphasis.position) + .isEqualTo(TextAnnotation.POSITION_AFTER); } @Test public void testOpenSesameAfter() { String value = "open sesame aFter"; - TextEmphasis textEmphasis = createTextEmphasis(value); + @Nullable TextEmphasis textEmphasis = createTextEmphasis(value); assertWithMessage("Text Emphasis must exist").that(textEmphasis).isNotNull(); - assertThat(textEmphasis.mark).isEqualTo(TextEmphasisSpan.MARK_OPEN_SESAME); - assertThat(textEmphasis.position).isEqualTo(TextAnnotation.POSITION_AFTER); + assertWithMessage("markShape").that(textEmphasis.markShape) + .isEqualTo(TextEmphasisSpan.MARK_SHAPE_SESAME); + assertWithMessage("markFill").that(textEmphasis.markFill) + .isEqualTo(TextEmphasisSpan.MARK_FILL_OPEN); + assertWithMessage("position").that(textEmphasis.position) + .isEqualTo(TextAnnotation.POSITION_AFTER); } @Test public void testOpenDotBefore() { String value = "open dot before"; - TextEmphasis textEmphasis = createTextEmphasis(value); - + @Nullable TextEmphasis textEmphasis = createTextEmphasis(value); assertWithMessage("Text Emphasis must exist").that(textEmphasis).isNotNull(); - assertThat(textEmphasis.mark).isEqualTo(TextEmphasisSpan.MARK_OPEN_DOT); - assertThat(textEmphasis.position).isEqualTo(TextAnnotation.POSITION_BEFORE); + assertWithMessage("markShape").that(textEmphasis.markShape) + .isEqualTo(TextEmphasisSpan.MARK_SHAPE_DOT); + assertWithMessage("markFill").that(textEmphasis.markFill) + .isEqualTo(TextEmphasisSpan.MARK_FILL_OPEN); + assertWithMessage("position").that(textEmphasis.position) + .isEqualTo(TextAnnotation.POSITION_BEFORE); } @Test public void testOpenCircleBefore() { String value = "Open circle Before"; - TextEmphasis textEmphasis = createTextEmphasis(value); - + @Nullable TextEmphasis textEmphasis = createTextEmphasis(value); assertWithMessage("Text Emphasis must exist").that(textEmphasis).isNotNull(); - assertThat(textEmphasis.mark).isEqualTo(TextEmphasisSpan.MARK_OPEN_CIRCLE); - assertThat(textEmphasis.position).isEqualTo(TextAnnotation.POSITION_BEFORE); + assertWithMessage("markShape").that(textEmphasis.markShape) + .isEqualTo(TextEmphasisSpan.MARK_SHAPE_CIRCLE); + assertWithMessage("markFill").that(textEmphasis.markFill) + .isEqualTo(TextEmphasisSpan.MARK_FILL_OPEN); + assertWithMessage("position").that(textEmphasis.position) + .isEqualTo(TextAnnotation.POSITION_BEFORE); } @Test public void testOpenSesameBefore() { String value = "open sesame Before"; - TextEmphasis textEmphasis = createTextEmphasis(value); - + @Nullable TextEmphasis textEmphasis = createTextEmphasis(value); assertWithMessage("Text Emphasis must exist").that(textEmphasis).isNotNull(); - assertThat(textEmphasis.mark).isEqualTo(TextEmphasisSpan.MARK_OPEN_SESAME); - assertThat(textEmphasis.position).isEqualTo(TextAnnotation.POSITION_BEFORE); + assertWithMessage("markShape").that(textEmphasis.markShape) + .isEqualTo(TextEmphasisSpan.MARK_SHAPE_SESAME); + assertWithMessage("markFill").that(textEmphasis.markFill) + .isEqualTo(TextEmphasisSpan.MARK_FILL_OPEN); + assertWithMessage("position").that(textEmphasis.position) + .isEqualTo(TextAnnotation.POSITION_BEFORE); } @Test public void testOpenDotOutside() { String value = "open dot Outside"; - TextEmphasis textEmphasis = createTextEmphasis(value); - + @Nullable TextEmphasis textEmphasis = createTextEmphasis(value); assertWithMessage("Text Emphasis must exist").that(textEmphasis).isNotNull(); - assertThat(textEmphasis.mark).isEqualTo(TextEmphasisSpan.MARK_OPEN_DOT); - assertThat(textEmphasis.position).isEqualTo(TextEmphasis.POSITION_OUTSIDE); + assertWithMessage("markShape").that(textEmphasis.markShape) + .isEqualTo(TextEmphasisSpan.MARK_SHAPE_DOT); + assertWithMessage("markFill").that(textEmphasis.markFill) + .isEqualTo(TextEmphasisSpan.MARK_FILL_OPEN); + assertWithMessage("position").that(textEmphasis.position) + .isEqualTo(TextEmphasis.POSITION_OUTSIDE); } @Test public void testOpenCircleOutside() { String value = "Open circle Outside"; - TextEmphasis textEmphasis = createTextEmphasis(value); - + @Nullable TextEmphasis textEmphasis = createTextEmphasis(value); assertWithMessage("Text Emphasis must exist").that(textEmphasis).isNotNull(); - assertThat(textEmphasis.mark).isEqualTo(TextEmphasisSpan.MARK_OPEN_CIRCLE); - assertThat(textEmphasis.position).isEqualTo(TextEmphasis.POSITION_OUTSIDE); + assertWithMessage("markShape").that(textEmphasis.markShape) + .isEqualTo(TextEmphasisSpan.MARK_SHAPE_CIRCLE); + assertWithMessage("markFill").that(textEmphasis.markFill) + .isEqualTo(TextEmphasisSpan.MARK_FILL_OPEN); + assertWithMessage("position").that(textEmphasis.position) + .isEqualTo(TextEmphasis.POSITION_OUTSIDE); } @Test public void testOpenSesameOutside() { String value = "open sesame outside"; - TextEmphasis textEmphasis = createTextEmphasis(value); - + @Nullable TextEmphasis textEmphasis = createTextEmphasis(value); assertWithMessage("Text Emphasis must exist").that(textEmphasis).isNotNull(); - assertThat(textEmphasis.mark).isEqualTo(TextEmphasisSpan.MARK_OPEN_SESAME); - assertThat(textEmphasis.position).isEqualTo(TextEmphasis.POSITION_OUTSIDE); + assertWithMessage("markShape").that(textEmphasis.markShape) + .isEqualTo(TextEmphasisSpan.MARK_SHAPE_SESAME); + assertWithMessage("markFill").that(textEmphasis.markFill) + .isEqualTo(TextEmphasisSpan.MARK_FILL_OPEN); + assertWithMessage("position").that(textEmphasis.position) + .isEqualTo(TextEmphasis.POSITION_OUTSIDE); } @Test public void testFilledDotOutside() { String value = "filled dot outside"; - TextEmphasis textEmphasis = createTextEmphasis(value); - + @Nullable TextEmphasis textEmphasis = createTextEmphasis(value); assertWithMessage("Text Emphasis must exist").that(textEmphasis).isNotNull(); - assertThat(textEmphasis.mark).isEqualTo(TextEmphasisSpan.MARK_FILLED_DOT); - assertThat(textEmphasis.position).isEqualTo(TextEmphasis.POSITION_OUTSIDE); + assertWithMessage("markShape").that(textEmphasis.markShape) + .isEqualTo(TextEmphasisSpan.MARK_SHAPE_DOT); + assertWithMessage("markFill").that(textEmphasis.markFill) + .isEqualTo(TextEmphasisSpan.MARK_FILL_FILLED); + assertWithMessage("position").that(textEmphasis.position) + .isEqualTo(TextEmphasis.POSITION_OUTSIDE); } @Test public void testFilledCircleOutside() { String value = "filled circle outside"; - TextEmphasis textEmphasis = createTextEmphasis(value); - + @Nullable TextEmphasis textEmphasis = createTextEmphasis(value); assertWithMessage("Text Emphasis must exist").that(textEmphasis).isNotNull(); - assertThat(textEmphasis.mark).isEqualTo(TextEmphasisSpan.MARK_FILLED_CIRCLE); - assertThat(textEmphasis.position).isEqualTo(TextEmphasis.POSITION_OUTSIDE); + assertWithMessage("markShape").that(textEmphasis.markShape) + .isEqualTo(TextEmphasisSpan.MARK_SHAPE_CIRCLE); + assertWithMessage("markFill").that(textEmphasis.markFill) + .isEqualTo(TextEmphasisSpan.MARK_FILL_FILLED); + assertWithMessage("position").that(textEmphasis.position) + .isEqualTo(TextEmphasis.POSITION_OUTSIDE); } @Test public void testFilledSesameOutside() { String value = "filled sesame outside"; - TextEmphasis textEmphasis = createTextEmphasis(value); - + @Nullable TextEmphasis textEmphasis = createTextEmphasis(value); assertWithMessage("Text Emphasis must exist").that(textEmphasis).isNotNull(); - assertThat(textEmphasis.mark).isEqualTo(TextEmphasisSpan.MARK_FILLED_SESAME); - assertThat(textEmphasis.position).isEqualTo(TextEmphasis.POSITION_OUTSIDE); + assertWithMessage("markShape").that(textEmphasis.markShape) + .isEqualTo(TextEmphasisSpan.MARK_SHAPE_SESAME); + assertWithMessage("markFill").that(textEmphasis.markFill) + .isEqualTo(TextEmphasisSpan.MARK_FILL_FILLED); + assertWithMessage("position").that(textEmphasis.position) + .isEqualTo(TextEmphasis.POSITION_OUTSIDE); } @Test public void testFilledDotAfter() { String value = "filled dot After"; - TextEmphasis textEmphasis = createTextEmphasis(value); - + @Nullable TextEmphasis textEmphasis = createTextEmphasis(value); assertWithMessage("Text Emphasis must exist").that(textEmphasis).isNotNull(); - assertThat(textEmphasis.mark).isEqualTo(TextEmphasisSpan.MARK_FILLED_DOT); - assertThat(textEmphasis.position).isEqualTo(TextAnnotation.POSITION_AFTER); + assertWithMessage("markShape").that(textEmphasis.markShape) + .isEqualTo(TextEmphasisSpan.MARK_SHAPE_DOT); + assertWithMessage("markFill").that(textEmphasis.markFill) + .isEqualTo(TextEmphasisSpan.MARK_FILL_FILLED); + assertWithMessage("position").that(textEmphasis.position) + .isEqualTo(TextAnnotation.POSITION_AFTER); } @Test public void testFilledCircleAfter() { String value = "filled circle after"; - TextEmphasis textEmphasis = createTextEmphasis(value); - + @Nullable TextEmphasis textEmphasis = createTextEmphasis(value); assertWithMessage("Text Emphasis must exist").that(textEmphasis).isNotNull(); - assertThat(textEmphasis.mark).isEqualTo(TextEmphasisSpan.MARK_FILLED_CIRCLE); - assertThat(textEmphasis.position).isEqualTo(TextAnnotation.POSITION_AFTER); + assertWithMessage("markShape").that(textEmphasis.markShape) + .isEqualTo(TextEmphasisSpan.MARK_SHAPE_CIRCLE); + assertWithMessage("markFill").that(textEmphasis.markFill) + .isEqualTo(TextEmphasisSpan.MARK_FILL_FILLED); + assertWithMessage("position").that(textEmphasis.position) + .isEqualTo(TextAnnotation.POSITION_AFTER); } @Test public void testFilledSesameAfter() { String value = "filled sesame After"; - TextEmphasis textEmphasis = createTextEmphasis(value); - + @Nullable TextEmphasis textEmphasis = createTextEmphasis(value); assertWithMessage("Text Emphasis must exist").that(textEmphasis).isNotNull(); - assertThat(textEmphasis.mark).isEqualTo(TextEmphasisSpan.MARK_FILLED_SESAME); - assertThat(textEmphasis.position).isEqualTo(TextAnnotation.POSITION_AFTER); + assertWithMessage("markShape").that(textEmphasis.markShape) + .isEqualTo(TextEmphasisSpan.MARK_SHAPE_SESAME); + assertWithMessage("markFill").that(textEmphasis.markFill) + .isEqualTo(TextEmphasisSpan.MARK_FILL_FILLED); + assertWithMessage("position").that(textEmphasis.position) + .isEqualTo(TextAnnotation.POSITION_AFTER); } @Test public void testFilledDotBefore() { String value = "filled dot before"; - TextEmphasis textEmphasis = createTextEmphasis(value); - + @Nullable TextEmphasis textEmphasis = createTextEmphasis(value); assertWithMessage("Text Emphasis must exist").that(textEmphasis).isNotNull(); - assertThat(textEmphasis.mark).isEqualTo(TextEmphasisSpan.MARK_FILLED_DOT); - assertThat(textEmphasis.position).isEqualTo(TextAnnotation.POSITION_BEFORE); + assertWithMessage("markShape").that(textEmphasis.markShape) + .isEqualTo(TextEmphasisSpan.MARK_SHAPE_DOT); + assertWithMessage("markFill").that(textEmphasis.markFill) + .isEqualTo(TextEmphasisSpan.MARK_FILL_FILLED); + assertWithMessage("position").that(textEmphasis.position) + .isEqualTo(TextAnnotation.POSITION_BEFORE); } @Test public void testFilledCircleBefore() { String value = "filled circle Before"; - TextEmphasis textEmphasis = createTextEmphasis(value); - + @Nullable TextEmphasis textEmphasis = createTextEmphasis(value); assertWithMessage("Text Emphasis must exist").that(textEmphasis).isNotNull(); - assertThat(textEmphasis.mark).isEqualTo(TextEmphasisSpan.MARK_FILLED_CIRCLE); - assertThat(textEmphasis.position).isEqualTo(TextAnnotation.POSITION_BEFORE); + assertWithMessage("markShape").that(textEmphasis.markShape) + .isEqualTo(TextEmphasisSpan.MARK_SHAPE_CIRCLE); + assertWithMessage("markFill").that(textEmphasis.markFill) + .isEqualTo(TextEmphasisSpan.MARK_FILL_FILLED); + assertWithMessage("position").that(textEmphasis.position) + .isEqualTo(TextAnnotation.POSITION_BEFORE); } @Test public void testFilledSesameBefore() { String value = "filled sesame Before"; - TextEmphasis textEmphasis = createTextEmphasis(value); - + @Nullable TextEmphasis textEmphasis = createTextEmphasis(value); assertWithMessage("Text Emphasis must exist").that(textEmphasis).isNotNull(); - assertThat(textEmphasis.mark).isEqualTo(TextEmphasisSpan.MARK_FILLED_SESAME); - assertThat(textEmphasis.position).isEqualTo(TextAnnotation.POSITION_BEFORE); + assertWithMessage("markShape").that(textEmphasis.markShape) + .isEqualTo(TextEmphasisSpan.MARK_SHAPE_SESAME); + assertWithMessage("markFill").that(textEmphasis.markFill) + .isEqualTo(TextEmphasisSpan.MARK_FILL_FILLED); + assertWithMessage("position").that(textEmphasis.position) + .isEqualTo(TextAnnotation.POSITION_BEFORE); } @Test public void testBeforeFilledSesame() { String value = "before filled sesame"; - TextEmphasis textEmphasis = createTextEmphasis(value); - + @Nullable TextEmphasis textEmphasis = createTextEmphasis(value); assertWithMessage("Text Emphasis must exist").that(textEmphasis).isNotNull(); - assertThat(textEmphasis.mark).isEqualTo(TextEmphasisSpan.MARK_FILLED_SESAME); - assertThat(textEmphasis.position).isEqualTo(TextAnnotation.POSITION_BEFORE); + assertWithMessage("markShape").that(textEmphasis.markShape) + .isEqualTo(TextEmphasisSpan.MARK_SHAPE_SESAME); + assertWithMessage("markFill").that(textEmphasis.markFill) + .isEqualTo(TextEmphasisSpan.MARK_FILL_FILLED); + assertWithMessage("position").that(textEmphasis.position) + .isEqualTo(TextAnnotation.POSITION_BEFORE); } @Test public void testBeforeSesameFilled() { String value = "before sesame filled"; - TextEmphasis textEmphasis = createTextEmphasis(value); - + @Nullable TextEmphasis textEmphasis = createTextEmphasis(value); assertWithMessage("Text Emphasis must exist").that(textEmphasis).isNotNull(); - assertThat(textEmphasis.mark).isEqualTo(TextEmphasisSpan.MARK_FILLED_SESAME); - assertThat(textEmphasis.position).isEqualTo(TextAnnotation.POSITION_BEFORE); + assertWithMessage("markShape").that(textEmphasis.markShape) + .isEqualTo(TextEmphasisSpan.MARK_SHAPE_SESAME); + assertWithMessage("markFill").that(textEmphasis.markFill) + .isEqualTo(TextEmphasisSpan.MARK_FILL_FILLED); + assertWithMessage("position").that(textEmphasis.position) + .isEqualTo(TextAnnotation.POSITION_BEFORE); + } + + @Test + public void testInvalidMarkShape() { + String value = "before sesamee filled"; + @Nullable TextEmphasis textEmphasis = createTextEmphasis(value); + assertWithMessage("Text Emphasis must exist").that(textEmphasis).isNotNull(); + assertWithMessage("markShape").that(textEmphasis.markShape) + .isEqualTo(TextEmphasisSpan.MARK_SHAPE_CIRCLE); + assertWithMessage("markFill").that(textEmphasis.markFill) + .isEqualTo(TextEmphasisSpan.MARK_FILL_FILLED); + assertWithMessage("position").that(textEmphasis.position) + .isEqualTo(TextAnnotation.POSITION_BEFORE); + } + + @Test + public void testInvalidMarkFill() { + String value = "before sesame filed"; + @Nullable TextEmphasis textEmphasis = createTextEmphasis(value); + assertWithMessage("Text Emphasis must exist").that(textEmphasis).isNotNull(); + assertWithMessage("markShape").that(textEmphasis.markShape) + .isEqualTo(TextEmphasisSpan.MARK_SHAPE_SESAME); + assertWithMessage("markFill").that(textEmphasis.markFill) + .isEqualTo(TextEmphasisSpan.MARK_FILL_FILLED); + assertWithMessage("position").that(textEmphasis.position) + .isEqualTo(TextAnnotation.POSITION_BEFORE); + } + + @Test + public void testInvalidPosition() { + String value = "befour sesame filled"; + @Nullable TextEmphasis textEmphasis = createTextEmphasis(value); + assertWithMessage("Text Emphasis must exist").that(textEmphasis).isNotNull(); + assertWithMessage("markShape").that(textEmphasis.markShape) + .isEqualTo(TextEmphasisSpan.MARK_SHAPE_SESAME); + assertWithMessage("markFill").that(textEmphasis.markFill) + .isEqualTo(TextEmphasisSpan.MARK_FILL_FILLED); + assertWithMessage("position").that(textEmphasis.position).isEqualTo(POSITION_OUTSIDE); } } diff --git a/library/core/src/test/java/com/google/android/exoplayer2/text/ttml/TtmlDecoderTest.java b/library/core/src/test/java/com/google/android/exoplayer2/text/ttml/TtmlDecoderTest.java index c4245572be..f9711a854e 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/text/ttml/TtmlDecoderTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/text/ttml/TtmlDecoderTest.java @@ -682,68 +682,80 @@ public final class TtmlDecoderTest { TtmlSubtitle subtitle = getSubtitle(TEXT_EMPHASIS_FILE); Spanned firstCue = getOnlyCueTextAtTimeUs(subtitle, 10_000_000); - assertThat(firstCue).hasNoSpans(); + assertThat(firstCue).hasTextEmphasisSpanBetween("None ".length(), "None おはよ".length()) + .withMarkAndPosition(TextEmphasisSpan.MARK_SHAPE_NONE, + TextEmphasisSpan.MARK_FILL_UNSPECIFIED, + TextAnnotation.POSITION_BEFORE); Spanned secondCue = getOnlyCueTextAtTimeUs(subtitle, 20_000_000); assertThat(secondCue) .hasTextEmphasisSpanBetween("Auto ".length(), "Auto ございます".length()) - .withMarkAndPosition(TextEmphasisSpan.MARK_FILLED_CIRCLE, + .withMarkAndPosition(TextEmphasisSpan.MARK_SHAPE_CIRCLE, + TextEmphasisSpan.MARK_FILL_UNSPECIFIED, TextAnnotation.POSITION_BEFORE); Spanned thirdCue = getOnlyCueTextAtTimeUs(subtitle, 30_000_000); assertThat(thirdCue) .hasTextEmphasisSpanBetween("Filled circle ".length(), "Filled circle こんばんは".length()) - .withMarkAndPosition(TextEmphasisSpan.MARK_FILLED_CIRCLE, + .withMarkAndPosition(TextEmphasisSpan.MARK_SHAPE_CIRCLE, TextEmphasisSpan.MARK_FILL_FILLED, TextAnnotation.POSITION_BEFORE); Spanned fourthCue = getOnlyCueTextAtTimeUs(subtitle, 40_000_000); assertThat(fourthCue) .hasTextEmphasisSpanBetween("Filled dot ".length(), "Filled dot ございます".length()) - .withMarkAndPosition(TextEmphasisSpan.MARK_FILLED_DOT, TextAnnotation.POSITION_BEFORE); + .withMarkAndPosition(TextEmphasisSpan.MARK_SHAPE_DOT, TextEmphasisSpan.MARK_FILL_FILLED, + TextAnnotation.POSITION_BEFORE); Spanned fifthCue = getOnlyCueTextAtTimeUs(subtitle, 50_000_000); assertThat(fifthCue) .hasTextEmphasisSpanBetween("Filled sesame ".length(), "Filled sesame おはよ".length()) - .withMarkAndPosition(TextEmphasisSpan.MARK_FILLED_SESAME, + .withMarkAndPosition(TextEmphasisSpan.MARK_SHAPE_SESAME, TextEmphasisSpan.MARK_FILL_FILLED, TextAnnotation.POSITION_BEFORE); Spanned sixthCue = getOnlyCueTextAtTimeUs(subtitle, 60_000_000); assertThat(sixthCue) .hasTextEmphasisSpanBetween("Open circle before ".length(), "Open circle before ございます".length()) - .withMarkAndPosition(TextEmphasisSpan.MARK_OPEN_CIRCLE, TextAnnotation.POSITION_BEFORE); + .withMarkAndPosition(TextEmphasisSpan.MARK_SHAPE_CIRCLE, TextEmphasisSpan.MARK_FILL_OPEN, + TextAnnotation.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, TextAnnotation.POSITION_AFTER); + .withMarkAndPosition(TextEmphasisSpan.MARK_SHAPE_DOT, TextEmphasisSpan.MARK_FILL_OPEN, + TextAnnotation.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, TextAnnotation.POSITION_BEFORE); + .withMarkAndPosition(TextEmphasisSpan.MARK_SHAPE_SESAME, TextEmphasisSpan.MARK_FILL_OPEN, + TextAnnotation.POSITION_BEFORE); Spanned ninthCue = getOnlyCueTextAtTimeUs(subtitle, 90_000_000); assertThat(ninthCue) .hasTextEmphasisSpanBetween("Auto outside ".length(), "Auto outside おはよ".length()) - .withMarkAndPosition(TextEmphasisSpan.MARK_FILLED_CIRCLE, + .withMarkAndPosition(TextEmphasisSpan.MARK_SHAPE_CIRCLE, + TextEmphasisSpan.MARK_FILL_UNSPECIFIED, TextAnnotation.POSITION_BEFORE); Spanned tenthCue = getOnlyCueTextAtTimeUs(subtitle, 100_000_000); assertThat(tenthCue) .hasTextEmphasisSpanBetween("Circle before ".length(), "Circle before ございます".length()) - .withMarkAndPosition(TextEmphasisSpan.MARK_FILLED_CIRCLE, TextAnnotation.POSITION_BEFORE); + .withMarkAndPosition(TextEmphasisSpan.MARK_SHAPE_CIRCLE, TextEmphasisSpan.MARK_FILL_FILLED, + TextAnnotation.POSITION_BEFORE); Spanned eleventhCue = getOnlyCueTextAtTimeUs(subtitle, 110_000_000); assertThat(eleventhCue) .hasTextEmphasisSpanBetween("Sesame after ".length(), "Sesame after おはよ".length()) - .withMarkAndPosition(TextEmphasisSpan.MARK_FILLED_SESAME, TextAnnotation.POSITION_AFTER); + .withMarkAndPosition(TextEmphasisSpan.MARK_SHAPE_SESAME, TextEmphasisSpan.MARK_FILL_FILLED, + TextAnnotation.POSITION_AFTER); Spanned twelfthCue = getOnlyCueTextAtTimeUs(subtitle, 120_000_000); assertThat(twelfthCue) .hasTextEmphasisSpanBetween("Dot outside ".length(), "Dot outside ございます".length()) - .withMarkAndPosition(TextEmphasisSpan.MARK_FILLED_DOT, TextAnnotation.POSITION_BEFORE); + .withMarkAndPosition(TextEmphasisSpan.MARK_SHAPE_DOT, TextEmphasisSpan.MARK_FILL_FILLED, + TextAnnotation.POSITION_BEFORE); Spanned thirteenthCue = getOnlyCueTextAtTimeUs(subtitle, 130_000_000); assertThat(thirteenthCue) @@ -753,25 +765,29 @@ public final class TtmlDecoderTest { Spanned fourteenthCue = getOnlyCueTextAtTimeUs(subtitle, 140_000_000); assertThat(fourteenthCue) .hasTextEmphasisSpanBetween("Auto (TBLR) ".length(), "Auto (TBLR) ございます".length()) - .withMarkAndPosition(TextEmphasisSpan.MARK_FILLED_SESAME, + .withMarkAndPosition(TextEmphasisSpan.MARK_SHAPE_SESAME, + TextEmphasisSpan.MARK_FILL_UNSPECIFIED, TextAnnotation.POSITION_BEFORE); Spanned fifteenthCue = getOnlyCueTextAtTimeUs(subtitle, 150_000_000); assertThat(fifteenthCue) .hasTextEmphasisSpanBetween("Auto (TBRL) ".length(), "Auto (TBRL) おはよ".length()) - .withMarkAndPosition(TextEmphasisSpan.MARK_FILLED_SESAME, + .withMarkAndPosition(TextEmphasisSpan.MARK_SHAPE_SESAME, + TextEmphasisSpan.MARK_FILL_UNSPECIFIED, TextAnnotation.POSITION_BEFORE); Spanned sixteenthCue = getOnlyCueTextAtTimeUs(subtitle, 160_000_000); assertThat(sixteenthCue) .hasTextEmphasisSpanBetween("Auto (TB) ".length(), "Auto (TB) ございます".length()) - .withMarkAndPosition(TextEmphasisSpan.MARK_FILLED_SESAME, + .withMarkAndPosition(TextEmphasisSpan.MARK_SHAPE_SESAME, + TextEmphasisSpan.MARK_FILL_UNSPECIFIED, TextAnnotation.POSITION_BEFORE); Spanned seventeenthCue = getOnlyCueTextAtTimeUs(subtitle, 170_000_000); assertThat(seventeenthCue) .hasTextEmphasisSpanBetween("Auto (LR) ".length(), "Auto (LR) おはよ".length()) - .withMarkAndPosition(TextEmphasisSpan.MARK_FILLED_CIRCLE, + .withMarkAndPosition(TextEmphasisSpan.MARK_SHAPE_CIRCLE, + TextEmphasisSpan.MARK_FILL_UNSPECIFIED, TextAnnotation.POSITION_BEFORE); } 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 8f6556c463..22b39665e9 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 @@ -17,8 +17,6 @@ package com.google.android.exoplayer2.text.ttml; import static android.graphics.Color.BLACK; import static com.google.android.exoplayer2.text.span.TextAnnotation.POSITION_BEFORE; -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.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; @@ -32,6 +30,7 @@ import android.text.Layout; import androidx.annotation.ColorInt; import androidx.test.ext.junit.runners.AndroidJUnit4; import com.google.android.exoplayer2.text.span.TextAnnotation; +import com.google.android.exoplayer2.text.span.TextEmphasisSpan; import org.junit.Test; import org.junit.runner.RunWith; @@ -92,7 +91,8 @@ public final class TtmlStyleTest { .that(style.hasBackgroundColor()) .isFalse(); assertThat(style.getTextEmphasis()).isNotNull(); - assertThat(style.getTextEmphasis().mark).isEqualTo(MARK_FILLED_DOT); + assertThat(style.getTextEmphasis().markShape).isEqualTo(TextEmphasisSpan.MARK_SHAPE_DOT); + assertThat(style.getTextEmphasis().markFill).isEqualTo(TextEmphasisSpan.MARK_FILL_FILLED); assertThat(style.getTextEmphasis().position).isEqualTo(POSITION_BEFORE); } @@ -118,7 +118,8 @@ public final class TtmlStyleTest { .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().markShape).isEqualTo(TextEmphasisSpan.MARK_SHAPE_DOT); + assertThat(style.getTextEmphasis().markFill).isEqualTo(TextEmphasisSpan.MARK_FILL_FILLED); assertThat(style.getTextEmphasis().position).isEqualTo(POSITION_BEFORE); } @@ -262,7 +263,8 @@ public final class TtmlStyleTest { 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().markShape).isEqualTo(TextEmphasisSpan.MARK_SHAPE_SESAME); + assertThat(style.getTextEmphasis().markFill).isEqualTo(TextEmphasisSpan.MARK_FILL_OPEN); assertThat(style.getTextEmphasis().position).isEqualTo(TextAnnotation.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 0c81ac1931..13d78c10c8 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 @@ -201,12 +201,13 @@ import java.util.regex.Pattern; return ""; } else if (span instanceof TextEmphasisSpan) { TextEmphasisSpan textEmphasisSpan = (TextEmphasisSpan) span; - String style = getTextEmphasisStyle(textEmphasisSpan.mark); + String style = getTextEmphasisStyle(textEmphasisSpan.markShape, textEmphasisSpan.markFill); String position = getTextEmphasisPosition(textEmphasisSpan.position); return Util - .formatInvariant("", - style, style, position, position); + .formatInvariant( + "", + style, position); } else { return null; } @@ -243,23 +244,39 @@ 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"; + private static String getTextEmphasisStyle(@TextEmphasisSpan.MarkShape int shape, + @TextEmphasisSpan.MarkFill int fill) { + StringBuilder builder = new StringBuilder(); + switch (fill) { + case TextEmphasisSpan.MARK_FILL_FILLED: + builder.append("filled "); + break; + case TextEmphasisSpan.MARK_FILL_OPEN: + builder.append("open "); + break; + case TextEmphasisSpan.MARK_FILL_UNSPECIFIED: default: - return "unset"; + break; } + + switch (shape) { + case TextEmphasisSpan.MARK_SHAPE_CIRCLE: + builder.append("circle"); + break; + case TextEmphasisSpan.MARK_SHAPE_DOT: + builder.append("dot"); + break; + case TextEmphasisSpan.MARK_SHAPE_SESAME: + builder.append("sesame"); + break; + case TextEmphasisSpan.MARK_SHAPE_NONE: + builder.append("none"); + break; + default: + builder.append("unset"); + break; + } + return builder.toString(); } private static String getTextEmphasisPosition(@TextAnnotation.Position int position){ diff --git a/library/ui/src/test/java/com/google/android/exoplayer2/ui/SpannedToHtmlConverterTest.java b/library/ui/src/test/java/com/google/android/exoplayer2/ui/SpannedToHtmlConverterTest.java index f3b95e9d42..49062d2233 100644 --- a/library/ui/src/test/java/com/google/android/exoplayer2/ui/SpannedToHtmlConverterTest.java +++ b/library/ui/src/test/java/com/google/android/exoplayer2/ui/SpannedToHtmlConverterTest.java @@ -285,13 +285,15 @@ public class SpannedToHtmlConverterTest { public void convert_supportsTextEmphasisSpan() { SpannableString spanned = new SpannableString("Text emphasis おはよ ございます "); spanned.setSpan( - new TextEmphasisSpan(TextEmphasisSpan.MARK_FILLED_CIRCLE, TextAnnotation.POSITION_BEFORE), + new TextEmphasisSpan(TextEmphasisSpan.MARK_SHAPE_CIRCLE, TextEmphasisSpan.MARK_FILL_FILLED, + TextAnnotation.POSITION_BEFORE), "Text emphasis ".length(), "Text emphasis おはよ".length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); spanned.setSpan( - new TextEmphasisSpan(TextEmphasisSpan.MARK_OPEN_SESAME, TextAnnotation.POSITION_AFTER), + new TextEmphasisSpan(TextEmphasisSpan.MARK_SHAPE_SESAME, TextEmphasisSpan.MARK_FILL_OPEN, + TextAnnotation.POSITION_AFTER), "Text emphasis おはよ ".length(), "Text emphasis おはよ ございます ".length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/truth/SpannedSubject.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/truth/SpannedSubject.java index 20109d29b9..b9f05c9e87 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/truth/SpannedSubject.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/truth/SpannedSubject.java @@ -1156,20 +1156,21 @@ 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 markShape The expected mark shape + * @param markFill The expected mark fill * @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, + AndSpanFlags withMarkAndPosition(@TextEmphasisSpan.MarkShape int markShape, + @TextEmphasisSpan.MarkFill int markFill, @TextAnnotation.Position int position); } private static final TextEmphasisDescription ALREADY_FAILED_WITH_MARK = - (mark, position) -> ALREADY_FAILED_AND_FLAGS; + (markShape, markFill, position) -> ALREADY_FAILED_AND_FLAGS; private static Factory> textEmphasisSubjects(Spanned actualSpanned) { return (FailureMetadata metadata, List spans) -> @@ -1189,32 +1190,38 @@ public final class SpannedSubject extends Subject { } @Override - public AndSpanFlags withMarkAndPosition(@TextEmphasisSpan.Mark int mark, + public AndSpanFlags withMarkAndPosition(@TextEmphasisSpan.MarkShape int markShape, + @TextEmphasisSpan.MarkFill int markFill, @TextAnnotation.Position int position) { List matchingSpanFlags = new ArrayList<>(); List textEmphasisMarksAndPositions = new ArrayList<>(); for (TextEmphasisSpan span : actualSpans) { - textEmphasisMarksAndPositions.add(new MarkAndPosition(span.mark, span.position)); - if (span.mark == mark && span.position == position) { + textEmphasisMarksAndPositions + .add(new MarkAndPosition(span.markShape, span.markFill, span.position)); + if (span.markFill == markFill && span.markShape == markShape && span.position == position) { matchingSpanFlags.add(actualSpanned.getSpanFlags(span)); } } check("textEmphasisMarkAndPosition") .that(textEmphasisMarksAndPositions) - .containsExactly(new MarkAndPosition(mark, position)); + .containsExactly(new MarkAndPosition(markShape, markFill, position)); return check("flags").about(spanFlags()).that(matchingSpanFlags); } private static final class MarkAndPosition { - @TextEmphasisSpan.Mark - private final int mark; + @TextEmphasisSpan.MarkShape + private final int markShape; + @TextEmphasisSpan.MarkFill + private final int markFill; @TextAnnotation.Position private final int position; - private MarkAndPosition(@TextEmphasisSpan.Mark int mark, + private MarkAndPosition(@TextEmphasisSpan.MarkShape int markShape, + @TextEmphasisSpan.MarkFill int markFill, @TextAnnotation.Position int position) { - this.mark = mark; + this.markFill = markFill; + this.markShape = markShape; this.position = position; } @@ -1228,18 +1235,20 @@ public final class SpannedSubject extends Subject { } TextEmphasisSubject.MarkAndPosition that = (TextEmphasisSubject.MarkAndPosition) o; - return (position == that.position) && (mark == that.mark); + return (position == that.position) && (markShape == that.markShape) && (markFill + == that.markFill); } @Override public int hashCode() { - int result = 34613 * mark + position; + int result = 34613 * markFill + 1993 * markShape + position; return result; } @Override public String toString() { - return String.format("{mark=%s,position=%s}", mark, position); + return String + .format("{markShape=%s, markFill=%s, position=%s}", markShape, markFill, position); } } } From 671032204f68e1930b8c99dbe95e39c20838b37a Mon Sep 17 00:00:00 2001 From: Denise LaFayette Date: Mon, 8 Mar 2021 10:12:11 -0800 Subject: [PATCH 5/6] Remove unused code --- .../android/exoplayer2/text/ttml/TextEmphasis.java | 9 --------- 1 file changed, 9 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/ttml/TextEmphasis.java b/library/core/src/main/java/com/google/android/exoplayer2/text/ttml/TextEmphasis.java index 5069f80e4c..f282d705bf 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/ttml/TextEmphasis.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/ttml/TextEmphasis.java @@ -115,15 +115,6 @@ import java.util.Set; this.position = position; } - @Override - public String toString() { - return "TextEmphasis{" + - "position=" + position + - ", markShape=" + markShape + - ", markFill=" + markFill + - '}'; - } - @Nullable public static TextEmphasis createTextEmphasis(@Nullable String value) { if (value == null) { return null; From 0f0e0c974be364cf5675811d71dbddfbe92666b9 Mon Sep 17 00:00:00 2001 From: Denise LaFayette Date: Tue, 9 Mar 2021 14:01:32 -0800 Subject: [PATCH 6/6] Fix missing background color behind emphasis mark Add more comments and tests for TextEmphasis --- library/common/jacoco.exec | Bin 0 -> 7602 bytes .../exoplayer2/text/ttml/TextEmphasis.java | 17 +++++++- .../text/ttml/TextEmphasisTest.java | 39 +++++++++++++++++- .../exoplayer2/ui/SpannedToHtmlConverter.java | 3 +- .../ui/SpannedToHtmlConverterTest.java | 15 ++++--- 5 files changed, 62 insertions(+), 12 deletions(-) create mode 100644 library/common/jacoco.exec diff --git a/library/common/jacoco.exec b/library/common/jacoco.exec new file mode 100644 index 0000000000000000000000000000000000000000..2fbf213ff6f2bf6bc867f31b6fcb6711689c4fe6 GIT binary patch literal 7602 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 literal 0 HcmV?d00001 diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/ttml/TextEmphasis.java b/library/core/src/main/java/com/google/android/exoplayer2/text/ttml/TextEmphasis.java index f282d705bf..1d53adb3ab 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/ttml/TextEmphasis.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/ttml/TextEmphasis.java @@ -132,6 +132,21 @@ import java.util.Set; return parseNodes(nodes); } + /** + * Parses the text emphasis description + * See https://www.w3.org/TR/ttml2/#style-attribute-textEmphasis + * + * The parser considers emphasis-style and emphasis-position independently + * If a valid style is not found, it reverts to the default style. + * If a valid position is not found, it reverts to the default position. + * + * Not implemented: + * - emphasis-color + * - quoted string emphasis-style + * + * @param nodes - the text emphasis description + * @return TextEmphasis object encapsulating the text emphasis description + */ private static @Nullable TextEmphasis parseNodes(Set nodes) { @MarkShape int markShape; @TextEmphasisSpan.MarkFill int markFill = TextEmphasisSpan.MARK_FILL_UNSPECIFIED; @@ -141,11 +156,9 @@ import java.util.Set; // attributes. markShape = TtmlNode.TEXT_EMPHASIS_NONE.equals(styleSet.iterator().next()) ? TextEmphasisSpan.MARK_SHAPE_NONE : MARK_SHAPE_AUTO; - // markFill is ignored when markShape is NONE or AUTO } else { Set fillSet = Sets.intersection(markFillValues, nodes).immutableCopy(); Set shapeSet = Sets.intersection(markShapeValues, nodes).immutableCopy(); - if (fillSet.size() == 0 && shapeSet.size() == 0) { // If an implementation does not recognize or otherwise distinguish an emphasis style value, // then it must be interpreted as if a style of auto were specified; as such, an diff --git a/library/core/src/test/java/com/google/android/exoplayer2/text/ttml/TextEmphasisTest.java b/library/core/src/test/java/com/google/android/exoplayer2/text/ttml/TextEmphasisTest.java index 128a2b2ea0..891422a8ea 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/text/ttml/TextEmphasisTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/text/ttml/TextEmphasisTest.java @@ -73,7 +73,6 @@ public class TextEmphasisTest { .isEqualTo(TextEmphasis.POSITION_OUTSIDE); } - @Test public void testAutoOutside() { String value = "auto outside"; @@ -560,4 +559,42 @@ public class TextEmphasisTest { .isEqualTo(TextEmphasisSpan.MARK_FILL_FILLED); assertWithMessage("position").that(textEmphasis.position).isEqualTo(POSITION_OUTSIDE); } + + @Test + public void testValidMixedWithInvalidDescription() { + String value = "blue open sesame foo bar after"; + @Nullable TextEmphasis textEmphasis = createTextEmphasis(value); + assertWithMessage("Text Emphasis must exist").that(textEmphasis).isNotNull(); + assertWithMessage("markShape").that(textEmphasis.markShape) + .isEqualTo(TextEmphasisSpan.MARK_SHAPE_SESAME); + assertWithMessage("markFill").that(textEmphasis.markFill) + .isEqualTo(TextEmphasisSpan.MARK_FILL_OPEN); + assertWithMessage("position").that(textEmphasis.position).isEqualTo( + TextAnnotation.POSITION_AFTER); + } + + @Test + public void testColorDescriptionNotSupported() { + String value = "blue"; + @Nullable TextEmphasis textEmphasis = createTextEmphasis(value); + assertWithMessage("Text Emphasis must exist").that(textEmphasis).isNotNull(); + assertWithMessage("markShape").that(textEmphasis.markShape) + .isEqualTo(MARK_SHAPE_AUTO); + assertWithMessage("markFill").that(textEmphasis.markFill) + .isEqualTo(TextEmphasisSpan.MARK_FILL_UNSPECIFIED); + assertWithMessage("position").that(textEmphasis.position).isEqualTo(POSITION_OUTSIDE); + } + + @Test + public void testQuotedStringStyleNotSupported() { + String value = "\"x\" after"; + @Nullable TextEmphasis textEmphasis = createTextEmphasis(value); + assertWithMessage("Text Emphasis must exist").that(textEmphasis).isNotNull(); + assertWithMessage("markShape").that(textEmphasis.markShape) + .isEqualTo(MARK_SHAPE_AUTO); + assertWithMessage("markFill").that(textEmphasis.markFill) + .isEqualTo(TextEmphasisSpan.MARK_FILL_UNSPECIFIED); + assertWithMessage("position").that(textEmphasis.position) + .isEqualTo(TextAnnotation.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 13d78c10c8..9fc4bbb63d 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 @@ -206,7 +206,8 @@ import java.util.regex.Pattern; return Util .formatInvariant( "", + + "-webkit-text-emphasis-position: %2$s; text-emphasis-position: %2$s; " + + "display: inline-block;'>", /** Sets background color behind emphasis mark */ style, position); } else { return null; diff --git a/library/ui/src/test/java/com/google/android/exoplayer2/ui/SpannedToHtmlConverterTest.java b/library/ui/src/test/java/com/google/android/exoplayer2/ui/SpannedToHtmlConverterTest.java index 49062d2233..fb4a1c1cc3 100644 --- a/library/ui/src/test/java/com/google/android/exoplayer2/ui/SpannedToHtmlConverterTest.java +++ b/library/ui/src/test/java/com/google/android/exoplayer2/ui/SpannedToHtmlConverterTest.java @@ -283,19 +283,19 @@ public class SpannedToHtmlConverterTest { @Test public void convert_supportsTextEmphasisSpan() { - SpannableString spanned = new SpannableString("Text emphasis おはよ ございます "); + SpannableString spanned = new SpannableString("Text emphasis おはよ ございます"); spanned.setSpan( new TextEmphasisSpan(TextEmphasisSpan.MARK_SHAPE_CIRCLE, TextEmphasisSpan.MARK_FILL_FILLED, TextAnnotation.POSITION_BEFORE), "Text emphasis ".length(), - "Text emphasis おはよ".length(), + "Text emphasis おはよ ".length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); spanned.setSpan( new TextEmphasisSpan(TextEmphasisSpan.MARK_SHAPE_SESAME, TextEmphasisSpan.MARK_FILL_OPEN, TextAnnotation.POSITION_AFTER), "Text emphasis おはよ ".length(), - "Text emphasis おはよ ございます ".length(), + "Text emphasis おはよ ございます".length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); SpannedToHtmlConverter.HtmlAndCss htmlAndCss = @@ -306,12 +306,11 @@ public class SpannedToHtmlConverterTest { .isEqualTo( "Text emphasis おはよ " - + "おはよ ございます "); + + "-webkit-text-emphasis-position: under left; text-emphasis-position: under left; " + + "display: inline-block;'>ございます"); } @Test