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