1 /*
2  * Copyright (C) 2017 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 
17 package android.graphics.fonts;
18 
19 import android.annotation.NonNull;
20 import android.annotation.Nullable;
21 import android.compat.annotation.UnsupportedAppUsage;
22 import android.os.Build;
23 import android.text.TextUtils;
24 
25 import java.util.ArrayList;
26 import java.util.Objects;
27 import java.util.regex.Pattern;
28 
29 /**
30  * Class that holds information about single font variation axis.
31  */
32 public final class FontVariationAxis {
33     @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
34     private final int mTag;
35     private final String mTagString;
36     @UnsupportedAppUsage
37     private final float mStyleValue;
38 
39     /**
40      * Construct FontVariationAxis.
41      *
42      * The axis tag must contain four ASCII characters. Tag string that are longer or shorter than
43      * four characters, or contains characters outside of U+0020..U+007E are invalid.
44      *
45      * @throws IllegalArgumentException If given tag string is invalid.
46      */
FontVariationAxis(@onNull String tagString, float styleValue)47     public FontVariationAxis(@NonNull String tagString, float styleValue) {
48         if (!isValidTag(tagString)) {
49             throw new IllegalArgumentException("Illegal tag pattern: " + tagString);
50         }
51         mTag = makeTag(tagString);
52         mTagString = tagString;
53         mStyleValue = styleValue;
54     }
55 
56     /**
57      * Returns the OpenType style tag value.
58      * @hide
59      */
getOpenTypeTagValue()60     public int getOpenTypeTagValue() {
61         return mTag;
62     }
63 
64     /**
65      * Returns the variable font axis tag associated to this axis.
66      */
getTag()67     public String getTag() {
68         return mTagString;
69     }
70 
71     /**
72      * Returns the style value associated to the given axis for this font.
73      */
getStyleValue()74     public float getStyleValue() {
75         return mStyleValue;
76     }
77 
78     /**
79      * Returns a valid font variation setting string for this object.
80      */
81     @Override
toString()82     public @NonNull String toString() {
83         return "'" + mTagString + "' " + Float.toString(mStyleValue);
84     }
85 
86     /**
87      * The 'tag' attribute value is read as four character values between U+0020 and U+007E
88      * inclusive.
89      */
90     private static final Pattern TAG_PATTERN = Pattern.compile("[\u0020-\u007E]{4}");
91 
92     /**
93      * Returns true if 'tagString' is valid for font variation axis tag.
94      */
isValidTag(String tagString)95     private static boolean isValidTag(String tagString) {
96         return tagString != null && TAG_PATTERN.matcher(tagString).matches();
97     }
98 
99     /**
100      * The 'styleValue' attribute has an optional leading '-', followed by '<digits>',
101      * '<digits>.<digits>', or '.<digits>' where '<digits>' is one or more of [0-9].
102      */
103     private static final Pattern STYLE_VALUE_PATTERN =
104             Pattern.compile("-?(([0-9]+(\\.[0-9]+)?)|(\\.[0-9]+))");
105 
isValidValueFormat(String valueString)106     private static boolean isValidValueFormat(String valueString) {
107         return valueString != null && STYLE_VALUE_PATTERN.matcher(valueString).matches();
108     }
109 
110     /** @hide */
makeTag(String tagString)111     public static int makeTag(String tagString) {
112         final char c1 = tagString.charAt(0);
113         final char c2 = tagString.charAt(1);
114         final char c3 = tagString.charAt(2);
115         final char c4 = tagString.charAt(3);
116         return (c1 << 24) | (c2 << 16) | (c3 << 8) | c4;
117     }
118 
119     /**
120      * Construct FontVariationAxis array from font variation settings.
121      *
122      * The settings string is constructed from multiple pairs of axis tag and style values. The axis
123      * tag must contain four ASCII characters and must be wrapped with single quotes (U+0027) or
124      * double quotes (U+0022). Axis strings that are longer or shorter than four characters, or
125      * contain characters outside of U+0020..U+007E are invalid. If a specified axis name is not
126      * defined in the font, the settings will be ignored.
127      *
128      * <pre>
129      *   FontVariationAxis.fromFontVariationSettings("'wdth' 1.0");
130      *   FontVariationAxis.fromFontVariationSettings("'AX  ' 1.0, 'FB  ' 2.0");
131      * </pre>
132      *
133      * @param settings font variation settings.
134      * @return FontVariationAxis[] the array of parsed font variation axis. {@code null} if settings
135      *                             has no font variation settings.
136      * @throws IllegalArgumentException If given string is not a valid font variation settings
137      *                                  format.
138      */
fromFontVariationSettings( @ullable String settings)139     public static @Nullable FontVariationAxis[] fromFontVariationSettings(
140             @Nullable String settings) {
141         if (settings == null || settings.isEmpty()) {
142             return null;
143         }
144         final ArrayList<FontVariationAxis> axisList = new ArrayList<>();
145         final int length = settings.length();
146         for (int i = 0; i < length; i++) {
147             final char c = settings.charAt(i);
148             if (Character.isWhitespace(c)) {
149                 continue;
150             }
151             if (!(c == '\'' || c == '"') || length < i + 6 || settings.charAt(i + 5) != c) {
152                 throw new IllegalArgumentException(
153                         "Tag should be wrapped with double or single quote: " + settings);
154             }
155             final String tagString = settings.substring(i + 1, i + 5);
156 
157             i += 6;  // Move to end of tag.
158             int endOfValueString = settings.indexOf(',', i);
159             if (endOfValueString == -1) {
160                 endOfValueString = length;
161             }
162             final float value;
163             try {
164                 // Float.parseFloat ignores leading/trailing whitespaces.
165                 value = Float.parseFloat(settings.substring(i, endOfValueString));
166             } catch (NumberFormatException e) {
167                 throw new IllegalArgumentException(
168                         "Failed to parse float string: " + e.getMessage());
169             }
170             axisList.add(new FontVariationAxis(tagString, value));
171             i = endOfValueString;
172         }
173         if (axisList.isEmpty()) {
174             return null;
175         }
176         return axisList.toArray(new FontVariationAxis[0]);
177     }
178 
179     /**
180      * Stringify the array of FontVariationAxis.
181      *
182      * @param axes an array of FontVariationAxis.
183      * @return String a valid font variation settings string.
184      */
toFontVariationSettings(@ullable FontVariationAxis[] axes)185     public static @NonNull String toFontVariationSettings(@Nullable FontVariationAxis[] axes) {
186         if (axes == null) {
187             return "";
188         }
189         return TextUtils.join(",", axes);
190     }
191 
192     @Override
equals(@ullable Object o)193     public boolean equals(@Nullable Object o) {
194         if (o == this) {
195             return true;
196         }
197         if (o == null || !(o instanceof FontVariationAxis)) {
198             return false;
199         }
200         FontVariationAxis axis = (FontVariationAxis) o;
201         return axis.mTag == mTag && axis.mStyleValue == mStyleValue;
202     }
203 
204     @Override
hashCode()205     public int hashCode() {
206         return Objects.hash(mTag, mStyleValue);
207     }
208 }
209 
210