/* * Copyright (C) 2014 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 com.google.android.exoplayer.text.ttml; import android.text.Spannable; import android.text.SpannableStringBuilder; import android.text.Spanned; import android.text.style.AlignmentSpan; import android.text.style.BackgroundColorSpan; import android.text.style.ForegroundColorSpan; import android.text.style.StrikethroughSpan; import android.text.style.StyleSpan; import android.text.style.TypefaceSpan; import android.text.style.UnderlineSpan; import java.util.ArrayList; import java.util.Iterator; import java.util.List; import java.util.TreeSet; /** * A package internal representation of TTML node. */ /* package */ final class TtmlNode { public static final long UNDEFINED_TIME = -1; 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_SMPTE_IMAGE = "smpte:image"; public static final String TAG_SMPTE_DATA = "smpte:data"; public static final String TAG_SMPTE_INFORMATION = "smpte:information"; public static final String ATTR_ID = "id"; 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"; public final String tag; public final String text; public final boolean isTextNode; public final long startTimeUs; public final long endTimeUs; public final TtmlStyle style; private List<TtmlNode> children; public static TtmlNode buildTextNode(String text, TtmlStyle style) { return new TtmlNode(null, applyTextElementSpacePolicy(text), UNDEFINED_TIME, UNDEFINED_TIME, style); } public static TtmlNode buildNode(String tag, long startTimeUs, long endTimeUs, TtmlStyle style) { return new TtmlNode(tag, null, startTimeUs, endTimeUs, style); } private TtmlNode(String tag, String text, long startTimeUs, long endTimeUs, TtmlStyle style) { this.tag = tag; this.text = text; this.style = style; this.isTextNode = text != null; this.startTimeUs = startTimeUs; this.endTimeUs = endTimeUs; } public boolean isActive(long timeUs) { return (startTimeUs == UNDEFINED_TIME && endTimeUs == UNDEFINED_TIME) || (startTimeUs <= timeUs && endTimeUs == UNDEFINED_TIME) || (startTimeUs == UNDEFINED_TIME && 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()]; Iterator<Long> eventTimeIterator = eventTimeSet.iterator(); int i = 0; while (eventTimeIterator.hasNext()) { long eventTimeUs = eventTimeIterator.next(); eventTimes[i++] = eventTimeUs; } return eventTimes; } private void getEventTimes(TreeSet<Long> out, boolean descendsPNode) { boolean isPNode = TAG_P.equals(tag); if (descendsPNode || isPNode) { if (startTimeUs != UNDEFINED_TIME) { out.add(startTimeUs); } if (endTimeUs != UNDEFINED_TIME) { out.add(endTimeUs); } } if (children == null) { return; } for (int i = 0; i < children.size(); i++) { children.get(i).getEventTimes(out, descendsPNode || isPNode); } } public CharSequence getText(long timeUs) { SpannableStringBuilder builder = getText(timeUs, new SpannableStringBuilder(), false); // 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; } private SpannableStringBuilder getText(long timeUs, SpannableStringBuilder builder, boolean descendsPNode) { if (isTextNode && descendsPNode) { int start = builder.length(); builder.append(text); applyStylesToSpan(builder, start, builder.length(), style); } else if (TAG_BR.equals(tag) && descendsPNode) { builder.append('\n'); } else if (TAG_METADATA.equals(tag)) { // Do nothing. } else if (isActive(timeUs)) { boolean isPNode = TAG_P.equals(tag); for (int i = 0; i < getChildCount(); ++i) { getChild(i).getText(timeUs, builder, descendsPNode || isPNode); } if (isPNode) { endParagraph(builder); } } return builder; } private static void applyStylesToSpan(SpannableStringBuilder builder, int start, int end, TtmlStyle style) { if (style.getStyle() != TtmlStyle.UNSPECIFIED) { builder.setSpan(new StyleSpan(style.getStyle()), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); } if (style.isLinethrough()) { builder.setSpan(new StrikethroughSpan(), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); } if (style.isUnderline()) { builder.setSpan(new UnderlineSpan(), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); } if (style.hasColorSpecified()) { builder.setSpan(new ForegroundColorSpan(style.getColor()), start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); } if (style.hasBackgroundColorSpecified()) { builder.setSpan(new BackgroundColorSpan(style.getBackgroundColor()), start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); } if (style.getFontFamily() != null) { builder.setSpan(new TypefaceSpan(style.getFontFamily()), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); } if (style.getTextAlign() != null) { builder.setSpan(new AlignmentSpan.Standard(style.getTextAlign()), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); } } /** * Invoked when the end of a paragraph is encountered. Adds a newline if there are one or more * non-space characters since the previous newline. * * @param builder The builder. */ private static void endParagraph(SpannableStringBuilder builder) { int position = builder.length() - 1; while (position >= 0 && builder.charAt(position) == ' ') { position--; } if (position >= 0 && builder.charAt(position) != '\n') { builder.append('\n'); } } /** * Applies the appropriate space policy to the given text element. * * @param in The text element to which the policy should be applied. * @return The result of applying the policy to the text element. */ private static String applyTextElementSpacePolicy(String in) { // Removes carriage return followed by line feed. See: http://www.w3.org/TR/xml/#sec-line-ends String out = in.replaceAll("\r\n", "\n"); // Apply suppress-at-line-break="auto" and // white-space-treatment="ignore-if-surrounding-linefeed" out = out.replaceAll(" *\n *", "\n"); // Apply linefeed-treatment="treat-as-space" out = out.replaceAll("\n", " "); // Apply white-space-collapse="true" out = out.replaceAll("[ \t\\x0B\f\r]+", " "); return out; } }