Commit cdfee891 by ibaker Committed by kim-vde

Use CSS inheritance for background-color in WebViewSubtitleOutput

CSS background-color isn't inherited to inner HTML elements by default:
https://developer.mozilla.org/en-US/docs/Web/CSS/background-color

But Android Span styling assumes an outer BackgroundColorSpan will
affect inner spans. This usually doesn't make a difference, because
HTML elements are transparent by default, so there's an implicit
inheritance by just being able to see through to the 'outer' element
underneath. However this doesn't work if the inner element sits outside
the bounding box of the outer element, e.g. <rt> (ruby text, sits above/below)
or a <span> with font-size > 100%.
END_PUBLIC

Demo of <rt> and font-size problems: http://go/cpl/ruby-backgrounds/1
Demo of CSS inheritance: http://go/cpl/css-inheritance/1

PiperOrigin-RevId: 320915999
parent 245459a3
...@@ -32,4 +32,12 @@ import com.google.android.exoplayer2.util.Util; ...@@ -32,4 +32,12 @@ import com.google.android.exoplayer2.util.Util;
"rgba(%d,%d,%d,%.3f)", "rgba(%d,%d,%d,%.3f)",
Color.red(color), Color.green(color), Color.blue(color), Color.alpha(color) / 255.0); Color.red(color), Color.green(color), Color.blue(color), Color.alpha(color) / 255.0);
} }
/**
* Returns a CSS selector that selects all elements with {@code class=className} and all their
* descendants.
*/
public static String cssAllClassDescendantsSelector(String className) {
return "." + className + ",." + className + " *";
}
} }
...@@ -33,10 +33,15 @@ import com.google.android.exoplayer2.text.span.HorizontalTextInVerticalContextSp ...@@ -33,10 +33,15 @@ import com.google.android.exoplayer2.text.span.HorizontalTextInVerticalContextSp
import com.google.android.exoplayer2.text.span.RubySpan; import com.google.android.exoplayer2.text.span.RubySpan;
import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.Util; import com.google.android.exoplayer2.util.Util;
import com.google.common.collect.ImmutableMap;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.Comparator; import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.regex.Pattern; import java.util.regex.Pattern;
/** /**
...@@ -46,7 +51,6 @@ import java.util.regex.Pattern; ...@@ -46,7 +51,6 @@ import java.util.regex.Pattern;
* <p>Supports all of the spans used by ExoPlayer's subtitle decoders, including custom ones found * <p>Supports all of the spans used by ExoPlayer's subtitle decoders, including custom ones found
* in {@link com.google.android.exoplayer2.text.span}. * in {@link com.google.android.exoplayer2.text.span}.
*/ */
// TODO: Add support for more span types - only a small selection are currently implemented.
/* package */ final class SpannedToHtmlConverter { /* package */ final class SpannedToHtmlConverter {
// Matches /n and /r/n in ampersand-encoding (returned from Html.escapeHtml). // Matches /n and /r/n in ampersand-encoding (returned from Html.escapeHtml).
...@@ -74,16 +78,29 @@ import java.util.regex.Pattern; ...@@ -74,16 +78,29 @@ import java.util.regex.Pattern;
* @param displayDensity The screen density of the device. WebView treats 1 CSS px as one Android * @param displayDensity The screen density of the device. WebView treats 1 CSS px as one Android
* dp, so to convert size values from Android px to CSS px we need to know the screen density. * dp, so to convert size values from Android px to CSS px we need to know the screen density.
*/ */
public static String convert(@Nullable CharSequence text, float displayDensity) { public static HtmlAndCss convert(@Nullable CharSequence text, float displayDensity) {
if (text == null) { if (text == null) {
return ""; return new HtmlAndCss("", /* cssRuleSets= */ ImmutableMap.of());
} }
if (!(text instanceof Spanned)) { if (!(text instanceof Spanned)) {
return escapeHtml(text); return new HtmlAndCss(escapeHtml(text), /* cssRuleSets= */ ImmutableMap.of());
} }
Spanned spanned = (Spanned) text; Spanned spanned = (Spanned) text;
SparseArray<Transition> spanTransitions = findSpanTransitions(spanned, displayDensity);
// Use CSS inheritance to ensure BackgroundColorSpans affect all inner elements
Set<Integer> backgroundColors = new HashSet<>();
for (BackgroundColorSpan backgroundColorSpan :
spanned.getSpans(0, spanned.length(), BackgroundColorSpan.class)) {
backgroundColors.add(backgroundColorSpan.getBackgroundColor());
}
HashMap<String, String> cssRuleSets = new HashMap<>();
for (int backgroundColor : backgroundColors) {
cssRuleSets.put(
HtmlUtils.cssAllClassDescendantsSelector("bg_" + backgroundColor),
Util.formatInvariant("background-color:%s;", HtmlUtils.toCssRgba(backgroundColor)));
}
SparseArray<Transition> spanTransitions = findSpanTransitions(spanned, displayDensity);
StringBuilder html = new StringBuilder(spanned.length()); StringBuilder html = new StringBuilder(spanned.length());
int previousTransition = 0; int previousTransition = 0;
for (int i = 0; i < spanTransitions.size(); i++) { for (int i = 0; i < spanTransitions.size(); i++) {
...@@ -104,7 +121,7 @@ import java.util.regex.Pattern; ...@@ -104,7 +121,7 @@ import java.util.regex.Pattern;
html.append(escapeHtml(spanned.subSequence(previousTransition, spanned.length()))); html.append(escapeHtml(spanned.subSequence(previousTransition, spanned.length())));
return html.toString(); return new HtmlAndCss(html.toString(), cssRuleSets);
} }
private static SparseArray<Transition> findSpanTransitions( private static SparseArray<Transition> findSpanTransitions(
...@@ -137,9 +154,7 @@ import java.util.regex.Pattern; ...@@ -137,9 +154,7 @@ import java.util.regex.Pattern;
"<span style='color:%s;'>", HtmlUtils.toCssRgba(colorSpan.getForegroundColor())); "<span style='color:%s;'>", HtmlUtils.toCssRgba(colorSpan.getForegroundColor()));
} else if (span instanceof BackgroundColorSpan) { } else if (span instanceof BackgroundColorSpan) {
BackgroundColorSpan colorSpan = (BackgroundColorSpan) span; BackgroundColorSpan colorSpan = (BackgroundColorSpan) span;
return Util.formatInvariant( return Util.formatInvariant("<span class='bg_%s'>", colorSpan.getBackgroundColor());
"<span style='background-color:%s;'>",
HtmlUtils.toCssRgba(colorSpan.getBackgroundColor()));
} else if (span instanceof HorizontalTextInVerticalContextSpan) { } else if (span instanceof HorizontalTextInVerticalContextSpan) {
return "<span style='text-combine-upright:all;'>"; return "<span style='text-combine-upright:all;'>";
} else if (span instanceof AbsoluteSizeSpan) { } else if (span instanceof AbsoluteSizeSpan) {
...@@ -231,6 +246,26 @@ import java.util.regex.Pattern; ...@@ -231,6 +246,26 @@ import java.util.regex.Pattern;
return NEWLINE_PATTERN.matcher(escaped).replaceAll("<br>"); return NEWLINE_PATTERN.matcher(escaped).replaceAll("<br>");
} }
/** Container class for an HTML string and associated CSS rulesets. */
public static class HtmlAndCss {
/** A raw HTML string. */
public final String html;
/**
* CSS rulesets used to style {@link #html}.
*
* <p>Each key is a CSS selector, and each value is a CSS declaration (i.e. a semi-colon
* separated list of colon-separated key-value pairs, e.g "prop1:val1;prop2:val2;").
*/
public final Map<String, String> cssRuleSets;
private HtmlAndCss(String html, Map<String, String> cssRuleSets) {
this.html = html;
this.cssRuleSets = cssRuleSets;
}
}
private static final class SpanInfo { private static final class SpanInfo {
/** /**
* Sort by end index (descending), then by opening tag and then closing tag (both ascending, for * Sort by end index (descending), then by opening tag and then closing tag (both ascending, for
......
...@@ -31,11 +31,14 @@ import android.widget.FrameLayout; ...@@ -31,11 +31,14 @@ import android.widget.FrameLayout;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import com.google.android.exoplayer2.text.CaptionStyleCompat; import com.google.android.exoplayer2.text.CaptionStyleCompat;
import com.google.android.exoplayer2.text.Cue; import com.google.android.exoplayer2.text.Cue;
import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.Util; import com.google.android.exoplayer2.util.Util;
import com.google.common.base.Charsets; import com.google.common.base.Charsets;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map;
/** /**
* A {@link SubtitleView.Output} that uses a {@link WebView} to render subtitles. * A {@link SubtitleView.Output} that uses a {@link WebView} to render subtitles.
...@@ -51,6 +54,8 @@ import java.util.List; ...@@ -51,6 +54,8 @@ import java.util.List;
*/ */
private static final float CSS_LINE_HEIGHT = 1.2f; private static final float CSS_LINE_HEIGHT = 1.2f;
private static final String DEFAULT_BACKGROUND_CSS_CLASS = "default_bg";
/** /**
* A {@link CanvasSubtitleOutput} used for displaying bitmap cues. * A {@link CanvasSubtitleOutput} used for displaying bitmap cues.
* *
...@@ -162,7 +167,7 @@ import java.util.List; ...@@ -162,7 +167,7 @@ import java.util.List;
StringBuilder html = new StringBuilder(); StringBuilder html = new StringBuilder();
html.append( html.append(
Util.formatInvariant( Util.formatInvariant(
"<html><body><div style='" "<body><div style='"
+ "-webkit-user-select:none;" + "-webkit-user-select:none;"
+ "position:fixed;" + "position:fixed;"
+ "top:0;" + "top:0;"
...@@ -179,8 +184,10 @@ import java.util.List; ...@@ -179,8 +184,10 @@ import java.util.List;
CSS_LINE_HEIGHT, CSS_LINE_HEIGHT,
convertCaptionStyleToCssTextShadow(style))); convertCaptionStyleToCssTextShadow(style)));
String backgroundColorCss = HtmlUtils.toCssRgba(style.backgroundColor); Map<String, String> cssRuleSets = new HashMap<>();
cssRuleSets.put(
HtmlUtils.cssAllClassDescendantsSelector(DEFAULT_BACKGROUND_CSS_CLASS),
Util.formatInvariant("background-color:%s;", HtmlUtils.toCssRgba(style.backgroundColor)));
for (int i = 0; i < textCues.size(); i++) { for (int i = 0; i < textCues.size(); i++) {
Cue cue = textCues.get(i); Cue cue = textCues.get(i);
float positionPercent = (cue.position != Cue.DIMEN_UNSET) ? (cue.position * 100) : 50; float positionPercent = (cue.position != Cue.DIMEN_UNSET) ? (cue.position * 100) : 50;
...@@ -255,6 +262,18 @@ import java.util.List; ...@@ -255,6 +262,18 @@ import java.util.List;
verticalTranslatePercent = lineAnchorTranslatePercent; verticalTranslatePercent = lineAnchorTranslatePercent;
} }
SpannedToHtmlConverter.HtmlAndCss htmlAndCss =
SpannedToHtmlConverter.convert(
cue.text, getContext().getResources().getDisplayMetrics().density);
for (String cssSelector : cssRuleSets.keySet()) {
@Nullable
String previousCssDeclarationBlock =
cssRuleSets.put(cssSelector, cssRuleSets.get(cssSelector));
Assertions.checkState(
previousCssDeclarationBlock == null
|| previousCssDeclarationBlock.equals(cssRuleSets.get(cssSelector)));
}
html.append( html.append(
Util.formatInvariant( Util.formatInvariant(
"<div style='" "<div style='"
...@@ -280,16 +299,21 @@ import java.util.List; ...@@ -280,16 +299,21 @@ import java.util.List;
windowCssColor, windowCssColor,
horizontalTranslatePercent, horizontalTranslatePercent,
verticalTranslatePercent)) verticalTranslatePercent))
.append(Util.formatInvariant("<span style='background-color:%s;'>", backgroundColorCss)) .append(Util.formatInvariant("<span class='%s'>", DEFAULT_BACKGROUND_CSS_CLASS))
.append( .append(htmlAndCss.html)
SpannedToHtmlConverter.convert(
cue.text, getContext().getResources().getDisplayMetrics().density))
.append("</span>") .append("</span>")
.append("</div>"); .append("</div>");
} }
html.append("</div></body></html>"); html.append("</div></body></html>");
StringBuilder htmlHead = new StringBuilder();
htmlHead.append("<html><head><style>");
for (String cssSelector : cssRuleSets.keySet()) {
htmlHead.append(cssSelector).append("{").append(cssRuleSets.get(cssSelector)).append("}");
}
htmlHead.append("</style></head>");
html.insert(0, htmlHead.toString());
webView.loadData( webView.loadData(
Base64.encodeToString(html.toString().getBytes(Charsets.UTF_8), Base64.NO_PADDING), Base64.encodeToString(html.toString().getBytes(Charsets.UTF_8), Base64.NO_PADDING),
"text/html", "text/html",
......
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