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.text.FontConfig;
20import android.graphics.fonts.FontVariationAxis;
21import android.util.Xml;
22
23import org.xmlpull.v1.XmlPullParser;
24import org.xmlpull.v1.XmlPullParserException;
25
26import android.annotation.Nullable;
27import com.android.internal.annotations.VisibleForTesting;
28
29import java.io.IOException;
30import java.io.InputStream;
31import java.util.ArrayList;
32import java.util.List;
33import java.util.regex.Pattern;
34
35/**
36 * Parser for font config files.
37 *
38 * @hide
39 */
40public class FontListParser {
41
42    /* Parse fallback list (no names) */
43    public static FontConfig parse(InputStream in) throws XmlPullParserException, IOException {
44        try {
45            XmlPullParser parser = Xml.newPullParser();
46            parser.setInput(in, null);
47            parser.nextTag();
48            return readFamilies(parser);
49        } finally {
50            in.close();
51        }
52    }
53
54    private static FontConfig readFamilies(XmlPullParser parser)
55            throws XmlPullParserException, IOException {
56        List<FontConfig.Family> families = new ArrayList<>();
57        List<FontConfig.Alias> aliases = new ArrayList<>();
58
59        parser.require(XmlPullParser.START_TAG, null, "familyset");
60        while (parser.next() != XmlPullParser.END_TAG) {
61            if (parser.getEventType() != XmlPullParser.START_TAG) continue;
62            String tag = parser.getName();
63            if (tag.equals("family")) {
64                families.add(readFamily(parser));
65            } else if (tag.equals("alias")) {
66                aliases.add(readAlias(parser));
67            } else {
68                skip(parser);
69            }
70        }
71        return new FontConfig(families.toArray(new FontConfig.Family[families.size()]),
72                aliases.toArray(new FontConfig.Alias[aliases.size()]));
73    }
74
75    private static FontConfig.Family readFamily(XmlPullParser parser)
76            throws XmlPullParserException, IOException {
77        String name = parser.getAttributeValue(null, "name");
78        String lang = parser.getAttributeValue(null, "lang");
79        String variant = parser.getAttributeValue(null, "variant");
80        List<FontConfig.Font> fonts = new ArrayList<FontConfig.Font>();
81        while (parser.next() != XmlPullParser.END_TAG) {
82            if (parser.getEventType() != XmlPullParser.START_TAG) continue;
83            String tag = parser.getName();
84            if (tag.equals("font")) {
85                fonts.add(readFont(parser));
86            } else {
87                skip(parser);
88            }
89        }
90        int intVariant = FontConfig.Family.VARIANT_DEFAULT;
91        if (variant != null) {
92            if (variant.equals("compact")) {
93                intVariant = FontConfig.Family.VARIANT_COMPACT;
94            } else if (variant.equals("elegant")) {
95                intVariant = FontConfig.Family.VARIANT_ELEGANT;
96            }
97        }
98        return new FontConfig.Family(name, fonts.toArray(new FontConfig.Font[fonts.size()]), lang,
99                intVariant);
100    }
101
102    /** Matches leading and trailing XML whitespace. */
103    private static final Pattern FILENAME_WHITESPACE_PATTERN =
104            Pattern.compile("^[ \\n\\r\\t]+|[ \\n\\r\\t]+$");
105
106    private static FontConfig.Font readFont(XmlPullParser parser)
107            throws XmlPullParserException, IOException {
108        String indexStr = parser.getAttributeValue(null, "index");
109        int index = indexStr == null ? 0 : Integer.parseInt(indexStr);
110        List<FontVariationAxis> axes = new ArrayList<FontVariationAxis>();
111        String weightStr = parser.getAttributeValue(null, "weight");
112        int weight = weightStr == null ? 400 : Integer.parseInt(weightStr);
113        boolean isItalic = "italic".equals(parser.getAttributeValue(null, "style"));
114        StringBuilder filename = new StringBuilder();
115        while (parser.next() != XmlPullParser.END_TAG) {
116            if (parser.getEventType() == XmlPullParser.TEXT) {
117                filename.append(parser.getText());
118            }
119            if (parser.getEventType() != XmlPullParser.START_TAG) continue;
120            String tag = parser.getName();
121            if (tag.equals("axis")) {
122                axes.add(readAxis(parser));
123            } else {
124                skip(parser);
125            }
126        }
127        String sanitizedName = FILENAME_WHITESPACE_PATTERN.matcher(filename).replaceAll("");
128        return new FontConfig.Font(sanitizedName, index,
129                axes.toArray(new FontVariationAxis[axes.size()]), weight, isItalic);
130    }
131
132    private static FontVariationAxis readAxis(XmlPullParser parser)
133            throws XmlPullParserException, IOException {
134        String tagStr = parser.getAttributeValue(null, "tag");
135        String styleValueStr = parser.getAttributeValue(null, "stylevalue");
136        skip(parser);  // axis tag is empty, ignore any contents and consume end tag
137        return new FontVariationAxis(tagStr, Float.parseFloat(styleValueStr));
138    }
139
140    private static FontConfig.Alias readAlias(XmlPullParser parser)
141            throws XmlPullParserException, IOException {
142        String name = parser.getAttributeValue(null, "name");
143        String toName = parser.getAttributeValue(null, "to");
144        String weightStr = parser.getAttributeValue(null, "weight");
145        int weight;
146        if (weightStr == null) {
147            weight = 400;
148        } else {
149            weight = Integer.parseInt(weightStr);
150        }
151        skip(parser);  // alias tag is empty, ignore any contents and consume end tag
152        return new FontConfig.Alias(name, toName, weight);
153    }
154
155    private static void skip(XmlPullParser parser) throws XmlPullParserException, IOException {
156        int depth = 1;
157        while (depth > 0) {
158            switch (parser.next()) {
159            case XmlPullParser.START_TAG:
160                depth++;
161                break;
162            case XmlPullParser.END_TAG:
163                depth--;
164                break;
165            }
166        }
167    }
168}
169