Commit 786a1ee8 by ibaker Committed by Oliver Woodman

Add ruby support to TtmlDecoder

I had to expand the info that gets passed around a bit, so I could join up the container, base & text ruby nodes.

Spec: https://www.w3.org/TR/2018/REC-ttml2-20181108/#style-attribute-ruby
PiperOrigin-RevId: 295931653
parent 4107375c
......@@ -49,6 +49,8 @@
* Catch-and-log all fatal exceptions in `TextRenderer` instead of re-throwing,
allowing playback to continue even if subtitles fail
([#6885](https://github.com/google/ExoPlayer/issues/6885)).
* Parse `tts:ruby` and `tts:rubyPosition` properties in TTML subtitles
(rendering is coming later).
* DRM:
* Add support for attaching DRM sessions to clear content in the demo app.
* Remove `DrmSessionManager` references from all renderers.
......
/*
* Copyright (C) 2020 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 android.text.Spanned;
/**
* A span used to mark a section of text for later deletion.
*
* <p>This is deliberately package-private because it's not generally supported by Android and
* results in surprising behaviour when simply calling {@link Spanned#toString} (i.e. the text isn't
* deleted).
*
* <p>This span is explicitly handled in {@code TtmlNode#cleanUpText}.
*/
/* package */ final class DeleteTextSpan {}
......@@ -22,6 +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.util.Assertions;
import com.google.android.exoplayer2.util.ColorParser;
import com.google.android.exoplayer2.util.Log;
......@@ -537,6 +538,40 @@ public final class TtmlDecoder extends SimpleSubtitleDecoder {
break;
}
break;
case TtmlNode.ATTR_TTS_RUBY:
switch (Util.toLowerInvariant(attributeValue)) {
case TtmlNode.RUBY_CONTAINER:
style = createIfNull(style).setRubyType(TtmlStyle.RUBY_TYPE_CONTAINER);
break;
case TtmlNode.RUBY_BASE:
case TtmlNode.RUBY_BASE_CONTAINER:
style = createIfNull(style).setRubyType(TtmlStyle.RUBY_TYPE_BASE);
break;
case TtmlNode.RUBY_TEXT:
case TtmlNode.RUBY_TEXT_CONTAINER:
style = createIfNull(style).setRubyType(TtmlStyle.RUBY_TYPE_TEXT);
break;
case TtmlNode.RUBY_DELIMITER:
style = createIfNull(style).setRubyType(TtmlStyle.RUBY_TYPE_DELIMITER);
break;
default:
// ignore
break;
}
break;
case TtmlNode.ATTR_TTS_RUBY_POSITION:
switch (Util.toLowerInvariant(attributeValue)) {
case TtmlNode.RUBY_BEFORE:
style = createIfNull(style).setRubyPosition(RubySpan.POSITION_OVER);
break;
case TtmlNode.RUBY_AFTER:
style = createIfNull(style).setRubyPosition(RubySpan.POSITION_UNDER);
break;
default:
// ignore
break;
}
break;
case TtmlNode.ATTR_TTS_TEXT_DECORATION:
switch (Util.toLowerInvariant(attributeValue)) {
case TtmlNode.LINETHROUGH:
......@@ -650,8 +685,9 @@ public final class TtmlDecoder extends SimpleSubtitleDecoder {
endTime = parent.endTimeUs;
}
}
return TtmlNode.buildNode(
parser.getName(), startTime, endTime, style, styleIds, regionId, imageId);
parser.getName(), startTime, endTime, style, styleIds, regionId, imageId, parent);
}
private static boolean isSupportedTag(String tag) {
......
......@@ -64,11 +64,24 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
public static final String ATTR_TTS_FONT_FAMILY = "fontFamily";
public static final String ATTR_TTS_FONT_WEIGHT = "fontWeight";
public static final String ATTR_TTS_COLOR = "color";
public static final String ATTR_TTS_RUBY = "ruby";
public static final String ATTR_TTS_RUBY_POSITION = "rubyPosition";
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_WRITING_MODE = "writingMode";
// Values for ruby
public static final String RUBY_CONTAINER = "container";
public static final String RUBY_BASE = "base";
public static final String RUBY_BASE_CONTAINER = "baseContainer";
public static final String RUBY_TEXT = "text";
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 textDecoration
public static final String LINETHROUGH = "linethrough";
public static final String NO_LINETHROUGH = "nolinethrough";
......@@ -102,6 +115,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
@Nullable private final String[] styleIds;
public final String regionId;
@Nullable public final String imageId;
@Nullable public final TtmlNode parent;
private final HashMap<String, Integer> nodeStartsByRegion;
private final HashMap<String, Integer> nodeEndsByRegion;
......@@ -117,7 +131,8 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
/* style= */ null,
/* styleIds= */ null,
ANONYMOUS_REGION_ID,
/* imageId= */ null);
/* imageId= */ null,
/* parent= */ null);
}
public static TtmlNode buildNode(
......@@ -127,9 +142,10 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
@Nullable TtmlStyle style,
@Nullable String[] styleIds,
String regionId,
@Nullable String imageId) {
@Nullable String imageId,
@Nullable TtmlNode parent) {
return new TtmlNode(
tag, /* text= */ null, startTimeUs, endTimeUs, style, styleIds, regionId, imageId);
tag, /* text= */ null, startTimeUs, endTimeUs, style, styleIds, regionId, imageId, parent);
}
private TtmlNode(
......@@ -140,7 +156,8 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
@Nullable TtmlStyle style,
@Nullable String[] styleIds,
String regionId,
@Nullable String imageId) {
@Nullable String imageId,
@Nullable TtmlNode parent) {
this.tag = tag;
this.text = text;
this.imageId = imageId;
......@@ -150,6 +167,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
this.startTimeUs = startTimeUs;
this.endTimeUs = endTimeUs;
this.regionId = Assertions.checkNotNull(regionId);
this.parent = parent;
nodeStartsByRegion = new HashMap<>();
nodeEndsByRegion = new HashMap<>();
}
......@@ -361,14 +379,19 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
regionOutput.setText(text);
}
if (resolvedStyle != null) {
TtmlRenderUtil.applyStylesToSpan(text, start, end, resolvedStyle);
TtmlRenderUtil.applyStylesToSpan(text, start, end, resolvedStyle, parent);
regionOutput.setVerticalType(resolvedStyle.getVerticalType());
}
}
private static void cleanUpText(SpannableStringBuilder builder) {
// Having joined the text elements, we need to do some final cleanup on the result.
// 1. Collapse multiple consecutive spaces into a single space.
// Remove any text covered by a DeleteTextSpan (e.g. ruby text).
DeleteTextSpan[] deleteTextSpans = builder.getSpans(0, builder.length(), DeleteTextSpan.class);
for (DeleteTextSpan deleteTextSpan : deleteTextSpans) {
builder.replace(builder.getSpanStart(deleteTextSpan), builder.getSpanEnd(deleteTextSpan), "");
}
// Collapse multiple consecutive spaces into a single space.
for (int i = 0; i < builder.length(); i++) {
if (builder.charAt(i) == ' ') {
int j = i + 1;
......@@ -381,7 +404,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
}
}
}
// 2. Remove any spaces from the start of each line.
// Remove any spaces from the start of each line.
if (builder.length() > 0 && builder.charAt(0) == ' ') {
builder.delete(0, 1);
}
......@@ -390,7 +413,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
builder.delete(i + 1, i + 2);
}
}
// 3. Remove any spaces from the end of each line.
// Remove any spaces from the end of each line.
if (builder.length() > 0 && builder.charAt(builder.length() - 1) == ' ') {
builder.delete(builder.length() - 1, builder.length());
}
......@@ -399,7 +422,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
builder.delete(i, i + 1);
}
}
// 4. Trim a trailing newline, if there is one.
// Trim a trailing newline, if there is one.
if (builder.length() > 0 && builder.charAt(builder.length() - 1) == '\n') {
builder.delete(builder.length() - 1, builder.length());
}
......
......@@ -16,6 +16,7 @@
package com.google.android.exoplayer2.text.ttml;
import android.text.Layout.Alignment;
import android.text.Spannable;
import android.text.SpannableStringBuilder;
import android.text.Spanned;
import android.text.style.AbsoluteSizeSpan;
......@@ -29,7 +30,12 @@ import android.text.style.TypefaceSpan;
import android.text.style.UnderlineSpan;
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.SpanUtil;
import com.google.android.exoplayer2.util.Log;
import com.google.android.exoplayer2.util.Util;
import java.util.ArrayDeque;
import java.util.Deque;
import java.util.Map;
/**
......@@ -37,6 +43,8 @@ import java.util.Map;
*/
/* package */ final class TtmlRenderUtil {
private static final String TAG = "TtmlRenderUtil";
@Nullable
public static TtmlStyle resolveStyle(
@Nullable TtmlStyle style, @Nullable String[] styleIds, Map<String, TtmlStyle> globalStyles) {
......@@ -71,8 +79,8 @@ import java.util.Map;
return style;
}
public static void applyStylesToSpan(SpannableStringBuilder builder,
int start, int end, TtmlStyle style) {
public static void applyStylesToSpan(
Spannable builder, int start, int end, TtmlStyle style, @Nullable TtmlNode parent) {
if (style.getStyle() != TtmlStyle.UNSPECIFIED) {
builder.setSpan(new StyleSpan(style.getStyle()), start, end,
......@@ -108,6 +116,53 @@ import java.util.Map;
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.
@Nullable TtmlNode containerNode = findRubyContainerNode(parent);
if (containerNode == null) {
// No matching container node
break;
}
@Nullable TtmlNode textNode = findRubyTextNode(containerNode);
if (textNode == null) {
// no matching text node
break;
}
String rubyText;
if (textNode.getChildCount() == 1 && textNode.getChild(0).text != null) {
rubyText = Util.castNonNull(textNode.getChild(0).text);
} else {
Log.i(TAG, "Skipping rubyText node without exactly one text child.");
break;
}
// TODO: Get rubyPosition from `textNode` when TTML inheritance is implemented.
@RubySpan.Position
int rubyPosition =
containerNode.style != null
? containerNode.style.getRubyPosition()
: RubySpan.POSITION_UNKNOWN;
builder.setSpan(
new RubySpan(rubyText, rubyPosition), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
break;
case TtmlStyle.RUBY_TYPE_DELIMITER:
// TODO: Add support for this when RubySpan supports parenthetical text. For now, just
// fall through and delete the text.
case TtmlStyle.RUBY_TYPE_TEXT:
// We can't just remove the text directly from `builder` here because TtmlNode has fixed
// ideas of where every node starts and ends (nodeStartsByRegion and nodeEndsByRegion) so
// all these indices become invalid if we mutate the underlying string at this point.
// Instead we add a special span that's then handled in TtmlNode#cleanUpText.
builder.setSpan(new DeleteTextSpan(), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
break;
case TtmlStyle.RUBY_TYPE_CONTAINER:
case TtmlStyle.UNSPECIFIED:
default:
// Do nothing
break;
}
@Nullable Alignment textAlign = style.getTextAlign();
if (textAlign != null) {
SpanUtil.addOrReplaceSpan(
......@@ -156,6 +211,35 @@ import java.util.Map;
}
}
@Nullable
private static TtmlNode findRubyTextNode(TtmlNode rubyContainerNode) {
Deque<TtmlNode> childNodesStack = new ArrayDeque<>();
childNodesStack.push(rubyContainerNode);
while (!childNodesStack.isEmpty()) {
TtmlNode childNode = childNodesStack.pop();
if (childNode.style != null && childNode.style.getRubyType() == TtmlStyle.RUBY_TYPE_TEXT) {
return childNode;
}
for (int i = childNode.getChildCount() - 1; i >= 0; i--) {
childNodesStack.push(childNode.getChild(i));
}
}
return null;
}
@Nullable
private static TtmlNode findRubyContainerNode(@Nullable TtmlNode node) {
while (node != null) {
@Nullable TtmlStyle style = node.style;
if (style != null && style.getRubyType() == TtmlStyle.RUBY_TYPE_CONTAINER) {
return node;
}
node = node.parent;
}
return null;
}
/**
* Called when the end of a paragraph is encountered. Adds a newline if there are one or more
* non-space characters since the previous newline.
......
......@@ -21,6 +21,7 @@ import androidx.annotation.IntDef;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.text.Cue;
import com.google.android.exoplayer2.text.Cue.VerticalType;
import com.google.android.exoplayer2.text.span.RubySpan;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
......@@ -61,6 +62,16 @@ import java.lang.annotation.RetentionPolicy;
private static final int OFF = 0;
private static final int ON = 1;
@Documented
@Retention(RetentionPolicy.SOURCE)
@IntDef({UNSPECIFIED, RUBY_TYPE_CONTAINER, RUBY_TYPE_BASE, RUBY_TYPE_TEXT, RUBY_TYPE_DELIMITER})
public @interface RubyType {}
public static final int RUBY_TYPE_CONTAINER = 1;
public static final int RUBY_TYPE_BASE = 2;
public static final int RUBY_TYPE_TEXT = 3;
public static final int RUBY_TYPE_DELIMITER = 4;
@Nullable private String fontFamily;
private int fontColor;
private boolean hasFontColor;
......@@ -73,6 +84,8 @@ import java.lang.annotation.RetentionPolicy;
@FontSizeUnit private int fontSizeUnit;
private float fontSize;
@Nullable private String id;
@RubyType private int rubyType;
@RubySpan.Position private int rubyPosition;
@Nullable private Layout.Alignment textAlign;
@OptionalBoolean private int textCombine;
@Cue.VerticalType private int verticalType;
......@@ -83,6 +96,8 @@ import java.lang.annotation.RetentionPolicy;
bold = UNSPECIFIED;
italic = UNSPECIFIED;
fontSizeUnit = UNSPECIFIED;
rubyType = UNSPECIFIED;
rubyPosition = RubySpan.POSITION_UNKNOWN;
textCombine = UNSPECIFIED;
verticalType = Cue.TYPE_UNSET;
}
......@@ -214,6 +229,9 @@ import java.lang.annotation.RetentionPolicy;
if (underline == UNSPECIFIED) {
underline = ancestor.underline;
}
if (rubyPosition == RubySpan.POSITION_UNKNOWN) {
rubyPosition = ancestor.rubyPosition;
}
if (textAlign == null && ancestor.textAlign != null) {
textAlign = ancestor.textAlign;
}
......@@ -228,8 +246,11 @@ import java.lang.annotation.RetentionPolicy;
if (chaining && !hasBackgroundColor && ancestor.hasBackgroundColor) {
setBackgroundColor(ancestor.backgroundColor);
}
if (chaining && verticalType == Cue.TYPE_UNSET) {
verticalType = ancestor.verticalType;
if (chaining && rubyType == UNSPECIFIED && ancestor.rubyType != UNSPECIFIED) {
rubyType = ancestor.rubyType;
}
if (chaining && verticalType == Cue.TYPE_UNSET && ancestor.verticalType != Cue.TYPE_UNSET) {
setVerticalType(ancestor.verticalType);
}
}
return this;
......@@ -245,6 +266,26 @@ import java.lang.annotation.RetentionPolicy;
return id;
}
public TtmlStyle setRubyType(@RubyType int rubyType) {
this.rubyType = rubyType;
return this;
}
@RubyType
public int getRubyType() {
return rubyType;
}
public TtmlStyle setRubyPosition(@RubySpan.Position int position) {
this.rubyPosition = position;
return this;
}
@RubySpan.Position
public int getRubyPosition() {
return rubyPosition;
}
@Nullable
public Layout.Alignment getTextAlign() {
return textAlign;
......
......@@ -27,6 +27,7 @@ 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.util.Assertions;
import com.google.android.exoplayer2.util.ColorParser;
import java.io.IOException;
......@@ -61,6 +62,7 @@ public final class TtmlDecoderTest {
private static final String BITMAP_UNSUPPORTED_REGION_FILE = "ttml/bitmap_unsupported_region.xml";
private static final String VERTICAL_TEXT_FILE = "ttml/vertical_text.xml";
private static final String TEXT_COMBINE_FILE = "ttml/text_combine.xml";
private static final String RUBIES_FILE = "ttml/rubies.xml";
@Test
public void testInlineAttributes() throws IOException, SubtitleDecoderException {
......@@ -606,6 +608,38 @@ public final class TtmlDecoderTest {
assertThat(thirdCue).hasNoHorizontalTextInVerticalContextSpanBetween(0, thirdCue.length());
}
@Test
public void testRubies() throws IOException, SubtitleDecoderException {
TtmlSubtitle subtitle = getSubtitle(RUBIES_FILE);
Spanned firstCue = getOnlyCueTextAtTimeUs(subtitle, 10_000_000);
assertThat(firstCue.toString()).isEqualTo("Cue with annotated text.");
assertThat(firstCue)
.hasRubySpanBetween("Cue with ".length(), "Cue with annotated".length())
.withTextAndPosition("1st rubies", RubySpan.POSITION_OVER);
assertThat(firstCue)
.hasRubySpanBetween("Cue with annotated ".length(), "Cue with annotated text".length())
.withTextAndPosition("2nd rubies", RubySpan.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);
Spanned thirdCue = getOnlyCueTextAtTimeUs(subtitle, 30_000_000);
assertThat(thirdCue.toString()).isEqualTo("Cue with annotated text.");
assertThat(thirdCue).hasNoRubySpanBetween(0, thirdCue.length());
Spanned fourthCue = getOnlyCueTextAtTimeUs(subtitle, 40_000_000);
assertThat(fourthCue.toString()).isEqualTo("Cue with text.");
assertThat(fourthCue).hasNoRubySpanBetween(0, fourthCue.length());
Spanned fifthCue = getOnlyCueTextAtTimeUs(subtitle, 50_000_000);
assertThat(fifthCue.toString()).isEqualTo("Cue with annotated text.");
assertThat(fifthCue).hasNoRubySpanBetween(0, fifthCue.length());
}
private static Spanned getOnlyCueTextAtTimeUs(Subtitle subtitle, long timeUs) {
Cue cue = getOnlyCueAtTimeUs(subtitle, timeUs);
assertThat(cue.text).isInstanceOf(Spanned.class);
......
......@@ -29,6 +29,7 @@ import android.text.Layout;
import androidx.annotation.ColorInt;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.android.exoplayer2.text.Cue;
import com.google.android.exoplayer2.text.span.RubySpan;
import org.junit.Test;
import org.junit.runner.RunWith;
......@@ -42,6 +43,8 @@ public final class TtmlStyleTest {
private static final float FONT_SIZE = 12.5f;
@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 Layout.Alignment TEXT_ALIGN = Layout.Alignment.ALIGN_CENTER;
private static final boolean TEXT_COMBINE = true;
@Cue.VerticalType private static final int VERTICAL_TYPE = Cue.VERTICAL_TYPE_RL;
......@@ -58,6 +61,8 @@ public final class TtmlStyleTest {
.setFontFamily(FONT_FAMILY)
.setFontSize(FONT_SIZE)
.setFontSizeUnit(FONT_SIZE_UNIT)
.setRubyType(RUBY_TYPE)
.setRubyPosition(RUBY_POSITION)
.setTextAlign(TEXT_ALIGN)
.setTextCombine(TEXT_COMBINE)
.setVerticalType(VERTICAL_TYPE);
......@@ -75,8 +80,12 @@ public final class TtmlStyleTest {
assertThat(style.getFontColor()).isEqualTo(FONT_COLOR);
assertThat(style.getFontSize()).isEqualTo(FONT_SIZE);
assertThat(style.getFontSizeUnit()).isEqualTo(FONT_SIZE_UNIT);
assertThat(style.getRubyPosition()).isEqualTo(RUBY_POSITION);
assertThat(style.getTextAlign()).isEqualTo(TEXT_ALIGN);
assertThat(style.getTextCombine()).isEqualTo(TEXT_COMBINE);
assertWithMessage("rubyType should not be inherited")
.that(style.getRubyType())
.isEqualTo(UNSPECIFIED);
assertWithMessage("backgroundColor should not be inherited")
.that(style.hasBackgroundColor())
.isFalse();
......@@ -99,11 +108,13 @@ public final class TtmlStyleTest {
assertThat(style.getFontColor()).isEqualTo(FONT_COLOR);
assertThat(style.getFontSize()).isEqualTo(FONT_SIZE);
assertThat(style.getFontSizeUnit()).isEqualTo(FONT_SIZE_UNIT);
assertThat(style.getRubyPosition()).isEqualTo(RUBY_POSITION);
assertThat(style.getTextAlign()).isEqualTo(TEXT_ALIGN);
assertThat(style.getTextCombine()).isEqualTo(TEXT_COMBINE);
assertWithMessage("backgroundColor should be chained")
.that(style.getBackgroundColor())
.isEqualTo(BACKGROUND_COLOR);
assertWithMessage("rubyType should be chained").that(style.getRubyType()).isEqualTo(RUBY_TYPE);
assertWithMessage("verticalType should be chained")
.that(style.getVerticalType())
.isEqualTo(VERTICAL_TYPE);
......@@ -207,6 +218,24 @@ public final class TtmlStyleTest {
}
@Test
public void testRubyType() {
TtmlStyle style = new TtmlStyle();
assertThat(style.getRubyType()).isEqualTo(UNSPECIFIED);
style.setRubyType(TtmlStyle.RUBY_TYPE_BASE);
assertThat(style.getRubyType()).isEqualTo(TtmlStyle.RUBY_TYPE_BASE);
}
@Test
public void testRubyPosition() {
TtmlStyle style = new TtmlStyle();
assertThat(style.getRubyPosition()).isEqualTo(RubySpan.POSITION_UNKNOWN);
style.setRubyPosition(RubySpan.POSITION_OVER);
assertThat(style.getRubyPosition()).isEqualTo(RubySpan.POSITION_OVER);
}
@Test
public void testTextAlign() {
TtmlStyle style = new TtmlStyle();
......
<!--
~ Copyright (C) 2020 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.
~
-->
<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"
xmlns="http://www.w3.org/2006/10/ttaf1">
<body>
<div>
<!-- Base before and after text, one with explicit position -->
<p begin="10s" end="18s">
Cue with
<span tts:ruby="container" tts:rubyPosition="before">
<span tts:ruby="base">annotated</span>
<span tts:ruby="text">1st rubies</span>
</span>
<span tts:ruby="container">
<span tts:ruby="text">2nd rubies</span>
<span tts:ruby="base">text</span>.
</span>
</p>
</div>
<div>
<!-- Delimiter (parenthetical) text is stripped -->
<p begin="20s" end="28s">
Cue with
<span tts:ruby="container">
<span tts:ruby="text">rubies</span>
<span tts:ruby="base">annotated</span>
<span tts:ruby="delimiter">alt-text</span>
</span>
text.
</p>
</div>
<div>
<!-- No text section -> no span -->
<p begin="30s" end="38s">
Cue with
<span tts:ruby="container" tts:rubyPosition="before">
<span tts:ruby="base">annotated</span>
</span>
text.</p>
</div>
<div>
<!-- No base section -> text still stripped-->
<p begin="40s" end="48s">
Cue with
<span tts:ruby="container" tts:rubyPosition="before">
<span tts:ruby="text">rubies</span>
</span>
text.
</p>
</div>
<div>
<!-- No container section -> text still stripped-->
<p begin="50s" end="58s">
Cue with
<span tts:ruby="text">rubies</span>
<span tts:ruby="base">annotated</span>
text.
</p>
</div>
</body>
</tt>
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment