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 checkedNatives = true; 172 try { 173 verify(); 174 } catch (Throwable t) { 175 if (createdCopy) { 176 T1DisposerRecord ref = new T1DisposerRecord(platname); 177 Disposer.addObjectRecord(bufferRef, ref); 178 bufferRef = null; 179 } 180 if (t instanceof FontFormatException) { 181 throw (FontFormatException)t; 182 } else { 183 throw new FontFormatException("Unexpected runtime exception."); 184 } 185 } 186 } 187 188 private synchronized ByteBuffer getBuffer() throws FontFormatException { 189 MappedByteBuffer mapBuf = (MappedByteBuffer)bufferRef.get(); 190 if (mapBuf == null) { 191 //System.out.println("open T1 " + platName); 192 try { 193 RandomAccessFile raf = (RandomAccessFile) 194 java.security.AccessController.doPrivileged( 195 new java.security.PrivilegedAction<Object>() { 196 public Object run() { 197 try { 198 return new RandomAccessFile(platName, "r"); 199 } catch (FileNotFoundException ffne) { 200 } 201 return null; 202 } 203 }); 204 FileChannel fc = raf.getChannel(); 205 fileSize = (int)fc.size(); 206 mapBuf = fc.map(FileChannel.MapMode.READ_ONLY, 0, fileSize); 207 mapBuf.position(0); 208 bufferRef = new WeakReference<>(mapBuf); 209 fc.close(); 210 } catch (NullPointerException e) { 211 throw new FontFormatException(e.toString()); 212 } catch (ClosedChannelException e) { 213 /* NIO I/O is interruptible, recurse to retry operation. 214 * Clear interrupts before recursing in case NIO didn't. 215 */ 216 Thread.interrupted(); 217 return getBuffer(); 218 } catch (IOException e) { 219 throw new FontFormatException(e.toString()); 220 } 221 } 222 return mapBuf; 223 } 224 225 protected void close() { 226 } 227 228 /* called from native code to read file into a direct byte buffer */ 229 void readFile(ByteBuffer buffer) { 230 RandomAccessFile raf = null; 231 FileChannel fc; 232 try { 233 raf = (RandomAccessFile) 234 java.security.AccessController.doPrivileged( 235 new java.security.PrivilegedAction<Object>() { 236 public Object run() { 237 try { 238 return new RandomAccessFile(platName, "r"); 239 } catch (FileNotFoundException fnfe) { 240 } 241 return null; 242 } 243 }); 244 fc = raf.getChannel(); 245 while (buffer.remaining() > 0 && fc.read(buffer) != -1) {} 246 } catch (NullPointerException npe) { 247 } catch (ClosedChannelException e) { 248 try { 249 if (raf != null) { 250 raf.close(); 251 raf = null; 252 } 253 } catch (IOException ioe) { 254 } 255 /* NIO I/O is interruptible, recurse to retry operation. 256 * Clear interrupts before recursing in case NIO didn't. 257 */ 258 Thread.interrupted(); 259 readFile(buffer); 260 } catch (IOException e) { 261 } finally { 262 if (raf != null) { 263 try { 264 raf.close(); 265 } catch (IOException e) { 266 } 267 } 268 } 269 } 270 271 public synchronized ByteBuffer readBlock(int offset, int length) { 272 ByteBuffer mappedBuf = null; 273 try { 274 mappedBuf = getBuffer(); 275 if (offset > fileSize) { 276 offset = fileSize; 277 } 278 mappedBuf.position(offset); 279 return mappedBuf.slice(); 280 } catch (FontFormatException e) { 281 return null; 282 } 283 } 284 285 private void verify() throws FontFormatException { 286 /* Normal usage should not call getBuffer(), as its state 287 * ie endianness, position etc, are shared. verify() can do 288 * this as its called only from within the constructor before 289 * there are other users of this object. 290 */ 291 ByteBuffer bb = getBuffer(); 292 if (bb.capacity() < 6) { 293 throw new FontFormatException("short file"); 294 } 295 int val = bb.get(0) & 0xff; 296 if ((bb.get(0) & 0xff) == 0x80) { 297 verifyPFB(bb); 298 bb.position(6); 299 } else { 300 verifyPFA(bb); 301 bb.position(0); 302 } 303 initNames(bb); 304 if (familyName == null || fullName == null) { 305 throw new FontFormatException("Font name not found"); 306 } 307 setStyle(); 308 } 309 310 public int getFileSize() { 311 if (fileSize == 0) { 312 try { 313 getBuffer(); 314 } catch (FontFormatException e) { 315 } 316 } 317 return fileSize; 318 } 319 320 private void verifyPFA(ByteBuffer bb) throws FontFormatException { 321 if (bb.getShort() != 0x2521) { // 0x2521 is %! 322 throw new FontFormatException("bad pfa font"); 323 } 324 // remind - additional verification needed? 325 } 326 327 private void verifyPFB(ByteBuffer bb) throws FontFormatException { 328 329 int pos = 0; 330 while (true) { 331 try { 332 int segType = bb.getShort(pos) & 0xffff; 333 if (segType == 0x8001 || segType == 0x8002) { 334 bb.order(ByteOrder.LITTLE_ENDIAN); 335 int segLen = bb.getInt(pos+2); 336 bb.order(ByteOrder.BIG_ENDIAN); 337 if (segLen <= 0) { 338 throw new FontFormatException("bad segment length"); 339 } 340 pos += segLen+6; 341 } else if (segType == 0x8003) { 342 return; 343 } else { 344 throw new FontFormatException("bad pfb file"); 345 } 346 } catch (BufferUnderflowException bue) { 347 throw new FontFormatException(bue.toString()); 348 } catch (Exception e) { 349 throw new FontFormatException(e.toString()); 350 } 351 } 352 } 353 354 private static final int PSEOFTOKEN = 0; 355 private static final int PSNAMETOKEN = 1; 356 private static final int PSSTRINGTOKEN = 2; 357 358 /* Need to parse the ascii contents of the Type1 font file, 359 * looking for FullName, FamilyName and FontName. 360 * If explicit names are not found then extract them from first text line. 361 * Operating on bytes so can't use Java String utilities, which 362 * is a large part of why this is a hack. 363 * 364 * Also check for mandatory FontType and verify if it is supported. 365 */ 366 private void initNames(ByteBuffer bb) throws FontFormatException { 367 boolean eof = false; 368 String fontType = null; 369 try { 370 //Parse font looking for explicit FullName, FamilyName and FontName 371 // (according to Type1 spec they are optional) 372 while ((fullName == null || familyName == null || psName == null || fontType == null) && !eof) { 373 int tokenType = nextTokenType(bb); 374 if (tokenType == PSNAMETOKEN) { 375 int pos = bb.position(); 376 if (bb.get(pos) == 'F') { 377 String s = getSimpleToken(bb); 378 if ("FullName".equals(s)) { 379 if (nextTokenType(bb)==PSSTRINGTOKEN) { 380 fullName = getString(bb); 381 } 382 } else if ("FamilyName".equals(s)) { 383 if (nextTokenType(bb)==PSSTRINGTOKEN) { 384 familyName = getString(bb); 385 } 386 } else if ("FontName".equals(s)) { 387 if (nextTokenType(bb)==PSNAMETOKEN) { 388 psName = getSimpleToken(bb); 389 } 390 } else if ("FontType".equals(s)) { 391 /* look for 392 /FontType id def 393 */ 394 String token = getSimpleToken(bb); 395 if ("def".equals(getSimpleToken(bb))) { 396 fontType = token; 397 } 398 } 399 } else { 400 while (bb.get() > ' '); // skip token 401 } 402 } else if (tokenType == PSEOFTOKEN) { 403 eof = true; 404 } 405 } 406 } catch (Exception e) { 407 throw new FontFormatException(e.toString()); 408 } 409 410 /* Ignore all fonts besides Type1 (e.g. Type3 fonts) */ 411 if (!"1".equals(fontType)) { 412 throw new FontFormatException("Unsupported font type"); 413 } 414 415 if (psName == null) { //no explicit FontName 416 // Try to extract font name from the first text line. 417 // According to Type1 spec first line consist of 418 // "%!FontType1-SpecVersion: FontName FontVersion" 419 // or 420 // "%!PS-AdobeFont-1.0: FontName version" 421 bb.position(0); 422 if (bb.getShort() != 0x2521) { //if pfb (do not start with "%!") 423 //skip segment header and "%!" 424 bb.position(8); 425 //NB: assume that first segment is ASCII one 426 // (is it possible to have valid Type1 font with first binary segment?) 427 } 428 String formatType = getSimpleToken(bb); 429 if (!formatType.startsWith("FontType1-") && !formatType.startsWith("PS-AdobeFont-")) { 430 throw new FontFormatException("Unsupported font format [" + formatType + "]"); 431 } 432 psName = getSimpleToken(bb); 433 } 434 435 //if we got to the end of file then we did not find at least one of FullName or FamilyName 436 //Try to deduce missing names from present ones 437 //NB: At least psName must be already initialized by this moment 438 if (eof) { 439 //if we find fullName or familyName then use it as another name too 440 if (fullName != null) { 441 familyName = fullName2FamilyName(fullName); 442 } else if (familyName != null) { 443 fullName = familyName; 444 } else { //fallback - use postscript font name to deduce full and family names 445 fullName = psName2FullName(psName); 446 familyName = psName2FamilyName(psName); 447 } 448 } 449 } 450 451 private String fullName2FamilyName(String name) { 452 String res, token; 453 int len, start, end; //length of family name part 454 455 //FamilyName is truncated version of FullName 456 //Truncated tail must contain only style modifiers 457 458 end = name.length(); 459 460 while (end > 0) { 461 start = end - 1; 462 while (start > 0 && name.charAt(start) != ' ') 463 start--; 464 //as soon as we meet first non style token truncate 465 // current tail and return 466 if (!isStyleToken(name.substring(start+1, end))) { 467 return name.substring(0, end); 468 } 469 end = start; 470 } 471 472 return name; //should not happen 473 } 474 475 private String expandAbbreviation(String abbr) { 476 if (styleAbbreviationsMapping.containsKey(abbr)) 477 return styleAbbreviationsMapping.get(abbr); 478 return abbr; 479 } 480 481 private boolean isStyleToken(String token) { 482 return styleNameTokes.contains(token); 483 } 484 485 private String psName2FullName(String name) { 486 String res; 487 int pos; 488 489 //According to Adobe technical note #5088 psName (aka FontName) has form 490 // <Family Name><VendorID>-<Weight><Width><Slant><Character Set> 491 //where spaces are not allowed. 492 493 //Conversion: Expand abbreviations in style portion (everything after '-'), 494 // replace '-' with space and insert missing spaces 495 pos = name.indexOf('-'); 496 if (pos >= 0) { 497 res = expandName(name.substring(0, pos), false); 498 res += " " + expandName(name.substring(pos+1), true); 499 } else { 500 res = expandName(name, false); 501 } 502 503 return res; 504 } 505 506 private String psName2FamilyName(String name) { 507 String tmp = name; 508 509 //According to Adobe technical note #5088 psName (aka FontName) has form 510 // <Family Name><VendorID>-<Weight><Width><Slant><Character Set> 511 //where spaces are not allowed. 512 513 //Conversion: Truncate style portion (everything after '-') 514 // and insert missing spaces 515 516 if (tmp.indexOf('-') > 0) { 517 tmp = tmp.substring(0, tmp.indexOf('-')); 518 } 519 520 return expandName(tmp, false); 521 } 522 523 private int nextCapitalLetter(String s, int off) { 524 for (; (off >=0) && off < s.length(); off++) { 525 if (s.charAt(off) >= 'A' && s.charAt(off) <= 'Z') 526 return off; 527 } 528 return -1; 529 } 530 531 private String expandName(String s, boolean tryExpandAbbreviations) { 532 StringBuilder res = new StringBuilder(s.length() + 10); 533 int start=0, end; 534 535 while(start < s.length()) { 536 end = nextCapitalLetter(s, start + 1); 537 if (end < 0) { 538 end = s.length(); 539 } 540 541 if (start != 0) { 542 res.append(" "); 543 } 544 545 if (tryExpandAbbreviations) { 546 res.append(expandAbbreviation(s.substring(start, end))); 547 } else { 548 res.append(s.substring(start, end)); 549 } 550 start = end; 551 } 552 553 return res.toString(); 554 } 555 556 /* skip lines beginning with "%" and leading white space on a line */ 557 private byte skip(ByteBuffer bb) { 558 byte b = bb.get(); 559 while (b == '%') { 560 while (true) { 561 b = bb.get(); 562 if (b == '\r' || b == '\n') { 563 break; 564 } 565 } 566 } 567 while (b <= ' ') { 568 b = bb.get(); 569 } 570 return b; 571 } 572 573 /* 574 * Token types: 575 * PSNAMETOKEN - / 576 * PSSTRINGTOKEN - literal text string 577 */ 578 private int nextTokenType(ByteBuffer bb) { 579 580 try { 581 byte b = skip(bb); 582 583 while (true) { 584 if (b == (byte)'/') { // PS defined name follows. 585 return PSNAMETOKEN; 586 } else if (b == (byte)'(') { // PS string follows 587 return PSSTRINGTOKEN; 588 } else if ((b == (byte)'\r') || (b == (byte)'\n')) { 589 b = skip(bb); 590 } else { 591 b = bb.get(); 592 } 593 } 594 } catch (BufferUnderflowException e) { 595 return PSEOFTOKEN; 596 } 597 } 598 599 /* Read simple token (sequence of non-whitespace characters) 600 starting from the current position. 601 Skip leading whitespaces (if any). */ 602 private String getSimpleToken(ByteBuffer bb) { 603 while (bb.get() <= ' '); 604 int pos1 = bb.position()-1; 605 while (bb.get() > ' '); 606 int pos2 = bb.position(); 607 byte[] nameBytes = new byte[pos2-pos1-1]; 608 bb.position(pos1); 609 bb.get(nameBytes); 610 try { 611 return new String(nameBytes, "US-ASCII"); 612 } catch (UnsupportedEncodingException e) { 613 return new String(nameBytes); 614 } 615 } 616 617 private String getString(ByteBuffer bb) { 618 int pos1 = bb.position(); 619 while (bb.get() != ')'); 620 int pos2 = bb.position(); 621 byte[] nameBytes = new byte[pos2-pos1-1]; 622 bb.position(pos1); 623 bb.get(nameBytes); 624 try { 625 return new String(nameBytes, "US-ASCII"); 626 } catch (UnsupportedEncodingException e) { 627 return new String(nameBytes); 628 } 629 } 630 631 632 public String getPostscriptName() { 633 return psName; 634 } 635 636 protected synchronized FontScaler getScaler() { 637 if (scaler == null) { 638 scaler = FontScaler.getScaler(this, 0, false, fileSize); 639 } 640 641 return scaler; 642 } 643 644 CharToGlyphMapper getMapper() { 645 if (mapper == null) { 646 mapper = new Type1GlyphMapper(this); 647 } 648 return mapper; 649 } 650 651 public int getNumGlyphs() { 652 try { 653 return getScaler().getNumGlyphs(); 654 } catch (FontScalerException e) { 655 scaler = FontScaler.getNullScaler(); 656 return getNumGlyphs(); 657 } 658 } 659 660 public int getMissingGlyphCode() { 661 try { 662 return getScaler().getMissingGlyphCode(); 663 } catch (FontScalerException e) { 664 scaler = FontScaler.getNullScaler(); 665 return getMissingGlyphCode(); 666 } 667 } 668 669 public int getGlyphCode(char charCode) { 670 try { 671 return getScaler().getGlyphCode(charCode); 672 } catch (FontScalerException e) { 673 scaler = FontScaler.getNullScaler(); 674 return getGlyphCode(charCode); 675 } 676 } 677 678 public String toString() { 679 return "** Type1 Font: Family="+familyName+ " Name="+fullName+ 680 " style="+style+" fileName="+getPublicFileName(); 681 } 682 }