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