/* * 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 static android.text.FontConfig.NamedFamilyList; import android.annotation.NonNull; import android.annotation.Nullable; import android.compat.annotation.UnsupportedAppUsage; import android.graphics.fonts.FontCustomizationParser; import android.graphics.fonts.FontStyle; import android.graphics.fonts.FontVariationAxis; import android.os.Build; import android.os.LocaleList; import android.text.FontConfig; import android.util.ArraySet; import android.util.Xml; import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParserException; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Set; import java.util.regex.Pattern; /** * Parser for font config files. * @hide */ public class FontListParser { private static final String TAG = "FontListParser"; // XML constants for FontFamily. private static final String ATTR_NAME = "name"; private static final String ATTR_LANG = "lang"; private static final String ATTR_VARIANT = "variant"; private static final String TAG_FONT = "font"; private static final String VARIANT_COMPACT = "compact"; private static final String VARIANT_ELEGANT = "elegant"; // XML constants for Font. public static final String ATTR_SUPPORTED_AXES = "supportedAxes"; public static final String ATTR_INDEX = "index"; public static final String ATTR_WEIGHT = "weight"; public static final String ATTR_POSTSCRIPT_NAME = "postScriptName"; public static final String ATTR_STYLE = "style"; public static final String ATTR_FALLBACK_FOR = "fallbackFor"; public static final String STYLE_ITALIC = "italic"; public static final String STYLE_NORMAL = "normal"; public static final String TAG_AXIS = "axis"; // XML constants for FontVariationAxis. public static final String ATTR_TAG = "tag"; public static final String ATTR_STYLEVALUE = "stylevalue"; // The tag string for variable font type resolution. private static final String TAG_WGHT = "wght"; private static final String TAG_ITAL = "ital"; /* Parse fallback list (no names) */ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) public static FontConfig parse(InputStream in) throws XmlPullParserException, IOException { XmlPullParser parser = Xml.newPullParser(); parser.setInput(in, null); parser.nextTag(); return readFamilies(parser, "/system/fonts/", new FontCustomizationParser.Result(), null, 0, 0, true); } /** * Parses system font config XMLs * * @param fontsXmlPath location of fonts.xml * @param systemFontDir location of system font directory * @param oemCustomizationXmlPath location of oem_customization.xml * @param productFontDir location of oem customized font directory * @param updatableFontMap map of updated font files. * @return font configuration * @throws IOException * @throws XmlPullParserException */ public static FontConfig parse( @NonNull String fontsXmlPath, @NonNull String systemFontDir, @Nullable String oemCustomizationXmlPath, @Nullable String productFontDir, @Nullable Map updatableFontMap, long lastModifiedDate, int configVersion ) throws IOException, XmlPullParserException { FontCustomizationParser.Result oemCustomization; if (oemCustomizationXmlPath != null) { try (InputStream is = new FileInputStream(oemCustomizationXmlPath)) { oemCustomization = FontCustomizationParser.parse(is, productFontDir, updatableFontMap); } catch (IOException e) { // OEM customization may not exists. Ignoring oemCustomization = new FontCustomizationParser.Result(); } } else { oemCustomization = new FontCustomizationParser.Result(); } try (InputStream is = new FileInputStream(fontsXmlPath)) { XmlPullParser parser = Xml.newPullParser(); parser.setInput(is, null); parser.nextTag(); return readFamilies(parser, systemFontDir, oemCustomization, updatableFontMap, lastModifiedDate, configVersion, false /* filter out the non-existing files */); } } /** * Parses the familyset tag in font.xml * @param parser a XML pull parser * @param fontDir A system font directory, e.g. "/system/fonts" * @param customization A OEM font customization * @param updatableFontMap A map of updated font files * @param lastModifiedDate A date that the system font is updated. * @param configVersion A version of system font config. * @param allowNonExistingFile true if allowing non-existing font files during parsing fonts.xml * @return result of fonts.xml * * @throws XmlPullParserException * @throws IOException * * @hide */ public static FontConfig readFamilies( @NonNull XmlPullParser parser, @NonNull String fontDir, @NonNull FontCustomizationParser.Result customization, @Nullable Map updatableFontMap, long lastModifiedDate, int configVersion, boolean allowNonExistingFile) throws XmlPullParserException, IOException { List families = new ArrayList<>(); List resultNamedFamilies = new ArrayList<>(); List aliases = new ArrayList<>(customization.getAdditionalAliases()); Map oemNamedFamilies = customization.getAdditionalNamedFamilies(); boolean firstFamily = true; parser.require(XmlPullParser.START_TAG, null, "familyset"); while (keepReading(parser)) { if (parser.getEventType() != XmlPullParser.START_TAG) continue; String tag = parser.getName(); if (tag.equals("family")) { final String name = parser.getAttributeValue(null, "name"); if (name == null) { FontConfig.FontFamily family = readFamily(parser, fontDir, updatableFontMap, allowNonExistingFile); if (family == null) { continue; } families.add(family); } else { FontConfig.NamedFamilyList namedFamilyList = readNamedFamily( parser, fontDir, updatableFontMap, allowNonExistingFile); if (namedFamilyList == null) { continue; } if (!oemNamedFamilies.containsKey(name)) { // The OEM customization overrides system named family. Skip if OEM // customization XML defines the same named family. resultNamedFamilies.add(namedFamilyList); } if (firstFamily) { // The first font family is used as a fallback family as well. families.addAll(namedFamilyList.getFamilies()); } } firstFamily = false; } else if (tag.equals("family-list")) { FontConfig.NamedFamilyList namedFamilyList = readNamedFamilyList( parser, fontDir, updatableFontMap, allowNonExistingFile); if (namedFamilyList == null) { continue; } if (!oemNamedFamilies.containsKey(namedFamilyList.getName())) { // The OEM customization overrides system named family. Skip if OEM // customization XML defines the same named family. resultNamedFamilies.add(namedFamilyList); } if (firstFamily) { // The first font family is used as a fallback family as well. families.addAll(namedFamilyList.getFamilies()); } firstFamily = false; } else if (tag.equals("alias")) { aliases.add(readAlias(parser)); } else { skip(parser); } } resultNamedFamilies.addAll(oemNamedFamilies.values()); // Filters aliases that point to non-existing families. Set namedFamilies = new ArraySet<>(); for (int i = 0; i < resultNamedFamilies.size(); ++i) { String name = resultNamedFamilies.get(i).getName(); if (name != null) { namedFamilies.add(name); } } List filtered = new ArrayList<>(); for (int i = 0; i < aliases.size(); ++i) { FontConfig.Alias alias = aliases.get(i); if (namedFamilies.contains(alias.getOriginal())) { filtered.add(alias); } } return new FontConfig(families, filtered, resultNamedFamilies, customization.getLocaleFamilyCustomizations(), lastModifiedDate, configVersion); } private static boolean keepReading(XmlPullParser parser) throws XmlPullParserException, IOException { int next = parser.next(); return next != XmlPullParser.END_TAG && next != XmlPullParser.END_DOCUMENT; } /** * Read family tag in fonts.xml or oem_customization.xml * * @param parser An XML parser. * @param fontDir a font directory name. * @param updatableFontMap a updated font file map. * @param allowNonExistingFile true to allow font file that doesn't exist. * @return a FontFamily instance. null if no font files are available in this FontFamily. */ public static @Nullable FontConfig.FontFamily readFamily(XmlPullParser parser, String fontDir, @Nullable Map updatableFontMap, boolean allowNonExistingFile) throws XmlPullParserException, IOException { final String lang = parser.getAttributeValue("", "lang"); final String variant = parser.getAttributeValue(null, "variant"); final String ignore = parser.getAttributeValue(null, "ignore"); final List fonts = new ArrayList<>(); while (keepReading(parser)) { if (parser.getEventType() != XmlPullParser.START_TAG) continue; final String tag = parser.getName(); if (tag.equals(TAG_FONT)) { FontConfig.Font font = readFont(parser, fontDir, updatableFontMap, allowNonExistingFile); if (font != null) { fonts.add(font); } } else { skip(parser); } } int intVariant = FontConfig.FontFamily.VARIANT_DEFAULT; if (variant != null) { if (variant.equals(VARIANT_COMPACT)) { intVariant = FontConfig.FontFamily.VARIANT_COMPACT; } else if (variant.equals(VARIANT_ELEGANT)) { intVariant = FontConfig.FontFamily.VARIANT_ELEGANT; } } boolean skip = (ignore != null && (ignore.equals("true") || ignore.equals("1"))); if (skip || fonts.isEmpty()) { return null; } return new FontConfig.FontFamily(fonts, LocaleList.forLanguageTags(lang), intVariant); } private static void throwIfAttributeExists(String attrName, XmlPullParser parser) { if (parser.getAttributeValue(null, attrName) != null) { throw new IllegalArgumentException(attrName + " cannot be used in FontFamily inside " + " family or family-list with name attribute."); } } /** * Read a font family with name attribute as a single element family-list element. */ public static @Nullable FontConfig.NamedFamilyList readNamedFamily( @NonNull XmlPullParser parser, @NonNull String fontDir, @Nullable Map updatableFontMap, boolean allowNonExistingFile) throws XmlPullParserException, IOException { final String name = parser.getAttributeValue(null, "name"); throwIfAttributeExists("lang", parser); throwIfAttributeExists("variant", parser); throwIfAttributeExists("ignore", parser); final FontConfig.FontFamily family = readFamily(parser, fontDir, updatableFontMap, allowNonExistingFile); if (family == null) { return null; } return new NamedFamilyList(Collections.singletonList(family), name); } /** * Read a family-list element */ public static @Nullable FontConfig.NamedFamilyList readNamedFamilyList( @NonNull XmlPullParser parser, @NonNull String fontDir, @Nullable Map updatableFontMap, boolean allowNonExistingFile) throws XmlPullParserException, IOException { final String name = parser.getAttributeValue(null, "name"); final List familyList = new ArrayList<>(); while (keepReading(parser)) { if (parser.getEventType() != XmlPullParser.START_TAG) continue; final String tag = parser.getName(); if (tag.equals("family")) { throwIfAttributeExists("name", parser); throwIfAttributeExists("lang", parser); throwIfAttributeExists("variant", parser); throwIfAttributeExists("ignore", parser); final FontConfig.FontFamily family = readFamily(parser, fontDir, updatableFontMap, allowNonExistingFile); if (family != null) { familyList.add(family); } } else { skip(parser); } } if (familyList.isEmpty()) { return null; } return new FontConfig.NamedFamilyList(familyList, name); } /** Matches leading and trailing XML whitespace. */ private static final Pattern FILENAME_WHITESPACE_PATTERN = Pattern.compile("^[ \\n\\r\\t]+|[ \\n\\r\\t]+$"); private static @Nullable FontConfig.Font readFont( @NonNull XmlPullParser parser, @NonNull String fontDir, @Nullable Map updatableFontMap, boolean allowNonExistingFile) throws XmlPullParserException, IOException { String indexStr = parser.getAttributeValue(null, ATTR_INDEX); int index = indexStr == null ? 0 : Integer.parseInt(indexStr); List axes = new ArrayList<>(); String weightStr = parser.getAttributeValue(null, ATTR_WEIGHT); int weight = weightStr == null ? FontStyle.FONT_WEIGHT_NORMAL : Integer.parseInt(weightStr); boolean isItalic = STYLE_ITALIC.equals(parser.getAttributeValue(null, ATTR_STYLE)); String fallbackFor = parser.getAttributeValue(null, ATTR_FALLBACK_FOR); String postScriptName = parser.getAttributeValue(null, ATTR_POSTSCRIPT_NAME); final String supportedAxes = parser.getAttributeValue(null, ATTR_SUPPORTED_AXES); StringBuilder filename = new StringBuilder(); while (keepReading(parser)) { if (parser.getEventType() == XmlPullParser.TEXT) { filename.append(parser.getText()); } if (parser.getEventType() != XmlPullParser.START_TAG) continue; String tag = parser.getName(); if (tag.equals(TAG_AXIS)) { axes.add(readAxis(parser)); } else { skip(parser); } } String sanitizedName = FILENAME_WHITESPACE_PATTERN.matcher(filename).replaceAll(""); int varTypeAxes = 0; if (supportedAxes != null) { for (String tag : supportedAxes.split(",")) { String strippedTag = tag.strip(); if (strippedTag.equals(TAG_WGHT)) { varTypeAxes |= FontConfig.Font.VAR_TYPE_AXES_WGHT; } else if (strippedTag.equals(TAG_ITAL)) { varTypeAxes |= FontConfig.Font.VAR_TYPE_AXES_ITAL; } } } if (postScriptName == null) { // If post script name was not provided, assume the file name is same to PostScript // name. postScriptName = sanitizedName.substring(0, sanitizedName.length() - 4); } String updatedName = findUpdatedFontFile(postScriptName, updatableFontMap); String filePath; String originalPath; if (updatedName != null) { filePath = updatedName; originalPath = fontDir + sanitizedName; } else { filePath = fontDir + sanitizedName; originalPath = null; } String varSettings; if (axes.isEmpty()) { varSettings = ""; } else { varSettings = FontVariationAxis.toFontVariationSettings( axes.toArray(new FontVariationAxis[0])); } File file = new File(filePath); if (!(allowNonExistingFile || file.isFile())) { return null; } return new FontConfig.Font(file, originalPath == null ? null : new File(originalPath), postScriptName, new FontStyle( weight, isItalic ? FontStyle.FONT_SLANT_ITALIC : FontStyle.FONT_SLANT_UPRIGHT ), index, varSettings, fallbackFor, varTypeAxes); } private static String findUpdatedFontFile(String psName, @Nullable Map updatableFontMap) { if (updatableFontMap != null) { File updatedFile = updatableFontMap.get(psName); if (updatedFile != null) { return updatedFile.getAbsolutePath(); } } return null; } private static FontVariationAxis readAxis(XmlPullParser parser) throws XmlPullParserException, IOException { String tagStr = parser.getAttributeValue(null, ATTR_TAG); String styleValueStr = parser.getAttributeValue(null, ATTR_STYLEVALUE); skip(parser); // axis tag is empty, ignore any contents and consume end tag return new FontVariationAxis(tagStr, Float.parseFloat(styleValueStr)); } /** * Reads alias elements */ public static FontConfig.Alias readAlias(XmlPullParser parser) throws XmlPullParserException, IOException { String name = parser.getAttributeValue(null, "name"); String toName = parser.getAttributeValue(null, "to"); String weightStr = parser.getAttributeValue(null, "weight"); int weight; if (weightStr == null) { weight = FontStyle.FONT_WEIGHT_NORMAL; } else { weight = Integer.parseInt(weightStr); } skip(parser); // alias tag is empty, ignore any contents and consume end tag return new FontConfig.Alias(name, toName, weight); } /** * Skip until next element */ public 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; case XmlPullParser.END_DOCUMENT: return; } } } }