FontListParser.java revision 0ec4f416ffbcbfb30fb61fb5bae2fe1c1cde8042
1/*
2 * Copyright (C) 2014 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;
18
19import android.util.Xml;
20
21import org.xmlpull.v1.XmlPullParser;
22import org.xmlpull.v1.XmlPullParserException;
23
24import com.android.internal.annotations.VisibleForTesting;
25
26import java.io.IOException;
27import java.io.InputStream;
28import java.util.ArrayList;
29import java.util.List;
30import java.util.regex.Pattern;
31
32/**
33 * Parser for font config files.
34 *
35 * @hide
36 */
37public class FontListParser {
38
39    public static class Config {
40        Config() {
41            families = new ArrayList<Family>();
42            aliases = new ArrayList<Alias>();
43        }
44        public List<Family> families;
45        public List<Alias> aliases;
46    }
47
48    public static class Axis {
49        Axis(int tag, float styleValue) {
50            this.tag = tag;
51            this.styleValue = styleValue;
52        }
53        public final int tag;
54        public final float styleValue;
55    }
56
57    public static class Font {
58        Font(String fontName, int ttcIndex, List<Axis> axes, int weight, boolean isItalic) {
59            this.fontName = fontName;
60            this.ttcIndex = ttcIndex;
61            this.axes = axes;
62            this.weight = weight;
63            this.isItalic = isItalic;
64        }
65        public String fontName;
66        public int ttcIndex;
67        public final List<Axis> axes;
68        public int weight;
69        public boolean isItalic;
70    }
71
72    public static class Alias {
73        public String name;
74        public String toName;
75        public int weight;
76    }
77
78    public static class Family {
79        public Family(String name, List<Font> fonts, String lang, String variant) {
80            this.name = name;
81            this.fonts = fonts;
82            this.lang = lang;
83            this.variant = variant;
84        }
85
86        public String name;
87        public List<Font> fonts;
88        public String lang;
89        public String variant;
90    }
91
92    /* Parse fallback list (no names) */
93    public static Config parse(InputStream in) throws XmlPullParserException, IOException {
94        try {
95            XmlPullParser parser = Xml.newPullParser();
96            parser.setInput(in, null);
97            parser.nextTag();
98            return readFamilies(parser);
99        } finally {
100            in.close();
101        }
102    }
103
104    // Note that a well-formed variation contains a four-character tag and a float as styleValue,
105    // with spacers in between. The tag is enclosd either by double quotes or single quotes.
106    @VisibleForTesting
107    public static Axis[] parseFontVariationSettings(String settings) {
108        String[] settingList = settings.split(",");
109        ArrayList<Axis> axisList = new ArrayList<>();
110        settingLoop:
111        for (String setting : settingList) {
112            int pos = 0;
113            while (pos < setting.length()) {
114                char c = setting.charAt(pos);
115                if (c == '\'' || c == '"') {
116                    break;
117                } else if (!isSpacer(c)) {
118                    continue settingLoop;  // Only spacers are allowed before tag appeared.
119                }
120                pos++;
121            }
122            if (pos + 7 > setting.length()) {
123                continue;  // 7 is the minimum length of tag-style value pair text.
124            }
125            if (setting.charAt(pos) != setting.charAt(pos + 5)) {
126                continue;  // Tag should be wrapped with double or single quote.
127            }
128            String tagString = setting.substring(pos + 1, pos + 5);
129            if (!TAG_PATTERN.matcher(tagString).matches()) {
130                continue;  // Skip incorrect format tag.
131            }
132            pos += 6;
133            while (pos < setting.length()) {
134                if (!isSpacer(setting.charAt(pos++))) {
135                    break;  // Skip spacers between the tag and the styleValue.
136                }
137            }
138            // Skip invalid styleValue
139            float styleValue;
140            String valueString = setting.substring(pos - 1);
141            if (!STYLE_VALUE_PATTERN.matcher(valueString).matches()) {
142                continue;  // Skip incorrect format styleValue.
143            }
144            try {
145                styleValue = Float.parseFloat(valueString);
146            } catch (NumberFormatException e) {
147                continue;  // ignoreing invalid number format
148            }
149            int tag = makeTag(tagString.charAt(0), tagString.charAt(1), tagString.charAt(2),
150                    tagString.charAt(3));
151            axisList.add(new Axis(tag, styleValue));
152        }
153        return axisList.toArray(new Axis[axisList.size()]);
154    }
155
156    @VisibleForTesting
157    public static int makeTag(char c1, char c2, char c3, char c4) {
158        return (c1 << 24) | (c2 << 16) | (c3 << 8) | c4;
159    }
160
161    private static boolean isSpacer(char c) {
162        return c == ' ' || c == '\r' || c == '\t' || c == '\n';
163    }
164
165    private static Config readFamilies(XmlPullParser parser)
166            throws XmlPullParserException, IOException {
167        Config config = new Config();
168        parser.require(XmlPullParser.START_TAG, null, "familyset");
169        while (parser.next() != XmlPullParser.END_TAG) {
170            if (parser.getEventType() != XmlPullParser.START_TAG) continue;
171            String tag = parser.getName();
172            if (tag.equals("family")) {
173                config.families.add(readFamily(parser));
174            } else if (tag.equals("alias")) {
175                config.aliases.add(readAlias(parser));
176            } else {
177                skip(parser);
178            }
179        }
180        return config;
181    }
182
183    private static Family readFamily(XmlPullParser parser)
184            throws XmlPullParserException, IOException {
185        String name = parser.getAttributeValue(null, "name");
186        String lang = parser.getAttributeValue(null, "lang");
187        String variant = parser.getAttributeValue(null, "variant");
188        List<Font> fonts = new ArrayList<Font>();
189        while (parser.next() != XmlPullParser.END_TAG) {
190            if (parser.getEventType() != XmlPullParser.START_TAG) continue;
191            String tag = parser.getName();
192            if (tag.equals("font")) {
193                fonts.add(readFont(parser));
194            } else {
195                skip(parser);
196            }
197        }
198        return new Family(name, fonts, lang, variant);
199    }
200
201    /** Matches leading and trailing XML whitespace. */
202    private static final Pattern FILENAME_WHITESPACE_PATTERN =
203            Pattern.compile("^[ \\n\\r\\t]+|[ \\n\\r\\t]+$");
204
205    private static Font readFont(XmlPullParser parser)
206            throws XmlPullParserException, IOException {
207        String indexStr = parser.getAttributeValue(null, "index");
208        int index = indexStr == null ? 0 : Integer.parseInt(indexStr);
209        List<Axis> axes = new ArrayList<Axis>();
210        String weightStr = parser.getAttributeValue(null, "weight");
211        int weight = weightStr == null ? 400 : Integer.parseInt(weightStr);
212        boolean isItalic = "italic".equals(parser.getAttributeValue(null, "style"));
213        StringBuilder filename = new StringBuilder();
214        while (parser.next() != XmlPullParser.END_TAG) {
215            if (parser.getEventType() == XmlPullParser.TEXT) {
216                filename.append(parser.getText());
217            }
218            if (parser.getEventType() != XmlPullParser.START_TAG) continue;
219            String tag = parser.getName();
220            if (tag.equals("axis")) {
221                axes.add(readAxis(parser));
222            } else {
223                skip(parser);
224            }
225        }
226        String fullFilename = "/system/fonts/" +
227                FILENAME_WHITESPACE_PATTERN.matcher(filename).replaceAll("");
228        return new Font(fullFilename, index, axes, weight, isItalic);
229    }
230
231    /** The 'tag' attribute value is read as four character values between U+0020 and U+007E
232     *  inclusive.
233     */
234    private static final Pattern TAG_PATTERN = Pattern.compile("[\\x20-\\x7E]{4}");
235
236    /** The 'styleValue' attribute has an optional leading '-', followed by '<digits>',
237     *  '<digits>.<digits>', or '.<digits>' where '<digits>' is one or more of [0-9].
238     */
239    private static final Pattern STYLE_VALUE_PATTERN =
240            Pattern.compile("-?(([0-9]+(\\.[0-9]+)?)|(\\.[0-9]+))");
241
242    private static Axis readAxis(XmlPullParser parser)
243            throws XmlPullParserException, IOException {
244        int tag = 0;
245        String tagStr = parser.getAttributeValue(null, "tag");
246        if (tagStr != null && TAG_PATTERN.matcher(tagStr).matches()) {
247            tag = makeTag(tagStr.charAt(0), tagStr.charAt(1), tagStr.charAt(2), tagStr.charAt(3));
248        } else {
249            throw new XmlPullParserException("Invalid tag attribute value.", parser, null);
250        }
251
252        float styleValue = 0;
253        String styleValueStr = parser.getAttributeValue(null, "stylevalue");
254        if (styleValueStr != null && STYLE_VALUE_PATTERN.matcher(styleValueStr).matches()) {
255            styleValue = Float.parseFloat(styleValueStr);
256        } else {
257            throw new XmlPullParserException("Invalid styleValue attribute value.", parser, null);
258        }
259
260        skip(parser);  // axis tag is empty, ignore any contents and consume end tag
261        return new Axis(tag, styleValue);
262    }
263
264    private static Alias readAlias(XmlPullParser parser)
265            throws XmlPullParserException, IOException {
266        Alias alias = new Alias();
267        alias.name = parser.getAttributeValue(null, "name");
268        alias.toName = parser.getAttributeValue(null, "to");
269        String weightStr = parser.getAttributeValue(null, "weight");
270        if (weightStr == null) {
271            alias.weight = 400;
272        } else {
273            alias.weight = Integer.parseInt(weightStr);
274        }
275        skip(parser);  // alias tag is empty, ignore any contents and consume end tag
276        return alias;
277    }
278
279    private static void skip(XmlPullParser parser) throws XmlPullParserException, IOException {
280        int depth = 1;
281        while (depth > 0) {
282            switch (parser.next()) {
283            case XmlPullParser.START_TAG:
284                depth++;
285                break;
286            case XmlPullParser.END_TAG:
287                depth--;
288                break;
289            }
290        }
291    }
292}
293