blob: cf4596b0c0020eec1d3d6c0eabc6aeb93b8f19ba [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/* $Id$ */
package org.apache.fop.render.pdf.pdfbox;
import java.awt.Rectangle;
import java.awt.geom.AffineTransform;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.text.DecimalFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import java.util.regex.Pattern;
import org.apache.commons.io.IOUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.fontbox.cff.CFFFont;
import org.apache.fontbox.cff.CFFFontROS;
import org.apache.fontbox.cff.charset.CFFCharset;
import org.apache.fontbox.cff.encoding.CFFEncoding;
import org.apache.fontbox.cmap.CMap;
import org.apache.fontbox.ttf.CMAPEncodingEntry;
import org.apache.fontbox.ttf.GlyphData;
import org.apache.fontbox.ttf.MaximumProfileTable;
import org.apache.fontbox.ttf.TrueTypeFont;
import org.apache.pdfbox.cos.COSArray;
import org.apache.pdfbox.cos.COSBase;
import org.apache.pdfbox.cos.COSBoolean;
import org.apache.pdfbox.cos.COSDictionary;
import org.apache.pdfbox.cos.COSFloat;
import org.apache.pdfbox.cos.COSInteger;
import org.apache.pdfbox.cos.COSName;
import org.apache.pdfbox.cos.COSNull;
import org.apache.pdfbox.cos.COSNumber;
import org.apache.pdfbox.cos.COSObject;
import org.apache.pdfbox.cos.COSStream;
import org.apache.pdfbox.cos.COSString;
import org.apache.pdfbox.encoding.DictionaryEncoding;
import org.apache.pdfbox.encoding.Encoding;
import org.apache.pdfbox.encoding.EncodingManager;
import org.apache.pdfbox.pdfparser.PDFStreamParser;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDDocumentCatalog;
import org.apache.pdfbox.pdmodel.PDPage;
import org.apache.pdfbox.pdmodel.PDPageNode;
import org.apache.pdfbox.pdmodel.PDResources;
import org.apache.pdfbox.pdmodel.common.COSObjectable;
import org.apache.pdfbox.pdmodel.common.COSStreamArray;
import org.apache.pdfbox.pdmodel.common.PDRectangle;
import org.apache.pdfbox.pdmodel.common.PDStream;
import org.apache.pdfbox.pdmodel.font.PDCIDFont;
import org.apache.pdfbox.pdmodel.font.PDCIDFontType0Font;
import org.apache.pdfbox.pdmodel.font.PDCIDFontType2Font;
import org.apache.pdfbox.pdmodel.font.PDFont;
import org.apache.pdfbox.pdmodel.font.PDFontDescriptorDictionary;
import org.apache.pdfbox.pdmodel.font.PDFontFactory;
import org.apache.pdfbox.pdmodel.font.PDSimpleFont;
import org.apache.pdfbox.pdmodel.font.PDTrueTypeFont;
import org.apache.pdfbox.pdmodel.font.PDType0Font;
import org.apache.pdfbox.pdmodel.font.PDType1Font;
import org.apache.pdfbox.pdmodel.interactive.annotation.PDAnnotation;
import org.apache.pdfbox.pdmodel.interactive.form.PDAcroForm;
import org.apache.pdfbox.util.operator.PDFOperator;
import org.apache.fop.events.EventBroadcaster;
import org.apache.fop.fonts.CIDFontType;
import org.apache.fop.fonts.CustomFont;
import org.apache.fop.fonts.EmbeddingMode;
import org.apache.fop.fonts.FontInfo;
import org.apache.fop.fonts.FontType;
import org.apache.fop.fonts.MultiByteFont;
import org.apache.fop.fonts.SingleByteEncoding;
import org.apache.fop.fonts.SingleByteFont;
import org.apache.fop.fonts.Typeface;
import org.apache.fop.fonts.truetype.FontFileReader;
import org.apache.fop.fonts.truetype.OTFSubSetFile;
import org.apache.fop.pdf.PDFArray;
import org.apache.fop.pdf.PDFDictionary;
import org.apache.fop.pdf.PDFDocument;
import org.apache.fop.pdf.PDFName;
import org.apache.fop.pdf.PDFNumber;
import org.apache.fop.pdf.PDFObject;
import org.apache.fop.pdf.PDFPage;
import org.apache.fop.pdf.PDFRoot;
import org.apache.fop.pdf.PDFStream;
import org.apache.fop.pdf.PDFText;
import org.apache.fop.pdf.RefPDFFont;
import org.apache.fop.util.CharUtilities;
/**
* This class provides an adapter for transferring content from a PDFBox PDDocument to
* FOP's PDFDocument. It is used to parse PDF using PDFBox and write content using
* FOP's PDF library.
*/
public class PDFBoxAdapter {
/** logging instance */
protected static final Log log = LogFactory.getLog(PDFBoxAdapter.class);
private static final Set FILTER_FILTER = new java.util.HashSet(
Arrays.asList(new String[] {"Filter", "DecodeParms"}));
private static final Pattern SUBSET_PATTERN = Pattern.compile("[A-Z][A-Z][A-Z][A-Z][A-Z][A-Z]\\+.+");
private final PDFPage targetPage;
private final PDFDocument pdfDoc;
private final Map clonedVersion;
private final Map<COSDictionary, PDSimpleFont> fontMap = new HashMap<COSDictionary, PDSimpleFont>();
private Map<COSName, String> newXObj = new HashMap<COSName, String>();
private Collection<String> parentFonts;
/**
* Creates a new PDFBoxAdapter.
* @param targetPage The target FOP PDF page object
* @param objectCache the object cache for reusing objects shared by multiple pages.
*/
public PDFBoxAdapter(PDFPage targetPage, Map objectCache) {
this.targetPage = targetPage;
this.pdfDoc = this.targetPage.getDocument();
this.clonedVersion = objectCache;
}
private Object cloneForNewDocument(Object base) throws IOException {
return cloneForNewDocument(base, base);
}
private Object cloneForNewDocument(Object base, Object keyBase) throws IOException {
return cloneForNewDocument(base, keyBase, Collections.EMPTY_LIST);
}
private Object cloneForNewDocument(Object base, Object keyBase, Collection exclude) throws IOException {
if (base == null) {
return null;
}
Object cached = getCachedClone(keyBase);
if (cached != null) {
// we are done, it has already been converted.
return cached;
} else if (base instanceof List) {
PDFArray array = new PDFArray();
cacheClonedObject(keyBase, array);
List list = (List)base;
for (Object o : list) {
array.add(cloneForNewDocument(o, o, exclude));
}
return array;
} else if (base instanceof COSObjectable && !(base instanceof COSBase)) {
Object o = ((COSObjectable)base).getCOSObject();
Object retval = cloneForNewDocument(o, o, exclude);
return cacheClonedObject(keyBase, retval);
} else if (base instanceof COSObject) {
return readCOSObject((COSObject) base, exclude);
} else if (base instanceof COSArray) {
PDFArray newArray = new PDFArray();
cacheClonedObject(keyBase, newArray);
COSArray array = (COSArray)base;
for (int i = 0; i < array.size(); i++) {
newArray.add(cloneForNewDocument(array.get(i), array.get(i), exclude));
}
return newArray;
} else if (base instanceof COSStreamArray) {
COSStreamArray array = (COSStreamArray)base;
PDFArray newArray = new PDFArray();
cacheClonedObject(keyBase, newArray);
for (int i = 0, c = array.getStreamCount(); i < c; i++) {
newArray.add(cloneForNewDocument(array.get(i)));
}
return newArray;
} else if (base instanceof COSStream) {
return readCOSStream((COSStream) base, keyBase);
} else if (base instanceof COSDictionary) {
return readCOSDictionary((COSDictionary) base, keyBase, exclude);
} else if (base instanceof COSName) {
PDFName newName = new PDFName(((COSName)base).getName());
return cacheClonedObject(keyBase, newName);
} else if (base instanceof COSInteger) {
PDFNumber number = new PDFNumber();
number.setNumber(((COSInteger)base).longValue());
return cacheClonedObject(keyBase, number);
} else if (base instanceof COSFloat) {
PDFNumber number = new PDFNumber();
number.setNumber(((COSFloat)base).floatValue());
return cacheClonedObject(keyBase, number);
} else if (base instanceof COSBoolean) {
//TODO Do we need a PDFBoolean here?
Boolean retval = ((COSBoolean)base).getValueAsObject();
if (keyBase instanceof COSObject) {
return cacheClonedObject(keyBase, new PDFBoolean(retval));
} else {
return cacheClonedObject(keyBase, retval);
}
} else if (base instanceof COSString) {
return readCOSString((COSString) base, keyBase);
} else if (base instanceof COSNull) {
return cacheClonedObject(keyBase, null);
} else {
throw new UnsupportedOperationException("NYI: " + base.getClass().getName());
}
}
private PDFDictionary readCOSDictionary(COSDictionary dic, Object keyBase, Collection exclude) throws IOException {
PDFDictionary newDict = new PDFDictionary();
cacheClonedObject(keyBase, newDict);
for (Map.Entry<COSName, COSBase> e : dic.entrySet()) {
if (!exclude.contains(e.getKey())) {
newDict.put(e.getKey().getName(), cloneForNewDocument(e.getValue(), e.getValue(), exclude));
}
}
return newDict;
}
private Object readCOSObject(COSObject object, Collection exclude) throws IOException {
if (log.isTraceEnabled()) {
log.trace("Cloning indirect object: "
+ object.getObjectNumber().longValue()
+ " " + object.getGenerationNumber().longValue());
}
Object obj = cloneForNewDocument(object.getObject(), object, exclude);
if (obj instanceof PDFObject) {
PDFObject pdfobj = (PDFObject)obj;
//pdfDoc.registerObject(pdfobj);
if (!pdfobj.hasObjectNumber()) {
throw new IllegalStateException("PDF object was not registered!");
}
if (log.isTraceEnabled()) {
log.trace("Object registered: "
+ pdfobj.getObjectNumber()
+ " " + pdfobj.getGeneration()
+ " for COSObject: "
+ object.getObjectNumber().longValue()
+ " " + object.getGenerationNumber().longValue());
}
}
return obj;
}
private Object readCOSString(COSString string, Object keyBase) {
//retval = ((COSString)base).getString(); //this is unsafe for binary content
byte[] bytes = string.getBytes();
//Be on the safe side and use the byte array to avoid encoding problems
//as PDFBox doesn't indicate whether the string is just
//a string (PDF 1.4, 3.2.3) or a text string (PDF 1.4, 3.8.1).
if (keyBase instanceof COSObject) {
return cacheClonedObject(keyBase, new PDFString(bytes));
} else {
if (PDFString.isUSASCII(bytes)) {
return cacheClonedObject(keyBase, string.getString());
} else {
return cacheClonedObject(keyBase, bytes);
}
}
}
private Object readCOSStream(COSStream originalStream, Object keyBase) throws IOException {
InputStream in;
Set filter;
if (pdfDoc.isEncryptionActive()) {
in = originalStream.getUnfilteredStream();
filter = FILTER_FILTER;
} else {
//transfer encoded data (don't reencode)
in = originalStream.getFilteredStream();
filter = Collections.EMPTY_SET;
}
PDFStream stream = new PDFStream();
OutputStream out = stream.getBufferOutputStream();
IOUtils.copyLarge(in, out);
transferDict(originalStream, stream, filter);
return cacheClonedObject(keyBase, stream);
}
private Object getCachedClone(Object base) {
return clonedVersion.get(getBaseKey(base));
}
private Object cacheClonedObject(Object base, Object cloned) {
Object key = getBaseKey(base);
if (key == null) {
return cloned;
}
PDFObject pdfobj = (PDFObject) cloned;
if (!pdfobj.hasObjectNumber()) {
pdfDoc.registerObject(pdfobj);
if (log.isTraceEnabled()) {
log.trace(key + ": " + pdfobj.getClass().getName() + " registered as "
+ pdfobj.getObjectNumber() + " " + pdfobj.getGeneration());
}
}
clonedVersion.put(key, cloned);
return cloned;
}
private Object getBaseKey(Object base) {
if (base instanceof COSObject) {
COSObject obj = (COSObject)base;
return obj.getObjectNumber().intValue() + " " + obj.getGenerationNumber().intValue();
} else {
return null;
}
}
private void transferDict(COSDictionary orgDict, PDFStream targetDict,
Set filter) throws IOException {
transferDict(orgDict, targetDict, filter, false);
}
private void transferDict(COSDictionary orgDict, PDFStream targetDict,
Set filter, boolean inclusive) throws IOException {
Set<COSName> keys = orgDict.keySet();
for (COSName key : keys) {
if (inclusive && !filter.contains(key.getName())) {
continue;
} else if (!inclusive && filter.contains(key.getName())) {
continue;
}
targetDict.put(key.getName(),
cloneForNewDocument(orgDict.getItem(key)));
}
}
private String getUniqueFontName(COSDictionary fontData) throws IOException {
PDSimpleFont font = getFont(fontData);
String extra = "";
String name = getName(font.getBaseFont()) + "_" + ((COSName)fontData.getItem(COSName.SUBTYPE)).getName();
if (font instanceof PDType0Font
&& ((PDType0Font) font).getDescendantFont() instanceof PDCIDFontType0Font
&& ((PDCIDFontType0Font) ((PDType0Font) font).getDescendantFont()).getType1CFont() != null) {
CFFFont cffFont =
((PDCIDFontType0Font) ((PDType0Font) font).getDescendantFont()).getType1CFont().getCFFFont();
if (cffFont instanceof CFFFontROS
&& ((CFFFontROS)cffFont).getFdSelect().getClass().getName()
.equals("org.apache.fontbox.cff.CFFParser$Format0FDSelect")) {
extra += "format0";
}
return name + extra;
}
if (font instanceof PDType0Font
&& font.getToUnicode() != null
&& ((PDType0Font) font).getDescendantFont() instanceof PDCIDFontType2Font) {
if (!isSubsetFont(font.getBaseFont())) {
extra = "f3";
}
return name + extra;
}
if (font instanceof PDTrueTypeFont && isSubsetFont(font.getBaseFont())) {
TrueTypeFont tt = ((PDTrueTypeFont) font).getTTFFont();
for (CMAPEncodingEntry c : tt.getCMAP().getCmaps()) {
if (c.getGlyphId(1) > 0) {
extra = "cid";
}
}
return name + extra;
}
// if (!isSubsetFont(font.getBaseFont())) {
// return font.getBaseFont() + "_" + ((COSName)fontData.getItem(COSName.SUBTYPE)).getName();
// }
if (font instanceof PDType1Font) {
if (((PDType1Font) font).getType1CFont() == null
|| ((PDType1Font) font).getType1CFont().getCFFFont() == null) {
if (font.getFontDescriptor() instanceof PDFontDescriptorDictionary) {
return name;
}
return null;
}
CFFEncoding encoding = ((PDType1Font)font).getType1CFont().getCFFFont().getEncoding();
String eClass = encoding.getClass().getName();
if (eClass.equals("org.apache.fontbox.cff.CFFParser$Format1Encoding")) {
extra = "f1enc";
} else if (eClass.equals("org.apache.fontbox.cff.CFFParser$Format0Encoding")) {
extra = "f0enc";
}
CFFCharset cs = ((PDType1Font)font).getType1CFont().getCFFFont().getCharset();
if (cs.getEntries().get(0).getSID() < OTFSubSetFile.NUM_STANDARD_STRINGS) {
extra += "stdcs";
}
if (cs.getClass().getName().equals("org.apache.fontbox.cff.CFFParser$Format1Charset")) {
extra += "f1cs";
}
return name + extra;
}
return null;
}
private static boolean isSubsetFont(String s) {
return SUBSET_PATTERN.matcher(s).matches();
}
private static String getName(String name) {
if (isSubsetFont(name)) {
return name.split("\\+")[1].replace(" ", "");
}
return name.replace(" ", "");
}
interface FOPPDFFont extends RefPDFFont {
String getFontName();
void setRef(PDFDictionary d);
String addFont(COSDictionary fontdata) throws IOException;
int size();
}
public class FOPPDFMultiByteFont extends MultiByteFont implements FOPPDFFont {
protected PDFDictionary ref;
private Map<Integer, Integer> newWidth = new TreeMap<Integer, Integer>();
private Map<String, Integer> charMapGlobal = new LinkedHashMap<String, Integer>();
private MergeTTFonts mergeTTFonts = new MergeTTFonts();
private MergeCFFFonts mergeCFFFonts = new MergeCFFFonts();
private Map<String, GlyphData> glyphs = new HashMap<String, GlyphData>();
public FOPPDFMultiByteFont(COSDictionary fontData, String name) throws IOException {
super(null, EmbeddingMode.SUBSET);
//this stops fop modifying font later on
setEmbeddingMode(EmbeddingMode.FULL);
readFontBBox(fontData);
setFontName(name);
addFont(fontData);
}
public String addFont(COSDictionary fontData) throws IOException {
PDSimpleFont font = getFont(fontData);
setProperties(this, font);
PDSimpleFont mainFont = font;
TrueTypeFont ttf = null;
if (font instanceof PDType0Font) {
PDCIDFont cidFont = (PDCIDFont) ((PDType0Font) font).getDescendantFont();
setDefaultWidth((int) cidFont.getDefaultWidth());
mainFont = cidFont;
if (cidFont instanceof PDCIDFontType0Font) {
setCIDType(CIDFontType.CIDTYPE0);
setFontType(FontType.CIDTYPE0);
} else {
ttf = ((PDCIDFontType2Font) cidFont).getTTFFont();
}
} else {
ttf = ((PDTrueTypeFont) font).getTTFFont();
setDefaultWidth(1000);
}
GlyphData[] glyphData = new GlyphData[0];
if (ttf != null) {
glyphData = ttf.getGlyph().getGlyphs();
}
Map<Integer, Integer> oldToNewGIMap = new HashMap<Integer, Integer>();
if (charMapGlobal.isEmpty()) {
oldToNewGIMap.put(0, 0); // .notdef glyph
}
CMap c = mainFont.getToUnicodeCMap();
if (c == null) {
c = font.getToUnicodeCMap();
}
Map<Integer, String> mapping = getMapping(mainFont, c, glyphData.length);
if (glyphData.length > 0 && differentGlyphData(glyphData, mapping)) {
// return null;
}
Map<Integer, String> gidToGlyph = new TreeMap<Integer, String>(mapping);
if (mainFont instanceof PDTrueTypeFont) {
CMAPEncodingEntry cmap = ttf.getCMAP().getCmaps()[0];
gidToGlyph.clear();
int[] gidToCode = cmap.getGlyphIdToCharacterCode();
for (int i = 1; i < glyphData.length; i++) {
String mappedChar = mapping.get(gidToCode[i]);
gidToGlyph.put(i, mappedChar);
}
}
readCharMap(font, gidToGlyph, glyphData, mainFont, oldToNewGIMap);
FontFileReader ffr = readFontFile(mainFont);
if (ttf != null) {
mergeMaxp(ttf, mergeTTFonts.maxp);
int sizeNoCompGlyphs = oldToNewGIMap.size();
mergeTTFonts.readFont(ffr, oldToNewGIMap, true);
if (oldToNewGIMap.size() > sizeNoCompGlyphs) {
cidSet.mapChar(256 * 256, (char) 0);
}
} else {
mergeCFFFonts.readType1CFont(new ByteArrayInputStream(ffr.getAllBytes()), getEmbedFontName());
}
return getFontName();
}
private void readCharMap(PDSimpleFont font, Map<Integer, String> gidToGlyph, GlyphData[] glyphData,
PDSimpleFont mainFont, Map<Integer, Integer> oldToNewGIMap) {
int widthPos = font.getFirstChar() + 1;
for (Map.Entry<Integer, String> i : gidToGlyph.entrySet()) {
String mappedChar = i.getValue();
int key = i.getKey();
boolean skipWidth = (mappedChar == null) || mappedChar.length() == 0;
if (skipWidth) {
mappedChar = (char)charMapGlobal.size() + "tmp";
} else if (mappedChar.length() > 1) {
mappedChar = "" + (char)mappedChar.hashCode();
}
if (!charMapGlobal.containsKey(mappedChar)) {
if (glyphData.length > 0
&& glyphData[key] == null
&& !CharUtilities.isAdjustableSpace(mappedChar.charAt(0))) {
continue;
}
boolean addToEnd = charMapGlobal.containsValue(key);
if (addToEnd) {
addPrivateUseMapping(mappedChar.charAt(0), charMapGlobal.size() + 1);
charMapGlobal.put(mappedChar, charMapGlobal.size() + 1);
} else {
addPrivateUseMapping(mappedChar.charAt(0), key);
charMapGlobal.put(mappedChar, key);
}
int glyph = 0;
if (hasChar(mappedChar.charAt(0))) {
glyph = (int) mapChar(mappedChar.charAt(0));
}
oldToNewGIMap.put(key, glyph);
if (!skipWidth) {
if (!(mainFont instanceof PDTrueTypeFont)) {
widthPos = key;
}
float w = font.getFontWidth(widthPos);
if (w >= 0) {
if (mainFont instanceof PDCIDFontType0Font) {
newWidth.put(key, (int)w);
} else {
newWidth.put(glyph, (int)w);
}
}
}
}
if (!skipWidth) {
widthPos++;
}
}
}
private Map<Integer, String> getMapping(PDSimpleFont font, CMap c, int len) throws IOException {
Map<Integer, String> mapping = new HashMap<Integer, String>();
if (font instanceof PDCIDFontType0Font) {
Collection<CFFFont.Mapping> mappings =
((PDCIDFontType0Font) font).getType1CFont().getCFFFont().getMappings();
for (CFFFont.Mapping m : mappings) {
String character = Encoding.getCharacterForName(m.getName());
mapping.put(m.getSID(), character);
}
}
if (c != null) {
int last = font.getLastChar();
if (last == -1) {
last = len;
}
int size = 1;
if (c.hasTwoByteMappings()) {
size = 2;
}
for (int i = font.getFirstChar(); i <= last; i++) {
String l = c.lookup(i, size);
if (l != null) {
mapping.put(i, l);
}
}
}
return mapping;
}
private boolean differentGlyphData(GlyphData[] data, Map<Integer, String> mapping) throws IOException {
Map<String, Integer> tmpMap = new HashMap<String, Integer>();
for (Map.Entry<Integer, String> entry : mapping.entrySet()) {
if (!tmpMap.containsKey(entry.getValue())) {
tmpMap.put(entry.getValue(), entry.getKey());
}
}
mapping.clear();
for (Map.Entry<String, Integer> entry : tmpMap.entrySet()) {
mapping.put(entry.getValue(), entry.getKey());
}
for (Map.Entry<Integer, String> n : mapping.entrySet()) {
if (data[n.getKey()] != null) {
if (glyphs.containsKey(n.getValue()) && !glyphs.get(n.getValue()).equals(data[n.getKey()])) {
return true;
}
glyphs.put(n.getValue(), data[n.getKey()]);
}
}
return false;
}
private FontFileReader readFontFile(PDFont font) throws IOException {
PDFontDescriptorDictionary fd = (PDFontDescriptorDictionary) font.getFontDescriptor();
PDStream ff = fd.getFontFile3();
if (ff == null) {
ff = fd.getFontFile2();
if (ff == null) {
ff = fd.getFontFile();
}
}
InputStream is = ff.createInputStream();
return new FontFileReader(new ByteArrayInputStream(IOUtils.toByteArray(is)));
}
public Map<Integer, Integer> getWidthsMap() {
return newWidth;
}
public PDFDictionary getRef() {
return ref;
}
public void setRef(PDFDictionary d) {
ref = d;
}
public int size() {
if (getFontType() == FontType.CIDTYPE0) {
return 1;
}
return 0;
}
private void readFontBBox(COSBase b) throws IOException {
if (b instanceof COSDictionary) {
COSDictionary dict = (COSDictionary)b;
for (Map.Entry<COSName, COSBase> n : dict.entrySet()) {
readFontBBox(n.getValue());
if (n.getKey() == COSName.FONT_BBOX) {
COSArray w = (COSArray)n.getValue();
float[] bboxf = w.toFloatArray();
int[] bbox = new int[bboxf.length];
for (int i = 0; i < bbox.length; i++) {
bbox[i] = (int)bboxf[i];
}
setFontBBox(bbox);
}
}
} else if (b instanceof COSObject) {
COSObject o = (COSObject)b;
readFontBBox(o.getObject());
} else if (b instanceof COSArray) {
COSArray o = (COSArray)b;
for (int i = 0; i < o.size(); i++) {
readFontBBox(o.get(i));
}
}
}
public boolean isEmbeddable() {
return true;
}
public InputStream getInputStream() throws IOException {
if (getFontType() == FontType.CIDTYPE0) {
mergeCFFFonts.writeFont();
return new ByteArrayInputStream(mergeCFFFonts.getFontSubset());
}
mergeTTFonts.writeFont(null);
return new ByteArrayInputStream(mergeTTFonts.getFontSubset());
}
}
public static class Cmap {
int platformId;
int platformEncodingId;
Map<Integer, Integer> glyphIdToCharacterCode = new TreeMap<Integer, Integer>();
}
public class FOPPDFSingleByteFont extends SingleByteFont implements FOPPDFFont {
private int fontCount;
private PDSimpleFont font;
protected PDFDictionary ref;
private Map<String, Integer> charMapGlobal = new LinkedHashMap<String, Integer>();
private Map<Integer, Integer> newWidth = new HashMap<Integer, Integer>();
private Map<String, byte[]> charStringsDict;
private Cmap newCmap = new Cmap();
private Map<Integer, String> encodingMap = new TreeMap<Integer, String>();
private int encodingSkip;
private MergeTTFonts mergeTTFonts = new MergeTTFonts();
private MergeCFFFonts mergeCFFFonts = new MergeCFFFonts();
private MergeType1Fonts mergeType1Fonts = new MergeType1Fonts();
private String embedName;
public FOPPDFSingleByteFont(COSDictionary fontData, String name) throws IOException {
super(null, EmbeddingMode.FULL);
if (fontData.getItem(COSName.SUBTYPE) == COSName.TRUE_TYPE) {
setFontType(FontType.TRUETYPE);
}
width = new int[0];
font = getFont(fontData);
setFirstChar(font.getFirstChar());
setLastChar(font.getLastChar());
loadFontFile(font);
float[] bBoxF = font.getFontBoundingBox().getCOSArray().toFloatArray();
int[] bBox = new int[bBoxF.length];
for (int i = 0; i < bBox.length; i++) {
bBox[i] = (int)bBoxF[i];
}
setFontBBox(bBox);
setFontName(name);
Object cmap = getCmap(font);
for (int i = font.getFirstChar(); i <= font.getLastChar(); i++) {
String mappedChar = getChar(cmap, i);
if (mappedChar != null && !charMapGlobal.containsKey(mappedChar)) {
charMapGlobal.put(mappedChar, i);
}
}
//mark font as used
notifyMapOperation();
setProperties(this, font);
if (font.getWidths() != null) {
//if width contains 0 we cant rely on codeToNameMap
boolean usesZero = font.getWidths().contains(0);
Set<Integer> codeToName = getCodeToName(font.getFontEncoding()).keySet();
for (int i = getFirstChar();
i <= Math.min(getLastChar(), getFirstChar() + font.getWidths().size()); i++) {
if (usesZero || codeToName.contains(i)) {
int w = font.getWidths().get(i - getFirstChar()).intValue();
newWidth.put(i, w);
} else {
newWidth.put(i, 0);
}
}
}
mapping = new FOPPDFEncoding();
encodingSkip = font.getLastChar() + 1;
addEncoding(font);
}
private Map<Integer, String> getCodeToName(Encoding encoding) {
Map<Integer, String> codeToName = new HashMap<Integer, String>();
if (encoding != null) {
COSBase cos = encoding.getCOSObject();
if (cos instanceof COSDictionary) {
COSDictionary enc = (COSDictionary) cos;
COSName baseEncodingName = (COSName) enc.getDictionaryObject(COSName.BASE_ENCODING);
if (baseEncodingName != null) {
try {
Encoding baseEncoding = EncodingManager.INSTANCE.getEncoding(baseEncodingName);
codeToName.putAll(baseEncoding.getCodeToNameMap());
} catch (IOException e) {
throw new RuntimeException(e);
}
}
COSArray differences = (COSArray)enc.getDictionaryObject(COSName.DIFFERENCES);
int currentIndex = -1;
for (int i = 0; differences != null && i < differences.size(); i++) {
COSBase next = differences.getObject(i);
if (next instanceof COSNumber) {
currentIndex = ((COSNumber)next).intValue();
} else if (next instanceof COSName) {
COSName name = (COSName)next;
codeToName.put(currentIndex++, name.getName());
}
}
} else {
return encoding.getCodeToNameMap();
}
}
return codeToName;
}
private Object getCmap(PDSimpleFont font) throws IOException {
if (font.getFontEncoding() != null) {
return font.getFontEncoding();
}
return font.getToUnicodeCMap();
}
private PDStream readFontFile(PDSimpleFont font) throws IOException {
PDFontDescriptorDictionary fd = (PDFontDescriptorDictionary) font.getFontDescriptor();
setFlags(fd.getFlags());
PDStream ff = fd.getFontFile3();
if (ff == null) {
ff = fd.getFontFile2();
if (ff == null) {
ff = fd.getFontFile();
}
} else {
setFontType(FontType.TYPE1C);
}
if (ff == null) {
throw new IOException(font.getBaseFont() + " no font file");
}
return ff;
}
private void loadFontFile(PDSimpleFont font) throws IOException {
PDStream ff = readFontFile(font);
mergeFontFile(ff.createInputStream(), font);
if (font instanceof PDTrueTypeFont) {
TrueTypeFont ttfont = ((PDTrueTypeFont) font).getTTFFont();
CMAPEncodingEntry[] cmapList = ttfont.getCMAP().getCmaps();
for (CMAPEncodingEntry c : cmapList) {
newCmap.platformId = c.getPlatformId();
newCmap.platformEncodingId = c.getPlatformEncodingId();
for (int i = 0; i < 256 * 256; i++) {
if (c.getGlyphId(i) != 0) {
newCmap.glyphIdToCharacterCode.put(i, c.getGlyphId(i));
}
}
}
mergeMaxp(ttfont, mergeTTFonts.maxp);
}
}
@Override
public boolean hasChar(char c) {
return charMapGlobal.containsKey(String.valueOf(c));
}
@Override
public char mapChar(char c) {
return mapping.mapChar(c);
}
public String getEmbedFontName() {
if (embedName == null) {
embedName = getName(font.getBaseFont());
}
return embedName;
}
public int[] getWidths() {
width = new int[getLastChar() - getFirstChar() + 1];
for (int i = getFirstChar(); i <= getLastChar(); i++) {
if (newWidth.containsKey(i)) {
width[i - getFirstChar()] = newWidth.get(i);
} else {
width[i - getFirstChar()] = 0;
}
}
return width.clone();
}
public String addFont(COSDictionary fontData) throws IOException {
PDSimpleFont font = getFont(fontData);
if (font instanceof PDType1Font && differentGlyphData((PDType1Font) font)) {
return null;
}
mergeWidths(font);
if (font.getFirstChar() < getFirstChar()) {
setFirstChar(font.getFirstChar());
}
for (int w : newWidth.keySet()) {
if (w > getLastChar()) {
setLastChar(w);
}
}
loadFontFile(font);
addEncoding(font);
return getFontName();
}
public int size() {
return fontCount;
}
private Map<String, byte[]> getCharStringsDict(PDType1Font font) throws IOException {
if (getFontType() == FontType.TYPE1) {
return font.getType1Font().getCharStringsDict();
}
return font.getType1CFont().getCFFFont().getCharStringsDict();
}
private boolean differentGlyphData(PDType1Font otherFont) throws IOException {
if (charStringsDict == null) {
charStringsDict = getCharStringsDict((PDType1Font) font);
}
for (Map.Entry<String, byte[]> s : getCharStringsDict(otherFont).entrySet()) {
if (charStringsDict.containsKey(s.getKey())) {
int numberDiff = 0;
byte[] b1 = charStringsDict.get(s.getKey());
byte[] b2 = s.getValue();
int b1Index = b1.length - 1;
int b2Index = b2.length - 1;
while (b1Index >= 0 && b2Index >= 0) {
if (b1[b1Index] != b2[b2Index]) {
numberDiff++;
if (numberDiff > 2) {
break;
}
}
b1Index--;
b2Index--;
}
if (numberDiff > 2) {
// log.info(getFontName() + " " + s.getKey() + " not equal " + numberdiff);
return true;
}
}
}
return false;
}
private void mergeWidths(PDSimpleFont font) throws IOException {
int w = 0;
int skipGlyphIndex = getLastChar() + 1;
Object cmap = getCmap(font);
Set<Integer> codeToName = getCodeToName(font.getFontEncoding()).keySet();
for (int i = font.getFirstChar(); i <= font.getLastChar(); i++) {
boolean addedWidth = false;
int glyphIndexPos = skipGlyphIndex;
if (font instanceof PDTrueTypeFont) {
glyphIndexPos = i;
}
int neww = 0;
if (font.getWidths() != null) {
neww = font.getWidths().get(i - font.getFirstChar()).intValue();
if (!newWidth.containsKey(i) || newWidth.get(i) == 0) {
if (getFontType() == FontType.TYPE1
|| font instanceof PDTrueTypeFont
|| codeToName.contains(i)) {
newWidth.put(i, neww);
glyphIndexPos = i;
} else {
newWidth.put(i, 0);
}
addedWidth = true;
}
}
String mappedChar = getChar(cmap, i);
if (mappedChar != null && !charMapGlobal.containsKey(mappedChar)) {
charMapGlobal.put(mappedChar, glyphIndexPos);
if (!addedWidth && w < font.getWidths().size()) {
newWidth.put(newWidth.size() + getFirstChar(), neww);
}
skipGlyphIndex++;
}
w++;
}
}
private String getChar(Object cmap, int i) throws IOException {
if (cmap instanceof CMap) {
CMap c = (CMap)cmap;
int size = 1;
if (c.hasTwoByteMappings()) {
size = 2;
}
return c.lookup(i, size);
}
Encoding enc = (Encoding)cmap;
if (enc instanceof DictionaryEncoding) {
return enc.getName(i);
}
return enc.getCharacter(i);
}
public String getEncodingName() {
if (font.getFontEncoding() != null) {
COSBase cosObject = font.getFontEncoding().getCOSObject();
if (cosObject != null) {
if (cosObject instanceof COSDictionary) {
COSBase item = ((COSDictionary) cosObject).getItem(COSName.BASE_ENCODING);
if (item != null) {
return ((COSName)item).getName();
}
} else if (cosObject instanceof COSName) {
return ((COSName) cosObject).getName();
} else {
throw new RuntimeException(cosObject.toString() + " not supported");
}
}
}
return null;
}
private void addEncoding(PDSimpleFont fontForEnc) {
List<String> added = new ArrayList<String>(encodingMap.values());
Map<Integer, String> codeToName = getCodeToName(fontForEnc.getFontEncoding());
for (int i = fontForEnc.getFirstChar(); i <= fontForEnc.getLastChar(); i++) {
if (codeToName.keySet().contains(i)) {
String s = codeToName.get(i);
if (!added.contains(s)) {
if (!encodingMap.containsKey(i)) {
encodingMap.put(i, s);
} else {
encodingMap.put(encodingSkip, s);
encodingSkip++;
}
}
}
}
}
class FOPPDFEncoding implements SingleByteEncoding {
private boolean cmap;
public String getName() {
return "FOPPDFEncoding";
}
public char mapChar(char c) {
if (charMapGlobal.containsKey(String.valueOf(c))) {
return (char)charMapGlobal.get(String.valueOf(c)).intValue();
}
return 0;
}
public String[] getCharNameMap() {
Collection<String> v = encodingMap.values();
return v.toArray(new String[v.size()]);
}
public char[] getUnicodeCharMap() {
if (cmap) {
if (font.getToUnicode() == null) {
return new char[0];
}
List<String> cmapStrings = new ArrayList<String>();
Map<Integer, String> cm = new HashMap<Integer, String>();
for (Map.Entry<String, Integer> o : charMapGlobal.entrySet()) {
cm.put(o.getValue(), o.getKey());
}
for (int i = 0; i < getLastChar() + 1; i++) {
if (cm.containsKey(i)) {
cmapStrings.add(cm.get(i));
} else {
cmapStrings.add(" ");
}
}
return fromStringToCharArray(cmapStrings);
}
cmap = true;
return toCharArray(encodingMap.keySet());
}
private char[] fromStringToCharArray(Collection<String> list) {
char[] ret = new char[list.size()];
int i = 0;
for (String e : list) {
if (e.length() > 0) {
ret[i++] = e.charAt(0);
}
}
return ret;
}
private char[] toCharArray(Collection<Integer> list) {
char[] ret = new char[list.size()];
int i = 0;
for (int e : list) {
ret[i++] = (char)e;
}
return ret;
}
}
public PDFDictionary getRef() {
return ref;
}
public void setRef(PDFDictionary d) {
ref = d;
}
public boolean isEmbeddable() {
return true;
}
public boolean isSymbolicFont() {
return false;
}
private void mergeFontFile(InputStream ff, PDSimpleFont pdSimpleFont) throws IOException {
if (getFontType() == FontType.TRUETYPE) {
Map<Integer, Integer> chars = new HashMap<Integer, Integer>();
chars.put(0, 0);
mergeTTFonts.readFont(new FontFileReader(ff), chars, false);
} else if (getFontType() == FontType.TYPE1) {
mergeType1Fonts.readFont(ff, (PDType1Font) pdSimpleFont);
} else {
mergeCFFFonts.readType1CFont(ff, getEmbedFontName());
}
fontCount++;
}
public InputStream getInputStream() throws IOException {
if (getFontType() == FontType.TYPE1C) {
mergeCFFFonts.writeFont();
return new ByteArrayInputStream(mergeCFFFonts.getFontSubset());
}
if (getFontType() == FontType.TRUETYPE) {
mergeTTFonts.writeFont(newCmap);
return new ByteArrayInputStream(mergeTTFonts.getFontSubset());
}
if (getFontType() == FontType.TYPE1) {
return new ByteArrayInputStream(mergeType1Fonts.writeFont());
}
return null;
}
}
private void setProperties(CustomFont cFont, PDSimpleFont font) {
if (font.getFontDescriptor() != null) {
cFont.setCapHeight((int) font.getFontDescriptor().getCapHeight());
cFont.setAscender((int)font.getFontDescriptor().getAscent());
cFont.setDescender((int)font.getFontDescriptor().getDescent());
cFont.setXHeight((int)font.getFontDescriptor().getXHeight());
cFont.setStemV((int)font.getFontDescriptor().getStemV());
}
}
private void mergeMaxp(TrueTypeFont ttf, MaximumProfileTable outMaxp) {
MaximumProfileTable mp = ttf.getMaximumProfile();
outMaxp.setVersion(mp.getVersion());
outMaxp.setNumGlyphs(outMaxp.getNumGlyphs() + mp.getNumGlyphs());
outMaxp.setMaxPoints(outMaxp.getMaxPoints() + mp.getMaxPoints());
outMaxp.setMaxContours(outMaxp.getMaxContours() + mp.getMaxContours());
outMaxp.setMaxCompositePoints(outMaxp.getMaxCompositePoints() + mp.getMaxCompositePoints());
outMaxp.setMaxCompositeContours(outMaxp.getMaxCompositeContours() + mp.getMaxCompositeContours());
outMaxp.setMaxZones(outMaxp.getMaxZones() + mp.getMaxZones());
outMaxp.setMaxTwilightPoints(outMaxp.getMaxTwilightPoints() + mp.getMaxTwilightPoints());
outMaxp.setMaxStorage(outMaxp.getMaxStorage() + mp.getMaxStorage());
outMaxp.setMaxFunctionDefs(outMaxp.getMaxFunctionDefs() + mp.getMaxFunctionDefs());
outMaxp.setMaxInstructionDefs(outMaxp.getMaxInstructionDefs() + mp.getMaxInstructionDefs());
outMaxp.setMaxStackElements(outMaxp.getMaxStackElements() + mp.getMaxStackElements());
outMaxp.setMaxSizeOfInstructions(outMaxp.getMaxSizeOfInstructions() + mp.getMaxSizeOfInstructions());
outMaxp.setMaxComponentElements(outMaxp.getMaxComponentElements() + mp.getMaxComponentElements());
outMaxp.setMaxComponentDepth(outMaxp.getMaxComponentDepth() + mp.getMaxComponentDepth());
}
public class PDFWriter {
protected StringBuilder s = new StringBuilder();
private String key;
private List<COSName> resourceNames;
public PDFWriter(String key, List<COSName> resourceNames) {
this.key = key;
this.resourceNames = resourceNames;
}
public String writeText(PDStream pdStream) throws IOException {
Iterator<Object> it = new PDFStreamParser(pdStream).getTokenIterator();
List<COSBase> arguments = new ArrayList<COSBase>();
while (it.hasNext()) {
Object o = it.next();
if (o instanceof PDFOperator) {
PDFOperator op = (PDFOperator)o;
readPDFArguments(op, arguments);
s.append(op.getOperation() + "\n");
arguments.clear();
if (op.getImageParameters() != null) {
for (Map.Entry<COSName, COSBase> cn : op.getImageParameters().entrySet()) {
arguments.add(cn.getKey());
arguments.add(cn.getValue());
}
readPDFArguments(op, arguments);
s.append("ID " + new String(op.getImageData(), "ISO-8859-1"));
arguments.clear();
s.append("EI\n");
}
} else {
arguments.add((COSBase)o);
}
}
return s.toString();
}
protected void readPDFArguments(PDFOperator op, Collection<COSBase> arguments) throws IOException {
for (COSBase c : arguments) {
processArg(op, c);
}
}
protected void processArg(PDFOperator op, COSBase c) throws IOException {
if (c instanceof COSInteger) {
s.append(((COSInteger) c).intValue());
s.append(" ");
} else if (c instanceof COSFloat) {
float f = ((COSFloat) c).floatValue();
s.append(new DecimalFormat("#.####").format(f));
s.append(" ");
} else if (c instanceof COSName) {
COSName cn = (COSName)c;
s.append("/" + cn.getName());
addKey(cn);
s.append(" ");
} else if (c instanceof COSString) {
s.append("<" + ((COSString) c).getHexString() + ">");
} else if (c instanceof COSArray) {
s.append("[");
readPDFArguments(op, (Collection<COSBase>) ((COSArray) c).toList());
s.append("] ");
} else if (c instanceof COSDictionary) {
Collection<COSBase> dictArgs = new ArrayList<COSBase>();
for (Map.Entry<COSName, COSBase> cn : ((COSDictionary)c).entrySet()) {
dictArgs.add(cn.getKey());
dictArgs.add(cn.getValue());
}
s.append("<<");
readPDFArguments(op, dictArgs);
s.append(">>");
} else {
throw new IOException(c + " not supported");
}
}
protected void addKey(COSName cn) {
if (resourceNames.contains(cn)) {
s.append(key);
}
}
}
public class MergeFontsPDFWriter extends PDFWriter {
private COSDictionary fonts;
private FontInfo fontInfo;
private Typeface font;
private PDSimpleFont oldFont = null;
private Map<COSName, String> fontsToRemove = new HashMap<COSName, String>();
public MergeFontsPDFWriter(COSDictionary fonts, FontInfo fontInfo, String key, List<COSName> resourceNames) {
super(key, resourceNames);
this.fonts = fonts;
this.fontInfo = fontInfo;
}
public String writeText(PDStream pdStream) throws IOException {
String txt = super.writeText(pdStream);
if (fontsToRemove.isEmpty()) {
return null;
}
for (COSName cn : fontsToRemove.keySet()) {
fonts.removeItem(cn);
}
return txt;
}
protected void readPDFArguments(PDFOperator op, Collection<COSBase> arguments) throws IOException {
for (COSBase c : arguments) {
if (c instanceof COSName) {
COSName cn = (COSName)c;
COSDictionary fontData = (COSDictionary)fonts.getDictionaryObject(cn.getName());
String internalName = fontsToRemove.get(cn);
if (internalName == null && fontData != null) {
internalName = getNewFont(fontData, fontInfo, fontsToRemove.values());
}
if (fontData == null || internalName == null) {
s.append("/" + cn.getName());
addKey(cn);
if (op.getOperation().equals("Tf")) {
font = null;
oldFont = null;
}
} else {
s.append("/" + internalName);
fontsToRemove.put(cn, internalName);
font = fontInfo.getUsedFonts().get(internalName);
oldFont = getFont(fontData);
}
s.append(" ");
} else if (c instanceof COSString && font != null && ((FOPPDFFont)font).size() != 1) {
List<String> word = readCOSString((COSString)c, oldFont);
if (word == null) {
s.append(PDFText.escapeString(((COSString) c).getString()));
} else {
String x = getMappedWord(word, font, ((COSString) c).getBytes());
if (x == null) {
s.append(PDFText.escapeString(((COSString) c).getString()));
} else {
s.append(x);
}
}
} else {
processArg(op, c);
}
}
}
private String getMappedWord(List<String> word, Typeface font, byte[] bytes) throws IOException {
StringBuffer newOct = new StringBuffer();
StringBuilder newHex = new StringBuilder();
int i = 0;
for (String str : word) {
Integer mapped = getMapping(bytes[i]);
if (mapped == null) {
char c = str.charAt(0);
if (str.length() > 1) {
c = (char) str.hashCode();
}
if (font.hasChar(c)) {
mapped = (int)font.mapChar(c);
} else {
return null;
}
}
newHex.append(String.format("%1$04x", mapped & 0xFFFF).toUpperCase(Locale.getDefault()));
PDFText.escapeStringChar((char)mapped.intValue(), newOct);
i++;
}
if (font instanceof SingleByteFont) {
return "(" + newOct.toString() + ")";
}
return "<" + newHex.toString() + ">";
}
private Integer getMapping(byte i) throws IOException {
if (oldFont.getFontEncoding() != null && font instanceof FOPPDFSingleByteFont) {
String name = oldFont.getFontEncoding().getName(i);
if (name != null && ((FOPPDFSingleByteFont)font).charMapGlobal.containsKey(name)) {
return ((FOPPDFSingleByteFont)font).charMapGlobal.get(name);
}
}
return null;
}
private List<String> readCOSString(COSString s, PDSimpleFont oldFont) throws IOException {
List<String> word = new ArrayList<String>();
byte[] string = s.getBytes();
int codeLength;
// String t1Str = new String(string, "UTF-8");
for (int i = 0; i < string.length; i += codeLength) {
codeLength = 1;
String c = oldFont.encode(string, i, codeLength);
// if (oldFont instanceof PDType1Font && i < t1Str.length()) {
// c = ((PDType1Font)oldFont).encodetype1(string, i, codeLength);
// }
if (c == null && i + 1 < string.length) {
codeLength++;
c = oldFont.encode(string, i, codeLength);
}
if (c == null) {
return null;
}
word.add(c);
}
return word;
}
}
private PDSimpleFont getFont(COSDictionary fontData) throws IOException {
if (!fontMap.containsKey(fontData)) {
if (fontMap.size() > 10) {
fontMap.clear();
}
fontMap.put(fontData, (PDSimpleFont)PDFontFactory.createFont(fontData));
}
return fontMap.get(fontData);
}
/**
* Creates a stream (from FOP's PDF library) from a PDF page parsed with PDFBox.
* @param sourceDoc the source PDF the given page to be copied belongs to
* @param page the page to transform into a stream
* @param key value to use as key for the stream
* @param eventBroadcaster events
* @param atdoc adjustment for stream
* @param fontinfo fonts
* @param pos rectangle
* @return the stream
* @throws IOException if an I/O error occurs
*/
public String createStreamFromPDFBoxPage(PDDocument sourceDoc, PDPage page, String key,
EventBroadcaster eventBroadcaster, AffineTransform atdoc, FontInfo fontinfo, Rectangle pos)
throws IOException {
handleAcroForm(sourceDoc, page, eventBroadcaster, atdoc);
PDResources sourcePageResources = page.findResources();
PDFDictionary pageResources = null;
PDStream pdStream = page.getContents();
COSDictionary fonts = (COSDictionary)sourcePageResources.getCOSDictionary().getDictionaryObject(COSName.FONT);
COSDictionary fontsBackup = null;
String uniqueName = Integer.toString(key.hashCode());
String newStream = null;
if (fonts != null && pdfDoc.isMergeFontsEnabled()) {
fontsBackup = new COSDictionary(fonts);
MergeFontsPDFWriter m = new MergeFontsPDFWriter(fonts, fontinfo, uniqueName,
getResourceNames(sourcePageResources.getCOSDictionary()));
newStream = m.writeText(pdStream);
parentFonts = m.fontsToRemove.values();
// if (newStream != null) {
// for (Object f : fonts.keySet().toArray()) {
// COSDictionary fontdata = (COSDictionary)fonts.getDictionaryObject((COSName)f);
// if (getUniqueFontName(fontdata) != null) {
// fonts.removeItem((COSName)f);
// }
// }
// }
}
if (newStream == null) {
newStream = new PDFWriter(uniqueName,
getResourceNames(sourcePageResources.getCOSDictionary())).writeText(pdStream);
}
pdStream = new PDStream(sourceDoc, new ByteArrayInputStream(newStream.getBytes("ISO-8859-1")));
mergeXObj(sourcePageResources.getCOSDictionary(), fontinfo, uniqueName);
pageResources = (PDFDictionary)cloneForNewDocument(sourcePageResources.getCOSDictionary());
PDFDictionary fontDict = (PDFDictionary)pageResources.get("Font");
if (fontDict != null && pdfDoc.isMergeFontsEnabled()) {
for (Map.Entry<String, Typeface> fontEntry : fontinfo.getUsedFonts().entrySet()) {
Typeface font = fontEntry.getValue();
if (font instanceof FOPPDFFont) {
FOPPDFFont pdfFont = (FOPPDFFont)font;
if (pdfFont.getRef() == null) {
pdfFont.setRef(new PDFDictionary());
pdfDoc.assignObjectNumber(pdfFont.getRef());
}
fontDict.put(fontEntry.getKey(), pdfFont.getRef());
}
}
}
updateXObj(sourcePageResources.getCOSDictionary(), pageResources);
if (fontsBackup != null) {
sourcePageResources.getCOSDictionary().setItem(COSName.FONT, fontsBackup);
}
COSStream originalPageContents = (COSStream)pdStream.getCOSObject();
bindOptionalContent(sourceDoc);
PDFStream pageStream;
Set filter;
if (originalPageContents instanceof COSStreamArray) {
COSStreamArray array = (COSStreamArray)originalPageContents;
pageStream = new PDFStream();
InputStream in = array.getUnfilteredStream();
OutputStream out = pageStream.getBufferOutputStream();
IOUtils.copyLarge(in, out);
filter = FILTER_FILTER;
} else {
pageStream = (PDFStream)cloneForNewDocument(originalPageContents);
filter = Collections.EMPTY_SET;
}
if (pageStream == null) {
pageStream = new PDFStream();
}
if (originalPageContents != null) {
transferDict(originalPageContents, pageStream, filter);
}
transferPageDict(fonts, uniqueName, sourcePageResources);
PDRectangle mediaBox = page.findMediaBox();
PDRectangle cropBox = page.findCropBox();
PDRectangle viewBox = cropBox != null ? cropBox : mediaBox;
//Handle the /Rotation entry on the page dict
int rotation = PDFUtil.getNormalizedRotation(page);
//Transform to FOP's user space
float w = (float)pos.getWidth() / 1000f;
float h = (float)pos.getHeight() / 1000f;
if (rotation == 90 || rotation == 270) {
float tmp = w;
w = h;
h = tmp;
}
atdoc.setTransform(AffineTransform.getScaleInstance(w / viewBox.getWidth(), h / viewBox.getHeight()));
atdoc.translate(0, viewBox.getHeight());
atdoc.rotate(-Math.PI);
atdoc.scale(-1, 1);
atdoc.translate(-viewBox.getLowerLeftX(), -viewBox.getLowerLeftY());
switch (rotation) {
case 90:
atdoc.scale(viewBox.getWidth() / viewBox.getHeight(), viewBox.getHeight() / viewBox.getWidth());
atdoc.translate(0, viewBox.getWidth());
atdoc.rotate(-Math.PI / 2.0);
atdoc.scale(viewBox.getWidth() / viewBox.getHeight(), viewBox.getHeight() / viewBox.getWidth());
break;
case 180:
atdoc.translate(viewBox.getWidth(), viewBox.getHeight());
atdoc.rotate(-Math.PI);
break;
case 270:
atdoc.translate(0, viewBox.getHeight());
atdoc.rotate(Math.toRadians(270 + 180));
atdoc.translate(-viewBox.getWidth(), -viewBox.getHeight());
break;
default:
//no additional transformations necessary
break;
}
StringBuffer boxStr = new StringBuffer();
boxStr.append(0).append(' ').append(0).append(' ');
boxStr.append(PDFNumber.doubleOut(mediaBox.getWidth())).append(' ');
boxStr.append(PDFNumber.doubleOut(mediaBox.getHeight())).append(" re W n\n");
return boxStr.toString() + pdStream.getInputStreamAsString();
}
private void mergeXObj(COSDictionary sourcePageResources, FontInfo fontinfo, String uniqueName) throws IOException {
COSDictionary xobj = (COSDictionary) sourcePageResources.getDictionaryObject(COSName.XOBJECT);
if (xobj != null && pdfDoc.isMergeFontsEnabled()) {
for (Map.Entry<COSName, COSBase> i : xobj.entrySet()) {
COSObject v = (COSObject) i.getValue();
COSStream stream = (COSStream) v.getObject();
COSDictionary res = (COSDictionary) stream.getDictionaryObject(COSName.RESOURCES);
if (res != null) {
COSDictionary src = (COSDictionary) res.getDictionaryObject(COSName.FONT);
if (src != null) {
COSDictionary target = (COSDictionary) sourcePageResources.getDictionaryObject(COSName.FONT);
if (target == null) {
sourcePageResources.setItem(COSName.FONT, src);
} else {
for (Map.Entry<COSName, COSBase> entry : src.entrySet()) {
if (!target.keySet().contains(entry.getKey())) {
target.setItem(entry.getKey(), entry.getValue());
}
}
}
PDFWriter writer = new MergeFontsPDFWriter(src, fontinfo, uniqueName,
getResourceNames(sourcePageResources));
String c = writer.writeText(PDStream.createFromCOS(stream));
if (c != null) {
stream.removeItem(COSName.FILTER);
newXObj.put(i.getKey(), c);
for (Object e : src.keySet().toArray()) {
COSName name = (COSName) e;
src.setItem(name.getName() + uniqueName, src.getItem(name));
src.removeItem(name);
}
}
}
}
}
}
}
private void updateXObj(COSDictionary sourcePageResources, PDFDictionary pageResources) throws IOException {
COSDictionary xobj = (COSDictionary) sourcePageResources.getDictionaryObject(COSName.XOBJECT);
if (xobj != null && pdfDoc.isMergeFontsEnabled()) {
PDFDictionary target = (PDFDictionary) pageResources.get("XObject");
for (COSName entry : xobj.keySet()) {
if (newXObj.containsKey(entry)) {
PDFStream s = (PDFStream) target.get(entry.getName());
s.setData(newXObj.get(entry).getBytes("UTF-8"));
PDFDictionary xobjr = (PDFDictionary) s.get("Resources");
xobjr.put("Font", pageResources.get("Font"));
}
}
}
}
private List<COSName> getResourceNames(COSDictionary sourcePageResources) {
List<COSName> resourceNames = new ArrayList<COSName>();
for (COSBase e : sourcePageResources.getValues()) {
if (e instanceof COSObject) {
e = ((COSObject) e).getObject();
}
if (e instanceof COSDictionary) {
COSDictionary d = (COSDictionary) e;
resourceNames.addAll(d.keySet());
}
}
return resourceNames;
}
private void transferPageDict(COSDictionary fonts, String uniqueName, PDResources sourcePageResources)
throws IOException {
if (fonts != null) {
for (Map.Entry<COSName, COSBase> f : fonts.entrySet()) {
String name = f.getKey().getName() + uniqueName;
targetPage.getPDFResources().addFont(name, (PDFDictionary)cloneForNewDocument(f.getValue()));
}
}
for (Map.Entry<COSName, COSBase> e : sourcePageResources.getCOSDictionary().entrySet()) {
transferDict(e, uniqueName);
}
}
private void transferDict(Map.Entry<COSName, COSBase> dict, String uniqueName) throws IOException {
COSBase src;
if (dict.getValue() instanceof COSObject) {
src = ((COSObject) dict.getValue()).getObject();
} else {
src = dict.getValue();
}
if (dict.getKey() != COSName.FONT && src instanceof COSDictionary) {
String name = dict.getKey().getName();
PDFDictionary newDict = (PDFDictionary) targetPage.getPDFResources().get(name);
if (newDict == null) {
newDict = new PDFDictionary(targetPage.getPDFResources());
}
COSDictionary srcDict = (COSDictionary) src;
for (Map.Entry<COSName, COSBase> v : srcDict.entrySet()) {
newDict.put(v.getKey().getName() + uniqueName, cloneForNewDocument(v.getValue()));
}
targetPage.getPDFResources().put(name, newDict);
}
}
private String getNewFont(COSDictionary fontData, FontInfo fontinfo, Collection<String> usedFonts)
throws IOException {
String base = getUniqueFontName(fontData);
if (base == null || usedFonts.contains(base) || (parentFonts != null && parentFonts.contains(base))) {
return null;
}
try {
for (Typeface t : fontinfo.getUsedFonts().values()) {
if (t instanceof FOPPDFFont && base.equals(t.getFontName())) {
return ((FOPPDFFont)t).addFont(fontData);
}
}
if (base.endsWith("cid") || fontData.getItem(COSName.SUBTYPE) != COSName.TYPE1
&& fontData.getItem(COSName.SUBTYPE) != COSName.TRUE_TYPE) {
fontinfo.addMetrics(base, new FOPPDFMultiByteFont(fontData, base));
} else {
fontinfo.addMetrics(base, new FOPPDFSingleByteFont(fontData, base));
}
} catch (IOException e) {
log.warn(e.getMessage());
return null;
}
fontinfo.useFont(base);
return base;
}
private void bindOptionalContent(PDDocument sourceDoc) throws IOException {
/*
* PDOptionalContentProperties ocProperties =
* sourceDoc.getDocumentCatalog().getOCProperties(); PDFDictionary ocDictionary =
* (PDFDictionary) cloneForNewDocument(ocProperties); if (ocDictionary != null) {
* this.pdfDoc.getRoot().put(COSName.OCPROPERTIES.getName(), ocDictionary); }
*/
}
private void handleAcroForm(PDDocument sourceDoc, PDPage page,
EventBroadcaster eventBroadcaster, AffineTransform at) throws IOException {
PDDocumentCatalog srcCatalog = sourceDoc.getDocumentCatalog();
PDAcroForm srcAcroForm = srcCatalog.getAcroForm();
List pageAnnotations = page.getAnnotations();
if (srcAcroForm == null && pageAnnotations.isEmpty()) {
return;
}
PDRectangle mediaBox = page.findMediaBox();
PDRectangle cropBox = page.findCropBox();
PDRectangle viewBox = cropBox != null ? cropBox : mediaBox;
for (Object obj : pageAnnotations) {
PDAnnotation annot = (PDAnnotation)obj;
PDRectangle rect = annot.getRectangle();
rect.move((float)(at.getTranslateX() - viewBox.getLowerLeftX()),
(float)at.getTranslateY() - viewBox.getLowerLeftY());
}
//Pseudo-cache the target page in place of the original source page.
//This essentially replaces the original page reference with the target page.
COSObject cosPage = null;
if (page.getCOSObject() instanceof COSObject) {
cosPage = (COSObject)page.getCOSObject();
} else {
PDPageNode pageNode = page.getParent();
COSArray kids = (COSArray)pageNode.getDictionary().getDictionaryObject(COSName.KIDS);
Iterator iter = kids.iterator();
while (iter.hasNext()) {
//Hopefully safe to cast, as kids need to be indirect objects
COSObject kid = (COSObject)iter.next();
if (kid.getObject() == page.getCOSObject()) {
cosPage = kid;
break;
}
}
if (cosPage == null) {
throw new IOException("Illegal PDF. Page not part of parent page node.");
}
}
cacheClonedObject(cosPage, this.targetPage);
COSArray annots = (COSArray) page.getCOSDictionary().getDictionaryObject(COSName.ANNOTS);
Set<COSObject> fields = Collections.emptySet();
if (annots != null) {
fields = new HashSet();
Iterator iter = annots.iterator();
while (iter.hasNext()) {
COSObject annot = (COSObject) iter.next();
COSObject fieldObject = annot;
COSDictionary field = (COSDictionary) fieldObject.getObject();
if ("Widget".equals(field.getNameAsString(COSName.SUBTYPE))) {
COSObject parent;
while ((parent = (COSObject) field.getItem(COSName.PARENT)) != null) {
fieldObject = parent;
field = (COSDictionary) fieldObject.getObject();
}
fields.add(fieldObject);
Collection<COSName> exclude = new ArrayList<COSName>();
exclude.add(COSName.P);
if (((COSDictionary)annot.getObject()).getItem(COSName.getPDFName("StructParent")) != null) {
exclude.add(COSName.PARENT);
}
PDFObject clonedAnnot = (PDFObject) cloneForNewDocument(annot, annot, exclude);
targetPage.addAnnotation(clonedAnnot);
}
}
}
boolean formAlreadyCopied = getCachedClone(srcAcroForm) != null;
PDFRoot catalog = this.pdfDoc.getRoot();
PDFDictionary destAcroForm = (PDFDictionary)catalog.get(COSName.ACRO_FORM.getName());
if (formAlreadyCopied) {
//skip, already copied
} else if (destAcroForm == null) {
if (srcAcroForm != null) {
//With this, only the first PDF's AcroForm is copied over. If later AcroForms have
//different properties besides the actual fields, these get lost. Only fields
//get merged.
Collection exclude = Collections.singletonList(COSName.FIELDS);
destAcroForm = (PDFDictionary)cloneForNewDocument(srcAcroForm, srcAcroForm, exclude);
} else {
//Work-around for incorrectly split PDFs which lack an AcroForm but have widgets
//on pages. This doesn't handle the case where field dicts have "C" entries
//(for the "CO" entry), so this may produce problems, but we have almost no chance
//to guess the calculation order.
destAcroForm = new PDFDictionary(pdfDoc.getRoot());
}
pdfDoc.registerObject(destAcroForm);
catalog.put(COSName.ACRO_FORM.getName(), destAcroForm);
}
PDFArray clonedFields = (PDFArray) destAcroForm.get(COSName.FIELDS.getName());
if (clonedFields == null) {
clonedFields = new PDFArray();
destAcroForm.put(COSName.FIELDS.getName(), clonedFields);
}
for (COSObject field : fields) {
PDFDictionary clone = (PDFDictionary) cloneForNewDocument(field, field, Arrays.asList(COSName.KIDS));
clonedFields.add(clone);
}
}
}