/* * Copyright (C) 2014 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package android.graphics; import android.util.Xml; import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParserException; import com.android.internal.annotations.VisibleForTesting; import java.io.IOException; import java.io.InputStream; import java.util.ArrayList; import java.util.List; import java.util.regex.Pattern; /** * Parser for font config files. * * @hide */ public class FontListParser { public static class Config { Config() { families = new ArrayList(); aliases = new ArrayList(); } public List families; public List aliases; } public static class Axis { Axis(int tag, float styleValue) { this.tag = tag; this.styleValue = styleValue; } public final int tag; public final float styleValue; } public static class Font { Font(String fontName, int ttcIndex, List axes, int weight, boolean isItalic) { this.fontName = fontName; this.ttcIndex = ttcIndex; this.axes = axes; this.weight = weight; this.isItalic = isItalic; } public String fontName; public int ttcIndex; public final List axes; public int weight; public boolean isItalic; } public static class Alias { public String name; public String toName; public int weight; } public static class Family { public Family(String name, List fonts, String lang, String variant) { this.name = name; this.fonts = fonts; this.lang = lang; this.variant = variant; } public String name; public List fonts; public String lang; public String variant; } /* Parse fallback list (no names) */ public static Config parse(InputStream in) throws XmlPullParserException, IOException { try { XmlPullParser parser = Xml.newPullParser(); parser.setInput(in, null); parser.nextTag(); return readFamilies(parser); } finally { in.close(); } } // Note that a well-formed variation contains a four-character tag and a float as styleValue, // with spacers in between. The tag is enclosd either by double quotes or single quotes. @VisibleForTesting public static Axis[] parseFontVariationSettings(String settings) { String[] settingList = settings.split(","); ArrayList axisList = new ArrayList<>(); settingLoop: for (String setting : settingList) { int pos = 0; while (pos < setting.length()) { char c = setting.charAt(pos); if (c == '\'' || c == '"') { break; } else if (!isSpacer(c)) { continue settingLoop; // Only spacers are allowed before tag appeared. } pos++; } if (pos + 7 > setting.length()) { continue; // 7 is the minimum length of tag-style value pair text. } if (setting.charAt(pos) != setting.charAt(pos + 5)) { continue; // Tag should be wrapped with double or single quote. } String tagString = setting.substring(pos + 1, pos + 5); if (!TAG_PATTERN.matcher(tagString).matches()) { continue; // Skip incorrect format tag. } pos += 6; while (pos < setting.length()) { if (!isSpacer(setting.charAt(pos++))) { break; // Skip spacers between the tag and the styleValue. } } // Skip invalid styleValue float styleValue; String valueString = setting.substring(pos - 1); if (!STYLE_VALUE_PATTERN.matcher(valueString).matches()) { continue; // Skip incorrect format styleValue. } try { styleValue = Float.parseFloat(valueString); } catch (NumberFormatException e) { continue; // ignoreing invalid number format } int tag = makeTag(tagString.charAt(0), tagString.charAt(1), tagString.charAt(2), tagString.charAt(3)); axisList.add(new Axis(tag, styleValue)); } return axisList.toArray(new Axis[axisList.size()]); } @VisibleForTesting public static int makeTag(char c1, char c2, char c3, char c4) { return (c1 << 24) | (c2 << 16) | (c3 << 8) | c4; } private static boolean isSpacer(char c) { return c == ' ' || c == '\r' || c == '\t' || c == '\n'; } private static Config readFamilies(XmlPullParser parser) throws XmlPullParserException, IOException { Config config = new Config(); parser.require(XmlPullParser.START_TAG, null, "familyset"); while (parser.next() != XmlPullParser.END_TAG) { if (parser.getEventType() != XmlPullParser.START_TAG) continue; String tag = parser.getName(); if (tag.equals("family")) { config.families.add(readFamily(parser)); } else if (tag.equals("alias")) { config.aliases.add(readAlias(parser)); } else { skip(parser); } } return config; } private static Family readFamily(XmlPullParser parser) throws XmlPullParserException, IOException { String name = parser.getAttributeValue(null, "name"); String lang = parser.getAttributeValue(null, "lang"); String variant = parser.getAttributeValue(null, "variant"); List fonts = new ArrayList(); while (parser.next() != XmlPullParser.END_TAG) { if (parser.getEventType() != XmlPullParser.START_TAG) continue; String tag = parser.getName(); if (tag.equals("font")) { fonts.add(readFont(parser)); } else { skip(parser); } } return new Family(name, fonts, lang, variant); } /** Matches leading and trailing XML whitespace. */ private static final Pattern FILENAME_WHITESPACE_PATTERN = Pattern.compile("^[ \\n\\r\\t]+|[ \\n\\r\\t]+$"); private static Font readFont(XmlPullParser parser) throws XmlPullParserException, IOException { String indexStr = parser.getAttributeValue(null, "index"); int index = indexStr == null ? 0 : Integer.parseInt(indexStr); List axes = new ArrayList(); String weightStr = parser.getAttributeValue(null, "weight"); int weight = weightStr == null ? 400 : Integer.parseInt(weightStr); boolean isItalic = "italic".equals(parser.getAttributeValue(null, "style")); StringBuilder filename = new StringBuilder(); while (parser.next() != XmlPullParser.END_TAG) { if (parser.getEventType() == XmlPullParser.TEXT) { filename.append(parser.getText()); } if (parser.getEventType() != XmlPullParser.START_TAG) continue; String tag = parser.getName(); if (tag.equals("axis")) { axes.add(readAxis(parser)); } else { skip(parser); } } String fullFilename = "/system/fonts/" + FILENAME_WHITESPACE_PATTERN.matcher(filename).replaceAll(""); return new Font(fullFilename, index, axes, weight, isItalic); } /** The 'tag' attribute value is read as four character values between U+0020 and U+007E * inclusive. */ private static final Pattern TAG_PATTERN = Pattern.compile("[\\x20-\\x7E]{4}"); /** The 'styleValue' attribute has an optional leading '-', followed by '', * '.', or '.' where '' is one or more of [0-9]. */ private static final Pattern STYLE_VALUE_PATTERN = Pattern.compile("-?(([0-9]+(\\.[0-9]+)?)|(\\.[0-9]+))"); private static Axis readAxis(XmlPullParser parser) throws XmlPullParserException, IOException { int tag = 0; String tagStr = parser.getAttributeValue(null, "tag"); if (tagStr != null && TAG_PATTERN.matcher(tagStr).matches()) { tag = makeTag(tagStr.charAt(0), tagStr.charAt(1), tagStr.charAt(2), tagStr.charAt(3)); } else { throw new XmlPullParserException("Invalid tag attribute value.", parser, null); } float styleValue = 0; String styleValueStr = parser.getAttributeValue(null, "stylevalue"); if (styleValueStr != null && STYLE_VALUE_PATTERN.matcher(styleValueStr).matches()) { styleValue = Float.parseFloat(styleValueStr); } else { throw new XmlPullParserException("Invalid styleValue attribute value.", parser, null); } skip(parser); // axis tag is empty, ignore any contents and consume end tag return new Axis(tag, styleValue); } private static Alias readAlias(XmlPullParser parser) throws XmlPullParserException, IOException { Alias alias = new Alias(); alias.name = parser.getAttributeValue(null, "name"); alias.toName = parser.getAttributeValue(null, "to"); String weightStr = parser.getAttributeValue(null, "weight"); if (weightStr == null) { alias.weight = 400; } else { alias.weight = Integer.parseInt(weightStr); } skip(parser); // alias tag is empty, ignore any contents and consume end tag return alias; } private static void skip(XmlPullParser parser) throws XmlPullParserException, IOException { int depth = 1; while (depth > 0) { switch (parser.next()) { case XmlPullParser.START_TAG: depth++; break; case XmlPullParser.END_TAG: depth--; break; } } } }