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