1 /*
2  * Copyright (C) 2014 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 
17 package android.graphics;
18 
19 import com.android.ide.common.rendering.api.LayoutLog;
20 import com.android.layoutlib.bridge.Bridge;
21 import com.android.layoutlib.bridge.impl.DelegateManager;
22 import com.android.tools.layoutlib.annotations.LayoutlibDelegate;
23 
24 import android.annotation.NonNull;
25 import android.annotation.Nullable;
26 import android.graphics.fonts.FontVariationAxis;
27 
28 import java.awt.Font;
29 import java.awt.FontFormatException;
30 import java.io.File;
31 import java.io.FileNotFoundException;
32 import java.nio.ByteBuffer;
33 import java.util.ArrayList;
34 import java.util.Collections;
35 import java.util.HashSet;
36 import java.util.LinkedHashMap;
37 import java.util.List;
38 import java.util.Map;
39 import java.util.Objects;
40 import java.util.Scanner;
41 import java.util.Set;
42 import java.util.logging.Logger;
43 
44 import libcore.util.NativeAllocationRegistry_Delegate;
45 
46 import static android.graphics.Typeface.RESOLVE_BY_FONT_TABLE;
47 import static android.graphics.Typeface_Delegate.SYSTEM_FONTS;
48 
49 /**
50  * Delegate implementing the native methods of android.graphics.FontFamily
51  *
52  * Through the layoutlib_create tool, the original native methods of FontFamily have been replaced
53  * by calls to methods of the same name in this delegate class.
54  *
55  * This class behaves like the original native implementation, but in Java, keeping previously
56  * native data into its own objects and mapping them to int that are sent back and forth between
57  * it and the original FontFamily class.
58  *
59  * @see DelegateManager
60  */
61 public class FontFamily_Delegate {
62 
63     public static final int DEFAULT_FONT_WEIGHT = 400;
64     public static final int BOLD_FONT_WEIGHT_DELTA = 300;
65     public static final int BOLD_FONT_WEIGHT = 700;
66 
67     private static final String FONT_SUFFIX_ITALIC = "Italic.ttf";
68     private static final String FN_ALL_FONTS_LIST = "fontsInSdk.txt";
69     private static final String EXTENSION_OTF = ".otf";
70 
71     private static final int CACHE_SIZE = 10;
72     // The cache has a drawback that if the font file changed after the font object was created,
73     // we will not update it.
74     private static final Map<String, FontInfo> sCache =
75             new LinkedHashMap<String, FontInfo>(CACHE_SIZE) {
76         @Override
77         protected boolean removeEldestEntry(Map.Entry<String, FontInfo> eldest) {
78             return size() > CACHE_SIZE;
79         }
80 
81         @Override
82         public FontInfo put(String key, FontInfo value) {
83             // renew this entry.
84             FontInfo removed = remove(key);
85             super.put(key, value);
86             return removed;
87         }
88     };
89 
90     /**
91      * A class associating {@link Font} with its metadata.
92      */
93     public static final class FontInfo {
94         @Nullable
95         public Font mFont;
96         public int mWeight;
97         public boolean mIsItalic;
98 
99         @Override
equals(Object o)100         public boolean equals(Object o) {
101             if (this == o) {
102                 return true;
103             }
104             if (o == null || getClass() != o.getClass()) {
105                 return false;
106             }
107             FontInfo fontInfo = (FontInfo) o;
108             return mWeight == fontInfo.mWeight && mIsItalic == fontInfo.mIsItalic;
109         }
110 
111         @Override
hashCode()112         public int hashCode() {
113             return Objects.hash(mWeight, mIsItalic);
114         }
115 
116         @Override
toString()117         public String toString() {
118             return "FontInfo{" + "mWeight=" + mWeight + ", mIsItalic=" + mIsItalic + '}';
119         }
120     }
121 
122     // ---- delegate manager ----
123     private static final DelegateManager<FontFamily_Delegate> sManager =
124             new DelegateManager<FontFamily_Delegate>(FontFamily_Delegate.class);
125     private static long sFamilyFinalizer = -1;
126 
127     // ---- delegate helper data ----
128     private static String sFontLocation;
129     private static final List<FontFamily_Delegate> sPostInitDelegate = new
130             ArrayList<FontFamily_Delegate>();
131     private static Set<String> SDK_FONTS;
132 
133 
134     // ---- delegate data ----
135 
136     // Order does not really matter but we use a LinkedHashMap to get reproducible results across
137     // render calls
138     private Map<FontInfo, Font> mFonts = new LinkedHashMap<>();
139 
140     /**
141      * The variant of the Font Family - compact or elegant.
142      * <p/>
143      * 0 is unspecified, 1 is compact and 2 is elegant. This needs to be kept in sync with values in
144      * android.graphics.FontFamily
145      *
146      * @see Paint#setElegantTextHeight(boolean)
147      */
148     private FontVariant mVariant;
149     // List of runnables to process fonts after sFontLoader is initialized.
150     private List<Runnable> mPostInitRunnables = new ArrayList<Runnable>();
151     /** @see #isValid() */
152     private boolean mValid = false;
153 
154 
155     // ---- Public helper class ----
156 
157     public enum FontVariant {
158         // The order needs to be kept in sync with android.graphics.FontFamily.
159         NONE, COMPACT, ELEGANT
160     }
161 
162     // ---- Public Helper methods ----
163 
getDelegate(long nativeFontFamily)164     public static FontFamily_Delegate getDelegate(long nativeFontFamily) {
165         return sManager.getDelegate(nativeFontFamily);
166     }
167 
setFontLocation(String fontLocation)168     public static synchronized void setFontLocation(String fontLocation) {
169         sFontLocation = fontLocation;
170         // init list of bundled fonts.
171         File allFonts = new File(fontLocation, FN_ALL_FONTS_LIST);
172         // Current number of fonts is 103. Use the next round number to leave scope for more fonts
173         // in the future.
174         Set<String> allFontsList = new HashSet<>(128);
175         Scanner scanner = null;
176         try {
177             scanner = new Scanner(allFonts);
178             while (scanner.hasNext()) {
179                 String name = scanner.next();
180                 // Skip font configuration files.
181                 if (!name.endsWith(".xml")) {
182                     allFontsList.add(name);
183                 }
184             }
185         } catch (FileNotFoundException e) {
186             Bridge.getLog().error(LayoutLog.TAG_BROKEN,
187                     "Unable to load the list of fonts. Try re-installing the SDK Platform from the SDK Manager.",
188                     e, null, null);
189         } finally {
190             if (scanner != null) {
191                 scanner.close();
192             }
193         }
194         SDK_FONTS = Collections.unmodifiableSet(allFontsList);
195         for (FontFamily_Delegate fontFamily : sPostInitDelegate) {
196             fontFamily.init();
197         }
198         sPostInitDelegate.clear();
199     }
200 
201     @Nullable
getFont(int desiredWeight, boolean isItalic)202     public Font getFont(int desiredWeight, boolean isItalic) {
203         FontInfo desiredStyle = new FontInfo();
204         desiredStyle.mWeight = desiredWeight;
205         desiredStyle.mIsItalic = isItalic;
206 
207         Font cachedFont = mFonts.get(desiredStyle);
208         if (cachedFont != null) {
209             return cachedFont;
210         }
211 
212         FontInfo bestFont = null;
213 
214         if (mFonts.size() == 1) {
215             // No need to compute the match since we only have one candidate
216             bestFont = mFonts.keySet().iterator().next();
217         } else {
218             int bestMatch = Integer.MAX_VALUE;
219 
220             for (FontInfo font : mFonts.keySet()) {
221                 int match = computeMatch(font, desiredStyle);
222                 if (match < bestMatch) {
223                     bestMatch = match;
224                     bestFont = font;
225                     if (bestMatch == 0) {
226                         break;
227                     }
228                 }
229             }
230         }
231 
232         if (bestFont == null) {
233             return null;
234         }
235 
236 
237         // Derive the font as required and add it to the list of Fonts.
238         deriveFont(bestFont, desiredStyle);
239         addFont(desiredStyle);
240         return desiredStyle.mFont;
241     }
242 
getVariant()243     public FontVariant getVariant() {
244         return mVariant;
245     }
246 
247     /**
248      * Returns if the FontFamily should contain any fonts. If this returns true and
249      * {@link #getFont(int, boolean)} returns an empty list, it means that an error occurred while
250      * loading the fonts. However, some fonts are deliberately skipped, for example they are not
251      * bundled with the SDK. In such a case, this method returns false.
252      */
isValid()253     public boolean isValid() {
254         return mValid;
255     }
256 
loadFont(String path)257     private static Font loadFont(String path) {
258         if (path.startsWith(SYSTEM_FONTS) ) {
259             String relativePath = path.substring(SYSTEM_FONTS.length());
260             File f = new File(sFontLocation, relativePath);
261 
262             try {
263                 return Font.createFont(Font.TRUETYPE_FONT, f);
264             } catch (Exception e) {
265                 if (path.endsWith(EXTENSION_OTF) && e instanceof FontFormatException) {
266                     // If we aren't able to load an Open Type font, don't log a warning just yet.
267                     // We wait for a case where font is being used. Only then we try to log the
268                     // warning.
269                     return null;
270                 }
271                 Bridge.getLog().fidelityWarning(LayoutLog.TAG_BROKEN,
272                         String.format("Unable to load font %1$s", relativePath),
273                         e, null, null);
274             }
275         } else {
276             Bridge.getLog().fidelityWarning(LayoutLog.TAG_UNSUPPORTED,
277                     "Only platform fonts located in " + SYSTEM_FONTS + "can be loaded.",
278                     null, null, null);
279         }
280 
281         return null;
282     }
283 
284     @Nullable
getFontLocation()285     public static String getFontLocation() {
286         return sFontLocation;
287     }
288 
289     // ---- delegate methods ----
290     @LayoutlibDelegate
addFont(FontFamily thisFontFamily, String path, int ttcIndex, FontVariationAxis[] axes, int weight, int italic)291     /*package*/ static boolean addFont(FontFamily thisFontFamily, String path, int ttcIndex,
292             FontVariationAxis[] axes, int weight, int italic) {
293         if (thisFontFamily.mBuilderPtr == 0) {
294             assert false : "Unable to call addFont after freezing.";
295             return false;
296         }
297         final FontFamily_Delegate delegate = getDelegate(thisFontFamily.mBuilderPtr);
298         return delegate != null && delegate.addFont(path, ttcIndex, weight, italic);
299     }
300 
301     // ---- native methods ----
302 
303     @LayoutlibDelegate
nInitBuilder(String lang, int variant)304     /*package*/ static long nInitBuilder(String lang, int variant) {
305         // TODO: support lang. This is required for japanese locale.
306         FontFamily_Delegate delegate = new FontFamily_Delegate();
307         // variant can be 0, 1 or 2.
308         assert variant < 3;
309         delegate.mVariant = FontVariant.values()[variant];
310         if (sFontLocation != null) {
311             delegate.init();
312         } else {
313             sPostInitDelegate.add(delegate);
314         }
315         return sManager.addNewDelegate(delegate);
316     }
317 
318     @LayoutlibDelegate
319     /*package*/ static long nCreateFamily(long builderPtr) {
320         return builderPtr;
321     }
322 
323     @LayoutlibDelegate
324     /*package*/ static long nGetFamilyReleaseFunc() {
325         synchronized (FontFamily_Delegate.class) {
326             if (sFamilyFinalizer == -1) {
327                 sFamilyFinalizer = NativeAllocationRegistry_Delegate.createFinalizer(
328                         sManager::removeJavaReferenceFor);
329             }
330         }
331         return sFamilyFinalizer;
332     }
333 
334     @LayoutlibDelegate
335     /*package*/ static boolean nAddFont(long builderPtr, ByteBuffer font, int ttcIndex,
336             int weight, int isItalic) {
337         assert false : "The only client of this method has been overridden.";
338         return false;
339     }
340 
341     @LayoutlibDelegate
342     /*package*/ static boolean nAddFontWeightStyle(long builderPtr, ByteBuffer font,
343             int ttcIndex, int weight, int isItalic) {
344         assert false : "The only client of this method has been overridden.";
345         return false;
346     }
347 
348     @LayoutlibDelegate
349     /*package*/ static void nAddAxisValue(long builderPtr, int tag, float value) {
350         assert false : "The only client of this method has been overridden.";
351     }
352 
353     static boolean addFont(long builderPtr, final String path, final int weight,
354             final boolean isItalic) {
355         final FontFamily_Delegate delegate = getDelegate(builderPtr);
356         int italic = isItalic ? 1 : 0;
357         if (delegate != null) {
358             if (sFontLocation == null) {
359                 delegate.mPostInitRunnables.add(() -> delegate.addFont(path, weight, italic));
360                 return true;
361             }
362             return delegate.addFont(path, weight, italic);
363         }
364         return false;
365     }
366 
367     @LayoutlibDelegate
368     /*package*/ static long nGetBuilderReleaseFunc() {
369         // Layoutlib uses the same reference for the builder and the font family,
370         // so it should not release that reference at the builder stage.
371         return -1;
372     }
373 
374     // ---- private helper methods ----
375 
376     private void init() {
377         for (Runnable postInitRunnable : mPostInitRunnables) {
378             postInitRunnable.run();
379         }
380         mPostInitRunnables = null;
381     }
382 
383     private boolean addFont(final String path, int ttcIndex, int weight, int italic) {
384         // FIXME: support ttc fonts. Hack JRE??
385         if (sFontLocation == null) {
386             mPostInitRunnables.add(() -> addFont(path, weight, italic));
387             return true;
388         }
389         return addFont(path, weight, italic);
390     }
391 
392      private boolean addFont(@NonNull String path) {
393          return addFont(path, DEFAULT_FONT_WEIGHT, path.endsWith(FONT_SUFFIX_ITALIC) ? 1 : RESOLVE_BY_FONT_TABLE);
394      }
395 
396     private boolean addFont(@NonNull String path, int weight, int italic) {
397         if (path.startsWith(SYSTEM_FONTS) &&
398                 !SDK_FONTS.contains(path.substring(SYSTEM_FONTS.length()))) {
399             Logger.getLogger(FontFamily_Delegate.class.getSimpleName()).warning("Unable to load font " + path);
400             return mValid = false;
401         }
402         // Set valid to true, even if the font fails to load.
403         mValid = true;
404         Font font = loadFont(path);
405         if (font == null) {
406             return false;
407         }
408         FontInfo fontInfo = new FontInfo();
409         fontInfo.mFont = font;
410         fontInfo.mWeight = weight;
411         fontInfo.mIsItalic = italic == RESOLVE_BY_FONT_TABLE ? font.isItalic() : italic == 1;
412         addFont(fontInfo);
413         return true;
414     }
415 
416     private boolean addFont(@NonNull FontInfo fontInfo) {
417         return mFonts.putIfAbsent(fontInfo, fontInfo.mFont) == null;
418     }
419 
420     /**
421      * Compute matching metric between two styles - 0 is an exact match.
422      */
423     public static int computeMatch(@NonNull FontInfo font1, @NonNull FontInfo font2) {
424         int score = Math.abs(font1.mWeight / 100 - font2.mWeight / 100);
425         if (font1.mIsItalic != font2.mIsItalic) {
426             score += 2;
427         }
428         return score;
429     }
430 
431     /**
432      * Try to derive a font from {@code srcFont} for the style in {@code outFont}.
433      * <p/>
434      * {@code outFont} is updated to reflect the style of the derived font.
435      * @param srcFont the source font
436      * @param outFont contains the desired font style. Updated to contain the derived font and
437      *                its style
438      */
439     public static void deriveFont(@NonNull FontInfo srcFont, @NonNull FontInfo outFont) {
440         int desiredWeight = outFont.mWeight;
441         int srcWeight = srcFont.mWeight;
442         assert srcFont.mFont != null;
443         Font derivedFont = srcFont.mFont;
444         int derivedStyle = 0;
445         // Embolden the font if required.
446         if (desiredWeight >= BOLD_FONT_WEIGHT && desiredWeight - srcWeight > BOLD_FONT_WEIGHT_DELTA / 2) {
447             derivedStyle |= Font.BOLD;
448             srcWeight += BOLD_FONT_WEIGHT_DELTA;
449         }
450         // Italicize the font if required.
451         if (outFont.mIsItalic && !srcFont.mIsItalic) {
452             derivedStyle |= Font.ITALIC;
453         } else if (outFont.mIsItalic != srcFont.mIsItalic) {
454             // The desired font is plain, but the src font is italics. We can't convert it back. So
455             // we update the value to reflect the true style of the font we're deriving.
456             outFont.mIsItalic = srcFont.mIsItalic;
457         }
458 
459         if (derivedStyle != 0) {
460             derivedFont = derivedFont.deriveFont(derivedStyle);
461         }
462 
463         outFont.mFont = derivedFont;
464         outFont.mWeight = srcWeight;
465         // No need to update mIsItalics, as it's already been handled above.
466     }
467 }
468