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