1 /* 2 * Copyright (C) 2016 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 package com.google.android.exoplayer2.text.ttml; 17 18 import android.text.Layout.Alignment; 19 import android.text.Spannable; 20 import android.text.SpannableStringBuilder; 21 import android.text.Spanned; 22 import android.text.style.AbsoluteSizeSpan; 23 import android.text.style.AlignmentSpan; 24 import android.text.style.BackgroundColorSpan; 25 import android.text.style.ForegroundColorSpan; 26 import android.text.style.RelativeSizeSpan; 27 import android.text.style.StrikethroughSpan; 28 import android.text.style.StyleSpan; 29 import android.text.style.TypefaceSpan; 30 import android.text.style.UnderlineSpan; 31 import androidx.annotation.Nullable; 32 import com.google.android.exoplayer2.text.span.HorizontalTextInVerticalContextSpan; 33 import com.google.android.exoplayer2.text.span.RubySpan; 34 import com.google.android.exoplayer2.text.span.SpanUtil; 35 import com.google.android.exoplayer2.util.Log; 36 import com.google.android.exoplayer2.util.Util; 37 import java.util.ArrayDeque; 38 import java.util.Deque; 39 import java.util.Map; 40 41 /** 42 * Package internal utility class to render styled <code>TtmlNode</code>s. 43 */ 44 /* package */ final class TtmlRenderUtil { 45 46 private static final String TAG = "TtmlRenderUtil"; 47 48 @Nullable resolveStyle( @ullable TtmlStyle style, @Nullable String[] styleIds, Map<String, TtmlStyle> globalStyles)49 public static TtmlStyle resolveStyle( 50 @Nullable TtmlStyle style, @Nullable String[] styleIds, Map<String, TtmlStyle> globalStyles) { 51 if (style == null) { 52 if (styleIds == null) { 53 // No styles at all. 54 return null; 55 } else if (styleIds.length == 1) { 56 // Only one single referential style present. 57 return globalStyles.get(styleIds[0]); 58 } else if (styleIds.length > 1) { 59 // Only multiple referential styles present. 60 TtmlStyle chainedStyle = new TtmlStyle(); 61 for (String id : styleIds) { 62 chainedStyle.chain(globalStyles.get(id)); 63 } 64 return chainedStyle; 65 } 66 } else /* style != null */ { 67 if (styleIds != null && styleIds.length == 1) { 68 // Merge a single referential style into inline style. 69 return style.chain(globalStyles.get(styleIds[0])); 70 } else if (styleIds != null && styleIds.length > 1) { 71 // Merge multiple referential styles into inline style. 72 for (String id : styleIds) { 73 style.chain(globalStyles.get(id)); 74 } 75 return style; 76 } 77 } 78 // Only inline styles available. 79 return style; 80 } 81 applyStylesToSpan( Spannable builder, int start, int end, TtmlStyle style, @Nullable TtmlNode parent)82 public static void applyStylesToSpan( 83 Spannable builder, int start, int end, TtmlStyle style, @Nullable TtmlNode parent) { 84 85 if (style.getStyle() != TtmlStyle.UNSPECIFIED) { 86 builder.setSpan(new StyleSpan(style.getStyle()), start, end, 87 Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); 88 } 89 if (style.isLinethrough()) { 90 builder.setSpan(new StrikethroughSpan(), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); 91 } 92 if (style.isUnderline()) { 93 builder.setSpan(new UnderlineSpan(), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); 94 } 95 if (style.hasFontColor()) { 96 SpanUtil.addOrReplaceSpan( 97 builder, 98 new ForegroundColorSpan(style.getFontColor()), 99 start, 100 end, 101 Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); 102 } 103 if (style.hasBackgroundColor()) { 104 SpanUtil.addOrReplaceSpan( 105 builder, 106 new BackgroundColorSpan(style.getBackgroundColor()), 107 start, 108 end, 109 Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); 110 } 111 if (style.getFontFamily() != null) { 112 SpanUtil.addOrReplaceSpan( 113 builder, 114 new TypefaceSpan(style.getFontFamily()), 115 start, 116 end, 117 Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); 118 } 119 switch (style.getRubyType()) { 120 case TtmlStyle.RUBY_TYPE_BASE: 121 // look for the sibling RUBY_TEXT and add it as span between start & end. 122 @Nullable TtmlNode containerNode = findRubyContainerNode(parent); 123 if (containerNode == null) { 124 // No matching container node 125 break; 126 } 127 @Nullable TtmlNode textNode = findRubyTextNode(containerNode); 128 if (textNode == null) { 129 // no matching text node 130 break; 131 } 132 String rubyText; 133 if (textNode.getChildCount() == 1 && textNode.getChild(0).text != null) { 134 rubyText = Util.castNonNull(textNode.getChild(0).text); 135 } else { 136 Log.i(TAG, "Skipping rubyText node without exactly one text child."); 137 break; 138 } 139 140 // TODO: Get rubyPosition from `textNode` when TTML inheritance is implemented. 141 @RubySpan.Position 142 int rubyPosition = 143 containerNode.style != null 144 ? containerNode.style.getRubyPosition() 145 : RubySpan.POSITION_UNKNOWN; 146 builder.setSpan( 147 new RubySpan(rubyText, rubyPosition), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); 148 break; 149 case TtmlStyle.RUBY_TYPE_DELIMITER: 150 // TODO: Add support for this when RubySpan supports parenthetical text. For now, just 151 // fall through and delete the text. 152 case TtmlStyle.RUBY_TYPE_TEXT: 153 // We can't just remove the text directly from `builder` here because TtmlNode has fixed 154 // ideas of where every node starts and ends (nodeStartsByRegion and nodeEndsByRegion) so 155 // all these indices become invalid if we mutate the underlying string at this point. 156 // Instead we add a special span that's then handled in TtmlNode#cleanUpText. 157 builder.setSpan(new DeleteTextSpan(), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); 158 break; 159 case TtmlStyle.RUBY_TYPE_CONTAINER: 160 case TtmlStyle.UNSPECIFIED: 161 default: 162 // Do nothing 163 break; 164 } 165 166 @Nullable Alignment textAlign = style.getTextAlign(); 167 if (textAlign != null) { 168 SpanUtil.addOrReplaceSpan( 169 builder, 170 new AlignmentSpan.Standard(textAlign), 171 start, 172 end, 173 Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); 174 } 175 if (style.getTextCombine()) { 176 SpanUtil.addOrReplaceSpan( 177 builder, 178 new HorizontalTextInVerticalContextSpan(), 179 start, 180 end, 181 Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); 182 } 183 switch (style.getFontSizeUnit()) { 184 case TtmlStyle.FONT_SIZE_UNIT_PIXEL: 185 SpanUtil.addOrReplaceSpan( 186 builder, 187 new AbsoluteSizeSpan((int) style.getFontSize(), true), 188 start, 189 end, 190 Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); 191 break; 192 case TtmlStyle.FONT_SIZE_UNIT_EM: 193 SpanUtil.addOrReplaceSpan( 194 builder, 195 new RelativeSizeSpan(style.getFontSize()), 196 start, 197 end, 198 Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); 199 break; 200 case TtmlStyle.FONT_SIZE_UNIT_PERCENT: 201 SpanUtil.addOrReplaceSpan( 202 builder, 203 new RelativeSizeSpan(style.getFontSize() / 100), 204 start, 205 end, 206 Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); 207 break; 208 case TtmlStyle.UNSPECIFIED: 209 // Do nothing. 210 break; 211 } 212 } 213 214 @Nullable findRubyTextNode(TtmlNode rubyContainerNode)215 private static TtmlNode findRubyTextNode(TtmlNode rubyContainerNode) { 216 Deque<TtmlNode> childNodesStack = new ArrayDeque<>(); 217 childNodesStack.push(rubyContainerNode); 218 while (!childNodesStack.isEmpty()) { 219 TtmlNode childNode = childNodesStack.pop(); 220 if (childNode.style != null && childNode.style.getRubyType() == TtmlStyle.RUBY_TYPE_TEXT) { 221 return childNode; 222 } 223 for (int i = childNode.getChildCount() - 1; i >= 0; i--) { 224 childNodesStack.push(childNode.getChild(i)); 225 } 226 } 227 228 return null; 229 } 230 231 @Nullable findRubyContainerNode(@ullable TtmlNode node)232 private static TtmlNode findRubyContainerNode(@Nullable TtmlNode node) { 233 while (node != null) { 234 @Nullable TtmlStyle style = node.style; 235 if (style != null && style.getRubyType() == TtmlStyle.RUBY_TYPE_CONTAINER) { 236 return node; 237 } 238 node = node.parent; 239 } 240 return null; 241 } 242 243 /** 244 * Called when the end of a paragraph is encountered. Adds a newline if there are one or more 245 * non-space characters since the previous newline. 246 * 247 * @param builder The builder. 248 */ endParagraph(SpannableStringBuilder builder)249 /* package */ static void endParagraph(SpannableStringBuilder builder) { 250 int position = builder.length() - 1; 251 while (position >= 0 && builder.charAt(position) == ' ') { 252 position--; 253 } 254 if (position >= 0 && builder.charAt(position) != '\n') { 255 builder.append('\n'); 256 } 257 } 258 259 /** 260 * Applies the appropriate space policy to the given text element. 261 * 262 * @param in The text element to which the policy should be applied. 263 * @return The result of applying the policy to the text element. 264 */ applyTextElementSpacePolicy(String in)265 /* package */ static String applyTextElementSpacePolicy(String in) { 266 // Removes carriage return followed by line feed. See: http://www.w3.org/TR/xml/#sec-line-ends 267 String out = in.replaceAll("\r\n", "\n"); 268 // Apply suppress-at-line-break="auto" and 269 // white-space-treatment="ignore-if-surrounding-linefeed" 270 out = out.replaceAll(" *\n *", "\n"); 271 // Apply linefeed-treatment="treat-as-space" 272 out = out.replaceAll("\n", " "); 273 // Apply white-space-collapse="true" 274 out = out.replaceAll("[ \t\\x0B\f\r]+", " "); 275 return out; 276 } 277 TtmlRenderUtil()278 private TtmlRenderUtil() {} 279 280 } 281