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