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