1 /* 2 * Copyright (c) 2008, 2019, Oracle and/or its affiliates. All rights reserved. 3 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. 4 * 5 * This code is free software; you can redistribute it and/or modify it 6 * under the terms of the GNU General Public License version 2 only, as 7 * published by the Free Software Foundation. Oracle designates this 8 * particular file as subject to the "Classpath" exception as provided 9 * by Oracle in the LICENSE file that accompanied this code. 10 * 11 * This code is distributed in the hope that it will be useful, but WITHOUT 12 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 13 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License 14 * version 2 for more details (a copy is included in the LICENSE file that 15 * accompanied this code). 16 * 17 * You should have received a copy of the GNU General Public License version 18 * 2 along with this work; if not, write to the Free Software Foundation, 19 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. 20 * 21 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA 22 * or visit www.oracle.com if you need additional information or have any 23 * questions. 24 */ 25 26 package sun.font; 27 28 import java.awt.Font; 29 import java.io.File; 30 import java.io.FileInputStream; 31 import java.io.FileOutputStream; 32 import java.io.IOException; 33 import java.net.InetAddress; 34 import java.net.UnknownHostException; 35 import java.nio.charset.Charset; 36 import java.nio.charset.StandardCharsets; 37 import java.nio.file.Files; 38 import java.util.HashMap; 39 import java.util.HashSet; 40 import java.util.Locale; 41 import java.util.Properties; 42 import java.util.Scanner; 43 import sun.awt.FcFontManager; 44 import sun.awt.FontConfiguration; 45 import sun.awt.FontDescriptor; 46 import sun.awt.SunToolkit; 47 import sun.font.CompositeFontDescriptor; 48 import sun.font.FontManager; 49 import sun.font.FontConfigManager.FontConfigInfo; 50 import sun.font.FontConfigManager.FcCompFont; 51 import sun.font.FontConfigManager.FontConfigFont; 52 import sun.java2d.SunGraphicsEnvironment; 53 import sun.util.logging.PlatformLogger; 54 55 public class FcFontConfiguration extends FontConfiguration { 56 57 /** Version of the cache file format understood by this code. 58 * Its part of the file name so that we can rev this at 59 * any time, even in a minor JDK update. 60 * It is stored as the value of the "version" property. 61 * This is distinct from the version of "libfontconfig" that generated 62 * the cached results, and which is the "fcversion" property in the file. 63 * {@code FontConfiguration.getVersion()} also returns a version string, 64 * and has meant the version of the fontconfiguration.properties file 65 * that was read. Since this class doesn't use such files, then what 66 * that really means is whether the methods on this class return 67 * values that are compatible with the classes that do directly read 68 * from such files. It is a compatible subset of version "1". 69 */ 70 private static final String fileVersion = "1"; 71 private String fcInfoFileName = null; 72 73 private FcCompFont[] fcCompFonts = null; 74 75 public FcFontConfiguration(SunFontManager fm) { 76 super(fm); 77 init(); 78 } 79 80 /* This isn't called but is needed to satisfy super-class contract. */ 81 public FcFontConfiguration(SunFontManager fm, 82 boolean preferLocaleFonts, 83 boolean preferPropFonts) { 84 super(fm, preferLocaleFonts, preferPropFonts); 85 init(); 86 } 87 88 @Override 89 public synchronized boolean init() { 90 if (fcCompFonts != null) { 91 return true; 92 } 93 94 setFontConfiguration(); 95 readFcInfo(); 96 FcFontManager fm = (FcFontManager) fontManager; 97 FontConfigManager fcm = fm.getFontConfigManager(); 98 if (fcCompFonts == null) { 99 fcCompFonts = fcm.loadFontConfig(); 100 if (fcCompFonts != null) { 101 try { 102 writeFcInfo(); 103 } catch (Exception e) { 104 if (FontUtilities.debugFonts()) { 105 warning("Exception writing fcInfo " + e); 106 } 107 } 108 } else if (FontUtilities.debugFonts()) { 109 warning("Failed to get info from libfontconfig"); 110 } 111 } else { 112 fcm.populateFontConfig(fcCompFonts); 113 } 114 115 if (fcCompFonts == null) { 116 return false; // couldn't load fontconfig. 117 } 118 119 // NB already in a privileged block from SGE 120 String javaHome = System.getProperty("java.home"); 121 if (javaHome == null) { 122 throw new Error("java.home property not set"); 123 } 124 String javaLib = javaHome + File.separator + "lib"; 125 getInstalledFallbackFonts(javaLib); 126 127 return true; 128 } 129 130 @Override 131 public String getFallbackFamilyName(String fontName, 132 String defaultFallback) { 133 // maintain compatibility with old font.properties files, which either 134 // had aliases for TimesRoman & Co. or defined mappings for them. 135 String compatibilityName = getCompatibilityFamilyName(fontName); 136 if (compatibilityName != null) { 137 return compatibilityName; 138 } 139 return defaultFallback; 140 } 141 142 @Override 143 protected String 144 getFaceNameFromComponentFontName(String componentFontName) { 145 return null; 146 } 147 148 @Override 149 protected String 150 getFileNameFromComponentFontName(String componentFontName) { 151 return null; 152 } 153 154 @Override 155 public String getFileNameFromPlatformName(String platformName) { 156 /* Platform name is the file name, but rather than returning 157 * the arg, return null*/ 158 return null; 159 } 160 161 @Override 162 protected Charset getDefaultFontCharset(String fontName) { 163 return Charset.forName("ISO8859_1"); 164 } 165 166 @Override 167 protected String getEncoding(String awtFontName, 168 String characterSubsetName) { 169 return "default"; 170 } 171 172 @Override 173 protected void initReorderMap() { 174 reorderMap = new HashMap<>(); 175 } 176 177 @Override 178 protected FontDescriptor[] buildFontDescriptors(int fontIndex, int styleIndex) { 179 CompositeFontDescriptor[] cfi = get2DCompositeFontInfo(); 180 int idx = fontIndex * NUM_STYLES + styleIndex; 181 String[] componentFaceNames = cfi[idx].getComponentFaceNames(); 182 FontDescriptor[] ret = new FontDescriptor[componentFaceNames.length]; 183 for (int i = 0; i < componentFaceNames.length; i++) { 184 ret[i] = new FontDescriptor(componentFaceNames[i], StandardCharsets.ISO_8859_1.newEncoder(), new int[0]); 185 } 186 187 return ret; 188 } 189 190 @Override 191 public int getNumberCoreFonts() { 192 return 1; 193 } 194 195 @Override 196 public String[] getPlatformFontNames() { 197 HashSet<String> nameSet = new HashSet<String>(); 198 FcFontManager fm = (FcFontManager) fontManager; 199 FontConfigManager fcm = fm.getFontConfigManager(); 200 FcCompFont[] fcCompFonts = fcm.loadFontConfig(); 201 for (int i=0; i<fcCompFonts.length; i++) { 202 for (int j=0; j<fcCompFonts[i].allFonts.length; j++) { 203 nameSet.add(fcCompFonts[i].allFonts[j].fontFile); 204 } 205 } 206 return nameSet.toArray(new String[0]); 207 } 208 209 @Override 210 public String getExtraFontPath() { 211 return null; 212 } 213 214 @Override 215 public boolean needToSearchForFile(String fileName) { 216 return false; 217 } 218 219 private FontConfigFont[] getFcFontList(FcCompFont[] fcFonts, 220 String fontname, int style) { 221 222 if (fontname.equals("dialog")) { 223 fontname = "sansserif"; 224 } else if (fontname.equals("dialoginput")) { 225 fontname = "monospaced"; 226 } 227 for (int i=0; i<fcFonts.length; i++) { 228 if (fontname.equals(fcFonts[i].jdkName) && 229 style == fcFonts[i].style) { 230 return fcFonts[i].allFonts; 231 } 232 } 233 return fcFonts[0].allFonts; 234 } 235 236 @Override 237 public CompositeFontDescriptor[] get2DCompositeFontInfo() { 238 239 FcFontManager fm = (FcFontManager) fontManager; 240 FontConfigManager fcm = fm.getFontConfigManager(); 241 FcCompFont[] fcCompFonts = fcm.loadFontConfig(); 242 243 CompositeFontDescriptor[] result = 244 new CompositeFontDescriptor[NUM_FONTS * NUM_STYLES]; 245 246 for (int fontIndex = 0; fontIndex < NUM_FONTS; fontIndex++) { 247 String fontName = publicFontNames[fontIndex]; 248 249 for (int styleIndex = 0; styleIndex < NUM_STYLES; styleIndex++) { 250 251 String faceName = fontName + "." + styleNames[styleIndex]; 252 FontConfigFont[] fcFonts = 253 getFcFontList(fcCompFonts, 254 fontNames[fontIndex], styleIndex); 255 256 int numFonts = fcFonts.length; 257 // fall back fonts listed in the lib/fonts/fallback directory 258 if (installedFallbackFontFiles != null) { 259 numFonts += installedFallbackFontFiles.length; 260 } 261 262 String[] fileNames = new String[numFonts]; 263 String[] faceNames = new String[numFonts]; 264 265 int index; 266 for (index = 0; index < fcFonts.length; index++) { 267 fileNames[index] = fcFonts[index].fontFile; 268 faceNames[index] = fcFonts[index].fullName; 269 } 270 271 if (installedFallbackFontFiles != null) { 272 System.arraycopy(installedFallbackFontFiles, 0, 273 fileNames, fcFonts.length, 274 installedFallbackFontFiles.length); 275 } 276 277 result[fontIndex * NUM_STYLES + styleIndex] 278 = new CompositeFontDescriptor( 279 faceName, 280 1, 281 faceNames, 282 fileNames, 283 null, null); 284 } 285 } 286 return result; 287 } 288 289 /** 290 * Gets the OS version string from a Linux release-specific file. 291 */ 292 private String getVersionString(File f){ 293 try { 294 Scanner sc = new Scanner(f); 295 return sc.findInLine("(\\d)+((\\.)(\\d)+)*"); 296 } 297 catch (Exception e){ 298 } 299 return null; 300 } 301 302 /** 303 * Sets the OS name and version from environment information. 304 */ 305 @Override 306 protected void setOsNameAndVersion() { 307 308 super.setOsNameAndVersion(); 309 310 if (!osName.equals("Linux")) { 311 return; 312 } 313 try { 314 File f; 315 if ((f = new File("/etc/lsb-release")).canRead()) { 316 /* Ubuntu and (perhaps others) use only lsb-release. 317 * Syntax and encoding is compatible with java properties. 318 * For Ubuntu the ID is "Ubuntu". 319 */ 320 Properties props = new Properties(); 321 props.load(new FileInputStream(f)); 322 osName = props.getProperty("DISTRIB_ID"); 323 osVersion = props.getProperty("DISTRIB_RELEASE"); 324 } else if ((f = new File("/etc/redhat-release")).canRead()) { 325 osName = "RedHat"; 326 osVersion = getVersionString(f); 327 } else if ((f = new File("/etc/SuSE-release")).canRead()) { 328 osName = "SuSE"; 329 osVersion = getVersionString(f); 330 } else if ((f = new File("/etc/turbolinux-release")).canRead()) { 331 osName = "Turbo"; 332 osVersion = getVersionString(f); 333 } else if ((f = new File("/etc/fedora-release")).canRead()) { 334 osName = "Fedora"; 335 osVersion = getVersionString(f); 336 } 337 } catch (Exception e) { 338 if (FontUtilities.debugFonts()) { 339 warning("Exception identifying Linux distro."); 340 } 341 } 342 } 343 344 private File getFcInfoFile() { 345 if (fcInfoFileName == null) { 346 // NB need security permissions to get true IP address, and 347 // we should have those as the whole initialisation is in a 348 // doPrivileged block. But in this case no exception is thrown, 349 // and it returns the loop back address, and so we end up with 350 // "localhost" 351 String hostname; 352 try { 353 hostname = InetAddress.getLocalHost().getHostName(); 354 } catch (UnknownHostException e) { 355 hostname = "localhost"; 356 } 357 String userDir = System.getProperty("user.home"); 358 String version = System.getProperty("java.version"); 359 String fs = File.separator; 360 String dir = userDir+fs+".java"+fs+"fonts"+fs+version; 361 Locale locale = SunToolkit.getStartupLocale(); 362 String lang = locale.getLanguage(); 363 String country = locale.getCountry(); 364 String name = "fcinfo-"+fileVersion+"-"+hostname+"-"+ 365 osName+"-"+osVersion+"-"+lang+"-"+country+".properties"; 366 fcInfoFileName = dir+fs+name; 367 } 368 return new File(fcInfoFileName); 369 } 370 371 private void writeFcInfo() { 372 Properties props = new Properties(); 373 props.setProperty("version", fileVersion); 374 FcFontManager fm = (FcFontManager) fontManager; 375 FontConfigManager fcm = fm.getFontConfigManager(); 376 FontConfigInfo fcInfo = fcm.getFontConfigInfo(); 377 props.setProperty("fcversion", Integer.toString(fcInfo.fcVersion)); 378 if (fcInfo.cacheDirs != null) { 379 for (int i=0;i<fcInfo.cacheDirs.length;i++) { 380 if (fcInfo.cacheDirs[i] != null) { 381 props.setProperty("cachedir."+i, fcInfo.cacheDirs[i]); 382 } 383 } 384 } 385 for (int i=0; i<fcCompFonts.length; i++) { 386 FcCompFont fci = fcCompFonts[i]; 387 String styleKey = fci.jdkName+"."+fci.style; 388 props.setProperty(styleKey+".length", 389 Integer.toString(fci.allFonts.length)); 390 for (int j=0; j<fci.allFonts.length; j++) { 391 props.setProperty(styleKey+"."+j+".file", 392 fci.allFonts[j].fontFile); 393 if (fci.allFonts[j].fullName != null) { 394 props.setProperty(styleKey+"."+j+".fullName", 395 fci.allFonts[j].fullName); 396 } 397 } 398 } 399 try { 400 /* This writes into a temp file then renames when done. 401 * Since the rename is an atomic action within the same 402 * directory no client will ever see a partially written file. 403 */ 404 File fcInfoFile = getFcInfoFile(); 405 File dir = fcInfoFile.getParentFile(); 406 dir.mkdirs(); 407 File tempFile = Files.createTempFile(dir.toPath(), "fcinfo", null).toFile(); 408 FileOutputStream fos = new FileOutputStream(tempFile); 409 props.store(fos, 410 "JDK Font Configuration Generated File: *Do Not Edit*"); 411 fos.close(); 412 boolean renamed = tempFile.renameTo(fcInfoFile); 413 if (!renamed && FontUtilities.debugFonts()) { 414 System.out.println("rename failed"); 415 warning("Failed renaming file to "+ getFcInfoFile()); 416 } 417 } catch (Exception e) { 418 if (FontUtilities.debugFonts()) { 419 warning("IOException writing to "+ getFcInfoFile()); 420 } 421 } 422 } 423 424 /* We want to be able to use this cache instead of invoking 425 * fontconfig except when we can detect the system cache has changed. 426 * But there doesn't seem to be a way to find the location of 427 * the system cache. 428 */ 429 private void readFcInfo() { 430 File fcFile = getFcInfoFile(); 431 if (!fcFile.exists()) { 432 if (FontUtilities.debugFonts()) { 433 warning("fontconfig info file " + fcFile.toString() + " does not exist"); 434 } 435 return; 436 } 437 Properties props = new Properties(); 438 FcFontManager fm = (FcFontManager) fontManager; 439 FontConfigManager fcm = fm.getFontConfigManager(); 440 try { 441 FileInputStream fis = new FileInputStream(fcFile); 442 props.load(fis); 443 fis.close(); 444 } catch (IOException e) { 445 if (FontUtilities.debugFonts()) { 446 warning("IOException reading from "+fcFile.toString()); 447 } 448 return; 449 } 450 String version = (String)props.get("version"); 451 if (version == null || !version.equals(fileVersion)) { 452 if (FontUtilities.debugFonts()) { 453 warning("fontconfig info file caused a version mismatch"); 454 } 455 return; 456 } 457 458 // If there's a new, different fontconfig installed on the 459 // system, we invalidate our fontconfig file. 460 String fcVersionStr = (String)props.get("fcversion"); 461 if (fcVersionStr != null) { 462 int fcVersion; 463 try { 464 fcVersion = Integer.parseInt(fcVersionStr); 465 if (fcVersion != 0 && 466 fcVersion != FontConfigManager.getFontConfigVersion()) { 467 if (FontUtilities.debugFonts()) { 468 warning("new, different fontconfig detected"); 469 } 470 return; 471 } 472 } catch (Exception e) { 473 if (FontUtilities.debugFonts()) { 474 warning("Exception parsing version " + fcVersionStr); 475 } 476 return; 477 } 478 } 479 480 // If we can locate the fontconfig cache dirs, then compare the 481 // time stamp of those with our properties file. If we are out 482 // of date then re-generate. 483 long lastModified = fcFile.lastModified(); 484 int cacheDirIndex = 0; 485 while (cacheDirIndex<4) { // should never be more than 2 anyway. 486 String dir = (String)props.get("cachedir."+cacheDirIndex); 487 if (dir == null) { 488 break; 489 } 490 File dirFile = new File(dir); 491 if (dirFile.exists() && dirFile.lastModified() > lastModified) { 492 if (FontUtilities.debugFonts()) { 493 warning("Out of date cache directories detected"); 494 } 495 return; 496 } 497 cacheDirIndex++; 498 } 499 500 String[] names = { "sansserif", "serif", "monospaced" }; 501 String[] fcnames = { "sans", "serif", "monospace" }; 502 int namesLen = names.length; 503 int numStyles = 4; 504 FcCompFont[] fci = new FcCompFont[namesLen*numStyles]; 505 506 try { 507 for (int i=0; i<namesLen; i++) { 508 for (int s=0; s<numStyles; s++) { 509 int index = i*numStyles+s; 510 fci[index] = new FcCompFont(); 511 String key = names[i]+"."+s; 512 fci[index].jdkName = names[i]; 513 fci[index].fcFamily = fcnames[i]; 514 fci[index].style = s; 515 String lenStr = (String)props.get(key+".length"); 516 int nfonts = Integer.parseInt(lenStr); 517 if (nfonts <= 0) { 518 if (FontUtilities.debugFonts()) { 519 warning("Bad non-positive .length entry in fontconfig file " + fcFile.toString()); 520 } 521 return; // bad file 522 } 523 fci[index].allFonts = new FontConfigFont[nfonts]; 524 for (int f=0; f<nfonts; f++) { 525 fci[index].allFonts[f] = new FontConfigFont(); 526 String fkey = key+"."+f+".fullName"; 527 String fullName = (String)props.get(fkey); 528 fci[index].allFonts[f].fullName = fullName; 529 fkey = key+"."+f+".file"; 530 String file = (String)props.get(fkey); 531 if (file == null) { 532 if (FontUtilities.debugFonts()) { 533 warning("Missing file value for key " + fkey + " in fontconfig file " + fcFile.toString()); 534 } 535 return; // bad file 536 } 537 fci[index].allFonts[f].fontFile = file; 538 } 539 fci[index].firstFont = fci[index].allFonts[0]; 540 541 } 542 } 543 fcCompFonts = fci; 544 } catch (Throwable t) { 545 if (FontUtilities.debugFonts()) { 546 warning(t.toString()); 547 } 548 } 549 550 if (FontUtilities.debugFonts()) { 551 PlatformLogger logger = FontUtilities.getLogger(); 552 logger.info("successfully parsed the fontconfig file at " + fcFile.toString()); 553 } 554 } 555 556 private static void warning(String msg) { 557 PlatformLogger logger = PlatformLogger.getLogger("sun.awt.FontConfiguration"); 558 logger.warning(msg); 559 } 560 }