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.webvtt; 17 18 import android.graphics.Typeface; 19 import android.text.Layout; 20 import android.text.TextUtils; 21 import androidx.annotation.IntDef; 22 import androidx.annotation.Nullable; 23 import com.google.android.exoplayer2.util.Util; 24 import java.lang.annotation.Documented; 25 import java.lang.annotation.Retention; 26 import java.lang.annotation.RetentionPolicy; 27 import java.util.Arrays; 28 import java.util.Collections; 29 import java.util.List; 30 import org.checkerframework.checker.nullness.qual.EnsuresNonNull; 31 32 /** 33 * Style object of a Css style block in a Webvtt file. 34 * 35 * @see <a href="https://w3c.github.io/webvtt/#applying-css-properties">W3C specification - Apply 36 * CSS properties</a> 37 */ 38 public final class WebvttCssStyle { 39 40 public static final int UNSPECIFIED = -1; 41 42 /** 43 * Style flag enum. Possible flag values are {@link #UNSPECIFIED}, {@link #STYLE_NORMAL}, {@link 44 * #STYLE_BOLD}, {@link #STYLE_ITALIC} and {@link #STYLE_BOLD_ITALIC}. 45 */ 46 @Documented 47 @Retention(RetentionPolicy.SOURCE) 48 @IntDef( 49 flag = true, 50 value = {UNSPECIFIED, STYLE_NORMAL, STYLE_BOLD, STYLE_ITALIC, STYLE_BOLD_ITALIC}) 51 public @interface StyleFlags {} 52 53 public static final int STYLE_NORMAL = Typeface.NORMAL; 54 public static final int STYLE_BOLD = Typeface.BOLD; 55 public static final int STYLE_ITALIC = Typeface.ITALIC; 56 public static final int STYLE_BOLD_ITALIC = Typeface.BOLD_ITALIC; 57 58 /** 59 * Font size unit enum. One of {@link #UNSPECIFIED}, {@link #FONT_SIZE_UNIT_PIXEL}, {@link 60 * #FONT_SIZE_UNIT_EM} or {@link #FONT_SIZE_UNIT_PERCENT}. 61 */ 62 @Documented 63 @Retention(RetentionPolicy.SOURCE) 64 @IntDef({UNSPECIFIED, FONT_SIZE_UNIT_PIXEL, FONT_SIZE_UNIT_EM, FONT_SIZE_UNIT_PERCENT}) 65 public @interface FontSizeUnit {} 66 67 public static final int FONT_SIZE_UNIT_PIXEL = 1; 68 public static final int FONT_SIZE_UNIT_EM = 2; 69 public static final int FONT_SIZE_UNIT_PERCENT = 3; 70 71 @Documented 72 @Retention(RetentionPolicy.SOURCE) 73 @IntDef({UNSPECIFIED, OFF, ON}) 74 private @interface OptionalBoolean {} 75 76 private static final int OFF = 0; 77 private static final int ON = 1; 78 79 // Selector properties. 80 private String targetId; 81 private String targetTag; 82 private List<String> targetClasses; 83 private String targetVoice; 84 85 // Style properties. 86 @Nullable private String fontFamily; 87 private int fontColor; 88 private boolean hasFontColor; 89 private int backgroundColor; 90 private boolean hasBackgroundColor; 91 @OptionalBoolean private int linethrough; 92 @OptionalBoolean private int underline; 93 @OptionalBoolean private int bold; 94 @OptionalBoolean private int italic; 95 @FontSizeUnit private int fontSizeUnit; 96 private float fontSize; 97 @Nullable private Layout.Alignment textAlign; 98 private boolean combineUpright; 99 100 // Calling reset() is forbidden because `this` isn't initialized. This can be safely suppressed 101 // because reset() only assigns fields, it doesn't read any. 102 @SuppressWarnings("nullness:method.invocation.invalid") WebvttCssStyle()103 public WebvttCssStyle() { 104 reset(); 105 } 106 107 @EnsuresNonNull({"targetId", "targetTag", "targetClasses", "targetVoice"}) reset()108 public void reset() { 109 targetId = ""; 110 targetTag = ""; 111 targetClasses = Collections.emptyList(); 112 targetVoice = ""; 113 fontFamily = null; 114 hasFontColor = false; 115 hasBackgroundColor = false; 116 linethrough = UNSPECIFIED; 117 underline = UNSPECIFIED; 118 bold = UNSPECIFIED; 119 italic = UNSPECIFIED; 120 fontSizeUnit = UNSPECIFIED; 121 textAlign = null; 122 combineUpright = false; 123 } 124 setTargetId(String targetId)125 public void setTargetId(String targetId) { 126 this.targetId = targetId; 127 } 128 setTargetTagName(String targetTag)129 public void setTargetTagName(String targetTag) { 130 this.targetTag = targetTag; 131 } 132 setTargetClasses(String[] targetClasses)133 public void setTargetClasses(String[] targetClasses) { 134 this.targetClasses = Arrays.asList(targetClasses); 135 } 136 setTargetVoice(String targetVoice)137 public void setTargetVoice(String targetVoice) { 138 this.targetVoice = targetVoice; 139 } 140 141 /** 142 * Returns a value in a score system compliant with the CSS Specificity rules. 143 * 144 * @see <a href="https://www.w3.org/TR/CSS2/cascade.html">CSS Cascading</a> 145 * <p>The score works as follows: 146 * <ul> 147 * <li>Id match adds 0x40000000 to the score. 148 * <li>Each class and voice match adds 4 to the score. 149 * <li>Tag matching adds 2 to the score. 150 * <li>Universal selector matching scores 1. 151 * </ul> 152 * 153 * @param id The id of the cue if present, {@code null} otherwise. 154 * @param tag Name of the tag, {@code null} if it refers to the entire cue. 155 * @param classes An array containing the classes the tag belongs to. Must not be null. 156 * @param voice Annotated voice if present, {@code null} otherwise. 157 * @return The score of the match, zero if there is no match. 158 */ getSpecificityScore( @ullable String id, @Nullable String tag, String[] classes, @Nullable String voice)159 public int getSpecificityScore( 160 @Nullable String id, @Nullable String tag, String[] classes, @Nullable String voice) { 161 if (targetId.isEmpty() && targetTag.isEmpty() && targetClasses.isEmpty() 162 && targetVoice.isEmpty()) { 163 // The selector is universal. It matches with the minimum score if and only if the given 164 // element is a whole cue. 165 return TextUtils.isEmpty(tag) ? 1 : 0; 166 } 167 int score = 0; 168 score = updateScoreForMatch(score, targetId, id, 0x40000000); 169 score = updateScoreForMatch(score, targetTag, tag, 2); 170 score = updateScoreForMatch(score, targetVoice, voice, 4); 171 if (score == -1 || !Arrays.asList(classes).containsAll(targetClasses)) { 172 return 0; 173 } else { 174 score += targetClasses.size() * 4; 175 } 176 return score; 177 } 178 179 /** 180 * Returns the style or {@link #UNSPECIFIED} when no style information is given. 181 * 182 * @return {@link #UNSPECIFIED}, {@link #STYLE_NORMAL}, {@link #STYLE_BOLD}, {@link #STYLE_BOLD} 183 * or {@link #STYLE_BOLD_ITALIC}. 184 */ getStyle()185 @StyleFlags public int getStyle() { 186 if (bold == UNSPECIFIED && italic == UNSPECIFIED) { 187 return UNSPECIFIED; 188 } 189 return (bold == ON ? STYLE_BOLD : STYLE_NORMAL) 190 | (italic == ON ? STYLE_ITALIC : STYLE_NORMAL); 191 } 192 isLinethrough()193 public boolean isLinethrough() { 194 return linethrough == ON; 195 } 196 setLinethrough(boolean linethrough)197 public WebvttCssStyle setLinethrough(boolean linethrough) { 198 this.linethrough = linethrough ? ON : OFF; 199 return this; 200 } 201 isUnderline()202 public boolean isUnderline() { 203 return underline == ON; 204 } 205 setUnderline(boolean underline)206 public WebvttCssStyle setUnderline(boolean underline) { 207 this.underline = underline ? ON : OFF; 208 return this; 209 } setBold(boolean bold)210 public WebvttCssStyle setBold(boolean bold) { 211 this.bold = bold ? ON : OFF; 212 return this; 213 } 214 setItalic(boolean italic)215 public WebvttCssStyle setItalic(boolean italic) { 216 this.italic = italic ? ON : OFF; 217 return this; 218 } 219 220 @Nullable getFontFamily()221 public String getFontFamily() { 222 return fontFamily; 223 } 224 setFontFamily(@ullable String fontFamily)225 public WebvttCssStyle setFontFamily(@Nullable String fontFamily) { 226 this.fontFamily = Util.toLowerInvariant(fontFamily); 227 return this; 228 } 229 getFontColor()230 public int getFontColor() { 231 if (!hasFontColor) { 232 throw new IllegalStateException("Font color not defined"); 233 } 234 return fontColor; 235 } 236 setFontColor(int color)237 public WebvttCssStyle setFontColor(int color) { 238 this.fontColor = color; 239 hasFontColor = true; 240 return this; 241 } 242 hasFontColor()243 public boolean hasFontColor() { 244 return hasFontColor; 245 } 246 getBackgroundColor()247 public int getBackgroundColor() { 248 if (!hasBackgroundColor) { 249 throw new IllegalStateException("Background color not defined."); 250 } 251 return backgroundColor; 252 } 253 setBackgroundColor(int backgroundColor)254 public WebvttCssStyle setBackgroundColor(int backgroundColor) { 255 this.backgroundColor = backgroundColor; 256 hasBackgroundColor = true; 257 return this; 258 } 259 hasBackgroundColor()260 public boolean hasBackgroundColor() { 261 return hasBackgroundColor; 262 } 263 264 @Nullable getTextAlign()265 public Layout.Alignment getTextAlign() { 266 return textAlign; 267 } 268 setTextAlign(@ullable Layout.Alignment textAlign)269 public WebvttCssStyle setTextAlign(@Nullable Layout.Alignment textAlign) { 270 this.textAlign = textAlign; 271 return this; 272 } 273 setFontSize(float fontSize)274 public WebvttCssStyle setFontSize(float fontSize) { 275 this.fontSize = fontSize; 276 return this; 277 } 278 setFontSizeUnit(short unit)279 public WebvttCssStyle setFontSizeUnit(short unit) { 280 this.fontSizeUnit = unit; 281 return this; 282 } 283 getFontSizeUnit()284 @FontSizeUnit public int getFontSizeUnit() { 285 return fontSizeUnit; 286 } 287 getFontSize()288 public float getFontSize() { 289 return fontSize; 290 } 291 setCombineUpright(boolean enabled)292 public void setCombineUpright(boolean enabled) { 293 this.combineUpright = enabled; 294 } 295 getCombineUpright()296 public boolean getCombineUpright() { 297 return combineUpright; 298 } 299 updateScoreForMatch( int currentScore, String target, @Nullable String actual, int score)300 private static int updateScoreForMatch( 301 int currentScore, String target, @Nullable String actual, int score) { 302 if (target.isEmpty() || currentScore == -1) { 303 return currentScore; 304 } 305 return target.equals(actual) ? currentScore + score : -1; 306 } 307 308 } 309