diff options
author | Robert Meyer <rmeyer@apache.org> | 2014-05-23 15:05:19 +0000 |
---|---|---|
committer | Robert Meyer <rmeyer@apache.org> | 2014-05-23 15:05:19 +0000 |
commit | 764eba0b31076d7220082ac167f18cc75c427e27 (patch) | |
tree | 3e209ccb9e5e1a376b94e9e5ea29a811b45e3129 /src/java/org/apache/fop | |
parent | c4e5885007f3c61837a12ab330658b131d978247 (diff) | |
download | xmlgraphics-fop-764eba0b31076d7220082ac167f18cc75c427e27.tar.gz xmlgraphics-fop-764eba0b31076d7220082ac167f18cc75c427e27.zip |
FOP-2354: Subset support for Type 1 fonts
git-svn-id: https://svn.apache.org/repos/asf/xmlgraphics/fop/trunk@1597112 13f79535-47bb-0310-9956-ffa450edef68
Diffstat (limited to 'src/java/org/apache/fop')
12 files changed, 1705 insertions, 34 deletions
diff --git a/src/java/org/apache/fop/fonts/AbstractCodePointMapping.java b/src/java/org/apache/fop/fonts/AbstractCodePointMapping.java index 903fe56ec..20cef2d0b 100644 --- a/src/java/org/apache/fop/fonts/AbstractCodePointMapping.java +++ b/src/java/org/apache/fop/fonts/AbstractCodePointMapping.java @@ -179,6 +179,10 @@ public class AbstractCodePointMapping implements SingleByteEncoding { return -1; } + public String getNameFromCodePoint(int idx) { + return getCharNameMap()[idx]; + } + /** {@inheritDoc} */ public String[] getCharNameMap() { if (this.charNameMap != null) { diff --git a/src/java/org/apache/fop/fonts/FontLoader.java b/src/java/org/apache/fop/fonts/FontLoader.java index 09e38260e..a04ee6d5f 100644 --- a/src/java/org/apache/fop/fonts/FontLoader.java +++ b/src/java/org/apache/fop/fonts/FontLoader.java @@ -99,11 +99,8 @@ public abstract class FontLoader { throw new IllegalArgumentException( "CID encoding mode not supported for Type 1 fonts"); } - if (embeddingMode == EmbeddingMode.SUBSET) { - throw new IllegalArgumentException( - "Subset embedding for Type 1 fonts is not supported"); - } - loader = new Type1FontLoader(fontFileURI, embedded, useKerning, resourceResolver); + loader = new Type1FontLoader(fontFileURI, embedded, embeddingMode, useKerning, + resourceResolver); } else { loader = new OFFontLoader(fontFileURI, subFontName, embedded, embeddingMode, encodingMode, useKerning, useAdvanced, resourceResolver); diff --git a/src/java/org/apache/fop/fonts/FontReader.java b/src/java/org/apache/fop/fonts/FontReader.java index 0448c317e..bd64a7595 100644 --- a/src/java/org/apache/fop/fonts/FontReader.java +++ b/src/java/org/apache/fop/fonts/FontReader.java @@ -162,13 +162,13 @@ public class FontReader extends DefaultHandler { isCID = true; TTFReader.checkMetricsVersion(attributes); } else if ("TRUETYPE".equals(attributes.getValue("type"))) { - singleFont = new SingleByteFont(resourceResolver); + singleFont = new SingleByteFont(resourceResolver, EmbeddingMode.AUTO); singleFont.setFontType(FontType.TRUETYPE); returnFont = singleFont; isCID = false; TTFReader.checkMetricsVersion(attributes); } else { - singleFont = new SingleByteFont(resourceResolver); + singleFont = new SingleByteFont(resourceResolver, EmbeddingMode.AUTO); singleFont.setFontType(FontType.TYPE1); returnFont = singleFont; isCID = false; diff --git a/src/java/org/apache/fop/fonts/SingleByteFont.java b/src/java/org/apache/fop/fonts/SingleByteFont.java index 2a6b04761..7b17c0b5d 100644 --- a/src/java/org/apache/fop/fonts/SingleByteFont.java +++ b/src/java/org/apache/fop/fonts/SingleByteFont.java @@ -21,7 +21,9 @@ package org.apache.fop.fonts; import java.awt.Rectangle; import java.util.ArrayList; +import java.util.Collections; import java.util.HashMap; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Set; @@ -34,6 +36,7 @@ import org.apache.xmlgraphics.fonts.Glyphs; import org.apache.fop.apps.io.InternalResourceResolver; import org.apache.fop.fonts.truetype.OpenFont.PostScriptVersion; +import org.apache.fop.util.CharUtilities; /** * Generic SingleByte font @@ -56,14 +59,30 @@ public class SingleByteFont extends CustomFont { private PostScriptVersion ttPostScriptVersion; - /** - * @param resourceResolver the URI resolver for controlling file access - */ + private int usedGlyphsCount; + private LinkedHashMap<Integer, String> usedGlyphNames; + private Map<Integer, Integer> usedGlyphs; + private Map<Integer, Character> usedCharsIndex; + public SingleByteFont(InternalResourceResolver resourceResolver) { super(resourceResolver); setEncoding(CodePointMapping.WIN_ANSI_ENCODING); } + public SingleByteFont(InternalResourceResolver resourceResolver, EmbeddingMode embeddingMode) { + this(resourceResolver); + setEmbeddingMode(embeddingMode); + if (embeddingMode != EmbeddingMode.FULL) { + usedGlyphNames = new LinkedHashMap<Integer, String>(); + usedGlyphs = new HashMap<Integer, Integer>(); + usedCharsIndex = new HashMap<Integer, Character>(); + + // The zeroth value is reserved for .notdef + usedGlyphs.put(0, 0); + usedGlyphsCount++; + } + } + /** {@inheritDoc} */ public boolean isEmbeddable() { return (!(getEmbedFileURI() == null @@ -72,7 +91,7 @@ public class SingleByteFont extends CustomFont { /** {@inheritDoc} */ public boolean isSubsetEmbedded() { - return false; + return getEmbeddingMode() != EmbeddingMode.FULL; } /** {@inheritDoc} */ @@ -182,22 +201,53 @@ public class SingleByteFont extends CustomFont { return d; } + private boolean isSubset() { + return getEmbeddingMode() == EmbeddingMode.SUBSET; + } + /** {@inheritDoc} */ @Override public char mapChar(char c) { notifyMapOperation(); char d = lookupChar(c); - if (d != SingleByteEncoding.NOT_FOUND_CODE_POINT) { - return d; - } else { + if (d == SingleByteEncoding.NOT_FOUND_CODE_POINT) { // Check for alternative d = findAlternative(c); if (d != SingleByteEncoding.NOT_FOUND_CODE_POINT) { return d; + } else { + this.warnMissingGlyph(c); + return Typeface.NOT_FOUND; } } - this.warnMissingGlyph(c); - return Typeface.NOT_FOUND; + if (isEmbeddable() && isSubset()) { + mapChar(d, c); + } + return d; + } + + private int mapChar(int glyphIndex, char unicode) { + // Reencode to a new subset font or get the reencoded value + // IOW, accumulate the accessed characters and build a character map for them + Integer subsetCharSelector = usedGlyphs.get(glyphIndex); + if (subsetCharSelector == null) { + int selector = usedGlyphsCount; + usedGlyphs.put(glyphIndex, selector); + usedCharsIndex.put(selector, unicode); + usedGlyphsCount++; + return selector; + } else { + return subsetCharSelector; + } + } + + private char getUnicode(int index) { + Character mapValue = usedCharsIndex.get(index); + if (mapValue != null) { + return mapValue.charValue(); + } else { + return CharUtilities.NOT_A_CHARACTER; + } } private char mapUnencodedChar(char ch) { @@ -457,5 +507,34 @@ public class SingleByteFont extends CustomFont { return ttPostScriptVersion; } + /** + * Returns a Map of used Glyphs. + * @return Map Map of used Glyphs + */ + public Map<Integer, Integer> getUsedGlyphs() { + return Collections.unmodifiableMap(usedGlyphs); + } + + public char getUnicodeFromSelector(int selector) { + return getUnicode(selector); + } + + public void mapUsedGlyphName(int gid, String value) { + usedGlyphNames.put(gid, value); + } + + public Map<Integer, String> getUsedGlyphNames() { + return usedGlyphNames; + } + + public String getGlyphName(int idx) { + if (idx < mapping.getCharNameMap().length) { + return mapping.getCharNameMap()[idx]; + } else { + int selector = usedGlyphs.get(idx); + char theChar = usedCharsIndex.get(selector); + return unencodedCharacters.get(theChar).getCharacter().getName(); + } + } } diff --git a/src/java/org/apache/fop/fonts/truetype/OFFontLoader.java b/src/java/org/apache/fop/fonts/truetype/OFFontLoader.java index 7168389ff..4216cf290 100644 --- a/src/java/org/apache/fop/fonts/truetype/OFFontLoader.java +++ b/src/java/org/apache/fop/fonts/truetype/OFFontLoader.java @@ -137,7 +137,7 @@ public class OFFontLoader extends FontLoader { returnFont = multiFont; multiFont.setTTCName(ttcFontName); } else { - singleFont = new SingleByteFont(resourceResolver); + singleFont = new SingleByteFont(resourceResolver, embeddingMode); returnFont = singleFont; } diff --git a/src/java/org/apache/fop/fonts/type1/AdobeStandardEncoding.java b/src/java/org/apache/fop/fonts/type1/AdobeStandardEncoding.java index 514d03185..d3d5a969d 100644 --- a/src/java/org/apache/fop/fonts/type1/AdobeStandardEncoding.java +++ b/src/java/org/apache/fop/fonts/type1/AdobeStandardEncoding.java @@ -26,7 +26,7 @@ import java.util.Map; * Enumerates the {@linkplain http://unicode.org/Public/MAPPINGS/VENDORS/ADOBE/stdenc.txt} for * characters found in a Type1 font. */ -enum AdobeStandardEncoding { +public enum AdobeStandardEncoding { /** space character */ space(0x0020, 0x20, "SPACE", "space"), /** space character */ @@ -407,4 +407,13 @@ enum AdobeStandardEncoding { AdobeStandardEncoding encoding = CACHE.get(adobeName); return encoding != null ? encoding.getAdobeCodePoint() : -1; } + + public static String getCharFromCodePoint(int codePoint) { + for (AdobeStandardEncoding encoding : CACHE.values()) { + if (encoding.getAdobeCodePoint() == codePoint) { + return encoding.getAdobeName(); + } + } + return ""; + } } diff --git a/src/java/org/apache/fop/fonts/type1/PFBData.java b/src/java/org/apache/fop/fonts/type1/PFBData.java index e75c60117..05f0ec78f 100644 --- a/src/java/org/apache/fop/fonts/type1/PFBData.java +++ b/src/java/org/apache/fop/fonts/type1/PFBData.java @@ -85,6 +85,14 @@ public class PFBData { } /** + * Gets the header segment of the font file + * @return Header segment as a byte array + */ + public byte[] getHeaderSegment() { + return this.headerSegment.clone(); + } + + /** * Sets the encrypted segment of the font file. * @param encryptedSeg the encrypted segment */ @@ -93,6 +101,14 @@ public class PFBData { } /** + * Gets the encrypted segment of the font file + * @return The encrypted segment as a byte array + */ + public byte[] getEncryptedSegment() { + return this.encryptedSegment.clone(); + } + + /** * Sets the trailer segment of the font file. * @param trailerSeg the trailer segment */ @@ -101,6 +117,14 @@ public class PFBData { } /** + * Gets the trailer segment of the font file + * @return The trailer segment as a byte array + */ + public byte[] getTrailerSegment() { + return this.trailerSegment.clone(); + } + + /** * Returns the full length of the raw font file. * @return int the raw file length */ diff --git a/src/java/org/apache/fop/fonts/type1/PostscriptParser.java b/src/java/org/apache/fop/fonts/type1/PostscriptParser.java new file mode 100644 index 000000000..05c3c6453 --- /dev/null +++ b/src/java/org/apache/fop/fonts/type1/PostscriptParser.java @@ -0,0 +1,655 @@ +/* + * 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.io.IOException; +import java.io.UnsupportedEncodingException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map.Entry; +import java.util.Scanner; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +class PostscriptParser { + + protected static final Log LOG = LogFactory.getLog(PostscriptParser.class); + /* Patterns used to identify Postscript elements */ + private static final String DICTIONARY = "dict"; + private static final String FIXED_ARRAY = "array"; + private static final String VARIABLE_ARRAY = "["; + private static final String SUBROUTINE = "{"; + /* A list of parsed subroutines so if they are encountered during the parsing + * phase of another element, they can be read and pattern matched. */ + private HashMap<String, PSSubroutine> subroutines = new HashMap<String, PSSubroutine>(); + + /** + * Parses the postscript document and returns a list of elements + * @param segment The byte array containing the postscript data + * @return A list of found Postscript elements + * @throws IOException + */ + public List<PSElement> parse(byte[] segment) throws IOException { + List<PSElement> parsedElements = new ArrayList<PSElement>(); + /* Currently only scan and store the top level element. For deeper + * Postscript parsing you can push and pop elements from a stack */ + PSElement foundElement = null; + String operator = null; + StringBuilder token = new StringBuilder(); + List<String> tokens = new ArrayList<String>(); + int startPoint = -1; + boolean specialDelimiter = false; + boolean lastWasSpecial = false; + for (int i = 0; i < segment.length; i++) { + byte cur = segment[i]; + if (foundElement != null && foundElement.hasMore()) { + foundElement.parse(cur, i); + continue; + } else { + char c = (char)cur; + if (!lastWasSpecial) { + specialDelimiter = (c == '{' || c == '}' || c == '[' || c == ']' + || (!token.toString().equals("") && c == '/')); + boolean isNotBreak = !(c == ' ' || c == '\r' || cur == 15 || cur == 12 + || cur == 10); + if (isNotBreak && !specialDelimiter) { + token.append(c); + continue; + } + } else { + lastWasSpecial = false; + token.append(c); + if (token.toString().equals("/")) { + continue; + } + } + } + try { + boolean setOp = false; + if ((foundElement == null || !foundElement.hasMore()) && token.length() > 1 + && token.charAt(0) == '/' && tokens.size() != 1 || hasEndToken(token.toString())) { + operator = token.toString(); + setOp = true; + if (tokens.size() > 2 && tokens.get(tokens.size() - 1).equals("def")) { + PSVariable newVar = new PSVariable(tokens.get(0), startPoint); + newVar.setValue(tokens.get(1)); + newVar.setEndPoint(i - operator.length()); + parsedElements.add(newVar); + } + tokens.clear(); + startPoint = i - token.length(); + } + if (operator != null) { + if (foundElement instanceof PSSubroutine) { + PSSubroutine sub = (PSSubroutine)foundElement; + subroutines.put(sub.getOperator(), sub); + parsedElements.add(sub); + if (!setOp) { + operator = ""; + } + } else { + if (foundElement != null) { + if (!hasMatch(foundElement.getOperator(), parsedElements)) { + parsedElements.add(foundElement); + } else { + LOG.warn("Duplicate " + foundElement.getOperator() + + " in font file, Ignoring."); + } + } + } + //Compare token against patterns and create an element if matched + foundElement = createElement(operator, token.toString(), startPoint); + } + } finally { + tokens.add(token.toString()); + token = new StringBuilder(); + if (specialDelimiter) { + specialDelimiter = false; + lastWasSpecial = true; + //Retrace special postscript character so it can be processed separately + i--; + } + } + } + return parsedElements; + } + + private boolean hasEndToken(String token) { + return token.equals("currentdict"); + } + + private boolean hasMatch(String operator, List<PSElement> elements) { + for (PSElement element : elements) { + if (element.getOperator().equals(operator)) { + return true; + } + } + return false; + } + + public PSElement createElement(String operator, String elementID, int startPoint) { + if (operator.equals("")) { + return null; + } + if (elementID.equals(FIXED_ARRAY)) { + return new PSFixedArray(operator, startPoint); + } else if (elementID.equals(VARIABLE_ARRAY)) { + return new PSVariableArray(operator, startPoint); + } else if (elementID.equals(SUBROUTINE)) { + return new PSSubroutine(operator, startPoint); + } else if (!operator.equals("/Private") && elementID.equals(DICTIONARY)) { + return new PSDictionary(operator, startPoint); + } + return null; + } + + /** + * A base Postscript element class + */ + public abstract class PSElement { + /* The identifying operator for this element */ + protected String operator; + private List<Byte> token; + /* Determines whether there is any more data to be read whilst parsing */ + protected boolean hasMore = true; + /* The locations of any entries containing binary data (e.g. arrays) */ + protected LinkedHashMap<String, int[]> binaryEntries; + /* The tokens parsed from the current element */ + protected List<String> tokens; + /* Determines whether binary data is currently being read / parsed */ + protected boolean readBinary = false; + /* The location of the element within the binary data */ + private int startPoint = -1; + protected int endPoint = -1; + /* A flag to determine if unexpected postscript has been found in the element */ + private boolean foundUnexpected = false; + + public PSElement(String operator, int startPoint) { + this.operator = operator; + this.startPoint = startPoint; + token = new ArrayList<Byte>(); + binaryEntries = new LinkedHashMap<String, int[]>(); + tokens = new ArrayList<String>(); + } + + /** + * Gets the Postscript element operator + * @return The operator returned as a string + */ + public String getOperator() { + return operator; + } + + /** + * The start location of the element within the source binary data + * @return The start location returned as an integer + */ + public int getStartPoint() { + return startPoint; + } + + /** + * The end location of the element within the source binary data + * @return The end location returned as an integer + */ + public int getEndPoint() { + return endPoint; + } + + /** + * Takes over the task of tokenizing the byte data + * @param cur The current byte being read + */ + public void parse(byte cur, int pos) throws UnsupportedEncodingException { + if (!readBinary) { + char c = (char)cur; + boolean specialDelimiter = (c == '{' || c == '}' || c == '[' || c == ']' + || c == '(' || c == ')'); + boolean isNotValidBreak = !(c == ' ' || cur == 15 || cur == 12 || c == '\r' + || c == 10); + if (isNotValidBreak && !specialDelimiter) { + token.add(cur); + } else { + parseToken(pos); + } + if (specialDelimiter) { + token.add(cur); + parseToken(pos); + } + } else { + parseByte(cur, pos); + } + } + + private void parseToken(int pos) throws UnsupportedEncodingException { + byte[] bytesToken = new byte[token.size()]; + for (int i = 0; i < token.size(); i++) { + bytesToken[i] = token.get(i).byteValue(); + } + parseToken(new String(bytesToken, "ASCII"), pos); + token.clear(); + } + + /** + * Passes responsibility for processing the byte stream to the PostScript object + * @param cur The byte currently being read + * @param pos The position of the given byte + */ + public abstract void parseByte(byte cur, int pos); + + /** + * Delegates the parse routine to a sub class + * @param token The token which to parse + */ + public abstract void parseToken(String token, int curPos); + + protected boolean isInteger(String intValue) { + try { + Integer.parseInt(intValue); + return true; + } catch (NumberFormatException ex) { + return false; + } + } + + public LinkedHashMap<String, int[]> getBinaryEntries() { + return binaryEntries; + } + + /** + * Gets the binary entry location of a given index from the array + * @param index The index for which to retrieve the binary data location + * @return + */ + public int[] getBinaryEntryByIndex(int index) { + int count = 0; + for (Entry<String, int[]> entry : binaryEntries.entrySet()) { + if (count == index) { + return entry.getValue(); + } + count++; + } + return new int[0]; + } + + /** + * Determines if more data is still to be parsed for the Postscript element. + * @return Returns true if more data exists + */ + public boolean hasMore() { + return hasMore; + } + + /** + * Sets a value to be true if an expected entry postscript is found in the element. + * An example is where the encoding table may have a series of postscript operators + * altering the state of the array. In this case the only option will be to + * fully embed the font to avoid incorrect encoding in the resulting subset. + * @param foundUnexpected true if unexpected postscript is found. + */ + protected void setFoundUnexpected(boolean foundUnexpected) { + this.foundUnexpected = foundUnexpected; + } + + /** + * Returns whether unexpected postscript has been found in the element + * @return true if unexpected postscript is found + */ + public boolean getFoundUnexpected() { + return this.foundUnexpected; + } + } + + /** + * An object representing a Postscript array with a fixed number of entries + */ + public class PSFixedArray extends PSElement { + + private String entry = ""; + private String token = ""; + private boolean finished = false; + protected int binaryLength = 0; + /* A list containing each entry and it's contents in the array */ + private HashMap<Integer, String> entries; + private static final String READ_ONLY = "readonly"; + + public PSFixedArray(String operator, int startPoint) { + super(operator, startPoint); + entries = new HashMap<Integer, String>(); + } + + @Override + public void parseToken(String token, int curPos) { + if (!checkForEnd(token) || token.equals("def")) { + hasMore = false; + endPoint = curPos; + return; + } + if (token.equals("dup")) { + if (entry.startsWith("dup")) { + addEntry(entry); + } + entry = ""; + tokens.clear(); + } + if (!token.equals(READ_ONLY)) { + entry += token + " "; + } + if (!token.trim().equals("")) { + tokens.add(token); + } + if (tokens.size() == 4 && tokens.get(0).equals("dup") && isInteger(tokens.get(2))) { + binaryLength = Integer.parseInt(tokens.get(2)); + readBinary = true; + } + } + + private boolean checkForEnd(String checkToken) { + boolean subFound = false; + //Check for a subroutine matching that of an array end definition + PSSubroutine sub = subroutines.get("/" + checkToken); + if (sub != null && sub.getSubroutine().contains("def")) { + subFound = true; + } + if (!finished && (subFound || checkToken.equals("def"))) { + finished = true; + addEntry(entry); + return false; + } else { + return !finished; + } + } + + /** + * Gets a map of array entries identified by index + * @return Returns the map of array entries + */ + public HashMap<Integer, String> getEntries() { + return entries; + } + + private void addEntry(String entry) { + if (!entry.equals("")) { + if (entry.indexOf('/') != -1 && entry.charAt(entry.indexOf('/') - 1) != ' ') { + entry = entry.replace("/", " /"); + } + int entryLen; + do { + entryLen = entry.length(); + entry = entry.replace(" ", " "); + } while (entry.length() != entryLen); + Scanner s = new Scanner(entry).useDelimiter(" "); + boolean valid = false; + do { + s.next(); + if (!s.hasNext()) { + break; + } + int id = s.nextInt(); + entries.put(id, entry); + valid = true; + } while (false); + if (!valid) { + setFoundUnexpected(true); + } + } + } + + @Override + public void parseByte(byte cur, int pos) { + if (binaryLength > 0) { + token += (char)cur; + binaryLength--; + } else { + if (readBinary) { + int bLength = Integer.parseInt(tokens.get(2)); + int start = pos - bLength; + int end = start + bLength; + binaryEntries.put(tokens.get(1), new int[] {start, end}); + token = ""; + readBinary = false; + } else { + tokens.add(token); + parseToken(token, pos); + token = ""; + } + } + } + } + + /** + * An object representing a Postscript array with a variable number of entries + */ + public class PSVariableArray extends PSElement { + private int level = 0; + private List<String> arrayItems; + private String entry = ""; + + public PSVariableArray(String operator, int startPoint) { + super(operator, startPoint); + arrayItems = new ArrayList<String>(); + } + + @Override + public void parseToken(String token, int curPos) { + entry += token + " "; + if (level <= 0 && token.length() > 0 && token.charAt(0) == ']') { + hasMore = false; + endPoint = curPos; + return; + } + /* If the array item is a subroutine, the following keeps track of the current level + * of the tokens being parsed so that it can identify the finish */ + if (token.equals("{")) { + level++; + } else if (token.equals("}")) { + level--; + if (!entry.equals("") && level == 0) { + arrayItems.add(entry); + entry = ""; + } + } + } + + /** + * Gets a list of found array entries within the variable array + * @return Returns the found array elements as a list + */ + public List<String> getEntries() { + return arrayItems; + } + + @Override + public void parseByte(byte cur, int pos) { + //Not currently used + } + } + + /** + * An object representing a Postscript subroutine element + */ + public class PSSubroutine extends PSElement { + private int level = 1; + private String entry = ""; + + public PSSubroutine(String operator, int startPoint) { + super(operator, startPoint); + } + + @Override + public void parseToken(String token, int curPos) { + if (level == 0 && token.length() > 0 && (token.equals("def") || token.equals("ifelse") + || token.charAt(0) == '}')) { + hasMore = false; + endPoint = curPos; + return; + } + if (token.equals("{")) { + level++; + } else if (token.equals("}")) { + level--; + } + entry += token + " "; + } + + /** + * Gets the parsed subroutine element as unmodified string + * @return The subroutine as a string + */ + public String getSubroutine() { + return entry.trim(); + } + + @Override + public void parseByte(byte cur, int pos) { + //Not currently used + } + } + + /** + * An object representing a Postscript dictionary + */ + public class PSDictionary extends PSElement { + /* A list of dictionary entries which they themselves could be variables, + * subroutines and arrays, This is currently left as parsed Strings as there is + * no need to delve deeper for our current purposes. */ + private HashMap<String, String> entries; + private String entry = ""; + private String token = ""; + protected int binaryLength = 0; + + public PSDictionary(String operator, int startPoint) { + super(operator, startPoint); + entries = new HashMap<String, String>(); + } + + @Override + public void parseToken(String token, int curPos) { + if (token.equals("end")) { + addEntry(entry); + hasMore = false; + endPoint = curPos; + return; + } + if (token.startsWith("/")) { + if (entry.trim().startsWith("/")) { + tokens.clear(); + addEntry(entry); + } + entry = ""; + } + if (tokens.size() >= 1 || token.startsWith("/")) { + tokens.add(token); + } + entry += token + " "; + if (tokens.size() == 3 && tokens.get(0).startsWith("/") && !tokens.get(2).equals("def") + && isInteger(tokens.get(1))) { + binaryLength = Integer.parseInt(tokens.get(1)); + readBinary = true; + } + } + + /** + * Gets a map of dictionary entries identified by their name + * @return Returns the dictionary entries as a map + */ + public HashMap<String, String> getEntries() { + return entries; + } + + private void addEntry(String entry) { + Scanner s = new Scanner(entry).useDelimiter(" "); + String id = s.next(); + entries.put(id, entry); + } + + @Override + public void parseByte(byte cur, int pos) { + if (binaryLength > 0) { + binaryLength--; + } else { + if (readBinary) { + int start = pos - Integer.parseInt(tokens.get(1)); + int end = pos; + binaryEntries.put(tokens.get(0), new int[] {start, end}); + readBinary = false; + } else { + tokens.add(token); + parseToken(token, pos); + } + } + } + } + + /** + * An object representing a Postscript variable + */ + public class PSVariable extends PSElement { + + /* The value of the parsed Postscript variable. */ + private String value = ""; + + public PSVariable(String operator, int startPoint) { + super(operator, startPoint); + } + + @Override + public void parseToken(String token, int curPos) { + if (token.equals("def")) { + hasMore = false; + endPoint = curPos; + return; + } + } + + @Override + public void parseByte(byte cur, int pos) { + //Not currently used + } + + /** + * Sets the value of the Postscript variable value + * @param value The value to set + */ + public void setValue(String value) { + this.value = value; + } + + /** + * Gets the value of the Postscript variable + * @return Returns the value as a String + */ + public String getValue() { + return value; + } + + /** + * Sets the end point location of the current Postscript variable. + * @param endPoint The end point location as an integer + */ + public void setEndPoint(int endPoint) { + this.endPoint = endPoint; + } + + } +} diff --git a/src/java/org/apache/fop/fonts/type1/Type1FontLoader.java b/src/java/org/apache/fop/fonts/type1/Type1FontLoader.java index 260ef209f..802c3c033 100644 --- a/src/java/org/apache/fop/fonts/type1/Type1FontLoader.java +++ b/src/java/org/apache/fop/fonts/type1/Type1FontLoader.java @@ -34,6 +34,7 @@ import org.apache.commons.logging.LogFactory; import org.apache.fop.apps.io.InternalResourceResolver; import org.apache.fop.fonts.CodePointMapping; +import org.apache.fop.fonts.EmbeddingMode; import org.apache.fop.fonts.FontLoader; import org.apache.fop.fonts.FontType; import org.apache.fop.fonts.SingleByteEncoding; @@ -48,6 +49,8 @@ public class Type1FontLoader extends FontLoader { private SingleByteFont singleFont; + private EmbeddingMode embeddingMode; + /** * Constructs a new Type 1 font loader. * @param fontFileURI the URI to the PFB file of a Type 1 font @@ -56,9 +59,10 @@ public class Type1FontLoader extends FontLoader { * @param resourceResolver the font resolver used to resolve URIs * @throws IOException In case of an I/O error */ - public Type1FontLoader(URI fontFileURI, boolean embedded, boolean useKerning, - InternalResourceResolver resourceResolver) throws IOException { + public Type1FontLoader(URI fontFileURI, boolean embedded, EmbeddingMode embeddingMode, + boolean useKerning, InternalResourceResolver resourceResolver) throws IOException { super(fontFileURI, embedded, useKerning, true, resourceResolver); + this.embeddingMode = embeddingMode; } private String getPFMURI(String pfbURI) { @@ -137,7 +141,7 @@ public class Type1FontLoader extends FontLoader { if (afm == null && pfm == null) { throw new IllegalArgumentException("Need at least an AFM or a PFM!"); } - singleFont = new SingleByteFont(resourceResolver); + singleFont = new SingleByteFont(resourceResolver, embeddingMode); singleFont.setFontType(FontType.TYPE1); if (this.embedded) { singleFont.setEmbedURI(this.fontFileURI); diff --git a/src/java/org/apache/fop/fonts/type1/Type1SubsetFile.java b/src/java/org/apache/fop/fonts/type1/Type1SubsetFile.java new file mode 100644 index 000000000..6e6a7f26b --- /dev/null +++ b/src/java/org/apache/fop/fonts/type1/Type1SubsetFile.java @@ -0,0 +1,772 @@ +/* + * 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.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.UnsupportedEncodingException; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Scanner; +import java.util.Set; + +import org.apache.commons.io.IOUtils; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.apache.fop.fonts.SingleByteFont; +import org.apache.fop.fonts.type1.PostscriptParser.PSDictionary; +import org.apache.fop.fonts.type1.PostscriptParser.PSElement; +import org.apache.fop.fonts.type1.PostscriptParser.PSFixedArray; +import org.apache.fop.fonts.type1.PostscriptParser.PSSubroutine; +import org.apache.fop.fonts.type1.PostscriptParser.PSVariable; + +public class Type1SubsetFile { + + protected static final Log LOG = LogFactory.getLog(Type1SubsetFile.class); + /* The subset list of char strings */ + private HashMap<String, byte[]> subsetCharStrings; + /* The list of character names in the subset font */ + private List<String> charNames = null; + /* A list of unique subroutines references */ + private LinkedHashMap<Integer, byte[]> uniqueSubs; + private SingleByteFont sbfont = null; + /* New line character */ + private String eol = "\n"; + /* An option to determine whether the subroutines are subset */ + private boolean subsetSubroutines = true; + private byte[] fullFont; + //List of parsed Postscript elements + private List<PSElement> headerSection; + private List<PSElement> mainSection; + //Determines whether the current font uses standard encoding + private boolean standardEncoding = false; + + //Type 1 operators + private static final int OP_SEAC = 6; + private static final int OP_CALLSUBR = 10; + private static final int OP_CALLOTHERSUBR = 16; + + public byte[] createSubset(InputStream in, SingleByteFont sbfont, + String fontPrefix) throws IOException { + fullFont = IOUtils.toByteArray(in); + byte[] subsetFont = createSubset(sbfont, fontPrefix, true); + //This should never happen but ensure that subset is shorter than original font + return (subsetFont.length == 0 || subsetFont.length > fullFont.length) + ? fullFont : subsetFont; + } + + /** + * Creates a new subset from the given type 1 font input stream + * @param in The type 1 font to subset + * @param sbfont The font object containing information such as the + * characters from which to create the subset + * @param fontPrefix The prefix used in identifying the subset font + * @param allSubroutines This option will force the subset to include all + * subroutines. + * @return Returns the subset as a byte array + * @throws IOException + */ + private byte[] createSubset(SingleByteFont sbfont, + String fontPrefix, boolean subsetSubroutines) throws IOException { + this.subsetSubroutines = subsetSubroutines; + InputStream in = new ByteArrayInputStream(fullFont); + //Initialise resources used for the font creation + this.sbfont = sbfont; + PFBParser pfbParser = new PFBParser(); + PFBData pfbData = pfbParser.parsePFB(in); + + PostscriptParser psParser = new PostscriptParser(); + charNames = new ArrayList<String>(); + + //Parse the header section of the font + if (headerSection == null) { + headerSection = psParser.parse(pfbData.getHeaderSegment()); + } + + //Read the encoding section + PSElement encoding = getElement("/Encoding", headerSection); + if (encoding.getFoundUnexpected()) { + //Fully embed the font as we're unable to interpret postscript on arrays + return new byte[0]; + } + List<String> subsetEncodingEntries = readEncoding(encoding); + + //Decode the main section in preparation for parsing + byte[] decoded = BinaryCoder.decodeBytes(pfbData.getEncryptedSegment(), 55665, 4); + + //Initialise the resources used to hold the subset data + uniqueSubs = new LinkedHashMap<Integer, byte[]>(); + subsetCharStrings = new HashMap<String, byte[]>(); + + //Parse the encoded main font section for elements + if (mainSection == null) { + mainSection = psParser.parse(decoded); + } + + //Process and write the main section + PSElement charStrings = getElement("/CharStrings", mainSection); + int result = readMainSection(mainSection, decoded, subsetEncodingEntries, charStrings); + if (result == 0) { + /* This check handles the case where a font uses a postscript method to return a + * subroutine index. As there is currently no java postscript interpreter and writing + * one would be very difficult it prevents us from handling this eventuality. The way + * this issue is being handled is to restart the subset process and include all + * subroutines. */ + uniqueSubs.clear(); + subsetCharStrings.clear(); + charNames.clear(); + return createSubset(sbfont, fontPrefix, false); + } + + //Write header section + ByteArrayOutputStream boasHeader = writeHeader(pfbData, encoding, subsetEncodingEntries); + + ByteArrayOutputStream boasMain = writeMainSection(decoded, mainSection, charStrings); + byte[] mainSectionBytes = boasMain.toByteArray(); + mainSectionBytes = BinaryCoder.encodeBytes(mainSectionBytes, 55665, 4); + boasMain = new ByteArrayOutputStream(); + boasMain.write(mainSectionBytes); + + ByteArrayOutputStream baosTrailer = new ByteArrayOutputStream(); + baosTrailer.write(pfbData.getTrailerSegment(), 0, pfbData.getTrailerSegment().length); + + return stitchFont(boasHeader, boasMain, baosTrailer); + } + + byte[] stitchFont(ByteArrayOutputStream boasHeader, ByteArrayOutputStream boasMain, + ByteArrayOutputStream boasTrailer) throws IOException { + int headerLength = boasHeader.size(); + int mainLength = boasMain.size(); + + boasMain.write(128); + boasMain.write(1); + updateSectionSize(boasTrailer.size()).writeTo(boasMain); + boasTrailer.write(128); + boasTrailer.write(3); + + boasTrailer.writeTo(boasMain); + + boasHeader.write(128); + boasHeader.write(2); + //You need to encode the main section first before getting it's size!!! + updateSectionSize(mainLength).writeTo(boasHeader); + boasMain.writeTo(boasHeader); + + ByteArrayOutputStream fullFont = new ByteArrayOutputStream(); + fullFont.write(128); + fullFont.write(1); + updateSectionSize(headerLength).writeTo(fullFont); + boasHeader.writeTo(fullFont); + + return fullFont.toByteArray(); + } + + private List<String> readEncoding(PSElement encoding) { + Map<Integer, Integer> usedGlyphs = sbfont.getUsedGlyphs(); + List<Integer> glyphs = new ArrayList<Integer>(usedGlyphs.keySet()); + Collections.sort(glyphs); + List<String> subsetEncodingEntries = new ArrayList<String>(); + //Handle custom encoding + if (encoding instanceof PSFixedArray) { + PSFixedArray encodingArray = (PSFixedArray)encoding; + for (int glyph : glyphs) { + /* Search for matching entries in the original font encoding table to add + * to the subset. As there may be more than one entry for a character (as + * was the case in a font where some glyphs were duplicated), a name search is + * performed and all matching entries are added. */ + List<String> matches = searchEntries(encodingArray.getEntries(), glyph); + /* If no matches are found, create a new entry for the character so + * that it can be added even if it's not in the current encoding. */ + if (matches.size() == 0) { + matches = new ArrayList<String>(); + if (glyph == 0) { + matches.add("dup 0 /.notdef put"); + } else { + matches.add(String.format("dup %d /%s put", glyph, + sbfont.getGlyphName(glyph))); + } + } + for (String match : matches) { + subsetEncodingEntries.add(match); + addToCharNames(match); + } + } + //Handle fixed encoding + } else if (encoding instanceof PSVariable) { + if (((PSVariable) encoding).getValue().equals("StandardEncoding")) { + standardEncoding = true; + sbfont.mapUsedGlyphName(0, "/.notdef"); + for (int glyph : glyphs) { + //Retrieve the character name and alternates for the given glyph + String name = sbfont.getGlyphName(glyph); + if (glyph != 0 && name != null && !name.trim().equals("")) { + sbfont.mapUsedGlyphName(glyph, "/" + name); + } + } + } else { + LOG.warn("Only Custom or StandardEncoding is supported when creating a Type 1 subset."); + } + } + return subsetEncodingEntries; + } + + private List<String> searchEntries(HashMap<Integer, String> encodingEntries, int glyph) { + List<String> matches = new ArrayList<String>(); + for (Entry<Integer, String> entry : encodingEntries.entrySet()) { + String tag = getEntryPart(entry.getValue(), 3); + String name = sbfont.getGlyphName(sbfont.getUsedGlyphs().get(glyph)); + if (name.equals(tag)) { + matches.add(entry.getValue()); + } + } + return matches; + } + + private ByteArrayOutputStream writeHeader(PFBData pfbData, PSElement encoding, + List<String> subsetEncodingEntries) throws UnsupportedEncodingException, + IOException { + ByteArrayOutputStream boasHeader = new ByteArrayOutputStream(); + boasHeader.write(pfbData.getHeaderSegment(), 0, encoding.getStartPoint() - 1); + + if (!standardEncoding) { + //Write out the new encoding table for the subset font + String encodingArray = eol + String.format("/Encoding %d array", 256) + eol + + "0 1 255 {1 index exch /.notdef put } for" + eol; + byte[] encodingDefinition = encodingArray.getBytes("ASCII"); + boasHeader.write(encodingDefinition, 0, encodingDefinition.length); + Set<Entry<Integer, String>> entrySet = sbfont.getUsedGlyphNames().entrySet(); + for (Entry<Integer, String> entry : entrySet) { + String arrayEntry = String.format("dup %d %s put", entry.getKey(), + entry.getValue()); + writeString(arrayEntry + eol, boasHeader); + } + writeString("readonly def" + eol, boasHeader); + } else { + String theEncoding = eol + "/Encoding StandardEncoding def" + eol; + boasHeader.write(theEncoding.getBytes("ASCII")); + } + boasHeader.write(pfbData.getHeaderSegment(), encoding.getEndPoint(), + pfbData.getHeaderSegment().length - encoding.getEndPoint()); + + return boasHeader; + } + + ByteArrayOutputStream updateSectionSize(int size) throws IOException { + //Update the size in the header for the previous section + ByteArrayOutputStream boas = new ByteArrayOutputStream(); + byte[] lowOrderSize = ByteBuffer.allocate(4).order(ByteOrder.LITTLE_ENDIAN).putInt( + size).array(); + boas.write(lowOrderSize); + return boas; + } + + private int readMainSection(List<PSElement> mainSection, byte[] decoded, + List<String> subsetEncodingEntries, PSElement charStrings) { + subsetEncodingEntries.add(0, "dup 0 /.notdef put"); + /* Reads and parses the charStrings section to subset the charString + * and it's referenced subroutines found in the main section for each glyph. */ + PSDictionary charStringsDict = (PSDictionary)charStrings; + for (String tag : sbfont.getUsedGlyphNames().values()) { + if (!tag.equals("/.notdef")) { + charNames.add(tag); + } + + int[] location = charStringsDict.getBinaryEntries().get(tag); + if (location == null) { + continue; + } + byte[] charStringEntry = getBinaryEntry(location, decoded); + + int skipBytes = 4; + PSElement element = getElement("lenIV", mainSection); + if (element != null && element instanceof PSVariable) { + PSVariable lenIV = (PSVariable)element; + try { + skipBytes = Integer.parseInt(lenIV.getValue()); + } catch (NumberFormatException ex) { + LOG.warn(String.format("Invalid value `%s` for lenIV found in font %s", lenIV.getValue(), + sbfont.getEmbedFileURI().toString())); + } + } + + charStringEntry = BinaryCoder.decodeBytes(charStringEntry, 4330, skipBytes); + PSFixedArray subroutines = (PSFixedArray)getElement("/Subrs", mainSection); + if (subsetSubroutines) { + /* Recursively scan the charString array for subroutines and if found, copy the + * entry to our subset entries and update any references. */ + charStringEntry = createSubsetCharStrings(decoded, charStringEntry, subroutines, + subsetEncodingEntries, tag); + } + if (charStringEntry.length == 0) { + return 0; + } + charStringEntry = BinaryCoder.encodeBytes(charStringEntry, 4330, skipBytes); + subsetCharStrings.put(tag, charStringEntry); + } + return 1; + } + + private byte[] createSubsetCharStrings(byte[] decoded, byte[] data, PSFixedArray subroutines, + List<String> subsetEncodingEntries, String glyphName) { + List<BytesNumber> operands = new ArrayList<BytesNumber>(); + for (int i = 0; i < data.length; i++) { + int cur = data[i] & 0xFF; + if (cur <= 31) { + int dataLength = data.length; + if (cur == OP_CALLSUBR) { + //Found subroutine. Read subroutine and recursively scan and update references + if (operands.size() == 0) { + continue; + } + if (uniqueSubs.get(operands.get(operands.size() - 1).getNumber()) == null) { + uniqueSubs.put(operands.get(operands.size() - 1).getNumber(), new byte[0]); + data = addSubroutine(subroutines, operands, decoded, subsetEncodingEntries, + glyphName, data, i, 1, -1, operands.get( + operands.size() - 1).getNumber()); + } else { + data = addSubroutine(subroutines, operands, decoded, subsetEncodingEntries, + glyphName, data, i, 1, getSubrIndex(operands.get( + operands.size() - 1).getNumber()), operands.get( + operands.size() - 1).getNumber()); + } + } else if (cur == 12) { + int next = data[++i] & 0xFF; + if (next == OP_SEAC) { + /* This charString references two other glyphs which must also be included + * for this character to be displayed properly. */ + int first = operands.get(operands.size() - 2).getNumber(); + int second = operands.get(operands.size() - 1).getNumber(); + String charFirst = AdobeStandardEncoding.getCharFromCodePoint(first); + String charSecond = AdobeStandardEncoding.getCharFromCodePoint(second); + subsetEncodingEntries.add(String.format("dup %d /%s put", + first, charFirst)); + subsetEncodingEntries.add(String.format("dup %d /%s put", + second, charSecond)); + sbfont.mapUsedGlyphName(first, "/" + charFirst); + sbfont.mapUsedGlyphName(second, "/" + charSecond); + } else if (next == OP_CALLOTHERSUBR) { + /* Search for a specific operator chain which results in a referenced + * subroutine being returned from a postscript method. If it's found then + * return null so the subset process can be restarted and all subroutines + * can be included. */ + int[] pattern = {12, 17, 10}; + int count = 0; + boolean matchesPattern = true; + if (data.length > i + 4) { + for (int pos = i + 1; pos < i + 4; pos++) { + if (data[pos] != pattern[count++]) { + matchesPattern = false; + } + } + } + if (matchesPattern) { + return new byte[0]; + } + data = addSubroutine(subroutines, operands, decoded, subsetEncodingEntries, + glyphName, data, i, 2, -1, operands.get(0).getNumber()); + } + } + if (data.length == 0) { + return new byte[0]; + } + i -= dataLength - data.length; + operands.clear(); + } else if (cur <= 246) { + operands.add(new BytesNumber(cur - 139, 1)); + } else if (cur <= 250) { + operands.add(new BytesNumber((cur - 247) * 256 + (data[i + 1] & 0xFF) + 108, 2)); + i++; + } else if (cur <= 254) { + operands.add(new BytesNumber(-(cur - 251) * 256 - (data[i + 1] & 0xFF) - 108, 2)); + i++; + } else if (cur == 255) { + int b1 = data[i + 1] & 0xFF; + int b2 = data[i + 2] & 0xFF; + int b3 = data[i + 3] & 0xFF; + int b4 = data[i + 4] & 0xFF; + int value = b1 << 24 | b2 << 16 | b3 << 8 | b4; + operands.add(new BytesNumber(value, 5)); + i += 4; + } + } + return data; + } + + private int getSubrIndex(int subID) { + int count = 0; + for (Integer key : uniqueSubs.keySet()) { + if (key == subID) { + return count; + } + count++; + } + return -1; + } + + private byte[] addSubroutine(PSFixedArray subroutines, List<BytesNumber> operands, byte[] decoded, + List<String> subsetEncodingEntries, String glyphName, byte[] data, int i, int opLength, + int existingSubrRef, int subrID) { + if (existingSubrRef == -1) { + int[] subrData = subroutines.getBinaryEntryByIndex(subrID); + byte[] subroutine = getBinaryEntry(subrData, decoded); + subroutine = BinaryCoder.decodeBytes(subroutine, 4330, 4); + subroutine = createSubsetCharStrings(decoded, subroutine, subroutines, + subsetEncodingEntries, glyphName); + if (subroutine.length == 0) { + return new byte[0]; + } + //Encode data + subroutine = BinaryCoder.encodeBytes(subroutine, 4330, 4); + uniqueSubs.put(subrID, subroutine); + } + int subRef = (existingSubrRef != -1) ? existingSubrRef : uniqueSubs.size() - 1; + data = constructNewRefData(i, data, operands, 1, subRef, opLength); + return data; + } + + private ByteArrayOutputStream writeMainSection(byte[] decoded, List<PSElement> mainSection, + PSElement charStrings) throws IOException { + ByteArrayOutputStream main = new ByteArrayOutputStream(); + PSElement subrs = getElement("/Subrs", mainSection); + + //Find the ID of the three most commonly subroutines defined in Type 1 fonts + String rd = findVariable(decoded, mainSection, new String[] + {"string currentfile exch readstring pop"}, "RD"); + String nd = findVariable(decoded, mainSection, new String[] + {"def", "noaccess def"}, "noaccess def"); + String np = findVariable(decoded, mainSection, new String[] + {"put", "noaccess put"}, "noaccess put"); + + main.write(decoded, 0, subrs.getStartPoint()); + //Write either the subset or full list of subroutines + if (subsetSubroutines) { + writeString(eol + String.format("/Subrs %d array", uniqueSubs.size()), main); + int count = 0; + for (Entry<Integer, byte[]> entry : uniqueSubs.entrySet()) { + byte[] newSubrBytes = (eol + String.format("dup %d %d %s ", count++, + entry.getValue().length, rd)).getBytes("ASCII"); + newSubrBytes = concatArray(newSubrBytes, entry.getValue()); + newSubrBytes = concatArray(newSubrBytes, String.format(" %s", np).getBytes("ASCII")); + main.write(newSubrBytes); + } + writeString(eol + nd, main); + } else { + int fullSubrsLength = subrs.getEndPoint() - subrs.getStartPoint(); + main.write(decoded, subrs.getStartPoint(), fullSubrsLength); + } + main.write(decoded, subrs.getEndPoint(), charStrings.getStartPoint() - subrs.getEndPoint()); + //Write the subset charString array + writeString(eol + String.format("/CharStrings %d dict dup begin", + subsetCharStrings.size()), main); + for (Entry<String, byte[]> entry : subsetCharStrings.entrySet()) { + writeString(eol + String.format("%s %d %s ", entry.getKey(), + entry.getValue().length, rd), + main); + main.write(entry.getValue()); + writeString(" " + nd, main); + } + writeString(eol + "end", main); + main.write(decoded, charStrings.getEndPoint(), decoded.length - charStrings.getEndPoint()); + + return main; + } + + private String findVariable(byte[] decoded, List<PSElement> elements, String[] matches, + String fallback) throws UnsupportedEncodingException { + for (PSElement element : elements) { + if (element instanceof PSSubroutine) { + byte[] var = new byte[element.getEndPoint() - element.getStartPoint()]; + System.arraycopy(decoded, element.getStartPoint(), var, 0, element.getEndPoint() + - element.getStartPoint()); + String found = readVariableContents(new String(var, "ASCII")).trim(); + for (String match : matches) { + if (match.equals(found)) { + return element.getOperator().substring(1, element.getOperator().length()); + } + } + } + } + return fallback; + } + + String readVariableContents(String variable) { + int level = 0; + String result = ""; + int start = 0; + int end = 0; + boolean reading = false; + List<Integer> results = new ArrayList<Integer>(); + for (int i = 0; i < variable.length(); i++) { + char curChar = variable.charAt(i); + boolean sectionEnd = false; + if (curChar == '{') { + level++; + sectionEnd = true; + } else if (curChar == '}') { + level--; + sectionEnd = true; + } else if (level == 1) { + if (!reading) { + reading = true; + start = i; + } + end = i; + } + if (sectionEnd && reading) { + results.add(start); + results.add(end); + reading = false; + } + } + for (int i = 0; i < results.size(); i += 2) { + result = result.concat(variable.substring(results.get(i), results.get(i + 1) + 1)); + } + return result; + } + + private void addToCharNames(String encodingEntry) { + int spaceCount = 0; + int lastSpaceIndex = 0; + int charIndex = 0; + String charName = ""; + //Extract the character name from an encoding entry + for (int i = 0; i < encodingEntry.length(); i++) { + boolean isSpace = encodingEntry.charAt(i) == ' '; + if (isSpace) { + spaceCount++; + switch (spaceCount - 1) { + case 1: charIndex = Integer.parseInt(encodingEntry.substring(lastSpaceIndex + 1, + i)); break; + case 2: charName = encodingEntry.substring(lastSpaceIndex + 1, i); break; + default: break; + } + } + if (isSpace) { + lastSpaceIndex = i; + } + } + sbfont.mapUsedGlyphName(charIndex, charName); + } + + private void writeString(String entry, ByteArrayOutputStream boas) + throws UnsupportedEncodingException, IOException { + byte[] byteEntry = entry.getBytes("ASCII"); + boas.write(byteEntry); + } + + /** + * A class used to store the last number operand and also it's size in bytes + */ + public static final class BytesNumber { + private int number; + private int numBytes; + private String name = null; + + public BytesNumber(int number, int numBytes) { + this.number = number; + this.numBytes = numBytes; + } + + public int getNumber() { + return this.number; + } + + public int getNumBytes() { + return this.numBytes; + } + + public void setName(String name) { + this.name = name; + } + + public String getName() { + return this.name; + } + } + + private byte[] constructNewRefData(int curDataPos, byte[] currentData, + List<BytesNumber> operands, int opNum, int curSubsetIndexSize, int operatorLength) { + //Create the new array with the modified reference + byte[] newData; + int operandsLenth = getOperandsLength(operands); + int startRef = curDataPos - operandsLenth + getOpPosition(opNum, operands) + + (1 - operatorLength); + byte[] preBytes = new byte[startRef]; + System.arraycopy(currentData, 0, preBytes, 0, startRef); + byte[] newRefBytes = createNewRef(curSubsetIndexSize, -1); + newData = concatArray(preBytes, newRefBytes); + byte[] postBytes = new byte[currentData.length - (startRef + + operands.get(opNum - 1).getNumBytes())]; + System.arraycopy(currentData, startRef + operands.get(opNum - 1).getNumBytes(), postBytes, 0, + currentData.length - (startRef + operands.get(opNum - 1).getNumBytes())); + return concatArray(newData, postBytes); + } + + int getOpPosition(int opNum, List<BytesNumber> operands) { + int byteCount = 0; + for (int i = 0; i < opNum - 1; i++) { + byteCount += operands.get(i).getNumBytes(); + } + return byteCount; + } + + int getOperandsLength(List<BytesNumber> operands) { + int length = 0; + for (BytesNumber number : operands) { + length += number.getNumBytes(); + } + return length; + } + + private byte[] createNewRef(int newRef, int forceLength) { + byte[] newRefBytes; + if ((forceLength == -1 && newRef <= 107) || forceLength == 1) { + newRefBytes = new byte[1]; + newRefBytes[0] = (byte)(newRef + 139); + } else if ((forceLength == -1 && newRef <= 1131) || forceLength == 2) { + newRefBytes = new byte[2]; + if (newRef <= 363) { + newRefBytes[0] = (byte)247; + } else if (newRef <= 619) { + newRefBytes[0] = (byte)248; + } else if (newRef <= 875) { + newRefBytes[0] = (byte)249; + } else { + newRefBytes[0] = (byte)250; + } + newRefBytes[1] = (byte)(newRef - 108); + } else { + newRefBytes = new byte[5]; + newRefBytes[0] = (byte)255; + newRefBytes[1] = (byte)(newRef >> 24); + newRefBytes[2] = (byte)(newRef >> 16); + newRefBytes[3] = (byte)(newRef >> 8); + newRefBytes[4] = (byte)newRef; + } + return newRefBytes; + } + + /** + * Concatenate two byte arrays together + * @param a The first array + * @param b The second array + * @return The concatenated array + */ + byte[] concatArray(byte[] a, byte[] b) { + int aLen = a.length; + int bLen = b.length; + byte[] c = new byte[aLen + bLen]; + System.arraycopy(a, 0, c, 0, aLen); + System.arraycopy(b, 0, c, aLen, bLen); + return c; + } + + /** + * Returns a section of a byte array determined by it's start and + * end position. + * @param position An array containing both the start and end position + * of the section to copy. + * @param decoded The array from which to copy a section of data + * @return Returns the copy of the data section + */ + byte[] getBinaryEntry(int[] position, byte[] decoded) { + int start = position[0]; + int finish = position[1]; + byte[] line = new byte[finish - start]; + System.arraycopy(decoded, start, line, 0, finish - start); + return line; + } + + private String getEntryPart(String entry, int part) { + Scanner s = new Scanner(entry).useDelimiter(" "); + for (int i = 1; i < part; i++) { + s.next(); + } + return s.next(); + } + + private PSElement getElement(String elementID, List<PSElement> elements) { + for (PSElement element : elements) { + if (element.getOperator().equals(elementID)) { + return element; + } + } + return null; + } + + /** + * Gets the list of subset character names + * @return Returns the subset character names + */ + public List<String> getCharNames() { + return charNames; + } + + /** + * A class to encode and decode sections of a type 1 font file. See Adobe + * Type 1 Font Format Section 7.2 for more details. + */ + public static class BinaryCoder { + public static byte[] decodeBytes(byte[] in, int inR, int n) { + byte[] out = new byte[in.length - n]; + int r = inR; + int c1 = 52845; + int c2 = 22719; + for (int i = 0; i < in.length; i++) { + int cypher = in[i] & 0xFF; + int plain = cypher ^ (r >> 8); + if (i >= n) { + out[i - n] = (byte)plain; + } + r = (cypher + r) * c1 + c2 & 0xFFFF; + } + return out; + } + + public static byte[] encodeBytes(byte[] in, int inR, int n) { + byte[] buffer = new byte[in.length + n]; + for (int i = 0; i < n; i++) { + buffer[i] = 0; + } + int r = inR; + int c1 = 52845; + int c2 = 22719; + System.arraycopy(in, 0, buffer, n, buffer.length - n); + byte[] out = new byte[buffer.length]; + for (int i = 0; i < buffer.length; i++) { + int plain = buffer[i] & 0xff; + int cipher = plain ^ r >> 8; + out[i] = (byte) cipher; + r = (cipher + r) * c1 + c2 & 0xffff; + } + return out; + } + } +} diff --git a/src/java/org/apache/fop/pdf/PDFFactory.java b/src/java/org/apache/fop/pdf/PDFFactory.java index 070630274..eb4f61077 100644 --- a/src/java/org/apache/fop/pdf/PDFFactory.java +++ b/src/java/org/apache/fop/pdf/PDFFactory.java @@ -23,6 +23,7 @@ package org.apache.fop.pdf; import java.awt.Color; import java.awt.geom.Point2D; import java.awt.geom.Rectangle2D; +import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; import java.text.DecimalFormat; @@ -32,6 +33,8 @@ import java.util.BitSet; import java.util.Iterator; import java.util.List; import java.util.Map; +import java.util.SortedSet; +import java.util.TreeSet; import org.apache.commons.io.IOUtils; import org.apache.commons.io.output.ByteArrayOutputStream; @@ -61,6 +64,7 @@ import org.apache.fop.fonts.truetype.OTFSubSetFile; import org.apache.fop.fonts.truetype.TTFSubSetFile; import org.apache.fop.fonts.type1.PFBData; import org.apache.fop.fonts.type1.PFBParser; +import org.apache.fop.fonts.type1.Type1SubsetFile; /** * This class provides method to create and register PDF objects. @@ -1372,11 +1376,50 @@ public class PDFFactory { } else { singleByteFont = (SingleByteFont)metrics; } - int firstChar = singleByteFont.getFirstChar(); - int lastChar = singleByteFont.getLastChar(); - nonBase14.setWidthMetrics(firstChar, - lastChar, - new PDFArray(null, metrics.getWidths())); + + int firstChar = 0; + int lastChar = 0; + boolean defaultChars = false; + if (singleByteFont.getEmbeddingMode() == EmbeddingMode.SUBSET) { + Map<Integer, Integer> usedGlyphs = singleByteFont.getUsedGlyphs(); + if (fonttype == FontType.TYPE1 && usedGlyphs.size() > 0) { + SortedSet<Integer> keys = new TreeSet<Integer>(usedGlyphs.keySet()); + keys.remove(0); + if (keys.size() > 0) { + firstChar = keys.first(); + lastChar = keys.last(); + int[] newWidths = new int[(lastChar - firstChar) + 1]; + for (int i = firstChar; i < lastChar + 1; i++) { + if (usedGlyphs.get(i) != null) { + if (i - singleByteFont.getFirstChar() < metrics.getWidths().length) { + newWidths[i - firstChar] = metrics.getWidths()[i + - singleByteFont.getFirstChar()]; + } else { + defaultChars = true; + break; + } + } else { + newWidths[i - firstChar] = 0; + } + } + nonBase14.setWidthMetrics(firstChar, + lastChar, + new PDFArray(null, newWidths)); + } + } else { + defaultChars = true; + } + } else { + defaultChars = true; + } + + if (defaultChars) { + firstChar = singleByteFont.getFirstChar(); + lastChar = singleByteFont.getLastChar(); + nonBase14.setWidthMetrics(firstChar, + lastChar, + new PDFArray(null, metrics.getWidths())); + } //Handle encoding SingleByteEncoding mapping = singleByteFont.getEncoding(); @@ -1493,7 +1536,7 @@ public class PDFFactory { desc.getStemV(), null); } else { // Create normal FontDescriptor - descriptor = new PDFFontDescriptor(desc.getEmbedFontName(), + descriptor = new PDFFontDescriptor(fontPrefix + desc.getEmbedFontName(), desc.getAscender(), desc.getDescender(), desc.getCapHeight(), @@ -1507,7 +1550,6 @@ public class PDFFactory { // Check if the font is embeddable if (desc.isEmbeddable()) { AbstractPDFStream stream = makeFontFile(desc, fontPrefix); - if (stream != null) { descriptor.setFontFile(desc.getFontType(), stream); getDocument().registerObject(stream); @@ -1586,10 +1628,19 @@ public class PDFFactory { } embeddedFont = getFontStream(font, fontBytes, isCFF); } else if (desc.getFontType() == FontType.TYPE1) { - PFBParser parser = new PFBParser(); - PFBData pfb = parser.parsePFB(in); - embeddedFont = new PDFT1Stream(); - ((PDFT1Stream) embeddedFont).setData(pfb); + if (font.getEmbeddingMode() != EmbeddingMode.SUBSET) { + embeddedFont = fullyEmbedType1Font(in); + } else { + assert font instanceof SingleByteFont; + SingleByteFont sbfont = (SingleByteFont)font; + Type1SubsetFile pfbFile = new Type1SubsetFile(); + byte[] subsetData = pfbFile.createSubset(in, sbfont, fontPrefix); + InputStream subsetStream = new ByteArrayInputStream(subsetData); + PFBParser parser = new PFBParser(); + PFBData pfb = parser.parsePFB(subsetStream); + embeddedFont = new PDFT1Stream(); + ((PDFT1Stream) embeddedFont).setData(pfb); + } } else { byte[] file = IOUtils.toByteArray(in); embeddedFont = new PDFTTFStream(file.length); @@ -1614,6 +1665,14 @@ public class PDFFactory { } } + private AbstractPDFStream fullyEmbedType1Font(InputStream in) throws IOException { + PFBParser parser = new PFBParser(); + PFBData pfb = parser.parsePFB(in); + AbstractPDFStream embeddedFont = new PDFT1Stream(); + ((PDFT1Stream) embeddedFont).setData(pfb); + return embeddedFont; + } + private byte[] getFontSubsetBytes(FontFileReader reader, MultiByteFont mbfont, String header, String fontPrefix, FontDescriptor desc, boolean isCFF) throws IOException { if (isCFF) { diff --git a/src/java/org/apache/fop/render/ps/PSFontUtils.java b/src/java/org/apache/fop/render/ps/PSFontUtils.java index 06191f84d..5bb723eea 100644 --- a/src/java/org/apache/fop/render/ps/PSFontUtils.java +++ b/src/java/org/apache/fop/render/ps/PSFontUtils.java @@ -19,6 +19,7 @@ package org.apache.fop.render.ps; +import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; import java.util.HashMap; @@ -27,6 +28,7 @@ import java.util.Locale; import java.util.Map; import java.util.Set; +import org.apache.commons.io.IOUtils; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.apache.fontbox.cff.CFFStandardString; @@ -61,6 +63,7 @@ import org.apache.fop.fonts.truetype.OpenFont.PostScriptVersion; import org.apache.fop.fonts.truetype.TTFFile; import org.apache.fop.fonts.truetype.TTFOutputStream; import org.apache.fop.fonts.truetype.TTFSubSetFile; +import org.apache.fop.fonts.type1.Type1SubsetFile; import org.apache.fop.render.ps.fonts.PSTTFOutputStream; import org.apache.fop.util.HexEncoder; @@ -272,13 +275,13 @@ public class PSFontUtils extends org.apache.xmlgraphics.ps.PSFontUtils { } gen.writeDSCComment(DSCConstants.BEGIN_RESOURCE, fontRes); if (fontType == FontType.TYPE1) { - embedType1Font(gen, in); + embedType1Font(gen, (SingleByteFont) tf, in); fontResource = PSFontResource.createFontResource(fontRes); } else if (fontType == FontType.TRUETYPE) { embedTrueTypeFont(gen, (SingleByteFont) tf, in); fontResource = PSFontResource.createFontResource(fontRes); } else { - composeType0Font(gen, (MultiByteFont) tf, in); + composeType0Font(gen, (MultiByteFont) tf, in); } gen.writeDSCComment(DSCConstants.END_RESOURCE); gen.getResourceTracker().registerSuppliedResource(fontRes); @@ -311,6 +314,71 @@ public class PSFontUtils extends org.apache.xmlgraphics.ps.PSFontUtils { } } + private static void embedType1Font(PSGenerator gen, SingleByteFont font, + InputStream fontStream) throws IOException { + if (font.getEmbeddingMode() == EmbeddingMode.AUTO) { + font.setEmbeddingMode(EmbeddingMode.FULL); + } + byte[] fullFont = IOUtils.toByteArray(fontStream); + fontStream = new ByteArrayInputStream(fullFont); + boolean embed = true; + if (font.getEmbeddingMode() == EmbeddingMode.SUBSET) { + Type1SubsetFile subset = new Type1SubsetFile(); + byte[] byteSubset = subset.createSubset(fontStream, font, ""); + fontStream = new ByteArrayInputStream(byteSubset); + } + embedType1Font(gen, fontStream); + if (font.getEmbeddingMode() == EmbeddingMode.SUBSET) { + writeEncoding(gen, font); + } + } + + private static void writeEncoding(PSGenerator gen, SingleByteFont font) throws IOException { + String psName = font.getEmbedFontName(); + gen.writeln("/" + psName + ".0.enc [ "); + int lengthCount = 0; + int charCount = 1; + int encodingCount = 0; + StringBuilder line = new StringBuilder(); + int lastGid = 0; + Set<Integer> keySet = font.getUsedGlyphNames().keySet(); + for (int gid : keySet) { + for (int i = lastGid; i < gid - 1; i++) { + line.append("/.notdef "); + lengthCount++; + if (lengthCount == 8) { + gen.writeln(line.toString()); + line = new StringBuilder(); + lengthCount = 0; + } + } + lastGid = gid; + line.append(font.getUsedGlyphNames().get(gid) + " "); + lengthCount++; + charCount++; + if (lengthCount == 8) { + gen.writeln(line.toString()); + line = new StringBuilder(); + lengthCount = 0; + } + if (charCount > 256) { + encodingCount++; + charCount = 1; + gen.writeln(line.toString()); + line = new StringBuilder(); + lengthCount = 0; + gen.writeln("] def"); + gen.writeln(String.format("/%s.%d %s.%d.enc /%s RE", psName, + encodingCount - 1, psName, encodingCount - 1, psName)); + gen.writeln("/" + psName + "." + encodingCount + ".enc [ "); + } + } + gen.writeln(line.toString()); + gen.writeln("] def"); + gen.writeln(String.format("/%s.%d %s.%d.enc /%s RE", psName, encodingCount, + psName, encodingCount, psName)); + } + private static void embedTrueTypeFont(PSGenerator gen, SingleByteFont font, InputStream fontStream) throws IOException { /* See Adobe Technical Note #5012, "The Type 42 Font Format Specification" */ |