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