| /* |
| * 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; |
| } |
| } |
| |
| } |