/* * 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]; if ((bufin.read(b) == b.length) && 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 { if (inStream.skip(80) != 80) { throw new IOException("premature EOF when skipping 80 bytes"); } dfItalic = inStream.readByte(); if (inStream.skip(2) != 2) { throw new IOException("premature EOF when skipping 2 bytes"); } inStream.readShort(); // dfWeight = dfCharSet = inStream.readByte(); if (inStream.skip(4) != 4) { throw new IOException("premature EOF when skipping 4 bytes"); } dfPitchAndFamily = inStream.readByte(); dfAvgWidth = inStream.readShort(); dfMaxWidth = inStream.readShort(); dfFirstChar = inStream.readByte(); dfLastChar = inStream.readByte(); if (inStream.skip(8) != 8) { throw new IOException("premature EOF when skipping 8 bytes"); } long faceOffset = inStream.readInt(); inStream.reset(); if (inStream.skip(faceOffset) != faceOffset) { throw new IOException("premature EOF when skipping faceOffset bytes"); } windowsName = inStream.readString(); inStream.reset(); if (inStream.skip(117) != 117) { throw new IOException("premature EOF when skipping 117 bytes"); } } /** * 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(); if (inStream.skip(4) != 4) { //Skip dfOriginTable throw new IOException("premature EOF when skipping dfOriginTable bytes"); } final long kernPairOffset = inStream.readInt(); if (inStream.skip(4) != 4) { //Skip dfTrackKernTable throw new IOException("premature EOF when skipping dfTrackKernTable bytes"); } long driverInfoOffset = inStream.readInt(); if (kernPairOffset > 0) { inStream.reset(); if (inStream.skip(kernPairOffset) != kernPairOffset) { throw new IOException("premature EOF when skipping kernPairOffset bytes"); } loadKernPairs(inStream); } inStream.reset(); if (inStream.skip(driverInfoOffset) != driverInfoOffset) { throw new IOException("premature EOF when skipping driverInfoOffset bytes"); } postscriptName = inStream.readString(); if (extMetricsOffset != 0) { inStream.reset(); if (inStream.skip(extMetricsOffset) != extMetricsOffset) { throw new IOException("premature EOF when skipping extMetricsOffset bytes"); } loadExtMetrics(inStream); } if (extentTableOffset != 0) { inStream.reset(); if (inStream.skip(extentTableOffset) != extentTableOffset) { throw new IOException("premature EOF when skipping extentTableOffset bytes"); } 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(g1); if (adjTab == null) { adjTab = new HashMap<Integer, Integer>(); } adjTab.put(g2, adj); kerningTab.put(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."); } if (inStream.skip(12) != 12) { //Skip etmPointSize, etmOrientation, etmMasterHeight, //etmMinScale, etmMaxScale, emtMasterUnits throw new IOException("premature EOF when skipping etmPointSize, ... bytes"); } 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; } } }