| /* |
| * Copyright (C) 2014 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package android.graphics; |
| |
| import com.android.ide.common.rendering.api.LayoutLog; |
| import com.android.layoutlib.bridge.Bridge; |
| import com.android.layoutlib.bridge.impl.DelegateManager; |
| import com.android.tools.layoutlib.annotations.LayoutlibDelegate; |
| |
| import android.annotation.NonNull; |
| import android.annotation.Nullable; |
| import android.graphics.fonts.FontVariationAxis; |
| |
| import java.awt.Font; |
| import java.awt.FontFormatException; |
| import java.io.File; |
| import java.io.FileNotFoundException; |
| import java.nio.ByteBuffer; |
| import java.util.ArrayList; |
| import java.util.Collections; |
| import java.util.HashSet; |
| import java.util.LinkedHashMap; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Objects; |
| import java.util.Scanner; |
| import java.util.Set; |
| import java.util.logging.Logger; |
| |
| import libcore.util.NativeAllocationRegistry_Delegate; |
| |
| import static android.graphics.Typeface.RESOLVE_BY_FONT_TABLE; |
| import static android.graphics.Typeface_Delegate.SYSTEM_FONTS; |
| |
| /** |
| * Delegate implementing the native methods of android.graphics.FontFamily |
| * |
| * Through the layoutlib_create tool, the original native methods of FontFamily have been replaced |
| * by calls to methods of the same name in this delegate class. |
| * |
| * This class behaves like the original native implementation, but in Java, keeping previously |
| * native data into its own objects and mapping them to int that are sent back and forth between |
| * it and the original FontFamily class. |
| * |
| * @see DelegateManager |
| */ |
| public class FontFamily_Delegate { |
| |
| public static final int DEFAULT_FONT_WEIGHT = 400; |
| public static final int BOLD_FONT_WEIGHT_DELTA = 300; |
| public static final int BOLD_FONT_WEIGHT = 700; |
| |
| private static final String FONT_SUFFIX_ITALIC = "Italic.ttf"; |
| private static final String FN_ALL_FONTS_LIST = "fontsInSdk.txt"; |
| private static final String EXTENSION_OTF = ".otf"; |
| |
| private static final int CACHE_SIZE = 10; |
| // The cache has a drawback that if the font file changed after the font object was created, |
| // we will not update it. |
| private static final Map<String, FontInfo> sCache = |
| new LinkedHashMap<String, FontInfo>(CACHE_SIZE) { |
| @Override |
| protected boolean removeEldestEntry(Map.Entry<String, FontInfo> eldest) { |
| return size() > CACHE_SIZE; |
| } |
| |
| @Override |
| public FontInfo put(String key, FontInfo value) { |
| // renew this entry. |
| FontInfo removed = remove(key); |
| super.put(key, value); |
| return removed; |
| } |
| }; |
| |
| /** |
| * A class associating {@link Font} with its metadata. |
| */ |
| public static final class FontInfo { |
| @Nullable |
| public Font mFont; |
| public int mWeight; |
| public boolean mIsItalic; |
| |
| @Override |
| public boolean equals(Object o) { |
| if (this == o) { |
| return true; |
| } |
| if (o == null || getClass() != o.getClass()) { |
| return false; |
| } |
| FontInfo fontInfo = (FontInfo) o; |
| return mWeight == fontInfo.mWeight && mIsItalic == fontInfo.mIsItalic; |
| } |
| |
| @Override |
| public int hashCode() { |
| return Objects.hash(mWeight, mIsItalic); |
| } |
| |
| @Override |
| public String toString() { |
| return "FontInfo{" + "mWeight=" + mWeight + ", mIsItalic=" + mIsItalic + '}'; |
| } |
| } |
| |
| // ---- delegate manager ---- |
| private static final DelegateManager<FontFamily_Delegate> sManager = |
| new DelegateManager<FontFamily_Delegate>(FontFamily_Delegate.class); |
| private static long sFamilyFinalizer = -1; |
| |
| // ---- delegate helper data ---- |
| private static String sFontLocation; |
| private static final List<FontFamily_Delegate> sPostInitDelegate = new |
| ArrayList<FontFamily_Delegate>(); |
| private static Set<String> SDK_FONTS; |
| |
| |
| // ---- delegate data ---- |
| |
| // Order does not really matter but we use a LinkedHashMap to get reproducible results across |
| // render calls |
| private Map<FontInfo, Font> mFonts = new LinkedHashMap<>(); |
| |
| /** |
| * The variant of the Font Family - compact or elegant. |
| * <p/> |
| * 0 is unspecified, 1 is compact and 2 is elegant. This needs to be kept in sync with values in |
| * android.graphics.FontFamily |
| * |
| * @see Paint#setElegantTextHeight(boolean) |
| */ |
| private FontVariant mVariant; |
| // List of runnables to process fonts after sFontLoader is initialized. |
| private List<Runnable> mPostInitRunnables = new ArrayList<Runnable>(); |
| /** @see #isValid() */ |
| private boolean mValid = false; |
| |
| |
| // ---- Public helper class ---- |
| |
| public enum FontVariant { |
| // The order needs to be kept in sync with android.graphics.FontFamily. |
| NONE, COMPACT, ELEGANT |
| } |
| |
| // ---- Public Helper methods ---- |
| |
| public static FontFamily_Delegate getDelegate(long nativeFontFamily) { |
| return sManager.getDelegate(nativeFontFamily); |
| } |
| |
| public static synchronized void setFontLocation(String fontLocation) { |
| sFontLocation = fontLocation; |
| // init list of bundled fonts. |
| File allFonts = new File(fontLocation, FN_ALL_FONTS_LIST); |
| // Current number of fonts is 103. Use the next round number to leave scope for more fonts |
| // in the future. |
| Set<String> allFontsList = new HashSet<>(128); |
| Scanner scanner = null; |
| try { |
| scanner = new Scanner(allFonts); |
| while (scanner.hasNext()) { |
| String name = scanner.next(); |
| // Skip font configuration files. |
| if (!name.endsWith(".xml")) { |
| allFontsList.add(name); |
| } |
| } |
| } catch (FileNotFoundException e) { |
| Bridge.getLog().error(LayoutLog.TAG_BROKEN, |
| "Unable to load the list of fonts. Try re-installing the SDK Platform from the SDK Manager.", |
| e, null, null); |
| } finally { |
| if (scanner != null) { |
| scanner.close(); |
| } |
| } |
| SDK_FONTS = Collections.unmodifiableSet(allFontsList); |
| for (FontFamily_Delegate fontFamily : sPostInitDelegate) { |
| fontFamily.init(); |
| } |
| sPostInitDelegate.clear(); |
| } |
| |
| @Nullable |
| public Font getFont(int desiredWeight, boolean isItalic) { |
| FontInfo desiredStyle = new FontInfo(); |
| desiredStyle.mWeight = desiredWeight; |
| desiredStyle.mIsItalic = isItalic; |
| |
| Font cachedFont = mFonts.get(desiredStyle); |
| if (cachedFont != null) { |
| return cachedFont; |
| } |
| |
| FontInfo bestFont = null; |
| |
| if (mFonts.size() == 1) { |
| // No need to compute the match since we only have one candidate |
| bestFont = mFonts.keySet().iterator().next(); |
| } else { |
| int bestMatch = Integer.MAX_VALUE; |
| |
| for (FontInfo font : mFonts.keySet()) { |
| int match = computeMatch(font, desiredStyle); |
| if (match < bestMatch) { |
| bestMatch = match; |
| bestFont = font; |
| if (bestMatch == 0) { |
| break; |
| } |
| } |
| } |
| } |
| |
| if (bestFont == null) { |
| return null; |
| } |
| |
| |
| // Derive the font as required and add it to the list of Fonts. |
| deriveFont(bestFont, desiredStyle); |
| addFont(desiredStyle); |
| return desiredStyle.mFont; |
| } |
| |
| public FontVariant getVariant() { |
| return mVariant; |
| } |
| |
| /** |
| * Returns if the FontFamily should contain any fonts. If this returns true and |
| * {@link #getFont(int, boolean)} returns an empty list, it means that an error occurred while |
| * loading the fonts. However, some fonts are deliberately skipped, for example they are not |
| * bundled with the SDK. In such a case, this method returns false. |
| */ |
| public boolean isValid() { |
| return mValid; |
| } |
| |
| private static Font loadFont(String path) { |
| if (path.startsWith(SYSTEM_FONTS) ) { |
| String relativePath = path.substring(SYSTEM_FONTS.length()); |
| File f = new File(sFontLocation, relativePath); |
| |
| try { |
| return Font.createFont(Font.TRUETYPE_FONT, f); |
| } catch (Exception e) { |
| if (path.endsWith(EXTENSION_OTF) && e instanceof FontFormatException) { |
| // If we aren't able to load an Open Type font, don't log a warning just yet. |
| // We wait for a case where font is being used. Only then we try to log the |
| // warning. |
| return null; |
| } |
| Bridge.getLog().fidelityWarning(LayoutLog.TAG_BROKEN, |
| String.format("Unable to load font %1$s", relativePath), |
| e, null, null); |
| } |
| } else { |
| Bridge.getLog().fidelityWarning(LayoutLog.TAG_UNSUPPORTED, |
| "Only platform fonts located in " + SYSTEM_FONTS + "can be loaded.", |
| null, null, null); |
| } |
| |
| return null; |
| } |
| |
| @Nullable |
| public static String getFontLocation() { |
| return sFontLocation; |
| } |
| |
| // ---- delegate methods ---- |
| @LayoutlibDelegate |
| /*package*/ static boolean addFont(FontFamily thisFontFamily, String path, int ttcIndex, |
| FontVariationAxis[] axes, int weight, int italic) { |
| if (thisFontFamily.mBuilderPtr == 0) { |
| assert false : "Unable to call addFont after freezing."; |
| return false; |
| } |
| final FontFamily_Delegate delegate = getDelegate(thisFontFamily.mBuilderPtr); |
| return delegate != null && delegate.addFont(path, ttcIndex, weight, italic); |
| } |
| |
| // ---- native methods ---- |
| |
| @LayoutlibDelegate |
| /*package*/ static long nInitBuilder(String lang, int variant) { |
| // TODO: support lang. This is required for japanese locale. |
| FontFamily_Delegate delegate = new FontFamily_Delegate(); |
| // variant can be 0, 1 or 2. |
| assert variant < 3; |
| delegate.mVariant = FontVariant.values()[variant]; |
| if (sFontLocation != null) { |
| delegate.init(); |
| } else { |
| sPostInitDelegate.add(delegate); |
| } |
| return sManager.addNewDelegate(delegate); |
| } |
| |
| @LayoutlibDelegate |
| /*package*/ static long nCreateFamily(long builderPtr) { |
| return builderPtr; |
| } |
| |
| @LayoutlibDelegate |
| /*package*/ static long nGetFamilyReleaseFunc() { |
| synchronized (FontFamily_Delegate.class) { |
| if (sFamilyFinalizer == -1) { |
| sFamilyFinalizer = NativeAllocationRegistry_Delegate.createFinalizer( |
| sManager::removeJavaReferenceFor); |
| } |
| } |
| return sFamilyFinalizer; |
| } |
| |
| @LayoutlibDelegate |
| /*package*/ static boolean nAddFont(long builderPtr, ByteBuffer font, int ttcIndex, |
| int weight, int isItalic) { |
| assert false : "The only client of this method has been overridden."; |
| return false; |
| } |
| |
| @LayoutlibDelegate |
| /*package*/ static boolean nAddFontWeightStyle(long builderPtr, ByteBuffer font, |
| int ttcIndex, int weight, int isItalic) { |
| assert false : "The only client of this method has been overridden."; |
| return false; |
| } |
| |
| @LayoutlibDelegate |
| /*package*/ static void nAddAxisValue(long builderPtr, int tag, float value) { |
| assert false : "The only client of this method has been overridden."; |
| } |
| |
| static boolean addFont(long builderPtr, final String path, final int weight, |
| final boolean isItalic) { |
| final FontFamily_Delegate delegate = getDelegate(builderPtr); |
| int italic = isItalic ? 1 : 0; |
| if (delegate != null) { |
| if (sFontLocation == null) { |
| delegate.mPostInitRunnables.add(() -> delegate.addFont(path, weight, italic)); |
| return true; |
| } |
| return delegate.addFont(path, weight, italic); |
| } |
| return false; |
| } |
| |
| @LayoutlibDelegate |
| /*package*/ static long nGetBuilderReleaseFunc() { |
| // Layoutlib uses the same reference for the builder and the font family, |
| // so it should not release that reference at the builder stage. |
| return -1; |
| } |
| |
| // ---- private helper methods ---- |
| |
| private void init() { |
| for (Runnable postInitRunnable : mPostInitRunnables) { |
| postInitRunnable.run(); |
| } |
| mPostInitRunnables = null; |
| } |
| |
| private boolean addFont(final String path, int ttcIndex, int weight, int italic) { |
| // FIXME: support ttc fonts. Hack JRE?? |
| if (sFontLocation == null) { |
| mPostInitRunnables.add(() -> addFont(path, weight, italic)); |
| return true; |
| } |
| return addFont(path, weight, italic); |
| } |
| |
| private boolean addFont(@NonNull String path) { |
| return addFont(path, DEFAULT_FONT_WEIGHT, path.endsWith(FONT_SUFFIX_ITALIC) ? 1 : RESOLVE_BY_FONT_TABLE); |
| } |
| |
| private boolean addFont(@NonNull String path, int weight, int italic) { |
| if (path.startsWith(SYSTEM_FONTS) && |
| !SDK_FONTS.contains(path.substring(SYSTEM_FONTS.length()))) { |
| Logger.getLogger(FontFamily_Delegate.class.getSimpleName()).warning("Unable to load font " + path); |
| return mValid = false; |
| } |
| // Set valid to true, even if the font fails to load. |
| mValid = true; |
| Font font = loadFont(path); |
| if (font == null) { |
| return false; |
| } |
| FontInfo fontInfo = new FontInfo(); |
| fontInfo.mFont = font; |
| fontInfo.mWeight = weight; |
| fontInfo.mIsItalic = italic == RESOLVE_BY_FONT_TABLE ? font.isItalic() : italic == 1; |
| addFont(fontInfo); |
| return true; |
| } |
| |
| private boolean addFont(@NonNull FontInfo fontInfo) { |
| return mFonts.putIfAbsent(fontInfo, fontInfo.mFont) == null; |
| } |
| |
| /** |
| * Compute matching metric between two styles - 0 is an exact match. |
| */ |
| public static int computeMatch(@NonNull FontInfo font1, @NonNull FontInfo font2) { |
| int score = Math.abs(font1.mWeight / 100 - font2.mWeight / 100); |
| if (font1.mIsItalic != font2.mIsItalic) { |
| score += 2; |
| } |
| return score; |
| } |
| |
| /** |
| * Try to derive a font from {@code srcFont} for the style in {@code outFont}. |
| * <p/> |
| * {@code outFont} is updated to reflect the style of the derived font. |
| * @param srcFont the source font |
| * @param outFont contains the desired font style. Updated to contain the derived font and |
| * its style |
| */ |
| public static void deriveFont(@NonNull FontInfo srcFont, @NonNull FontInfo outFont) { |
| int desiredWeight = outFont.mWeight; |
| int srcWeight = srcFont.mWeight; |
| assert srcFont.mFont != null; |
| Font derivedFont = srcFont.mFont; |
| int derivedStyle = 0; |
| // Embolden the font if required. |
| if (desiredWeight >= BOLD_FONT_WEIGHT && desiredWeight - srcWeight > BOLD_FONT_WEIGHT_DELTA / 2) { |
| derivedStyle |= Font.BOLD; |
| srcWeight += BOLD_FONT_WEIGHT_DELTA; |
| } |
| // Italicize the font if required. |
| if (outFont.mIsItalic && !srcFont.mIsItalic) { |
| derivedStyle |= Font.ITALIC; |
| } else if (outFont.mIsItalic != srcFont.mIsItalic) { |
| // The desired font is plain, but the src font is italics. We can't convert it back. So |
| // we update the value to reflect the true style of the font we're deriving. |
| outFont.mIsItalic = srcFont.mIsItalic; |
| } |
| |
| if (derivedStyle != 0) { |
| derivedFont = derivedFont.deriveFont(derivedStyle); |
| } |
| |
| outFont.mFont = derivedFont; |
| outFont.mWeight = srcWeight; |
| // No need to update mIsItalics, as it's already been handled above. |
| } |
| } |