Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support tts:shear in TTML parser and WebView output #8720

Merged
merged 2 commits into from
Apr 1, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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}.
Expand Down Expand Up @@ -370,7 +376,8 @@ public Cue(
/* bitmapHeight= */ DIMEN_UNSET,
/* windowColorSet= */ false,
/* windowColor= */ Color.BLACK,
/* verticalType= */ TYPE_UNSET);
/* verticalType= */ TYPE_UNSET,
0f);
}

/**
Expand Down Expand Up @@ -415,7 +422,8 @@ public Cue(
/* bitmapHeight= */ DIMEN_UNSET,
windowColorSet,
windowColor,
/* verticalType= */ TYPE_UNSET);
/* verticalType= */ TYPE_UNSET,
0f);
}

private Cue(
Expand All @@ -433,7 +441,8 @@ private 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);
Expand All @@ -455,6 +464,7 @@ private 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. */
Expand All @@ -479,6 +489,7 @@ public static final class Builder {
private boolean windowColorSet;
@ColorInt private int windowColor;
@VerticalType private int verticalType;
private float shearDegrees;

public Builder() {
text = null;
Expand Down Expand Up @@ -514,6 +525,7 @@ private Builder(Cue cue) {
windowColorSet = cue.windowColorSet;
windowColor = cue.windowColor;
verticalType = cue.verticalType;
shearDegrees = cue.shearDegrees;
}

/**
Expand Down Expand Up @@ -794,6 +806,14 @@ public Builder setVerticalType(@VerticalType int verticalType) {
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.
*
Expand Down Expand Up @@ -821,7 +841,8 @@ public Cue build() {
bitmapHeight,
windowColorSet,
windowColor,
verticalType);
verticalType,
shearDegrees);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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$");
Expand Down Expand Up @@ -614,6 +616,9 @@ private static String[] parseStyleIds(String parentStyleIds) {
createIfNull(style)
.setTextEmphasis(TextEmphasis.parse(Util.toLowerInvariant(attributeValue)));
break;
case TtmlNode.ATTR_TTS_SHEAR:
style = createIfNull(style).setShearPercentage(parseShear(attributeValue));
break;
default:
// ignore
break;
Expand Down Expand Up @@ -755,6 +760,27 @@ private static void parseFontSize(String expression, TtmlStyle out) throws
}
}

private static float parseShear(String expression) {
Matcher matcher = SIGNED_PERCENTAGE.matcher(expression);
if (matcher.matches()) {
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 = Math.max(-100f, value);
value = Math.min(100f, value);
return value;
} catch (NumberFormatException e) {
Log.w(TAG, "NumberFormatException while parsing shear: " + expression);
}
} else {
Log.w(TAG, "Invalid value for shear: " + expression);
}
return 0f;
}

/**
* Parses a time expression, returning the parsed timestamp.
* <p>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@
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";
Expand Down Expand Up @@ -408,6 +409,16 @@ private void applyStyleToOutput(
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());
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,8 @@
@Nullable private Layout.Alignment textAlign;
@OptionalBoolean private int textCombine;
@Nullable private TextEmphasis textEmphasis;
private float shearPercentage;


public TtmlStyle() {
linethrough = UNSPECIFIED;
Expand Down Expand Up @@ -185,6 +187,15 @@ public boolean hasBackgroundColor() {
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.
Expand Down Expand Up @@ -242,6 +253,9 @@ private TtmlStyle inherit(@Nullable TtmlStyle ancestor, boolean chaining) {
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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ public void buildAndBuildUponWorkAsExpected() {
.setSize(0.8f)
.setWindowColor(Color.CYAN)
.setVerticalType(Cue.VERTICAL_TYPE_RL)
.setShearDegrees(-15f)
.build();

Cue modifiedCue = cue.buildUpon().build();
Expand All @@ -61,6 +62,7 @@ public void buildAndBuildUponWorkAsExpected() {
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);
Expand All @@ -74,6 +76,7 @@ public void buildAndBuildUponWorkAsExpected() {
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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -816,6 +817,37 @@ public void textEmphasis() throws IOException, SubtitleDecoderException {
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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -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() {
Expand Down Expand Up @@ -94,6 +96,7 @@ public void inheritStyle() {
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
Expand Down Expand Up @@ -121,6 +124,7 @@ public void chainStyle() {
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
Expand Down Expand Up @@ -267,4 +271,15 @@ public void textEmphasis() {
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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -262,9 +262,8 @@ private void updateWebView() {
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 =
Expand All @@ -285,7 +284,8 @@ private void updateWebView() {
+ "writing-mode:%s;"
+ "font-size:%s;"
+ "background-color:%s;"
+ "transform:translate(%s%%,%s%%);"
+ "transform:translate(%s%%,%s%%)"
+ "%s;"
+ "'>",
positionProperty,
positionPercent,
Expand All @@ -298,7 +298,8 @@ private void updateWebView() {
cueTextSizeCssPx,
windowCssColor,
horizontalTranslatePercent,
verticalTranslatePercent))
verticalTranslatePercent,
getBlockShear(cue)))
.append(Util.formatInvariant("<span class='%s'>", DEFAULT_BACKGROUND_CSS_CLASS))
.append(htmlAndCss.html)
.append("</span>")
Expand All @@ -320,6 +321,16 @@ private void updateWebView() {
"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.
*
Expand Down
Loading