Add tate-chu-yoko support to WebVTT decoding

PiperOrigin-RevId: 288285953
This commit is contained in:
ibaker 2020-01-06 13:51:42 +00:00 committed by Ian Baker
parent 06fcf29edd
commit a98fc7ca48
9 changed files with 140 additions and 7 deletions

View file

@ -47,6 +47,8 @@
next cue ([#6833](https://github.com/google/ExoPlayer/issues/6833)).
* Parse \<ruby\> and \<rt\> tags in WebVTT subtitles (rendering is coming
later).
* Parse `text-combine-upright` CSS property (i.e. tate-chu-yoko) in WebVTT
subtitles (rendering is coming later).
### 2.11.1 (2019-12-20) ###

View file

@ -0,0 +1,32 @@
/*
* 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.span;
/**
* A styling span for horizontal text in a vertical context.
*
* <p>This is used in vertical text to write some characters in a horizontal orientation, known in
* Japanese as tate-chu-yoko.
*
* <p>More information on <a
* href="https://www.w3.org/TR/jlreq/#handling_of_tatechuyoko">tate-chu-yoko</a> and <a
* href="https://developer.android.com/guide/topics/text/spans">span styling</a>.
*/
// NOTE: There's no Android layout support for this, so this span currently doesn't extend any
// styling superclasses (e.g. MetricAffectingSpan). The only way to render this styling is to
// extract the spans and do the layout manually.
public final class HorizontalTextInVerticalContextSpan {}

View file

@ -31,14 +31,19 @@ import java.util.regex.Pattern;
*/
/* package */ final class CssParser {
private static final String TAG = "CssParser";
private static final String RULE_START = "{";
private static final String RULE_END = "}";
private static final String PROPERTY_BGCOLOR = "background-color";
private static final String PROPERTY_FONT_FAMILY = "font-family";
private static final String PROPERTY_FONT_WEIGHT = "font-weight";
private static final String PROPERTY_TEXT_COMBINE_UPRIGHT = "text-combine-upright";
private static final String VALUE_ALL = "all";
private static final String VALUE_DIGITS = "digits";
private static final String PROPERTY_TEXT_DECORATION = "text-decoration";
private static final String VALUE_BOLD = "bold";
private static final String VALUE_UNDERLINE = "underline";
private static final String RULE_START = "{";
private static final String RULE_END = "}";
private static final String PROPERTY_FONT_STYLE = "font-style";
private static final String VALUE_ITALIC = "italic";
@ -182,6 +187,8 @@ import java.util.regex.Pattern;
style.setFontColor(ColorParser.parseCssColor(value));
} else if (PROPERTY_BGCOLOR.equals(property)) {
style.setBackgroundColor(ColorParser.parseCssColor(value));
} else if (PROPERTY_TEXT_COMBINE_UPRIGHT.equals(property)) {
style.setCombineUpright(VALUE_ALL.equals(value) || value.startsWith(VALUE_DIGITS));
} else if (PROPERTY_TEXT_DECORATION.equals(property)) {
if (VALUE_UNDERLINE.equals(value)) {
style.setUnderline(true);

View file

@ -95,6 +95,7 @@ public final class WebvttCssStyle {
@FontSizeUnit private int fontSizeUnit;
private float fontSize;
@Nullable private Layout.Alignment textAlign;
private boolean combineUpright;
// Calling reset() is forbidden because `this` isn't initialized. This can be safely suppressed
// because reset() only assigns fields, it doesn't read any.
@ -118,6 +119,7 @@ public final class WebvttCssStyle {
italic = UNSPECIFIED;
fontSizeUnit = UNSPECIFIED;
textAlign = null;
combineUpright = false;
}
public void setTargetId(String targetId) {
@ -287,6 +289,14 @@ public final class WebvttCssStyle {
return fontSize;
}
public void setCombineUpright(boolean enabled) {
this.combineUpright = enabled;
}
public boolean getCombineUpright() {
return combineUpright;
}
private static int updateScoreForMatch(
int currentScore, String target, @Nullable String actual, int score) {
if (target.isEmpty() || currentScore == -1) {

View file

@ -37,6 +37,7 @@ import androidx.annotation.IntDef;
import androidx.annotation.NonNull;
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.util.Assertions;
import com.google.android.exoplayer2.util.Log;
@ -571,6 +572,10 @@ public final class WebvttCueParser {
// Do nothing.
break;
}
if (style.getCombineUpright()) {
spannedText.setSpan(
new HorizontalTextInVerticalContextSpan(), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
}
}
/**

View file

@ -0,0 +1,18 @@
WEBVTT
NOTE https://developer.mozilla.org/en-US/docs/Web/CSS/text-combine-upright
NOTE The `digits` values are ignored in CssParser and all assumed to be `all`
STYLE
::cue(.tcu-all) {
text-combine-upright: all;
}
::cue(.tcu-digits) {
text-combine-upright: digits 4;
}
00:00:00.000 --> 00:00:01.000 vertical:rl
Combine <c.tcu-all>all</c> test
00:03.000 --> 00:04.000 vertical:rl
Combine <c.tcu-digits>0004</c> digits

View file

@ -53,6 +53,8 @@ public class WebvttDecoderTest {
private static final String WITH_TAGS_FILE = "webvtt/with_tags";
private static final String WITH_CSS_STYLES = "webvtt/with_css_styles";
private static final String WITH_CSS_COMPLEX_SELECTORS = "webvtt/with_css_complex_selectors";
private static final String WITH_CSS_TEXT_COMBINE_UPRIGHT =
"webvtt/with_css_text_combine_upright";
private static final String WITH_BOM = "webvtt/with_bom";
private static final String EMPTY_FILE = "webvtt/empty";
@ -460,6 +462,20 @@ public class WebvttDecoderTest {
.isEqualTo(Typeface.ITALIC);
}
@Test
public void testWebvttWithCssTextCombineUpright() throws Exception {
WebvttSubtitle subtitle = getSubtitleForTestAsset(WITH_CSS_TEXT_COMBINE_UPRIGHT);
Spanned firstCueText = getUniqueSpanTextAt(subtitle, 500_000);
assertThat(firstCueText)
.hasHorizontalTextInVerticalContextSpanBetween("Combine ".length(), "Combine all".length());
Spanned secondCueText = getUniqueSpanTextAt(subtitle, 3_500_000);
assertThat(secondCueText)
.hasHorizontalTextInVerticalContextSpanBetween(
"Combine ".length(), "Combine 0004".length());
}
private WebvttSubtitle getSubtitleForTestAsset(String asset)
throws IOException, SubtitleDecoderException {
WebvttDecoder decoder = new WebvttDecoder();

View file

@ -30,11 +30,13 @@ import android.text.style.UnderlineSpan;
import androidx.annotation.CheckResult;
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.common.truth.FailureMetadata;
import com.google.common.truth.Subject;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
/** A Truth {@link Subject} for assertions on {@link Spanned} instances containing text styling. */
@ -183,12 +185,10 @@ public final class SpannedSubject extends Subject {
}
List<UnderlineSpan> underlineSpans = findMatchingSpans(start, end, UnderlineSpan.class);
List<Integer> allFlags = new ArrayList<>();
for (UnderlineSpan span : underlineSpans) {
allFlags.add(actual.getSpanFlags(span));
}
if (underlineSpans.size() == 1) {
return check("UnderlineSpan (start=%s,end=%s)", start, end).about(spanFlags()).that(allFlags);
return check("UnderlineSpan (start=%s,end=%s)", start, end)
.about(spanFlags())
.that(Collections.singletonList(actual.getSpanFlags(underlineSpans.get(0))));
}
failWithExpectedSpan(start, end, UnderlineSpan.class, actual.toString().substring(start, end));
return ALREADY_FAILED_WITH_FLAGS;
@ -274,6 +274,35 @@ public final class SpannedSubject extends Subject {
return check("RubySpan (start=%s,end=%s)", start, end).about(rubySpans(actual)).that(rubySpans);
}
/**
* Checks that the subject has an {@link HorizontalTextInVerticalContextSpan} from {@code start}
* to {@code end}.
*
* @param start The start of the expected span.
* @param end The end of the expected span.
* @return A {@link WithSpanFlags} object for optional additional assertions on the flags.
*/
public WithSpanFlags hasHorizontalTextInVerticalContextSpanBetween(int start, int end) {
if (actual == null) {
failWithoutActual(simpleFact("Spanned must not be null"));
return ALREADY_FAILED_WITH_FLAGS;
}
List<HorizontalTextInVerticalContextSpan> horizontalInVerticalSpans =
findMatchingSpans(start, end, HorizontalTextInVerticalContextSpan.class);
if (horizontalInVerticalSpans.size() == 1) {
return check("HorizontalTextInVerticalContextSpan (start=%s,end=%s)", start, end)
.about(spanFlags())
.that(Collections.singletonList(actual.getSpanFlags(horizontalInVerticalSpans.get(0))));
}
failWithExpectedSpan(
start,
end,
HorizontalTextInVerticalContextSpan.class,
actual.toString().substring(start, end));
return ALREADY_FAILED_WITH_FLAGS;
}
private <T> List<T> findMatchingSpans(int startIndex, int endIndex, Class<T> spanClazz) {
List<T> spans = new ArrayList<>();
for (T span : actual.getSpans(startIndex, endIndex, spanClazz)) {

View file

@ -30,6 +30,7 @@ import android.text.style.ForegroundColorSpan;
import android.text.style.StyleSpan;
import android.text.style.UnderlineSpan;
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.common.truth.ExpectFailure;
import org.junit.Test;
@ -453,6 +454,19 @@ public class SpannedSubjectTest {
.contains(String.valueOf(Spanned.SPAN_INCLUSIVE_EXCLUSIVE));
}
@Test
public void horizontalTextInVerticalContextSpan_success() {
SpannableString spannable = SpannableString.valueOf("vertical text with horizontal section");
int start = "vertical text with ".length();
int end = start + "horizontal".length();
spannable.setSpan(
new HorizontalTextInVerticalContextSpan(), start, end, Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
assertThat(spannable)
.hasHorizontalTextInVerticalContextSpanBetween(start, end)
.withFlags(Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
}
private static AssertionError expectFailure(
ExpectFailure.SimpleSubjectBuilderCallback<SpannedSubject, Spanned> callback) {
return expectFailureAbout(spanned(), callback);