1 /* 2 * Copyright (C) 2014 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; 18 19 import com.android.ide.common.rendering.api.ILayoutLog; 20 import com.android.layoutlib.bridge.Bridge; 21 import com.android.layoutlib.bridge.impl.DelegateManager; 22 import com.android.tools.layoutlib.annotations.LayoutlibDelegate; 23 24 import android.annotation.NonNull; 25 import android.annotation.Nullable; 26 import android.graphics.fonts.FontVariationAxis; 27 28 import java.awt.Font; 29 import java.awt.FontFormatException; 30 import java.io.File; 31 import java.io.FileNotFoundException; 32 import java.nio.ByteBuffer; 33 import java.util.ArrayList; 34 import java.util.Collections; 35 import java.util.HashSet; 36 import java.util.LinkedHashMap; 37 import java.util.List; 38 import java.util.Map; 39 import java.util.Objects; 40 import java.util.Scanner; 41 import java.util.Set; 42 import java.util.logging.Logger; 43 44 import libcore.util.NativeAllocationRegistry_Delegate; 45 46 import static android.graphics.Typeface.RESOLVE_BY_FONT_TABLE; 47 import static android.graphics.Typeface_Delegate.SYSTEM_FONTS; 48 49 /** 50 * Delegate implementing the native methods of android.graphics.FontFamily 51 * 52 * Through the layoutlib_create tool, the original native methods of FontFamily have been replaced 53 * by calls to methods of the same name in this delegate class. 54 * 55 * This class behaves like the original native implementation, but in Java, keeping previously 56 * native data into its own objects and mapping them to int that are sent back and forth between 57 * it and the original FontFamily class. 58 * 59 * @see DelegateManager 60 */ 61 public class FontFamily_Delegate { 62 63 public static final int DEFAULT_FONT_WEIGHT = 400; 64 public static final int BOLD_FONT_WEIGHT_DELTA = 300; 65 public static final int BOLD_FONT_WEIGHT = 700; 66 67 private static final String FONT_SUFFIX_ITALIC = "Italic.ttf"; 68 private static final String FN_ALL_FONTS_LIST = "fontsInSdk.txt"; 69 private static final String EXTENSION_OTF = ".otf"; 70 71 private static final int CACHE_SIZE = 10; 72 // The cache has a drawback that if the font file changed after the font object was created, 73 // we will not update it. 74 private static final Map<String, FontInfo> sCache = 75 new LinkedHashMap<String, FontInfo>(CACHE_SIZE) { 76 @Override 77 protected boolean removeEldestEntry(Map.Entry<String, FontInfo> eldest) { 78 return size() > CACHE_SIZE; 79 } 80 81 @Override 82 public FontInfo put(String key, FontInfo value) { 83 // renew this entry. 84 FontInfo removed = remove(key); 85 super.put(key, value); 86 return removed; 87 } 88 }; 89 90 /** 91 * A class associating {@link Font} with its metadata. 92 */ 93 public static final class FontInfo { 94 @Nullable 95 public Font mFont; 96 public int mWeight; 97 public boolean mIsItalic; 98 99 @Override equals(Object o)100 public boolean equals(Object o) { 101 if (this == o) { 102 return true; 103 } 104 if (o == null || getClass() != o.getClass()) { 105 return false; 106 } 107 FontInfo fontInfo = (FontInfo) o; 108 return mWeight == fontInfo.mWeight && mIsItalic == fontInfo.mIsItalic; 109 } 110 111 @Override hashCode()112 public int hashCode() { 113 return Objects.hash(mWeight, mIsItalic); 114 } 115 116 @Override toString()117 public String toString() { 118 return "FontInfo{" + "mWeight=" + mWeight + ", mIsItalic=" + mIsItalic + '}'; 119 } 120 } 121 122 // ---- delegate manager ---- 123 private static final DelegateManager<FontFamily_Delegate> sManager = 124 new DelegateManager<FontFamily_Delegate>(FontFamily_Delegate.class); 125 private static long sFamilyFinalizer = -1; 126 127 // ---- delegate helper data ---- 128 private static String sFontLocation; 129 private static final List<FontFamily_Delegate> sPostInitDelegate = new 130 ArrayList<FontFamily_Delegate>(); 131 private static Set<String> SDK_FONTS; 132 133 134 // ---- delegate data ---- 135 136 // Order does not really matter but we use a LinkedHashMap to get reproducible results across 137 // render calls 138 private Map<FontInfo, Font> mFonts = new LinkedHashMap<>(); 139 140 /** 141 * The variant of the Font Family - compact or elegant. 142 * <p/> 143 * 0 is unspecified, 1 is compact and 2 is elegant. This needs to be kept in sync with values in 144 * android.graphics.FontFamily 145 * 146 * @see Paint#setElegantTextHeight(boolean) 147 */ 148 private FontVariant mVariant; 149 // List of runnables to process fonts after sFontLoader is initialized. 150 private List<Runnable> mPostInitRunnables = new ArrayList<Runnable>(); 151 /** @see #isValid() */ 152 private boolean mValid = false; 153 154 155 // ---- Public helper class ---- 156 157 public enum FontVariant { 158 // The order needs to be kept in sync with android.graphics.FontFamily. 159 NONE, COMPACT, ELEGANT 160 } 161 162 // ---- Public Helper methods ---- 163 getDelegate(long nativeFontFamily)164 public static FontFamily_Delegate getDelegate(long nativeFontFamily) { 165 return sManager.getDelegate(nativeFontFamily); 166 } 167 setFontLocation(String fontLocation)168 public static synchronized void setFontLocation(String fontLocation) { 169 sFontLocation = fontLocation; 170 // init list of bundled fonts. 171 File allFonts = new File(fontLocation, FN_ALL_FONTS_LIST); 172 // Current number of fonts is 103. Use the next round number to leave scope for more fonts 173 // in the future. 174 Set<String> allFontsList = new HashSet<>(128); 175 Scanner scanner = null; 176 try { 177 scanner = new Scanner(allFonts); 178 while (scanner.hasNext()) { 179 String name = scanner.next(); 180 // Skip font configuration files. 181 if (!name.endsWith(".xml")) { 182 allFontsList.add(name); 183 } 184 } 185 } catch (FileNotFoundException e) { 186 Bridge.getLog().error(ILayoutLog.TAG_BROKEN, 187 "Unable to load the list of fonts. Try re-installing the SDK Platform from the SDK Manager.", 188 e, null, null); 189 } finally { 190 if (scanner != null) { 191 scanner.close(); 192 } 193 } 194 SDK_FONTS = Collections.unmodifiableSet(allFontsList); 195 for (FontFamily_Delegate fontFamily : sPostInitDelegate) { 196 fontFamily.init(); 197 } 198 sPostInitDelegate.clear(); 199 } 200 201 @Nullable getFont(int desiredWeight, boolean isItalic)202 public Font getFont(int desiredWeight, boolean isItalic) { 203 FontInfo desiredStyle = new FontInfo(); 204 desiredStyle.mWeight = desiredWeight; 205 desiredStyle.mIsItalic = isItalic; 206 207 Font cachedFont = mFonts.get(desiredStyle); 208 if (cachedFont != null) { 209 return cachedFont; 210 } 211 212 FontInfo bestFont = null; 213 214 if (mFonts.size() == 1) { 215 // No need to compute the match since we only have one candidate 216 bestFont = mFonts.keySet().iterator().next(); 217 } else { 218 int bestMatch = Integer.MAX_VALUE; 219 220 for (FontInfo font : mFonts.keySet()) { 221 int match = computeMatch(font, desiredStyle); 222 if (match < bestMatch) { 223 bestMatch = match; 224 bestFont = font; 225 if (bestMatch == 0) { 226 break; 227 } 228 } 229 } 230 } 231 232 if (bestFont == null) { 233 return null; 234 } 235 236 237 // Derive the font as required and add it to the list of Fonts. 238 deriveFont(bestFont, desiredStyle); 239 addFont(desiredStyle); 240 return desiredStyle.mFont; 241 } 242 getVariant()243 public FontVariant getVariant() { 244 return mVariant; 245 } 246 247 /** 248 * Returns if the FontFamily should contain any fonts. If this returns true and 249 * {@link #getFont(int, boolean)} returns an empty list, it means that an error occurred while 250 * loading the fonts. However, some fonts are deliberately skipped, for example they are not 251 * bundled with the SDK. In such a case, this method returns false. 252 */ isValid()253 public boolean isValid() { 254 return mValid; 255 } 256 loadFont(String path)257 private static Font loadFont(String path) { 258 if (path.startsWith(SYSTEM_FONTS) ) { 259 String relativePath = path.substring(SYSTEM_FONTS.length()); 260 File f = new File(sFontLocation, relativePath); 261 262 try { 263 return Font.createFont(Font.TRUETYPE_FONT, f); 264 } catch (Exception e) { 265 if (path.endsWith(EXTENSION_OTF) && e instanceof FontFormatException) { 266 // If we aren't able to load an Open Type font, don't log a warning just yet. 267 // We wait for a case where font is being used. Only then we try to log the 268 // warning. 269 return null; 270 } 271 Bridge.getLog().fidelityWarning(ILayoutLog.TAG_BROKEN, 272 String.format("Unable to load font %1$s", relativePath), 273 e, null, null); 274 } 275 } else { 276 Bridge.getLog().fidelityWarning(ILayoutLog.TAG_UNSUPPORTED, 277 "Only platform fonts located in " + SYSTEM_FONTS + "can be loaded.", 278 null, null, null); 279 } 280 281 return null; 282 } 283 284 @Nullable getFontLocation()285 public static String getFontLocation() { 286 return sFontLocation; 287 } 288 289 // ---- delegate methods ---- 290 @LayoutlibDelegate addFont(FontFamily thisFontFamily, String path, int ttcIndex, FontVariationAxis[] axes, int weight, int italic)291 /*package*/ static boolean addFont(FontFamily thisFontFamily, String path, int ttcIndex, 292 FontVariationAxis[] axes, int weight, int italic) { 293 if (thisFontFamily.mBuilderPtr == 0) { 294 assert false : "Unable to call addFont after freezing."; 295 return false; 296 } 297 final FontFamily_Delegate delegate = getDelegate(thisFontFamily.mBuilderPtr); 298 return delegate != null && delegate.addFont(path, ttcIndex, weight, italic); 299 } 300 301 // ---- native methods ---- 302 303 @LayoutlibDelegate nInitBuilder(String lang, int variant)304 /*package*/ static long nInitBuilder(String lang, int variant) { 305 // TODO: support lang. This is required for japanese locale. 306 FontFamily_Delegate delegate = new FontFamily_Delegate(); 307 // variant can be 0, 1 or 2. 308 assert variant < 3; 309 delegate.mVariant = FontVariant.values()[variant]; 310 if (sFontLocation != null) { 311 delegate.init(); 312 } else { 313 sPostInitDelegate.add(delegate); 314 } 315 return sManager.addNewDelegate(delegate); 316 } 317 318 @LayoutlibDelegate 319 /*package*/ static long nCreateFamily(long builderPtr) { 320 return builderPtr; 321 } 322 323 @LayoutlibDelegate 324 /*package*/ static long nGetFamilyReleaseFunc() { 325 synchronized (FontFamily_Delegate.class) { 326 if (sFamilyFinalizer == -1) { 327 sFamilyFinalizer = NativeAllocationRegistry_Delegate.createFinalizer( 328 sManager::removeJavaReferenceFor); 329 } 330 } 331 return sFamilyFinalizer; 332 } 333 334 @LayoutlibDelegate 335 /*package*/ static boolean nAddFont(long builderPtr, ByteBuffer font, int ttcIndex, 336 int weight, int isItalic) { 337 assert false : "The only client of this method has been overridden."; 338 return false; 339 } 340 341 @LayoutlibDelegate 342 /*package*/ static boolean nAddFontWeightStyle(long builderPtr, ByteBuffer font, 343 int ttcIndex, int weight, int isItalic) { 344 assert false : "The only client of this method has been overridden."; 345 return false; 346 } 347 348 @LayoutlibDelegate 349 /*package*/ static void nAddAxisValue(long builderPtr, int tag, float value) { 350 assert false : "The only client of this method has been overridden."; 351 } 352 353 static boolean addFont(long builderPtr, final String path, final int weight, 354 final boolean isItalic) { 355 final FontFamily_Delegate delegate = getDelegate(builderPtr); 356 int italic = isItalic ? 1 : 0; 357 if (delegate != null) { 358 if (sFontLocation == null) { 359 delegate.mPostInitRunnables.add(() -> delegate.addFont(path, weight, italic)); 360 return true; 361 } 362 return delegate.addFont(path, weight, italic); 363 } 364 return false; 365 } 366 367 @LayoutlibDelegate 368 /*package*/ static long nGetBuilderReleaseFunc() { 369 // Layoutlib uses the same reference for the builder and the font family, 370 // so it should not release that reference at the builder stage. 371 return -1; 372 } 373 374 // ---- private helper methods ---- 375 376 private void init() { 377 for (Runnable postInitRunnable : mPostInitRunnables) { 378 postInitRunnable.run(); 379 } 380 mPostInitRunnables = null; 381 } 382 383 private boolean addFont(final String path, int ttcIndex, int weight, int italic) { 384 // FIXME: support ttc fonts. Hack JRE?? 385 if (sFontLocation == null) { 386 mPostInitRunnables.add(() -> addFont(path, weight, italic)); 387 return true; 388 } 389 return addFont(path, weight, italic); 390 } 391 392 private boolean addFont(@NonNull String path) { 393 return addFont(path, DEFAULT_FONT_WEIGHT, path.endsWith(FONT_SUFFIX_ITALIC) ? 1 : RESOLVE_BY_FONT_TABLE); 394 } 395 396 private boolean addFont(@NonNull String path, int weight, int italic) { 397 if (path.startsWith(SYSTEM_FONTS) && 398 !SDK_FONTS.contains(path.substring(SYSTEM_FONTS.length()))) { 399 Logger.getLogger(FontFamily_Delegate.class.getSimpleName()).warning("Unable to load font " + path); 400 return mValid = false; 401 } 402 // Set valid to true, even if the font fails to load. 403 mValid = true; 404 Font font = loadFont(path); 405 if (font == null) { 406 return false; 407 } 408 FontInfo fontInfo = new FontInfo(); 409 fontInfo.mFont = font; 410 fontInfo.mWeight = weight; 411 fontInfo.mIsItalic = italic == RESOLVE_BY_FONT_TABLE ? font.isItalic() : italic == 1; 412 addFont(fontInfo); 413 return true; 414 } 415 416 private boolean addFont(@NonNull FontInfo fontInfo) { 417 return mFonts.putIfAbsent(fontInfo, fontInfo.mFont) == null; 418 } 419 420 /** 421 * Compute matching metric between two styles - 0 is an exact match. 422 */ 423 public static int computeMatch(@NonNull FontInfo font1, @NonNull FontInfo font2) { 424 int score = Math.abs(font1.mWeight / 100 - font2.mWeight / 100); 425 if (font1.mIsItalic != font2.mIsItalic) { 426 score += 2; 427 } 428 return score; 429 } 430 431 /** 432 * Try to derive a font from {@code srcFont} for the style in {@code outFont}. 433 * <p/> 434 * {@code outFont} is updated to reflect the style of the derived font. 435 * @param srcFont the source font 436 * @param outFont contains the desired font style. Updated to contain the derived font and 437 * its style 438 */ 439 public static void deriveFont(@NonNull FontInfo srcFont, @NonNull FontInfo outFont) { 440 int desiredWeight = outFont.mWeight; 441 int srcWeight = srcFont.mWeight; 442 assert srcFont.mFont != null; 443 Font derivedFont = srcFont.mFont; 444 int derivedStyle = 0; 445 // Embolden the font if required. 446 if (desiredWeight >= BOLD_FONT_WEIGHT && desiredWeight - srcWeight > BOLD_FONT_WEIGHT_DELTA / 2) { 447 derivedStyle |= Font.BOLD; 448 srcWeight += BOLD_FONT_WEIGHT_DELTA; 449 } 450 // Italicize the font if required. 451 if (outFont.mIsItalic && !srcFont.mIsItalic) { 452 derivedStyle |= Font.ITALIC; 453 } else if (outFont.mIsItalic != srcFont.mIsItalic) { 454 // The desired font is plain, but the src font is italics. We can't convert it back. So 455 // we update the value to reflect the true style of the font we're deriving. 456 outFont.mIsItalic = srcFont.mIsItalic; 457 } 458 459 if (derivedStyle != 0) { 460 derivedFont = derivedFont.deriveFont(derivedStyle); 461 } 462 463 outFont.mFont = derivedFont; 464 outFont.mWeight = srcWeight; 465 // No need to update mIsItalics, as it's already been handled above. 466 } 467 } 468