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