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