blob: 587f7d5ab5420e516fcf27eae3235bd8ff1b76a4 [file] [log] [blame]
/*
* 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 com.google.android.exoplayer2.text.webvtt;
import android.graphics.Typeface;
import android.text.TextUtils;
import androidx.annotation.ColorInt;
import androidx.annotation.IntDef;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.text.span.TextAnnotation;
import com.google.common.base.Ascii;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
/**
* Style object of a Css style block in a Webvtt file.
*
* @see <a href="https://w3c.github.io/webvtt/#applying-css-properties">W3C specification - Apply
* CSS properties</a>
*/
public final class WebvttCssStyle {
public static final int UNSPECIFIED = -1;
/**
* Style flag enum. Possible flag values are {@link #UNSPECIFIED}, {@link #STYLE_NORMAL}, {@link
* #STYLE_BOLD}, {@link #STYLE_ITALIC} and {@link #STYLE_BOLD_ITALIC}.
*/
@Documented
@Retention(RetentionPolicy.SOURCE)
@IntDef(
flag = true,
value = {UNSPECIFIED, STYLE_NORMAL, STYLE_BOLD, STYLE_ITALIC, STYLE_BOLD_ITALIC})
public @interface StyleFlags {}
public static final int STYLE_NORMAL = Typeface.NORMAL;
public static final int STYLE_BOLD = Typeface.BOLD;
public static final int STYLE_ITALIC = Typeface.ITALIC;
public static final int STYLE_BOLD_ITALIC = Typeface.BOLD_ITALIC;
/**
* Font size unit enum. One of {@link #UNSPECIFIED}, {@link #FONT_SIZE_UNIT_PIXEL}, {@link
* #FONT_SIZE_UNIT_EM} or {@link #FONT_SIZE_UNIT_PERCENT}.
*/
@Documented
@Retention(RetentionPolicy.SOURCE)
@IntDef({UNSPECIFIED, FONT_SIZE_UNIT_PIXEL, FONT_SIZE_UNIT_EM, FONT_SIZE_UNIT_PERCENT})
public @interface FontSizeUnit {}
public static final int FONT_SIZE_UNIT_PIXEL = 1;
public static final int FONT_SIZE_UNIT_EM = 2;
public static final int FONT_SIZE_UNIT_PERCENT = 3;
@Documented
@Retention(RetentionPolicy.SOURCE)
@IntDef({UNSPECIFIED, OFF, ON})
private @interface OptionalBoolean {}
private static final int OFF = 0;
private static final int ON = 1;
// Selector properties.
private String targetId;
private String targetTag;
private Set<String> targetClasses;
private String targetVoice;
// Style properties.
@Nullable private String fontFamily;
@ColorInt private int fontColor;
private boolean hasFontColor;
private int backgroundColor;
private boolean hasBackgroundColor;
@OptionalBoolean private int linethrough;
@OptionalBoolean private int underline;
@OptionalBoolean private int bold;
@OptionalBoolean private int italic;
@FontSizeUnit private int fontSizeUnit;
private float fontSize;
@TextAnnotation.Position private int rubyPosition;
private boolean combineUpright;
public WebvttCssStyle() {
targetId = "";
targetTag = "";
targetClasses = Collections.emptySet();
targetVoice = "";
fontFamily = null;
hasFontColor = false;
hasBackgroundColor = false;
linethrough = UNSPECIFIED;
underline = UNSPECIFIED;
bold = UNSPECIFIED;
italic = UNSPECIFIED;
fontSizeUnit = UNSPECIFIED;
rubyPosition = TextAnnotation.POSITION_UNKNOWN;
combineUpright = false;
}
public void setTargetId(String targetId) {
this.targetId = targetId;
}
public void setTargetTagName(String targetTag) {
this.targetTag = targetTag;
}
public void setTargetClasses(String[] targetClasses) {
this.targetClasses = new HashSet<>(Arrays.asList(targetClasses));
}
public void setTargetVoice(String targetVoice) {
this.targetVoice = targetVoice;
}
/**
* Returns a value in a score system compliant with the CSS Specificity rules.
*
* <p>The score works as follows:
*
* <ul>
* <li>Id match adds 0x40000000 to the score.
* <li>Each class and voice match adds 4 to the score.
* <li>Tag matching adds 2 to the score.
* <li>Universal selector matching scores 1.
* </ul>
*
* @param id The id of the cue if present, {@code null} otherwise.
* @param tag Name of the tag, {@code null} if it refers to the entire cue.
* @param classes An array containing the classes the tag belongs to. Must not be null.
* @param voice Annotated voice if present, {@code null} otherwise.
* @return The score of the match, zero if there is no match.
* @see <a href="https://www.w3.org/TR/CSS2/cascade.html">CSS Cascading</a>
*/
public int getSpecificityScore(
@Nullable String id, @Nullable String tag, Set<String> classes, @Nullable String voice) {
if (targetId.isEmpty()
&& targetTag.isEmpty()
&& targetClasses.isEmpty()
&& targetVoice.isEmpty()) {
// The selector is universal. It matches with the minimum score if and only if the given
// element is a whole cue.
return TextUtils.isEmpty(tag) ? 1 : 0;
}
int score = 0;
score = updateScoreForMatch(score, targetId, id, 0x40000000);
score = updateScoreForMatch(score, targetTag, tag, 2);
score = updateScoreForMatch(score, targetVoice, voice, 4);
if (score == -1 || !classes.containsAll(targetClasses)) {
return 0;
} else {
score += targetClasses.size() * 4;
}
return score;
}
/**
* Returns the style or {@link #UNSPECIFIED} when no style information is given.
*
* @return {@link #UNSPECIFIED}, {@link #STYLE_NORMAL}, {@link #STYLE_BOLD}, {@link #STYLE_BOLD}
* or {@link #STYLE_BOLD_ITALIC}.
*/
@StyleFlags
public int getStyle() {
if (bold == UNSPECIFIED && italic == UNSPECIFIED) {
return UNSPECIFIED;
}
return (bold == ON ? STYLE_BOLD : STYLE_NORMAL) | (italic == ON ? STYLE_ITALIC : STYLE_NORMAL);
}
public boolean isLinethrough() {
return linethrough == ON;
}
public WebvttCssStyle setLinethrough(boolean linethrough) {
this.linethrough = linethrough ? ON : OFF;
return this;
}
public boolean isUnderline() {
return underline == ON;
}
public WebvttCssStyle setUnderline(boolean underline) {
this.underline = underline ? ON : OFF;
return this;
}
public WebvttCssStyle setBold(boolean bold) {
this.bold = bold ? ON : OFF;
return this;
}
public WebvttCssStyle setItalic(boolean italic) {
this.italic = italic ? ON : OFF;
return this;
}
@Nullable
public String getFontFamily() {
return fontFamily;
}
public WebvttCssStyle setFontFamily(@Nullable String fontFamily) {
this.fontFamily = fontFamily == null ? null : Ascii.toLowerCase(fontFamily);
return this;
}
public int getFontColor() {
if (!hasFontColor) {
throw new IllegalStateException("Font color not defined");
}
return fontColor;
}
public WebvttCssStyle setFontColor(int color) {
this.fontColor = color;
hasFontColor = true;
return this;
}
public boolean hasFontColor() {
return hasFontColor;
}
public int getBackgroundColor() {
if (!hasBackgroundColor) {
throw new IllegalStateException("Background color not defined.");
}
return backgroundColor;
}
public WebvttCssStyle setBackgroundColor(int backgroundColor) {
this.backgroundColor = backgroundColor;
hasBackgroundColor = true;
return this;
}
public boolean hasBackgroundColor() {
return hasBackgroundColor;
}
public WebvttCssStyle setFontSize(float fontSize) {
this.fontSize = fontSize;
return this;
}
public WebvttCssStyle setFontSizeUnit(@FontSizeUnit int unit) {
this.fontSizeUnit = unit;
return this;
}
@FontSizeUnit
public int getFontSizeUnit() {
return fontSizeUnit;
}
public float getFontSize() {
return fontSize;
}
public WebvttCssStyle setRubyPosition(@TextAnnotation.Position int rubyPosition) {
this.rubyPosition = rubyPosition;
return this;
}
@TextAnnotation.Position
public int getRubyPosition() {
return rubyPosition;
}
public WebvttCssStyle setCombineUpright(boolean enabled) {
this.combineUpright = enabled;
return this;
}
public boolean getCombineUpright() {
return combineUpright;
}
private static int updateScoreForMatch(
int currentScore, String target, @Nullable String actual, int score) {
if (target.isEmpty() || currentScore == -1) {
return currentScore;
}
return target.equals(actual) ? currentScore + score : -1;
}
}