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