/** * Copyright (c) 2003, www.pdfbox.org * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * 1. Redistributions of source code must retain the above copyright notice, * this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright notice, * this list of conditions and the following disclaimer in the documentation * and/or other materials provided with the distribution. * 3. Neither the name of pdfbox; nor the names of its * contributors may be used to endorse or promote products derived from this * software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. * * http://www.pdfbox.org * */ package org.pdfbox.afmparser; import java.io.InputStream; import java.io.IOException; import java.util.StringTokenizer; import org.pdfbox.afmtypes.Composite; import org.pdfbox.afmtypes.CompositePart; import org.pdfbox.afmtypes.CharMetric; import org.pdfbox.afmtypes.FontMetric; import org.pdfbox.afmtypes.KernPair; import org.pdfbox.afmtypes.Ligature; import org.pdfbox.afmtypes.TrackKern; import org.pdfbox.util.BoundingBox; /** * This class is used to parse AFM(Adobe Font Metrics) documents. * * @see AFM Documentation * * @author Ben Litchfield (ben@csh.rit.edu) * @version $Revision: 1.9 $ */ public class AFMParser { /** * This is a comment in a AFM file. */ public static final String COMMENT = "Comment"; /** * This is the constant used in the AFM file to start a font metrics item. */ public static final String START_FONT_METRICS = "StartFontMetrics"; /** * This is the constant used in the AFM file to end a font metrics item. */ public static final String END_FONT_METRICS = "EndFontMetrics"; /** * This is the font name. */ public static final String FONT_NAME = "FontName"; /** * This is the full name. */ public static final String FULL_NAME = "FullName"; /** * This is the Family name. */ public static final String FAMILY_NAME = "FamilyName"; /** * This is the weight. */ public static final String WEIGHT = "Weight"; /** * This is the font bounding box. */ public static final String FONT_BBOX = "FontBBox"; /** * This is the version of the font. */ public static final String VERSION = "Version"; /** * This is the notice. */ public static final String NOTICE = "Notice"; /** * This is the encoding scheme. */ public static final String ENCODING_SCHEME = "EncodingScheme"; /** * This is the mapping scheme. */ public static final String MAPPING_SCHEME = "MappingScheme"; /** * This is the escape character. */ public static final String ESC_CHAR = "EscChar"; /** * This is the character set. */ public static final String CHARACTER_SET = "CharacterSet"; /** * This is the characters attribute. */ public static final String CHARACTERS = "Characters"; /** * This will determine if this is a base font. */ public static final String IS_BASE_FONT = "IsBaseFont"; /** * This is the V Vector attribute. */ public static final String V_VECTOR = "VVector"; /** * This will tell if the V is fixed. */ public static final String IS_FIXED_V = "IsFixedV"; /** * This is the cap height attribute. */ public static final String CAP_HEIGHT = "CapHeight"; /** * This is the X height. */ public static final String X_HEIGHT = "XHeight"; /** * This is ascender attribute. */ public static final String ASCENDER = "Ascender"; /** * This is the descender attribute. */ public static final String DESCENDER = "Descender"; /** * The underline position. */ public static final String UNDERLINE_POSITION = "UnderlinePosition"; /** * This is the Underline thickness. */ public static final String UNDERLINE_THICKNESS = "UnderlineThickness"; /** * This is the italic angle. */ public static final String ITALIC_ANGLE = "ItalicAngle"; /** * This is the char width. */ public static final String CHAR_WIDTH = "CharWidth"; /** * This will determine if this is fixed pitch. */ public static final String IS_FIXED_PITCH = "IsFixedPitch"; /** * This is the start of character metrics. */ public static final String START_CHAR_METRICS = "StartCharMetrics"; /** * This is the end of character metrics. */ public static final String END_CHAR_METRICS = "EndCharMetrics"; /** * The character metrics c value. */ public static final String CHARMETRICS_C = "C"; /** * The character metrics c value. */ public static final String CHARMETRICS_CH = "CH"; /** * The character metrics value. */ public static final String CHARMETRICS_WX = "WX"; /** * The character metrics value. */ public static final String CHARMETRICS_W0X = "W0X"; /** * The character metrics value. */ public static final String CHARMETRICS_W1X = "W1X"; /** * The character metrics value. */ public static final String CHARMETRICS_WY = "WY"; /** * The character metrics value. */ public static final String CHARMETRICS_W0Y = "W0Y"; /** * The character metrics value. */ public static final String CHARMETRICS_W1Y = "W1Y"; /** * The character metrics value. */ public static final String CHARMETRICS_W = "W"; /** * The character metrics value. */ public static final String CHARMETRICS_W0 = "W0"; /** * The character metrics value. */ public static final String CHARMETRICS_W1 = "W1"; /** * The character metrics value. */ public static final String CHARMETRICS_VV = "VV"; /** * The character metrics value. */ public static final String CHARMETRICS_N = "N"; /** * The character metrics value. */ public static final String CHARMETRICS_B = "B"; /** * The character metrics value. */ public static final String CHARMETRICS_L = "L"; /** * The character metrics value. */ public static final String STD_HW = "StdHW"; /** * The character metrics value. */ public static final String STD_VW = "StdVW"; /** * This is the start of track kern data. */ public static final String START_TRACK_KERN = "StartTrackKern"; /** * This is the end of track kern data. */ public static final String END_TRACK_KERN = "EndTrackKern"; /** * This is the start of kern data. */ public static final String START_KERN_DATA = "StartKernData"; /** * This is the end of kern data. */ public static final String END_KERN_DATA = "EndKernData"; /** * This is the start of kern pairs data. */ public static final String START_KERN_PAIRS = "StartKernPairs"; /** * This is the end of kern pairs data. */ public static final String END_KERN_PAIRS = "EndKernPairs"; /** * This is the start of kern pairs data. */ public static final String START_KERN_PAIRS0 = "StartKernPairs0"; /** * This is the start of kern pairs data. */ public static final String START_KERN_PAIRS1 = "StartKernPairs1"; /** * This is the start compisites data section. */ public static final String START_COMPOSITES = "StartComposites"; /** * This is the end compisites data section. */ public static final String END_COMPOSITES = "EndComposites"; /** * This is a composite character. */ public static final String CC = "CC"; /** * This is a composite charater part. */ public static final String PCC = "PCC"; /** * This is a kern pair. */ public static final String KERN_PAIR_KP = "KP"; /** * This is a kern pair. */ public static final String KERN_PAIR_KPH = "KPH"; /** * This is a kern pair. */ public static final String KERN_PAIR_KPX = "KPX"; /** * This is a kern pair. */ public static final String KERN_PAIR_KPY = "KPY"; private static final int BITS_IN_HEX = 16; private InputStream input; private FontMetric result; /** * A method to test parsing of all AFM documents in the resources * directory. * * @param args Ignored. * * @throws IOException If there is an error parsing one of the documents. */ public static void main( String[] args ) throws IOException { java.io.File afmDir = new java.io.File( "Resources/afm" ); java.io.File[] files = afmDir.listFiles(); for( int i=0; i< files.length; i++ ) { if( files[i].getPath().toUpperCase().endsWith( ".AFM" ) ) { long start = System.currentTimeMillis(); java.io.FileInputStream input = new java.io.FileInputStream( files[i] ); AFMParser parser = new AFMParser( input ); parser.parse(); long stop = System.currentTimeMillis(); System.out.println( "Parsing:" + files[i].getPath() + " " + (stop-start) ); } } } /** * Constructor. * * @param in The input stream to read the AFM document from. */ public AFMParser( InputStream in ) { input = in; } /** * This will parse the AFM document. This will close the Input stream * when the parsing is finished. * * @throws IOException If there is an IO error reading the document. */ public void parse() throws IOException { result = parseFontMetric(); } /** * This will get the result of the parsing. * * @return The parsed java object. */ public FontMetric getResult() { return result; } /** * This will parse a font metrics item. * * @return The parse font metrics item. * * @throws IOException If there is an error reading the AFM file. */ private FontMetric parseFontMetric() throws IOException { FontMetric fontMetrics = new FontMetric(); String startFontMetrics = readString(); if( !START_FONT_METRICS.equals( startFontMetrics ) ) { throw new IOException( "Error: The AFM file should start with " + START_FONT_METRICS + " and not '" + startFontMetrics + "'" ); } fontMetrics.setAFMVersion( readFloat() ); String nextCommand = null; while( !END_FONT_METRICS.equals( (nextCommand = readString() ) ) ) { if( FONT_NAME.equals( nextCommand ) ) { fontMetrics.setFontName( readLine() ); } else if( FULL_NAME.equals( nextCommand ) ) { fontMetrics.setFullName( readLine() ); } else if( FAMILY_NAME.equals( nextCommand ) ) { fontMetrics.setFamilyName( readLine() ); } else if( WEIGHT.equals( nextCommand ) ) { fontMetrics.setWeight( readLine() ); } else if( FONT_BBOX.equals( nextCommand ) ) { BoundingBox bBox = new BoundingBox(); bBox.setLowerLeftX( readFloat() ); bBox.setLowerLeftY( readFloat() ); bBox.setUpperRightX( readFloat() ); bBox.setUpperRightY( readFloat() ); fontMetrics.setFontBBox( bBox ); } else if( VERSION.equals( nextCommand ) ) { fontMetrics.setFontVersion( readLine() ); } else if( NOTICE.equals( nextCommand ) ) { fontMetrics.setNotice( readLine() ); } else if( ENCODING_SCHEME.equals( nextCommand ) ) { fontMetrics.setEncodingScheme( readLine() ); } else if( MAPPING_SCHEME.equals( nextCommand ) ) { fontMetrics.setMappingScheme( readInt() ); } else if( ESC_CHAR.equals( nextCommand ) ) { fontMetrics.setEscChar( readInt() ); } else if( CHARACTER_SET.equals( nextCommand ) ) { fontMetrics.setCharacterSet( readLine() ); } else if( CHARACTERS.equals( nextCommand ) ) { fontMetrics.setCharacters( readInt() ); } else if( IS_BASE_FONT.equals( nextCommand ) ) { fontMetrics.setIsBaseFont( readBoolean() ); } else if( V_VECTOR.equals( nextCommand ) ) { float[] vector = new float[2]; vector[0] = readFloat(); vector[1] = readFloat(); fontMetrics.setVVector( vector ); } else if( IS_FIXED_V.equals( nextCommand ) ) { fontMetrics.setIsFixedV( readBoolean() ); } else if( CAP_HEIGHT.equals( nextCommand ) ) { fontMetrics.setCapHeight( readFloat() ); } else if( X_HEIGHT.equals( nextCommand ) ) { fontMetrics.setXHeight( readFloat() ); } else if( ASCENDER.equals( nextCommand ) ) { fontMetrics.setAscender( readFloat() ); } else if( DESCENDER.equals( nextCommand ) ) { fontMetrics.setDescender( readFloat() ); } else if( STD_HW.equals( nextCommand ) ) { fontMetrics.setStandardHorizontalWidth( readFloat() ); } else if( STD_VW.equals( nextCommand ) ) { fontMetrics.setStandardVerticalWidth( readFloat() ); } else if( COMMENT.equals( nextCommand ) ) { fontMetrics.addComment( readLine() ); } else if( UNDERLINE_POSITION.equals( nextCommand ) ) { fontMetrics.setUnderlinePosition( readFloat() ); } else if( UNDERLINE_THICKNESS.equals( nextCommand ) ) { fontMetrics.setUnderlineThickness( readFloat() ); } else if( ITALIC_ANGLE.equals( nextCommand ) ) { fontMetrics.setItalicAngle( readFloat() ); } else if( CHAR_WIDTH.equals( nextCommand ) ) { float[] widths = new float[2]; widths[0] = readFloat(); widths[1] = readFloat(); fontMetrics.setCharWidth( widths ); } else if( IS_FIXED_PITCH.equals( nextCommand ) ) { fontMetrics.setFixedPitch( readBoolean() ); } else if( START_CHAR_METRICS.equals( nextCommand ) ) { int count = readInt(); for( int i=0; i= 2 not='" + hexString ); } if( hexString.charAt( 0 ) != '<' || hexString.charAt( hexString.length() -1 ) != '>' ) { throw new IOException( "String should be enclosed by angle brackets '" + hexString+ "'" ); } hexString = hexString.substring( 1, hexString.length() -1 ); byte[] data = new byte[ (hexString.length() / 2) ]; for( int i=0; i or FF, the spec is a little //unclear, wait and see if it breaks anything. String charCode = metricsTokenizer.nextToken(); charMetric.setCharacterCode( Integer.parseInt( charCode, BITS_IN_HEX ) ); verifySemicolon( metricsTokenizer ); } else if( nextCommand.equals( CHARMETRICS_WX ) ) { String wx = metricsTokenizer.nextToken(); charMetric.setWx( Float.parseFloat( wx ) ); verifySemicolon( metricsTokenizer ); } else if( nextCommand.equals( CHARMETRICS_W0X ) ) { String w0x = metricsTokenizer.nextToken(); charMetric.setW0x( Float.parseFloat( w0x ) ); verifySemicolon( metricsTokenizer ); } else if( nextCommand.equals( CHARMETRICS_W1X ) ) { String w1x = metricsTokenizer.nextToken(); charMetric.setW0x( Float.parseFloat( w1x ) ); verifySemicolon( metricsTokenizer ); } else if( nextCommand.equals( CHARMETRICS_WY ) ) { String wy = metricsTokenizer.nextToken(); charMetric.setWy( Float.parseFloat( wy ) ); verifySemicolon( metricsTokenizer ); } else if( nextCommand.equals( CHARMETRICS_W0Y ) ) { String w0y = metricsTokenizer.nextToken(); charMetric.setW0y( Float.parseFloat( w0y ) ); verifySemicolon( metricsTokenizer ); } else if( nextCommand.equals( CHARMETRICS_W1Y ) ) { String w1y = metricsTokenizer.nextToken(); charMetric.setW0y( Float.parseFloat( w1y ) ); verifySemicolon( metricsTokenizer ); } else if( nextCommand.equals( CHARMETRICS_W ) ) { String w0 = metricsTokenizer.nextToken(); String w1 = metricsTokenizer.nextToken(); float[] w = new float[2]; w[0] = Float.parseFloat( w0 ); w[1] = Float.parseFloat( w1 ); charMetric.setW( w ); verifySemicolon( metricsTokenizer ); } else if( nextCommand.equals( CHARMETRICS_W0 ) ) { String w00 = metricsTokenizer.nextToken(); String w01 = metricsTokenizer.nextToken(); float[] w0 = new float[2]; w0[0] = Float.parseFloat( w00 ); w0[1] = Float.parseFloat( w01 ); charMetric.setW0( w0 ); verifySemicolon( metricsTokenizer ); } else if( nextCommand.equals( CHARMETRICS_W1 ) ) { String w10 = metricsTokenizer.nextToken(); String w11 = metricsTokenizer.nextToken(); float[] w1 = new float[2]; w1[0] = Float.parseFloat( w10 ); w1[1] = Float.parseFloat( w11 ); charMetric.setW1( w1 ); verifySemicolon( metricsTokenizer ); } else if( nextCommand.equals( CHARMETRICS_VV ) ) { String vv0 = metricsTokenizer.nextToken(); String vv1 = metricsTokenizer.nextToken(); float[] vv = new float[2]; vv[0] = Float.parseFloat( vv0 ); vv[1] = Float.parseFloat( vv1 ); charMetric.setVv( vv ); verifySemicolon( metricsTokenizer ); } else if( nextCommand.equals( CHARMETRICS_N ) ) { String name = metricsTokenizer.nextToken(); charMetric.setName( name ); verifySemicolon( metricsTokenizer ); } else if( nextCommand.equals( CHARMETRICS_B ) ) { String llx = metricsTokenizer.nextToken(); String lly = metricsTokenizer.nextToken(); String urx = metricsTokenizer.nextToken(); String ury = metricsTokenizer.nextToken(); BoundingBox box = new BoundingBox(); box.setLowerLeftX( Float.parseFloat( llx ) ); box.setLowerLeftY( Float.parseFloat( lly ) ); box.setUpperRightX( Float.parseFloat( urx ) ); box.setUpperRightY( Float.parseFloat( ury ) ); charMetric.setBoundingBox( box ); verifySemicolon( metricsTokenizer ); } else if( nextCommand.equals( CHARMETRICS_L ) ) { String successor = metricsTokenizer.nextToken(); String ligature = metricsTokenizer.nextToken(); Ligature lig = new Ligature(); lig.setSuccessor( successor ); lig.setLigature( ligature ); charMetric.addLigature( lig ); verifySemicolon( metricsTokenizer ); } else { throw new IOException( "Unknown CharMetrics command '" + nextCommand + "'" ); } } } catch( NumberFormatException e ) { throw new IOException( "Error: Corrupt AFM document:" + e ); } return charMetric; } /** * This is used to verify that a semicolon is the next token in the stream. * * @param tokenizer The tokenizer to read from. * * @throws IOException If the semicolon is missing. */ private void verifySemicolon( StringTokenizer tokenizer ) throws IOException { if( tokenizer.hasMoreTokens() ) { String semicolon = tokenizer.nextToken(); if( !semicolon.equals( ";" ) ) { throw new IOException( "Error: Expected semicolon in stream actual='" + semicolon + "'" ); } } else { throw new IOException( "CharMetrics is missing a semicolon after a command" ); } } /** * This will read a boolean from the stream. * * @return The boolean in the stream. */ private boolean readBoolean() throws IOException { String theBoolean = readString(); return Boolean.valueOf( theBoolean ).booleanValue(); } /** * This will read an integer from the stream. * * @return The integer in the stream. */ private int readInt() throws IOException { String theInt = readString(); try { return Integer.parseInt( theInt ); } catch( NumberFormatException e ) { throw new IOException( "Error parsing AFM document:" + e ); } } /** * This will read a float from the stream. * * @return The float in the stream. */ private float readFloat() throws IOException { String theFloat = readString(); return Float.parseFloat( theFloat ); } /** * This will read until the end of a line. * * @return The string that is read. */ private String readLine() throws IOException { //First skip the whitespace StringBuffer buf = new StringBuffer(); int nextByte = input.read(); while( isWhitespace( nextByte ) ) { nextByte = input.read(); //do nothing just skip the whitespace. } buf.append( (char)nextByte ); //now read the data while( !isEOL(nextByte = input.read()) ) { buf.append( (char)nextByte ); } return buf.toString(); } /** * This will read a string from the input stream and stop at any whitespace. * * @return The string read from the stream. * * @throws IOException If an IO error occurs when reading from the stream. */ private String readString() throws IOException { //First skip the whitespace StringBuffer buf = new StringBuffer(); int nextByte = input.read(); while( isWhitespace( nextByte ) ) { nextByte = input.read(); //do nothing just skip the whitespace. } buf.append( (char)nextByte ); //now read the data while( !isWhitespace(nextByte = input.read()) ) { buf.append( (char)nextByte ); } return buf.toString(); } /** * This will determine if the byte is a whitespace character or not. * * @param character The character to test for whitespace. * * @return true If the character is whitespace as defined by the AFM spec. */ private boolean isEOL( int character ) { return character == 0x0D || character == 0x0A; } /** * This will determine if the byte is a whitespace character or not. * * @param character The character to test for whitespace. * * @return true If the character is whitespace as defined by the AFM spec. */ private boolean isWhitespace( int character ) { return character == ' ' || character == '\t' || character == 0x0D || character == 0x0A; } }