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;
"rgba(%d,%d,%d,%.3f)",
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
import com.google.android.exoplayer2.text.span.RubySpan;
import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.Util;
import com.google.common.collect.ImmutableMap;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
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
* 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 {
// Matches /n and /r/n in ampersand-encoding (returned from Html.escapeHtml).
......@@ -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
* 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) {
return "";
return new HtmlAndCss("", /* cssRuleSets= */ ImmutableMap.of());
}
if (!(text instanceof Spanned)) {
return escapeHtml(text);
return new HtmlAndCss(escapeHtml(text), /* cssRuleSets= */ ImmutableMap.of());
}
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());
int previousTransition = 0;
for (int i = 0; i < spanTransitions.size(); i++) {
......@@ -104,7 +121,7 @@ import java.util.regex.Pattern;
html.append(escapeHtml(spanned.subSequence(previousTransition, spanned.length())));
return html.toString();
return new HtmlAndCss(html.toString(), cssRuleSets);
}
private static SparseArray<Transition> findSpanTransitions(
......@@ -137,9 +154,7 @@ import java.util.regex.Pattern;
"<span style='color:%s;'>", HtmlUtils.toCssRgba(colorSpan.getForegroundColor()));
} else if (span instanceof BackgroundColorSpan) {
BackgroundColorSpan colorSpan = (BackgroundColorSpan) span;
return Util.formatInvariant(
"<span style='background-color:%s;'>",
HtmlUtils.toCssRgba(colorSpan.getBackgroundColor()));
return Util.formatInvariant("<span class='bg_%s'>", colorSpan.getBackgroundColor());
} else if (span instanceof HorizontalTextInVerticalContextSpan) {
return "<span style='text-combine-upright:all;'>";
} else if (span instanceof AbsoluteSizeSpan) {
......@@ -231,6 +246,26 @@ import java.util.regex.Pattern;
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 {
/**
* Sort by end index (descending), then by opening tag and then closing tag (both ascending, for
......
......@@ -31,11 +31,14 @@ import android.widget.FrameLayout;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.text.CaptionStyleCompat;
import com.google.android.exoplayer2.text.Cue;
import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.Util;
import com.google.common.base.Charsets;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* A {@link SubtitleView.Output} that uses a {@link WebView} to render subtitles.
......@@ -51,6 +54,8 @@ import java.util.List;
*/
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.
*
......@@ -162,7 +167,7 @@ import java.util.List;
StringBuilder html = new StringBuilder();
html.append(
Util.formatInvariant(
"<html><body><div style='"
"<body><div style='"
+ "-webkit-user-select:none;"
+ "position:fixed;"
+ "top:0;"
......@@ -179,8 +184,10 @@ import java.util.List;
CSS_LINE_HEIGHT,
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++) {
Cue cue = textCues.get(i);
float positionPercent = (cue.position != Cue.DIMEN_UNSET) ? (cue.position * 100) : 50;
......@@ -255,6 +262,18 @@ import java.util.List;
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(
Util.formatInvariant(
"<div style='"
......@@ -280,16 +299,21 @@ import java.util.List;
windowCssColor,
horizontalTranslatePercent,
verticalTranslatePercent))
.append(Util.formatInvariant("<span style='background-color:%s;'>", backgroundColorCss))
.append(
SpannedToHtmlConverter.convert(
cue.text, getContext().getResources().getDisplayMetrics().density))
.append(Util.formatInvariant("<span class='%s'>", DEFAULT_BACKGROUND_CSS_CLASS))
.append(htmlAndCss.html)
.append("</span>")
.append("</div>");
}
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(
Base64.encodeToString(html.toString().getBytes(Charsets.UTF_8), Base64.NO_PADDING),
"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