mirror of
https://github.com/samsonjs/media.git
synced 2026-04-02 10:45:51 +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)).
|
||||
* 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) ###
|
||||
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
||||
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);
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -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_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();
|
||||
|
|
|
|||
|
|
@ -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)) {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
Loading…
Reference in a new issue