From 8cddd4d80dc3f3491ffae59d26c5c8bc1f50426e Mon Sep 17 00:00:00 2001 From: apodob Date: Tue, 3 Aug 2021 12:25:07 +0100 Subject: [PATCH] Add `font-size` support to WebVTT `CssParser`. This CL addresses the github issue [#8946](https://github.com/google/ExoPlayer/issues/8964). That issue requests support for `font-size` CSS property in WebVTT subtitle format. This CL: * Adds support for `font-size` property by extending capabilities of WebVTT `CssParser`. Implementation of `font-size` property value parsing is based on the one in `TtmlDecoder`. * Adds unit test along with test file containing WebVTT subtitles with all currently supported `font-size` units. #minor-release PiperOrigin-RevId: 388423859 --- RELEASENOTES.md | 2 + .../{CssParser.java => WebvttCssParser.java} | 38 +++++++++++-- .../text/webvtt/WebvttCssStyle.java | 2 +- .../exoplayer2/text/webvtt/WebvttDecoder.java | 4 +- ...rserTest.java => WebvttCssParserTest.java} | 14 ++--- .../text/webvtt/WebvttDecoderTest.java | 53 +++++++++++++++++++ .../test/assets/media/webvtt/with_font_size | 49 +++++++++++++++++ 7 files changed, 149 insertions(+), 13 deletions(-) rename library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/{CssParser.java => WebvttCssParser.java} (90%) rename library/core/src/test/java/com/google/android/exoplayer2/text/webvtt/{CssParserTest.java => WebvttCssParserTest.java} (96%) create mode 100644 testdata/src/test/assets/media/webvtt/with_font_size diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 424a083c67..99510826c9 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -117,6 +117,8 @@ * Text: * TTML: Inherit the `rubyPosition` value from a containing `` element. + * WebVTT: Add support for CSS `font-size` property + ([#8964](https://github.com/google/ExoPlayer/issues/8964)). * Ad playback: * Support changing ad break positions in the player logic ([#5067](https://github.com/google/ExoPlayer/issues/5067). 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/WebvttCssParser.java similarity index 90% rename from library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/CssParser.java rename to library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttCssParser.java index 7302324b48..6fd236e98f 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/WebvttCssParser.java @@ -20,8 +20,10 @@ import androidx.annotation.Nullable; import com.google.android.exoplayer2.text.span.TextAnnotation; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.ColorParser; +import com.google.android.exoplayer2.util.Log; import com.google.android.exoplayer2.util.ParsableByteArray; import com.google.android.exoplayer2.util.Util; +import com.google.common.base.Ascii; import java.util.ArrayList; import java.util.List; import java.util.regex.Matcher; @@ -31,9 +33,9 @@ import java.util.regex.Pattern; * Provides a CSS parser for STYLE blocks in Webvtt files. Supports only a subset of the CSS * features. */ -/* package */ final class CssParser { +/* package */ final class WebvttCssParser { - private static final String TAG = "CssParser"; + private static final String TAG = "WebvttCssParser"; private static final String RULE_START = "{"; private static final String RULE_END = "}"; @@ -41,6 +43,7 @@ import java.util.regex.Pattern; 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_FONT_SIZE = "font-size"; private static final String PROPERTY_RUBY_POSITION = "ruby-position"; private static final String VALUE_OVER = "over"; private static final String VALUE_UNDER = "under"; @@ -54,12 +57,14 @@ import java.util.regex.Pattern; private static final String VALUE_ITALIC = "italic"; private static final Pattern VOICE_NAME_PATTERN = Pattern.compile("\\[voice=\"([^\"]*)\"\\]"); + private static final Pattern FONT_SIZE_PATTERN = + Pattern.compile("^((?:[0-9]*\\.)?[0-9]+)(px|em|%)$"); // Temporary utility data structures. private final ParsableByteArray styleInput; private final StringBuilder stringBuilder; - public CssParser() { + public WebvttCssParser() { styleInput = new ParsableByteArray(); stringBuilder = new StringBuilder(); } @@ -213,6 +218,8 @@ import java.util.regex.Pattern; if (VALUE_ITALIC.equals(value)) { style.setItalic(true); } + } else if (PROPERTY_FONT_SIZE.equals(property)) { + parseFontSize(value, style); } // TODO: Fill remaining supported styles. } @@ -336,6 +343,31 @@ import java.util.regex.Pattern; return stringBuilder.toString(); } + private static void parseFontSize(String fontSize, WebvttCssStyle style) { + Matcher matcher = FONT_SIZE_PATTERN.matcher(Ascii.toLowerCase(fontSize)); + if (!matcher.matches()) { + Log.w(TAG, "Invalid font-size: '" + fontSize + "'."); + return; + } + String unit = Assertions.checkNotNull(matcher.group(2)); + switch (unit) { + case "px": + style.setFontSizeUnit(WebvttCssStyle.FONT_SIZE_UNIT_PIXEL); + break; + case "em": + style.setFontSizeUnit(WebvttCssStyle.FONT_SIZE_UNIT_EM); + break; + case "%": + style.setFontSizeUnit(WebvttCssStyle.FONT_SIZE_UNIT_PERCENT); + break; + default: + // this line should never be reached because when the fontSize matches the FONT_SIZE_PATTERN + // unit must be one of: px, em, % + throw new IllegalStateException(); + } + style.setFontSize(Float.parseFloat(Assertions.checkNotNull(matcher.group(1)))); + } + /** * Sets the target of a {@link WebvttCssStyle} by splitting a selector of the form {@code * ::cue(tag#id.class1.class2[voice="someone"]}, where every element is optional. 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 5b0b572aba..587f7d5ab5 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 @@ -263,7 +263,7 @@ public final class WebvttCssStyle { return this; } - public WebvttCssStyle setFontSizeUnit(short unit) { + public WebvttCssStyle setFontSizeUnit(@FontSizeUnit int unit) { this.fontSizeUnit = unit; return this; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttDecoder.java b/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttDecoder.java index c7d0e24429..0d04a3f4be 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttDecoder.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttDecoder.java @@ -44,12 +44,12 @@ public final class WebvttDecoder extends SimpleSubtitleDecoder { private static final String STYLE_START = "STYLE"; private final ParsableByteArray parsableWebvttData; - private final CssParser cssParser; + private final WebvttCssParser cssParser; public WebvttDecoder() { super("WebvttDecoder"); parsableWebvttData = new ParsableByteArray(); - cssParser = new CssParser(); + cssParser = new WebvttCssParser(); } @Override diff --git a/library/core/src/test/java/com/google/android/exoplayer2/text/webvtt/CssParserTest.java b/library/core/src/test/java/com/google/android/exoplayer2/text/webvtt/WebvttCssParserTest.java similarity index 96% rename from library/core/src/test/java/com/google/android/exoplayer2/text/webvtt/CssParserTest.java rename to library/core/src/test/java/com/google/android/exoplayer2/text/webvtt/WebvttCssParserTest.java index 2dcb3af157..095dd02b66 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/text/webvtt/CssParserTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/text/webvtt/WebvttCssParserTest.java @@ -15,7 +15,7 @@ */ package com.google.android.exoplayer2.text.webvtt; -import static com.google.android.exoplayer2.text.webvtt.CssParser.parseNextToken; +import static com.google.android.exoplayer2.text.webvtt.WebvttCssParser.parseNextToken; import static com.google.common.truth.Truth.assertThat; import androidx.test.ext.junit.runners.AndroidJUnit4; @@ -29,15 +29,15 @@ import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; -/** Unit test for {@link CssParser}. */ +/** Unit test for {@link WebvttCssParser}. */ @RunWith(AndroidJUnit4.class) -public final class CssParserTest { +public final class WebvttCssParserTest { - private CssParser parser; + private WebvttCssParser parser; @Before public void setUp() { - parser = new CssParser(); + parser = new WebvttCssParser(); } @Test @@ -232,13 +232,13 @@ public final class CssParserTest { private void assertSkipsToEndOfSkip(String expectedLine, String s) { ParsableByteArray input = new ParsableByteArray(Util.getUtf8Bytes(s)); - CssParser.skipWhitespaceAndComments(input); + WebvttCssParser.skipWhitespaceAndComments(input); assertThat(input.readLine()).isEqualTo(expectedLine); } private void assertInputLimit(String expectedLine, String s) { ParsableByteArray input = new ParsableByteArray(Util.getUtf8Bytes(s)); - CssParser.skipStyleBlock(input); + WebvttCssParser.skipStyleBlock(input); assertThat(input.readLine()).isEqualTo(expectedLine); } 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 eb565b09f5..4044d3d390 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 @@ -54,6 +54,7 @@ public class WebvttDecoderTest { private static final String WITH_BAD_CUE_HEADER_FILE = "media/webvtt/with_bad_cue_header"; private static final String WITH_TAGS_FILE = "media/webvtt/with_tags"; private static final String WITH_CSS_STYLES = "media/webvtt/with_css_styles"; + private static final String WITH_FONT_SIZE = "media/webvtt/with_font_size"; private static final String WITH_CSS_COMPLEX_SELECTORS = "media/webvtt/with_css_complex_selectors"; private static final String WITH_CSS_TEXT_COMBINE_UPRIGHT = @@ -400,6 +401,58 @@ public class WebvttDecoderTest { assertThat(secondCue.text.toString()).isEqualTo("This is the third subtitle."); } + @Test + public void decodeWithCssFontSizeStyle() throws Exception { + WebvttSubtitle subtitle = getSubtitleForTestAsset(WITH_FONT_SIZE); + + assertThat(subtitle.getEventTimeCount()).isEqualTo(12); + + assertThat(subtitle.getEventTime(0)).isEqualTo(0L); + assertThat(subtitle.getEventTime(1)).isEqualTo(2_000_000L); + Cue firstCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(0))); + assertThat(firstCue.text.toString()).isEqualTo("Sentence with font-size set to 4.4em."); + assertThat((Spanned) firstCue.text) + .hasRelativeSizeSpanBetween(0, "Sentence with font-size set to 4.4em.".length()) + .withSizeChange(4.4f); + + assertThat(subtitle.getEventTime(2)).isEqualTo(2_100_000L); + assertThat(subtitle.getEventTime(3)).isEqualTo(2_400_000L); + Cue secondCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(2))); + assertThat(secondCue.text.toString()).isEqualTo("Sentence with bad font-size unit."); + assertThat((Spanned) secondCue.text).hasNoSpans(); + + assertThat(subtitle.getEventTime(4)).isEqualTo(2_500_000L); + assertThat(subtitle.getEventTime(5)).isEqualTo(4_000_000L); + Cue thirdCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(4))); + assertThat(thirdCue.text.toString()).isEqualTo("Absolute font-size expressed in px unit!"); + assertThat((Spanned) thirdCue.text) + .hasAbsoluteSizeSpanBetween(0, "Absolute font-size expressed in px unit!".length()) + .withAbsoluteSize(2); + + assertThat(subtitle.getEventTime(6)).isEqualTo(4_500_000L); + assertThat(subtitle.getEventTime(7)).isEqualTo(6_000_000L); + Cue fourthCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(6))); + assertThat(fourthCue.text.toString()).isEqualTo("Relative font-size expressed in % unit!"); + assertThat((Spanned) fourthCue.text) + .hasRelativeSizeSpanBetween(0, "Relative font-size expressed in % unit!".length()) + .withSizeChange(0.035f); + + assertThat(subtitle.getEventTime(8)).isEqualTo(6_100_000L); + assertThat(subtitle.getEventTime(9)).isEqualTo(6_400_000L); + Cue fifthCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(8))); + assertThat(fifthCue.text.toString()).isEqualTo("Sentence with bad font-size value."); + assertThat((Spanned) secondCue.text).hasNoSpans(); + + assertThat(subtitle.getEventTime(10)).isEqualTo(6_500_000L); + assertThat(subtitle.getEventTime(11)).isEqualTo(8_000_000L); + Cue sixthCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(10))); + assertThat(sixthCue.text.toString()) + .isEqualTo("Upper and lower case letters in font-size unit."); + assertThat((Spanned) sixthCue.text) + .hasAbsoluteSizeSpanBetween(0, "Upper and lower case letters in font-size unit.".length()) + .withAbsoluteSize(2); + } + @Test public void webvttWithCssStyle() throws Exception { WebvttSubtitle subtitle = getSubtitleForTestAsset(WITH_CSS_STYLES); diff --git a/testdata/src/test/assets/media/webvtt/with_font_size b/testdata/src/test/assets/media/webvtt/with_font_size new file mode 100644 index 0000000000..91ff9b7801 --- /dev/null +++ b/testdata/src/test/assets/media/webvtt/with_font_size @@ -0,0 +1,49 @@ +WEBVTT + +STYLE +::cue(.unit-em) { + font-size: 4.4em; +} + +STYLE +::cue(.unit-px) { + font-size: 2px; +} + +STYLE +::cue(.unit-percent) { + font-size: 3.5% +} + +STYLE +::cue(.case-insensitivity) { + font-size: 2Px; +} + +STYLE +::cue(.bad-unit) { + font-size: 4.4ef; +} + +STYLE +::cue(.bad-value) { + font-size: 3.5.5% +} + +00:00.000 --> 00:02.000 +Sentence with font-size set to 4.4em. + +00:02.100 --> 00:02.400 +Sentence with bad font-size unit. + +00:02.500 --> 00:04.000 +Absolute font-size expressed in px unit! + +00:04.500 --> 00:06.000 +Relative font-size expressed in % unit! + +00:06.100 --> 00:06.400 +Sentence with bad font-size value. + +00:06.500 --> 00:08.000 +Upper and lower case letters in font-size unit.