/* * $Id: Type1Font.java,v 1.8 2009-08-07 23:19:48 tomoke Exp $ * * Copyright 2004 Sun Microsystems, Inc., 4150 Network Circle, * Santa Clara, California 95054, U.S.A. All rights reserved. * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA */ package com.sun.pdfview.font; import java.awt.geom.AffineTransform; import java.awt.geom.GeneralPath; import java.awt.geom.NoninvertibleTransformException; import java.awt.geom.Point2D; import java.io.IOException; import java.util.HashMap; import java.util.Map; import java.util.TreeMap; import com.sun.pdfview.PDFFile; import com.sun.pdfview.PDFObject; /** * A representation, with parser, of an Adobe Type 1 font. * @author Mike Wessler * * @todo * The parsing of the encrypted section needs work. A fix has just gone into * readArray, but it is very heuristic (as is all of readArray) and simply * stops the read at any invalid input. */ public class Type1Font extends OutlineFont { String chr2name[]; int password; byte[] subrs[]; int lenIV; Map<String,Object> name2outline; Map<String,FlPoint> name2width; AffineTransform at; /** the Type1 stack of command values */ float stack[] = new float[100]; /** the current position in the Type1 stack */ int sloc = 0; /** the stack of postscript commands (used by callothersubr) */ float psStack[] = new float[3]; /** the current position in the postscript stack */ int psLoc = 0; /** * create a new Type1Font based on a font data stream and an encoding. * @param baseName the postscript name of this font * @param src the Font object as a stream with a dictionary * @param descriptor the descriptor for this font */ public Type1Font(String baseName, PDFObject src, PDFFontDescriptor descriptor) throws IOException { super(baseName, src, descriptor); if (descriptor != null && descriptor.getFontFile() != null) { // parse that file, filling name2outline and chr2name int start = descriptor.getFontFile().getDictRef("Length1").getIntValue(); int len = descriptor.getFontFile().getDictRef("Length2").getIntValue(); byte font[] = descriptor.getFontFile().getStream(); parseFont(font, start, len); } } /** Read a font from it's data, start position and length */ protected void parseFont(byte[] font, int start, int len) { name2width = new HashMap<String,FlPoint>(); byte data[] = null; if (isASCII(font, start)) { byte[] bData = readASCII(font, start, start + len); data = decrypt(bData, 0, bData.length, 55665, 4); } else { data = decrypt(font, start, start + len, 55665, 4); } // encoding is in cleartext area chr2name = readEncoding(font); int lenIVLoc = findSlashName(data, "lenIV"); PSParser psp = new PSParser(data, 0); if (lenIVLoc < 0) { lenIV = 4; } else { psp.setLoc(lenIVLoc + 6); lenIV = Integer.parseInt(psp.readThing()); } password = 4330; int matrixloc = findSlashName(font, "FontMatrix"); if (matrixloc < 0) { System.out.println("No FontMatrix!"); at = new AffineTransform(0.001f, 0, 0, 0.001f, 0, 0); } else { PSParser psp2 = new PSParser(font, matrixloc + 11); // read [num num num num num num] float xf[] = psp2.readArray(6); // System.out.println("FONT MATRIX: "+xf); at = new AffineTransform(xf); } subrs = readSubrs(data); name2outline = new TreeMap<String,Object>(readChars(data)); // at this point, name2outline holds name -> byte[]. } /** * parse the encoding portion of the font definition * @param d the font definition stream * @return an array of the glyphs corresponding to each byte */ private String[] readEncoding(byte[] d) { byte[][] ary = readArray(d, "Encoding", "def"); String res[] = new String[256]; for (int i = 0; i < ary.length; i++) { if (ary[i] != null) { if (ary[i][0] == '/') { res[i] = new String(ary[i]).substring(1); } else { res[i] = new String(ary[i]); } } else { res[i] = null; } } return res; } /** * read the subroutines out of the font definition * @param d the font definition stream * @return an array of the subroutines, each as a byte array. */ private byte[][] readSubrs(byte[] d) { return readArray(d, "Subrs", "index"); } /** * read a named array out of the font definition. * <p> * this function attempts to parse an array out of a postscript * definition without doing any postscript. It's actually looking * for things that look like "dup <i>id</i> <i>elt</i> put", and * placing the <i>elt</i> at the <i>i</i>th position in the array. * @param d the font definition stream * @param key the name of the array * @param end a string that appears at the end of the array * @return an array consisting of a byte array for each entry */ private byte[][] readArray(byte[] d, String key, String end) { int i = findSlashName(d, key); if (i < 0) { // not found. return new byte[0][]; } // now find things that look like "dup id elt put" // end at "def" PSParser psp = new PSParser(d, i); String type = psp.readThing(); // read the key (i is the start of the key) double val; type = psp.readThing(); if (type.equals("StandardEncoding")) { byte[] stdenc[] = new byte[FontSupport.standardEncoding.length][]; for (i = 0; i < stdenc.length; i++) { stdenc[i] = FontSupport.getName(FontSupport.standardEncoding[i]).getBytes(); } return stdenc; } int len = Integer.parseInt(type); byte[] out[] = new byte[len][]; byte[] line; while (true) { String s = psp.readThing(); if (s.equals("dup")) { String thing = psp.readThing(); int id = 0; try { id = Integer.parseInt(thing); } catch (Exception e) { break; } String elt = psp.readThing(); line = elt.getBytes(); if (Character.isDigit(elt.charAt(0))) { int hold = Integer.parseInt(elt); String special = psp.readThing(); if (special.equals("-|") || special.equals("RD")) { psp.setLoc(psp.getLoc() + 1); line = psp.getNEncodedBytes(hold, password, lenIV); } } out[id] = line; } else if (s.equals(end)) { break; } } return out; } /** * decrypt an array using the Adobe Type 1 Font decryption algorithm. * @param d the input array of bytes * @param start where in the array to start decoding * @param end where in the array to stop decoding * @param key the decryption key * @param skip how many bytes to skip initially * @return the decrypted bytes. The length of this array will be * (start-end-skip) bytes long */ private byte[] decrypt(byte[] d, int start, int end, int key, int skip) { if (end - start - skip < 0) { skip = 0; } byte[] o = new byte[end - start - skip]; int r = key; int ipos; int c1 = 52845; int c2 = 22719; for (ipos = start; ipos < end; ipos++) { int c = d[ipos] & 0xff; int p = (c ^ (r >> 8)) & 0xff; r = ((c + r) * c1 + c2) & 0xffff; if (ipos - start - skip >= 0) { o[ipos - start - skip] = (byte) p; } } return o; } /** * Read data formatted as ASCII strings as binary data * * @param data the data, formatted as ASCII strings * @param start where in the array to start decrypting * @param end where in the array to stop decrypting */ private byte[] readASCII(byte[] data, int start, int end) { // each byte of output is derived from one character (two bytes) of // input byte[] o = new byte[(end - start) / 2]; int count = 0; int bit = 0; for (int loc = start; loc < end; loc++) { char c = (char) (data[loc] & 0xff); byte b = (byte) 0; if (c >= '0' && c <= '9') { b = (byte) (c - '0'); } else if (c >= 'a' && c <= 'f') { b = (byte) (10 + (c - 'a')); } else if (c >= 'A' && c <= 'F') { b = (byte) (10 + (c - 'A')); } else { // linefeed or something. Skip. continue; } // which half of the byte are we? if ((bit++ % 2) == 0) { o[count] = (byte) (b << 4); } else { o[count++] |= b; } } return o; } /** * Determine if data is in ASCII or binary format. According to the spec, * if any of the first 4 bytes are not character codes ('0' - '9' or * 'A' - 'F' or 'a' - 'f'), then the data is binary. Otherwise it is * ASCII */ private boolean isASCII(byte[] data, int start) { // look at the first 4 bytes for (int i = start; i < start + 4; i++) { // get the byte as a character char c = (char) (data[i] & 0xff); if (c >= '0' && c <= '9') { continue; } else if (c >= 'a' && c <= 'f') { continue; } else if (c >= 'A' && c <= 'F') { continue; } else { // out of range return false; } } // all were in range, so it is ASCII return true; } /** * PostScript reader (not a parser, as the name would seem to indicate). */ class PSParser { byte[] data; int loc; /** * create a PostScript reader given some data and an initial offset * into that data. * @param data the bytes of the postscript information * @param start an initial offset into the data */ public PSParser(byte[] data, int start) { this.data = data; this.loc = start; // System.out.println("PSParser.constructor: start: " + start + // ", Length: " + data.length); // System.out.print (new String(data, loc, data.length - loc)); // System.out.println(" - end -\n"); } /** * get the next postscript "word". This is basically the next * non-whitespace block between two whitespace delimiters. * This means that something like " [2 4 53]" will produce * three items, while " [2 4 56 ]" will produce four. */ public String readThing() { // skip whitespace // System.out.println("PAParser: whitespace: \""); while (PDFFile.isWhiteSpace(data[loc])) { // System.out.print (new String(data, loc, 1)); loc++; } // System.out.print("\": thing: "); // read thing int start = loc; while (!PDFFile.isWhiteSpace(data[loc])) { loc++; if (!PDFFile.isRegularCharacter(data[loc])) { break; // leave with the delimiter included } } String s = new String(data, start, loc - start); // System.out.println(": Read: "+s); return s; } /** * read a set of numbers from the input. This method doesn't * pay any attention to "[" or "]" delimiters, and reads any * non-numeric items as the number 0. * @param count the number of items to read * @return an array of count floats */ public float[] readArray(int count) { float[] ary = new float[count]; int idx = 0; while (idx < count) { String thing = readThing(); if (thing.charAt(0) == '[') { thing = thing.substring(1); } if (thing.endsWith("]")) { thing = thing.substring(0, thing.length() - 1); } if (thing.length() > 0) { ary[idx++] = Float.valueOf(thing).floatValue(); } } return ary; } /** * get the current location within the input stream */ public int getLoc() { return loc; } /** * set the current location within the input stream */ public void setLoc(int loc) { this.loc = loc; } /** * treat the next n bytes of the input stream as encoded * information to be decrypted. * @param n the number of bytes to decrypt * @param key the decryption key * @param skip the number of bytes to skip at the beginning of the * decryption * @return an array of decrypted bytes. The length of the array * will be n-skip. */ public byte[] getNEncodedBytes(int n, int key, int skip) { byte[] result = decrypt(data, loc, loc + n, key, skip); loc += n; return result; } } /** * get the index into the byte array of a slashed name, like "/name". * @param d the search array * @param name the name to look for, without the initial / * @return the index of the first occurance of /name in the array. */ private int findSlashName(byte[] d, String name) { int i; for (i = 0; i < d.length; i++) { if (d[i] == '/') { // check for key boolean found = true; for (int j = 0; j < name.length(); j++) { if (d[i + j + 1] != name.charAt(j)) { found = false; break; } } if (found) { return i; } } } return -1; } /** * get the character definitions of the font. * @param d the font data * @return a HashMap that maps string glyph names to byte arrays of * decoded font data. */ private HashMap<String,byte[]> readChars(byte[] d) { // skip thru data until we find "/"+key HashMap<String,byte[]> hm = new HashMap<String,byte[]>(); int i = findSlashName(d, "CharStrings"); if (i < 0) { // not found return hm; } PSParser psp = new PSParser(d, i); // read /name len -| [len bytes] |- // until "end" while (true) { String s = psp.readThing(); char c = s.charAt(0); if (c == '/') { int len = Integer.parseInt(psp.readThing()); String go = psp.readThing(); // it's -| or RD if (go.equals("-|") || go.equals("RD")) { psp.setLoc(psp.getLoc() + 1); byte[] line = psp.getNEncodedBytes(len, password, lenIV); hm.put(s.substring(1), line); } } else if (s.equals("end")) { break; } } return hm; } /** * pop the next item off the stack */ private float pop() { float val = 0; if (sloc > 0) { val = stack[--sloc]; } return val; } int callcount = 0; /** * parse glyph data into a GeneralPath, and return the advance width. * The working point is passed in as a parameter in order to allow * recursion. * @param cs the decrypted glyph data * @param gp a GeneralPath into which the glyph shape will be stored * @param pt a FlPoint object that will be used to generate the path * @param wid a FlPoint into which the advance width will be placed. */ private void parse(byte[] cs, GeneralPath gp, FlPoint pt, FlPoint wid) { // System.out.println("--- cmd length is "+cs.length); int loc = 0; float x1, x2, x3, y1, y2, y3; while (loc < cs.length) { int v = ((int) cs[loc++]) & 0xff; if (v == 255) { stack[sloc++] = ((((int) cs[loc]) & 0xff) << 24) + ((((int) cs[loc + 1]) & 0xff) << 16) + ((((int) cs[loc + 2]) & 0xff) << 8) + ((((int) cs[loc + 3]) & 0xff)); loc += 4; // System.out.println("Pushed long "+stack[sloc-1]); } else if (v >= 251) { stack[sloc++] = -((v - 251) << 8) - (((int) cs[loc]) & 0xff) - 108; loc++; // System.out.println("Pushed lo "+stack[sloc-1]); } else if (v >= 247) { stack[sloc++] = ((v - 247) << 8) + (((int) cs[loc]) & 0xff) + 108; loc++; // System.out.println("Pushed hi "+stack[sloc-1]); } else if (v >= 32) { stack[sloc++] = v - 139; // System.out.println("Pushed "+stack[sloc-1]); } else { // System.out.println("CMD: "+v+" (stack is size "+sloc+")"); switch (v) { case 0: // x throw new RuntimeException("Bad command (" + v + ")"); case 1: // hstem sloc = 0; break; case 2: // x throw new RuntimeException("Bad command (" + v + ")"); case 3: // vstem sloc = 0; break; case 4: // y vmoveto pt.y += pop(); gp.moveTo(pt.x, pt.y); sloc = 0; break; case 5: // x y rlineto pt.y += pop(); pt.x += pop(); gp.lineTo(pt.x, pt.y); sloc = 0; break; case 6: // x hlineto pt.x += pop(); gp.lineTo(pt.x, pt.y); sloc = 0; break; case 7: // y vlineto pt.y += pop(); gp.lineTo(pt.x, pt.y); sloc = 0; break; case 8: // x1 y1 x2 y2 x3 y3 rcurveto y3 = pop(); x3 = pop(); y2 = pop(); x2 = pop(); y1 = pop(); x1 = pop(); gp.curveTo(pt.x + x1, pt.y + y1, pt.x + x1 + x2, pt.y + y1 + y2, pt.x + x1 + x2 + x3, pt.y + y1 + y2 + y3); pt.x += x1 + x2 + x3; pt.y += y1 + y2 + y3; sloc = 0; break; case 9: // closepath gp.closePath(); sloc = 0; break; case 10: // n callsubr int n = (int) pop(); if (subrs[n] == null) { System.out.println("No subroutine #" + n); } else { callcount++; if (callcount > 10) { System.out.println("Call stack too large"); // throw new RuntimeException("Call stack too large"); } else { parse(subrs[n], gp, pt, wid); } callcount--; } break; case 11: // return return; case 12: // ext... v = ((int) cs[loc++]) & 0xff; if (v == 6) { // s x y b a seac char a = (char) pop(); char b = (char) pop(); float y = pop(); float x = pop(); buildAccentChar(x, y, a, b, gp); sloc = 0; } else if (v == 7) { // x y w h sbw wid.y = pop(); wid.x = pop(); pt.y = pop(); pt.x = pop(); sloc = 0; } else if (v == 12) { // a b div -> a/b float b = pop(); float a = pop(); stack[sloc++] = a / b; } else if (v == 33) { // a b setcurrentpoint pt.y = pop(); pt.x = pop(); gp.moveTo(pt.x, pt.y); sloc = 0; } else if (v == 0) { // dotsection sloc = 0; } else if (v == 1) { // vstem3 sloc = 0; } else if (v == 2) { // hstem3 sloc = 0; } else if (v == 16) { // n callothersubr int cn = (int) pop(); int countargs = (int) pop(); // System.out.println("Called othersubr with index "+cn); switch (cn) { case 0: // push args2 and args3 onto stack psStack[psLoc++] = pop(); psStack[psLoc++] = pop(); pop(); break; case 3: // push 3 onto the postscript stack psStack[psLoc++] = 3; break; default: // push arguments onto the postscript stack for (int i = 0; i > countargs; i--) { psStack[psLoc++] = pop(); } break; } } else if (v == 17) { // pop // pop from the postscript stack onto the type1 stack stack[sloc++] = psStack[psLoc - 1]; psLoc--; } else { throw new RuntimeException("Bad command (" + v + ")"); } break; case 13: // s w hsbw wid.x = pop(); wid.y = 0; pt.x = pop(); pt.y = 0; // gp.moveTo(pt.x, pt.y); sloc = 0; break; case 14: // endchar // return; break; case 15: // x case 16: // x case 17: // x case 18: // x case 19: // x case 20: // x throw new RuntimeException("Bad command (" + v + ")"); case 21: // x y rmoveto pt.y += pop(); pt.x += pop(); gp.moveTo(pt.x, pt.y); sloc = 0; break; case 22: // x hmoveto pt.x += pop(); gp.moveTo(pt.x, pt.y); sloc = 0; break; case 23: // x case 24: // x case 25: // x case 26: // x case 27: // x case 28: // x case 29: // x throw new RuntimeException("Bad command (" + v + ")"); case 30: // y1 x2 y2 x3 vhcurveto x3 = pop(); y2 = pop(); x2 = pop(); y1 = pop(); x1 = y3 = 0; gp.curveTo(pt.x, pt.y + y1, pt.x + x2, pt.y + y1 + y2, pt.x + x2 + x3, pt.y + y1 + y2); pt.x += x2 + x3; pt.y += y1 + y2; sloc = 0; break; case 31: // x1 x2 y2 y3 hvcurveto y3 = pop(); y2 = pop(); x2 = pop(); x1 = pop(); y1 = x3 = 0; gp.curveTo(pt.x + x1, pt.y, pt.x + x1 + x2, pt.y + y2, pt.x + x1 + x2, pt.y + y2 + y3); pt.x += x1 + x2; pt.y += y2 + y3; sloc = 0; break; } } } } /** * build an accented character out of two pre-defined glyphs. * @param x the x offset of the accent relativ to the sidebearing of the base char * @param y the y offset of the accent relativ to the sidebearing of the base char * @param a the index of the accent glyph * @param b the index of the base glyph * @param gp the GeneralPath into which the combined glyph will be * written. */ private void buildAccentChar(float x, float y, char a, char b, GeneralPath gp) { // get the outline of the accent GeneralPath pathA = getOutline(a, getWidth(a, null)); // don't manipulate the original glyph pathA = (GeneralPath) pathA.clone(); try { // undo the effect of the transform applied in read final AffineTransform xformA = at.createInverse(); pathA.transform(xformA); // Best x can�t be calcualted cause we don�t know the left sidebearing of the base character. // Leaving x=0 gives the best results. // see Chapter 6 of http://partners.adobe.com/public/developer/en/font/5015.Type1_Supp.pdf // and the definition of the seac-Command in http://partners.adobe.com/public/developer/en/font/T1_SPEC.PDF final AffineTransform xformA2 = AffineTransform.getTranslateInstance(0, y); pathA.transform(xformA2); //<-- goht net fürs 'ä' } catch (NoninvertibleTransformException nte) { pathA.transform(AffineTransform.getTranslateInstance(0, y)); } GeneralPath pathB = getOutline(b, getWidth(b, null)); // don't manipulate the original glyph pathB = (GeneralPath) pathB.clone(); try { AffineTransform xformB = at.createInverse(); pathB.transform(xformB); } catch (NoninvertibleTransformException nte) { // ignore } gp.append(pathB, false); gp.append(pathA, false); } /** * Get the width of a given character * * This method is overridden to work if the width array hasn't been * populated (as for one of the 14 base fonts) */ @Override public float getWidth(char code, String name) { // we don't have first and last chars, so therefore no width array if (getFirstChar() == -1 || getLastChar() == -1) { String key = chr2name[code & 0xff]; // use a name if one is provided if (name != null) { key = name; } if (key != null && name2outline.containsKey(key)) { if (!name2width.containsKey(key)) { // glyph has not yet been parsed // getting the outline will force it to get read getOutline(key, 0); } FlPoint width = name2width.get(key); if (width != null) { return width.x / getDefaultWidth(); } } return 0; } // return the width that has been specified return super.getWidth(code, name); } /** * Decrypt a glyph stored in byte form */ private synchronized GeneralPath parseGlyph(byte[] cs, FlPoint advance, AffineTransform at) { GeneralPath gp = new GeneralPath(); FlPoint curpoint = new FlPoint(); sloc = 0; parse(cs, gp, curpoint, advance); gp.transform(at); return gp; } /** * Get a glyph outline by name * * @param name the name of the desired glyph * @return the glyph outline, or null if unavailable */ protected GeneralPath getOutline(String name, float width) { // make sure we have a valid name if (name == null || !name2outline.containsKey(name)) { name = ".notdef"; } // get whatever is stored in name. Could be a GeneralPath, could be byte[] Object obj = name2outline.get(name); // if it's a byte array, it needs to be parsed // otherwise, just return the path if (obj instanceof GeneralPath) { return (GeneralPath) obj; } else { byte[] cs = (byte[]) obj; FlPoint advance = new FlPoint(); GeneralPath gp = parseGlyph(cs, advance, at); if (width != 0 && advance.x != 0) { // scale the glyph to fit in the width Point2D p = new Point2D.Float(advance.x, advance.y); at.transform(p, p); double scale = width / p.getX(); AffineTransform xform = AffineTransform.getScaleInstance(scale, 1.0); gp.transform(xform); } // put the parsed object in the cache name2outline.put(name, gp); name2width.put(name, advance); return gp; } } /** * Get a glyph outline by character code * * Note this method must always return an outline * * @param src the character code of the desired glyph * @return the glyph outline */ protected GeneralPath getOutline(char src, float width) { return getOutline(chr2name[src & 0xff], width); } }