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..51c262f8a9 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 expressed in graphics coordinates to be applied to this block. 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, + 0f); } /** @@ -415,7 +422,8 @@ public final class Cue { /* bitmapHeight= */ DIMEN_UNSET, windowColorSet, windowColor, - /* verticalType= */ TYPE_UNSET); + /* verticalType= */ TYPE_UNSET, + 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,14 @@ 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 +841,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..cca125f6bb 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 @@ -81,7 +81,9 @@ 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$"); @@ -613,6 +615,13 @@ public final class TtmlDecoder extends SimpleSubtitleDecoder { style = createIfNull(style) .setTextEmphasis(TextEmphasis.parse(Util.toLowerInvariant(attributeValue))); + case TtmlNode.ATTR_TTS_SHEAR: + style = createIfNull(style); + try { + parseShear(attributeValue, style); + } catch (SubtitleDecoderException e) { + Log.w(TAG, "Failed parsing shear value: " + attributeValue); + } break; default: // ignore @@ -755,6 +764,27 @@ public final class TtmlDecoder extends SimpleSubtitleDecoder { } } + private static void parseShear(String expression, TtmlStyle out) throws + SubtitleDecoderException { + Matcher matcher = SIGNED_PERCENTAGE.matcher(expression); + if (matcher.matches()) { + try { + float value = Float.parseFloat(matcher.group(1)); + // 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 = Math.max(-100f, value); + value = Math.min(100f, value); + out.setShearPercentage(value); + } catch (NumberFormatException e) { + throw new SubtitleDecoderException("Invalid expression for shear: '" + expression + "'.", + e); + } + } else { + throw new SubtitleDecoderException("Invalid expression for shear: '" + expression + "'."); + } + } + /** * Parses a time expression, returning the parsed timestamp. *
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..0b1a0538b0 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() != 0.0f && 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..203322957f 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
@@ -87,6 +87,8 @@ 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;
@@ -185,6 +187,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 +253,9 @@ import java.lang.annotation.RetentionPolicy;
if (textEmphasis == null) {
textEmphasis = ancestor.textEmphasis;
}
+ if (shearPercentage == 0.0f) {
+ 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..786358343c 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,37 @@ public final class TtmlDecoderTest {
TextAnnotation.POSITION_BEFORE);
}
+ @Test
+ public void shear() throws IOException, SubtitleDecoderException {
+ TtmlSubtitle subtitle = getSubtitle(SHEAR_FILE);
+ final float TOLERANCE = 0.01f;
+
+ Cue firstCue = getOnlyCueAtTimeUs(subtitle, 10_000_000);
+ assertThat(firstCue.shearDegrees).isEqualTo(0f);
+
+ Cue secondCue = getOnlyCueAtTimeUs(subtitle, 20_000_000);
+ assertThat(secondCue.shearDegrees).isWithin(TOLERANCE).of(-15f);
+
+ Cue thirdCue = getOnlyCueAtTimeUs(subtitle, 30_000_000);
+ assertThat(thirdCue.shearDegrees).isWithin(TOLERANCE).of(15f);
+
+ Cue fourthCue = getOnlyCueAtTimeUs(subtitle, 40_000_000);
+ assertThat(fourthCue.shearDegrees).isWithin(TOLERANCE).of(-15f);
+
+ Cue fifthCue = getOnlyCueAtTimeUs(subtitle, 50_000_000);
+ assertThat(fifthCue.shearDegrees).isWithin(TOLERANCE).of(-22.5f);
+
+ Cue sixthCue = getOnlyCueAtTimeUs(subtitle, 60_000_000);
+ assertThat(sixthCue.shearDegrees).isWithin(TOLERANCE).of(0f);
+
+ Cue seventhCue = getOnlyCueAtTimeUs(subtitle, 70_000_000);
+ assertThat(seventhCue.shearDegrees).isWithin(TOLERANCE).of(-90f);
+
+ Cue eighthCue = getOnlyCueAtTimeUs(subtitle, 80_000_000);
+ assertThat(eighthCue.shearDegrees).isWithin(TOLERANCE).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..3a81194b88 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);
+ assertWithMessage("shear").that(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);
+ assertWithMessage("shear").that(style.getShearPercentage()).isEqualTo(SHEAR_PERCENTAGE);
}
@Test
@@ -267,4 +271,15 @@ public final class TtmlStyleTest {
assertThat(style.getTextEmphasis().markFill).isEqualTo(TextEmphasisSpan.MARK_FILL_OPEN);
assertThat(style.getTextEmphasis().position).isEqualTo(TextAnnotation.POSITION_AFTER);
}
+
+ public void shear() {
+ TtmlStyle style = new TtmlStyle();
+ assertThat(style.getShearPercentage()).isEqualTo(0f);
+ 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..277b3b94ec 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
@@ -262,9 +262,8 @@ import java.util.Map;
verticalTranslatePercent = lineAnchorTranslatePercent;
}
- SpannedToHtmlConverter.HtmlAndCss htmlAndCss =
- SpannedToHtmlConverter.convert(
- cue.text, getContext().getResources().getDisplayMetrics().density);
+ SpannedToHtmlConverter.HtmlAndCss htmlAndCss = SpannedToHtmlConverter
+ .convert(cue.text, getContext().getResources().getDisplayMetrics().density);
for (String cssSelector : cssRuleSets.keySet()) {
@Nullable
String previousCssDeclarationBlock =
@@ -285,7 +284,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 +298,8 @@ import java.util.Map;
cueTextSizeCssPx,
windowCssColor,
horizontalTranslatePercent,
- verticalTranslatePercent))
+ verticalTranslatePercent,
+ getBlockShear(cue)))
.append(Util.formatInvariant("", DEFAULT_BACKGROUND_CSS_CLASS))
.append(htmlAndCss.html)
.append("")
@@ -320,6 +321,16 @@ import java.util.Map;
"base64");
}
+ private static String getBlockShear(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..d7274bd0a7
--- /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%