mirror of
https://github.com/samsonjs/media.git
synced 2026-04-27 15:07:40 +00:00
Add tate-chu-yoko support to WebVTT decoding
PiperOrigin-RevId: 288285953
This commit is contained in:
parent
06fcf29edd
commit
a98fc7ca48
9 changed files with 140 additions and 7 deletions
|
|
@ -47,6 +47,8 @@
|
||||||
next cue ([#6833](https://github.com/google/ExoPlayer/issues/6833)).
|
next cue ([#6833](https://github.com/google/ExoPlayer/issues/6833)).
|
||||||
* Parse \<ruby\> and \<rt\> tags in WebVTT subtitles (rendering is coming
|
* Parse \<ruby\> and \<rt\> tags in WebVTT subtitles (rendering is coming
|
||||||
later).
|
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) ###
|
### 2.11.1 (2019-12-20) ###
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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 {}
|
||||||
|
|
@ -31,14 +31,19 @@ import java.util.regex.Pattern;
|
||||||
*/
|
*/
|
||||||
/* package */ final class CssParser {
|
/* 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_BGCOLOR = "background-color";
|
||||||
private static final String PROPERTY_FONT_FAMILY = "font-family";
|
private static final String PROPERTY_FONT_FAMILY = "font-family";
|
||||||
private static final String PROPERTY_FONT_WEIGHT = "font-weight";
|
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 PROPERTY_TEXT_DECORATION = "text-decoration";
|
||||||
private static final String VALUE_BOLD = "bold";
|
private static final String VALUE_BOLD = "bold";
|
||||||
private static final String VALUE_UNDERLINE = "underline";
|
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 PROPERTY_FONT_STYLE = "font-style";
|
||||||
private static final String VALUE_ITALIC = "italic";
|
private static final String VALUE_ITALIC = "italic";
|
||||||
|
|
||||||
|
|
@ -182,6 +187,8 @@ import java.util.regex.Pattern;
|
||||||
style.setFontColor(ColorParser.parseCssColor(value));
|
style.setFontColor(ColorParser.parseCssColor(value));
|
||||||
} else if (PROPERTY_BGCOLOR.equals(property)) {
|
} else if (PROPERTY_BGCOLOR.equals(property)) {
|
||||||
style.setBackgroundColor(ColorParser.parseCssColor(value));
|
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)) {
|
} else if (PROPERTY_TEXT_DECORATION.equals(property)) {
|
||||||
if (VALUE_UNDERLINE.equals(value)) {
|
if (VALUE_UNDERLINE.equals(value)) {
|
||||||
style.setUnderline(true);
|
style.setUnderline(true);
|
||||||
|
|
|
||||||
|
|
@ -95,6 +95,7 @@ public final class WebvttCssStyle {
|
||||||
@FontSizeUnit private int fontSizeUnit;
|
@FontSizeUnit private int fontSizeUnit;
|
||||||
private float fontSize;
|
private float fontSize;
|
||||||
@Nullable private Layout.Alignment textAlign;
|
@Nullable private Layout.Alignment textAlign;
|
||||||
|
private boolean combineUpright;
|
||||||
|
|
||||||
// Calling reset() is forbidden because `this` isn't initialized. This can be safely suppressed
|
// Calling reset() is forbidden because `this` isn't initialized. This can be safely suppressed
|
||||||
// because reset() only assigns fields, it doesn't read any.
|
// because reset() only assigns fields, it doesn't read any.
|
||||||
|
|
@ -118,6 +119,7 @@ public final class WebvttCssStyle {
|
||||||
italic = UNSPECIFIED;
|
italic = UNSPECIFIED;
|
||||||
fontSizeUnit = UNSPECIFIED;
|
fontSizeUnit = UNSPECIFIED;
|
||||||
textAlign = null;
|
textAlign = null;
|
||||||
|
combineUpright = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setTargetId(String targetId) {
|
public void setTargetId(String targetId) {
|
||||||
|
|
@ -287,6 +289,14 @@ public final class WebvttCssStyle {
|
||||||
return fontSize;
|
return fontSize;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void setCombineUpright(boolean enabled) {
|
||||||
|
this.combineUpright = enabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean getCombineUpright() {
|
||||||
|
return combineUpright;
|
||||||
|
}
|
||||||
|
|
||||||
private static int updateScoreForMatch(
|
private static int updateScoreForMatch(
|
||||||
int currentScore, String target, @Nullable String actual, int score) {
|
int currentScore, String target, @Nullable String actual, int score) {
|
||||||
if (target.isEmpty() || currentScore == -1) {
|
if (target.isEmpty() || currentScore == -1) {
|
||||||
|
|
|
||||||
|
|
@ -37,6 +37,7 @@ import androidx.annotation.IntDef;
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import com.google.android.exoplayer2.text.Cue;
|
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.RubySpan;
|
||||||
import com.google.android.exoplayer2.util.Assertions;
|
import com.google.android.exoplayer2.util.Assertions;
|
||||||
import com.google.android.exoplayer2.util.Log;
|
import com.google.android.exoplayer2.util.Log;
|
||||||
|
|
@ -571,6 +572,10 @@ public final class WebvttCueParser {
|
||||||
// Do nothing.
|
// Do nothing.
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
if (style.getCombineUpright()) {
|
||||||
|
spannedText.setSpan(
|
||||||
|
new HorizontalTextInVerticalContextSpan(), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
@ -53,6 +53,8 @@ public class WebvttDecoderTest {
|
||||||
private static final String WITH_TAGS_FILE = "webvtt/with_tags";
|
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_STYLES = "webvtt/with_css_styles";
|
||||||
private static final String WITH_CSS_COMPLEX_SELECTORS = "webvtt/with_css_complex_selectors";
|
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 WITH_BOM = "webvtt/with_bom";
|
||||||
private static final String EMPTY_FILE = "webvtt/empty";
|
private static final String EMPTY_FILE = "webvtt/empty";
|
||||||
|
|
||||||
|
|
@ -460,6 +462,20 @@ public class WebvttDecoderTest {
|
||||||
.isEqualTo(Typeface.ITALIC);
|
.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)
|
private WebvttSubtitle getSubtitleForTestAsset(String asset)
|
||||||
throws IOException, SubtitleDecoderException {
|
throws IOException, SubtitleDecoderException {
|
||||||
WebvttDecoder decoder = new WebvttDecoder();
|
WebvttDecoder decoder = new WebvttDecoder();
|
||||||
|
|
|
||||||
|
|
@ -30,11 +30,13 @@ import android.text.style.UnderlineSpan;
|
||||||
import androidx.annotation.CheckResult;
|
import androidx.annotation.CheckResult;
|
||||||
import androidx.annotation.ColorInt;
|
import androidx.annotation.ColorInt;
|
||||||
import androidx.annotation.Nullable;
|
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.RubySpan;
|
||||||
import com.google.common.truth.FailureMetadata;
|
import com.google.common.truth.FailureMetadata;
|
||||||
import com.google.common.truth.Subject;
|
import com.google.common.truth.Subject;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
/** A Truth {@link Subject} for assertions on {@link Spanned} instances containing text styling. */
|
/** 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<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) {
|
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));
|
failWithExpectedSpan(start, end, UnderlineSpan.class, actual.toString().substring(start, end));
|
||||||
return ALREADY_FAILED_WITH_FLAGS;
|
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);
|
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) {
|
private <T> List<T> findMatchingSpans(int startIndex, int endIndex, Class<T> spanClazz) {
|
||||||
List<T> spans = new ArrayList<>();
|
List<T> spans = new ArrayList<>();
|
||||||
for (T span : actual.getSpans(startIndex, endIndex, spanClazz)) {
|
for (T span : actual.getSpans(startIndex, endIndex, spanClazz)) {
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,7 @@ import android.text.style.ForegroundColorSpan;
|
||||||
import android.text.style.StyleSpan;
|
import android.text.style.StyleSpan;
|
||||||
import android.text.style.UnderlineSpan;
|
import android.text.style.UnderlineSpan;
|
||||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
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.RubySpan;
|
||||||
import com.google.common.truth.ExpectFailure;
|
import com.google.common.truth.ExpectFailure;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
|
|
@ -453,6 +454,19 @@ public class SpannedSubjectTest {
|
||||||
.contains(String.valueOf(Spanned.SPAN_INCLUSIVE_EXCLUSIVE));
|
.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(
|
private static AssertionError expectFailure(
|
||||||
ExpectFailure.SimpleSubjectBuilderCallback<SpannedSubject, Spanned> callback) {
|
ExpectFailure.SimpleSubjectBuilderCallback<SpannedSubject, Spanned> callback) {
|
||||||
return expectFailureAbout(spanned(), callback);
|
return expectFailureAbout(spanned(), callback);
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue