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

Reply via email to