blob: 8d2724622e0f02424fd6c6d695c2bb79f9e2fac4 [file] [log] [blame]
/*
* Copyright (C) 2016 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.ttml;
import static com.google.android.exoplayer2.util.Assertions.checkNotNull;
import android.text.Spannable;
import android.text.SpannableStringBuilder;
import android.text.Spanned;
import android.text.style.AbsoluteSizeSpan;
import android.text.style.BackgroundColorSpan;
import android.text.style.ForegroundColorSpan;
import android.text.style.RelativeSizeSpan;
import android.text.style.StrikethroughSpan;
import android.text.style.StyleSpan;
import android.text.style.TypefaceSpan;
import android.text.style.UnderlineSpan;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.text.Cue;
import com.google.android.exoplayer2.text.span.HorizontalTextInVerticalContextSpan;
import com.google.android.exoplayer2.text.span.RubySpan;
import com.google.android.exoplayer2.text.span.SpanUtil;
import com.google.android.exoplayer2.text.span.TextAnnotation;
import com.google.android.exoplayer2.text.span.TextEmphasisSpan;
import com.google.android.exoplayer2.util.Log;
import com.google.android.exoplayer2.util.Util;
import java.util.ArrayDeque;
import java.util.Deque;
import java.util.Map;
/** Package internal utility class to render styled <code>TtmlNode</code>s. */
/* package */ final class TtmlRenderUtil {
private static final String TAG = "TtmlRenderUtil";
@Nullable
public static TtmlStyle resolveStyle(
@Nullable TtmlStyle style, @Nullable String[] styleIds, Map<String, TtmlStyle> globalStyles) {
if (style == null) {
if (styleIds == null) {
// No styles at all.
return null;
} else if (styleIds.length == 1) {
// Only one single referential style present.
return globalStyles.get(styleIds[0]);
} else if (styleIds.length > 1) {
// Only multiple referential styles present.
TtmlStyle chainedStyle = new TtmlStyle();
for (String id : styleIds) {
chainedStyle.chain(globalStyles.get(id));
}
return chainedStyle;
}
} else /* style != null */ {
if (styleIds != null && styleIds.length == 1) {
// Merge a single referential style into inline style.
return style.chain(globalStyles.get(styleIds[0]));
} else if (styleIds != null && styleIds.length > 1) {
// Merge multiple referential styles into inline style.
for (String id : styleIds) {
style.chain(globalStyles.get(id));
}
return style;
}
}
// Only inline styles available.
return style;
}
public static void applyStylesToSpan(
Spannable builder,
int start,
int end,
TtmlStyle style,
@Nullable TtmlNode parent,
Map<String, TtmlStyle> globalStyles,
@Cue.VerticalType int verticalType) {
if (style.getStyle() != TtmlStyle.UNSPECIFIED) {
builder.setSpan(
new StyleSpan(style.getStyle()), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
}
if (style.isLinethrough()) {
builder.setSpan(new StrikethroughSpan(), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
}
if (style.isUnderline()) {
builder.setSpan(new UnderlineSpan(), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
}
if (style.hasFontColor()) {
SpanUtil.addOrReplaceSpan(
builder,
new ForegroundColorSpan(style.getFontColor()),
start,
end,
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
}
if (style.hasBackgroundColor()) {
SpanUtil.addOrReplaceSpan(
builder,
new BackgroundColorSpan(style.getBackgroundColor()),
start,
end,
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
}
if (style.getFontFamily() != null) {
SpanUtil.addOrReplaceSpan(
builder,
new TypefaceSpan(style.getFontFamily()),
start,
end,
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
}
if (style.getTextEmphasis() != null) {
TextEmphasis textEmphasis = checkNotNull(style.getTextEmphasis());
@TextEmphasisSpan.MarkShape int markShape;
@TextEmphasisSpan.MarkFill int markFill;
if (textEmphasis.markShape == TextEmphasis.MARK_SHAPE_AUTO) {
// If a vertical writing mode applies, then 'auto' is equivalent to 'filled sesame';
// otherwise, it's equivalent to 'filled circle':
// https://www.w3.org/TR/ttml2/#style-value-emphasis-style
markShape =
(verticalType == Cue.VERTICAL_TYPE_LR || verticalType == Cue.VERTICAL_TYPE_RL)
? TextEmphasisSpan.MARK_SHAPE_SESAME
: TextEmphasisSpan.MARK_SHAPE_CIRCLE;
markFill = TextEmphasisSpan.MARK_FILL_FILLED;
} else {
markShape = textEmphasis.markShape;
markFill = textEmphasis.markFill;
}
@TextEmphasis.Position int position;
if (textEmphasis.position == TextEmphasis.POSITION_OUTSIDE) {
// 'outside' is not supported by TextEmphasisSpan, so treat it as 'before':
// https://www.w3.org/TR/ttml2/#style-value-annotation-position
position = TextAnnotation.POSITION_BEFORE;
} else {
position = textEmphasis.position;
}
SpanUtil.addOrReplaceSpan(
builder,
new TextEmphasisSpan(markShape, markFill, position),
start,
end,
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
}
switch (style.getRubyType()) {
case TtmlStyle.RUBY_TYPE_BASE:
// look for the sibling RUBY_TEXT and add it as span between start & end.
@Nullable TtmlNode containerNode = findRubyContainerNode(parent, globalStyles);
if (containerNode == null) {
// No matching container node
break;
}
@Nullable TtmlNode textNode = findRubyTextNode(containerNode, globalStyles);
if (textNode == null) {
// no matching text node
break;
}
String rubyText;
if (textNode.getChildCount() == 1 && textNode.getChild(0).text != null) {
rubyText = Util.castNonNull(textNode.getChild(0).text);
} else {
Log.i(TAG, "Skipping rubyText node without exactly one text child.");
break;
}
@Nullable
TtmlStyle textStyle = resolveStyle(textNode.style, textNode.getStyleIds(), globalStyles);
// Use position from ruby text node if defined.
@TextAnnotation.Position
int rubyPosition =
textStyle != null ? textStyle.getRubyPosition() : TextAnnotation.POSITION_UNKNOWN;
if (rubyPosition == TextAnnotation.POSITION_UNKNOWN) {
// If ruby position is not defined, use position info from container node.
@Nullable
TtmlStyle containerStyle =
resolveStyle(containerNode.style, containerNode.getStyleIds(), globalStyles);
rubyPosition = containerStyle != null ? containerStyle.getRubyPosition() : rubyPosition;
}
builder.setSpan(
new RubySpan(rubyText, rubyPosition), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
break;
case TtmlStyle.RUBY_TYPE_DELIMITER:
// TODO: Add support for this when RubySpan supports parenthetical text. For now, just
// fall through and delete the text.
case TtmlStyle.RUBY_TYPE_TEXT:
// We can't just remove the text directly from `builder` here because TtmlNode has fixed
// ideas of where every node starts and ends (nodeStartsByRegion and nodeEndsByRegion) so
// all these indices become invalid if we mutate the underlying string at this point.
// Instead we add a special span that's then handled in TtmlNode#cleanUpText.
builder.setSpan(new DeleteTextSpan(), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
break;
case TtmlStyle.RUBY_TYPE_CONTAINER:
case TtmlStyle.UNSPECIFIED:
default:
// Do nothing
break;
}
if (style.getTextCombine()) {
SpanUtil.addOrReplaceSpan(
builder,
new HorizontalTextInVerticalContextSpan(),
start,
end,
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
}
switch (style.getFontSizeUnit()) {
case TtmlStyle.FONT_SIZE_UNIT_PIXEL:
SpanUtil.addOrReplaceSpan(
builder,
new AbsoluteSizeSpan((int) style.getFontSize(), true),
start,
end,
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
break;
case TtmlStyle.FONT_SIZE_UNIT_EM:
SpanUtil.addOrReplaceSpan(
builder,
new RelativeSizeSpan(style.getFontSize()),
start,
end,
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);
break;
case TtmlStyle.UNSPECIFIED:
// Do nothing.
break;
}
}
@Nullable
private static TtmlNode findRubyTextNode(
TtmlNode rubyContainerNode, Map<String, TtmlStyle> globalStyles) {
Deque<TtmlNode> childNodesStack = new ArrayDeque<>();
childNodesStack.push(rubyContainerNode);
while (!childNodesStack.isEmpty()) {
TtmlNode childNode = childNodesStack.pop();
@Nullable
TtmlStyle style = resolveStyle(childNode.style, childNode.getStyleIds(), globalStyles);
if (style != null && style.getRubyType() == TtmlStyle.RUBY_TYPE_TEXT) {
return childNode;
}
for (int i = childNode.getChildCount() - 1; i >= 0; i--) {
childNodesStack.push(childNode.getChild(i));
}
}
return null;
}
@Nullable
private static TtmlNode findRubyContainerNode(
@Nullable TtmlNode node, Map<String, TtmlStyle> globalStyles) {
while (node != null) {
@Nullable TtmlStyle style = resolveStyle(node.style, node.getStyleIds(), globalStyles);
if (style != null && style.getRubyType() == TtmlStyle.RUBY_TYPE_CONTAINER) {
return node;
}
node = node.parent;
}
return null;
}
/**
* Called when the end of a paragraph is encountered. Adds a newline if there are one or more
* non-space characters since the previous newline.
*
* @param builder The builder.
*/
/* package */ static void endParagraph(SpannableStringBuilder builder) {
int position = builder.length() - 1;
while (position >= 0 && builder.charAt(position) == ' ') {
position--;
}
if (position >= 0 && builder.charAt(position) != '\n') {
builder.append('\n');
}
}
/**
* Applies the appropriate space policy to the given text element.
*
* @param in The text element to which the policy should be applied.
* @return The result of applying the policy to the text element.
*/
/* package */ static String applyTextElementSpacePolicy(String in) {
// Removes carriage return followed by line feed. See: http://www.w3.org/TR/xml/#sec-line-ends
String out = in.replaceAll("\r\n", "\n");
// Apply suppress-at-line-break="auto" and
// white-space-treatment="ignore-if-surrounding-linefeed"
out = out.replaceAll(" *\n *", "\n");
// Apply linefeed-treatment="treat-as-space"
out = out.replaceAll("\n", " ");
// Apply white-space-collapse="true"
out = out.replaceAll("[ \t\\x0B\f\r]+", " ");
return out;
}
private TtmlRenderUtil() {}
}