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 17 package android.graphics; 18 19 import static android.text.FontConfig.NamedFamilyList; 20 21 import android.annotation.NonNull; 22 import android.annotation.Nullable; 23 import android.compat.annotation.UnsupportedAppUsage; 24 import android.graphics.fonts.FontCustomizationParser; 25 import android.graphics.fonts.FontStyle; 26 import android.graphics.fonts.FontVariationAxis; 27 import android.os.Build; 28 import android.os.LocaleList; 29 import android.text.FontConfig; 30 import android.util.ArraySet; 31 import android.util.Xml; 32 33 import org.xmlpull.v1.XmlPullParser; 34 import org.xmlpull.v1.XmlPullParserException; 35 36 import java.io.File; 37 import java.io.FileInputStream; 38 import java.io.IOException; 39 import java.io.InputStream; 40 import java.util.ArrayList; 41 import java.util.Collections; 42 import java.util.List; 43 import java.util.Map; 44 import java.util.Set; 45 import java.util.regex.Pattern; 46 47 /** 48 * Parser for font config files. 49 * @hide 50 */ 51 public class FontListParser { 52 private static final String TAG = "FontListParser"; 53 54 // XML constants for FontFamily. 55 private static final String ATTR_NAME = "name"; 56 private static final String ATTR_LANG = "lang"; 57 private static final String ATTR_VARIANT = "variant"; 58 private static final String TAG_FONT = "font"; 59 private static final String VARIANT_COMPACT = "compact"; 60 private static final String VARIANT_ELEGANT = "elegant"; 61 62 // XML constants for Font. 63 public static final String ATTR_SUPPORTED_AXES = "supportedAxes"; 64 public static final String ATTR_INDEX = "index"; 65 public static final String ATTR_WEIGHT = "weight"; 66 public static final String ATTR_POSTSCRIPT_NAME = "postScriptName"; 67 public static final String ATTR_STYLE = "style"; 68 public static final String ATTR_FALLBACK_FOR = "fallbackFor"; 69 public static final String STYLE_ITALIC = "italic"; 70 public static final String STYLE_NORMAL = "normal"; 71 public static final String TAG_AXIS = "axis"; 72 73 // XML constants for FontVariationAxis. 74 public static final String ATTR_TAG = "tag"; 75 public static final String ATTR_STYLEVALUE = "stylevalue"; 76 77 // The tag string for variable font type resolution. 78 private static final String TAG_WGHT = "wght"; 79 private static final String TAG_ITAL = "ital"; 80 81 /* Parse fallback list (no names) */ 82 @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) parse(InputStream in)83 public static FontConfig parse(InputStream in) throws XmlPullParserException, IOException { 84 XmlPullParser parser = Xml.newPullParser(); 85 parser.setInput(in, null); 86 parser.nextTag(); 87 return readFamilies(parser, "/system/fonts/", new FontCustomizationParser.Result(), null, 88 0, 0, true); 89 } 90 91 /** 92 * Parses system font config XMLs 93 * 94 * @param fontsXmlPath location of fonts.xml 95 * @param systemFontDir location of system font directory 96 * @param oemCustomizationXmlPath location of oem_customization.xml 97 * @param productFontDir location of oem customized font directory 98 * @param updatableFontMap map of updated font files. 99 * @return font configuration 100 * @throws IOException 101 * @throws XmlPullParserException 102 */ parse( @onNull String fontsXmlPath, @NonNull String systemFontDir, @Nullable String oemCustomizationXmlPath, @Nullable String productFontDir, @Nullable Map<String, File> updatableFontMap, long lastModifiedDate, int configVersion )103 public static FontConfig parse( 104 @NonNull String fontsXmlPath, 105 @NonNull String systemFontDir, 106 @Nullable String oemCustomizationXmlPath, 107 @Nullable String productFontDir, 108 @Nullable Map<String, File> updatableFontMap, 109 long lastModifiedDate, 110 int configVersion 111 ) throws IOException, XmlPullParserException { 112 FontCustomizationParser.Result oemCustomization; 113 if (oemCustomizationXmlPath != null) { 114 try (InputStream is = new FileInputStream(oemCustomizationXmlPath)) { 115 oemCustomization = FontCustomizationParser.parse(is, productFontDir, 116 updatableFontMap); 117 } catch (IOException e) { 118 // OEM customization may not exists. Ignoring 119 oemCustomization = new FontCustomizationParser.Result(); 120 } 121 } else { 122 oemCustomization = new FontCustomizationParser.Result(); 123 } 124 125 try (InputStream is = new FileInputStream(fontsXmlPath)) { 126 XmlPullParser parser = Xml.newPullParser(); 127 parser.setInput(is, null); 128 parser.nextTag(); 129 return readFamilies(parser, systemFontDir, oemCustomization, updatableFontMap, 130 lastModifiedDate, configVersion, false /* filter out the non-existing files */); 131 } 132 } 133 134 /** 135 * Parses the familyset tag in font.xml 136 * @param parser a XML pull parser 137 * @param fontDir A system font directory, e.g. "/system/fonts" 138 * @param customization A OEM font customization 139 * @param updatableFontMap A map of updated font files 140 * @param lastModifiedDate A date that the system font is updated. 141 * @param configVersion A version of system font config. 142 * @param allowNonExistingFile true if allowing non-existing font files during parsing fonts.xml 143 * @return result of fonts.xml 144 * 145 * @throws XmlPullParserException 146 * @throws IOException 147 * 148 * @hide 149 */ readFamilies( @onNull XmlPullParser parser, @NonNull String fontDir, @NonNull FontCustomizationParser.Result customization, @Nullable Map<String, File> updatableFontMap, long lastModifiedDate, int configVersion, boolean allowNonExistingFile)150 public static FontConfig readFamilies( 151 @NonNull XmlPullParser parser, 152 @NonNull String fontDir, 153 @NonNull FontCustomizationParser.Result customization, 154 @Nullable Map<String, File> updatableFontMap, 155 long lastModifiedDate, 156 int configVersion, 157 boolean allowNonExistingFile) 158 throws XmlPullParserException, IOException { 159 List<FontConfig.FontFamily> families = new ArrayList<>(); 160 List<FontConfig.NamedFamilyList> resultNamedFamilies = new ArrayList<>(); 161 List<FontConfig.Alias> aliases = new ArrayList<>(customization.getAdditionalAliases()); 162 163 Map<String, NamedFamilyList> oemNamedFamilies = 164 customization.getAdditionalNamedFamilies(); 165 166 boolean firstFamily = true; 167 parser.require(XmlPullParser.START_TAG, null, "familyset"); 168 while (keepReading(parser)) { 169 if (parser.getEventType() != XmlPullParser.START_TAG) continue; 170 String tag = parser.getName(); 171 if (tag.equals("family")) { 172 final String name = parser.getAttributeValue(null, "name"); 173 if (name == null) { 174 FontConfig.FontFamily family = readFamily(parser, fontDir, updatableFontMap, 175 allowNonExistingFile); 176 if (family == null) { 177 continue; 178 } 179 families.add(family); 180 181 } else { 182 FontConfig.NamedFamilyList namedFamilyList = readNamedFamily( 183 parser, fontDir, updatableFontMap, allowNonExistingFile); 184 if (namedFamilyList == null) { 185 continue; 186 } 187 if (!oemNamedFamilies.containsKey(name)) { 188 // The OEM customization overrides system named family. Skip if OEM 189 // customization XML defines the same named family. 190 resultNamedFamilies.add(namedFamilyList); 191 } 192 if (firstFamily) { 193 // The first font family is used as a fallback family as well. 194 families.addAll(namedFamilyList.getFamilies()); 195 } 196 } 197 firstFamily = false; 198 } else if (tag.equals("family-list")) { 199 FontConfig.NamedFamilyList namedFamilyList = readNamedFamilyList( 200 parser, fontDir, updatableFontMap, allowNonExistingFile); 201 if (namedFamilyList == null) { 202 continue; 203 } 204 if (!oemNamedFamilies.containsKey(namedFamilyList.getName())) { 205 // The OEM customization overrides system named family. Skip if OEM 206 // customization XML defines the same named family. 207 resultNamedFamilies.add(namedFamilyList); 208 } 209 if (firstFamily) { 210 // The first font family is used as a fallback family as well. 211 families.addAll(namedFamilyList.getFamilies()); 212 } 213 firstFamily = false; 214 } else if (tag.equals("alias")) { 215 aliases.add(readAlias(parser)); 216 } else { 217 skip(parser); 218 } 219 } 220 221 resultNamedFamilies.addAll(oemNamedFamilies.values()); 222 223 // Filters aliases that point to non-existing families. 224 Set<String> namedFamilies = new ArraySet<>(); 225 for (int i = 0; i < resultNamedFamilies.size(); ++i) { 226 String name = resultNamedFamilies.get(i).getName(); 227 if (name != null) { 228 namedFamilies.add(name); 229 } 230 } 231 List<FontConfig.Alias> filtered = new ArrayList<>(); 232 for (int i = 0; i < aliases.size(); ++i) { 233 FontConfig.Alias alias = aliases.get(i); 234 if (namedFamilies.contains(alias.getOriginal())) { 235 filtered.add(alias); 236 } 237 } 238 239 return new FontConfig(families, filtered, resultNamedFamilies, 240 customization.getLocaleFamilyCustomizations(), 241 lastModifiedDate, 242 configVersion); 243 } 244 keepReading(XmlPullParser parser)245 private static boolean keepReading(XmlPullParser parser) 246 throws XmlPullParserException, IOException { 247 int next = parser.next(); 248 return next != XmlPullParser.END_TAG && next != XmlPullParser.END_DOCUMENT; 249 } 250 251 /** 252 * Read family tag in fonts.xml or oem_customization.xml 253 * 254 * @param parser An XML parser. 255 * @param fontDir a font directory name. 256 * @param updatableFontMap a updated font file map. 257 * @param allowNonExistingFile true to allow font file that doesn't exist. 258 * @return a FontFamily instance. null if no font files are available in this FontFamily. 259 */ readFamily(XmlPullParser parser, String fontDir, @Nullable Map<String, File> updatableFontMap, boolean allowNonExistingFile)260 public static @Nullable FontConfig.FontFamily readFamily(XmlPullParser parser, String fontDir, 261 @Nullable Map<String, File> updatableFontMap, boolean allowNonExistingFile) 262 throws XmlPullParserException, IOException { 263 final String lang = parser.getAttributeValue("", "lang"); 264 final String variant = parser.getAttributeValue(null, "variant"); 265 final String ignore = parser.getAttributeValue(null, "ignore"); 266 final List<FontConfig.Font> fonts = new ArrayList<>(); 267 while (keepReading(parser)) { 268 if (parser.getEventType() != XmlPullParser.START_TAG) continue; 269 final String tag = parser.getName(); 270 if (tag.equals(TAG_FONT)) { 271 FontConfig.Font font = readFont(parser, fontDir, updatableFontMap, 272 allowNonExistingFile); 273 if (font != null) { 274 fonts.add(font); 275 } 276 } else { 277 skip(parser); 278 } 279 } 280 int intVariant = FontConfig.FontFamily.VARIANT_DEFAULT; 281 if (variant != null) { 282 if (variant.equals(VARIANT_COMPACT)) { 283 intVariant = FontConfig.FontFamily.VARIANT_COMPACT; 284 } else if (variant.equals(VARIANT_ELEGANT)) { 285 intVariant = FontConfig.FontFamily.VARIANT_ELEGANT; 286 } 287 } 288 289 boolean skip = (ignore != null && (ignore.equals("true") || ignore.equals("1"))); 290 if (skip || fonts.isEmpty()) { 291 return null; 292 } 293 return new FontConfig.FontFamily(fonts, LocaleList.forLanguageTags(lang), intVariant); 294 } 295 throwIfAttributeExists(String attrName, XmlPullParser parser)296 private static void throwIfAttributeExists(String attrName, XmlPullParser parser) { 297 if (parser.getAttributeValue(null, attrName) != null) { 298 throw new IllegalArgumentException(attrName + " cannot be used in FontFamily inside " 299 + " family or family-list with name attribute."); 300 } 301 } 302 303 /** 304 * Read a font family with name attribute as a single element family-list element. 305 */ readNamedFamily( @onNull XmlPullParser parser, @NonNull String fontDir, @Nullable Map<String, File> updatableFontMap, boolean allowNonExistingFile)306 public static @Nullable FontConfig.NamedFamilyList readNamedFamily( 307 @NonNull XmlPullParser parser, @NonNull String fontDir, 308 @Nullable Map<String, File> updatableFontMap, boolean allowNonExistingFile) 309 throws XmlPullParserException, IOException { 310 final String name = parser.getAttributeValue(null, "name"); 311 throwIfAttributeExists("lang", parser); 312 throwIfAttributeExists("variant", parser); 313 throwIfAttributeExists("ignore", parser); 314 315 final FontConfig.FontFamily family = readFamily(parser, fontDir, updatableFontMap, 316 allowNonExistingFile); 317 if (family == null) { 318 return null; 319 } 320 return new NamedFamilyList(Collections.singletonList(family), name); 321 } 322 323 /** 324 * Read a family-list element 325 */ readNamedFamilyList( @onNull XmlPullParser parser, @NonNull String fontDir, @Nullable Map<String, File> updatableFontMap, boolean allowNonExistingFile)326 public static @Nullable FontConfig.NamedFamilyList readNamedFamilyList( 327 @NonNull XmlPullParser parser, @NonNull String fontDir, 328 @Nullable Map<String, File> updatableFontMap, boolean allowNonExistingFile) 329 throws XmlPullParserException, IOException { 330 final String name = parser.getAttributeValue(null, "name"); 331 final List<FontConfig.FontFamily> familyList = new ArrayList<>(); 332 while (keepReading(parser)) { 333 if (parser.getEventType() != XmlPullParser.START_TAG) continue; 334 final String tag = parser.getName(); 335 if (tag.equals("family")) { 336 throwIfAttributeExists("name", parser); 337 throwIfAttributeExists("lang", parser); 338 throwIfAttributeExists("variant", parser); 339 throwIfAttributeExists("ignore", parser); 340 341 final FontConfig.FontFamily family = readFamily(parser, fontDir, updatableFontMap, 342 allowNonExistingFile); 343 if (family != null) { 344 familyList.add(family); 345 } 346 } else { 347 skip(parser); 348 } 349 } 350 351 if (familyList.isEmpty()) { 352 return null; 353 } 354 return new FontConfig.NamedFamilyList(familyList, name); 355 } 356 357 /** Matches leading and trailing XML whitespace. */ 358 private static final Pattern FILENAME_WHITESPACE_PATTERN = 359 Pattern.compile("^[ \\n\\r\\t]+|[ \\n\\r\\t]+$"); 360 readFont( @onNull XmlPullParser parser, @NonNull String fontDir, @Nullable Map<String, File> updatableFontMap, boolean allowNonExistingFile)361 private static @Nullable FontConfig.Font readFont( 362 @NonNull XmlPullParser parser, 363 @NonNull String fontDir, 364 @Nullable Map<String, File> updatableFontMap, 365 boolean allowNonExistingFile) 366 throws XmlPullParserException, IOException { 367 368 String indexStr = parser.getAttributeValue(null, ATTR_INDEX); 369 int index = indexStr == null ? 0 : Integer.parseInt(indexStr); 370 List<FontVariationAxis> axes = new ArrayList<>(); 371 String weightStr = parser.getAttributeValue(null, ATTR_WEIGHT); 372 int weight = weightStr == null ? FontStyle.FONT_WEIGHT_NORMAL : Integer.parseInt(weightStr); 373 boolean isItalic = STYLE_ITALIC.equals(parser.getAttributeValue(null, ATTR_STYLE)); 374 String fallbackFor = parser.getAttributeValue(null, ATTR_FALLBACK_FOR); 375 String postScriptName = parser.getAttributeValue(null, ATTR_POSTSCRIPT_NAME); 376 final String supportedAxes = parser.getAttributeValue(null, ATTR_SUPPORTED_AXES); 377 StringBuilder filename = new StringBuilder(); 378 while (keepReading(parser)) { 379 if (parser.getEventType() == XmlPullParser.TEXT) { 380 filename.append(parser.getText()); 381 } 382 if (parser.getEventType() != XmlPullParser.START_TAG) continue; 383 String tag = parser.getName(); 384 if (tag.equals(TAG_AXIS)) { 385 axes.add(readAxis(parser)); 386 } else { 387 skip(parser); 388 } 389 } 390 String sanitizedName = FILENAME_WHITESPACE_PATTERN.matcher(filename).replaceAll(""); 391 392 int varTypeAxes = 0; 393 if (supportedAxes != null) { 394 for (String tag : supportedAxes.split(",")) { 395 String strippedTag = tag.strip(); 396 if (strippedTag.equals(TAG_WGHT)) { 397 varTypeAxes |= FontConfig.Font.VAR_TYPE_AXES_WGHT; 398 } else if (strippedTag.equals(TAG_ITAL)) { 399 varTypeAxes |= FontConfig.Font.VAR_TYPE_AXES_ITAL; 400 } 401 } 402 } 403 404 if (postScriptName == null) { 405 // If post script name was not provided, assume the file name is same to PostScript 406 // name. 407 postScriptName = sanitizedName.substring(0, sanitizedName.length() - 4); 408 } 409 410 String updatedName = findUpdatedFontFile(postScriptName, updatableFontMap); 411 String filePath; 412 String originalPath; 413 if (updatedName != null) { 414 filePath = updatedName; 415 originalPath = fontDir + sanitizedName; 416 } else { 417 filePath = fontDir + sanitizedName; 418 originalPath = null; 419 } 420 421 String varSettings; 422 if (axes.isEmpty()) { 423 varSettings = ""; 424 } else { 425 varSettings = FontVariationAxis.toFontVariationSettings( 426 axes.toArray(new FontVariationAxis[0])); 427 } 428 429 File file = new File(filePath); 430 431 if (!(allowNonExistingFile || file.isFile())) { 432 return null; 433 } 434 435 return new FontConfig.Font(file, 436 originalPath == null ? null : new File(originalPath), 437 postScriptName, 438 new FontStyle( 439 weight, 440 isItalic ? FontStyle.FONT_SLANT_ITALIC : FontStyle.FONT_SLANT_UPRIGHT 441 ), 442 index, 443 varSettings, 444 fallbackFor, 445 varTypeAxes); 446 } 447 findUpdatedFontFile(String psName, @Nullable Map<String, File> updatableFontMap)448 private static String findUpdatedFontFile(String psName, 449 @Nullable Map<String, File> updatableFontMap) { 450 if (updatableFontMap != null) { 451 File updatedFile = updatableFontMap.get(psName); 452 if (updatedFile != null) { 453 return updatedFile.getAbsolutePath(); 454 } 455 } 456 return null; 457 } 458 readAxis(XmlPullParser parser)459 private static FontVariationAxis readAxis(XmlPullParser parser) 460 throws XmlPullParserException, IOException { 461 String tagStr = parser.getAttributeValue(null, ATTR_TAG); 462 String styleValueStr = parser.getAttributeValue(null, ATTR_STYLEVALUE); 463 skip(parser); // axis tag is empty, ignore any contents and consume end tag 464 return new FontVariationAxis(tagStr, Float.parseFloat(styleValueStr)); 465 } 466 467 /** 468 * Reads alias elements 469 */ readAlias(XmlPullParser parser)470 public static FontConfig.Alias readAlias(XmlPullParser parser) 471 throws XmlPullParserException, IOException { 472 String name = parser.getAttributeValue(null, "name"); 473 String toName = parser.getAttributeValue(null, "to"); 474 String weightStr = parser.getAttributeValue(null, "weight"); 475 int weight; 476 if (weightStr == null) { 477 weight = FontStyle.FONT_WEIGHT_NORMAL; 478 } else { 479 weight = Integer.parseInt(weightStr); 480 } 481 skip(parser); // alias tag is empty, ignore any contents and consume end tag 482 return new FontConfig.Alias(name, toName, weight); 483 } 484 485 /** 486 * Skip until next element 487 */ skip(XmlPullParser parser)488 public static void skip(XmlPullParser parser) throws XmlPullParserException, IOException { 489 int depth = 1; 490 while (depth > 0) { 491 switch (parser.next()) { 492 case XmlPullParser.START_TAG: 493 depth++; 494 break; 495 case XmlPullParser.END_TAG: 496 depth--; 497 break; 498 case XmlPullParser.END_DOCUMENT: 499 return; 500 } 501 } 502 } 503 } 504