blob: afcf9bd9f43be9ff6c820939bf1870c99d75bf20 [file] [log] [blame]
/*
* 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.
}
}