Fix TTML handling of inherited percentage tts:fontSize
values
The percentage should be interpreted as relative to the size of a parent node. This change makes this inheritance work correctly for percentages in both the parent and child. It does not fix the case of a non-percentage parent size with a percentage child size. PiperOrigin-RevId: 649631055
This commit is contained in:
parent
8f72054f2b
commit
bb2fd002ae
@ -14,6 +14,9 @@
|
||||
* Audio:
|
||||
* Video:
|
||||
* Text:
|
||||
* TTML: Fix handling of percentage `tts:fontSize` values to ensure they
|
||||
are correctly inherited from parent nodes with percentage `tts:fontSize`
|
||||
values.
|
||||
* Metadata:
|
||||
* Image:
|
||||
* DataSource:
|
||||
|
@ -17,6 +17,7 @@ package androidx.media3.common.text;
|
||||
|
||||
import android.text.Spannable;
|
||||
import android.text.style.ForegroundColorSpan;
|
||||
import android.text.style.RelativeSizeSpan;
|
||||
import androidx.media3.common.util.UnstableApi;
|
||||
|
||||
/**
|
||||
@ -44,14 +45,52 @@ public final class SpanUtil {
|
||||
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);
|
||||
}
|
||||
removeIfStartEndAndFlagsMatch(spannable, existingSpan, start, end, spanFlags);
|
||||
}
|
||||
spannable.setSpan(span, start, end, spanFlags);
|
||||
}
|
||||
|
||||
/**
|
||||
* Modifies the size of the text between {@code start} and {@code end} relative to any existing
|
||||
* {@link RelativeSizeSpan} instances which cover <b>at least the same range</b>.
|
||||
*
|
||||
* <p>{@link RelativeSizeSpan} instances which only cover a part of the text between {@code start}
|
||||
* and {@code end} are ignored.
|
||||
*
|
||||
* <p>A new {@link RelativeSizeSpan} instance is added between {@code start} and {@code end} with
|
||||
* its {@code sizeChange} value computed by modifying the {@code size} parameter by the {@code
|
||||
* sizeChange} of {@link RelativeSizeSpan} instances covering between {@code start} and {@code
|
||||
* end}.
|
||||
*
|
||||
* <p>{@link RelativeSizeSpan} instances with the same {@code start}, {@code end}, and {@code
|
||||
* spanFlags} are removed.
|
||||
*
|
||||
* @param spannable The {@link Spannable} to add the {@link RelativeSizeSpan} to.
|
||||
* @param size The fraction to modify the text size by.
|
||||
* @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 addInheritedRelativeSizeSpan(
|
||||
Spannable spannable, float size, int start, int end, int spanFlags) {
|
||||
for (RelativeSizeSpan existingSpan : spannable.getSpans(start, end, RelativeSizeSpan.class)) {
|
||||
if (spannable.getSpanStart(existingSpan) <= start
|
||||
&& spannable.getSpanEnd(existingSpan) >= end) {
|
||||
size *= existingSpan.getSizeChange();
|
||||
}
|
||||
removeIfStartEndAndFlagsMatch(spannable, existingSpan, start, end, spanFlags);
|
||||
}
|
||||
spannable.setSpan(new RelativeSizeSpan(size), start, end, spanFlags);
|
||||
}
|
||||
|
||||
private static void removeIfStartEndAndFlagsMatch(
|
||||
Spannable spannable, Object span, int start, int end, int spanFlags) {
|
||||
if (spannable.getSpanStart(span) == start
|
||||
&& spannable.getSpanEnd(span) == end
|
||||
&& spannable.getSpanFlags(span) == spanFlags) {
|
||||
spannable.removeSpan(span);
|
||||
}
|
||||
}
|
||||
|
||||
private SpanUtil() {}
|
||||
}
|
||||
|
@ -15,6 +15,7 @@
|
||||
*/
|
||||
package androidx.media3.common.text;
|
||||
|
||||
import static androidx.media3.test.utils.truth.SpannedSubject.assertThat;
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
|
||||
import android.graphics.Color;
|
||||
@ -23,6 +24,7 @@ import android.text.SpannableString;
|
||||
import android.text.Spanned;
|
||||
import android.text.style.BackgroundColorSpan;
|
||||
import android.text.style.ForegroundColorSpan;
|
||||
import android.text.style.RelativeSizeSpan;
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
@ -83,4 +85,76 @@ public class SpanUtilTest {
|
||||
.containsExactly(originalSpan, differentStart, differentEnd, differentFlags)
|
||||
.inOrder();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void addInheritedRelativeSizeSpan_noExistingSpans() {
|
||||
Spannable spannable = SpannableString.valueOf("test text");
|
||||
|
||||
SpanUtil.addInheritedRelativeSizeSpan(
|
||||
spannable,
|
||||
/* size= */ 0.5f,
|
||||
/* start= */ 2,
|
||||
/* end= */ 5,
|
||||
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
|
||||
assertThat(spannable).hasRelativeSizeSpanBetween(2, 5).withSizeChange(0.5f);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void addInheritedRelativeSizeSpan_existingSpanWithSameRange_replaced() {
|
||||
Spannable spannable = SpannableString.valueOf("test text");
|
||||
spannable.setSpan(
|
||||
new RelativeSizeSpan(1.6f), /* start= */ 2, /* end= */ 5, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
|
||||
SpanUtil.addInheritedRelativeSizeSpan(
|
||||
spannable,
|
||||
/* size= */ 0.5f,
|
||||
/* start= */ 2,
|
||||
/* end= */ 5,
|
||||
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
|
||||
RelativeSizeSpan[] spans = spannable.getSpans(2, 5, RelativeSizeSpan.class);
|
||||
assertThat(spans).hasLength(1);
|
||||
assertThat(spans[0].getSizeChange()).isWithin(0.0000001f).of(0.8f);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void addInheritedRelativeSizeSpan_existingLongerSpan() {
|
||||
Spannable spannable = SpannableString.valueOf("test text");
|
||||
spannable.setSpan(
|
||||
new RelativeSizeSpan(1.6f), /* start= */ 1, /* end= */ 6, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
|
||||
SpanUtil.addInheritedRelativeSizeSpan(
|
||||
spannable,
|
||||
/* size= */ 0.5f,
|
||||
/* start= */ 2,
|
||||
/* end= */ 5,
|
||||
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
|
||||
RelativeSizeSpan[] spans = spannable.getSpans(2, 5, RelativeSizeSpan.class);
|
||||
assertThat(spans).hasLength(2);
|
||||
assertThat(spannable).hasRelativeSizeSpanBetween(2, 5).withSizeChange(0.8f);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void addInheritedRelativeSizeSpan_existingIncompleteSpans_ignored() {
|
||||
Spannable spannable = SpannableString.valueOf("test text");
|
||||
spannable.setSpan(
|
||||
new RelativeSizeSpan(2.3f), /* start= */ 1, /* end= */ 4, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
spannable.setSpan(
|
||||
new RelativeSizeSpan(1.6f), /* start= */ 3, /* end= */ 4, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
spannable.setSpan(
|
||||
new RelativeSizeSpan(2.3f), /* start= */ 3, /* end= */ 6, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
|
||||
SpanUtil.addInheritedRelativeSizeSpan(
|
||||
spannable,
|
||||
/* size= */ 0.5f,
|
||||
/* start= */ 2,
|
||||
/* end= */ 5,
|
||||
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
|
||||
RelativeSizeSpan[] spans = spannable.getSpans(2, 5, RelativeSizeSpan.class);
|
||||
assertThat(spans).hasLength(4);
|
||||
assertThat(spannable).hasRelativeSizeSpanBetween(2, 5).withSizeChange(0.5f);
|
||||
}
|
||||
}
|
||||
|
@ -28,6 +28,7 @@ import androidx.media3.common.util.ColorParser;
|
||||
import androidx.media3.extractor.text.Subtitle;
|
||||
import androidx.media3.extractor.text.ttml.TtmlParser;
|
||||
import androidx.media3.test.utils.TestUtil;
|
||||
import androidx.media3.test.utils.truth.SpannedSubject;
|
||||
import androidx.test.core.app.ApplicationProvider;
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
import java.io.IOException;
|
||||
@ -149,10 +150,10 @@ public final class DelegatingSubtitleDecoderTtmlParserTest {
|
||||
public void inheritGlobalStyleOverriddenByInlineAttributes() throws IOException {
|
||||
Subtitle subtitle = getSubtitle(INHERIT_STYLE_OVERRIDE_TTML_FILE);
|
||||
|
||||
assertThat(subtitle.getEventTimeCount()).isEqualTo(4);
|
||||
assertThat(subtitle.getEventTimeCount()).isEqualTo(6);
|
||||
|
||||
Spanned firstCueText = getOnlyCueTextAtTimeUs(subtitle, 10_000_000);
|
||||
assertThat(firstCueText.toString()).isEqualTo("text 1");
|
||||
assertThat(firstCueText.toString()).isEqualTo("default + s0 styles");
|
||||
assertThat(firstCueText).hasTypefaceSpanBetween(0, firstCueText.length()).withFamily("serif");
|
||||
assertThat(firstCueText).hasBoldItalicSpanBetween(0, firstCueText.length());
|
||||
assertThat(firstCueText).hasUnderlineSpanBetween(0, firstCueText.length());
|
||||
@ -162,9 +163,12 @@ public final class DelegatingSubtitleDecoderTtmlParserTest {
|
||||
assertThat(firstCueText)
|
||||
.hasForegroundColorSpanBetween(0, firstCueText.length())
|
||||
.withColor(0xFFFFFF00);
|
||||
SpannedSubject.assertThat(firstCueText)
|
||||
.hasRelativeSizeSpanBetween(0, firstCueText.length())
|
||||
.withSizeChange(1.5f);
|
||||
|
||||
Spanned secondCueText = getOnlyCueTextAtTimeUs(subtitle, 20_000_000);
|
||||
assertThat(secondCueText.toString()).isEqualTo("text 2");
|
||||
assertThat(secondCueText.toString()).isEqualTo("default + s0 + overrides");
|
||||
assertThat(secondCueText)
|
||||
.hasTypefaceSpanBetween(0, secondCueText.length())
|
||||
.withFamily("sansSerif");
|
||||
@ -176,6 +180,15 @@ public final class DelegatingSubtitleDecoderTtmlParserTest {
|
||||
assertThat(secondCueText)
|
||||
.hasForegroundColorSpanBetween(0, secondCueText.length())
|
||||
.withColor(0xFFFFFF00);
|
||||
SpannedSubject.assertThat(secondCueText)
|
||||
.hasRelativeSizeSpanBetween(0, secondCueText.length())
|
||||
.withSizeChange(0.9f);
|
||||
|
||||
Spanned thirdCueText = getOnlyCueTextAtTimeUs(subtitle, 30_000_000);
|
||||
assertThat(thirdCueText.toString()).isEqualTo("default styling only");
|
||||
assertThat(thirdCueText)
|
||||
.hasRelativeSizeSpanBetween(0, thirdCueText.length())
|
||||
.withSizeChange(0.75f);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -239,12 +239,8 @@ import java.util.Map;
|
||||
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
break;
|
||||
case TtmlStyle.FONT_SIZE_UNIT_PERCENT:
|
||||
SpanUtil.addOrReplaceSpan(
|
||||
builder,
|
||||
new RelativeSizeSpan(style.getFontSize() / 100),
|
||||
start,
|
||||
end,
|
||||
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
SpanUtil.addInheritedRelativeSizeSpan(
|
||||
builder, style.getFontSize() / 100, start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
break;
|
||||
case TtmlStyle.UNSPECIFIED:
|
||||
// Do nothing.
|
||||
|
@ -325,10 +325,10 @@ public final class TtmlParserTest {
|
||||
public void inheritGlobalStyleOverriddenByInlineAttributes() throws Exception {
|
||||
ImmutableList<CuesWithTiming> allCues = getAllCues(INHERIT_STYLE_OVERRIDE_TTML_FILE);
|
||||
|
||||
assertThat(allCues).hasSize(2);
|
||||
assertThat(allCues).hasSize(3);
|
||||
|
||||
Spanned firstCueText = getOnlyCueTextAtIndex(allCues, 0);
|
||||
assertThat(firstCueText.toString()).isEqualTo("text 1");
|
||||
assertThat(firstCueText.toString()).isEqualTo("default + s0 styles");
|
||||
assertThat(firstCueText).hasTypefaceSpanBetween(0, firstCueText.length()).withFamily("serif");
|
||||
assertThat(firstCueText).hasBoldItalicSpanBetween(0, firstCueText.length());
|
||||
assertThat(firstCueText).hasUnderlineSpanBetween(0, firstCueText.length());
|
||||
@ -338,9 +338,12 @@ public final class TtmlParserTest {
|
||||
assertThat(firstCueText)
|
||||
.hasForegroundColorSpanBetween(0, firstCueText.length())
|
||||
.withColor(0xFFFFFF00);
|
||||
assertThat(firstCueText)
|
||||
.hasRelativeSizeSpanBetween(0, firstCueText.length())
|
||||
.withSizeChange(1.5f);
|
||||
|
||||
Spanned secondCueText = getOnlyCueTextAtIndex(allCues, 1);
|
||||
assertThat(secondCueText.toString()).isEqualTo("text 2");
|
||||
assertThat(secondCueText.toString()).isEqualTo("default + s0 + overrides");
|
||||
assertThat(secondCueText)
|
||||
.hasTypefaceSpanBetween(0, secondCueText.length())
|
||||
.withFamily("sansSerif");
|
||||
@ -352,6 +355,15 @@ public final class TtmlParserTest {
|
||||
assertThat(secondCueText)
|
||||
.hasForegroundColorSpanBetween(0, secondCueText.length())
|
||||
.withColor(0xFFFFFF00);
|
||||
assertThat(secondCueText)
|
||||
.hasRelativeSizeSpanBetween(0, secondCueText.length())
|
||||
.withSizeChange(0.9f);
|
||||
|
||||
Spanned thirdCueText = getOnlyCueTextAtIndex(allCues, 2);
|
||||
assertThat(thirdCueText.toString()).isEqualTo("default styling only");
|
||||
assertThat(thirdCueText)
|
||||
.hasRelativeSizeSpanBetween(0, thirdCueText.length())
|
||||
.withSizeChange(0.75f);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -2,28 +2,35 @@
|
||||
xmlns:ttp="http://www.w3.org/2006/10/ttaf1#parameter"
|
||||
xmlns:tts="http://www.w3.org/2006/10/ttaf1#style"
|
||||
xmlns:ttm="http://www.w3.org/2006/10/ttaf1#metadata">
|
||||
<head>
|
||||
<styling>
|
||||
<style id="s0"
|
||||
tts:fontWeight="bold"
|
||||
tts:fontStyle="italic"
|
||||
tts:fontFamily="serif"
|
||||
tts:textDecoration="underline"
|
||||
tts:backgroundColor="blue"
|
||||
tts:color="yellow"/>
|
||||
</styling>
|
||||
</head>
|
||||
<body>
|
||||
<head>
|
||||
<styling>
|
||||
<style id="default-style"
|
||||
tts:fontSize="75%"/>
|
||||
<style id="s0"
|
||||
tts:fontWeight="bold"
|
||||
tts:fontStyle="italic"
|
||||
tts:fontFamily="serif"
|
||||
tts:textDecoration="underline"
|
||||
tts:backgroundColor="blue"
|
||||
tts:color="yellow"
|
||||
tts:fontSize="200%"/>
|
||||
</styling>
|
||||
</head>
|
||||
<body style="default-style">
|
||||
<div>
|
||||
<p style="s0" begin="10s" end="18s">default + s0 styles</p>
|
||||
</div>
|
||||
<div>
|
||||
<p style="s0" begin="20s" end="28s"
|
||||
tts:fontWeight="normal"
|
||||
tts:fontFamily="sansSerif"
|
||||
tts:backgroundColor="red"
|
||||
tts:color="yellow"
|
||||
tts:fontSize="120%"
|
||||
>default + s0 + overrides</p>
|
||||
</div>
|
||||
<div>
|
||||
<p style="s0" begin="10s" end="18s">text 1</p>
|
||||
</div>
|
||||
<div>
|
||||
<p style="s0" begin="20s" end="28s"
|
||||
tts:fontWeight="normal"
|
||||
tts:fontFamily="sansSerif"
|
||||
tts:backgroundColor="red"
|
||||
tts:color="yellow"
|
||||
>text 2</p>
|
||||
<p begin="30s" end="38s">default styling only</p>
|
||||
</div>
|
||||
</body>
|
||||
</tt>
|
||||
|
@ -17,6 +17,7 @@
|
||||
package androidx.media3.test.utils.truth;
|
||||
|
||||
import static com.google.common.base.Preconditions.checkNotNull;
|
||||
import static com.google.common.truth.Correspondence.tolerance;
|
||||
import static com.google.common.truth.Fact.fact;
|
||||
import static com.google.common.truth.Fact.simpleFact;
|
||||
import static com.google.common.truth.Truth.assertAbout;
|
||||
@ -1070,7 +1071,10 @@ public final class SpannedSubject extends Subject {
|
||||
}
|
||||
}
|
||||
|
||||
check("sizeChange").that(spanSizes).containsExactly(size);
|
||||
check("sizeChange")
|
||||
.that(spanSizes)
|
||||
.comparingElementsUsing(tolerance(0.0000001))
|
||||
.containsExactly(size);
|
||||
return check("flags").about(spanFlags()).that(matchingSpanFlags);
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user