FontListParser.java revision 20e5d91739fb88a02afb4888bf9f938308bc9b7b
13b8b46f3a46ccf35a6bb6a828af0f2d011cc9abeAndrew Scull/*
23b8b46f3a46ccf35a6bb6a828af0f2d011cc9abeAndrew Scull * Copyright (C) 2014 The Android Open Source Project
33b8b46f3a46ccf35a6bb6a828af0f2d011cc9abeAndrew Scull *
43b8b46f3a46ccf35a6bb6a828af0f2d011cc9abeAndrew Scull * Licensed under the Apache License, Version 2.0 (the "License");
53b8b46f3a46ccf35a6bb6a828af0f2d011cc9abeAndrew Scull * you may not use this file except in compliance with the License.
63b8b46f3a46ccf35a6bb6a828af0f2d011cc9abeAndrew Scull * You may obtain a copy of the License at
73b8b46f3a46ccf35a6bb6a828af0f2d011cc9abeAndrew Scull *
83b8b46f3a46ccf35a6bb6a828af0f2d011cc9abeAndrew Scull *      http://www.apache.org/licenses/LICENSE-2.0
93b8b46f3a46ccf35a6bb6a828af0f2d011cc9abeAndrew Scull *
103b8b46f3a46ccf35a6bb6a828af0f2d011cc9abeAndrew Scull * Unless required by applicable law or agreed to in writing, software
113b8b46f3a46ccf35a6bb6a828af0f2d011cc9abeAndrew Scull * distributed under the License is distributed on an "AS IS" BASIS,
123b8b46f3a46ccf35a6bb6a828af0f2d011cc9abeAndrew Scull * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
133b8b46f3a46ccf35a6bb6a828af0f2d011cc9abeAndrew Scull * See the License for the specific language governing permissions and
143b8b46f3a46ccf35a6bb6a828af0f2d011cc9abeAndrew Scull * limitations under the License.
153b8b46f3a46ccf35a6bb6a828af0f2d011cc9abeAndrew Scull */
163b8b46f3a46ccf35a6bb6a828af0f2d011cc9abeAndrew Scull
173b8b46f3a46ccf35a6bb6a828af0f2d011cc9abeAndrew Scullpackage android.graphics;
183b8b46f3a46ccf35a6bb6a828af0f2d011cc9abeAndrew Scull
193b8b46f3a46ccf35a6bb6a828af0f2d011cc9abeAndrew Scullimport android.text.FontConfig;
20d86b8fea43ebb6e5c31691b44d8ceb0d8d3c9072Jeff Sharkeyimport android.util.Xml;
213b8b46f3a46ccf35a6bb6a828af0f2d011cc9abeAndrew Scull
22d86b8fea43ebb6e5c31691b44d8ceb0d8d3c9072Jeff Sharkeyimport org.xmlpull.v1.XmlPullParser;
23d86b8fea43ebb6e5c31691b44d8ceb0d8d3c9072Jeff Sharkeyimport org.xmlpull.v1.XmlPullParserException;
243b8b46f3a46ccf35a6bb6a828af0f2d011cc9abeAndrew Scull
253b8b46f3a46ccf35a6bb6a828af0f2d011cc9abeAndrew Scullimport android.annotation.Nullable;
263b8b46f3a46ccf35a6bb6a828af0f2d011cc9abeAndrew Scullimport com.android.internal.annotations.VisibleForTesting;
273b8b46f3a46ccf35a6bb6a828af0f2d011cc9abeAndrew Scull
283b8b46f3a46ccf35a6bb6a828af0f2d011cc9abeAndrew Scullimport java.io.IOException;
293b8b46f3a46ccf35a6bb6a828af0f2d011cc9abeAndrew Scullimport java.io.InputStream;
303b8b46f3a46ccf35a6bb6a828af0f2d011cc9abeAndrew Scullimport java.util.ArrayList;
313b8b46f3a46ccf35a6bb6a828af0f2d011cc9abeAndrew Scullimport java.util.List;
323b8b46f3a46ccf35a6bb6a828af0f2d011cc9abeAndrew Scullimport java.util.regex.Pattern;
333b8b46f3a46ccf35a6bb6a828af0f2d011cc9abeAndrew Scull
343b8b46f3a46ccf35a6bb6a828af0f2d011cc9abeAndrew Scull/**
353b8b46f3a46ccf35a6bb6a828af0f2d011cc9abeAndrew Scull * Parser for font config files.
363b8b46f3a46ccf35a6bb6a828af0f2d011cc9abeAndrew Scull *
37d86b8fea43ebb6e5c31691b44d8ceb0d8d3c9072Jeff Sharkey * @hide
383b8b46f3a46ccf35a6bb6a828af0f2d011cc9abeAndrew Scull */
393b8b46f3a46ccf35a6bb6a828af0f2d011cc9abeAndrew Scullpublic class FontListParser {
403b8b46f3a46ccf35a6bb6a828af0f2d011cc9abeAndrew Scull
413b8b46f3a46ccf35a6bb6a828af0f2d011cc9abeAndrew Scull    /* Parse fallback list (no names) */
423b8b46f3a46ccf35a6bb6a828af0f2d011cc9abeAndrew Scull    public static FontConfig parse(InputStream in) throws XmlPullParserException, IOException {
433b8b46f3a46ccf35a6bb6a828af0f2d011cc9abeAndrew Scull        try {
443b8b46f3a46ccf35a6bb6a828af0f2d011cc9abeAndrew Scull            XmlPullParser parser = Xml.newPullParser();
453b8b46f3a46ccf35a6bb6a828af0f2d011cc9abeAndrew Scull            parser.setInput(in, null);
463b8b46f3a46ccf35a6bb6a828af0f2d011cc9abeAndrew Scull            parser.nextTag();
473b8b46f3a46ccf35a6bb6a828af0f2d011cc9abeAndrew Scull            return readFamilies(parser);
483b8b46f3a46ccf35a6bb6a828af0f2d011cc9abeAndrew Scull        } finally {
493b8b46f3a46ccf35a6bb6a828af0f2d011cc9abeAndrew Scull            in.close();
503b8b46f3a46ccf35a6bb6a828af0f2d011cc9abeAndrew Scull        }
513b8b46f3a46ccf35a6bb6a828af0f2d011cc9abeAndrew Scull    }
523b8b46f3a46ccf35a6bb6a828af0f2d011cc9abeAndrew Scull
533b8b46f3a46ccf35a6bb6a828af0f2d011cc9abeAndrew Scull    // Note that a well-formed variation contains a four-character tag and a float as styleValue,
543b8b46f3a46ccf35a6bb6a828af0f2d011cc9abeAndrew Scull    // with spacers in between. The tag is enclosd either by double quotes or single quotes.
553b8b46f3a46ccf35a6bb6a828af0f2d011cc9abeAndrew Scull    @VisibleForTesting
563b8b46f3a46ccf35a6bb6a828af0f2d011cc9abeAndrew Scull    public static ArrayList<FontConfig.Axis> parseFontVariationSettings(@Nullable String settings) {
573b8b46f3a46ccf35a6bb6a828af0f2d011cc9abeAndrew Scull        ArrayList<FontConfig.Axis> axisList = new ArrayList<>();
583b8b46f3a46ccf35a6bb6a828af0f2d011cc9abeAndrew Scull        if (settings == null) {
593b8b46f3a46ccf35a6bb6a828af0f2d011cc9abeAndrew Scull            return axisList;
603b8b46f3a46ccf35a6bb6a828af0f2d011cc9abeAndrew Scull        }
613b8b46f3a46ccf35a6bb6a828af0f2d011cc9abeAndrew Scull        String[] settingList = settings.split(",");
62d86b8fea43ebb6e5c31691b44d8ceb0d8d3c9072Jeff Sharkey        settingLoop:
633b8b46f3a46ccf35a6bb6a828af0f2d011cc9abeAndrew Scull        for (String setting : settingList) {
643b8b46f3a46ccf35a6bb6a828af0f2d011cc9abeAndrew Scull            int pos = 0;
653b8b46f3a46ccf35a6bb6a828af0f2d011cc9abeAndrew Scull            while (pos < setting.length()) {
663b8b46f3a46ccf35a6bb6a828af0f2d011cc9abeAndrew Scull                char c = setting.charAt(pos);
673b8b46f3a46ccf35a6bb6a828af0f2d011cc9abeAndrew Scull                if (c == '\'' || c == '"') {
683b8b46f3a46ccf35a6bb6a828af0f2d011cc9abeAndrew Scull                    break;
693b8b46f3a46ccf35a6bb6a828af0f2d011cc9abeAndrew Scull                } else if (!isSpacer(c)) {
703b8b46f3a46ccf35a6bb6a828af0f2d011cc9abeAndrew Scull                    continue settingLoop;  // Only spacers are allowed before tag appeared.
713b8b46f3a46ccf35a6bb6a828af0f2d011cc9abeAndrew Scull                }
723b8b46f3a46ccf35a6bb6a828af0f2d011cc9abeAndrew Scull                pos++;
733b8b46f3a46ccf35a6bb6a828af0f2d011cc9abeAndrew Scull            }
743b8b46f3a46ccf35a6bb6a828af0f2d011cc9abeAndrew Scull            if (pos + 7 > setting.length()) {
753b8b46f3a46ccf35a6bb6a828af0f2d011cc9abeAndrew Scull                continue;  // 7 is the minimum length of tag-style value pair text.
763b8b46f3a46ccf35a6bb6a828af0f2d011cc9abeAndrew Scull            }
77d86b8fea43ebb6e5c31691b44d8ceb0d8d3c9072Jeff Sharkey            if (setting.charAt(pos) != setting.charAt(pos + 5)) {
783b8b46f3a46ccf35a6bb6a828af0f2d011cc9abeAndrew Scull                continue;  // Tag should be wrapped with double or single quote.
793b8b46f3a46ccf35a6bb6a828af0f2d011cc9abeAndrew Scull            }
803b8b46f3a46ccf35a6bb6a828af0f2d011cc9abeAndrew Scull            String tagString = setting.substring(pos + 1, pos + 5);
813b8b46f3a46ccf35a6bb6a828af0f2d011cc9abeAndrew Scull            if (!TAG_PATTERN.matcher(tagString).matches()) {
823b8b46f3a46ccf35a6bb6a828af0f2d011cc9abeAndrew Scull                continue;  // Skip incorrect format tag.
833b8b46f3a46ccf35a6bb6a828af0f2d011cc9abeAndrew Scull            }
843b8b46f3a46ccf35a6bb6a828af0f2d011cc9abeAndrew Scull            pos += 6;
853b8b46f3a46ccf35a6bb6a828af0f2d011cc9abeAndrew Scull            while (pos < setting.length()) {
863b8b46f3a46ccf35a6bb6a828af0f2d011cc9abeAndrew Scull                if (!isSpacer(setting.charAt(pos++))) {
873b8b46f3a46ccf35a6bb6a828af0f2d011cc9abeAndrew Scull                    break;  // Skip spacers between the tag and the styleValue.
883b8b46f3a46ccf35a6bb6a828af0f2d011cc9abeAndrew Scull                }
893b8b46f3a46ccf35a6bb6a828af0f2d011cc9abeAndrew Scull            }
903b8b46f3a46ccf35a6bb6a828af0f2d011cc9abeAndrew Scull            // Skip invalid styleValue
913b8b46f3a46ccf35a6bb6a828af0f2d011cc9abeAndrew Scull            float styleValue;
923b8b46f3a46ccf35a6bb6a828af0f2d011cc9abeAndrew Scull            String valueString = setting.substring(pos - 1);
933b8b46f3a46ccf35a6bb6a828af0f2d011cc9abeAndrew Scull            if (!STYLE_VALUE_PATTERN.matcher(valueString).matches()) {
943b8b46f3a46ccf35a6bb6a828af0f2d011cc9abeAndrew Scull                continue;  // Skip incorrect format styleValue.
95d86b8fea43ebb6e5c31691b44d8ceb0d8d3c9072Jeff Sharkey            }
963b8b46f3a46ccf35a6bb6a828af0f2d011cc9abeAndrew Scull            try {
973b8b46f3a46ccf35a6bb6a828af0f2d011cc9abeAndrew Scull                styleValue = Float.parseFloat(valueString);
983b8b46f3a46ccf35a6bb6a828af0f2d011cc9abeAndrew Scull            } catch (NumberFormatException e) {
993b8b46f3a46ccf35a6bb6a828af0f2d011cc9abeAndrew Scull                continue;  // ignoreing invalid number format
1003b8b46f3a46ccf35a6bb6a828af0f2d011cc9abeAndrew Scull            }
1013b8b46f3a46ccf35a6bb6a828af0f2d011cc9abeAndrew Scull            int tag = makeTag(tagString);
1023b8b46f3a46ccf35a6bb6a828af0f2d011cc9abeAndrew Scull            axisList.add(new FontConfig.Axis(tag, styleValue));
1033b8b46f3a46ccf35a6bb6a828af0f2d011cc9abeAndrew Scull        }
1043b8b46f3a46ccf35a6bb6a828af0f2d011cc9abeAndrew Scull        return axisList;
1053b8b46f3a46ccf35a6bb6a828af0f2d011cc9abeAndrew Scull    }
1063b8b46f3a46ccf35a6bb6a828af0f2d011cc9abeAndrew Scull
1073b8b46f3a46ccf35a6bb6a828af0f2d011cc9abeAndrew Scull    public static int makeTag(String tagString) {
1083b8b46f3a46ccf35a6bb6a828af0f2d011cc9abeAndrew Scull        char c1 = tagString.charAt(0);
1093b8b46f3a46ccf35a6bb6a828af0f2d011cc9abeAndrew Scull        char c2 = tagString.charAt(1);
110d86b8fea43ebb6e5c31691b44d8ceb0d8d3c9072Jeff Sharkey        char c3 = tagString.charAt(2);
1113b8b46f3a46ccf35a6bb6a828af0f2d011cc9abeAndrew Scull        char c4 = tagString.charAt(3);
1123b8b46f3a46ccf35a6bb6a828af0f2d011cc9abeAndrew Scull        return (c1 << 24) | (c2 << 16) | (c3 << 8) | c4;
1133b8b46f3a46ccf35a6bb6a828af0f2d011cc9abeAndrew Scull    }
1143b8b46f3a46ccf35a6bb6a828af0f2d011cc9abeAndrew Scull
1153b8b46f3a46ccf35a6bb6a828af0f2d011cc9abeAndrew Scull    private static boolean isSpacer(char c) {
1163b8b46f3a46ccf35a6bb6a828af0f2d011cc9abeAndrew Scull        return c == ' ' || c == '\r' || c == '\t' || c == '\n';
1173b8b46f3a46ccf35a6bb6a828af0f2d011cc9abeAndrew Scull    }
1183b8b46f3a46ccf35a6bb6a828af0f2d011cc9abeAndrew Scull
119    private static FontConfig readFamilies(XmlPullParser parser)
120            throws XmlPullParserException, IOException {
121        List<FontConfig.Family> families = new ArrayList<>();
122        List<FontConfig.Alias> aliases = new ArrayList<>();
123
124        parser.require(XmlPullParser.START_TAG, null, "familyset");
125        while (parser.next() != XmlPullParser.END_TAG) {
126            if (parser.getEventType() != XmlPullParser.START_TAG) continue;
127            String tag = parser.getName();
128            if (tag.equals("family")) {
129                families.add(readFamily(parser));
130            } else if (tag.equals("alias")) {
131                aliases.add(readAlias(parser));
132            } else {
133                skip(parser);
134            }
135        }
136        return new FontConfig(families.toArray(new FontConfig.Family[families.size()]),
137                aliases.toArray(new FontConfig.Alias[aliases.size()]));
138    }
139
140    private static FontConfig.Family readFamily(XmlPullParser parser)
141            throws XmlPullParserException, IOException {
142        String name = parser.getAttributeValue(null, "name");
143        String lang = parser.getAttributeValue(null, "lang");
144        String variant = parser.getAttributeValue(null, "variant");
145        List<FontConfig.Font> fonts = new ArrayList<FontConfig.Font>();
146        while (parser.next() != XmlPullParser.END_TAG) {
147            if (parser.getEventType() != XmlPullParser.START_TAG) continue;
148            String tag = parser.getName();
149            if (tag.equals("font")) {
150                fonts.add(readFont(parser));
151            } else {
152                skip(parser);
153            }
154        }
155        int intVariant = FontConfig.Family.VARIANT_DEFAULT;
156        if (variant != null) {
157            if (variant.equals("compact")) {
158                intVariant = FontConfig.Family.VARIANT_COMPACT;
159            } else if (variant.equals("elegant")) {
160                intVariant = FontConfig.Family.VARIANT_ELEGANT;
161            }
162        }
163        return new FontConfig.Family(name, fonts.toArray(new FontConfig.Font[fonts.size()]), lang,
164                intVariant);
165    }
166
167    /** Matches leading and trailing XML whitespace. */
168    private static final Pattern FILENAME_WHITESPACE_PATTERN =
169            Pattern.compile("^[ \\n\\r\\t]+|[ \\n\\r\\t]+$");
170
171    private static FontConfig.Font readFont(XmlPullParser parser)
172            throws XmlPullParserException, IOException {
173        String indexStr = parser.getAttributeValue(null, "index");
174        int index = indexStr == null ? 0 : Integer.parseInt(indexStr);
175        List<FontConfig.Axis> axes = new ArrayList<FontConfig.Axis>();
176        String weightStr = parser.getAttributeValue(null, "weight");
177        int weight = weightStr == null ? 400 : Integer.parseInt(weightStr);
178        boolean isItalic = "italic".equals(parser.getAttributeValue(null, "style"));
179        StringBuilder filename = new StringBuilder();
180        while (parser.next() != XmlPullParser.END_TAG) {
181            if (parser.getEventType() == XmlPullParser.TEXT) {
182                filename.append(parser.getText());
183            }
184            if (parser.getEventType() != XmlPullParser.START_TAG) continue;
185            String tag = parser.getName();
186            if (tag.equals("axis")) {
187                axes.add(readAxis(parser));
188            } else {
189                skip(parser);
190            }
191        }
192        String fullFilename = "/system/fonts/" +
193                FILENAME_WHITESPACE_PATTERN.matcher(filename).replaceAll("");
194        return new FontConfig.Font(fullFilename, index,
195                axes.toArray(new FontConfig.Axis[axes.size()]), weight, isItalic);
196    }
197
198    /** The 'tag' attribute value is read as four character values between U+0020 and U+007E
199     *  inclusive.
200     */
201    private static final Pattern TAG_PATTERN = Pattern.compile("[\\x20-\\x7E]{4}");
202
203    public static boolean isValidTag(String tagString) {
204        if (tagString == null || tagString.length() != 4) {
205            return false;
206        }
207        return TAG_PATTERN.matcher(tagString).matches();
208    }
209
210    /** The 'styleValue' attribute has an optional leading '-', followed by '<digits>',
211     *  '<digits>.<digits>', or '.<digits>' where '<digits>' is one or more of [0-9].
212     */
213    private static final Pattern STYLE_VALUE_PATTERN =
214            Pattern.compile("-?(([0-9]+(\\.[0-9]+)?)|(\\.[0-9]+))");
215
216    private static FontConfig.Axis readAxis(XmlPullParser parser)
217            throws XmlPullParserException, IOException {
218        int tag = 0;
219        String tagStr = parser.getAttributeValue(null, "tag");
220        if (isValidTag(tagStr)) {
221            tag = makeTag(tagStr);
222        } else {
223            throw new XmlPullParserException("Invalid tag attribute value.", parser, null);
224        }
225
226        float styleValue = 0;
227        String styleValueStr = parser.getAttributeValue(null, "stylevalue");
228        if (styleValueStr != null && STYLE_VALUE_PATTERN.matcher(styleValueStr).matches()) {
229            styleValue = Float.parseFloat(styleValueStr);
230        } else {
231            throw new XmlPullParserException("Invalid styleValue attribute value.", parser, null);
232        }
233
234        skip(parser);  // axis tag is empty, ignore any contents and consume end tag
235        return new FontConfig.Axis(tag, styleValue);
236    }
237
238    private static FontConfig.Alias readAlias(XmlPullParser parser)
239            throws XmlPullParserException, IOException {
240        String name = parser.getAttributeValue(null, "name");
241        String toName = parser.getAttributeValue(null, "to");
242        String weightStr = parser.getAttributeValue(null, "weight");
243        int weight;
244        if (weightStr == null) {
245            weight = 400;
246        } else {
247            weight = Integer.parseInt(weightStr);
248        }
249        skip(parser);  // alias tag is empty, ignore any contents and consume end tag
250        return new FontConfig.Alias(name, toName, weight);
251    }
252
253    private static void skip(XmlPullParser parser) throws XmlPullParserException, IOException {
254        int depth = 1;
255        while (depth > 0) {
256            switch (parser.next()) {
257            case XmlPullParser.START_TAG:
258                depth++;
259                break;
260            case XmlPullParser.END_TAG:
261                depth--;
262                break;
263            }
264        }
265    }
266}
267