504 lines
20 KiB
Java
504 lines
20 KiB
Java
/*
|
|
* 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<String, File> 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<String, File> updatableFontMap,
|
|
long lastModifiedDate,
|
|
int configVersion,
|
|
boolean allowNonExistingFile)
|
|
throws XmlPullParserException, IOException {
|
|
List<FontConfig.FontFamily> families = new ArrayList<>();
|
|
List<FontConfig.NamedFamilyList> resultNamedFamilies = new ArrayList<>();
|
|
List<FontConfig.Alias> aliases = new ArrayList<>(customization.getAdditionalAliases());
|
|
|
|
Map<String, NamedFamilyList> 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<String> namedFamilies = new ArraySet<>();
|
|
for (int i = 0; i < resultNamedFamilies.size(); ++i) {
|
|
String name = resultNamedFamilies.get(i).getName();
|
|
if (name != null) {
|
|
namedFamilies.add(name);
|
|
}
|
|
}
|
|
List<FontConfig.Alias> 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<String, File> 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<FontConfig.Font> 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<String, File> 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<String, File> updatableFontMap, boolean allowNonExistingFile)
|
|
throws XmlPullParserException, IOException {
|
|
final String name = parser.getAttributeValue(null, "name");
|
|
final List<FontConfig.FontFamily> 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<String, File> updatableFontMap,
|
|
boolean allowNonExistingFile)
|
|
throws XmlPullParserException, IOException {
|
|
|
|
String indexStr = parser.getAttributeValue(null, ATTR_INDEX);
|
|
int index = indexStr == null ? 0 : Integer.parseInt(indexStr);
|
|
List<FontVariationAxis> 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<String, File> 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;
|
|
}
|
|
}
|
|
}
|
|
}
|