From 1fd62c685ae9ea5e153d6cef126aa5f7b39a0242 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Thu, 1 Apr 2021 16:05:00 +0100 Subject: [PATCH] Merge pull request #8720 from dlafayet:tts-shear-block PiperOrigin-RevId: 365998615 --- RELEASENOTES.md | 5 ++- .../google/android/exoplayer2/text/Cue.java | 27 +++++++++++-- .../exoplayer2/text/ttml/TtmlDecoder.java | 40 +++++++++++++++++-- .../exoplayer2/text/ttml/TtmlNode.java | 11 +++++ .../exoplayer2/text/ttml/TtmlStyle.java | 15 +++++++ .../android/exoplayer2/text/CueTest.java | 3 ++ .../exoplayer2/text/ttml/TtmlDecoderTest.java | 30 ++++++++++++++ .../exoplayer2/text/ttml/TtmlStyleTest.java | 18 ++++++++- .../exoplayer2/ui/WebViewSubtitleOutput.java | 17 +++++++- testdata/src/test/assets/media/ttml/shear.xml | 32 +++++++++++++++ 10 files changed, 185 insertions(+), 13 deletions(-) create mode 100644 testdata/src/test/assets/media/ttml/shear.xml diff --git a/RELEASENOTES.md b/RELEASENOTES.md index bdbff6d09e..53eb6113d2 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -7,8 +7,8 @@ * Add group setting to `PlayerNotificationManager`. * Fix `StyledPlayerView` scrubber not reappearing correctly in some cases ([#8646](https://github.com/google/ExoPlayer/issues/8646)). - * Fix measurement of `StyledPlayerView` and `StyledPlayerControlView` - when `wrap_content` is used + * Fix measurement of `StyledPlayerView` and `StyledPlayerControlView` when + `wrap_content` is used ([#8726](https://github.com/google/ExoPlayer/issues/8726)). * Audio: * Report unexpected discontinuities in @@ -76,6 +76,7 @@ * Fix CEA-708 priority handling to sort cues in the order defined by the spec ([#8704](https://github.com/google/ExoPlayer/issues/8704)). * Support TTML `textEmphasis` attributes, used for Japanese boutens. + * Support TTML `shear` attributes. * MediaSession extension: Remove dependency to core module and rely on common only. The `TimelineQueueEditor` uses a new `MediaDescriptionConverter` for this purpose and does not rely on the `ConcatenatingMediaSource` anymore. diff --git a/library/common/src/main/java/com/google/android/exoplayer2/text/Cue.java b/library/common/src/main/java/com/google/android/exoplayer2/text/Cue.java index 268133ad40..49a45e1b22 100644 --- a/library/common/src/main/java/com/google/android/exoplayer2/text/Cue.java +++ b/library/common/src/main/java/com/google/android/exoplayer2/text/Cue.java @@ -269,6 +269,12 @@ public final class Cue { */ public final @VerticalType int verticalType; + /** + * The shear angle in degrees to be applied to this Cue, expressed in graphics coordinates. This + * results in a skew transform for the block along the inline progression axis. + */ + public final float shearDegrees; + /** * Creates a text cue whose {@link #textAlignment} is null, whose type parameters are set to * {@link #TYPE_UNSET} and whose dimension parameters are set to {@link #DIMEN_UNSET}. @@ -370,7 +376,8 @@ public final class Cue { /* bitmapHeight= */ DIMEN_UNSET, /* windowColorSet= */ false, /* windowColor= */ Color.BLACK, - /* verticalType= */ TYPE_UNSET); + /* verticalType= */ TYPE_UNSET, + /* shearDegrees= */ 0f); } /** @@ -415,7 +422,8 @@ public final class Cue { /* bitmapHeight= */ DIMEN_UNSET, windowColorSet, windowColor, - /* verticalType= */ TYPE_UNSET); + /* verticalType= */ TYPE_UNSET, + /* shearDegrees= */ 0f); } private Cue( @@ -433,7 +441,8 @@ public final class Cue { float bitmapHeight, boolean windowColorSet, int windowColor, - @VerticalType int verticalType) { + @VerticalType int verticalType, + float shearDegrees) { // Exactly one of text or bitmap should be set. if (text == null) { Assertions.checkNotNull(bitmap); @@ -455,6 +464,7 @@ public final class Cue { this.textSizeType = textSizeType; this.textSize = textSize; this.verticalType = verticalType; + this.shearDegrees = shearDegrees; } /** Returns a new {@link Cue.Builder} initialized with the same values as this Cue. */ @@ -479,6 +489,7 @@ public final class Cue { private boolean windowColorSet; @ColorInt private int windowColor; @VerticalType private int verticalType; + private float shearDegrees; public Builder() { text = null; @@ -514,6 +525,7 @@ public final class Cue { windowColorSet = cue.windowColorSet; windowColor = cue.windowColor; verticalType = cue.verticalType; + shearDegrees = cue.shearDegrees; } /** @@ -794,6 +806,12 @@ public final class Cue { return this; } + /** Sets the shear angle for this Cue. */ + public Builder setShearDegrees(float shearDegrees) { + this.shearDegrees = shearDegrees; + return this; + } + /** * Gets the vertical formatting for this Cue. * @@ -821,7 +839,8 @@ public final class Cue { bitmapHeight, windowColorSet, windowColor, - verticalType); + verticalType, + shearDegrees); } } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/ttml/TtmlDecoder.java b/library/core/src/main/java/com/google/android/exoplayer2/text/ttml/TtmlDecoder.java index d024ba22b6..898fd01576 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/ttml/TtmlDecoder.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/ttml/TtmlDecoder.java @@ -15,6 +15,9 @@ */ package com.google.android.exoplayer2.text.ttml; +import static java.lang.Math.max; +import static java.lang.Math.min; + import android.text.Layout; import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; @@ -81,7 +84,8 @@ public final class TtmlDecoder extends SimpleSubtitleDecoder { private static final Pattern OFFSET_TIME = Pattern.compile("^([0-9]+(?:\\.[0-9]+)?)(h|m|s|ms|f|t)$"); private static final Pattern FONT_SIZE = Pattern.compile("^(([0-9]*.)?[0-9]+)(px|em|%)$"); - private static final Pattern PERCENTAGE_COORDINATES = + static final Pattern SIGNED_PERCENTAGE = Pattern.compile("^([-+]?\\d+\\.?\\d*?)%$"); + static final Pattern PERCENTAGE_COORDINATES = Pattern.compile("^(\\d+\\.?\\d*?)% (\\d+\\.?\\d*?)%$"); private static final Pattern PIXEL_COORDINATES = Pattern.compile("^(\\d+\\.?\\d*?)px (\\d+\\.?\\d*?)px$"); @@ -614,6 +618,9 @@ public final class TtmlDecoder extends SimpleSubtitleDecoder { createIfNull(style) .setTextEmphasis(TextEmphasis.parse(Util.toLowerInvariant(attributeValue))); break; + case TtmlNode.ATTR_TTS_SHEAR: + style = createIfNull(style).setShearPercentage(parseShear(attributeValue)); + break; default: // ignore break; @@ -755,11 +762,36 @@ public final class TtmlDecoder extends SimpleSubtitleDecoder { } } + /** + * Returns the parsed shear percentage (between -100.0 and +100.0 inclusive), or {@link + * TtmlStyle#UNSPECIFIED_SHEAR} if parsing failed. + */ + private static float parseShear(String expression) { + Matcher matcher = SIGNED_PERCENTAGE.matcher(expression); + if (!matcher.matches()) { + Log.w(TAG, "Invalid value for shear: " + expression); + return TtmlStyle.UNSPECIFIED_SHEAR; + } + try { + String percentage = Assertions.checkNotNull(matcher.group(1)); + float value = Float.parseFloat(percentage); + // https://www.w3.org/TR/2018/REC-ttml2-20181108/#semantics-style-procedures-shear + // If the absolute value of the specified percentage is greater than 100%, then it must be + // interpreted as if 100% were specified with the appropriate sign. + value = max(-100f, value); + value = min(100f, value); + return value; + } catch (NumberFormatException e) { + Log.w(TAG, "Failed to parse shear: " + expression, e); + return TtmlStyle.UNSPECIFIED_SHEAR; + } + } + /** * Parses a time expression, returning the parsed timestamp. - *

- * For the format of a time expression, see: - * timeExpression + * + *

For the format of a time expression, see: timeExpression * * @param time A string that includes the time expression. * @param frameAndTickRate The effective frame and tick rates of the stream. diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/ttml/TtmlNode.java b/library/core/src/main/java/com/google/android/exoplayer2/text/ttml/TtmlNode.java index 6dce77b985..96c2dbe5f4 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/ttml/TtmlNode.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/ttml/TtmlNode.java @@ -71,6 +71,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; public static final String ATTR_TTS_TEXT_COMBINE = "textCombine"; public static final String ATTR_TTS_TEXT_EMPHASIS = "textEmphasis"; public static final String ATTR_TTS_WRITING_MODE = "writingMode"; + public static final String ATTR_TTS_SHEAR = "shear"; // Values for ruby public static final String RUBY_CONTAINER = "container"; @@ -408,6 +409,16 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; if (resolvedStyle != null) { TtmlRenderUtil.applyStylesToSpan( text, start, end, resolvedStyle, parent, globalStyles, verticalType); + if (resolvedStyle.getShearPercentage() != TtmlStyle.UNSPECIFIED_SHEAR && TAG_P.equals(tag)) { + // Shear style should only be applied to P nodes + // https://www.w3.org/TR/2018/REC-ttml2-20181108/#style-attribute-shear + // The spec doesn't specify the coordinate system to use for block shear + // however the spec shows examples of how different values are expected to be rendered. + // See: https://www.w3.org/TR/2018/REC-ttml2-20181108/#style-attribute-shear + // https://www.w3.org/TR/2018/REC-ttml2-20181108/#style-attribute-fontShear + // This maps the shear percentage to shear angle in graphics coordinates + regionOutput.setShearDegrees((resolvedStyle.getShearPercentage() * -90) / 100); + } regionOutput.setTextAlignment(resolvedStyle.getTextAlign()); } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/ttml/TtmlStyle.java b/library/core/src/main/java/com/google/android/exoplayer2/text/ttml/TtmlStyle.java index 4f73601e99..d04fde3570 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/ttml/TtmlStyle.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/ttml/TtmlStyle.java @@ -30,6 +30,7 @@ import java.lang.annotation.RetentionPolicy; /* package */ final class TtmlStyle { public static final int UNSPECIFIED = -1; + public static final float UNSPECIFIED_SHEAR = Float.MAX_VALUE; @Documented @Retention(RetentionPolicy.SOURCE) @@ -87,6 +88,7 @@ import java.lang.annotation.RetentionPolicy; @Nullable private Layout.Alignment textAlign; @OptionalBoolean private int textCombine; @Nullable private TextEmphasis textEmphasis; + private float shearPercentage; public TtmlStyle() { linethrough = UNSPECIFIED; @@ -97,6 +99,7 @@ import java.lang.annotation.RetentionPolicy; rubyType = UNSPECIFIED; rubyPosition = TextAnnotation.POSITION_UNKNOWN; textCombine = UNSPECIFIED; + shearPercentage = UNSPECIFIED_SHEAR; } /** @@ -185,6 +188,15 @@ import java.lang.annotation.RetentionPolicy; return hasBackgroundColor; } + public TtmlStyle setShearPercentage(float shearPercentage) { + this.shearPercentage = shearPercentage; + return this; + } + + public float getShearPercentage() { + return shearPercentage; + } + /** * Chains this style to referential style. Local properties which are already set are never * overridden. @@ -242,6 +254,9 @@ import java.lang.annotation.RetentionPolicy; if (textEmphasis == null) { textEmphasis = ancestor.textEmphasis; } + if (shearPercentage == UNSPECIFIED_SHEAR) { + shearPercentage = ancestor.shearPercentage; + } // attributes not inherited as of http://www.w3.org/TR/ttml1/ if (chaining && !hasBackgroundColor && ancestor.hasBackgroundColor) { setBackgroundColor(ancestor.backgroundColor); diff --git a/library/core/src/test/java/com/google/android/exoplayer2/text/CueTest.java b/library/core/src/test/java/com/google/android/exoplayer2/text/CueTest.java index c16cb928b1..74d87e08b8 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/text/CueTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/text/CueTest.java @@ -45,6 +45,7 @@ public class CueTest { .setSize(0.8f) .setWindowColor(Color.CYAN) .setVerticalType(Cue.VERTICAL_TYPE_RL) + .setShearDegrees(-15f) .build(); Cue modifiedCue = cue.buildUpon().build(); @@ -61,6 +62,7 @@ public class CueTest { assertThat(cue.windowColor).isEqualTo(Color.CYAN); assertThat(cue.windowColorSet).isTrue(); assertThat(cue.verticalType).isEqualTo(Cue.VERTICAL_TYPE_RL); + assertThat(cue.shearDegrees).isEqualTo(-15f); assertThat(modifiedCue.text).isSameInstanceAs(cue.text); assertThat(modifiedCue.textAlignment).isEqualTo(cue.textAlignment); @@ -74,6 +76,7 @@ public class CueTest { assertThat(modifiedCue.windowColor).isEqualTo(cue.windowColor); assertThat(modifiedCue.windowColorSet).isEqualTo(cue.windowColorSet); assertThat(modifiedCue.verticalType).isEqualTo(cue.verticalType); + assertThat(modifiedCue.shearDegrees).isEqualTo(cue.shearDegrees); } @Test diff --git a/library/core/src/test/java/com/google/android/exoplayer2/text/ttml/TtmlDecoderTest.java b/library/core/src/test/java/com/google/android/exoplayer2/text/ttml/TtmlDecoderTest.java index e3913670a1..2e908f3325 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/text/ttml/TtmlDecoderTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/text/ttml/TtmlDecoderTest.java @@ -69,6 +69,7 @@ public final class TtmlDecoderTest { private static final String TEXT_COMBINE_FILE = "media/ttml/text_combine.xml"; private static final String RUBIES_FILE = "media/ttml/rubies.xml"; private static final String TEXT_EMPHASIS_FILE = "media/ttml/text_emphasis.xml"; + private static final String SHEAR_FILE = "media/ttml/shear.xml"; @Test public void inlineAttributes() throws IOException, SubtitleDecoderException { @@ -816,6 +817,35 @@ public final class TtmlDecoderTest { TextAnnotation.POSITION_BEFORE); } + @Test + public void shear() throws IOException, SubtitleDecoderException { + TtmlSubtitle subtitle = getSubtitle(SHEAR_FILE); + + Cue firstCue = getOnlyCueAtTimeUs(subtitle, 10_000_000); + assertThat(firstCue.shearDegrees).isZero(); + + Cue secondCue = getOnlyCueAtTimeUs(subtitle, 20_000_000); + assertThat(secondCue.shearDegrees).isWithin(0.01f).of(-15f); + + Cue thirdCue = getOnlyCueAtTimeUs(subtitle, 30_000_000); + assertThat(thirdCue.shearDegrees).isWithin(0.01f).of(15f); + + Cue fourthCue = getOnlyCueAtTimeUs(subtitle, 40_000_000); + assertThat(fourthCue.shearDegrees).isWithin(0.01f).of(-15f); + + Cue fifthCue = getOnlyCueAtTimeUs(subtitle, 50_000_000); + assertThat(fifthCue.shearDegrees).isWithin(0.01f).of(-22.5f); + + Cue sixthCue = getOnlyCueAtTimeUs(subtitle, 60_000_000); + assertThat(sixthCue.shearDegrees).isWithin(0.01f).of(0f); + + Cue seventhCue = getOnlyCueAtTimeUs(subtitle, 70_000_000); + assertThat(seventhCue.shearDegrees).isWithin(0.01f).of(-90f); + + Cue eighthCue = getOnlyCueAtTimeUs(subtitle, 80_000_000); + assertThat(eighthCue.shearDegrees).isWithin(0.01f).of(90f); + } + private static Spanned getOnlyCueTextAtTimeUs(Subtitle subtitle, long timeUs) { Cue cue = getOnlyCueAtTimeUs(subtitle, timeUs); assertThat(cue.text).isInstanceOf(Spanned.class); diff --git a/library/core/src/test/java/com/google/android/exoplayer2/text/ttml/TtmlStyleTest.java b/library/core/src/test/java/com/google/android/exoplayer2/text/ttml/TtmlStyleTest.java index 6b57108393..4583701cc3 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/text/ttml/TtmlStyleTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/text/ttml/TtmlStyleTest.java @@ -49,6 +49,7 @@ public final class TtmlStyleTest { private static final Layout.Alignment TEXT_ALIGN = Layout.Alignment.ALIGN_CENTER; private static final boolean TEXT_COMBINE = true; public static final String TEXT_EMPHASIS_STYLE = "dot before"; + public static final float SHEAR_PERCENTAGE = 16f; private final TtmlStyle populatedStyle = new TtmlStyle() @@ -66,7 +67,8 @@ public final class TtmlStyleTest { .setRubyPosition(RUBY_POSITION) .setTextAlign(TEXT_ALIGN) .setTextCombine(TEXT_COMBINE) - .setTextEmphasis(TextEmphasis.parse(TEXT_EMPHASIS_STYLE)); + .setTextEmphasis(TextEmphasis.parse(TEXT_EMPHASIS_STYLE)) + .setShearPercentage(SHEAR_PERCENTAGE); @Test public void inheritStyle() { @@ -94,6 +96,7 @@ public final class TtmlStyleTest { assertThat(style.getTextEmphasis().markShape).isEqualTo(TextEmphasisSpan.MARK_SHAPE_DOT); assertThat(style.getTextEmphasis().markFill).isEqualTo(TextEmphasisSpan.MARK_FILL_FILLED); assertThat(style.getTextEmphasis().position).isEqualTo(POSITION_BEFORE); + assertThat(style.getShearPercentage()).isEqualTo(SHEAR_PERCENTAGE); } @Test @@ -121,6 +124,7 @@ public final class TtmlStyleTest { assertThat(style.getTextEmphasis().markShape).isEqualTo(TextEmphasisSpan.MARK_SHAPE_DOT); assertThat(style.getTextEmphasis().markFill).isEqualTo(TextEmphasisSpan.MARK_FILL_FILLED); assertThat(style.getTextEmphasis().position).isEqualTo(POSITION_BEFORE); + assertThat(style.getShearPercentage()).isEqualTo(SHEAR_PERCENTAGE); } @Test @@ -267,4 +271,16 @@ public final class TtmlStyleTest { assertThat(style.getTextEmphasis().markFill).isEqualTo(TextEmphasisSpan.MARK_FILL_OPEN); assertThat(style.getTextEmphasis().position).isEqualTo(TextAnnotation.POSITION_AFTER); } + + @Test + public void shear() { + TtmlStyle style = new TtmlStyle(); + assertThat(style.getShearPercentage()).isEqualTo(TtmlStyle.UNSPECIFIED_SHEAR); + style.setShearPercentage(101f); + assertThat(style.getShearPercentage()).isEqualTo(101f); + style.setShearPercentage(-200f); + assertThat(style.getShearPercentage()).isEqualTo(-200f); + style.setShearPercentage(0.1f); + assertThat(style.getShearPercentage()).isEqualTo(0.1f); + } } diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/WebViewSubtitleOutput.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/WebViewSubtitleOutput.java index f3de4298a5..f594056325 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/WebViewSubtitleOutput.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/WebViewSubtitleOutput.java @@ -285,7 +285,8 @@ import java.util.Map; + "writing-mode:%s;" + "font-size:%s;" + "background-color:%s;" - + "transform:translate(%s%%,%s%%);" + + "transform:translate(%s%%,%s%%)" + + "%s;" + "'>", positionProperty, positionPercent, @@ -298,7 +299,8 @@ import java.util.Map; cueTextSizeCssPx, windowCssColor, horizontalTranslatePercent, - verticalTranslatePercent)) + verticalTranslatePercent, + getBlockShearTransformFunction(cue))) .append(Util.formatInvariant("", DEFAULT_BACKGROUND_CSS_CLASS)) .append(htmlAndCss.html) .append("") @@ -320,6 +322,17 @@ import java.util.Map; "base64"); } + private static String getBlockShearTransformFunction(Cue cue) { + if (cue.shearDegrees != 0.0f) { + String direction = + (cue.verticalType == Cue.VERTICAL_TYPE_LR || cue.verticalType == Cue.VERTICAL_TYPE_RL) + ? "skewY" + : "skewX"; + return Util.formatInvariant("%s(%.2fdeg)", direction, cue.shearDegrees); + } + return ""; + } + /** * Converts a text size to a CSS px value. * diff --git a/testdata/src/test/assets/media/ttml/shear.xml b/testdata/src/test/assets/media/ttml/shear.xml new file mode 100644 index 0000000000..9ac83e700b --- /dev/null +++ b/testdata/src/test/assets/media/ttml/shear.xml @@ -0,0 +1,32 @@ + + +

+

0%

+
+
+

16.67%

+
+
+

-16.67%

+
+
+

+16.67%

+
+
+

+25%

+
+
+

Invalid

+
+
+

100.01%

+
+
+

-101.1%

+
+ +