blob: b09082e65ca42e029ff5ac5cbd9a4456103d3bbb [file] [log] [blame]
/*
* Copyright (C) 2018 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.fonts;
import android.annotation.IntRange;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.content.res.AssetFileDescriptor;
import android.content.res.AssetManager;
import android.content.res.Resources;
import android.os.LocaleList;
import android.os.ParcelFileDescriptor;
import android.util.TypedValue;
import com.android.internal.util.Preconditions;
import dalvik.annotation.optimization.CriticalNative;
import libcore.util.NativeAllocationRegistry;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.channels.FileChannel;
import java.util.Arrays;
import java.util.Objects;
/**
* A font class can be used for creating FontFamily.
*/
public final class Font {
private static final String TAG = "Font";
private static final int NOT_SPECIFIED = -1;
private static final int STYLE_ITALIC = 1;
private static final int STYLE_NORMAL = 0;
/**
* A builder class for creating new Font.
*/
public static final class Builder {
private static final NativeAllocationRegistry sFontRegistry =
NativeAllocationRegistry.createMalloced(Font.class.getClassLoader(),
nGetReleaseNativeFont());
private @Nullable ByteBuffer mBuffer;
private @Nullable File mFile;
private @NonNull String mLocaleList = "";
private @IntRange(from = -1, to = 1000) int mWeight = NOT_SPECIFIED;
private @IntRange(from = -1, to = 1) int mItalic = NOT_SPECIFIED;
private @IntRange(from = 0) int mTtcIndex = 0;
private @Nullable FontVariationAxis[] mAxes = null;
private @Nullable IOException mException;
/**
* Constructs a builder with a byte buffer.
*
* Note that only direct buffer can be used as the source of font data.
*
* @see ByteBuffer#allocateDirect(int)
* @param buffer a byte buffer of a font data
*/
public Builder(@NonNull ByteBuffer buffer) {
Preconditions.checkNotNull(buffer, "buffer can not be null");
if (!buffer.isDirect()) {
throw new IllegalArgumentException(
"Only direct buffer can be used as the source of font data.");
}
mBuffer = buffer;
}
/**
* Construct a builder with a byte buffer and file path.
*
* This method is intended to be called only from SystemFonts.
* @hide
*/
public Builder(@NonNull ByteBuffer buffer, @NonNull File path,
@NonNull String localeList) {
this(buffer);
mFile = path;
mLocaleList = localeList;
}
/**
* Constructs a builder with a file path.
*
* @param path a file path to the font file
*/
public Builder(@NonNull File path) {
Preconditions.checkNotNull(path, "path can not be null");
try (FileInputStream fis = new FileInputStream(path)) {
final FileChannel fc = fis.getChannel();
mBuffer = fc.map(FileChannel.MapMode.READ_ONLY, 0, fc.size());
} catch (IOException e) {
mException = e;
}
mFile = path;
}
/**
* Constructs a builder with a file descriptor.
*
* @param fd a file descriptor
*/
public Builder(@NonNull ParcelFileDescriptor fd) {
this(fd, 0, -1);
}
/**
* Constructs a builder with a file descriptor.
*
* @param fd a file descriptor
* @param offset an offset to of the font data in the file
* @param size a size of the font data. If -1 is passed, use until end of the file.
*/
public Builder(@NonNull ParcelFileDescriptor fd, @IntRange(from = 0) long offset,
@IntRange(from = -1) long size) {
try (FileInputStream fis = new FileInputStream(fd.getFileDescriptor())) {
final FileChannel fc = fis.getChannel();
size = (size == -1) ? fc.size() - offset : size;
mBuffer = fc.map(FileChannel.MapMode.READ_ONLY, offset, size);
} catch (IOException e) {
mException = e;
}
}
/**
* Constructs a builder from an asset manager and a file path in an asset directory.
*
* @param am the application's asset manager
* @param path the file name of the font data in the asset directory
*/
public Builder(@NonNull AssetManager am, @NonNull String path) {
try {
mBuffer = createBuffer(am, path, true /* is asset */, 0 /* cookie */);
} catch (IOException e) {
mException = e;
}
}
/**
* Constructs a builder from an asset manager and a file path in an asset directory.
*
* @param am the application's asset manager
* @param path the file name of the font data in the asset directory
* @param isAsset true if the undelying data is in asset
* @param cookie set asset cookie
* @hide
*/
public Builder(@NonNull AssetManager am, @NonNull String path, boolean isAsset,
int cookie) {
try {
mBuffer = createBuffer(am, path, isAsset, cookie);
} catch (IOException e) {
mException = e;
}
}
/**
* Constructs a builder from resources.
*
* Resource ID must points the font file. XML font can not be used here.
*
* @param res the resource of this application.
* @param resId the resource ID of font file.
*/
public Builder(@NonNull Resources res, int resId) {
final TypedValue value = new TypedValue();
res.getValue(resId, value, true);
if (value.string == null) {
mException = new FileNotFoundException(resId + " not found");
return;
}
final String str = value.string.toString();
if (str.toLowerCase().endsWith(".xml")) {
mException = new FileNotFoundException(resId + " must be font file.");
return;
}
try {
mBuffer = createBuffer(res.getAssets(), str, false, value.assetCookie);
} catch (IOException e) {
mException = e;
}
}
/**
* Creates a buffer containing font data using the assetManager and other
* provided inputs.
*
* @param am the application's asset manager
* @param path the file name of the font data in the asset directory
* @param isAsset true if the undelying data is in asset
* @param cookie set asset cookie
* @return buffer containing the contents of the file
*
* @hide
*/
public static ByteBuffer createBuffer(@NonNull AssetManager am, @NonNull String path,
boolean isAsset, int cookie) throws IOException {
Preconditions.checkNotNull(am, "assetManager can not be null");
Preconditions.checkNotNull(path, "path can not be null");
// Attempt to open as FD, which should work unless the asset is compressed
AssetFileDescriptor assetFD;
try {
if (isAsset) {
assetFD = am.openFd(path);
} else if (cookie > 0) {
assetFD = am.openNonAssetFd(cookie, path);
} else {
assetFD = am.openNonAssetFd(path);
}
try (FileInputStream fis = assetFD.createInputStream()) {
final FileChannel fc = fis.getChannel();
long startOffset = assetFD.getStartOffset();
long declaredLength = assetFD.getDeclaredLength();
return fc.map(FileChannel.MapMode.READ_ONLY, startOffset, declaredLength);
}
} catch (IOException e) {
// failed to open as FD so now we will attempt to open as an input stream
}
try (InputStream assetStream = isAsset ? am.open(path, AssetManager.ACCESS_BUFFER)
: am.openNonAsset(cookie, path, AssetManager.ACCESS_BUFFER)) {
int capacity = assetStream.available();
ByteBuffer buffer = ByteBuffer.allocateDirect(capacity);
buffer.order(ByteOrder.nativeOrder());
assetStream.read(buffer.array(), buffer.arrayOffset(), assetStream.available());
if (assetStream.read() != -1) {
throw new IOException("Unable to access full contents of " + path);
}
return buffer;
}
}
/**
* Sets weight of the font.
*
* Tells the system the weight of the given font. If this function is not called, the system
* will resolve the weight value by reading font tables.
*
* Here are pairs of the common names and their values.
* <p>
* <table>
* <thead>
* <tr>
* <th align="center">Value</th>
* <th align="center">Name</th>
* <th align="center">Android Definition</th>
* </tr>
* </thead>
* <tbody>
* <tr>
* <td align="center">100</td>
* <td align="center">Thin</td>
* <td align="center">{@link FontStyle#FONT_WEIGHT_THIN}</td>
* </tr>
* <tr>
* <td align="center">200</td>
* <td align="center">Extra Light (Ultra Light)</td>
* <td align="center">{@link FontStyle#FONT_WEIGHT_EXTRA_LIGHT}</td>
* </tr>
* <tr>
* <td align="center">300</td>
* <td align="center">Light</td>
* <td align="center">{@link FontStyle#FONT_WEIGHT_LIGHT}</td>
* </tr>
* <tr>
* <td align="center">400</td>
* <td align="center">Normal (Regular)</td>
* <td align="center">{@link FontStyle#FONT_WEIGHT_NORMAL}</td>
* </tr>
* <tr>
* <td align="center">500</td>
* <td align="center">Medium</td>
* <td align="center">{@link FontStyle#FONT_WEIGHT_MEDIUM}</td>
* </tr>
* <tr>
* <td align="center">600</td>
* <td align="center">Semi Bold (Demi Bold)</td>
* <td align="center">{@link FontStyle#FONT_WEIGHT_SEMI_BOLD}</td>
* </tr>
* <tr>
* <td align="center">700</td>
* <td align="center">Bold</td>
* <td align="center">{@link FontStyle#FONT_WEIGHT_BOLD}</td>
* </tr>
* <tr>
* <td align="center">800</td>
* <td align="center">Extra Bold (Ultra Bold)</td>
* <td align="center">{@link FontStyle#FONT_WEIGHT_EXTRA_BOLD}</td>
* </tr>
* <tr>
* <td align="center">900</td>
* <td align="center">Black (Heavy)</td>
* <td align="center">{@link FontStyle#FONT_WEIGHT_BLACK}</td>
* </tr>
* </tbody>
* </p>
*
* @see FontStyle#FONT_WEIGHT_THIN
* @see FontStyle#FONT_WEIGHT_EXTRA_LIGHT
* @see FontStyle#FONT_WEIGHT_LIGHT
* @see FontStyle#FONT_WEIGHT_NORMAL
* @see FontStyle#FONT_WEIGHT_MEDIUM
* @see FontStyle#FONT_WEIGHT_SEMI_BOLD
* @see FontStyle#FONT_WEIGHT_BOLD
* @see FontStyle#FONT_WEIGHT_EXTRA_BOLD
* @see FontStyle#FONT_WEIGHT_BLACK
* @param weight a weight value
* @return this builder
*/
public @NonNull Builder setWeight(
@IntRange(from = FontStyle.FONT_WEIGHT_MIN, to = FontStyle.FONT_WEIGHT_MAX)
int weight) {
Preconditions.checkArgument(
FontStyle.FONT_WEIGHT_MIN <= weight && weight <= FontStyle.FONT_WEIGHT_MAX);
mWeight = weight;
return this;
}
/**
* Sets italic information of the font.
*
* Tells the system the style of the given font. If this function is not called, the system
* will resolve the style by reading font tables.
*
* For example, if you want to use italic font as upright font, call {@code
* setSlant(FontStyle.FONT_SLANT_UPRIGHT)} explicitly.
*
* @return this builder
*/
public @NonNull Builder setSlant(@FontStyle.FontSlant int slant) {
mItalic = slant == FontStyle.FONT_SLANT_UPRIGHT ? STYLE_NORMAL : STYLE_ITALIC;
return this;
}
/**
* Sets an index of the font collection. See {@link android.R.attr#ttcIndex}.
*
* @param ttcIndex An index of the font collection. If the font source is not font
* collection, do not call this method or specify 0.
* @return this builder
*/
public @NonNull Builder setTtcIndex(@IntRange(from = 0) int ttcIndex) {
mTtcIndex = ttcIndex;
return this;
}
/**
* Sets the font variation settings.
*
* @param variationSettings see {@link FontVariationAxis#fromFontVariationSettings(String)}
* @return this builder
* @throws IllegalArgumentException If given string is not a valid font variation settings
* format.
*/
public @NonNull Builder setFontVariationSettings(@Nullable String variationSettings) {
mAxes = FontVariationAxis.fromFontVariationSettings(variationSettings);
return this;
}
/**
* Sets the font variation settings.
*
* @param axes an array of font variation axis tag-value pairs
* @return this builder
*/
public @NonNull Builder setFontVariationSettings(@Nullable FontVariationAxis[] axes) {
mAxes = axes == null ? null : axes.clone();
return this;
}
/**
* Creates the font based on the configured values.
* @return the Font object
*/
public @NonNull Font build() throws IOException {
if (mException != null) {
throw new IOException("Failed to read font contents", mException);
}
if (mWeight == NOT_SPECIFIED || mItalic == NOT_SPECIFIED) {
final int packed = FontFileUtil.analyzeStyle(mBuffer, mTtcIndex, mAxes);
if (FontFileUtil.isSuccess(packed)) {
if (mWeight == NOT_SPECIFIED) {
mWeight = FontFileUtil.unpackWeight(packed);
}
if (mItalic == NOT_SPECIFIED) {
mItalic = FontFileUtil.unpackItalic(packed) ? STYLE_ITALIC : STYLE_NORMAL;
}
} else {
mWeight = 400;
mItalic = STYLE_NORMAL;
}
}
mWeight = Math.max(FontStyle.FONT_WEIGHT_MIN,
Math.min(FontStyle.FONT_WEIGHT_MAX, mWeight));
final boolean italic = (mItalic == STYLE_ITALIC);
final int slant = (mItalic == STYLE_ITALIC)
? FontStyle.FONT_SLANT_ITALIC : FontStyle.FONT_SLANT_UPRIGHT;
final long builderPtr = nInitBuilder();
if (mAxes != null) {
for (FontVariationAxis axis : mAxes) {
nAddAxis(builderPtr, axis.getOpenTypeTagValue(), axis.getStyleValue());
}
}
final ByteBuffer readonlyBuffer = mBuffer.asReadOnlyBuffer();
final String filePath = mFile == null ? "" : mFile.getAbsolutePath();
final long ptr = nBuild(builderPtr, readonlyBuffer, filePath, mWeight, italic,
mTtcIndex);
final Font font = new Font(ptr, readonlyBuffer, mFile,
new FontStyle(mWeight, slant), mTtcIndex, mAxes, mLocaleList);
sFontRegistry.registerNativeAllocation(font, ptr);
return font;
}
/**
* Native methods for creating Font
*/
private static native long nInitBuilder();
@CriticalNative
private static native void nAddAxis(long builderPtr, int tag, float value);
private static native long nBuild(
long builderPtr, @NonNull ByteBuffer buffer, @NonNull String filePath, int weight,
boolean italic, int ttcIndex);
@CriticalNative
private static native long nGetReleaseNativeFont();
}
private final long mNativePtr; // address of the shared ptr of minikin::Font
private final @NonNull ByteBuffer mBuffer;
private final @Nullable File mFile;
private final FontStyle mFontStyle;
private final @IntRange(from = 0) int mTtcIndex;
private final @Nullable FontVariationAxis[] mAxes;
private final @NonNull String mLocaleList;
/**
* Use Builder instead
*/
private Font(long nativePtr, @NonNull ByteBuffer buffer, @Nullable File file,
@NonNull FontStyle fontStyle, @IntRange(from = 0) int ttcIndex,
@Nullable FontVariationAxis[] axes, @NonNull String localeList) {
mBuffer = buffer;
mFile = file;
mFontStyle = fontStyle;
mNativePtr = nativePtr;
mTtcIndex = ttcIndex;
mAxes = axes;
mLocaleList = localeList;
}
/**
* Returns a font file buffer.
*
* @return a font buffer
*/
public @NonNull ByteBuffer getBuffer() {
return mBuffer;
}
/**
* Returns a file path of this font.
*
* This returns null if this font is not created from regular file.
*
* @return a file path of the font
*/
public @Nullable File getFile() {
return mFile;
}
/**
* Get a style associated with this font.
*
* @see Builder#setWeight(int)
* @see Builder#setSlant(int)
* @return a font style
*/
public @NonNull FontStyle getStyle() {
return mFontStyle;
}
/**
* Get a TTC index value associated with this font.
*
* If TTF/OTF file is provided, this value is always 0.
*
* @see Builder#setTtcIndex(int)
* @return a TTC index value
*/
public @IntRange(from = 0) int getTtcIndex() {
return mTtcIndex;
}
/**
* Get a font variation settings associated with this font
*
* @see Builder#setFontVariationSettings(String)
* @see Builder#setFontVariationSettings(FontVariationAxis[])
* @return font variation settings
*/
public @Nullable FontVariationAxis[] getAxes() {
return mAxes == null ? null : mAxes.clone();
}
/**
* Get a locale list of this font.
*
* This is always empty if this font is not a system font.
* @return a locale list
*/
public @NonNull LocaleList getLocaleList() {
return LocaleList.forLanguageTags(mLocaleList);
}
/** @hide */
public long getNativePtr() {
return mNativePtr;
}
@Override
public boolean equals(@Nullable Object o) {
if (o == this) {
return true;
}
if (o == null || !(o instanceof Font)) {
return false;
}
Font f = (Font) o;
return mFontStyle.equals(f.mFontStyle) && f.mTtcIndex == mTtcIndex
&& Arrays.equals(f.mAxes, mAxes) && f.mBuffer.equals(mBuffer)
&& Objects.equals(f.mLocaleList, mLocaleList);
}
@Override
public int hashCode() {
return Objects.hash(mFontStyle, mTtcIndex, Arrays.hashCode(mAxes), mBuffer, mLocaleList);
}
@Override
public String toString() {
return "Font {"
+ "path=" + mFile
+ ", style=" + mFontStyle
+ ", ttcIndex=" + mTtcIndex
+ ", axes=" + FontVariationAxis.toFontVariationSettings(mAxes)
+ ", localeList=" + mLocaleList
+ ", buffer=" + mBuffer
+ "}";
}
}