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