| /* |
| * 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; |
| |
| import java.awt.geom.RectangularShape; |
| import java.io.IOException; |
| import java.io.InputStream; |
| import java.util.Iterator; |
| import java.util.List; |
| import java.util.Set; |
| |
| import org.apache.commons.io.IOUtils; |
| |
| import org.apache.fop.fonts.CodePointMapping; |
| import org.apache.fop.fonts.FontLoader; |
| import org.apache.fop.fonts.FontResolver; |
| import org.apache.fop.fonts.FontType; |
| import org.apache.fop.fonts.SingleByteEncoding; |
| import org.apache.fop.fonts.SingleByteFont; |
| |
| /** |
| * Loads a Type 1 font into memory directly from the original font file. |
| */ |
| public class Type1FontLoader extends FontLoader { |
| |
| private SingleByteFont singleFont; |
| |
| /** |
| * Constructs a new Type 1 font loader. |
| * @param fontFileURI the URI to the PFB file of a Type 1 font |
| * @param embedded indicates whether the font is embedded or referenced |
| * @param useKerning indicates whether to load kerning information if available |
| * @param resolver the font resolver used to resolve URIs |
| * @throws IOException In case of an I/O error |
| */ |
| public Type1FontLoader(String fontFileURI, boolean embedded, boolean useKerning, |
| FontResolver resolver) throws IOException { |
| super(fontFileURI, embedded, useKerning, true, resolver); |
| } |
| |
| private String getPFMURI(String pfbURI) { |
| String pfbExt = pfbURI.substring(pfbURI.length() - 3, pfbURI.length()); |
| String pfmExt = pfbExt.substring(0, 2) |
| + (Character.isUpperCase(pfbExt.charAt(2)) ? "M" : "m"); |
| return pfbURI.substring(0, pfbURI.length() - 4) + "." + pfmExt; |
| } |
| |
| private static final String[] AFM_EXTENSIONS = new String[] {".AFM", ".afm", ".Afm"}; |
| |
| /** {@inheritDoc} */ |
| @Override |
| protected void read() throws IOException { |
| AFMFile afm = null; |
| PFMFile pfm = null; |
| |
| InputStream afmIn = null; |
| for (int i = 0; i < AFM_EXTENSIONS.length; i++) { |
| try { |
| String afmUri = this.fontFileURI.substring(0, this.fontFileURI.length() - 4) |
| + AFM_EXTENSIONS[i]; |
| afmIn = openFontUri(resolver, afmUri); |
| if (afmIn != null) { |
| break; |
| } |
| } catch (IOException ioe) { |
| //Ignore, AFM probably not available under the URI |
| } |
| } |
| if (afmIn != null) { |
| try { |
| AFMParser afmParser = new AFMParser(); |
| afm = afmParser.parse(afmIn); |
| } finally { |
| IOUtils.closeQuietly(afmIn); |
| } |
| } |
| |
| String pfmUri = getPFMURI(this.fontFileURI); |
| InputStream pfmIn = null; |
| try { |
| pfmIn = openFontUri(resolver, pfmUri); |
| } catch (IOException ioe) { |
| //Ignore, PFM probably not available under the URI |
| } |
| if (pfmIn != null) { |
| try { |
| pfm = new PFMFile(); |
| pfm.load(pfmIn); |
| } catch (IOException ioe) { |
| if (afm == null) { |
| //Ignore the exception if we have a valid PFM. PFM is only the fallback. |
| throw ioe; |
| } |
| } finally { |
| IOUtils.closeQuietly(pfmIn); |
| } |
| } |
| |
| if (afm == null && pfm == null) { |
| throw new java.io.FileNotFoundException( |
| "Neither an AFM nor a PFM file was found for " + this.fontFileURI); |
| } |
| buildFont(afm, pfm); |
| this.loaded = true; |
| } |
| |
| private void buildFont(AFMFile afm, PFMFile pfm) { |
| if (afm == null && pfm == null) { |
| throw new IllegalArgumentException("Need at least an AFM or a PFM!"); |
| } |
| singleFont = new SingleByteFont(); |
| singleFont.setFontType(FontType.TYPE1); |
| singleFont.setResolver(this.resolver); |
| if (this.embedded) { |
| singleFont.setEmbedFileName(this.fontFileURI); |
| } |
| returnFont = singleFont; |
| |
| handleEncoding(afm, pfm); |
| handleFontName(afm, pfm); |
| handleMetrics(afm, pfm); |
| } |
| |
| private void handleEncoding(AFMFile afm, PFMFile pfm) { |
| //Encoding |
| if (afm != null) { |
| String encoding = afm.getEncodingScheme(); |
| singleFont.setUseNativeEncoding(true); |
| if ("AdobeStandardEncoding".equals(encoding)) { |
| singleFont.setEncoding(CodePointMapping.STANDARD_ENCODING); |
| addUnencodedBasedOnEncoding(afm); |
| //char codes in the AFM cannot be relied on in this case, so we override |
| afm.overridePrimaryEncoding(singleFont.getEncoding()); |
| } else { |
| String effEncodingName; |
| if ("FontSpecific".equals(encoding)) { |
| effEncodingName = afm.getFontName() + "Encoding"; |
| } else { |
| effEncodingName = encoding; |
| } |
| if (log.isDebugEnabled()) { |
| log.debug("Unusual font encoding encountered: " |
| + encoding + " -> " + effEncodingName); |
| } |
| CodePointMapping mapping = buildCustomEncoding(effEncodingName, afm); |
| singleFont.setEncoding(mapping); |
| addUnencodedBasedOnAFM(afm); |
| } |
| } else { |
| if (pfm.getCharSet() >= 0 && pfm.getCharSet() <= 2) { |
| singleFont.setEncoding(pfm.getCharSetName() + "Encoding"); |
| } else { |
| log.warn("The PFM reports an unsupported encoding (" |
| + pfm.getCharSetName() + "). The font may not work as expected."); |
| singleFont.setEncoding("WinAnsiEncoding"); //Try fallback, no guarantees! |
| } |
| } |
| } |
| |
| private Set<String> toGlyphSet(String[] glyphNames) { |
| Set<String> glyphSet = new java.util.HashSet<String>(); |
| for (String name : glyphNames) { |
| glyphSet.add(name); |
| } |
| return glyphSet; |
| } |
| |
| /** |
| * Adds characters not encoded in the font's primary encoding. This method is used when we |
| * don't trust the AFM to expose the same encoding as the primary font. |
| * @param afm the AFM file. |
| */ |
| private void addUnencodedBasedOnEncoding(AFMFile afm) { |
| SingleByteEncoding encoding = singleFont.getEncoding(); |
| Set<String> glyphNames = toGlyphSet(encoding.getCharNameMap()); |
| List<AFMCharMetrics> charMetrics = afm.getCharMetrics(); |
| for (AFMCharMetrics metrics : charMetrics) { |
| String charName = metrics.getCharName(); |
| if (charName != null && !glyphNames.contains(charName)) { |
| singleFont.addUnencodedCharacter(metrics.getCharacter(), |
| (int)Math.round(metrics.getWidthX())); |
| } |
| } |
| } |
| |
| /** |
| * Adds characters not encoded in the font's primary encoding. This method is used when |
| * the primary encoding is built based on the character codes in the AFM rather than |
| * the specified encoding (ex. with symbolic fonts). |
| * @param afm the AFM file |
| */ |
| private void addUnencodedBasedOnAFM(AFMFile afm) { |
| List charMetrics = afm.getCharMetrics(); |
| for (int i = 0, c = afm.getCharCount(); i < c; i++) { |
| AFMCharMetrics metrics = (AFMCharMetrics)charMetrics.get(i); |
| if (!metrics.hasCharCode() && metrics.getCharacter() != null) { |
| singleFont.addUnencodedCharacter(metrics.getCharacter(), |
| (int)Math.round(metrics.getWidthX())); |
| } |
| } |
| } |
| |
| private void handleFontName(AFMFile afm, PFMFile pfm) { |
| //Font name |
| if (afm != null) { |
| returnFont.setFontName(afm.getFontName()); //PostScript font name |
| returnFont.setFullName(afm.getFullName()); |
| Set names = new java.util.HashSet(); |
| names.add(afm.getFamilyName()); |
| returnFont.setFamilyNames(names); |
| } else { |
| returnFont.setFontName(pfm.getPostscriptName()); |
| String fullName = pfm.getPostscriptName(); |
| fullName = fullName.replace('-', ' '); //Hack! Try to emulate full name |
| returnFont.setFullName(fullName); //emulate afm.getFullName() |
| Set names = new java.util.HashSet(); |
| names.add(pfm.getWindowsName()); //emulate afm.getFamilyName() |
| returnFont.setFamilyNames(names); |
| } |
| } |
| |
| private void handleMetrics(AFMFile afm, PFMFile pfm) { |
| //Basic metrics |
| if (afm != null) { |
| if (afm.getCapHeight() != null) { |
| returnFont.setCapHeight(afm.getCapHeight().intValue()); |
| } |
| if (afm.getXHeight() != null) { |
| returnFont.setXHeight(afm.getXHeight().intValue()); |
| } |
| if (afm.getAscender() != null) { |
| returnFont.setAscender(afm.getAscender().intValue()); |
| } |
| if (afm.getDescender() != null) { |
| returnFont.setDescender(afm.getDescender().intValue()); |
| } |
| |
| returnFont.setFontBBox(afm.getFontBBoxAsIntArray()); |
| if (afm.getStdVW() != null) { |
| returnFont.setStemV(afm.getStdVW().intValue()); |
| } else { |
| returnFont.setStemV(80); //Arbitrary value |
| } |
| returnFont.setItalicAngle((int)afm.getWritingDirectionMetrics(0).getItalicAngle()); |
| } else { |
| returnFont.setFontBBox(pfm.getFontBBox()); |
| returnFont.setStemV(pfm.getStemV()); |
| returnFont.setItalicAngle(pfm.getItalicAngle()); |
| } |
| if (pfm != null) { |
| //Sometimes the PFM has these metrics while the AFM doesn't (ex. Symbol) |
| if (returnFont.getCapHeight() == 0) { |
| returnFont.setCapHeight(pfm.getCapHeight()); |
| } |
| if (returnFont.getXHeight(1) == 0) { |
| returnFont.setXHeight(pfm.getXHeight()); |
| } |
| if (returnFont.getAscender() == 0) { |
| returnFont.setAscender(pfm.getLowerCaseAscent()); |
| } |
| if (returnFont.getDescender() == 0) { |
| returnFont.setDescender(pfm.getLowerCaseDescent()); |
| } |
| } |
| |
| //Fallbacks when some crucial font metrics aren't available |
| //(the following are all optional in AFM, but FontBBox is always available) |
| if (returnFont.getXHeight(1) == 0) { |
| int xHeight = 0; |
| if (afm != null) { |
| AFMCharMetrics chm = afm.getChar("x"); |
| if (chm != null) { |
| RectangularShape rect = chm.getBBox(); |
| if (rect != null) { |
| xHeight = (int)Math.round(rect.getMinX()); |
| } |
| } |
| } |
| if (xHeight == 0) { |
| xHeight = Math.round(returnFont.getFontBBox()[3] * 0.6f); |
| } |
| returnFont.setXHeight(xHeight); |
| } |
| if (returnFont.getAscender() == 0) { |
| int asc = 0; |
| if (afm != null) { |
| AFMCharMetrics chm = afm.getChar("d"); |
| if (chm != null) { |
| RectangularShape rect = chm.getBBox(); |
| if (rect != null) { |
| asc = (int)Math.round(rect.getMinX()); |
| } |
| } |
| } |
| if (asc == 0) { |
| asc = Math.round(returnFont.getFontBBox()[3] * 0.9f); |
| } |
| returnFont.setAscender(asc); |
| } |
| if (returnFont.getDescender() == 0) { |
| int desc = 0; |
| if (afm != null) { |
| AFMCharMetrics chm = afm.getChar("p"); |
| if (chm != null) { |
| RectangularShape rect = chm.getBBox(); |
| if (rect != null) { |
| desc = (int)Math.round(rect.getMinX()); |
| } |
| } |
| } |
| if (desc == 0) { |
| desc = returnFont.getFontBBox()[1]; |
| } |
| returnFont.setDescender(desc); |
| } |
| if (returnFont.getCapHeight() == 0) { |
| returnFont.setCapHeight(returnFont.getAscender()); |
| } |
| |
| if (afm != null) { |
| String charSet = afm.getCharacterSet(); |
| int flags = 0; |
| if ("Special".equals(charSet)) { |
| flags |= 4; //bit 3: Symbolic |
| } else { |
| if (singleFont.getEncoding().mapChar('A') == 'A') { |
| //High likelyhood that the font is non-symbolic |
| flags |= 32; //bit 6: Nonsymbolic |
| } else { |
| flags |= 4; //bit 3: Symbolic |
| } |
| } |
| if (afm.getWritingDirectionMetrics(0).isFixedPitch()) { |
| flags |= 1; //bit 1: FixedPitch |
| } |
| if (afm.getWritingDirectionMetrics(0).getItalicAngle() != 0.0) { |
| flags |= 64; //bit 7: Italic |
| } |
| returnFont.setFlags(flags); |
| |
| returnFont.setFirstChar(afm.getFirstChar()); |
| returnFont.setLastChar(afm.getLastChar()); |
| for (AFMCharMetrics chm : afm.getCharMetrics()) { |
| if (chm.hasCharCode()) { |
| singleFont.setWidth(chm.getCharCode(), (int)Math.round(chm.getWidthX())); |
| } |
| } |
| if (useKerning) { |
| returnFont.replaceKerningMap(afm.createXKerningMapEncoded()); |
| } |
| } else { |
| returnFont.setFlags(pfm.getFlags()); |
| returnFont.setFirstChar(pfm.getFirstChar()); |
| returnFont.setLastChar(pfm.getLastChar()); |
| for (short i = pfm.getFirstChar(); i <= pfm.getLastChar(); i++) { |
| singleFont.setWidth(i, pfm.getCharWidth(i)); |
| } |
| if (useKerning) { |
| returnFont.replaceKerningMap(pfm.getKerning()); |
| } |
| } |
| } |
| |
| private CodePointMapping buildCustomEncoding(String encodingName, AFMFile afm) { |
| List chars = afm.getCharMetrics(); |
| int mappingCount = 0; |
| //Just count the first time... |
| Iterator iter = chars.iterator(); |
| while (iter.hasNext()) { |
| AFMCharMetrics charMetrics = (AFMCharMetrics)iter.next(); |
| if (charMetrics.getCharCode() >= 0) { |
| String u = charMetrics.getUnicodeSequence(); |
| if (u != null && u.length() == 1) { |
| mappingCount++; |
| } |
| } |
| } |
| //...and now build the table. |
| int[] table = new int[mappingCount * 2]; |
| String[] charNameMap = new String[256]; |
| iter = chars.iterator(); |
| int idx = 0; |
| while (iter.hasNext()) { |
| AFMCharMetrics charMetrics = (AFMCharMetrics)iter.next(); |
| if (charMetrics.getCharCode() >= 0) { |
| charNameMap[charMetrics.getCharCode()] = charMetrics.getCharName(); |
| String unicodes = charMetrics.getUnicodeSequence(); |
| if (unicodes == null) { |
| log.info("No Unicode mapping for glyph: " + charMetrics); |
| } else if (unicodes.length() == 1) { |
| table[idx] = charMetrics.getCharCode(); |
| idx++; |
| table[idx] = unicodes.charAt(0); |
| idx++; |
| } else { |
| log.warn("Multi-character representation of glyph not currently supported: " |
| + charMetrics); |
| } |
| } |
| } |
| return new CodePointMapping(encodingName, table, charNameMap); |
| } |
| } |