/* * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You under the Apache License, Version 2.0 * (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.apache.fontbox.afm; import java.io.IOException; import java.io.InputStream; import java.util.ArrayList; import java.util.List; import java.util.StringTokenizer; import org.apache.fontbox.util.BoundingBox; import org.apache.fontbox.util.Charsets; /** * This class is used to parse AFM(Adobe Font Metrics) documents. * * @see <A href="http://partners.adobe.com/asn/developer/type/">AFM Documentation</A> * * @author Ben Litchfield * */ 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 final InputStream input; /** * Constructor. * * @param in The input stream to read the AFM document from. */ public AFMParser( InputStream in ) { input = in; } /** * This will parse the AFM document. The input stream is closed * when the parsing is finished. * * @return the parsed FontMetric * * @throws IOException If there is an IO error reading the document. */ public FontMetrics parse() throws IOException { return parseFontMetric(false); } /** * This will parse the AFM document. The input stream is closed * when the parsing is finished. * * @param reducedDataset parse a reduced subset of data if set to true * @return the parsed FontMetric * * @throws IOException If there is an IO error reading the document. */ public FontMetrics parse(boolean reducedDataset) throws IOException { return parseFontMetric(reducedDataset); } /** * 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 FontMetrics parseFontMetric(boolean reducedDataset) throws IOException { FontMetrics fontMetrics = new FontMetrics(); 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; boolean charMetricsRead = false; while (!END_FONT_METRICS.equals(nextCommand = readString())) { switch (nextCommand) { case FONT_NAME: fontMetrics.setFontName( readLine() ); break; case FULL_NAME: fontMetrics.setFullName( readLine() ); break; case FAMILY_NAME: fontMetrics.setFamilyName( readLine() ); break; case WEIGHT: fontMetrics.setWeight( readLine() ); break; case FONT_BBOX: BoundingBox bBox = new BoundingBox(); bBox.setLowerLeftX( readFloat() ); bBox.setLowerLeftY( readFloat() ); bBox.setUpperRightX( readFloat() ); bBox.setUpperRightY( readFloat() ); fontMetrics.setFontBBox( bBox ); break; case VERSION: fontMetrics.setFontVersion( readLine() ); break; case NOTICE: fontMetrics.setNotice( readLine() ); break; case ENCODING_SCHEME: fontMetrics.setEncodingScheme( readLine() ); break; case MAPPING_SCHEME: fontMetrics.setMappingScheme( readInt() ); break; case ESC_CHAR: fontMetrics.setEscChar( readInt() ); break; case CHARACTER_SET: fontMetrics.setCharacterSet( readLine() ); break; case CHARACTERS: fontMetrics.setCharacters( readInt() ); break; case IS_BASE_FONT: fontMetrics.setIsBaseFont( readBoolean() ); break; case V_VECTOR: float[] vector = new float[2]; vector[0] = readFloat(); vector[1] = readFloat(); fontMetrics.setVVector( vector ); break; case IS_FIXED_V: fontMetrics.setIsFixedV( readBoolean() ); break; case CAP_HEIGHT: fontMetrics.setCapHeight( readFloat() ); break; case X_HEIGHT: fontMetrics.setXHeight( readFloat() ); break; case ASCENDER: fontMetrics.setAscender( readFloat() ); break; case DESCENDER: fontMetrics.setDescender( readFloat() ); break; case STD_HW: fontMetrics.setStandardHorizontalWidth( readFloat() ); break; case STD_VW: fontMetrics.setStandardVerticalWidth( readFloat() ); break; case COMMENT: fontMetrics.addComment( readLine() ); break; case UNDERLINE_POSITION: fontMetrics.setUnderlinePosition( readFloat() ); break; case UNDERLINE_THICKNESS: fontMetrics.setUnderlineThickness( readFloat() ); break; case ITALIC_ANGLE: fontMetrics.setItalicAngle( readFloat() ); break; case CHAR_WIDTH: float[] widths = new float[2]; widths[0] = readFloat(); widths[1] = readFloat(); fontMetrics.setCharWidth( widths ); break; case IS_FIXED_PITCH: fontMetrics.setFixedPitch( readBoolean() ); break; case START_CHAR_METRICS: int countMetrics = readInt(); List<CharMetric> charMetrics = new ArrayList<>(countMetrics); for (int i = 0; i < countMetrics; i++) { CharMetric charMetric = parseCharMetric(); charMetrics.add( charMetric ); } String endCharMetrics = readString(); if (!endCharMetrics.equals(END_CHAR_METRICS)) { throw new IOException( "Error: Expected '" + END_CHAR_METRICS + "' actual '" + endCharMetrics + "'"); } charMetricsRead = true; fontMetrics.setCharMetrics(charMetrics); break; case START_COMPOSITES: if( !reducedDataset) { int countComposites = readInt(); for (int i = 0; i < countComposites; i++) { Composite part = parseComposite(); fontMetrics.addComposite( part ); } String endComposites = readString(); if (!endComposites.equals(END_COMPOSITES)) { throw new IOException( "Error: Expected '" + END_COMPOSITES + "' actual '" + endComposites + "'"); } } break; case START_KERN_DATA: if( !reducedDataset) { parseKernData( fontMetrics ); } break; default: if (reducedDataset && charMetricsRead) { break; } throw new IOException( "Unknown AFM key '" + nextCommand + "'" ); } } return fontMetrics; } /** * This will parse the kern data. * * @param fontMetrics The metrics class to put the parsed data into. * * @throws IOException If there is an error parsing the data. */ private void parseKernData( FontMetrics fontMetrics ) throws IOException { String nextCommand; while( !(nextCommand = readString()).equals( END_KERN_DATA ) ) { switch(nextCommand) { case START_TRACK_KERN: int countTrackKern = readInt(); for (int i = 0; i < countTrackKern; i++) { TrackKern kern = new TrackKern(); kern.setDegree( readInt() ); kern.setMinPointSize( readFloat() ); kern.setMinKern( readFloat() ); kern.setMaxPointSize( readFloat() ); kern.setMaxKern( readFloat() ); fontMetrics.addTrackKern( kern ); } String endTrackKern = readString(); if (!endTrackKern.equals(END_TRACK_KERN)) { throw new IOException( "Error: Expected '" + END_TRACK_KERN + "' actual '" + endTrackKern + "'"); } break; case START_KERN_PAIRS: int countKernPairs = readInt(); for (int i = 0; i < countKernPairs; i++) { KernPair pair = parseKernPair(); fontMetrics.addKernPair( pair ); } String endKernPairs = readString(); if (!endKernPairs.equals(END_KERN_PAIRS)) { throw new IOException( "Error: Expected '" + END_KERN_PAIRS + "' actual '" + endKernPairs + "'"); } break; case START_KERN_PAIRS0: int countKernPairs0 = readInt(); for (int i = 0; i < countKernPairs0; i++) { KernPair pair = parseKernPair(); fontMetrics.addKernPair0( pair ); } String endKernPairs0 = readString(); if (!endKernPairs0.equals(END_KERN_PAIRS)) { throw new IOException( "Error: Expected '" + END_KERN_PAIRS + "' actual '" + endKernPairs0 + "'"); } break; case START_KERN_PAIRS1: int countKernPairs1 = readInt(); for (int i = 0; i < countKernPairs1; i++) { KernPair pair = parseKernPair(); fontMetrics.addKernPair1( pair ); } String endKernPairs1 = readString(); if (!endKernPairs1.equals(END_KERN_PAIRS)) { throw new IOException( "Error: Expected '" + END_KERN_PAIRS + "' actual '" + endKernPairs1 + "'"); } break; default: throw new IOException( "Unknown kerning data type '" + nextCommand + "'" ); } } } /** * This will parse a kern pair from the data stream. * * @return The kern pair that was parsed from the stream. * * @throws IOException If there is an error reading from the stream. */ private KernPair parseKernPair() throws IOException { KernPair kernPair = new KernPair(); String cmd = readString(); switch(cmd) { case KERN_PAIR_KP: kernPair.setFirstKernCharacter(readString()); kernPair.setSecondKernCharacter(readString()); kernPair.setX(readFloat()); kernPair.setY(readFloat()); break; case KERN_PAIR_KPH: kernPair.setFirstKernCharacter(hexToString(readString())); kernPair.setSecondKernCharacter(hexToString(readString())); kernPair.setX(readFloat()); kernPair.setY(readFloat()); break; case KERN_PAIR_KPX: kernPair.setFirstKernCharacter(readString()); kernPair.setSecondKernCharacter(readString()); kernPair.setX(readFloat()); kernPair.setY( 0 ); break; case KERN_PAIR_KPY: kernPair.setFirstKernCharacter(readString()); kernPair.setSecondKernCharacter(readString()); kernPair.setX( 0 ); kernPair.setY(readFloat()); break; default: throw new IOException( "Error expected kern pair command actual='" + cmd + "'" ); } return kernPair; } /** * This will convert and angle bracket hex string to a string. * * @param hexString An angle bracket string. * * @return The bytes of the hex string. * * @throws IOException If the string is in an invalid format. */ private String hexToString( String hexString ) throws IOException { if( hexString.length() < 2 ) { throw new IOException( "Error: Expected hex string of length >= 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<hexString.length(); i+=2 ) { String hex = "" + hexString.charAt( i ) + hexString.charAt( i+1 ); try { data[ i / 2 ] = (byte)Integer.parseInt( hex, BITS_IN_HEX ); } catch( NumberFormatException e ) { throw new IOException( "Error parsing AFM file:" + e ); } } return new String( data, Charsets.ISO_8859_1 ); } /** * This will parse a composite part from the stream. * * @return The composite. * * @throws IOException If there is an error parsing the composite. */ private Composite parseComposite() throws IOException { Composite composite = new Composite(); String partData = readLine(); StringTokenizer tokenizer = new StringTokenizer( partData, " ;" ); String cc = tokenizer.nextToken(); if( !cc.equals( CC ) ) { throw new IOException( "Expected '" + CC + "' actual='" + cc + "'" ); } String name = tokenizer.nextToken(); composite.setName( name ); int partCount; try { partCount = Integer.parseInt( tokenizer.nextToken() ); } catch( NumberFormatException e ) { throw new IOException( "Error parsing AFM document:" + e ); } for( int i=0; i<partCount; i++ ) { CompositePart part = new CompositePart(); String pcc = tokenizer.nextToken(); if( !pcc.equals( PCC ) ) { throw new IOException( "Expected '" + PCC + "' actual='" + pcc + "'" ); } String partName = tokenizer.nextToken(); try { int x = Integer.parseInt( tokenizer.nextToken() ); int y = Integer.parseInt( tokenizer.nextToken() ); part.setName( partName ); part.setXDisplacement( x ); part.setYDisplacement( y ); composite.addPart( part ); } catch( NumberFormatException e ) { throw new IOException( "Error parsing AFM document:" + e ); } } return composite; } /** * This will parse a single CharMetric object from the stream. * * @return The next char metric in the stream. * * @throws IOException If there is an error reading from the stream. */ private CharMetric parseCharMetric() throws IOException { CharMetric charMetric = new CharMetric(); String metrics = readLine(); StringTokenizer metricsTokenizer = new StringTokenizer( metrics ); try { while( metricsTokenizer.hasMoreTokens() ) { String nextCommand = metricsTokenizer.nextToken(); switch(nextCommand) { case CHARMETRICS_C: String charCodeC = metricsTokenizer.nextToken(); charMetric.setCharacterCode(Integer.parseInt(charCodeC)); verifySemicolon( metricsTokenizer ); break; case CHARMETRICS_CH: //Is the hex string <FF> or FF, the spec is a little //unclear, wait and see if it breaks anything. String charCodeCH = metricsTokenizer.nextToken(); charMetric.setCharacterCode(Integer.parseInt(charCodeCH, BITS_IN_HEX)); verifySemicolon( metricsTokenizer ); break; case CHARMETRICS_WX: charMetric.setWx(Float.parseFloat(metricsTokenizer.nextToken())); verifySemicolon( metricsTokenizer ); break; case CHARMETRICS_W0X: charMetric.setW0x(Float.parseFloat(metricsTokenizer.nextToken())); verifySemicolon( metricsTokenizer ); break; case CHARMETRICS_W1X: charMetric.setW1x(Float.parseFloat(metricsTokenizer.nextToken())); verifySemicolon( metricsTokenizer ); break; case CHARMETRICS_WY: charMetric.setWy(Float.parseFloat(metricsTokenizer.nextToken())); verifySemicolon( metricsTokenizer ); break; case CHARMETRICS_W0Y: charMetric.setW0y(Float.parseFloat(metricsTokenizer.nextToken())); verifySemicolon( metricsTokenizer ); break; case CHARMETRICS_W1Y: charMetric.setW1y(Float.parseFloat(metricsTokenizer.nextToken())); verifySemicolon( metricsTokenizer ); break; case CHARMETRICS_W: float[] w = new float[2]; w[0] = Float.parseFloat(metricsTokenizer.nextToken()); w[1] = Float.parseFloat(metricsTokenizer.nextToken()); charMetric.setW( w ); verifySemicolon( metricsTokenizer ); break; case CHARMETRICS_W0: float[] w0 = new float[2]; w0[0] = Float.parseFloat(metricsTokenizer.nextToken()); w0[1] = Float.parseFloat(metricsTokenizer.nextToken()); charMetric.setW0( w0 ); verifySemicolon( metricsTokenizer ); break; case CHARMETRICS_W1: float[] w1 = new float[2]; w1[0] = Float.parseFloat(metricsTokenizer.nextToken()); w1[1] = Float.parseFloat(metricsTokenizer.nextToken()); charMetric.setW1( w1 ); verifySemicolon( metricsTokenizer ); break; case CHARMETRICS_VV: float[] vv = new float[2]; vv[0] = Float.parseFloat(metricsTokenizer.nextToken()); vv[1] = Float.parseFloat(metricsTokenizer.nextToken()); charMetric.setVv( vv ); verifySemicolon( metricsTokenizer ); break; case CHARMETRICS_N: charMetric.setName(metricsTokenizer.nextToken()); verifySemicolon( metricsTokenizer ); break; case CHARMETRICS_B: BoundingBox box = new BoundingBox(); box.setLowerLeftX(Float.parseFloat(metricsTokenizer.nextToken())); box.setLowerLeftY(Float.parseFloat(metricsTokenizer.nextToken())); box.setUpperRightX(Float.parseFloat(metricsTokenizer.nextToken())); box.setUpperRightY(Float.parseFloat(metricsTokenizer.nextToken())); charMetric.setBoundingBox( box ); verifySemicolon( metricsTokenizer ); break; case CHARMETRICS_L: Ligature lig = new Ligature(); lig.setSuccessor(metricsTokenizer.nextToken()); lig.setLigature(metricsTokenizer.nextToken()); charMetric.addLigature( lig ); verifySemicolon( metricsTokenizer ); break; default: 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 ); } /** * 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 StringBuilder buf = new StringBuilder(60); 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 StringBuilder buf = new StringBuilder(24); 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; } }