1/* 2 * Copyright (C) 2008 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 17package com.android.layoutlib.bridge.impl; 18 19import org.xml.sax.Attributes; 20import org.xml.sax.SAXException; 21import org.xml.sax.helpers.DefaultHandler; 22 23import android.graphics.Typeface; 24 25import java.awt.Font; 26import java.io.File; 27import java.io.FileInputStream; 28import java.io.FileNotFoundException; 29import java.io.IOException; 30import java.util.ArrayList; 31import java.util.HashSet; 32import java.util.List; 33import java.util.Set; 34 35import javax.xml.parsers.ParserConfigurationException; 36import javax.xml.parsers.SAXParser; 37import javax.xml.parsers.SAXParserFactory; 38 39/** 40 * Provides {@link Font} object to the layout lib. 41 * <p/> 42 * The fonts are loaded from the SDK directory. Family/style mapping is done by parsing the 43 * fonts.xml file located alongside the ttf files. 44 */ 45public final class FontLoader { 46 private static final String FONTS_SYSTEM = "system_fonts.xml"; 47 private static final String FONTS_VENDOR = "vendor_fonts.xml"; 48 private static final String FONTS_FALLBACK = "fallback_fonts.xml"; 49 50 private static final String NODE_FAMILYSET = "familyset"; 51 private static final String NODE_FAMILY = "family"; 52 private static final String NODE_NAME = "name"; 53 private static final String NODE_FILE = "file"; 54 55 private static final String ATTRIBUTE_VARIANT = "variant"; 56 private static final String ATTRIBUTE_VALUE_ELEGANT = "elegant"; 57 private static final String FONT_SUFFIX_NONE = ".ttf"; 58 private static final String FONT_SUFFIX_REGULAR = "-Regular.ttf"; 59 private static final String FONT_SUFFIX_BOLD = "-Bold.ttf"; 60 private static final String FONT_SUFFIX_ITALIC = "-Italic.ttf"; 61 private static final String FONT_SUFFIX_BOLDITALIC = "-BoldItalic.ttf"; 62 63 // This must match the values of Typeface styles so that we can use them for indices in this 64 // array. 65 private static final int[] AWT_STYLES = new int[] { 66 Font.PLAIN, 67 Font.BOLD, 68 Font.ITALIC, 69 Font.BOLD | Font.ITALIC 70 }; 71 private static int[] DERIVE_BOLD_ITALIC = new int[] { 72 Typeface.ITALIC, Typeface.BOLD, Typeface.NORMAL 73 }; 74 private static int[] DERIVE_ITALIC = new int[] { Typeface.NORMAL }; 75 private static int[] DERIVE_BOLD = new int[] { Typeface.NORMAL }; 76 77 private static final List<FontInfo> mMainFonts = new ArrayList<FontInfo>(); 78 private static final List<FontInfo> mFallbackFonts = new ArrayList<FontInfo>(); 79 80 private final String mOsFontsLocation; 81 82 public static FontLoader create(String fontOsLocation) { 83 try { 84 SAXParserFactory parserFactory = SAXParserFactory.newInstance(); 85 parserFactory.setNamespaceAware(true); 86 87 // parse the system fonts 88 FontHandler handler = parseFontFile(parserFactory, fontOsLocation, FONTS_SYSTEM); 89 List<FontInfo> systemFonts = handler.getFontList(); 90 91 92 // parse the fallback fonts 93 handler = parseFontFile(parserFactory, fontOsLocation, FONTS_FALLBACK); 94 List<FontInfo> fallbackFonts = handler.getFontList(); 95 96 return new FontLoader(fontOsLocation, systemFonts, fallbackFonts); 97 } catch (ParserConfigurationException e) { 98 // return null below 99 } catch (SAXException e) { 100 // return null below 101 } catch (FileNotFoundException e) { 102 // return null below 103 } catch (IOException e) { 104 // return null below 105 } 106 107 return null; 108 } 109 110 private static FontHandler parseFontFile(SAXParserFactory parserFactory, 111 String fontOsLocation, String fontFileName) 112 throws ParserConfigurationException, SAXException, IOException, FileNotFoundException { 113 114 SAXParser parser = parserFactory.newSAXParser(); 115 File f = new File(fontOsLocation, fontFileName); 116 117 FontHandler definitionParser = new FontHandler( 118 fontOsLocation + File.separator); 119 parser.parse(new FileInputStream(f), definitionParser); 120 return definitionParser; 121 } 122 123 private FontLoader(String fontOsLocation, 124 List<FontInfo> fontList, List<FontInfo> fallBackList) { 125 mOsFontsLocation = fontOsLocation; 126 mMainFonts.addAll(fontList); 127 mFallbackFonts.addAll(fallBackList); 128 } 129 130 131 public String getOsFontsLocation() { 132 return mOsFontsLocation; 133 } 134 135 /** 136 * Returns a {@link Font} object given a family name and a style value (constant in 137 * {@link Typeface}). 138 * @param family the family name 139 * @param style a 1-item array containing the requested style. Based on the font being read 140 * the actual style may be different. The array contains the actual style after 141 * the method returns. 142 * @return the font object or null if no match could be found. 143 */ 144 public synchronized List<Font> getFont(String family, int style) { 145 List<Font> result = new ArrayList<Font>(); 146 147 if (family == null) { 148 return result; 149 } 150 151 152 // get the font objects from the main list based on family. 153 for (FontInfo info : mMainFonts) { 154 if (info.families.contains(family)) { 155 result.add(info.font[style]); 156 break; 157 } 158 } 159 160 // add all the fallback fonts for the given style 161 for (FontInfo info : mFallbackFonts) { 162 result.add(info.font[style]); 163 } 164 165 return result; 166 } 167 168 169 public synchronized List<Font> getFallbackFonts(int style) { 170 List<Font> result = new ArrayList<Font>(); 171 // add all the fallback fonts 172 for (FontInfo info : mFallbackFonts) { 173 result.add(info.font[style]); 174 } 175 return result; 176 } 177 178 179 private final static class FontInfo { 180 final Font[] font = new Font[4]; // Matches the 4 type-face styles. 181 final Set<String> families; 182 183 FontInfo() { 184 families = new HashSet<String>(); 185 } 186 } 187 188 private final static class FontHandler extends DefaultHandler { 189 private final String mOsFontsLocation; 190 191 private FontInfo mFontInfo = null; 192 private final StringBuilder mBuilder = new StringBuilder(); 193 private List<FontInfo> mFontList = new ArrayList<FontInfo>(); 194 private boolean isCompactFont = true; 195 196 private FontHandler(String osFontsLocation) { 197 super(); 198 mOsFontsLocation = osFontsLocation; 199 } 200 201 public List<FontInfo> getFontList() { 202 return mFontList; 203 } 204 205 /* (non-Javadoc) 206 * @see org.xml.sax.helpers.DefaultHandler#startElement(java.lang.String, java.lang.String, java.lang.String, org.xml.sax.Attributes) 207 */ 208 @Override 209 public void startElement(String uri, String localName, String name, Attributes attributes) 210 throws SAXException { 211 if (NODE_FAMILYSET.equals(localName)) { 212 mFontList = new ArrayList<FontInfo>(); 213 } else if (NODE_FAMILY.equals(localName)) { 214 if (mFontList != null) { 215 mFontInfo = null; 216 } 217 } else if (NODE_NAME.equals(localName)) { 218 if (mFontList != null && mFontInfo == null) { 219 mFontInfo = new FontInfo(); 220 } 221 } else if (NODE_FILE.equals(localName)) { 222 if (mFontList != null && mFontInfo == null) { 223 mFontInfo = new FontInfo(); 224 } 225 if (ATTRIBUTE_VALUE_ELEGANT.equals(attributes.getValue(ATTRIBUTE_VARIANT))) { 226 isCompactFont = false; 227 } else { 228 isCompactFont = true; 229 } 230 } 231 232 mBuilder.setLength(0); 233 234 super.startElement(uri, localName, name, attributes); 235 } 236 237 /* (non-Javadoc) 238 * @see org.xml.sax.helpers.DefaultHandler#characters(char[], int, int) 239 */ 240 @Override 241 public void characters(char[] ch, int start, int length) throws SAXException { 242 if (isCompactFont) { 243 mBuilder.append(ch, start, length); 244 } 245 } 246 247 /* (non-Javadoc) 248 * @see org.xml.sax.helpers.DefaultHandler#endElement(java.lang.String, java.lang.String, java.lang.String) 249 */ 250 @Override 251 public void endElement(String uri, String localName, String name) throws SAXException { 252 if (NODE_FAMILY.equals(localName)) { 253 if (mFontInfo != null) { 254 // if has a normal font file, add to the list 255 if (mFontInfo.font[Typeface.NORMAL] != null) { 256 mFontList.add(mFontInfo); 257 258 // create missing font styles, order is important. 259 if (mFontInfo.font[Typeface.BOLD_ITALIC] == null) { 260 computeDerivedFont(Typeface.BOLD_ITALIC, DERIVE_BOLD_ITALIC); 261 } 262 if (mFontInfo.font[Typeface.ITALIC] == null) { 263 computeDerivedFont(Typeface.ITALIC, DERIVE_ITALIC); 264 } 265 if (mFontInfo.font[Typeface.BOLD] == null) { 266 computeDerivedFont(Typeface.BOLD, DERIVE_BOLD); 267 } 268 } 269 270 mFontInfo = null; 271 } 272 } else if (NODE_NAME.equals(localName)) { 273 // handle a new name for an existing Font Info 274 if (mFontInfo != null) { 275 String family = trimXmlWhitespaces(mBuilder.toString()); 276 mFontInfo.families.add(family); 277 } 278 } else if (NODE_FILE.equals(localName)) { 279 // handle a new file for an existing Font Info 280 if (isCompactFont && mFontInfo != null) { 281 String fileName = trimXmlWhitespaces(mBuilder.toString()); 282 Font font = getFont(fileName); 283 if (font != null) { 284 if (fileName.endsWith(FONT_SUFFIX_REGULAR)) { 285 mFontInfo.font[Typeface.NORMAL] = font; 286 } else if (fileName.endsWith(FONT_SUFFIX_BOLD)) { 287 mFontInfo.font[Typeface.BOLD] = font; 288 } else if (fileName.endsWith(FONT_SUFFIX_ITALIC)) { 289 mFontInfo.font[Typeface.ITALIC] = font; 290 } else if (fileName.endsWith(FONT_SUFFIX_BOLDITALIC)) { 291 mFontInfo.font[Typeface.BOLD_ITALIC] = font; 292 } else if (fileName.endsWith(FONT_SUFFIX_NONE)) { 293 mFontInfo.font[Typeface.NORMAL] = font; 294 } 295 } 296 } 297 } 298 } 299 300 private Font getFont(String fileName) { 301 try { 302 File file = new File(mOsFontsLocation, fileName); 303 if (file.exists()) { 304 return Font.createFont(Font.TRUETYPE_FONT, file); 305 } 306 } catch (Exception e) { 307 308 } 309 310 return null; 311 } 312 313 private void computeDerivedFont( int toCompute, int[] basedOnList) { 314 for (int basedOn : basedOnList) { 315 if (mFontInfo.font[basedOn] != null) { 316 mFontInfo.font[toCompute] = 317 mFontInfo.font[basedOn].deriveFont(AWT_STYLES[toCompute]); 318 return; 319 } 320 } 321 322 // we really shouldn't stop there. This means we don't have a NORMAL font... 323 assert false; 324 } 325 326 private String trimXmlWhitespaces(String value) { 327 if (value == null) { 328 return null; 329 } 330 331 // look for carriage return and replace all whitespace around it by just 1 space. 332 int index; 333 334 while ((index = value.indexOf('\n')) != -1) { 335 // look for whitespace on each side 336 int left = index - 1; 337 while (left >= 0) { 338 if (Character.isWhitespace(value.charAt(left))) { 339 left--; 340 } else { 341 break; 342 } 343 } 344 345 int right = index + 1; 346 int count = value.length(); 347 while (right < count) { 348 if (Character.isWhitespace(value.charAt(right))) { 349 right++; 350 } else { 351 break; 352 } 353 } 354 355 // remove all between left and right (non inclusive) and replace by a single space. 356 String leftString = null; 357 if (left >= 0) { 358 leftString = value.substring(0, left + 1); 359 } 360 String rightString = null; 361 if (right < count) { 362 rightString = value.substring(right); 363 } 364 365 if (leftString != null) { 366 value = leftString; 367 if (rightString != null) { 368 value += " " + rightString; 369 } 370 } else { 371 value = rightString != null ? rightString : ""; 372 } 373 } 374 375 // now we un-escape the string 376 int length = value.length(); 377 char[] buffer = value.toCharArray(); 378 379 for (int i = 0 ; i < length ; i++) { 380 if (buffer[i] == '\\') { 381 if (buffer[i+1] == 'n') { 382 // replace the char with \n 383 buffer[i+1] = '\n'; 384 } 385 386 // offset the rest of the buffer since we go from 2 to 1 char 387 System.arraycopy(buffer, i+1, buffer, i, length - i - 1); 388 length--; 389 } 390 } 391 392 return new String(buffer, 0, length); 393 } 394 395 } 396} 397