/*
* Copyright (C) 2008 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 com.android.layoutlib.bridge.impl;
import org.xml.sax.Attributes;
import org.xml.sax.SAXException;
import org.xml.sax.helpers.DefaultHandler;
import android.graphics.Typeface;
import java.awt.Font;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;
/**
* Provides {@link Font} object to the layout lib.
*
* The fonts are loaded from the SDK directory. Family/style mapping is done by parsing the
* fonts.xml file located alongside the ttf files.
*/
public final class FontLoader {
private static final String FONTS_SYSTEM = "system_fonts.xml";
private static final String FONTS_VENDOR = "vendor_fonts.xml";
private static final String FONTS_FALLBACK = "fallback_fonts.xml";
private static final String NODE_FAMILYSET = "familyset";
private static final String NODE_FAMILY = "family";
private static final String NODE_NAME = "name";
private static final String NODE_FILE = "file";
private static final String ATTRIBUTE_VARIANT = "variant";
private static final String ATTRIBUTE_VALUE_ELEGANT = "elegant";
private static final String FONT_SUFFIX_NONE = ".ttf";
private static final String FONT_SUFFIX_REGULAR = "-Regular.ttf";
private static final String FONT_SUFFIX_BOLD = "-Bold.ttf";
private static final String FONT_SUFFIX_ITALIC = "-Italic.ttf";
private static final String FONT_SUFFIX_BOLDITALIC = "-BoldItalic.ttf";
// This must match the values of Typeface styles so that we can use them for indices in this
// array.
private static final int[] AWT_STYLES = new int[] {
Font.PLAIN,
Font.BOLD,
Font.ITALIC,
Font.BOLD | Font.ITALIC
};
private static int[] DERIVE_BOLD_ITALIC = new int[] {
Typeface.ITALIC, Typeface.BOLD, Typeface.NORMAL
};
private static int[] DERIVE_ITALIC = new int[] { Typeface.NORMAL };
private static int[] DERIVE_BOLD = new int[] { Typeface.NORMAL };
private static final List mMainFonts = new ArrayList();
private static final List mFallbackFonts = new ArrayList();
private final String mOsFontsLocation;
public static FontLoader create(String fontOsLocation) {
try {
SAXParserFactory parserFactory = SAXParserFactory.newInstance();
parserFactory.setNamespaceAware(true);
// parse the system fonts
FontHandler handler = parseFontFile(parserFactory, fontOsLocation, FONTS_SYSTEM);
List systemFonts = handler.getFontList();
// parse the fallback fonts
handler = parseFontFile(parserFactory, fontOsLocation, FONTS_FALLBACK);
List fallbackFonts = handler.getFontList();
return new FontLoader(fontOsLocation, systemFonts, fallbackFonts);
} catch (ParserConfigurationException e) {
// return null below
} catch (SAXException e) {
// return null below
} catch (FileNotFoundException e) {
// return null below
} catch (IOException e) {
// return null below
}
return null;
}
private static FontHandler parseFontFile(SAXParserFactory parserFactory,
String fontOsLocation, String fontFileName)
throws ParserConfigurationException, SAXException, IOException, FileNotFoundException {
SAXParser parser = parserFactory.newSAXParser();
File f = new File(fontOsLocation, fontFileName);
FontHandler definitionParser = new FontHandler(
fontOsLocation + File.separator);
parser.parse(new FileInputStream(f), definitionParser);
return definitionParser;
}
private FontLoader(String fontOsLocation,
List fontList, List fallBackList) {
mOsFontsLocation = fontOsLocation;
mMainFonts.addAll(fontList);
mFallbackFonts.addAll(fallBackList);
}
public String getOsFontsLocation() {
return mOsFontsLocation;
}
/**
* Returns a {@link Font} object given a family name and a style value (constant in
* {@link Typeface}).
* @param family the family name
* @param style a 1-item array containing the requested style. Based on the font being read
* the actual style may be different. The array contains the actual style after
* the method returns.
* @return the font object or null if no match could be found.
*/
public synchronized List getFont(String family, int style) {
List result = new ArrayList();
if (family == null) {
return result;
}
// get the font objects from the main list based on family.
for (FontInfo info : mMainFonts) {
if (info.families.contains(family)) {
result.add(info.font[style]);
break;
}
}
// add all the fallback fonts for the given style
for (FontInfo info : mFallbackFonts) {
result.add(info.font[style]);
}
return result;
}
public synchronized List getFallbackFonts(int style) {
List result = new ArrayList();
// add all the fallback fonts
for (FontInfo info : mFallbackFonts) {
result.add(info.font[style]);
}
return result;
}
private final static class FontInfo {
final Font[] font = new Font[4]; // Matches the 4 type-face styles.
final Set families;
FontInfo() {
families = new HashSet();
}
}
private final static class FontHandler extends DefaultHandler {
private final String mOsFontsLocation;
private FontInfo mFontInfo = null;
private final StringBuilder mBuilder = new StringBuilder();
private List mFontList = new ArrayList();
private boolean isCompactFont = true;
private FontHandler(String osFontsLocation) {
super();
mOsFontsLocation = osFontsLocation;
}
public List getFontList() {
return mFontList;
}
/* (non-Javadoc)
* @see org.xml.sax.helpers.DefaultHandler#startElement(java.lang.String, java.lang.String, java.lang.String, org.xml.sax.Attributes)
*/
@Override
public void startElement(String uri, String localName, String name, Attributes attributes)
throws SAXException {
if (NODE_FAMILYSET.equals(localName)) {
mFontList = new ArrayList();
} else if (NODE_FAMILY.equals(localName)) {
if (mFontList != null) {
mFontInfo = null;
}
} else if (NODE_NAME.equals(localName)) {
if (mFontList != null && mFontInfo == null) {
mFontInfo = new FontInfo();
}
} else if (NODE_FILE.equals(localName)) {
if (mFontList != null && mFontInfo == null) {
mFontInfo = new FontInfo();
}
if (ATTRIBUTE_VALUE_ELEGANT.equals(attributes.getValue(ATTRIBUTE_VARIANT))) {
isCompactFont = false;
} else {
isCompactFont = true;
}
}
mBuilder.setLength(0);
super.startElement(uri, localName, name, attributes);
}
/* (non-Javadoc)
* @see org.xml.sax.helpers.DefaultHandler#characters(char[], int, int)
*/
@Override
public void characters(char[] ch, int start, int length) throws SAXException {
if (isCompactFont) {
mBuilder.append(ch, start, length);
}
}
/* (non-Javadoc)
* @see org.xml.sax.helpers.DefaultHandler#endElement(java.lang.String, java.lang.String, java.lang.String)
*/
@Override
public void endElement(String uri, String localName, String name) throws SAXException {
if (NODE_FAMILY.equals(localName)) {
if (mFontInfo != null) {
// if has a normal font file, add to the list
if (mFontInfo.font[Typeface.NORMAL] != null) {
mFontList.add(mFontInfo);
// create missing font styles, order is important.
if (mFontInfo.font[Typeface.BOLD_ITALIC] == null) {
computeDerivedFont(Typeface.BOLD_ITALIC, DERIVE_BOLD_ITALIC);
}
if (mFontInfo.font[Typeface.ITALIC] == null) {
computeDerivedFont(Typeface.ITALIC, DERIVE_ITALIC);
}
if (mFontInfo.font[Typeface.BOLD] == null) {
computeDerivedFont(Typeface.BOLD, DERIVE_BOLD);
}
}
mFontInfo = null;
}
} else if (NODE_NAME.equals(localName)) {
// handle a new name for an existing Font Info
if (mFontInfo != null) {
String family = trimXmlWhitespaces(mBuilder.toString());
mFontInfo.families.add(family);
}
} else if (NODE_FILE.equals(localName)) {
// handle a new file for an existing Font Info
if (isCompactFont && mFontInfo != null) {
String fileName = trimXmlWhitespaces(mBuilder.toString());
Font font = getFont(fileName);
if (font != null) {
if (fileName.endsWith(FONT_SUFFIX_REGULAR)) {
mFontInfo.font[Typeface.NORMAL] = font;
} else if (fileName.endsWith(FONT_SUFFIX_BOLD)) {
mFontInfo.font[Typeface.BOLD] = font;
} else if (fileName.endsWith(FONT_SUFFIX_ITALIC)) {
mFontInfo.font[Typeface.ITALIC] = font;
} else if (fileName.endsWith(FONT_SUFFIX_BOLDITALIC)) {
mFontInfo.font[Typeface.BOLD_ITALIC] = font;
} else if (fileName.endsWith(FONT_SUFFIX_NONE)) {
mFontInfo.font[Typeface.NORMAL] = font;
}
}
}
}
}
private Font getFont(String fileName) {
try {
File file = new File(mOsFontsLocation, fileName);
if (file.exists()) {
return Font.createFont(Font.TRUETYPE_FONT, file);
}
} catch (Exception e) {
}
return null;
}
private void computeDerivedFont( int toCompute, int[] basedOnList) {
for (int basedOn : basedOnList) {
if (mFontInfo.font[basedOn] != null) {
mFontInfo.font[toCompute] =
mFontInfo.font[basedOn].deriveFont(AWT_STYLES[toCompute]);
return;
}
}
// we really shouldn't stop there. This means we don't have a NORMAL font...
assert false;
}
private String trimXmlWhitespaces(String value) {
if (value == null) {
return null;
}
// look for carriage return and replace all whitespace around it by just 1 space.
int index;
while ((index = value.indexOf('\n')) != -1) {
// look for whitespace on each side
int left = index - 1;
while (left >= 0) {
if (Character.isWhitespace(value.charAt(left))) {
left--;
} else {
break;
}
}
int right = index + 1;
int count = value.length();
while (right < count) {
if (Character.isWhitespace(value.charAt(right))) {
right++;
} else {
break;
}
}
// remove all between left and right (non inclusive) and replace by a single space.
String leftString = null;
if (left >= 0) {
leftString = value.substring(0, left + 1);
}
String rightString = null;
if (right < count) {
rightString = value.substring(right);
}
if (leftString != null) {
value = leftString;
if (rightString != null) {
value += " " + rightString;
}
} else {
value = rightString != null ? rightString : "";
}
}
// now we un-escape the string
int length = value.length();
char[] buffer = value.toCharArray();
for (int i = 0 ; i < length ; i++) {
if (buffer[i] == '\\') {
if (buffer[i+1] == 'n') {
// replace the char with \n
buffer[i+1] = '\n';
}
// offset the rest of the buffer since we go from 2 to 1 char
System.arraycopy(buffer, i+1, buffer, i, length - i - 1);
length--;
}
}
return new String(buffer, 0, length);
}
}
}