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