Commit 14e401f5 by ibaker Committed by Oliver Woodman

Update TtmlDecoder to keep only one Span of each type

The current code relies on Android's evaluation order of spans, which
doesn't seem to be defined anywhere.

PiperOrigin-RevId: 288700011
parent 8e26505e
/*
* 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;
import android.text.Spannable;
import android.text.style.ForegroundColorSpan;
/**
* Utility methods for Android <a href="https://developer.android.com/guide/topics/text/spans">span
* styling</a>.
*/
public final class SpanUtil {
/**
* Adds {@code span} to {@code spannable} between {@code start} and {@code end}, removing any
* existing spans of the same type and with the same indices and flags.
*
* <p>This is useful for types of spans that don't make sense to duplicate and where the
* evaluation order might have an unexpected impact on the final text, e.g. {@link
* ForegroundColorSpan}.
*
* @param spannable The {@link Spannable} to add {@code span} to.
* @param span The span object to be added.
* @param start The start index to add the new span at.
* @param end The end index to add the new span at.
* @param spanFlags The flags to pass to {@link Spannable#setSpan(Object, int, int, int)}.
*/
public static void addOrReplaceSpan(
Spannable spannable, Object span, int start, int end, int spanFlags) {
Object[] existingSpans = spannable.getSpans(start, end, span.getClass());
for (Object existingSpan : existingSpans) {
if (spannable.getSpanStart(existingSpan) == start
&& spannable.getSpanEnd(existingSpan) == end
&& spannable.getSpanFlags(existingSpan) == spanFlags) {
spannable.removeSpan(existingSpan);
}
}
spannable.setSpan(span, start, end, spanFlags);
}
private SpanUtil() {}
}
...@@ -15,7 +15,6 @@ ...@@ -15,7 +15,6 @@
*/ */
package com.google.android.exoplayer2.text.ttml; package com.google.android.exoplayer2.text.ttml;
import android.text.Spannable;
import android.text.SpannableStringBuilder; import android.text.SpannableStringBuilder;
import android.text.Spanned; import android.text.Spanned;
import android.text.style.AbsoluteSizeSpan; import android.text.style.AbsoluteSizeSpan;
...@@ -27,6 +26,7 @@ import android.text.style.StrikethroughSpan; ...@@ -27,6 +26,7 @@ import android.text.style.StrikethroughSpan;
import android.text.style.StyleSpan; import android.text.style.StyleSpan;
import android.text.style.TypefaceSpan; import android.text.style.TypefaceSpan;
import android.text.style.UnderlineSpan; import android.text.style.UnderlineSpan;
import com.google.android.exoplayer2.text.SpanUtil;
import java.util.Map; import java.util.Map;
/** /**
...@@ -77,32 +77,60 @@ import java.util.Map; ...@@ -77,32 +77,60 @@ import java.util.Map;
builder.setSpan(new UnderlineSpan(), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); builder.setSpan(new UnderlineSpan(), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
} }
if (style.hasFontColor()) { if (style.hasFontColor()) {
builder.setSpan(new ForegroundColorSpan(style.getFontColor()), start, end, SpanUtil.addOrReplaceSpan(
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); builder,
new ForegroundColorSpan(style.getFontColor()),
start,
end,
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
} }
if (style.hasBackgroundColor()) { if (style.hasBackgroundColor()) {
builder.setSpan(new BackgroundColorSpan(style.getBackgroundColor()), start, end, SpanUtil.addOrReplaceSpan(
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); builder,
new BackgroundColorSpan(style.getBackgroundColor()),
start,
end,
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
} }
if (style.getFontFamily() != null) { if (style.getFontFamily() != null) {
builder.setSpan(new TypefaceSpan(style.getFontFamily()), start, end, SpanUtil.addOrReplaceSpan(
builder,
new TypefaceSpan(style.getFontFamily()),
start,
end,
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
} }
if (style.getTextAlign() != null) { if (style.getTextAlign() != null) {
builder.setSpan(new AlignmentSpan.Standard(style.getTextAlign()), start, end, SpanUtil.addOrReplaceSpan(
builder,
new AlignmentSpan.Standard(style.getTextAlign()),
start,
end,
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
} }
switch (style.getFontSizeUnit()) { switch (style.getFontSizeUnit()) {
case TtmlStyle.FONT_SIZE_UNIT_PIXEL: case TtmlStyle.FONT_SIZE_UNIT_PIXEL:
builder.setSpan(new AbsoluteSizeSpan((int) style.getFontSize(), true), start, end, SpanUtil.addOrReplaceSpan(
builder,
new AbsoluteSizeSpan((int) style.getFontSize(), true),
start,
end,
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
break; break;
case TtmlStyle.FONT_SIZE_UNIT_EM: case TtmlStyle.FONT_SIZE_UNIT_EM:
builder.setSpan(new RelativeSizeSpan(style.getFontSize()), start, end, SpanUtil.addOrReplaceSpan(
builder,
new RelativeSizeSpan(style.getFontSize()),
start,
end,
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
break; break;
case TtmlStyle.FONT_SIZE_UNIT_PERCENT: case TtmlStyle.FONT_SIZE_UNIT_PERCENT:
builder.setSpan(new RelativeSizeSpan(style.getFontSize() / 100), start, end, SpanUtil.addOrReplaceSpan(
builder,
new RelativeSizeSpan(style.getFontSize() / 100),
start,
end,
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
break; break;
case TtmlStyle.UNSPECIFIED: case TtmlStyle.UNSPECIFIED:
......
...@@ -15,11 +15,11 @@ ...@@ -15,11 +15,11 @@
*/ */
package com.google.android.exoplayer2.text.webvtt; package com.google.android.exoplayer2.text.webvtt;
import static com.google.android.exoplayer2.text.SpanUtil.addOrReplaceSpan;
import static java.lang.annotation.RetentionPolicy.SOURCE; import static java.lang.annotation.RetentionPolicy.SOURCE;
import android.graphics.Typeface; import android.graphics.Typeface;
import android.text.Layout; import android.text.Layout;
import android.text.Spannable;
import android.text.SpannableStringBuilder; import android.text.SpannableStringBuilder;
import android.text.Spanned; import android.text.Spanned;
import android.text.SpannedString; import android.text.SpannedString;
...@@ -535,7 +535,12 @@ public final class WebvttCueParser { ...@@ -535,7 +535,12 @@ public final class WebvttCueParser {
return; return;
} }
if (style.getStyle() != WebvttCssStyle.UNSPECIFIED) { if (style.getStyle() != WebvttCssStyle.UNSPECIFIED) {
addOrReplaceSpan(spannedText, new StyleSpan(style.getStyle()), start, end); addOrReplaceSpan(
spannedText,
new StyleSpan(style.getStyle()),
start,
end,
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
} }
if (style.isLinethrough()) { if (style.isLinethrough()) {
spannedText.setSpan(new StrikethroughSpan(), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); spannedText.setSpan(new StrikethroughSpan(), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
...@@ -544,29 +549,62 @@ public final class WebvttCueParser { ...@@ -544,29 +549,62 @@ public final class WebvttCueParser {
spannedText.setSpan(new UnderlineSpan(), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); spannedText.setSpan(new UnderlineSpan(), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
} }
if (style.hasFontColor()) { if (style.hasFontColor()) {
addOrReplaceSpan(spannedText, new ForegroundColorSpan(style.getFontColor()), start, end); addOrReplaceSpan(
spannedText,
new ForegroundColorSpan(style.getFontColor()),
start,
end,
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
} }
if (style.hasBackgroundColor()) { if (style.hasBackgroundColor()) {
addOrReplaceSpan( addOrReplaceSpan(
spannedText, new BackgroundColorSpan(style.getBackgroundColor()), start, end); spannedText,
new BackgroundColorSpan(style.getBackgroundColor()),
start,
end,
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
} }
if (style.getFontFamily() != null) { if (style.getFontFamily() != null) {
addOrReplaceSpan(spannedText, new TypefaceSpan(style.getFontFamily()), start, end); addOrReplaceSpan(
spannedText,
new TypefaceSpan(style.getFontFamily()),
start,
end,
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
} }
Layout.Alignment textAlign = style.getTextAlign(); Layout.Alignment textAlign = style.getTextAlign();
if (textAlign != null) { if (textAlign != null) {
addOrReplaceSpan(spannedText, new AlignmentSpan.Standard(textAlign), start, end); addOrReplaceSpan(
spannedText,
new AlignmentSpan.Standard(textAlign),
start,
end,
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
} }
switch (style.getFontSizeUnit()) { switch (style.getFontSizeUnit()) {
case WebvttCssStyle.FONT_SIZE_UNIT_PIXEL: case WebvttCssStyle.FONT_SIZE_UNIT_PIXEL:
addOrReplaceSpan( addOrReplaceSpan(
spannedText, new AbsoluteSizeSpan((int) style.getFontSize(), true), start, end); spannedText,
new AbsoluteSizeSpan((int) style.getFontSize(), true),
start,
end,
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
break; break;
case WebvttCssStyle.FONT_SIZE_UNIT_EM: case WebvttCssStyle.FONT_SIZE_UNIT_EM:
addOrReplaceSpan(spannedText, new RelativeSizeSpan(style.getFontSize()), start, end); addOrReplaceSpan(
spannedText,
new RelativeSizeSpan(style.getFontSize()),
start,
end,
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
break; break;
case WebvttCssStyle.FONT_SIZE_UNIT_PERCENT: case WebvttCssStyle.FONT_SIZE_UNIT_PERCENT:
addOrReplaceSpan(spannedText, new RelativeSizeSpan(style.getFontSize() / 100), start, end); addOrReplaceSpan(
spannedText,
new RelativeSizeSpan(style.getFontSize() / 100),
start,
end,
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
break; break;
case WebvttCssStyle.UNSPECIFIED: case WebvttCssStyle.UNSPECIFIED:
// Do nothing. // Do nothing.
...@@ -579,26 +617,6 @@ public final class WebvttCueParser { ...@@ -579,26 +617,6 @@ public final class WebvttCueParser {
} }
/** /**
* Adds {@code span} to {@code spannedText} between {@code start} and {@code end}, removing any
* existing spans of the same type and with the same indices.
*
* <p>This is useful for types of spans that don't make sense to duplicate and where the
* evaluation order might have an unexpected impact on the final text, e.g. {@link
* ForegroundColorSpan}.
*/
private static void addOrReplaceSpan(
SpannableStringBuilder spannedText, Object span, int start, int end) {
Object[] existingSpans = spannedText.getSpans(start, end, span.getClass());
for (Object existingSpan : existingSpans) {
if (spannedText.getSpanStart(existingSpan) == start
&& spannedText.getSpanEnd(existingSpan) == end) {
spannedText.removeSpan(existingSpan);
}
}
spannedText.setSpan(span, start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
}
/**
* Returns the tag name for the given tag contents. * Returns the tag name for the given tag contents.
* *
* @param tagExpression Characters between &amp;lt: and &amp;gt; of a start or end tag. * @param tagExpression Characters between &amp;lt: and &amp;gt; of a start or end tag.
......
/*
* 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;
import static com.google.common.truth.Truth.assertThat;
import android.graphics.Color;
import android.text.Spannable;
import android.text.SpannableString;
import android.text.Spanned;
import android.text.style.BackgroundColorSpan;
import android.text.style.ForegroundColorSpan;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import org.junit.Test;
import org.junit.runner.RunWith;
/** Tests for {@link SpanUtil}. */
@RunWith(AndroidJUnit4.class)
public class SpanUtilTest {
@Test
public void addOrReplaceSpan_replacesSameTypeAndIndexes() {
Spannable spannable = SpannableString.valueOf("test text");
spannable.setSpan(
new ForegroundColorSpan(Color.CYAN),
/* start= */ 2,
/* end= */ 5,
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
ForegroundColorSpan newSpan = new ForegroundColorSpan(Color.BLUE);
SpanUtil.addOrReplaceSpan(
spannable, newSpan, /* start= */ 2, /* end= */ 5, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
Object[] spans = spannable.getSpans(0, spannable.length(), Object.class);
assertThat(spans).asList().containsExactly(newSpan);
}
@Test
public void addOrReplaceSpan_ignoresDifferentType() {
Spannable spannable = SpannableString.valueOf("test text");
ForegroundColorSpan originalSpan = new ForegroundColorSpan(Color.CYAN);
spannable.setSpan(originalSpan, /* start= */ 2, /* end= */ 5, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
BackgroundColorSpan newSpan = new BackgroundColorSpan(Color.BLUE);
SpanUtil.addOrReplaceSpan(spannable, newSpan, 2, 5, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
Object[] spans = spannable.getSpans(0, spannable.length(), Object.class);
assertThat(spans).asList().containsExactly(originalSpan, newSpan).inOrder();
}
@Test
public void addOrReplaceSpan_ignoresDifferentStartEndAndFlags() {
Spannable spannable = SpannableString.valueOf("test text");
ForegroundColorSpan originalSpan = new ForegroundColorSpan(Color.CYAN);
spannable.setSpan(originalSpan, /* start= */ 2, /* end= */ 5, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
ForegroundColorSpan differentStart = new ForegroundColorSpan(Color.GREEN);
SpanUtil.addOrReplaceSpan(
spannable, differentStart, /* start= */ 3, /* end= */ 5, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
ForegroundColorSpan differentEnd = new ForegroundColorSpan(Color.BLUE);
SpanUtil.addOrReplaceSpan(
spannable, differentEnd, /* start= */ 2, /* end= */ 6, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
ForegroundColorSpan differentFlags = new ForegroundColorSpan(Color.GREEN);
SpanUtil.addOrReplaceSpan(
spannable, differentFlags, /* start= */ 2, /* end= */ 5, Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
Object[] spans = spannable.getSpans(0, spannable.length(), Object.class);
assertThat(spans)
.asList()
.containsExactly(originalSpan, differentStart, differentEnd, differentFlags)
.inOrder();
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment