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