1 /* 2 * Copyright (c) 2003, 2018, 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.lang.ref.WeakReference; 29 import java.awt.FontFormatException; 30 import java.io.FileNotFoundException; 31 import java.io.IOException; 32 import java.io.RandomAccessFile; 33 import java.io.UnsupportedEncodingException; 34 import java.lang.ref.WeakReference; 35 import java.nio.ByteBuffer; 36 import java.nio.ByteOrder; 37 import java.nio.MappedByteBuffer; 38 import java.nio.BufferUnderflowException; 39 import java.nio.channels.ClosedChannelException; 40 import java.nio.channels.FileChannel; 41 import sun.java2d.Disposer; 42 import sun.java2d.DisposerRecord; 43 import java.util.HashSet; 44 import java.util.HashMap; 45 import java.awt.Font; 46 47 /* 48 * Adobe Technical Note 5040 details the format of PFB files. 49 * the file is divided into ascii and binary sections. Each section 50 * starts with a header 51 * 0x8001 - start of binary data, is followed by 4 bytes length, then data 52 * 0x8002 - start of ascii data, is followed by 4 bytes length, then data 53 * 0x8003 - end of data segment 54 * The length is organised as LSB->MSB. 55 * 56 * Note: I experimented with using a MappedByteBuffer and 57 * there were two problems/questions. 58 * 1. If a global buffer is used rather than one allocated in the calling 59 * context, then we need to synchronize on all uses of that data, which 60 * means more code would beed to be synchronized with probable repercussions 61 * elsewhere. 62 * 2. It is not clear whether to free the buffer when the file is closed. 63 * If we have the contents in memory then why keep open files around? 64 * The mmapped buffer doesn't need it. 65 * Also regular GC is what frees the buffer. So closing the file and nulling 66 * out the reference still needs to wait for the buffer to be GC'd to 67 * reclaim the storage. 68 * If the contents of the buffer are persistent there's no need 69 * to worry about synchronization. 70 * Perhaps could use a WeakReference, and when its referent is gone, and 71 * need it can just reopen the file. 72 * Type1 fonts thus don't use up file descriptor references, but can 73 * use memory footprint in a way that's managed by the host O/S. 74 * The main "pain" may be the different model means code needs to be written 75 * without assumptions as to how this is handled by the different subclasses 76 * of FileFont. 77 */ 78 public class Type1Font extends FileFont { 79 80 private static class T1DisposerRecord implements DisposerRecord { 81 String fileName = null; 82 83 T1DisposerRecord(String name) { 84 fileName = name; 85 } 86 87 public synchronized void dispose() { 88 java.security.AccessController.doPrivileged( 89 new java.security.PrivilegedAction<Object>() { 90 public Object run() { 91 92 if (fileName != null) { 93 (new java.io.File(fileName)).delete(); 94 } 95 return null; 96 } 97 }); 98 } 99 } 100 101 WeakReference<Object> bufferRef = new WeakReference<>(null); 102 103 private String psName = null; 104 105 private static HashMap<String, String> styleAbbreviationsMapping; 106 private static HashSet<String> styleNameTokes; 107 108 static { 109 styleAbbreviationsMapping = new HashMap<>(); 110 styleNameTokes = new HashSet<>(); 111 112 /* These abbreviation rules are taken from Appendix 1 of Adobe Technical Note #5088 */ 113 /* NB: this list is not complete - we did not include abbreviations which contain 114 several capital letters because current expansion algorithm do not support this. 115 (namely we have omited MM aka "Multiple Master", OsF aka "Oldstyle figures", 116 OS aka "Oldstyle", SC aka "Small caps" and DS aka "Display" */ 117 String[] nm = {"Black", "Bold", "Book", "Demi", "Heavy", "Light", 118 "Meduium", "Nord", "Poster", "Regular", "Super", "Thin", 119 "Compressed", "Condensed", "Compact", "Extended", "Narrow", 120 "Inclined", "Italic", "Kursiv", "Oblique", "Upright", "Sloped", 121 "Semi", "Ultra", "Extra", 122 "Alternate", "Alternate", "Deutsche Fraktur", "Expert", "Inline", "Ornaments", 123 "Outline", "Roman", "Rounded", "Script", "Shaded", "Swash", "Titling", "Typewriter"}; 124 String[] abbrv = {"Blk", "Bd", "Bk", "Dm", "Hv", "Lt", 125 "Md", "Nd", "Po", "Rg", "Su", "Th", 126 "Cm", "Cn", "Ct", "Ex", "Nr", 127 "Ic", "It", "Ks", "Obl", "Up", "Sl", 128 "Sm", "Ult", "X", 129 "A", "Alt", "Dfr", "Exp", "In", "Or", 130 "Ou", "Rm", "Rd", "Scr", "Sh", "Sw", "Ti", "Typ"}; 131 /* This is only subset of names from nm[] because we want to distinguish things 132 like "Lucida Sans TypeWriter Bold" and "Lucida Sans Bold". 133 Names from "Design and/or special purpose" group are omitted. */ 134 String[] styleTokens = {"Black", "Bold", "Book", "Demi", "Heavy", "Light", 135 "Medium", "Nord", "Poster", "Regular", "Super", "Thin", 136 "Compressed", "Condensed", "Compact", "Extended", "Narrow", 137 "Inclined", "Italic", "Kursiv", "Oblique", "Upright", "Sloped", "Slanted", 138 "Semi", "Ultra", "Extra"}; 139 140 for(int i=0; i<nm.length; i++) { 141 styleAbbreviationsMapping.put(abbrv[i], nm[i]); 142 } 143 for(int i=0; i<styleTokens.length; i++) { 144 styleNameTokes.add(styleTokens[i]); 145 } 146 } 147 148 149 /** 150 * Constructs a Type1 Font. 151 * @param platname - Platform identifier of the font. Typically file name. 152 * @param nativeNames - Native names - typically XLFDs on Unix. 153 */ 154 public Type1Font(String platname, Object nativeNames) 155 throws FontFormatException { 156 157 this(platname, nativeNames, false); 158 } 159 160 /** 161 * - does basic verification of the file 162 * - reads the names (full, family). 163 * - determines the style of the font. 164 * @throws FontFormatException if the font can't be opened 165 * or fails verification, or there's no usable cmap 166 */ 167 public Type1Font(String platname, Object nativeNames, boolean createdCopy) 168 throws FontFormatException { 169 super(platname, nativeNames); 170 fontRank = Font2D.TYPE1_RANK; 171 try { 172 verify(); 173 } catch (Throwable t) { 174 if (createdCopy) { 175 T1DisposerRecord ref = new T1DisposerRecord(platname); 176 Disposer.addObjectRecord(bufferRef, ref); 177 bufferRef = null; 178 } 179 if (t instanceof FontFormatException) { 180 throw (FontFormatException)t; 181 } else { 182 throw new FontFormatException("Unexpected runtime exception."); 183 } 184 } 185 } 186 187 private synchronized ByteBuffer getBuffer() throws FontFormatException { 188 MappedByteBuffer mapBuf = (MappedByteBuffer)bufferRef.get(); 189 if (mapBuf == null) { 190 //System.out.println("open T1 " + platName); 191 try { 192 RandomAccessFile raf = (RandomAccessFile) 193 java.security.AccessController.doPrivileged( 194 new java.security.PrivilegedAction<Object>() { 195 public Object run() { 196 try { 197 return new RandomAccessFile(platName, "r"); 198 } catch (FileNotFoundException ffne) { 199 } 200 return null; 201 } 202 }); 203 FileChannel fc = raf.getChannel(); 204 fileSize = (int)fc.size(); 205 mapBuf = fc.map(FileChannel.MapMode.READ_ONLY, 0, fileSize); 206 mapBuf.position(0); 207 bufferRef = new WeakReference<>(mapBuf); 208 fc.close(); 209 } catch (NullPointerException e) { 210 throw new FontFormatException(e.toString()); 211 } catch (ClosedChannelException e) { 212 /* NIO I/O is interruptible, recurse to retry operation. 213 * Clear interrupts before recursing in case NIO didn't. 214 */ 215 Thread.interrupted(); 216 return getBuffer(); 217 } catch (IOException e) { 218 throw new FontFormatException(e.toString()); 219 } 220 } 221 return mapBuf; 222 } 223 224 protected void close() { 225 } 226 227 /* called from native code to read file into a direct byte buffer */ 228 void readFile(ByteBuffer buffer) { 229 RandomAccessFile raf = null; 230 FileChannel fc; 231 try { 232 raf = (RandomAccessFile) 233 java.security.AccessController.doPrivileged( 234 new java.security.PrivilegedAction<Object>() { 235 public Object run() { 236 try { 237 return new RandomAccessFile(platName, "r"); 238 } catch (FileNotFoundException fnfe) { 239 } 240 return null; 241 } 242 }); 243 fc = raf.getChannel(); 244 while (buffer.remaining() > 0 && fc.read(buffer) != -1) {} 245 } catch (NullPointerException npe) { 246 } catch (ClosedChannelException e) { 247 try { 248 if (raf != null) { 249 raf.close(); 250 raf = null; 251 } 252 } catch (IOException ioe) { 253 } 254 /* NIO I/O is interruptible, recurse to retry operation. 255 * Clear interrupts before recursing in case NIO didn't. 256 */ 257 Thread.interrupted(); 258 readFile(buffer); 259 } catch (IOException e) { 260 } finally { 261 if (raf != null) { 262 try { 263 raf.close(); 264 } catch (IOException e) { 265 } 266 } 267 } 268 } 269 270 public synchronized ByteBuffer readBlock(int offset, int length) { 271 ByteBuffer mappedBuf = null; 272 try { 273 mappedBuf = getBuffer(); 274 if (offset > fileSize) { 275 offset = fileSize; 276 } 277 mappedBuf.position(offset); 278 return mappedBuf.slice(); 279 } catch (FontFormatException e) { 280 return null; 281 } 282 } 283 284 private void verify() throws FontFormatException { 285 /* Normal usage should not call getBuffer(), as its state 286 * ie endianness, position etc, are shared. verify() can do 287 * this as its called only from within the constructor before 288 * there are other users of this object. 289 */ 290 ByteBuffer bb = getBuffer(); 291 if (bb.capacity() < 6) { 292 throw new FontFormatException("short file"); 293 } 294 int val = bb.get(0) & 0xff; 295 if ((bb.get(0) & 0xff) == 0x80) { 296 verifyPFB(bb); 297 bb.position(6); 298 } else { 299 verifyPFA(bb); 300 bb.position(0); 301 } 302 initNames(bb); 303 if (familyName == null || fullName == null) { 304 throw new FontFormatException("Font name not found"); 305 } 306 setStyle(); 307 } 308 309 public int getFileSize() { 310 if (fileSize == 0) { 311 try { 312 getBuffer(); 313 } catch (FontFormatException e) { 314 } 315 } 316 return fileSize; 317 } 318 319 private void verifyPFA(ByteBuffer bb) throws FontFormatException { 320 if (bb.getShort() != 0x2521) { // 0x2521 is %! 321 throw new FontFormatException("bad pfa font"); 322 } 323 // remind - additional verification needed? 324 } 325 326 private void verifyPFB(ByteBuffer bb) throws FontFormatException { 327 328 int pos = 0; 329 while (true) { 330 try { 331 int segType = bb.getShort(pos) & 0xffff; 332 if (segType == 0x8001 || segType == 0x8002) { 333 bb.order(ByteOrder.LITTLE_ENDIAN); 334 int segLen = bb.getInt(pos+2); 335 bb.order(ByteOrder.BIG_ENDIAN); 336 if (segLen <= 0) { 337 throw new FontFormatException("bad segment length"); 338 } 339 pos += segLen+6; 340 } else if (segType == 0x8003) { 341 return; 342 } else { 343 throw new FontFormatException("bad pfb file"); 344 } 345 } catch (BufferUnderflowException bue) { 346 throw new FontFormatException(bue.toString()); 347 } catch (Exception e) { 348 throw new FontFormatException(e.toString()); 349 } 350 } 351 } 352 353 private static final int PSEOFTOKEN = 0; 354 private static final int PSNAMETOKEN = 1; 355 private static final int PSSTRINGTOKEN = 2; 356 357 /* Need to parse the ascii contents of the Type1 font file, 358 * looking for FullName, FamilyName and FontName. 359 * If explicit names are not found then extract them from first text line. 360 * Operating on bytes so can't use Java String utilities, which 361 * is a large part of why this is a hack. 362 * 363 * Also check for mandatory FontType and verify if it is supported. 364 */ 365 private void initNames(ByteBuffer bb) throws FontFormatException { 366 boolean eof = false; 367 String fontType = null; 368 try { 369 //Parse font looking for explicit FullName, FamilyName and FontName 370 // (according to Type1 spec they are optional) 371 while ((fullName == null || familyName == null || psName == null || fontType == null) && !eof) { 372 int tokenType = nextTokenType(bb); 373 if (tokenType == PSNAMETOKEN) { 374 int pos = bb.position(); 375 if (bb.get(pos) == 'F') { 376 String s = getSimpleToken(bb); 377 if ("FullName".equals(s)) { 378 if (nextTokenType(bb)==PSSTRINGTOKEN) { 379 fullName = getString(bb); 380 } 381 } else if ("FamilyName".equals(s)) { 382 if (nextTokenType(bb)==PSSTRINGTOKEN) { 383 familyName = getString(bb); 384 } 385 } else if ("FontName".equals(s)) { 386 if (nextTokenType(bb)==PSNAMETOKEN) { 387 psName = getSimpleToken(bb); 388 } 389 } else if ("FontType".equals(s)) { 390 /* look for 391 /FontType id def 392 */ 393 String token = getSimpleToken(bb); 394 if ("def".equals(getSimpleToken(bb))) { 395 fontType = token; 396 } 397 } 398 } else { 399 while (bb.get() > ' '); // skip token 400 } 401 } else if (tokenType == PSEOFTOKEN) { 402 eof = true; 403 } 404 } 405 } catch (Exception e) { 406 throw new FontFormatException(e.toString()); 407 } 408 409 /* Ignore all fonts besides Type1 (e.g. Type3 fonts) */ 410 if (!"1".equals(fontType)) { 411 throw new FontFormatException("Unsupported font type"); 412 } 413 414 if (psName == null) { //no explicit FontName 415 // Try to extract font name from the first text line. 416 // According to Type1 spec first line consist of 417 // "%!FontType1-SpecVersion: FontName FontVersion" 418 // or 419 // "%!PS-AdobeFont-1.0: FontName version" 420 bb.position(0); 421 if (bb.getShort() != 0x2521) { //if pfb (do not start with "%!") 422 //skip segment header and "%!" 423 bb.position(8); 424 //NB: assume that first segment is ASCII one 425 // (is it possible to have valid Type1 font with first binary segment?) 426 } 427 String formatType = getSimpleToken(bb); 428 if (!formatType.startsWith("FontType1-") && !formatType.startsWith("PS-AdobeFont-")) { 429 throw new FontFormatException("Unsupported font format [" + formatType + "]"); 430 } 431 psName = getSimpleToken(bb); 432 } 433 434 //if we got to the end of file then we did not find at least one of FullName or FamilyName 435 //Try to deduce missing names from present ones 436 //NB: At least psName must be already initialized by this moment 437 if (eof) { 438 //if we find fullName or familyName then use it as another name too 439 if (fullName != null) { 440 familyName = fullName2FamilyName(fullName); 441 } else if (familyName != null) { 442 fullName = familyName; 443 } else { //fallback - use postscript font name to deduce full and family names 444 fullName = psName2FullName(psName); 445 familyName = psName2FamilyName(psName); 446 } 447 } 448 } 449 450 private String fullName2FamilyName(String name) { 451 String res, token; 452 int len, start, end; //length of family name part 453 454 //FamilyName is truncated version of FullName 455 //Truncated tail must contain only style modifiers 456 457 end = name.length(); 458 459 while (end > 0) { 460 start = end - 1; 461 while (start > 0 && name.charAt(start) != ' ') 462 start--; 463 //as soon as we meet first non style token truncate 464 // current tail and return 465 if (!isStyleToken(name.substring(start+1, end))) { 466 return name.substring(0, end); 467 } 468 end = start; 469 } 470 471 return name; //should not happen 472 } 473 474 private String expandAbbreviation(String abbr) { 475 if (styleAbbreviationsMapping.containsKey(abbr)) 476 return styleAbbreviationsMapping.get(abbr); 477 return abbr; 478 } 479 480 private boolean isStyleToken(String token) { 481 return styleNameTokes.contains(token); 482 } 483 484 private String psName2FullName(String name) { 485 String res; 486 int pos; 487 488 //According to Adobe technical note #5088 psName (aka FontName) has form 489 // <Family Name><VendorID>-<Weight><Width><Slant><Character Set> 490 //where spaces are not allowed. 491 492 //Conversion: Expand abbreviations in style portion (everything after '-'), 493 // replace '-' with space and insert missing spaces 494 pos = name.indexOf('-'); 495 if (pos >= 0) { 496 res = expandName(name.substring(0, pos), false); 497 res += " " + expandName(name.substring(pos+1), true); 498 } else { 499 res = expandName(name, false); 500 } 501 502 return res; 503 } 504 505 private String psName2FamilyName(String name) { 506 String tmp = name; 507 508 //According to Adobe technical note #5088 psName (aka FontName) has form 509 // <Family Name><VendorID>-<Weight><Width><Slant><Character Set> 510 //where spaces are not allowed. 511 512 //Conversion: Truncate style portion (everything after '-') 513 // and insert missing spaces 514 515 if (tmp.indexOf('-') > 0) { 516 tmp = tmp.substring(0, tmp.indexOf('-')); 517 } 518 519 return expandName(tmp, false); 520 } 521 522 private int nextCapitalLetter(String s, int off) { 523 for (; (off >=0) && off < s.length(); off++) { 524 if (s.charAt(off) >= 'A' && s.charAt(off) <= 'Z') 525 return off; 526 } 527 return -1; 528 } 529 530 private String expandName(String s, boolean tryExpandAbbreviations) { 531 StringBuilder res = new StringBuilder(s.length() + 10); 532 int start=0, end; 533 534 while(start < s.length()) { 535 end = nextCapitalLetter(s, start + 1); 536 if (end < 0) { 537 end = s.length(); 538 } 539 540 if (start != 0) { 541 res.append(" "); 542 } 543 544 if (tryExpandAbbreviations) { 545 res.append(expandAbbreviation(s.substring(start, end))); 546 } else { 547 res.append(s.substring(start, end)); 548 } 549 start = end; 550 } 551 552 return res.toString(); 553 } 554 555 /* skip lines beginning with "%" and leading white space on a line */ 556 private byte skip(ByteBuffer bb) { 557 byte b = bb.get(); 558 while (b == '%') { 559 while (true) { 560 b = bb.get(); 561 if (b == '\r' || b == '\n') { 562 break; 563 } 564 } 565 } 566 while (b <= ' ') { 567 b = bb.get(); 568 } 569 return b; 570 } 571 572 /* 573 * Token types: 574 * PSNAMETOKEN - / 575 * PSSTRINGTOKEN - literal text string 576 */ 577 private int nextTokenType(ByteBuffer bb) { 578 579 try { 580 byte b = skip(bb); 581 582 while (true) { 583 if (b == (byte)'/') { // PS defined name follows. 584 return PSNAMETOKEN; 585 } else if (b == (byte)'(') { // PS string follows 586 return PSSTRINGTOKEN; 587 } else if ((b == (byte)'\r') || (b == (byte)'\n')) { 588 b = skip(bb); 589 } else { 590 b = bb.get(); 591 } 592 } 593 } catch (BufferUnderflowException e) { 594 return PSEOFTOKEN; 595 } 596 } 597 598 /* Read simple token (sequence of non-whitespace characters) 599 starting from the current position. 600 Skip leading whitespaces (if any). */ 601 private String getSimpleToken(ByteBuffer bb) { 602 while (bb.get() <= ' '); 603 int pos1 = bb.position()-1; 604 while (bb.get() > ' '); 605 int pos2 = bb.position(); 606 byte[] nameBytes = new byte[pos2-pos1-1]; 607 bb.position(pos1); 608 bb.get(nameBytes); 609 try { 610 return new String(nameBytes, "US-ASCII"); 611 } catch (UnsupportedEncodingException e) { 612 return new String(nameBytes); 613 } 614 } 615 616 private String getString(ByteBuffer bb) { 617 int pos1 = bb.position(); 618 while (bb.get() != ')'); 619 int pos2 = bb.position(); 620 byte[] nameBytes = new byte[pos2-pos1-1]; 621 bb.position(pos1); 622 bb.get(nameBytes); 623 try { 624 return new String(nameBytes, "US-ASCII"); 625 } catch (UnsupportedEncodingException e) { 626 return new String(nameBytes); 627 } 628 } 629 630 631 public String getPostscriptName() { 632 return psName; 633 } 634 635 protected synchronized FontScaler getScaler() { 636 if (scaler == null) { 637 scaler = FontScaler.getScaler(this, 0, false, fileSize); 638 } 639 640 return scaler; 641 } 642 643 CharToGlyphMapper getMapper() { 644 if (mapper == null) { 645 mapper = new Type1GlyphMapper(this); 646 } 647 return mapper; 648 } 649 650 public int getNumGlyphs() { 651 try { 652 return getScaler().getNumGlyphs(); 653 } catch (FontScalerException e) { 654 scaler = FontScaler.getNullScaler(); 655 return getNumGlyphs(); 656 } 657 } 658 659 public int getMissingGlyphCode() { 660 try { 661 return getScaler().getMissingGlyphCode(); 662 } catch (FontScalerException e) { 663 scaler = FontScaler.getNullScaler(); 664 return getMissingGlyphCode(); 665 } 666 } 667 668 public int getGlyphCode(char charCode) { 669 try { 670 return getScaler().getGlyphCode(charCode); 671 } catch (FontScalerException e) { 672 scaler = FontScaler.getNullScaler(); 673 return getGlyphCode(charCode); 674 } 675 } 676 677 public String toString() { 678 return "** Type1 Font: Family="+familyName+ " Name="+fullName+ 679 " style="+style+" fileName="+getPublicFileName(); 680 } 681 }