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.SpannableStringBuilder;
19 import com.google.android.exoplayer2.C;
20 import com.google.android.exoplayer2.text.Cue;
21 import com.google.android.exoplayer2.util.Assertions;
22 import java.util.ArrayList;
23 import java.util.HashMap;
24 import java.util.List;
25 import java.util.Map;
26 import java.util.Map.Entry;
27 import java.util.TreeMap;
28 import java.util.TreeSet;
29 
30 /**
31  * A package internal representation of TTML node.
32  */
33 /* package */ final class TtmlNode {
34 
35   public static final String TAG_TT = "tt";
36   public static final String TAG_HEAD = "head";
37   public static final String TAG_BODY = "body";
38   public static final String TAG_DIV = "div";
39   public static final String TAG_P = "p";
40   public static final String TAG_SPAN = "span";
41   public static final String TAG_BR = "br";
42   public static final String TAG_STYLE = "style";
43   public static final String TAG_STYLING = "styling";
44   public static final String TAG_LAYOUT = "layout";
45   public static final String TAG_REGION = "region";
46   public static final String TAG_METADATA = "metadata";
47   public static final String TAG_SMPTE_IMAGE = "smpte:image";
48   public static final String TAG_SMPTE_DATA = "smpte:data";
49   public static final String TAG_SMPTE_INFORMATION = "smpte:information";
50 
51   public static final String ANONYMOUS_REGION_ID = "";
52   public static final String ATTR_ID = "id";
53   public static final String ATTR_TTS_BACKGROUND_COLOR = "backgroundColor";
54   public static final String ATTR_TTS_EXTENT = "extent";
55   public static final String ATTR_TTS_FONT_STYLE = "fontStyle";
56   public static final String ATTR_TTS_FONT_SIZE = "fontSize";
57   public static final String ATTR_TTS_FONT_FAMILY = "fontFamily";
58   public static final String ATTR_TTS_FONT_WEIGHT = "fontWeight";
59   public static final String ATTR_TTS_COLOR = "color";
60   public static final String ATTR_TTS_ORIGIN = "origin";
61   public static final String ATTR_TTS_TEXT_DECORATION = "textDecoration";
62   public static final String ATTR_TTS_TEXT_ALIGN = "textAlign";
63 
64   public static final String LINETHROUGH = "linethrough";
65   public static final String NO_LINETHROUGH = "nolinethrough";
66   public static final String UNDERLINE = "underline";
67   public static final String NO_UNDERLINE = "nounderline";
68   public static final String ITALIC = "italic";
69   public static final String BOLD = "bold";
70 
71   public static final String LEFT = "left";
72   public static final String CENTER = "center";
73   public static final String RIGHT = "right";
74   public static final String START = "start";
75   public static final String END = "end";
76 
77   public final String tag;
78   public final String text;
79   public final boolean isTextNode;
80   public final long startTimeUs;
81   public final long endTimeUs;
82   public final TtmlStyle style;
83   public final String regionId;
84 
85   private final String[] styleIds;
86   private final HashMap<String, Integer> nodeStartsByRegion;
87   private final HashMap<String, Integer> nodeEndsByRegion;
88 
89   private List<TtmlNode> children;
90 
buildTextNode(String text)91   public static TtmlNode buildTextNode(String text) {
92     return new TtmlNode(null, TtmlRenderUtil.applyTextElementSpacePolicy(text), C.TIME_UNSET,
93         C.TIME_UNSET, null, null, ANONYMOUS_REGION_ID);
94   }
95 
buildNode(String tag, long startTimeUs, long endTimeUs, TtmlStyle style, String[] styleIds, String regionId)96   public static TtmlNode buildNode(String tag, long startTimeUs, long endTimeUs,
97       TtmlStyle style, String[] styleIds, String regionId) {
98     return new TtmlNode(tag, null, startTimeUs, endTimeUs, style, styleIds, regionId);
99   }
100 
TtmlNode(String tag, String text, long startTimeUs, long endTimeUs, TtmlStyle style, String[] styleIds, String regionId)101   private TtmlNode(String tag, String text, long startTimeUs, long endTimeUs,
102       TtmlStyle style, String[] styleIds, String regionId) {
103     this.tag = tag;
104     this.text = text;
105     this.style = style;
106     this.styleIds = styleIds;
107     this.isTextNode = text != null;
108     this.startTimeUs = startTimeUs;
109     this.endTimeUs = endTimeUs;
110     this.regionId = Assertions.checkNotNull(regionId);
111     nodeStartsByRegion = new HashMap<>();
112     nodeEndsByRegion = new HashMap<>();
113   }
114 
isActive(long timeUs)115   public boolean isActive(long timeUs) {
116     return (startTimeUs == C.TIME_UNSET && endTimeUs == C.TIME_UNSET)
117         || (startTimeUs <= timeUs && endTimeUs == C.TIME_UNSET)
118         || (startTimeUs == C.TIME_UNSET && timeUs < endTimeUs)
119         || (startTimeUs <= timeUs && timeUs < endTimeUs);
120   }
121 
addChild(TtmlNode child)122   public void addChild(TtmlNode child) {
123     if (children == null) {
124       children = new ArrayList<>();
125     }
126     children.add(child);
127   }
128 
getChild(int index)129   public TtmlNode getChild(int index) {
130     if (children == null) {
131       throw new IndexOutOfBoundsException();
132     }
133     return children.get(index);
134   }
135 
getChildCount()136   public int getChildCount() {
137     return children == null ? 0 : children.size();
138   }
139 
getEventTimesUs()140   public long[] getEventTimesUs() {
141     TreeSet<Long> eventTimeSet = new TreeSet<>();
142     getEventTimes(eventTimeSet, false);
143     long[] eventTimes = new long[eventTimeSet.size()];
144     int i = 0;
145     for (long eventTimeUs : eventTimeSet) {
146       eventTimes[i++] = eventTimeUs;
147     }
148     return eventTimes;
149   }
150 
getEventTimes(TreeSet<Long> out, boolean descendsPNode)151   private void getEventTimes(TreeSet<Long> out, boolean descendsPNode) {
152     boolean isPNode = TAG_P.equals(tag);
153     if (descendsPNode || isPNode) {
154       if (startTimeUs != C.TIME_UNSET) {
155         out.add(startTimeUs);
156       }
157       if (endTimeUs != C.TIME_UNSET) {
158         out.add(endTimeUs);
159       }
160     }
161     if (children == null) {
162       return;
163     }
164     for (int i = 0; i < children.size(); i++) {
165       children.get(i).getEventTimes(out, descendsPNode || isPNode);
166     }
167   }
168 
getStyleIds()169   public String[] getStyleIds() {
170     return styleIds;
171   }
172 
getCues(long timeUs, Map<String, TtmlStyle> globalStyles, Map<String, TtmlRegion> regionMap)173   public List<Cue> getCues(long timeUs, Map<String, TtmlStyle> globalStyles,
174       Map<String, TtmlRegion> regionMap) {
175     TreeMap<String, SpannableStringBuilder> regionOutputs = new TreeMap<>();
176     traverseForText(timeUs, false, regionId, regionOutputs);
177     traverseForStyle(globalStyles, regionOutputs);
178     List<Cue> cues = new ArrayList<>();
179     for (Entry<String, SpannableStringBuilder> entry : regionOutputs.entrySet()) {
180       TtmlRegion region = regionMap.get(entry.getKey());
181       cues.add(new Cue(cleanUpText(entry.getValue()), null, region.line, region.lineType,
182           Cue.TYPE_UNSET, region.position, Cue.TYPE_UNSET, region.width));
183     }
184     return cues;
185   }
186 
traverseForText(long timeUs, boolean descendsPNode, String inheritedRegion, Map<String, SpannableStringBuilder> regionOutputs)187   private void traverseForText(long timeUs,  boolean descendsPNode,
188       String inheritedRegion, Map<String, SpannableStringBuilder> regionOutputs) {
189     nodeStartsByRegion.clear();
190     nodeEndsByRegion.clear();
191     String resolvedRegionId = regionId;
192     if (ANONYMOUS_REGION_ID.equals(resolvedRegionId)) {
193       resolvedRegionId = inheritedRegion;
194     }
195     if (isTextNode && descendsPNode) {
196       getRegionOutput(resolvedRegionId, regionOutputs).append(text);
197     } else if (TAG_BR.equals(tag) && descendsPNode) {
198       getRegionOutput(resolvedRegionId, regionOutputs).append('\n');
199     } else if (TAG_METADATA.equals(tag)) {
200       // Do nothing.
201     } else if (isActive(timeUs)) {
202       boolean isPNode = TAG_P.equals(tag);
203       for (Entry<String, SpannableStringBuilder> entry : regionOutputs.entrySet()) {
204         nodeStartsByRegion.put(entry.getKey(), entry.getValue().length());
205       }
206       for (int i = 0; i < getChildCount(); i++) {
207         getChild(i).traverseForText(timeUs, descendsPNode || isPNode, resolvedRegionId,
208             regionOutputs);
209       }
210       if (isPNode) {
211         TtmlRenderUtil.endParagraph(getRegionOutput(resolvedRegionId, regionOutputs));
212       }
213       for (Entry<String, SpannableStringBuilder> entry : regionOutputs.entrySet()) {
214         nodeEndsByRegion.put(entry.getKey(), entry.getValue().length());
215       }
216     }
217   }
218 
getRegionOutput(String resolvedRegionId, Map<String, SpannableStringBuilder> regionOutputs)219   private static SpannableStringBuilder getRegionOutput(String resolvedRegionId,
220       Map<String, SpannableStringBuilder> regionOutputs) {
221     if (!regionOutputs.containsKey(resolvedRegionId)) {
222       regionOutputs.put(resolvedRegionId, new SpannableStringBuilder());
223     }
224     return regionOutputs.get(resolvedRegionId);
225   }
226 
traverseForStyle(Map<String, TtmlStyle> globalStyles, Map<String, SpannableStringBuilder> regionOutputs)227   private void traverseForStyle(Map<String, TtmlStyle> globalStyles,
228       Map<String, SpannableStringBuilder> regionOutputs) {
229     for (Entry<String, Integer> entry : nodeEndsByRegion.entrySet()) {
230       String regionId = entry.getKey();
231       int start = nodeStartsByRegion.containsKey(regionId) ? nodeStartsByRegion.get(regionId) : 0;
232       applyStyleToOutput(globalStyles, regionOutputs.get(regionId), start, entry.getValue());
233       for (int i = 0; i < getChildCount(); ++i) {
234         getChild(i).traverseForStyle(globalStyles, regionOutputs);
235       }
236     }
237   }
238 
applyStyleToOutput(Map<String, TtmlStyle> globalStyles, SpannableStringBuilder regionOutput, int start, int end)239   private void applyStyleToOutput(Map<String, TtmlStyle> globalStyles,
240       SpannableStringBuilder regionOutput, int start, int end) {
241     if (start != end) {
242       TtmlStyle resolvedStyle = TtmlRenderUtil.resolveStyle(style, styleIds, globalStyles);
243       if (resolvedStyle != null) {
244         TtmlRenderUtil.applyStylesToSpan(regionOutput, start, end, resolvedStyle);
245       }
246     }
247   }
248 
cleanUpText(SpannableStringBuilder builder)249   private SpannableStringBuilder cleanUpText(SpannableStringBuilder builder) {
250     // Having joined the text elements, we need to do some final cleanup on the result.
251     // 1. Collapse multiple consecutive spaces into a single space.
252     int builderLength = builder.length();
253     for (int i = 0; i < builderLength; i++) {
254       if (builder.charAt(i) == ' ') {
255         int j = i + 1;
256         while (j < builder.length() && builder.charAt(j) == ' ') {
257           j++;
258         }
259         int spacesToDelete = j - (i + 1);
260         if (spacesToDelete > 0) {
261           builder.delete(i, i + spacesToDelete);
262           builderLength -= spacesToDelete;
263         }
264       }
265     }
266     // 2. Remove any spaces from the start of each line.
267     if (builderLength > 0 && builder.charAt(0) == ' ') {
268       builder.delete(0, 1);
269       builderLength--;
270     }
271     for (int i = 0; i < builderLength - 1; i++) {
272       if (builder.charAt(i) == '\n' && builder.charAt(i + 1) == ' ') {
273         builder.delete(i + 1, i + 2);
274         builderLength--;
275       }
276     }
277     // 3. Remove any spaces from the end of each line.
278     if (builderLength > 0 && builder.charAt(builderLength - 1) == ' ') {
279       builder.delete(builderLength - 1, builderLength);
280       builderLength--;
281     }
282     for (int i = 0; i < builderLength - 1; i++) {
283       if (builder.charAt(i) == ' ' && builder.charAt(i + 1) == '\n') {
284         builder.delete(i, i + 1);
285         builderLength--;
286       }
287     }
288     // 4. Trim a trailing newline, if there is one.
289     if (builderLength > 0 && builder.charAt(builderLength - 1) == '\n') {
290       builder.delete(builderLength - 1, builderLength);
291       /*builderLength--;*/
292     }
293     return builder;
294   }
295 
296 }
297