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