Merge pull request #8653 from dlafayet:textemphasis

PiperOrigin-RevId: 364363882
This commit is contained in:
Ian Baker 2021-03-24 18:05:46 +00:00
commit 6c688891e3
21 changed files with 1940 additions and 127 deletions

View File

@ -71,6 +71,7 @@
[#8456](https://github.com/google/ExoPlayer/issues/8456)).
* Fix CEA-708 priority handling to sort cues in the order defined by the
spec ([#8704](https://github.com/google/ExoPlayer/issues/8704)).
* Support TTML `textEmphasis` attributes, used for Japanese boutens.
* MediaSession extension: Remove dependency to core module and rely on common
only. The `TimelineQueueEditor` uses a new `MediaDescriptionConverter` for
this purpose and does not rely on the `ConcatenatingMediaSource` anymore.

View File

@ -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,13 @@ import java.lang.annotation.Retention;
// rubies (e.g. HTML <rp> 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.
*
* <p>For vertical text it should be positioned to the right, same as CSS's <a
* href="https://developer.mozilla.org/en-US/docs/Web/CSS/ruby-position">ruby-position</a>.
*/
public static final int POSITION_OVER = 1;
/**
* The ruby text should be positioned below the base text.
*
* <p>For vertical text it should be positioned to the left, same as CSS's <a
* href="https://developer.mozilla.org/en-US/docs/Web/CSS/ruby-position">ruby-position</a>.
*/
public static final int POSITION_UNDER = 2;
/**
* The possible positions of the ruby text relative to the base text.
*
* <p>One of:
*
* <ul>
* <li>{@link #POSITION_UNKNOWN}
* <li>{@link #POSITION_OVER}
* <li>{@link #POSITION_UNDER}
* </ul>
*/
@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;
}

View File

@ -0,0 +1,62 @@
/*
* Copyright 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;
/** Properties of a text annotation (i.e. ruby, text emphasis marks). */
public final 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.
*
* <p>For vertical text it should be positioned to the right, same as CSS's <a
* href="https://developer.mozilla.org/en-US/docs/Web/CSS/ruby-position">ruby-position</a>.
*/
public static final int POSITION_BEFORE = 1;
/**
* For horizontal text, the text annotation should be positioned below the base text.
*
* <p>For vertical text it should be positioned to the left, same as CSS's <a
* href="https://developer.mozilla.org/en-US/docs/Web/CSS/ruby-position">ruby-position</a>.
*/
public static final int POSITION_AFTER = 2;
/**
* The possible positions of the annotation text relative to the base text.
*
* <p>One of:
*
* <ul>
* <li>{@link #POSITION_UNKNOWN}
* <li>{@link #POSITION_BEFORE}
* <li>{@link #POSITION_AFTER}
* </ul>
*/
@Documented
@Retention(SOURCE)
@IntDef({POSITION_UNKNOWN, POSITION_BEFORE, POSITION_AFTER})
public @interface Position {}
private TextAnnotation() {}
}

View File

@ -0,0 +1,94 @@
/*
* Copyright 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;
/**
* A styling span for text emphasis marks.
*
* <p>These are pronunciation aids such as <a
* href="https://www.w3.org/TR/jlreq/?lang=en#term.emphasis-dots">Japanese boutens</a> which can be
* rendered using the <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/text-emphasis">
* text-emphasis</a> CSS property.
*/
// NOTE: There's no Android layout support for text emphasis, so this span currently doesn't extend
// any styling superclasses (e.g. MetricAffectingSpan). The only way to render this emphasis is to
// extract the spans and do the layout manually.
public final class TextEmphasisSpan {
/**
* The possible mark shapes that can be used.
*
* <p>One of:
*
* <ul>
* <li>{@link #MARK_SHAPE_NONE}
* <li>{@link #MARK_SHAPE_CIRCLE}
* <li>{@link #MARK_SHAPE_DOT}
* <li>{@link #MARK_SHAPE_SESAME}
* </ul>
*/
@Documented
@Retention(SOURCE)
@IntDef({MARK_SHAPE_NONE, MARK_SHAPE_CIRCLE, MARK_SHAPE_DOT, MARK_SHAPE_SESAME})
public @interface MarkShape {}
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 mark fills that can be used.
*
* <p>One of:
*
* <ul>
* <li>{@link #MARK_FILL_UNKNOWN}
* <li>{@link #MARK_FILL_FILLED}
* <li>{@link #MARK_FILL_OPEN}
* </ul>
*/
@Documented
@Retention(SOURCE)
@IntDef({MARK_FILL_UNKNOWN, MARK_FILL_FILLED, MARK_FILL_OPEN})
public @interface MarkFill {}
public static final int MARK_FILL_UNKNOWN = 0;
public static final int MARK_FILL_FILLED = 1;
public static final int MARK_FILL_OPEN = 2;
/** The mark shape used for text emphasis. */
@MarkShape public int markShape;
/** The mark fill for the text emphasis mark. */
@MarkShape public int markFill;
/** The position of the text emphasis relative to the base text. */
@TextAnnotation.Position public final int position;
public TextEmphasisSpan(
@MarkShape int shape, @MarkFill int fill, @TextAnnotation.Position int position) {
this.markShape = shape;
this.markFill = fill;
this.position = position;
}
}

View File

@ -0,0 +1,217 @@
/*
* Copyright 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 java.lang.annotation.RetentionPolicy.SOURCE;
import android.text.TextUtils;
import androidx.annotation.IntDef;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.text.Cue;
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.Iterables;
import com.google.common.collect.Sets;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.util.Set;
import java.util.regex.Pattern;
/**
* Represents a <a
* href="https://www.w3.org/TR/2018/REC-ttml2-20181108/#style-attribute-textEmphasis">
* tts:textEmphasis</a> attribute.
*/
/* package */ final class TextEmphasis {
@Documented
@Retention(SOURCE)
@IntDef({
TextEmphasisSpan.MARK_SHAPE_NONE,
TextEmphasisSpan.MARK_SHAPE_CIRCLE,
TextEmphasisSpan.MARK_SHAPE_DOT,
TextEmphasisSpan.MARK_SHAPE_SESAME,
MARK_SHAPE_AUTO
})
@interface MarkShape {}
/**
* The "auto" mark shape is only defined in TTML and is resolved to a concrete shape when building
* the {@link Cue}. Hence, it is not defined in {@link TextEmphasisSpan.MarkShape}.
*/
public static final int MARK_SHAPE_AUTO = -1;
@Documented
@Retention(SOURCE)
@IntDef({
TextAnnotation.POSITION_UNKNOWN,
TextAnnotation.POSITION_BEFORE,
TextAnnotation.POSITION_AFTER,
POSITION_OUTSIDE
})
public @interface Position {}
/**
* The "outside" position is only defined in TTML and is resolved before outputting a {@link Cue}
* object. Hence, it is not defined in {@link TextAnnotation.Position}.
*/
public static final int POSITION_OUTSIDE = -2;
private static final Pattern WHITESPACE_PATTERN = Pattern.compile("\\s+");
private static final ImmutableSet<String> SINGLE_STYLE_VALUES =
ImmutableSet.of(TtmlNode.TEXT_EMPHASIS_AUTO, TtmlNode.TEXT_EMPHASIS_NONE);
private static final ImmutableSet<String> MARK_SHAPE_VALUES =
ImmutableSet.of(
TtmlNode.TEXT_EMPHASIS_MARK_DOT,
TtmlNode.TEXT_EMPHASIS_MARK_SESAME,
TtmlNode.TEXT_EMPHASIS_MARK_CIRCLE);
private static final ImmutableSet<String> MARK_FILL_VALUES =
ImmutableSet.of(TtmlNode.TEXT_EMPHASIS_MARK_FILLED, TtmlNode.TEXT_EMPHASIS_MARK_OPEN);
private static final ImmutableSet<String> POSITION_VALUES =
ImmutableSet.of(
TtmlNode.ANNOTATION_POSITION_AFTER,
TtmlNode.ANNOTATION_POSITION_BEFORE,
TtmlNode.ANNOTATION_POSITION_OUTSIDE);
/** The text emphasis mark shape. */
@MarkShape public final int markShape;
/** The fill style of the text emphasis mark. */
@TextEmphasisSpan.MarkFill public final int markFill;
/** The position of the text emphasis relative to the base text. */
@Position public final int position;
private TextEmphasis(
@MarkShape int markShape,
@TextEmphasisSpan.MarkFill int markFill,
@TextAnnotation.Position int position) {
this.markShape = markShape;
this.markFill = markFill;
this.position = position;
}
/**
* Parses a TTML <a
* href="https://www.w3.org/TR/2018/REC-ttml2-20181108/#style-attribute-textEmphasis">
* tts:textEmphasis</a> attribute. Returns null if parsing fails.
*
* <p>The parser searches for {@code emphasis-style} and {@code emphasis-position} independently.
* If a valid style is not found, the default style is used. If a valid position is not found, the
* default position is used.
*
* <p>Not implemented:
*
* <ul>
* <li>{@code emphasis-color}
* <li>Quoted string {@code emphasis-style}
* </ul>
*/
@Nullable
public static TextEmphasis parse(@Nullable String value) {
if (value == null) {
return null;
}
String parsingValue = value.trim();
if (parsingValue.isEmpty()) {
return null;
}
return parseWords(ImmutableSet.copyOf(TextUtils.split(parsingValue, WHITESPACE_PATTERN)));
}
private static TextEmphasis parseWords(ImmutableSet<String> nodes) {
Set<String> matchingPositions = Sets.intersection(POSITION_VALUES, nodes);
// 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;
switch (Iterables.getFirst(matchingPositions, TtmlNode.ANNOTATION_POSITION_OUTSIDE)) {
case TtmlNode.ANNOTATION_POSITION_AFTER:
position = TextAnnotation.POSITION_AFTER;
break;
case TtmlNode.ANNOTATION_POSITION_OUTSIDE:
position = POSITION_OUTSIDE;
break;
case TtmlNode.ANNOTATION_POSITION_BEFORE:
default:
// 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:
// https://www.w3.org/TR/2018/REC-ttml2-20181108/#style-attribute-textEmphasis
position = TextAnnotation.POSITION_BEFORE;
}
Set<String> matchingSingleStyles = Sets.intersection(SINGLE_STYLE_VALUES, nodes);
if (!matchingSingleStyles.isEmpty()) {
// If "none" or "auto" are found in the description, ignore the other style (fill, shape)
// attributes.
@MarkShape int markShape;
switch (matchingSingleStyles.iterator().next()) {
case TtmlNode.TEXT_EMPHASIS_NONE:
markShape = TextEmphasisSpan.MARK_SHAPE_NONE;
break;
case TtmlNode.TEXT_EMPHASIS_AUTO:
default:
markShape = MARK_SHAPE_AUTO;
}
// markFill is ignored when markShape is NONE or AUTO
return new TextEmphasis(markShape, TextEmphasisSpan.MARK_FILL_UNKNOWN, position);
}
Set<String> matchingFills = Sets.intersection(MARK_FILL_VALUES, nodes);
Set<String> matchingShapes = Sets.intersection(MARK_SHAPE_VALUES, nodes);
if (matchingFills.isEmpty() && matchingShapes.isEmpty()) {
// 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.
//
// markFill is ignored when markShape is NONE or AUTO.
return new TextEmphasis(MARK_SHAPE_AUTO, TextEmphasisSpan.MARK_FILL_UNKNOWN, position);
}
@TextEmphasisSpan.MarkFill int markFill;
switch (Iterables.getFirst(matchingFills, TtmlNode.TEXT_EMPHASIS_MARK_FILLED)) {
case TtmlNode.TEXT_EMPHASIS_MARK_OPEN:
markFill = TextEmphasisSpan.MARK_FILL_OPEN;
break;
case TtmlNode.TEXT_EMPHASIS_MARK_FILLED:
default:
markFill = TextEmphasisSpan.MARK_FILL_FILLED;
}
@MarkShape int markShape;
switch (Iterables.getFirst(matchingShapes, TtmlNode.TEXT_EMPHASIS_MARK_CIRCLE)) {
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;
}
return new TextEmphasis(markShape, markFill, position);
}
}

View File

@ -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;
@ -582,11 +582,11 @@ public final class TtmlDecoder extends SimpleSubtitleDecoder {
break;
case TtmlNode.ATTR_TTS_RUBY_POSITION:
switch (Util.toLowerInvariant(attributeValue)) {
case TtmlNode.RUBY_BEFORE:
style = createIfNull(style).setRubyPosition(RubySpan.POSITION_OVER);
case TtmlNode.ANNOTATION_POSITION_BEFORE:
style = createIfNull(style).setRubyPosition(TextAnnotation.POSITION_BEFORE);
break;
case TtmlNode.RUBY_AFTER:
style = createIfNull(style).setRubyPosition(RubySpan.POSITION_UNDER);
case TtmlNode.ANNOTATION_POSITION_AFTER:
style = createIfNull(style).setRubyPosition(TextAnnotation.POSITION_AFTER);
break;
default:
// ignore
@ -609,6 +609,11 @@ public final class TtmlDecoder extends SimpleSubtitleDecoder {
break;
}
break;
case TtmlNode.ATTR_TTS_TEXT_EMPHASIS:
style =
createIfNull(style)
.setTextEmphasis(TextEmphasis.parse(Util.toLowerInvariant(attributeValue)));
break;
default:
// ignore
break;

View File

@ -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
@ -79,9 +80,11 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
public static final String RUBY_TEXT_CONTAINER = "textContainer";
public static final String RUBY_DELIMITER = "delimiter";
// Values for rubyPosition
public static final String RUBY_BEFORE = "before";
public static final String RUBY_AFTER = "after";
// Values for text annotation (i.e. ruby, text emphasis) position
public static final String ANNOTATION_POSITION_BEFORE = "before";
public static final String ANNOTATION_POSITION_AFTER = "after";
public static final String ANNOTATION_POSITION_OUTSIDE = "outside";
// Values for textDecoration
public static final String LINETHROUGH = "linethrough";
public static final String NO_LINETHROUGH = "nolinethrough";
@ -106,6 +109,15 @@ 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";
@Nullable public final String tag;
@Nullable public final String text;
public final boolean isTextNode;
@ -243,7 +255,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
TreeMap<String, Cue.Builder> regionTextOutputs = new TreeMap<>();
traverseForText(timeUs, false, regionId, regionTextOutputs);
traverseForStyle(timeUs, globalStyles, regionTextOutputs);
traverseForStyle(timeUs, globalStyles, regionMap, regionId, regionTextOutputs);
List<Cue> cues = new ArrayList<>();
@ -354,26 +366,39 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
}
private void traverseForStyle(
long timeUs, Map<String, TtmlStyle> globalStyles, Map<String, Cue.Builder> regionOutputs) {
long timeUs,
Map<String, TtmlStyle> globalStyles,
Map<String, TtmlRegion> regionMaps,
String inheritedRegion,
Map<String, Cue.Builder> regionOutputs) {
if (!isActive(timeUs)) {
return;
}
String resolvedRegionId = ANONYMOUS_REGION_ID.equals(regionId) ? inheritedRegion : regionId;
for (Map.Entry<String, Integer> 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<String, TtmlStyle> globalStyles, Cue.Builder regionOutput, int start, int end) {
Map<String, TtmlStyle> 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 +406,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());
}
}

View File

@ -15,6 +15,8 @@
*/
package com.google.android.exoplayer2.text.ttml;
import static com.google.android.exoplayer2.util.Assertions.checkNotNull;
import android.text.Spannable;
import android.text.SpannableStringBuilder;
import android.text.Spanned;
@ -27,9 +29,12 @@ 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.TextAnnotation;
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 +88,8 @@ import java.util.Map;
int end,
TtmlStyle style,
@Nullable TtmlNode parent,
Map<String, TtmlStyle> globalStyles) {
Map<String, TtmlStyle> globalStyles,
@Cue.VerticalType int verticalType) {
if (style.getStyle() != TtmlStyle.UNSPECIFIED) {
builder.setSpan(new StyleSpan(style.getStyle()), start, end,
@ -119,6 +125,40 @@ import java.util.Map;
end,
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
}
if (style.getTextEmphasis() != null) {
TextEmphasis textEmphasis = checkNotNull(style.getTextEmphasis());
@TextEmphasisSpan.MarkShape int markShape;
@TextEmphasisSpan.MarkFill int markFill;
if (textEmphasis.markShape == TextEmphasis.MARK_SHAPE_AUTO) {
// If a vertical writing mode applies, then 'auto' is equivalent to 'filled sesame';
// otherwise, it's equivalent to 'filled circle':
// https://www.w3.org/TR/ttml2/#style-value-emphasis-style
markShape =
(verticalType == Cue.VERTICAL_TYPE_LR || verticalType == Cue.VERTICAL_TYPE_RL)
? TextEmphasisSpan.MARK_SHAPE_SESAME
: TextEmphasisSpan.MARK_SHAPE_CIRCLE;
markFill = TextEmphasisSpan.MARK_FILL_FILLED;
} else {
markShape = textEmphasis.markShape;
markFill = textEmphasis.markFill;
}
@TextEmphasis.Position int position;
if (textEmphasis.position == TextEmphasis.POSITION_OUTSIDE) {
// 'outside' is not supported by TextEmphasisSpan, so treat it as 'before':
// https://www.w3.org/TR/ttml2/#style-value-annotation-position
position = TextAnnotation.POSITION_BEFORE;
} else {
position = textEmphasis.position;
}
SpanUtil.addOrReplaceSpan(
builder,
new TextEmphasisSpan(markShape, markFill, 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.
@ -141,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;

View File

@ -19,7 +19,7 @@ import android.graphics.Typeface;
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,9 +83,10 @@ 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;
@Nullable private TextEmphasis textEmphasis;
public TtmlStyle() {
linethrough = UNSPECIFIED;
@ -94,7 +95,7 @@ import java.lang.annotation.RetentionPolicy;
italic = UNSPECIFIED;
fontSizeUnit = UNSPECIFIED;
rubyType = UNSPECIFIED;
rubyPosition = RubySpan.POSITION_UNKNOWN;
rubyPosition = TextAnnotation.POSITION_UNKNOWN;
textCombine = UNSPECIFIED;
}
@ -225,7 +226,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) {
@ -238,6 +239,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);
@ -269,12 +273,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;
}
@ -299,6 +303,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;

View File

@ -17,7 +17,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 +195,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));

View File

@ -20,7 +20,7 @@ import android.text.TextUtils;
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 +95,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 +111,7 @@ public final class WebvttCssStyle {
bold = UNSPECIFIED;
italic = UNSPECIFIED;
fontSizeUnit = UNSPECIFIED;
rubyPosition = RubySpan.POSITION_UNKNOWN;
rubyPosition = TextAnnotation.POSITION_UNKNOWN;
combineUpright = false;
}
@ -272,12 +272,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;
}

View File

@ -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<Element> nestedElements,
List<WebvttCssStyle> styles) {
@RubySpan.Position int rubyTagPosition = getRubyPosition(styles, cueId, startTag);
@TextAnnotation.Position int rubyTagPosition = getRubyPosition(styles, cueId, startTag);
List<Element> 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 <rt> element's ruby-position if set, otherwise the <ruby> 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<WebvttCssStyle> styles, @Nullable String cueId, StartTag startTag) {
List<StyleMatch> 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();

View File

@ -0,0 +1,729 @@
/*
* Copyright 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.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.parse;
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}. */
@RunWith(AndroidJUnit4.class)
public class TextEmphasisTest {
@Test
public void testNull() {
@Nullable TextEmphasis textEmphasis = parse(null);
assertWithMessage("Text Emphasis must be null").that(textEmphasis).isNull();
}
@Test
public void testEmpty() {
@Nullable TextEmphasis textEmphasis = parse("");
assertWithMessage("Text Emphasis must be null").that(textEmphasis).isNull();
}
@Test
public void testEmptyWithWhitespace() {
@Nullable TextEmphasis textEmphasis = parse(" ");
assertWithMessage("Text Emphasis must be null").that(textEmphasis).isNull();
}
@Test
public void testNone() {
String value = "none";
@Nullable TextEmphasis textEmphasis = parse(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";
@Nullable TextEmphasis textEmphasis = parse(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_UNKNOWN);
assertWithMessage("position")
.that(textEmphasis.position)
.isEqualTo(TextEmphasis.POSITION_OUTSIDE);
}
@Test
public void testInvalid() {
String value = "invalid";
@Nullable TextEmphasis textEmphasis = parse(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_UNKNOWN);
assertWithMessage("position")
.that(textEmphasis.position)
.isEqualTo(TextEmphasis.POSITION_OUTSIDE);
}
@Test
public void testAutoOutside() {
String value = "auto outside";
@Nullable TextEmphasis textEmphasis = parse(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_UNKNOWN);
assertWithMessage("position")
.that(textEmphasis.position)
.isEqualTo(TextEmphasis.POSITION_OUTSIDE);
}
@Test
public void testAutoAfter() {
String value = "auto after";
@Nullable TextEmphasis textEmphasis = parse(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_UNKNOWN);
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.
*/
@Test
public void testFilled() {
String value = "filled";
@Nullable TextEmphasis textEmphasis = parse(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(TextEmphasis.POSITION_OUTSIDE);
}
@Test
public void testOpen() {
String value = "open";
@Nullable TextEmphasis textEmphasis = parse(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_OPEN);
assertWithMessage("position")
.that(textEmphasis.position)
.isEqualTo(TextEmphasis.POSITION_OUTSIDE);
}
@Test
public void testOpenAfter() {
String value = "open after";
@Nullable TextEmphasis textEmphasis = parse(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_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.
*/
@Test
public void testDotBefore() {
String value = "dot before";
@Nullable TextEmphasis textEmphasis = parse(value);
assertWithMessage("Text Emphasis must exist").that(textEmphasis).isNotNull();
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";
@Nullable TextEmphasis textEmphasis = parse(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 testSesameBefore() {
String value = "sesame before";
@Nullable TextEmphasis textEmphasis = parse(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 testDotAfter() {
String value = "dot after";
@Nullable TextEmphasis textEmphasis = parse(value);
assertWithMessage("Text Emphasis must exist").that(textEmphasis).isNotNull();
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";
@Nullable TextEmphasis textEmphasis = parse(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_AFTER);
}
@Test
public void testSesameAfter() {
String value = "sesame after";
@Nullable TextEmphasis textEmphasis = parse(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_AFTER);
}
@Test
public void testDotOutside() {
String value = "dot outside";
@Nullable TextEmphasis textEmphasis = parse(value);
assertWithMessage("Text Emphasis must exist").that(textEmphasis).isNotNull();
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";
@Nullable TextEmphasis textEmphasis = parse(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(TextEmphasis.POSITION_OUTSIDE);
}
@Test
public void testSesameOutside() {
String value = "sesame outside";
@Nullable TextEmphasis textEmphasis = parse(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(TextEmphasis.POSITION_OUTSIDE);
}
@Test
public void testOpenDotAfter() {
String value = "open dot after";
@Nullable TextEmphasis textEmphasis = parse(value);
assertWithMessage("Text Emphasis must exist").that(textEmphasis).isNotNull();
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";
@Nullable TextEmphasis textEmphasis = parse(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_OPEN);
assertWithMessage("position")
.that(textEmphasis.position)
.isEqualTo(TextAnnotation.POSITION_AFTER);
}
@Test
public void testOpenSesameAfter() {
String value = "open sesame after";
@Nullable TextEmphasis textEmphasis = parse(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 testOpenDotBefore() {
String value = "open dot before";
@Nullable TextEmphasis textEmphasis = parse(value);
assertWithMessage("Text Emphasis must exist").that(textEmphasis).isNotNull();
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";
@Nullable TextEmphasis textEmphasis = parse(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_OPEN);
assertWithMessage("position")
.that(textEmphasis.position)
.isEqualTo(TextAnnotation.POSITION_BEFORE);
}
@Test
public void testOpenSesameBefore() {
String value = "open sesame before";
@Nullable TextEmphasis textEmphasis = parse(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_BEFORE);
}
@Test
public void testOpenDotOutside() {
String value = "open dot Outside";
@Nullable TextEmphasis textEmphasis = parse(value);
assertWithMessage("Text Emphasis must exist").that(textEmphasis).isNotNull();
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";
@Nullable TextEmphasis textEmphasis = parse(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_OPEN);
assertWithMessage("position")
.that(textEmphasis.position)
.isEqualTo(TextEmphasis.POSITION_OUTSIDE);
}
@Test
public void testOpenSesameOutside() {
String value = "open sesame outside";
@Nullable TextEmphasis textEmphasis = parse(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(TextEmphasis.POSITION_OUTSIDE);
}
@Test
public void testFilledDotOutside() {
String value = "filled dot outside";
@Nullable TextEmphasis textEmphasis = parse(value);
assertWithMessage("Text Emphasis must exist").that(textEmphasis).isNotNull();
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";
@Nullable TextEmphasis textEmphasis = parse(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(TextEmphasis.POSITION_OUTSIDE);
}
@Test
public void testFilledSesameOutside() {
String value = "filled sesame outside";
@Nullable TextEmphasis textEmphasis = parse(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(TextEmphasis.POSITION_OUTSIDE);
}
@Test
public void testFilledDotAfter() {
String value = "filled dot after";
@Nullable TextEmphasis textEmphasis = parse(value);
assertWithMessage("Text Emphasis must exist").that(textEmphasis).isNotNull();
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";
@Nullable TextEmphasis textEmphasis = parse(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_AFTER);
}
@Test
public void testFilledSesameAfter() {
String value = "filled sesame after";
@Nullable TextEmphasis textEmphasis = parse(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_AFTER);
}
@Test
public void testFilledDotBefore() {
String value = "filled dot before";
@Nullable TextEmphasis textEmphasis = parse(value);
assertWithMessage("Text Emphasis must exist").that(textEmphasis).isNotNull();
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";
@Nullable TextEmphasis textEmphasis = parse(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 testFilledSesameBefore() {
String value = "filled sesame before";
@Nullable TextEmphasis textEmphasis = parse(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 testBeforeFilledSesame() {
String value = "before filled sesame";
@Nullable TextEmphasis textEmphasis = parse(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 testBeforeSesameFilled() {
String value = "before sesame filled";
@Nullable TextEmphasis textEmphasis = parse(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 testInvalidMarkShape() {
String value = "before sesamee filled";
@Nullable TextEmphasis textEmphasis = parse(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 = parse(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 = parse(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);
}
@Test
public void testValidMixedWithInvalidDescription() {
String value = "blue open sesame foo bar after";
@Nullable TextEmphasis textEmphasis = TextEmphasis.parse(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 = TextEmphasis.parse(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_UNKNOWN);
assertWithMessage("position").that(textEmphasis.position).isEqualTo(POSITION_OUTSIDE);
}
@Test
public void testQuotedStringStyleNotSupported() {
String value = "\"x\" after";
@Nullable TextEmphasis textEmphasis = TextEmphasis.parse(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_UNKNOWN);
assertWithMessage("position")
.that(textEmphasis.position)
.isEqualTo(TextAnnotation.POSITION_AFTER);
}
}

View File

@ -27,7 +27,8 @@ import com.google.android.exoplayer2.testutil.TestUtil;
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;
import java.io.IOException;
@ -67,6 +68,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 +111,12 @@ public final class TtmlDecoderTest {
* framework level. Tests that <i>lime</i> resolves to <code>#FF00FF00</code> not <code>#00FF00
* </code>.
*
* @throws IOException thrown if reading subtitle file fails.
* @see <a
* href="https://github.com/android/platform_frameworks_base/blob/jb-mr2-release/graphics/java/android/graphics/Color.java#L414">
* JellyBean Color</a> <a
* href="https://github.com/android/platform_frameworks_base/blob/kitkat-mr2.2-release/graphics/java/android/graphics/Color.java#L414">
* Kitkat Color</a>
* @throws IOException thrown if reading subtitle file fails.
*/
@Test
public void lime() throws IOException, SubtitleDecoderException {
@ -646,16 +648,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.");
@ -674,6 +676,146 @@ 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)
.hasTextEmphasisSpanBetween("None ".length(), "None おはよ".length())
.withMarkAndPosition(
TextEmphasisSpan.MARK_SHAPE_NONE,
TextEmphasisSpan.MARK_FILL_UNKNOWN,
TextAnnotation.POSITION_BEFORE);
Spanned secondCue = getOnlyCueTextAtTimeUs(subtitle, 20_000_000);
assertThat(secondCue)
.hasTextEmphasisSpanBetween("Auto ".length(), "Auto ございます".length())
.withMarkAndPosition(
TextEmphasisSpan.MARK_SHAPE_CIRCLE,
TextEmphasisSpan.MARK_FILL_FILLED,
TextAnnotation.POSITION_BEFORE);
Spanned thirdCue = getOnlyCueTextAtTimeUs(subtitle, 30_000_000);
assertThat(thirdCue)
.hasTextEmphasisSpanBetween("Filled circle ".length(), "Filled circle こんばんは".length())
.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_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_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_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_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_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_SHAPE_CIRCLE,
TextEmphasisSpan.MARK_FILL_FILLED,
TextAnnotation.POSITION_BEFORE);
Spanned tenthCue = getOnlyCueTextAtTimeUs(subtitle, 100_000_000);
assertThat(tenthCue)
.hasTextEmphasisSpanBetween("Circle before ".length(), "Circle before ございます".length())
.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_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_SHAPE_DOT,
TextEmphasisSpan.MARK_FILL_FILLED,
TextAnnotation.POSITION_BEFORE);
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_SHAPE_SESAME,
TextEmphasisSpan.MARK_FILL_FILLED,
TextAnnotation.POSITION_BEFORE);
Spanned fifteenthCue = getOnlyCueTextAtTimeUs(subtitle, 150_000_000);
assertThat(fifteenthCue)
.hasTextEmphasisSpanBetween("Auto (TBRL) ".length(), "Auto (TBRL) おはよ".length())
.withMarkAndPosition(
TextEmphasisSpan.MARK_SHAPE_SESAME,
TextEmphasisSpan.MARK_FILL_FILLED,
TextAnnotation.POSITION_BEFORE);
Spanned sixteenthCue = getOnlyCueTextAtTimeUs(subtitle, 160_000_000);
assertThat(sixteenthCue)
.hasTextEmphasisSpanBetween("Auto (TB) ".length(), "Auto (TB) ございます".length())
.withMarkAndPosition(
TextEmphasisSpan.MARK_SHAPE_SESAME,
TextEmphasisSpan.MARK_FILL_FILLED,
TextAnnotation.POSITION_BEFORE);
Spanned seventeenthCue = getOnlyCueTextAtTimeUs(subtitle, 170_000_000);
assertThat(seventeenthCue)
.hasTextEmphasisSpanBetween("Auto (LR) ".length(), "Auto (LR) おはよ".length())
.withMarkAndPosition(
TextEmphasisSpan.MARK_SHAPE_CIRCLE,
TextEmphasisSpan.MARK_FILL_FILLED,
TextAnnotation.POSITION_BEFORE);
}
private static Spanned getOnlyCueTextAtTimeUs(Subtitle subtitle, long timeUs) {
Cue cue = getOnlyCueAtTimeUs(subtitle, timeUs);
assertThat(cue.text).isInstanceOf(Spanned.class);

View File

@ -16,6 +16,7 @@
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.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;
@ -28,7 +29,8 @@ 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 com.google.android.exoplayer2.text.span.TextEmphasisSpan;
import org.junit.Test;
import org.junit.runner.RunWith;
@ -43,9 +45,10 @@ 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";
private final TtmlStyle populatedStyle =
new TtmlStyle()
@ -62,7 +65,8 @@ public final class TtmlStyleTest {
.setRubyType(RUBY_TYPE)
.setRubyPosition(RUBY_POSITION)
.setTextAlign(TEXT_ALIGN)
.setTextCombine(TEXT_COMBINE);
.setTextCombine(TEXT_COMBINE)
.setTextEmphasis(TextEmphasis.parse(TEXT_EMPHASIS_STYLE));
@Test
public void inheritStyle() {
@ -86,6 +90,10 @@ public final class TtmlStyleTest {
assertWithMessage("backgroundColor should not be inherited")
.that(style.hasBackgroundColor())
.isFalse();
assertThat(style.getTextEmphasis()).isNotNull();
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);
}
@Test
@ -109,6 +117,10 @@ 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().markShape).isEqualTo(TextEmphasisSpan.MARK_SHAPE_DOT);
assertThat(style.getTextEmphasis().markFill).isEqualTo(TextEmphasisSpan.MARK_FILL_FILLED);
assertThat(style.getTextEmphasis().position).isEqualTo(POSITION_BEFORE);
}
@Test
@ -221,9 +233,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
@ -245,4 +257,14 @@ 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.parse("open sesame after"));
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);
}
}

View File

@ -26,7 +26,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4;
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 +349,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 +357,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 <rt> tags with different positions nested in a single <ruby> 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 <ruby> span with no <rt> tags.
Cue fourthCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(6)));

View File

@ -31,6 +31,8 @@ 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;
import com.google.common.collect.ImmutableMap;
@ -186,17 +188,26 @@ 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 "<ruby style='ruby-position:over;'>";
case RubySpan.POSITION_UNDER:
case TextAnnotation.POSITION_AFTER:
return "<ruby style='ruby-position:under;'>";
case RubySpan.POSITION_UNKNOWN:
case TextAnnotation.POSITION_UNKNOWN:
return "<ruby style='ruby-position:unset;'>";
default:
return null;
}
} else if (span instanceof UnderlineSpan) {
return "<u>";
} else if (span instanceof TextEmphasisSpan) {
TextEmphasisSpan textEmphasisSpan = (TextEmphasisSpan) span;
String style = getTextEmphasisStyle(textEmphasisSpan.markShape, textEmphasisSpan.markFill);
String position = getTextEmphasisPosition(textEmphasisSpan.position);
return Util.formatInvariant(
"<span style='-webkit-text-emphasis-style:%1$s;text-emphasis-style:%1$s;"
+ "-webkit-text-emphasis-position:%2$s;text-emphasis-position:%2$s;"
+ "display:inline-block;'>",
style, position);
} else {
return null;
}
@ -209,7 +220,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 "</span>";
} else if (span instanceof TypefaceSpan) {
@Nullable String fontFamily = ((TypefaceSpan) span).getFamily();
@ -232,6 +244,52 @@ import java.util.regex.Pattern;
return null;
}
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_UNKNOWN:
default:
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) {
switch (position) {
case TextAnnotation.POSITION_AFTER:
return "under left";
case TextAnnotation.POSITION_UNKNOWN:
case TextAnnotation.POSITION_BEFORE:
default:
return "over right";
}
}
private static Transition getOrCreate(SparseArray<Transition> transitions, int key) {
@Nullable Transition transition = transitions.get(key);
if (transition == null) {

View File

@ -34,6 +34,8 @@ 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;
import org.robolectric.annotation.Config;
@ -250,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);
@ -279,6 +281,42 @@ public class SpannedToHtmlConverterTest {
+ "section");
}
@Test
public void convert_supportsTextEmphasisSpan() {
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(),
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(),
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
SpannedToHtmlConverter.HtmlAndCss htmlAndCss =
SpannedToHtmlConverter.convert(spanned, displayDensity);
assertThat(htmlAndCss.cssRuleSets).isEmpty();
assertThat(htmlAndCss.html)
.isEqualTo(
"Text emphasis <span style='"
+ "-webkit-text-emphasis-style:filled circle;text-emphasis-style:filled circle;"
+ "-webkit-text-emphasis-position:over right;text-emphasis-position:over right;"
+ "display:inline-block;'>&#12362;&#12399;&#12424;</span> <span style='"
+ "-webkit-text-emphasis-style:open sesame;text-emphasis-style:open sesame;"
+ "-webkit-text-emphasis-position:under left;text-emphasis-position:under left;"
+ "display:inline-block;'>&#12372;&#12374;&#12356;&#12414;&#12377;</span>");
}
@Test
public void convert_supportsUnderlineSpan() {
SpannableString spanned = new SpannableString("String with underlined section.");

View File

@ -0,0 +1,65 @@
<tt xmlns:ttm="http://www.w3.org/2006/10/ttaf1#metadata"
xmlns:ttp="http://www.w3.org/2006/10/ttaf1#parameter"
xmlns:tts="http://www.w3.org/2006/10/ttaf1#style"
xmlns="http://www.w3.org/ns/ttml">
<head>
<region xml:id="region_tbrl" tts:extent="80.000% 80.000%" tts:origin="10.000% 10.000%" tts:writingMode="tbrl"/>
<region xml:id="region_tblr" tts:extent="80.000% 80.000%" tts:origin="10.000% 10.000%" tts:writingMode="tblr"/>
<region xml:id="region_tb" tts:extent="80.000% 80.000%" tts:origin="10.000% 10.000%" tts:writingMode="tb"/>
<region xml:id="region_lr" tts:extent="80.000% 80.000%" tts:origin="10.000% 10.000%" tts:writingMode="lr"/>
</head>
<body>
<div>
<p begin="10s" end="18s">None <span tts:textEmphasis="none">おはよ</span></p>
</div>
<div>
<p begin="20s" end="28s">Auto <span tts:textEmphasis="auto">ございます</span></p>
</div>
<div>
<p begin="30s" end="38s">Filled circle <span tts:textEmphasis="filled circle">こんばんは</span></p>
</div>
<div>
<p begin="40s" end="48s">Filled dot <span tts:textEmphasis="filled dot">ございます</span></p>
</div>
<div>
<p begin="50s" end="58s">Filled sesame <span tts:textEmphasis="filled sesame">おはよ</span></p>
</div>
<div>
<p begin="60s" end="68s">Open circle before <span tts:textEmphasis="open circle before">ございます</span></p>
</div>
<div>
<p begin="70s" end="78s">Open dot after <span tts:textEmphasis="open dot after">おはよ</span></p>
</div>
<div>
<p begin="80s" end="88s">Open sesame outside <span tts:textEmphasis="open sesame outside">ございます</span></p>
</div>
<div>
<p begin="90s" end="98s">Auto outside <span tts:textEmphasis="auto outside">おはよ</span></p>
</div>
<div>
<p begin="100s" end="108s">Circle before <span tts:textEmphasis="circle before">ございます</span></p>
</div>
<div>
<p begin="110s" end="118s">Sesame after <span tts:textEmphasis="sesame after">おはよ</span></p>
</div>
<div>
<p begin="120s" end="128s">Dot outside <span tts:textEmphasis="dot outside">ございます</span></p>
</div>
<div>
<p begin="130s" end="138s">No textEmphasis property <span>おはよ</span></p>
</div>
<div>
<p begin="140s" end="148s" region="region_tbrl">Auto (TBLR) <span tts:textEmphasis="auto">ございます</span></p>
</div>
<div>
<p begin="150s" end="158s" region="region_tblr">Auto (TBRL) <span tts:textEmphasis="auto">おはよ</span></p>
</div>
<div>
<p begin="160s" end="168s" region="region_tb">Auto (TB) <span tts:textEmphasis="auto">ございます</span></p>
</div>
<div>
<p begin="170s" end="178s" region="region_lr">Auto (LR) <span tts:textEmphasis="auto">おはよ</span></p>
</div>
</body>
</tt>

View File

@ -38,6 +38,8 @@ 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;
import com.google.common.truth.FailureMetadata;
@ -578,6 +580,44 @@ 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 EmphasizedText} object for optional additional assertions on the flags.
*/
public EmphasizedText hasTextEmphasisSpanBetween(int start, int end) {
if (actual == null) {
failWithoutActual(simpleFact("Spanned must not be null"));
return ALREADY_FAILED_WITH_MARK_AND_POSITION;
}
List<TextEmphasisSpan> 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_AND_POSITION;
}
/**
* Checks that the subject has no {@link TextEmphasisSpan}s on any of the text between {@code
* start} and {@code end}.
*
* <p>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}.
@ -1033,7 +1073,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 =
@ -1057,7 +1097,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<Integer> matchingSpanFlags = new ArrayList<>();
List<TextAndPosition> spanTextsAndPositions = new ArrayList<>();
for (RubySpan span : actualSpans) {
@ -1074,7 +1114,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;
@ -1110,4 +1150,108 @@ public final class SpannedSubject extends Subject {
}
}
}
/** Allows assertions about a span's text emphasis mark and its position. */
public interface EmphasizedText {
/**
* Checks that at least one of the matched spans has the expected {@code mark} and {@code
* position}.
*
* @param markShape The expected mark shape.
* @param markFill The expected mark fill style.
* @param position The expected position of the mark.
* @return A {@link AndSpanFlags} object for optional additional assertions on the flags.
*/
AndSpanFlags withMarkAndPosition(
@TextEmphasisSpan.MarkShape int markShape,
@TextEmphasisSpan.MarkFill int markFill,
@TextAnnotation.Position int position);
}
private static final EmphasizedText ALREADY_FAILED_WITH_MARK_AND_POSITION =
(markShape, markFill, position) -> ALREADY_FAILED_AND_FLAGS;
private static Factory<TextEmphasisSubject, List<TextEmphasisSpan>> textEmphasisSubjects(
Spanned actualSpanned) {
return (FailureMetadata metadata, List<TextEmphasisSpan> spans) ->
new TextEmphasisSubject(metadata, spans, actualSpanned);
}
private static final class TextEmphasisSubject extends Subject implements EmphasizedText {
private final List<TextEmphasisSpan> actualSpans;
private final Spanned actualSpanned;
private TextEmphasisSubject(
FailureMetadata metadata, List<TextEmphasisSpan> actualSpans, Spanned actualSpanned) {
super(metadata, actualSpans);
this.actualSpans = actualSpans;
this.actualSpanned = actualSpanned;
}
@Override
public AndSpanFlags withMarkAndPosition(
@TextEmphasisSpan.MarkShape int markShape,
@TextEmphasisSpan.MarkFill int markFill,
@TextAnnotation.Position int position) {
List<Integer> matchingSpanFlags = new ArrayList<>();
List<MarkAndPosition> textEmphasisMarksAndPositions = new ArrayList<>();
for (TextEmphasisSpan span : actualSpans) {
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(markShape, markFill, position));
return check("flags").about(spanFlags()).that(matchingSpanFlags);
}
private static final class MarkAndPosition {
@TextEmphasisSpan.MarkShape private final int markShape;
@TextEmphasisSpan.MarkFill private final int markFill;
@TextAnnotation.Position private final int position;
private MarkAndPosition(
@TextEmphasisSpan.MarkShape int markShape,
@TextEmphasisSpan.MarkFill int markFill,
@TextAnnotation.Position int position) {
this.markFill = markFill;
this.markShape = markShape;
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)
&& (markShape == that.markShape)
&& (markFill == that.markFill);
}
@Override
public int hashCode() {
int result = markShape;
result = 31 * result + markFill;
result = 31 * result + position;
return result;
}
@Override
public String toString() {
return String.format(
"{markShape=%s,markFill=%s,position=%s}", markShape, markFill, position);
}
}
}
}

View File

@ -41,6 +41,9 @@ 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.TextAnnotation;
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;
@ -607,23 +610,26 @@ 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(
@ -631,7 +637,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'");
@ -640,7 +646,8 @@ 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(
@ -648,27 +655,32 @@ 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);
}
@ -676,7 +688,191 @@ 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
public void textEmphasis_success() {
SpannableString spannable =
createSpannable(
new TextEmphasisSpan(
TextEmphasisSpan.MARK_SHAPE_CIRCLE,
TextEmphasisSpan.MARK_FILL_FILLED,
TextAnnotation.POSITION_AFTER));
assertThat(spannable)
.hasTextEmphasisSpanBetween(SPAN_START, SPAN_END)
.withMarkAndPosition(
TextEmphasisSpan.MARK_SHAPE_CIRCLE,
TextEmphasisSpan.MARK_FILL_FILLED,
TextAnnotation.POSITION_AFTER)
.andFlags(Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
}
@Test
public void textEmphasis_wrongIndex() {
checkHasSpanFailsDueToIndexMismatch(
new TextEmphasisSpan(
TextEmphasisSpan.MARK_SHAPE_CIRCLE,
TextEmphasisSpan.MARK_FILL_FILLED,
TextAnnotation.POSITION_AFTER),
SpannedSubject::hasTextEmphasisSpanBetween);
}
@Test
public void textEmphasis_wrongMarkShape() {
SpannableString spannable =
createSpannable(
new TextEmphasisSpan(
TextEmphasisSpan.MARK_SHAPE_CIRCLE,
TextEmphasisSpan.MARK_FILL_FILLED,
TextAnnotation.POSITION_AFTER));
AssertionError expected =
expectFailure(
whenTesting ->
whenTesting
.that(spannable)
.hasTextEmphasisSpanBetween(SPAN_START, SPAN_END)
.withMarkAndPosition(
TextEmphasisSpan.MARK_SHAPE_SESAME,
TextEmphasisSpan.MARK_FILL_FILLED,
TextAnnotation.POSITION_AFTER));
assertThat(expected).factValue("value of").contains("textEmphasisMarkAndPosition");
assertThat(expected)
.factValue("expected")
.contains(
Util.formatInvariant(
"{markShape=%d,markFill=%d,position=%d}",
TextEmphasisSpan.MARK_SHAPE_SESAME,
TextEmphasisSpan.MARK_FILL_FILLED,
TextAnnotation.POSITION_AFTER));
assertThat(expected)
.factValue("but was")
.contains(
Util.formatInvariant(
"{markShape=%d,markFill=%d,position=%d}",
TextEmphasisSpan.MARK_SHAPE_CIRCLE,
TextEmphasisSpan.MARK_FILL_FILLED,
TextAnnotation.POSITION_AFTER));
}
@Test
public void textEmphasis_wrongMarkFill() {
SpannableString spannable =
createSpannable(
new TextEmphasisSpan(
TextEmphasisSpan.MARK_SHAPE_CIRCLE,
TextEmphasisSpan.MARK_FILL_FILLED,
TextAnnotation.POSITION_AFTER));
AssertionError expected =
expectFailure(
whenTesting ->
whenTesting
.that(spannable)
.hasTextEmphasisSpanBetween(SPAN_START, SPAN_END)
.withMarkAndPosition(
TextEmphasisSpan.MARK_SHAPE_CIRCLE,
TextEmphasisSpan.MARK_FILL_OPEN,
TextAnnotation.POSITION_AFTER));
assertThat(expected).factValue("value of").contains("textEmphasisMarkAndPosition");
assertThat(expected)
.factValue("expected")
.contains(
Util.formatInvariant(
"{markShape=%d,markFill=%d,position=%d}",
TextEmphasisSpan.MARK_SHAPE_CIRCLE,
TextEmphasisSpan.MARK_FILL_OPEN,
TextAnnotation.POSITION_AFTER));
assertThat(expected)
.factValue("but was")
.contains(
Util.formatInvariant(
"{markShape=%d,markFill=%d,position=%d}",
TextEmphasisSpan.MARK_SHAPE_CIRCLE,
TextEmphasisSpan.MARK_FILL_FILLED,
TextAnnotation.POSITION_AFTER));
}
@Test
public void textEmphasis_wrongPosition() {
SpannableString spannable =
createSpannable(
new TextEmphasisSpan(
TextEmphasisSpan.MARK_SHAPE_CIRCLE,
TextEmphasisSpan.MARK_FILL_FILLED,
TextAnnotation.POSITION_BEFORE));
AssertionError expected =
expectFailure(
whenTesting ->
whenTesting
.that(spannable)
.hasTextEmphasisSpanBetween(SPAN_START, SPAN_END)
.withMarkAndPosition(
TextEmphasisSpan.MARK_SHAPE_CIRCLE,
TextEmphasisSpan.MARK_FILL_FILLED,
TextAnnotation.POSITION_AFTER));
assertThat(expected).factValue("value of").contains("textEmphasisMarkAndPosition");
assertThat(expected)
.factValue("expected")
.contains(
Util.formatInvariant(
"{markShape=%d,markFill=%d,position=%d}",
TextEmphasisSpan.MARK_SHAPE_CIRCLE,
TextEmphasisSpan.MARK_FILL_FILLED,
TextAnnotation.POSITION_AFTER));
assertThat(expected)
.factValue("but was")
.contains(
Util.formatInvariant(
"{markShape=%d,markFill=%d,position=%d}",
TextEmphasisSpan.MARK_SHAPE_CIRCLE,
TextEmphasisSpan.MARK_FILL_FILLED,
TextAnnotation.POSITION_BEFORE));
}
@Test
public void textEmphasis_wrongFlags() {
checkHasSpanFailsDueToFlagMismatch(
new TextEmphasisSpan(
TextEmphasisSpan.MARK_SHAPE_CIRCLE,
TextEmphasisSpan.MARK_FILL_FILLED,
TextAnnotation.POSITION_AFTER),
(subject, start, end) ->
subject
.hasTextEmphasisSpanBetween(start, end)
.withMarkAndPosition(
TextEmphasisSpan.MARK_SHAPE_CIRCLE,
TextEmphasisSpan.MARK_FILL_FILLED,
TextAnnotation.POSITION_AFTER));
}
@Test
public void noTextEmphasis_success() {
SpannableString spannable =
createSpannableWithUnrelatedSpanAnd(
new TextEmphasisSpan(
TextEmphasisSpan.MARK_SHAPE_CIRCLE,
TextEmphasisSpan.MARK_FILL_FILLED,
TextAnnotation.POSITION_AFTER));
assertThat(spannable).hasNoTextEmphasisSpanBetween(UNRELATED_SPAN_START, UNRELATED_SPAN_END);
}
@Test
public void noTextEmphasis_failure() {
checkHasNoSpanFails(
new TextEmphasisSpan(
TextEmphasisSpan.MARK_SHAPE_CIRCLE,
TextEmphasisSpan.MARK_FILL_FILLED,
TextAnnotation.POSITION_AFTER),
SpannedSubject::hasNoTextEmphasisSpanBetween);
}
@Test