Hi,
For my project I use PDFBox to merge and overlay documents and to fill
forms.
BTW, thanks for your effort guys.
Merge and fill works perfect, but in overlay I faced some issues:
- no support for COSArray
- if I apply two overlays on a document and they have images with the
same name, the one image overrides the other one
- if the original document has an unusual MediaBox (for example Cognos
produces MediaBox [0 0 612 -792]) the overlay falls outside of the document
To fix the above issues I've created my own overlay implementation.
See attachment.
Instead of merging the contents and resources, it creates a BBox with
the overlay content.
I thought I share it with you so you can reuse some parts if you want.
Regards,
Balazs
ps: I'm not on the mailing list, please send any replies to my email
address directly
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.apache.log4j.Logger;
import org.apache.pdfbox.cos.COSArray;
import org.apache.pdfbox.cos.COSBase;
import org.apache.pdfbox.cos.COSDictionary;
import org.apache.pdfbox.cos.COSInteger;
import org.apache.pdfbox.cos.COSName;
import org.apache.pdfbox.cos.COSObject;
import org.apache.pdfbox.cos.COSStream;
import org.apache.pdfbox.exceptions.COSVisitorException;
import org.apache.pdfbox.io.RandomAccessBuffer;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDDocumentCatalog;
import org.apache.pdfbox.pdmodel.PDPage;
import org.apache.pdfbox.pdmodel.PDResources;
import org.apache.pdfbox.pdmodel.common.PDRectangle;
/**
* Adds an overlay to an existing PDF document.
*
* @author bjerk
*/
public class Overlay {
public static final String USAGE = "Usage: Overlay <input.pdf>
[<defaultOverlay.pdf>] [-odd <oddPageOverlay.pdf>] [-even
<evenPageOverlay.pdf>] [-first <firstPageOverlay.pdf>] [-last
<lastPageOverlay.pdf>] [-page <pageNumber> <specificPageOverlay.pdf> 0..n]
[-position foreground|background] <output.pdf>";
public enum Position { FOREGROUND, BACKGROUND };
private static final Logger logger = Logger.getLogger(Overlay.class);
private static final String XOBJECT_PREFIX = "OL";
private PDDocument pdfDocument;
private PDDocument defaultOverlay;
private LayoutPage defaultOverlayPage;
private PDDocument firstPageOverlay;
private LayoutPage firstPageOverlayPage;
private PDDocument lastPageOverlay;
private LayoutPage lastPageOverlayPage;
private PDDocument oddPageOverlay;
private LayoutPage oddPageOverlayPage;
private PDDocument evenPageOverlay;
private LayoutPage evenPageOverlayPage;
private Map<Integer, PDDocument> specificPageOverlay = new HashMap<Integer,
PDDocument>();
private Map<Integer, LayoutPage> specificPageOverlayPage = new
HashMap<Integer, LayoutPage>();
private Position position = Position.BACKGROUND;
/**
* This will overlay a document and write out the results.
*
* @param args command line arguments
* @throws Exception
* @see #USAGE
*/
public static void main(final String[] args) throws Exception {
String inputFile = null;
String outputFile = null;
String defaultOverlayFile = null;
String firstPageOverlayFile = null;
String lastPageOverlayFile = null;
String oddPageOverlayFile = null;
String evenPageOverlayFile = null;
Map<Integer, String> specificPageOverlayFile = new HashMap<Integer,
String>();
Position position = Position.BACKGROUND;
// input arguments
for (int i = 0; i < args.length; i++) {
String arg = args[i].trim();
if (i == 0) {
inputFile = arg;
} else if (i == (args.length - 1)) {
outputFile = arg;
} else if (arg.equals("-position")
&& ((i + 1) < args.length)) {
if ("foreground".equalsIgnoreCase(args[i + 1].trim())) {
position = Position.FOREGROUND;
}
i += 1;
} else if (arg.equals("-odd")
&& ((i + 1) < args.length)) {
oddPageOverlayFile = args[i + 1].trim();
i += 1;
} else if (arg.equals("-even")
&& ((i + 1) < args.length)) {
evenPageOverlayFile = args[i + 1].trim();
i += 1;
} else if (arg.equals("-first")
&& ((i + 1) < args.length)) {
firstPageOverlayFile = args[i + 1].trim();
i += 1;
} else if (arg.equals("-last")
&& ((i + 1) < args.length)) {
lastPageOverlayFile = args[i + 1].trim();
i += 1;
} else if (arg.equals("-page")
&& ((i + 2) < args.length)
&& (isInteger(args[i + 1].trim()))) {
specificPageOverlayFile.put(Integer.parseInt(args[i +
1].trim()), args[i + 2].trim());
i += 2;
} else if (defaultOverlayFile == null) {
defaultOverlayFile = arg;
} else {
logger.error(USAGE);
throw new Exception(USAGE);
}
}
if ((inputFile == null)
|| (outputFile == null)) {
logger.error(USAGE);
throw new Exception(USAGE);
}
try {
Overlay overlayer = new Overlay();
overlayer.overlay(inputFile, defaultOverlayFile,
firstPageOverlayFile, lastPageOverlayFile, oddPageOverlayFile,
evenPageOverlayFile, specificPageOverlayFile, outputFile, position);
} catch (Exception e) {
logger.error("Overlay failed: " + e.getMessage(), e);
throw e;
}
}
/**
* This will add overlays to a documents.
*
* @param inputFile input file
* @param defaultOverlayFile default overlay file
* @param firstPageOverlayFile first page overlay file
* @param lastPageOverlayFile last page overlay file
* @param oddPageOverlayFile odd page overlay file
* @param evenPageOverlayFile even page overlay file
* @param specificPageOverlayFile map of overlay files for specific pages
* @param outputFile output file
* @param position the position of the overlay
* @throws IOException exception
* @throws COSVisitorException exception
*/
public void overlay(String inputFile, String defaultOverlayFile, String
firstPageOverlayFile, String lastPageOverlayFile, String oddPageOverlayFile,
String evenPageOverlayFile, Map<Integer, String> specificPageOverlayFile,
String outputFile, Position position) throws IOException, COSVisitorException {
try {
pdfDocument = PDDocument.load(inputFile);
if (defaultOverlayFile != null) {
defaultOverlay = PDDocument.load(defaultOverlayFile);
defaultOverlayPage = getLayoutPage(defaultOverlay);
}
if (firstPageOverlayFile != null) {
firstPageOverlay = PDDocument.load(firstPageOverlayFile);
firstPageOverlayPage = getLayoutPage(firstPageOverlay);
}
if (lastPageOverlayFile != null) {
lastPageOverlay = PDDocument.load(lastPageOverlayFile);
lastPageOverlayPage = getLayoutPage(lastPageOverlay);
}
if (oddPageOverlayFile != null) {
oddPageOverlay = PDDocument.load(oddPageOverlayFile);
oddPageOverlayPage = getLayoutPage(oddPageOverlay);
}
if (evenPageOverlayFile != null) {
evenPageOverlay = PDDocument.load(evenPageOverlayFile);
evenPageOverlayPage = getLayoutPage(evenPageOverlay);
}
for (Map.Entry<Integer, String> e :
specificPageOverlayFile.entrySet()) {
PDDocument doc = PDDocument.load(e.getValue());
specificPageOverlay.put(e.getKey(), doc);
specificPageOverlayPage.put(e.getKey(), getLayoutPage(doc));
}
this.position = position;
PDDocumentCatalog pdfCatalog = pdfDocument.getDocumentCatalog();
processPages(pdfCatalog.getAllPages());
pdfDocument.save(outputFile);
} finally {
if (pdfDocument != null) {
pdfDocument.close();
}
if (defaultOverlay != null) {
defaultOverlay.close();
}
if (firstPageOverlay != null) {
firstPageOverlay.close();
}
if (lastPageOverlay != null) {
lastPageOverlay.close();
}
if (oddPageOverlay != null) {
oddPageOverlay.close();
}
if (evenPageOverlay != null) {
evenPageOverlay.close();
}
for (Map.Entry<Integer, PDDocument> e :
specificPageOverlay.entrySet()) {
e.getValue().close();
}
}
}
private static boolean isInteger(String str) {
try {
Integer.parseInt(str);
} catch (NumberFormatException nfe) {
return false;
}
return true;
}
/**
* Stores the overlay page information.
*/
private static class LayoutPage {
private final PDRectangle mediaBox;
private final COSStream contentStream;
private final COSDictionary resources;
private LayoutPage(PDRectangle mediaBox, COSStream contentStream,
COSDictionary resources) {
this.mediaBox = mediaBox;
this.contentStream = contentStream;
this.resources = resources;
}
}
private LayoutPage getLayoutPage(PDDocument doc) throws IOException {
PDDocumentCatalog catalog = doc.getDocumentCatalog();
PDPage page = (PDPage) catalog.getAllPages().get(0);
COSBase contents =
page.getCOSDictionary().getDictionaryObject(COSName.CONTENTS);
PDResources resources = page.findResources();
if (resources == null) {
resources = new PDResources();
}
return new LayoutPage(page.getMediaBox(),
createContentStream(contents), resources.getCOSDictionary());
}
private COSStream createContentStream(COSBase contents) throws IOException {
List<COSStream> contentStreams = createContentStreamList(contents);
// concatenate streams
COSStream concatStream = new COSStream(new RandomAccessBuffer());
OutputStream out = concatStream.createUnfilteredStream();
for (COSStream contentStream : contentStreams) {
InputStream in = contentStream.getUnfilteredStream();
byte[] buf = new byte[2048];
int n;
while ((n = in.read(buf)) > 0) {
out.write(buf, 0, n);
}
out.flush();
}
out.close();
concatStream.setFilters(COSName.FLATE_DECODE);
return concatStream;
}
private List<COSStream> createContentStreamList(COSBase contents) throws
IOException {
List<COSStream> contentStreams = new ArrayList<COSStream>();
if (contents instanceof COSStream) {
contentStreams.add((COSStream) contents);
} else if (contents instanceof COSArray) {
for (COSBase item : (COSArray) contents) {
contentStreams.addAll(createContentStreamList(item));
}
} else if (contents instanceof COSObject) {
contentStreams.addAll(createContentStreamList(((COSObject)
contents).getObject()));
} else {
throw new IOException("Contents are unknown type:" +
contents.getClass().getName());
}
return contentStreams;
}
private void processPages(List<?> pages) throws IOException {
int pageCount = 0;
for (Object p : pages) {
PDPage page = (PDPage) p;
COSDictionary pageDictionary = page.getCOSDictionary();
COSBase contents =
pageDictionary.getDictionaryObject(COSName.CONTENTS);
COSArray contentArray = new COSArray();
switch (position) {
case FOREGROUND:
// save state
contentArray.add(createStream("q\n"));
// original content
if (contents instanceof COSStream) {
contentArray.add(contents);
} else if (contents instanceof COSArray) {
contentArray.addAll((COSArray) contents);
} else {
throw new IOException("Contents are unknown type:" +
contents.getClass().getName());
}
// restore state
contentArray.add(createStream("Q\n"));
// overlay content
overlayPage(contentArray, page, pageCount + 1, pages.size());
break;
case BACKGROUND:
// overlay content
overlayPage(contentArray, page, pageCount + 1, pages.size());
// original content
if (contents instanceof COSStream) {
contentArray.add(contents);
} else if (contents instanceof COSArray) {
contentArray.addAll((COSArray) contents);
} else {
throw new IOException("Contents are unknown type:" +
contents.getClass().getName());
}
break;
}
pageDictionary.setItem(COSName.CONTENTS, contentArray);
pageCount++;
}
}
private void overlayPage(COSArray array, PDPage page, int pageNumber, int
numberOfPages) throws IOException {
LayoutPage layoutPage = null;
if (specificPageOverlayPage.containsKey(pageNumber)) {
layoutPage = specificPageOverlayPage.get(pageNumber);
} else if ((pageNumber == 1) && (firstPageOverlayPage != null)) {
layoutPage = firstPageOverlayPage;
} else if ((pageNumber == numberOfPages) && (lastPageOverlayPage !=
null)) {
layoutPage = lastPageOverlayPage;
} else if ((pageNumber % 2 == 1) && (oddPageOverlayPage != null)) {
layoutPage = oddPageOverlayPage;
} else if ((pageNumber % 2 == 0) && (evenPageOverlayPage != null)) {
layoutPage = evenPageOverlayPage;
} else if (defaultOverlayPage != null) {
layoutPage = defaultOverlayPage;
}
if (layoutPage != null) {
PDResources resources = page.findResources();
if (resources == null) {
resources = new PDResources();
page.setResources(resources);
}
String xObjectId = createOverlayXObject(page, layoutPage,
layoutPage.contentStream);
array.add(createOverlayStream(page, layoutPage, xObjectId));
}
}
private String createOverlayXObject(PDPage page, LayoutPage layoutPage,
COSStream contentStream) {
PDResources resources = page.findResources();
// determine new ID
COSDictionary dict = (COSDictionary)
resources.getCOSDictionary().getDictionaryObject(COSName.XOBJECT);
if (dict == null) {
dict = new COSDictionary();
resources.getCOSDictionary().setItem(COSName.XOBJECT, dict);
}
int id = 0;
while (dict.getDictionaryObject(XOBJECT_PREFIX + id) != null) {
id++;
}
String xObjectId = XOBJECT_PREFIX + id;
// wrap the layout content in a BBox and add it to page
COSStream xobj = contentStream;
xobj.setItem(COSName.RESOURCES, layoutPage.resources);
xobj.setItem(COSName.TYPE, COSName.XOBJECT);
xobj.setItem(COSName.SUBTYPE, COSName.FORM);
xobj.setInt(COSName.FORMTYPE, 1);
COSArray matrix = new COSArray();
matrix.add(COSInteger.get(1));
matrix.add(COSInteger.get(0));
matrix.add(COSInteger.get(0));
matrix.add(COSInteger.get(1));
matrix.add(COSInteger.get(0));
matrix.add(COSInteger.get(0));
xobj.setItem(COSName.MATRIX, matrix);
COSArray bbox = new COSArray();
bbox.add(COSInteger.get(0));
bbox.add(COSInteger.get(0));
bbox.add(COSInteger.get((int) layoutPage.mediaBox.getWidth()));
bbox.add(COSInteger.get((int) layoutPage.mediaBox.getHeight()));
xobj.setItem(COSName.BBOX, bbox);
dict.setItem(xObjectId, xobj);
return xObjectId;
}
private COSStream createOverlayStream(PDPage page, LayoutPage layoutPage,
String xObjectId) throws IOException {
// create new content stream that executes the XObject content
PDRectangle pageMediaBox = page.getMediaBox();
float scale = 1;
float hShift = (pageMediaBox.getWidth() -
layoutPage.mediaBox.getWidth()) / 2.0f;
float vShift = (pageMediaBox.getHeight() -
layoutPage.mediaBox.getHeight()) / 2.0f;
return createStream("q\nq " + scale + " 0 0 " + scale + " " + hShift +
" " + vShift + " cm /" + xObjectId + " Do Q\nQ\n");
}
private COSStream createStream(String content) throws IOException {
COSStream stream = new COSStream(new RandomAccessBuffer());
OutputStream out = stream.createUnfilteredStream();
out.write(content.getBytes("ISO-8859-1"));
out.close();
stream.setFilters(COSName.FLATE_DECODE);
return stream;
}
}