From a98fc7ca487269d6c803907ee3d9ced9a6f86620 Mon Sep 17 00:00:00 2001 From: ibaker Date: Mon, 6 Jan 2020 13:51:42 +0000 Subject: [PATCH] Add tate-chu-yoko support to WebVTT decoding PiperOrigin-RevId: 288285953 --- RELEASENOTES.md | 2 + .../HorizontalTextInVerticalContextSpan.java | 32 +++++++++++++++ .../exoplayer2/text/webvtt/CssParser.java | 11 +++++- .../text/webvtt/WebvttCssStyle.java | 10 +++++ .../text/webvtt/WebvttCueParser.java | 5 +++ .../webvtt/with_css_text_combine_upright | 18 +++++++++ .../text/webvtt/WebvttDecoderTest.java | 16 ++++++++ .../testutil/truth/SpannedSubject.java | 39 ++++++++++++++++--- .../testutil/truth/SpannedSubjectTest.java | 14 +++++++ 9 files changed, 140 insertions(+), 7 deletions(-) create mode 100644 library/core/src/main/java/com/google/android/exoplayer2/text/span/HorizontalTextInVerticalContextSpan.java create mode 100644 library/core/src/test/assets/webvtt/with_css_text_combine_upright diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 0810d4fd97..5f411b7100 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -47,6 +47,8 @@ next cue ([#6833](https://github.com/google/ExoPlayer/issues/6833)). * Parse \ and \ 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) ### diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/span/HorizontalTextInVerticalContextSpan.java b/library/core/src/main/java/com/google/android/exoplayer2/text/span/HorizontalTextInVerticalContextSpan.java new file mode 100644 index 0000000000..587e1647c6 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/span/HorizontalTextInVerticalContextSpan.java @@ -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. + * + *

This is used in vertical text to write some characters in a horizontal orientation, known in + * Japanese as tate-chu-yoko. + * + *

More information on tate-chu-yoko and span styling. + */ +// 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 {} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/CssParser.java b/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/CssParser.java index 9a5ac40a05..7d5d51b706 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/CssParser.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/CssParser.java @@ -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); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttCssStyle.java b/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttCssStyle.java index 1369859552..cd08ad18cf 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttCssStyle.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttCssStyle.java @@ -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) { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttCueParser.java b/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttCueParser.java index 6de57783e0..fe36043800 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttCueParser.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttCueParser.java @@ -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); + } } /** diff --git a/library/core/src/test/assets/webvtt/with_css_text_combine_upright b/library/core/src/test/assets/webvtt/with_css_text_combine_upright new file mode 100644 index 0000000000..fd198a9c71 --- /dev/null +++ b/library/core/src/test/assets/webvtt/with_css_text_combine_upright @@ -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 all test + +00:03.000 --> 00:04.000 vertical:rl +Combine 0004 digits diff --git a/library/core/src/test/java/com/google/android/exoplayer2/text/webvtt/WebvttDecoderTest.java b/library/core/src/test/java/com/google/android/exoplayer2/text/webvtt/WebvttDecoderTest.java index 063d4e1bfd..b33439f4f3 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/text/webvtt/WebvttDecoderTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/text/webvtt/WebvttDecoderTest.java @@ -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(); diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/truth/SpannedSubject.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/truth/SpannedSubject.java index 55e2117e04..b6efa1e7b7 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/truth/SpannedSubject.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/truth/SpannedSubject.java @@ -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 underlineSpans = findMatchingSpans(start, end, UnderlineSpan.class); - List 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 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 List findMatchingSpans(int startIndex, int endIndex, Class spanClazz) { List spans = new ArrayList<>(); for (T span : actual.getSpans(startIndex, endIndex, spanClazz)) { diff --git a/testutils/src/test/java/com/google/android/exoplayer2/testutil/truth/SpannedSubjectTest.java b/testutils/src/test/java/com/google/android/exoplayer2/testutil/truth/SpannedSubjectTest.java index 3eb0509eb4..c3badd9bb9 100644 --- a/testutils/src/test/java/com/google/android/exoplayer2/testutil/truth/SpannedSubjectTest.java +++ b/testutils/src/test/java/com/google/android/exoplayer2/testutil/truth/SpannedSubjectTest.java @@ -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 callback) { return expectFailureAbout(spanned(), callback);