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