1 /*
2  * Copyright (C) 2018 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.fonts;
18 
19 import android.annotation.IntRange;
20 import android.annotation.NonNull;
21 import android.annotation.Nullable;
22 import android.content.res.AssetFileDescriptor;
23 import android.content.res.AssetManager;
24 import android.content.res.Resources;
25 import android.graphics.Paint;
26 import android.graphics.RectF;
27 import android.os.LocaleList;
28 import android.os.ParcelFileDescriptor;
29 import android.text.TextUtils;
30 import android.util.TypedValue;
31 
32 import com.android.internal.annotations.GuardedBy;
33 import com.android.internal.util.Preconditions;
34 
35 import dalvik.annotation.optimization.CriticalNative;
36 import dalvik.annotation.optimization.FastNative;
37 
38 import libcore.util.NativeAllocationRegistry;
39 
40 import java.io.File;
41 import java.io.FileInputStream;
42 import java.io.FileNotFoundException;
43 import java.io.IOException;
44 import java.io.InputStream;
45 import java.nio.ByteBuffer;
46 import java.nio.ByteOrder;
47 import java.nio.channels.FileChannel;
48 import java.util.Arrays;
49 import java.util.Collections;
50 import java.util.IdentityHashMap;
51 import java.util.Objects;
52 import java.util.Set;
53 
54 /**
55  * A font class can be used for creating FontFamily.
56  */
57 public final class Font {
58     private static final String TAG = "Font";
59 
60     private static final int NOT_SPECIFIED = -1;
61     private static final int STYLE_ITALIC = 1;
62     private static final int STYLE_NORMAL = 0;
63 
64     private static class NoImagePreloadHolder {
65         private static final NativeAllocationRegistry BUFFER_REGISTRY =
66                 NativeAllocationRegistry.createMalloced(
67                         ByteBuffer.class.getClassLoader(), nGetReleaseNativeFont());
68 
69         private static final NativeAllocationRegistry FONT_REGISTRY =
70                 NativeAllocationRegistry.createMalloced(Font.class.getClassLoader(),
71                         nGetReleaseNativeFont());
72     }
73 
74     /**
75      * A builder class for creating new Font.
76      */
77     public static final class Builder {
78 
79 
80         private @Nullable ByteBuffer mBuffer;
81         private @Nullable File mFile;
82         private @Nullable Font mFont;
83         private @NonNull String mLocaleList = "";
84         private @IntRange(from = -1, to = 1000) int mWeight = NOT_SPECIFIED;
85         private @IntRange(from = -1, to = 1) int mItalic = NOT_SPECIFIED;
86         private @IntRange(from = 0) int mTtcIndex = 0;
87         private @Nullable FontVariationAxis[] mAxes = null;
88         private @Nullable IOException mException;
89 
90         /**
91          * Constructs a builder with a byte buffer.
92          *
93          * Note that only direct buffer can be used as the source of font data.
94          *
95          * @see ByteBuffer#allocateDirect(int)
96          * @param buffer a byte buffer of a font data
97          */
Builder(@onNull ByteBuffer buffer)98         public Builder(@NonNull ByteBuffer buffer) {
99             Preconditions.checkNotNull(buffer, "buffer can not be null");
100             if (!buffer.isDirect()) {
101                 throw new IllegalArgumentException(
102                         "Only direct buffer can be used as the source of font data.");
103             }
104             mBuffer = buffer;
105         }
106 
107         /**
108          * Construct a builder with a byte buffer and file path.
109          *
110          * This method is intended to be called only from SystemFonts.
111          * @hide
112          */
Builder(@onNull ByteBuffer buffer, @NonNull File path, @NonNull String localeList)113         public Builder(@NonNull ByteBuffer buffer, @NonNull File path,
114                 @NonNull String localeList) {
115             this(buffer);
116             mFile = path;
117             mLocaleList = localeList;
118         }
119 
120         /**
121          * Construct a builder with a byte buffer and file path.
122          *
123          * This method is intended to be called only from SystemFonts.
124          * @param path font file path
125          * @param localeList comma concatenated BCP47 compliant language tag.
126          * @hide
127          */
Builder(@onNull File path, @NonNull String localeList)128         public Builder(@NonNull File path, @NonNull String localeList) {
129             this(path);
130             mLocaleList = localeList;
131         }
132 
133         /**
134          * Constructs a builder with a file path.
135          *
136          * @param path a file path to the font file
137          */
Builder(@onNull File path)138         public Builder(@NonNull File path) {
139             Preconditions.checkNotNull(path, "path can not be null");
140             try (FileInputStream fis = new FileInputStream(path)) {
141                 final FileChannel fc = fis.getChannel();
142                 mBuffer = fc.map(FileChannel.MapMode.READ_ONLY, 0, fc.size());
143             } catch (IOException e) {
144                 mException = e;
145             }
146             mFile = path;
147         }
148 
149         /**
150          * Constructs a builder with a file descriptor.
151          *
152          * @param fd a file descriptor
153          */
Builder(@onNull ParcelFileDescriptor fd)154         public Builder(@NonNull ParcelFileDescriptor fd) {
155             this(fd, 0, -1);
156         }
157 
158         /**
159          * Constructs a builder with a file descriptor.
160          *
161          * @param fd a file descriptor
162          * @param offset an offset to of the font data in the file
163          * @param size a size of the font data. If -1 is passed, use until end of the file.
164          */
Builder(@onNull ParcelFileDescriptor fd, @IntRange(from = 0) long offset, @IntRange(from = -1) long size)165         public Builder(@NonNull ParcelFileDescriptor fd, @IntRange(from = 0) long offset,
166                 @IntRange(from = -1) long size) {
167             try (FileInputStream fis = new FileInputStream(fd.getFileDescriptor())) {
168                 final FileChannel fc = fis.getChannel();
169                 size = (size == -1) ? fc.size() - offset : size;
170                 mBuffer = fc.map(FileChannel.MapMode.READ_ONLY, offset, size);
171             } catch (IOException e) {
172                 mException = e;
173             }
174         }
175 
176         /**
177          * Constructs a builder from an asset manager and a file path in an asset directory.
178          *
179          * @param am the application's asset manager
180          * @param path the file name of the font data in the asset directory
181          */
Builder(@onNull AssetManager am, @NonNull String path)182         public Builder(@NonNull AssetManager am, @NonNull String path) {
183             try {
184                 mBuffer = createBuffer(am, path, true /* is asset */, AssetManager.COOKIE_UNKNOWN);
185             } catch (IOException e) {
186                 mException = e;
187             }
188         }
189 
190         /**
191          * Constructs a builder from an asset manager and a file path in an asset directory.
192          *
193          * @param am the application's asset manager
194          * @param path the file name of the font data in the asset directory
195          * @param isAsset true if the undelying data is in asset
196          * @param cookie set asset cookie
197          * @hide
198          */
Builder(@onNull AssetManager am, @NonNull String path, boolean isAsset, int cookie)199         public Builder(@NonNull AssetManager am, @NonNull String path, boolean isAsset,
200                 int cookie) {
201             try {
202                 mBuffer = createBuffer(am, path, isAsset, cookie);
203             } catch (IOException e) {
204                 mException = e;
205             }
206         }
207 
208         /**
209          * Constructs a builder from resources.
210          *
211          * Resource ID must points the font file. XML font can not be used here.
212          *
213          * @param res the resource of this application.
214          * @param resId the resource ID of font file.
215          */
Builder(@onNull Resources res, int resId)216         public Builder(@NonNull Resources res, int resId) {
217             final TypedValue value = new TypedValue();
218             res.getValue(resId, value, true);
219             if (value.string == null) {
220                 mException = new FileNotFoundException(resId + " not found");
221                 return;
222             }
223             final String str = value.string.toString();
224             if (str.toLowerCase().endsWith(".xml")) {
225                 mException = new FileNotFoundException(resId + " must be font file.");
226                 return;
227             }
228 
229             try {
230                 mBuffer = createBuffer(res.getAssets(), str, false, value.assetCookie);
231             } catch (IOException e) {
232                 mException = e;
233             }
234         }
235 
236         /**
237          * Constructs a builder from existing Font instance.
238          *
239          * @param font the font instance.
240          */
Builder(@onNull Font font)241         public Builder(@NonNull Font font) {
242             mFont = font;
243             // Copies all parameters as a default value.
244             mBuffer = font.getBuffer();
245             mWeight = font.getStyle().getWeight();
246             mItalic = font.getStyle().getSlant();
247             mAxes = font.getAxes();
248             mFile = font.getFile();
249             mTtcIndex = font.getTtcIndex();
250         }
251 
252         /**
253          * Creates a buffer containing font data using the assetManager and other
254          * provided inputs.
255          *
256          * @param am the application's asset manager
257          * @param path the file name of the font data in the asset directory
258          * @param isAsset true if the undelying data is in asset
259          * @param cookie set asset cookie
260          * @return buffer containing the contents of the file
261          *
262          * @hide
263          */
createBuffer(@onNull AssetManager am, @NonNull String path, boolean isAsset, int cookie)264         public static ByteBuffer createBuffer(@NonNull AssetManager am, @NonNull String path,
265                                               boolean isAsset, int cookie) throws IOException {
266             Preconditions.checkNotNull(am, "assetManager can not be null");
267             Preconditions.checkNotNull(path, "path can not be null");
268 
269             // Attempt to open as FD, which should work unless the asset is compressed
270             AssetFileDescriptor assetFD;
271             try {
272                 if (isAsset) {
273                     assetFD = am.openFd(path);
274                 } else if (cookie > 0) {
275                     assetFD = am.openNonAssetFd(cookie, path);
276                 } else {
277                     assetFD = am.openNonAssetFd(path);
278                 }
279 
280                 try (FileInputStream fis = assetFD.createInputStream()) {
281                     final FileChannel fc = fis.getChannel();
282                     long startOffset = assetFD.getStartOffset();
283                     long declaredLength = assetFD.getDeclaredLength();
284                     return fc.map(FileChannel.MapMode.READ_ONLY, startOffset, declaredLength);
285                 }
286             } catch (IOException e) {
287                 // failed to open as FD so now we will attempt to open as an input stream
288             }
289 
290             try (InputStream assetStream = isAsset ? am.open(path, AssetManager.ACCESS_BUFFER)
291                     : am.openNonAsset(cookie, path, AssetManager.ACCESS_BUFFER)) {
292 
293                 int capacity = assetStream.available();
294                 ByteBuffer buffer = ByteBuffer.allocateDirect(capacity);
295                 buffer.order(ByteOrder.nativeOrder());
296                 assetStream.read(buffer.array(), buffer.arrayOffset(), assetStream.available());
297 
298                 if (assetStream.read() != -1) {
299                     throw new IOException("Unable to access full contents of " + path);
300                 }
301 
302                 return buffer;
303             }
304         }
305 
306         /**
307          * Sets weight of the font.
308          *
309          * Tells the system the weight of the given font. If this function is not called, the system
310          * will resolve the weight value by reading font tables.
311          *
312          * Here are pairs of the common names and their values.
313          * <p>
314          *  <table>
315          *  <thead>
316          *  <tr>
317          *  <th align="center">Value</th>
318          *  <th align="center">Name</th>
319          *  <th align="center">Android Definition</th>
320          *  </tr>
321          *  </thead>
322          *  <tbody>
323          *  <tr>
324          *  <td align="center">100</td>
325          *  <td align="center">Thin</td>
326          *  <td align="center">{@link FontStyle#FONT_WEIGHT_THIN}</td>
327          *  </tr>
328          *  <tr>
329          *  <td align="center">200</td>
330          *  <td align="center">Extra Light (Ultra Light)</td>
331          *  <td align="center">{@link FontStyle#FONT_WEIGHT_EXTRA_LIGHT}</td>
332          *  </tr>
333          *  <tr>
334          *  <td align="center">300</td>
335          *  <td align="center">Light</td>
336          *  <td align="center">{@link FontStyle#FONT_WEIGHT_LIGHT}</td>
337          *  </tr>
338          *  <tr>
339          *  <td align="center">400</td>
340          *  <td align="center">Normal (Regular)</td>
341          *  <td align="center">{@link FontStyle#FONT_WEIGHT_NORMAL}</td>
342          *  </tr>
343          *  <tr>
344          *  <td align="center">500</td>
345          *  <td align="center">Medium</td>
346          *  <td align="center">{@link FontStyle#FONT_WEIGHT_MEDIUM}</td>
347          *  </tr>
348          *  <tr>
349          *  <td align="center">600</td>
350          *  <td align="center">Semi Bold (Demi Bold)</td>
351          *  <td align="center">{@link FontStyle#FONT_WEIGHT_SEMI_BOLD}</td>
352          *  </tr>
353          *  <tr>
354          *  <td align="center">700</td>
355          *  <td align="center">Bold</td>
356          *  <td align="center">{@link FontStyle#FONT_WEIGHT_BOLD}</td>
357          *  </tr>
358          *  <tr>
359          *  <td align="center">800</td>
360          *  <td align="center">Extra Bold (Ultra Bold)</td>
361          *  <td align="center">{@link FontStyle#FONT_WEIGHT_EXTRA_BOLD}</td>
362          *  </tr>
363          *  <tr>
364          *  <td align="center">900</td>
365          *  <td align="center">Black (Heavy)</td>
366          *  <td align="center">{@link FontStyle#FONT_WEIGHT_BLACK}</td>
367          *  </tr>
368          *  </tbody>
369          * </p>
370          *
371          * @see FontStyle#FONT_WEIGHT_THIN
372          * @see FontStyle#FONT_WEIGHT_EXTRA_LIGHT
373          * @see FontStyle#FONT_WEIGHT_LIGHT
374          * @see FontStyle#FONT_WEIGHT_NORMAL
375          * @see FontStyle#FONT_WEIGHT_MEDIUM
376          * @see FontStyle#FONT_WEIGHT_SEMI_BOLD
377          * @see FontStyle#FONT_WEIGHT_BOLD
378          * @see FontStyle#FONT_WEIGHT_EXTRA_BOLD
379          * @see FontStyle#FONT_WEIGHT_BLACK
380          * @param weight a weight value
381          * @return this builder
382          */
setWeight( @ntRangefrom = FontStyle.FONT_WEIGHT_MIN, to = FontStyle.FONT_WEIGHT_MAX) int weight)383         public @NonNull Builder setWeight(
384                 @IntRange(from = FontStyle.FONT_WEIGHT_MIN, to = FontStyle.FONT_WEIGHT_MAX)
385                 int weight) {
386             Preconditions.checkArgument(
387                     FontStyle.FONT_WEIGHT_MIN <= weight && weight <= FontStyle.FONT_WEIGHT_MAX);
388             mWeight = weight;
389             return this;
390         }
391 
392         /**
393          * Sets italic information of the font.
394          *
395          * Tells the system the style of the given font. If this function is not called, the system
396          * will resolve the style by reading font tables.
397          *
398          * For example, if you want to use italic font as upright font, call {@code
399          * setSlant(FontStyle.FONT_SLANT_UPRIGHT)} explicitly.
400          *
401          * @return this builder
402          */
setSlant(@ontStyle.FontSlant int slant)403         public @NonNull Builder setSlant(@FontStyle.FontSlant int slant) {
404             mItalic = slant == FontStyle.FONT_SLANT_UPRIGHT ? STYLE_NORMAL : STYLE_ITALIC;
405             return this;
406         }
407 
408         /**
409          * Sets an index of the font collection. See {@link android.R.attr#ttcIndex}.
410          *
411          * @param ttcIndex An index of the font collection. If the font source is not font
412          *                 collection, do not call this method or specify 0.
413          * @return this builder
414          */
setTtcIndex(@ntRangefrom = 0) int ttcIndex)415         public @NonNull Builder setTtcIndex(@IntRange(from = 0) int ttcIndex) {
416             mTtcIndex = ttcIndex;
417             return this;
418         }
419 
420         /**
421          * Sets the font variation settings.
422          *
423          * @param variationSettings see {@link FontVariationAxis#fromFontVariationSettings(String)}
424          * @return this builder
425          * @throws IllegalArgumentException If given string is not a valid font variation settings
426          *                                  format.
427          */
setFontVariationSettings(@ullable String variationSettings)428         public @NonNull Builder setFontVariationSettings(@Nullable String variationSettings) {
429             mAxes = FontVariationAxis.fromFontVariationSettings(variationSettings);
430             return this;
431         }
432 
433         /**
434          * Sets the font variation settings.
435          *
436          * @param axes an array of font variation axis tag-value pairs
437          * @return this builder
438          */
setFontVariationSettings(@ullable FontVariationAxis[] axes)439         public @NonNull Builder setFontVariationSettings(@Nullable FontVariationAxis[] axes) {
440             mAxes = axes == null ? null : axes.clone();
441             return this;
442         }
443 
444         /**
445          * Creates the font based on the configured values.
446          * @return the Font object
447          */
build()448         public @NonNull Font build() throws IOException {
449             if (mException != null) {
450                 throw new IOException("Failed to read font contents", mException);
451             }
452             if (mWeight == NOT_SPECIFIED || mItalic == NOT_SPECIFIED) {
453                 final int packed = FontFileUtil.analyzeStyle(mBuffer, mTtcIndex, mAxes);
454                 if (FontFileUtil.isSuccess(packed)) {
455                     if (mWeight == NOT_SPECIFIED) {
456                         mWeight = FontFileUtil.unpackWeight(packed);
457                     }
458                     if (mItalic == NOT_SPECIFIED) {
459                         mItalic = FontFileUtil.unpackItalic(packed) ? STYLE_ITALIC : STYLE_NORMAL;
460                     }
461                 } else {
462                     mWeight = 400;
463                     mItalic = STYLE_NORMAL;
464                 }
465             }
466             mWeight = Math.max(FontStyle.FONT_WEIGHT_MIN,
467                     Math.min(FontStyle.FONT_WEIGHT_MAX, mWeight));
468             final boolean italic = (mItalic == STYLE_ITALIC);
469             final int slant = (mItalic == STYLE_ITALIC)
470                     ? FontStyle.FONT_SLANT_ITALIC : FontStyle.FONT_SLANT_UPRIGHT;
471             final long builderPtr = nInitBuilder();
472             if (mAxes != null) {
473                 for (FontVariationAxis axis : mAxes) {
474                     nAddAxis(builderPtr, axis.getOpenTypeTagValue(), axis.getStyleValue());
475                 }
476             }
477             final ByteBuffer readonlyBuffer = mBuffer.asReadOnlyBuffer();
478             final String filePath = mFile == null ? "" : mFile.getAbsolutePath();
479 
480             long ptr;
481             final Font font;
482             if (mFont == null) {
483                 ptr = nBuild(builderPtr, readonlyBuffer, filePath, mLocaleList, mWeight, italic,
484                         mTtcIndex);
485                 font = new Font(ptr);
486             } else {
487                 ptr = nClone(mFont.getNativePtr(), builderPtr, mWeight, italic, mTtcIndex);
488                 font = new Font(ptr);
489             }
490             return font;
491         }
492 
493         /**
494          * Native methods for creating Font
495          */
nInitBuilder()496         private static native long nInitBuilder();
497         @CriticalNative
nAddAxis(long builderPtr, int tag, float value)498         private static native void nAddAxis(long builderPtr, int tag, float value);
nBuild( long builderPtr, @NonNull ByteBuffer buffer, @NonNull String filePath, @NonNull String localeList, int weight, boolean italic, int ttcIndex)499         private static native long nBuild(
500                 long builderPtr, @NonNull ByteBuffer buffer, @NonNull String filePath,
501                 @NonNull String localeList, int weight, boolean italic, int ttcIndex);
502 
503         @FastNative
nClone(long fontPtr, long builderPtr, int weight, boolean italic, int ttcIndex)504         private static native long nClone(long fontPtr, long builderPtr, int weight,
505                 boolean italic, int ttcIndex);
506     }
507 
508     private final long mNativePtr;  // address of the shared ptr of minikin::Font
509     private final Object mLock = new Object();
510 
511     @GuardedBy("mLock")
512     private @NonNull ByteBuffer mBuffer = null;
513     @GuardedBy("mLock")
514     private boolean mIsFileInitialized = false;
515     @GuardedBy("mLock")
516     private @Nullable File mFile = null;
517     @GuardedBy("mLock")
518     private FontStyle mFontStyle = null;
519     @GuardedBy("mLock")
520     private @Nullable FontVariationAxis[] mAxes = null;
521     @GuardedBy("mLock")
522     private @NonNull LocaleList mLocaleList = null;
523 
524     /**
525      * Use Builder instead
526      *
527      * Caller must increment underlying minikin::Font ref count.
528      * This class takes the ownership of the passing native objects.
529      *
530      * @hide
531      */
Font(long nativePtr)532     public Font(long nativePtr) {
533         mNativePtr = nativePtr;
534 
535         NoImagePreloadHolder.FONT_REGISTRY.registerNativeAllocation(this, mNativePtr);
536     }
537 
538     /**
539      * Returns a font file buffer.
540      *
541      * Duplicate before reading values by {@link ByteBuffer#duplicate()} for avoiding unexpected
542      * reading position sharing.
543      *
544      * @return a font buffer
545      */
getBuffer()546     public @NonNull ByteBuffer getBuffer() {
547         synchronized (mLock) {
548             if (mBuffer == null) {
549                 // Create new instance of native FontWrapper, i.e. incrementing ref count of
550                 // minikin Font instance for keeping buffer fo ByteBuffer reference which may live
551                 // longer than this object.
552                 long ref = nCloneFont(mNativePtr);
553                 ByteBuffer fromNative = nNewByteBuffer(mNativePtr);
554 
555                 // Bind ByteBuffer's lifecycle with underlying font object.
556                 NoImagePreloadHolder.BUFFER_REGISTRY.registerNativeAllocation(fromNative, ref);
557 
558                 // JNI NewDirectBuffer creates writable ByteBuffer even if it is mmaped readonly.
559                 mBuffer = fromNative.asReadOnlyBuffer();
560             }
561             return mBuffer;
562         }
563     }
564 
565     /**
566      * Returns a file path of this font.
567      *
568      * This returns null if this font is not created from regular file.
569      *
570      * @return a file path of the font
571      */
getFile()572     public @Nullable File getFile() {
573         synchronized (mLock) {
574             if (!mIsFileInitialized) {
575                 String path = nGetFontPath(mNativePtr);
576                 if (!TextUtils.isEmpty(path)) {
577                     mFile = new File(path);
578                 }
579                 mIsFileInitialized = true;
580             }
581             return mFile;
582         }
583     }
584 
585     /**
586      * Get a style associated with this font.
587      *
588      * @see Builder#setWeight(int)
589      * @see Builder#setSlant(int)
590      * @return a font style
591      */
getStyle()592     public @NonNull FontStyle getStyle() {
593         synchronized (mLock) {
594             if (mFontStyle == null) {
595                 int packedStyle = nGetPackedStyle(mNativePtr);
596                 mFontStyle = new FontStyle(
597                         FontFileUtil.unpackWeight(packedStyle),
598                         FontFileUtil.unpackItalic(packedStyle)
599                                 ? FontStyle.FONT_SLANT_ITALIC : FontStyle.FONT_SLANT_UPRIGHT);
600             }
601             return mFontStyle;
602         }
603     }
604 
605     /**
606      * Get a TTC index value associated with this font.
607      *
608      * If TTF/OTF file is provided, this value is always 0.
609      *
610      * @see Builder#setTtcIndex(int)
611      * @return a TTC index value
612      */
getTtcIndex()613     public @IntRange(from = 0) int getTtcIndex() {
614         return nGetIndex(mNativePtr);
615     }
616 
617     /**
618      * Get a font variation settings associated with this font
619      *
620      * @see Builder#setFontVariationSettings(String)
621      * @see Builder#setFontVariationSettings(FontVariationAxis[])
622      * @return font variation settings
623      */
getAxes()624     public @Nullable FontVariationAxis[] getAxes() {
625         synchronized (mLock) {
626             if (mAxes == null) {
627                 int axisCount = nGetAxisCount(mNativePtr);
628                 mAxes = new FontVariationAxis[axisCount];
629                 char[] charBuffer = new char[4];
630                 for (int i = 0; i < axisCount; ++i) {
631                     long packedAxis = nGetAxisInfo(mNativePtr, i);
632                     float value = Float.intBitsToFloat((int) (packedAxis & 0x0000_0000_FFFF_FFFFL));
633                     charBuffer[0] = (char) ((packedAxis & 0xFF00_0000_0000_0000L) >>> 56);
634                     charBuffer[1] = (char) ((packedAxis & 0x00FF_0000_0000_0000L) >>> 48);
635                     charBuffer[2] = (char) ((packedAxis & 0x0000_FF00_0000_0000L) >>> 40);
636                     charBuffer[3] = (char) ((packedAxis & 0x0000_00FF_0000_0000L) >>> 32);
637                     mAxes[i] = new FontVariationAxis(new String(charBuffer), value);
638                 }
639             }
640         }
641         return mAxes;
642     }
643 
644     /**
645      * Get a locale list of this font.
646      *
647      * This is always empty if this font is not a system font.
648      * @return a locale list
649      */
getLocaleList()650     public @NonNull LocaleList getLocaleList() {
651         synchronized (mLock) {
652             if (mLocaleList == null) {
653                 String langTags = nGetLocaleList(mNativePtr);
654                 if (TextUtils.isEmpty(langTags)) {
655                     mLocaleList = LocaleList.getEmptyLocaleList();
656                 } else {
657                     mLocaleList = LocaleList.forLanguageTags(langTags);
658                 }
659             }
660             return mLocaleList;
661         }
662     }
663 
664     /**
665      * Retrieve the glyph horizontal advance and bounding box.
666      *
667      * Note that {@link android.graphics.Typeface} in {@link android.graphics.Paint} is ignored.
668      *
669      * @param glyphId a glyph ID
670      * @param paint a paint object used for resolving glyph style
671      * @param outBoundingBox a nullable destination object. If null is passed, this function just
672      *                      return the horizontal advance. If non-null is passed, this function
673      *                      fills bounding box information to this object.
674      * @return the amount of horizontal advance in pixels
675      */
getGlyphBounds(@ntRangefrom = 0) int glyphId, @NonNull Paint paint, @Nullable RectF outBoundingBox)676     public float getGlyphBounds(@IntRange(from = 0) int glyphId, @NonNull Paint paint,
677             @Nullable RectF outBoundingBox) {
678         return nGetGlyphBounds(mNativePtr, glyphId, paint.getNativeInstance(), outBoundingBox);
679     }
680 
681     /**
682      * Retrieve the font metrics information.
683      *
684      * Note that {@link android.graphics.Typeface} in {@link android.graphics.Paint} is ignored.
685      *
686      * @param paint a paint object used for retrieving font metrics.
687      * @param outMetrics a nullable destination object. If null is passed, this function only
688      *                  retrieve recommended interline spacing. If non-null is passed, this function
689      *                  fills to font metrics to it.
690      *
691      * @see Paint#getFontMetrics()
692      * @see Paint#getFontMetricsInt()
693      */
getMetrics(@onNull Paint paint, @Nullable Paint.FontMetrics outMetrics)694     public void getMetrics(@NonNull Paint paint, @Nullable Paint.FontMetrics outMetrics) {
695         nGetFontMetrics(mNativePtr, paint.getNativeInstance(), outMetrics);
696     }
697 
698     /** @hide */
getNativePtr()699     public long getNativePtr() {
700         return mNativePtr;
701     }
702 
703     /**
704      * Returns the unique ID of the source font data.
705      *
706      * You can use this identifier as a key of the cache or checking if two fonts can be
707      * interpolated with font variation settings.
708      * <pre>
709      * <code>
710      *   // Following three Fonts, fontA, fontB, fontC have the same identifier.
711      *   Font fontA = new Font.Builder("/path/to/font").build();
712      *   Font fontB = new Font.Builder(fontA).setTtcIndex(1).build();
713      *   Font fontC = new Font.Builder(fontB).setFontVariationSettings("'wght' 700).build();
714      *
715      *   // Following fontD has the different identifier from above three.
716      *   Font fontD = new Font.Builder("/path/to/another/font").build();
717      *
718      *   // Following fontE has different identifier from above four even the font path is the same.
719      *   // To get the same identifier, please create new Font instance from existing fonts.
720      *   Font fontE = new Font.Builder("/path/to/font").build();
721      * </code>
722      * </pre>
723      *
724      * Here is an example of caching font object that has
725      * <pre>
726      * <code>
727      *   private LongSparseArray<SparseArray<Font>> mCache = new LongSparseArray<>();
728      *
729      *   private Font getFontWeightVariation(Font font, int weight) {
730      *       // Different collection index is treated as different font.
731      *       long key = ((long) font.getSourceIdentifier()) << 32 | (long) font.getTtcIndex();
732      *
733      *       SparseArray<Font> weightCache = mCache.get(key);
734      *       if (weightCache == null) {
735      *          weightCache = new SparseArray<>();
736      *          mCache.put(key, weightCache);
737      *       }
738      *
739      *       Font cachedFont = weightCache.get(weight);
740      *       if (cachedFont != null) {
741      *         return cachedFont;
742      *       }
743      *
744      *       Font newFont = new Font.Builder(cachedFont)
745      *           .setFontVariationSettings("'wght' " + weight);
746      *           .build();
747      *
748      *       weightCache.put(weight, newFont);
749      *       return newFont;
750      *   }
751      * </code>
752      * </pre>
753      * @return an unique identifier for the font source data.
754      */
getSourceIdentifier()755     public int getSourceIdentifier() {
756         return nGetSourceId(mNativePtr);
757     }
758 
759     /**
760      * Returns true if the given font is created from the same source data from this font.
761      *
762      * This method essentially compares {@link ByteBuffer} inside Font, but has some optimization
763      * for faster comparing. This method compares the internal object before going to one-by-one
764      * byte compare with {@link ByteBuffer}. This typically works efficiently if you compares the
765      * font that is created from {@link Builder#Builder(Font)}.
766      *
767      * This API is typically useful for checking if two fonts can be interpolated by font variation
768      * axes. For example, when you call {@link android.text.TextShaper} for the same
769      * string but different style, you may get two font objects which is created from the same
770      * source but have different parameters. You may want to animate between them by interpolating
771      * font variation settings if these fonts are created from the same source.
772      *
773      * @param other a font object to be compared.
774      * @return true if given font is created from the same source from this font. Otherwise false.
775      */
isSameSource(@onNull Font other)776     private boolean isSameSource(@NonNull Font other) {
777         Objects.requireNonNull(other);
778 
779         ByteBuffer myBuffer = getBuffer();
780         ByteBuffer otherBuffer = other.getBuffer();
781 
782         // Shortcut for the same instance.
783         if (myBuffer == otherBuffer) {
784             return true;
785         }
786 
787         // Shortcut for different font buffer check by comparing size.
788         if (myBuffer.capacity() != otherBuffer.capacity()) {
789             return false;
790         }
791 
792         // ByteBuffer#equals compares all bytes which is not performant for e.g. HashMap. Since
793         // underlying native font object holds buffer address, check if this buffer points exactly
794         // the same address as a shortcut of equality. For being compatible with of API30 or before,
795         // check buffer position even if the buffer points the same address.
796         if (getSourceIdentifier() == other.getSourceIdentifier()
797                 && myBuffer.position() == otherBuffer.position()) {
798             return true;
799         }
800 
801         // Unfortunately, need to compare bytes one-by-one since the buffer may be different font
802         // file but has the same file size, or two font has same content but they are allocated
803         // differently. For being compatible with API30 ore before, compare with ByteBuffer#equals.
804         return myBuffer.equals(otherBuffer);
805     }
806 
807     /** @hide */
paramEquals(@onNull Font f)808     public boolean paramEquals(@NonNull Font f) {
809         return f.getStyle().equals(getStyle())
810                 && f.getTtcIndex() == getTtcIndex()
811                 && Arrays.equals(f.getAxes(), getAxes())
812                 && Objects.equals(f.getLocaleList(), getLocaleList())
813                 && Objects.equals(getFile(), f.getFile());
814     }
815 
816     @Override
equals(@ullable Object o)817     public boolean equals(@Nullable Object o) {
818         if (o == this) {
819             return true;
820         }
821         if (!(o instanceof Font)) {
822             return false;
823         }
824 
825         Font f = (Font) o;
826 
827         // The underlying minikin::Font object is the source of the truth of font information. Thus,
828         // Pointer equality is the object equality.
829         if (nGetMinikinFontPtr(mNativePtr) == nGetMinikinFontPtr(f.mNativePtr)) {
830             return true;
831         }
832 
833         if (!paramEquals(f)) {
834             return false;
835         }
836 
837         return isSameSource(f);
838     }
839 
840     @Override
hashCode()841     public int hashCode() {
842         return Objects.hash(
843                 getStyle(),
844                 getTtcIndex(),
845                 Arrays.hashCode(getAxes()),
846                 // Use Buffer size instead of ByteBuffer#hashCode since ByteBuffer#hashCode traverse
847                 // data which is not performant e.g. for HashMap. The hash collision are less likely
848                 // happens because it is unlikely happens the different font files has exactly the
849                 // same size.
850                 getLocaleList());
851     }
852 
853     @Override
toString()854     public String toString() {
855         return "Font {"
856             + "path=" + getFile()
857             + ", style=" + getStyle()
858             + ", ttcIndex=" + getTtcIndex()
859             + ", axes=" + FontVariationAxis.toFontVariationSettings(getAxes())
860             + ", localeList=" + getLocaleList()
861             + ", buffer=" + getBuffer()
862             + "}";
863     }
864 
865     /** @hide */
getAvailableFonts()866     public static Set<Font> getAvailableFonts() {
867         // The font uniqueness is already calculated in the native code. So use IdentityHashMap
868         // for avoiding hash/equals calculation.
869         IdentityHashMap<Font, Font> map = new IdentityHashMap<>();
870         for (long nativePtr : nGetAvailableFontSet()) {
871             Font font = new Font(nativePtr);
872             map.put(font, font);
873         }
874         return Collections.unmodifiableSet(map.keySet());
875     }
876 
877     @CriticalNative
nGetMinikinFontPtr(long font)878     private static native long nGetMinikinFontPtr(long font);
879 
880     @CriticalNative
nCloneFont(long font)881     private static native long nCloneFont(long font);
882 
883     @FastNative
nNewByteBuffer(long font)884     private static native ByteBuffer nNewByteBuffer(long font);
885 
886     @CriticalNative
nGetBufferAddress(long font)887     private static native long nGetBufferAddress(long font);
888 
889     @CriticalNative
nGetSourceId(long font)890     private static native int nGetSourceId(long font);
891 
892     @CriticalNative
nGetReleaseNativeFont()893     private static native long nGetReleaseNativeFont();
894 
895     @FastNative
nGetGlyphBounds(long font, int glyphId, long paint, RectF rect)896     private static native float nGetGlyphBounds(long font, int glyphId, long paint, RectF rect);
897 
898     @FastNative
nGetFontMetrics(long font, long paint, Paint.FontMetrics metrics)899     private static native float nGetFontMetrics(long font, long paint, Paint.FontMetrics metrics);
900 
901     @FastNative
nGetFontPath(long fontPtr)902     private static native String nGetFontPath(long fontPtr);
903 
904     @FastNative
nGetLocaleList(long familyPtr)905     private static native String nGetLocaleList(long familyPtr);
906 
907     @CriticalNative
nGetPackedStyle(long fontPtr)908     private static native int nGetPackedStyle(long fontPtr);
909 
910     @CriticalNative
nGetIndex(long fontPtr)911     private static native int nGetIndex(long fontPtr);
912 
913     @CriticalNative
nGetAxisCount(long fontPtr)914     private static native int nGetAxisCount(long fontPtr);
915 
916     @CriticalNative
nGetAxisInfo(long fontPtr, int i)917     private static native long nGetAxisInfo(long fontPtr, int i);
918 
919     @FastNative
nGetAvailableFontSet()920     private static native long[] nGetAvailableFontSet();
921 }
922