summaryrefslogtreecommitdiffstats
path: root/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/ttml/TtmlNode.java
blob: 16d0f28f6b98d2de87ef09c99910d116792490c7 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
/*
 * 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 org.mozilla.thirdparty.com.google.android.exoplayer2.text.ttml;

import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.text.SpannableStringBuilder;
import android.util.Base64;
import android.util.Pair;
import androidx.annotation.Nullable;
import org.mozilla.thirdparty.com.google.android.exoplayer2.C;
import org.mozilla.thirdparty.com.google.android.exoplayer2.text.Cue;
import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.TreeMap;
import java.util.TreeSet;

/**
 * A package internal representation of TTML node.
 */
/* package */ final class TtmlNode {

  public static final String TAG_TT = "tt";
  public static final String TAG_HEAD = "head";
  public static final String TAG_BODY = "body";
  public static final String TAG_DIV = "div";
  public static final String TAG_P = "p";
  public static final String TAG_SPAN = "span";
  public static final String TAG_BR = "br";
  public static final String TAG_STYLE = "style";
  public static final String TAG_STYLING = "styling";
  public static final String TAG_LAYOUT = "layout";
  public static final String TAG_REGION = "region";
  public static final String TAG_METADATA = "metadata";
  public static final String TAG_IMAGE = "image";
  public static final String TAG_DATA = "data";
  public static final String TAG_INFORMATION = "information";

  public static final String ANONYMOUS_REGION_ID = "";
  public static final String ATTR_ID = "id";
  public static final String ATTR_TTS_ORIGIN = "origin";
  public static final String ATTR_TTS_EXTENT = "extent";
  public static final String ATTR_TTS_DISPLAY_ALIGN = "displayAlign";
  public static final String ATTR_TTS_BACKGROUND_COLOR = "backgroundColor";
  public static final String ATTR_TTS_FONT_STYLE = "fontStyle";
  public static final String ATTR_TTS_FONT_SIZE = "fontSize";
  public static final String ATTR_TTS_FONT_FAMILY = "fontFamily";
  public static final String ATTR_TTS_FONT_WEIGHT = "fontWeight";
  public static final String ATTR_TTS_COLOR = "color";
  public static final String ATTR_TTS_TEXT_DECORATION = "textDecoration";
  public static final String ATTR_TTS_TEXT_ALIGN = "textAlign";

  public static final String LINETHROUGH = "linethrough";
  public static final String NO_LINETHROUGH = "nolinethrough";
  public static final String UNDERLINE = "underline";
  public static final String NO_UNDERLINE = "nounderline";
  public static final String ITALIC = "italic";
  public static final String BOLD = "bold";

  public static final String LEFT = "left";
  public static final String CENTER = "center";
  public static final String RIGHT = "right";
  public static final String START = "start";
  public static final String END = "end";

  @Nullable public final String tag;
  @Nullable public final String text;
  public final boolean isTextNode;
  public final long startTimeUs;
  public final long endTimeUs;
  @Nullable public final TtmlStyle style;
  @Nullable private final String[] styleIds;
  public final String regionId;
  @Nullable public final String imageId;

  private final HashMap<String, Integer> nodeStartsByRegion;
  private final HashMap<String, Integer> nodeEndsByRegion;

  private List<TtmlNode> children;

  public static TtmlNode buildTextNode(String text) {
    return new TtmlNode(
        /* tag= */ null,
        TtmlRenderUtil.applyTextElementSpacePolicy(text),
        /* startTimeUs= */ C.TIME_UNSET,
        /* endTimeUs= */ C.TIME_UNSET,
        /* style= */ null,
        /* styleIds= */ null,
        ANONYMOUS_REGION_ID,
        /* imageId= */ null);
  }

  public static TtmlNode buildNode(
      @Nullable String tag,
      long startTimeUs,
      long endTimeUs,
      @Nullable TtmlStyle style,
      @Nullable String[] styleIds,
      String regionId,
      @Nullable String imageId) {
    return new TtmlNode(
        tag, /* text= */ null, startTimeUs, endTimeUs, style, styleIds, regionId, imageId);
  }

  private TtmlNode(
      @Nullable String tag,
      @Nullable String text,
      long startTimeUs,
      long endTimeUs,
      @Nullable TtmlStyle style,
      @Nullable String[] styleIds,
      String regionId,
      @Nullable String imageId) {
    this.tag = tag;
    this.text = text;
    this.imageId = imageId;
    this.style = style;
    this.styleIds = styleIds;
    this.isTextNode = text != null;
    this.startTimeUs = startTimeUs;
    this.endTimeUs = endTimeUs;
    this.regionId = Assertions.checkNotNull(regionId);
    nodeStartsByRegion = new HashMap<>();
    nodeEndsByRegion = new HashMap<>();
  }

  public boolean isActive(long timeUs) {
    return (startTimeUs == C.TIME_UNSET && endTimeUs == C.TIME_UNSET)
        || (startTimeUs <= timeUs && endTimeUs == C.TIME_UNSET)
        || (startTimeUs == C.TIME_UNSET && timeUs < endTimeUs)
        || (startTimeUs <= timeUs && timeUs < endTimeUs);
  }

  public void addChild(TtmlNode child) {
    if (children == null) {
      children = new ArrayList<>();
    }
    children.add(child);
  }

  public TtmlNode getChild(int index) {
    if (children == null) {
      throw new IndexOutOfBoundsException();
    }
    return children.get(index);
  }

  public int getChildCount() {
    return children == null ? 0 : children.size();
  }

  public long[] getEventTimesUs() {
    TreeSet<Long> eventTimeSet = new TreeSet<>();
    getEventTimes(eventTimeSet, false);
    long[] eventTimes = new long[eventTimeSet.size()];
    int i = 0;
    for (long eventTimeUs : eventTimeSet) {
      eventTimes[i++] = eventTimeUs;
    }
    return eventTimes;
  }

  private void getEventTimes(TreeSet<Long> out, boolean descendsPNode) {
    boolean isPNode = TAG_P.equals(tag);
    boolean isDivNode = TAG_DIV.equals(tag);
    if (descendsPNode || isPNode || (isDivNode && imageId != null)) {
      if (startTimeUs != C.TIME_UNSET) {
        out.add(startTimeUs);
      }
      if (endTimeUs != C.TIME_UNSET) {
        out.add(endTimeUs);
      }
    }
    if (children == null) {
      return;
    }
    for (int i = 0; i < children.size(); i++) {
      children.get(i).getEventTimes(out, descendsPNode || isPNode);
    }
  }

  public String[] getStyleIds() {
    return styleIds;
  }

  public List<Cue> getCues(
      long timeUs,
      Map<String, TtmlStyle> globalStyles,
      Map<String, TtmlRegion> regionMap,
      Map<String, String> imageMap) {

    List<Pair<String, String>> regionImageOutputs = new ArrayList<>();
    traverseForImage(timeUs, regionId, regionImageOutputs);

    TreeMap<String, SpannableStringBuilder> regionTextOutputs = new TreeMap<>();
    traverseForText(timeUs, false, regionId, regionTextOutputs);
    traverseForStyle(timeUs, globalStyles, regionTextOutputs);

    List<Cue> cues = new ArrayList<>();

    // Create image based cues.
    for (Pair<String, String> regionImagePair : regionImageOutputs) {
      String encodedBitmapData = imageMap.get(regionImagePair.second);
      if (encodedBitmapData == null) {
        // Image reference points to an invalid image. Do nothing.
        continue;
      }

      byte[] bitmapData = Base64.decode(encodedBitmapData, Base64.DEFAULT);
      Bitmap bitmap = BitmapFactory.decodeByteArray(bitmapData, /* offset= */ 0, bitmapData.length);
      TtmlRegion region = regionMap.get(regionImagePair.first);

      cues.add(
          new Cue(
              bitmap,
              region.position,
              Cue.ANCHOR_TYPE_START,
              region.line,
              region.lineAnchor,
              region.width,
              region.height));
    }

    // Create text based cues.
    for (Entry<String, SpannableStringBuilder> entry : regionTextOutputs.entrySet()) {
      TtmlRegion region = regionMap.get(entry.getKey());
      cues.add(
          new Cue(
              cleanUpText(entry.getValue()),
              /* textAlignment= */ null,
              region.line,
              region.lineType,
              region.lineAnchor,
              region.position,
              /* positionAnchor= */ Cue.TYPE_UNSET,
              region.width,
              region.textSizeType,
              region.textSize));
    }

    return cues;
  }

  private void traverseForImage(
      long timeUs, String inheritedRegion, List<Pair<String, String>> regionImageList) {
    String resolvedRegionId = ANONYMOUS_REGION_ID.equals(regionId) ? inheritedRegion : regionId;
    if (isActive(timeUs) && TAG_DIV.equals(tag) && imageId != null) {
      regionImageList.add(new Pair<>(resolvedRegionId, imageId));
      return;
    }
    for (int i = 0; i < getChildCount(); ++i) {
      getChild(i).traverseForImage(timeUs, resolvedRegionId, regionImageList);
    }
  }

  private void traverseForText(
      long timeUs,
      boolean descendsPNode,
      String inheritedRegion,
      Map<String, SpannableStringBuilder> regionOutputs) {
    nodeStartsByRegion.clear();
    nodeEndsByRegion.clear();
    if (TAG_METADATA.equals(tag)) {
      // Ignore metadata tag.
      return;
    }

    String resolvedRegionId = ANONYMOUS_REGION_ID.equals(regionId) ? inheritedRegion : regionId;

    if (isTextNode && descendsPNode) {
      getRegionOutput(resolvedRegionId, regionOutputs).append(text);
    } else if (TAG_BR.equals(tag) && descendsPNode) {
      getRegionOutput(resolvedRegionId, regionOutputs).append('\n');
    } else if (isActive(timeUs)) {
      // This is a container node, which can contain zero or more children.
      for (Entry<String, SpannableStringBuilder> entry : regionOutputs.entrySet()) {
        nodeStartsByRegion.put(entry.getKey(), entry.getValue().length());
      }

      boolean isPNode = TAG_P.equals(tag);
      for (int i = 0; i < getChildCount(); i++) {
        getChild(i).traverseForText(timeUs, descendsPNode || isPNode, resolvedRegionId,
            regionOutputs);
      }
      if (isPNode) {
        TtmlRenderUtil.endParagraph(getRegionOutput(resolvedRegionId, regionOutputs));
      }

      for (Entry<String, SpannableStringBuilder> entry : regionOutputs.entrySet()) {
        nodeEndsByRegion.put(entry.getKey(), entry.getValue().length());
      }
    }
  }

  private static SpannableStringBuilder getRegionOutput(
      String resolvedRegionId, Map<String, SpannableStringBuilder> regionOutputs) {
    if (!regionOutputs.containsKey(resolvedRegionId)) {
      regionOutputs.put(resolvedRegionId, new SpannableStringBuilder());
    }
    return regionOutputs.get(resolvedRegionId);
  }

  private void traverseForStyle(
      long timeUs,
      Map<String, TtmlStyle> globalStyles,
      Map<String, SpannableStringBuilder> regionOutputs) {
    if (!isActive(timeUs)) {
      return;
    }
    for (Entry<String, Integer> entry : nodeEndsByRegion.entrySet()) {
      String regionId = entry.getKey();
      int start = nodeStartsByRegion.containsKey(regionId) ? nodeStartsByRegion.get(regionId) : 0;
      int end = entry.getValue();
      if (start != end) {
        SpannableStringBuilder regionOutput = regionOutputs.get(regionId);
        applyStyleToOutput(globalStyles, regionOutput, start, end);
      }
    }
    for (int i = 0; i < getChildCount(); ++i) {
      getChild(i).traverseForStyle(timeUs, globalStyles, regionOutputs);
    }
  }

  private void applyStyleToOutput(
      Map<String, TtmlStyle> globalStyles,
      SpannableStringBuilder regionOutput,
      int start,
      int end) {
    TtmlStyle resolvedStyle = TtmlRenderUtil.resolveStyle(style, styleIds, globalStyles);
    if (resolvedStyle != null) {
      TtmlRenderUtil.applyStylesToSpan(regionOutput, start, end, resolvedStyle);
    }
  }

  private SpannableStringBuilder cleanUpText(SpannableStringBuilder builder) {
    // Having joined the text elements, we need to do some final cleanup on the result.
    // 1. Collapse multiple consecutive spaces into a single space.
    int builderLength = builder.length();
    for (int i = 0; i < builderLength; i++) {
      if (builder.charAt(i) == ' ') {
        int j = i + 1;
        while (j < builder.length() && builder.charAt(j) == ' ') {
          j++;
        }
        int spacesToDelete = j - (i + 1);
        if (spacesToDelete > 0) {
          builder.delete(i, i + spacesToDelete);
          builderLength -= spacesToDelete;
        }
      }
    }
    // 2. Remove any spaces from the start of each line.
    if (builderLength > 0 && builder.charAt(0) == ' ') {
      builder.delete(0, 1);
      builderLength--;
    }
    for (int i = 0; i < builderLength - 1; i++) {
      if (builder.charAt(i) == '\n' && builder.charAt(i + 1) == ' ') {
        builder.delete(i + 1, i + 2);
        builderLength--;
      }
    }
    // 3. Remove any spaces from the end of each line.
    if (builderLength > 0 && builder.charAt(builderLength - 1) == ' ') {
      builder.delete(builderLength - 1, builderLength);
      builderLength--;
    }
    for (int i = 0; i < builderLength - 1; i++) {
      if (builder.charAt(i) == ' ' && builder.charAt(i + 1) == '\n') {
        builder.delete(i, i + 1);
        builderLength--;
      }
    }
    // 4. Trim a trailing newline, if there is one.
    if (builderLength > 0 && builder.charAt(builderLength - 1) == '\n') {
      builder.delete(builderLength - 1, builderLength);
      /*builderLength--;*/
    }
    return builder;
  }

}