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 */, 0 /* cookie */); 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 @CriticalNative nGetReleaseNativeFont()501 private static native long nGetReleaseNativeFont(); 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 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 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