blob: d19c8d478ad2df62f41445a2488c37264c2f9b92 [file] [log] [blame]
/*
* 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.
*/
/* $Id$ */
package org.apache.fop.fonts.type1;
// Java
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.HashMap;
import java.util.Map;
import org.apache.commons.io.IOUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.xmlgraphics.fonts.Glyphs;
/**
* This class represents a PFM file (or parts of it) as a Java object.
*/
public class PFMFile {
// Header stuff
private String windowsName;
private String postscriptName;
private short dfItalic;
//private int dfWeight;
private short dfCharSet;
private short dfPitchAndFamily;
private int dfAvgWidth;
private int dfMaxWidth;
private int dfMinWidth;
private short dfFirstChar;
private short dfLastChar;
// Extension stuff
// ---
// Extend Text Metrics
private int etmCapHeight;
private int etmXHeight;
private int etmLowerCaseAscent;
private int etmLowerCaseDescent;
// Extent table
private int[] extentTable;
private Map<Integer, Map<Integer, Integer>> kerningTab = new HashMap<Integer, Map<Integer, Integer>>();
/**
* logging instance
*/
protected Log log = LogFactory.getLog(PFMFile.class);
/**
* Parses a PFM file
*
* @param inStream The stream from which to read the PFM file.
* @throws IOException In case of an I/O problem
*/
public void load(InputStream inStream) throws IOException {
byte[] pfmBytes = IOUtils.toByteArray(inStream);
InputStream bufin = inStream;
bufin = new ByteArrayInputStream(pfmBytes);
PFMInputStream in = new PFMInputStream(bufin);
bufin.mark(512);
short sh1 = in.readByte();
short sh2 = in.readByte();
if (sh1 == 128 && sh2 == 1) {
//Found the first section header of a PFB file!
IOUtils.closeQuietly(in);
throw new IOException("Cannot parse PFM file. You probably specified the PFB file"
+ " of a Type 1 font as parameter instead of the PFM.");
}
bufin.reset();
byte[] b = new byte[16];
bufin.read(b);
if (new String(b, "US-ASCII").equalsIgnoreCase("StartFontMetrics")) {
//Found the header of a AFM file!
IOUtils.closeQuietly(in);
throw new IOException("Cannot parse PFM file. You probably specified the AFM file"
+ " of a Type 1 font as parameter instead of the PFM.");
}
bufin.reset();
final int version = in.readShort();
if (version != 256) {
log.warn("PFM version expected to be '256' but got '" + version + "'."
+ " Please make sure you specify the PFM as parameter"
+ " and not the PFB or the AFM.");
}
//final long filesize = in.readInt();
bufin.reset();
loadHeader(in);
loadExtension(in);
}
/**
* Parses the header of the PFM file.
*
* @param inStream The stream from which to read the PFM file.
* @throws IOException In case of an I/O problem
*/
private void loadHeader(PFMInputStream inStream) throws IOException {
inStream.skip(80);
dfItalic = inStream.readByte();
inStream.skip(2);
inStream.readShort(); // dfWeight =
dfCharSet = inStream.readByte();
inStream.skip(4);
dfPitchAndFamily = inStream.readByte();
dfAvgWidth = inStream.readShort();
dfMaxWidth = inStream.readShort();
dfFirstChar = inStream.readByte();
dfLastChar = inStream.readByte();
inStream.skip(8);
long faceOffset = inStream.readInt();
inStream.reset();
inStream.skip(faceOffset);
windowsName = inStream.readString();
inStream.reset();
inStream.skip(117);
}
/**
* Parses the extension part of the PFM file.
*
* @param inStream The stream from which to read the PFM file.
*/
private void loadExtension(PFMInputStream inStream) throws IOException {
final int size = inStream.readShort();
if (size != 30) {
log.warn("Size of extension block was expected to be "
+ "30 bytes, but was " + size + " bytes.");
}
final long extMetricsOffset = inStream.readInt();
final long extentTableOffset = inStream.readInt();
inStream.skip(4); //Skip dfOriginTable
final long kernPairOffset = inStream.readInt();
inStream.skip(4); //Skip dfTrackKernTable
long driverInfoOffset = inStream.readInt();
if (kernPairOffset > 0) {
inStream.reset();
inStream.skip(kernPairOffset);
loadKernPairs(inStream);
}
inStream.reset();
inStream.skip(driverInfoOffset);
postscriptName = inStream.readString();
if (extMetricsOffset != 0) {
inStream.reset();
inStream.skip(extMetricsOffset);
loadExtMetrics(inStream);
}
if (extentTableOffset != 0) {
inStream.reset();
inStream.skip(extentTableOffset);
loadExtentTable(inStream);
}
}
/**
* Parses the kernPairs part of the pfm file
*
* @param inStream The stream from which to read the PFM file.
*/
private void loadKernPairs(PFMInputStream inStream) throws IOException {
int i = inStream.readShort();
if (log.isTraceEnabled()) {
log.trace(i + " kerning pairs");
}
while (i > 0) {
int g1 = (int) inStream.readByte();
i--;
int g2 = (int) inStream.readByte();
int adj = inStream.readShort();
if (adj > 0x8000) {
adj = -(0x10000 - adj);
}
if (log.isTraceEnabled()) {
log.trace("Char no: (" + g1 + ", " + g2 + ") kern: " + adj);
final String glyph1 = Glyphs.TEX8R_GLYPH_NAMES[g1];
final String glyph2 = Glyphs.TEX8R_GLYPH_NAMES[g2];
log.trace("glyphs: " + glyph1 + ", " + glyph2);
}
Map<Integer, Integer> adjTab = kerningTab.get(Integer.valueOf(g1));
if (adjTab == null) {
adjTab = new HashMap<Integer, Integer>();
}
adjTab.put(Integer.valueOf(g2), Integer.valueOf(adj));
kerningTab.put(Integer.valueOf(g1), adjTab);
}
}
/**
* Parses the extended metrics part of the PFM file.
*
* @param inStream The stream from which to read the PFM file.
*/
private void loadExtMetrics(PFMInputStream inStream) throws IOException {
final int size = inStream.readShort();
if (size != 52) {
log.warn("Size of extension block was expected to be "
+ "52 bytes, but was " + size + " bytes.");
}
inStream.skip(12); //Skip etmPointSize, etmOrientation, etmMasterHeight,
//etmMinScale, etmMaxScale, emtMasterUnits
etmCapHeight = inStream.readShort();
etmXHeight = inStream.readShort();
etmLowerCaseAscent = inStream.readShort();
etmLowerCaseDescent = -(inStream.readShort());
//Ignore the rest of the values
}
/**
* Parses the extent table of the PFM file.
*
* @param inStream The stream from which to read the PFM file.
*/
private void loadExtentTable(PFMInputStream inStream) throws IOException {
extentTable = new int[dfLastChar - dfFirstChar + 1];
dfMinWidth = dfMaxWidth;
for (short i = dfFirstChar; i <= dfLastChar; i++) {
extentTable[i - dfFirstChar] = inStream.readShort();
if (extentTable[i - dfFirstChar] < dfMinWidth) {
dfMinWidth = extentTable[i - dfFirstChar];
}
}
}
/**
* Returns the Windows name of the font.
*
* @return The Windows name.
*/
public String getWindowsName() {
return windowsName;
}
/**
* Return the kerning table. The kerning table is a Map with
* strings with glyphnames as keys, containing Maps as value.
* The value map contains a glyph name string key and an Integer value
*
* @return A Map containing the kerning table
*/
public Map<Integer, Map<Integer, Integer>> getKerning() {
return kerningTab;
}
/**
* Returns the Postscript name of the font.
*
* @return The Postscript name.
*/
public String getPostscriptName() {
return postscriptName;
}
/**
* Returns the charset used for the font.
*
* @return The charset (0=WinAnsi).
*/
public short getCharSet() {
return dfCharSet;
}
/**
* Returns the charset of the font as a string.
*
* @return The name of the charset.
*/
public String getCharSetName() {
//TODO Had to remove the detection for Expert(Subset) encoding. The PFM is not suitable
//for detecting these character sets. We have to parse the AFM for that.
switch (dfCharSet) {
case 0:
return "WinAnsi"; // AKA ISOAdobe
case 2:
if ("Symbol".equals(getPostscriptName())) {
return "Symbol";
}
break;
case 128:
return "Shift-JIS (Japanese)";
default:
log.warn("Unknown charset detected (" + dfCharSet
+ ", 0x" + Integer.toHexString(dfCharSet)
+ "). Trying fallback to WinAnsi.");
}
return "WinAnsi";
}
/**
* Returns the number of the character that defines
* the first entry in the widths list.
*
* @return The number of the first character.
*/
public short getFirstChar() {
return dfFirstChar;
}
/**
* Returns the number of the character that defines
* the last entry in the widths list.
*
* @return The number of the last character.
*/
public short getLastChar() {
return dfLastChar;
}
/**
* Returns the CapHeight parameter for the font (height of uppercase H).
*
* @return The CapHeight parameter.
*/
public int getCapHeight() {
return etmCapHeight;
}
/**
* Returns the XHeight parameter for the font (height of lowercase x).
*
* @return The CapHeight parameter.
*/
public int getXHeight() {
return etmXHeight;
}
/**
* Returns the LowerCaseAscent parameter for the font (height of lowercase d).
*
* @return The LowerCaseAscent parameter.
*/
public int getLowerCaseAscent() {
return etmLowerCaseAscent;
}
/**
* Returns the LowerCaseDescent parameter for the font (height of lowercase p).
*
* @return The LowerCaseDescent parameter.
*/
public int getLowerCaseDescent() {
return etmLowerCaseDescent;
}
/**
* Tells whether the font has proportional character spacing.
*
* @return ex. true for Times, false for Courier.
*/
public boolean getIsProportional() {
return ((dfPitchAndFamily & 1) == 1);
}
/**
* Returns the bounding box for the font.
* Note: this value is just an approximation,
* it does not really exist in the PFM file.
*
* @return The calculated Font BBox.
*/
public int[] getFontBBox() {
int[] bbox = new int[4];
// Just guessing....
if (!getIsProportional() && (dfAvgWidth == dfMaxWidth)) {
bbox[0] = -20;
} else {
bbox[0] = -100;
}
bbox[1] = getLowerCaseDescent() - 5;
bbox[2] = dfMaxWidth + 10;
bbox[3] = getLowerCaseAscent() + 5;
return bbox;
}
/**
* Indicates whether the font is non-symbolic (Font uses the Adobe standard Latin character
* set or a subset of it).
* @return true if the font is non-symbolic
*/
public boolean isNonSymbolic() {
return (dfCharSet != 2); //!= Symbol fonts
}
/**
* Returns the characteristics flags for the font as
* needed for a PDF font descriptor (See PDF specs).
*
* @return The characteristics flags.
*/
public int getFlags() {
int flags = 0;
if (!getIsProportional()) {
flags |= 1; //bit 1: FixedPitch
}
if (isNonSymbolic()) {
flags |= 32; //bit 6: Nonsymbolic
} else {
flags |= 4; //bit 3: Symbolic
}
//int serif = dfPitchAndFamily & 0xFFFE;
if ((dfPitchAndFamily & 16) != 0) {
flags |= 2; //bit 2: Serif
}
if ((dfPitchAndFamily & 64) != 0) {
flags |= 8; //bit 4: Script
}
if (dfItalic != 0) {
flags |= 64; //bit 7: Italic
}
return flags;
}
/**
* Returns the width of the dominant vertical stems of the font.
* Note: this value is just an approximation,
* it does not really exist in the PFM file.
*
* @return The vertical stem width.
*/
public int getStemV() {
// Just guessing....
if (dfItalic != 0) {
return (int) Math.round(dfMinWidth * 0.25);
} else {
return (int) Math.round(dfMinWidth * 0.6);
}
}
/**
* Returns the italic angle of the font.
* Note: this value is just an approximation,
* it does not really exist in the PFM file.
*
* @return The italic angle.
*/
public int getItalicAngle() {
if (dfItalic != 0) {
return -16; // Just guessing....
} else {
return 0;
}
}
/**
* Returns the width of a character
*
* @param which The number of the character for which the width is requested.
* @return The width of a character.
*/
public int getCharWidth(short which) {
if (extentTable != null) {
return extentTable[which - dfFirstChar];
} else {
//Fixed-width font (PFM may have no extent table)
//we'll just use the average width
return this.dfAvgWidth;
}
}
}