How to print with "Comments Summary"

Acrobat has a tucked-away feature that allows you to print a version of the document with the contents of all the annotations displayed and referenced against their visual position in the document. This is useful for seeing at a glance what the contents of note annotations are, since you can't click on them to open them once the document is printed.

We can emulate this behaviour in the PDF Viewer by adding a custom feature. This feature will react to its event by taking the associated document and creating new document content dynamically with the information we need. We can then print the new document and discard it.

For each page in the source document, we will need to:

  1. Create a scaled-down version of the original page's appearance including all its annotations
  2. Create a new page in the target document and add the scaled down version to this page (so that we have some space to the side for displaying the comment text)
  3. Use a layout box to add the textual contents of each annotation
  4. Draw a line to connect each annotation's visual appearance with the contents we just created.

The key PDF Library API calls that we need to use to create the new page are:

  • PDFPage.drawCanvas
  • PDFPage.drawLayoutBox
  • PDFPage.drawLine

Finally we have to print the page. The interface between the Java printing system and the PDF Library is defined via the Printable interface, which we get for each page from a PDFParser object.

Sample source code for this is shown below. You can modify the scale and positions of the various elements, font size, colours etc as you see fit. The createComment methods allows you to decide which pieces of information from the annotation you want to display in the box and how.

import java.awt.Color;
import java.awt.print.*;
import java.io.File;
import java.text.DateFormat;
import java.security.*;
import java.util.*;
import javax.print.*;
import javax.print.attribute.*;
import javax.print.attribute.standard.*;
import javax.swing.*;
import org.faceless.pdf2.*;
import org.faceless.pdf2.viewer2.*;

public class PrintAnnotationComments extends ViewerWidget {

    static final PDFFont HELVETICA = new StandardFont(StandardFont.HELVETICA);
    static final PDFFont HELVETICABOLD = new StandardFont(StandardFont.HELVETICABOLD);
    static final DateFormat DATEFORMAT = DateFormat.getDateTimeInstance();

    public PrintAnnotationComments() {
        super("PrintAnnotationComments");
        setMenu("File\tPrintAnnotationComments");
    }

    public void action(ViewerEvent event) {
        // Here we create a new document that will store the "commentised"
        // version of the original
        PDF pdf = event.getPDF();
        final PDF commentPdf = new PDF();
        for (Iterator i = pdf.getPages().iterator(); i.hasNext(); ) {
            addPage(commentPdf, (PDFPage) i.next());
        }
        // Now print it
        try {
            AccessController.doPrivileged(new PrivilegedExceptionAction() {
                public Object run() throws PrintException, PrinterException {
                    print(commentPdf);
                    return null;
                }
            });
        } catch (PrivilegedActionException e) {
            Util.displayThrowable(e, event.getViewer());
        }
    }

    void addPage(PDF commentPdf, PDFPage sourcePage) {
        int sourceWidth = sourcePage.getWidth();
        int sourceHeight = sourcePage.getHeight();
        PDFPage commentPage = commentPdf.newPage(sourceWidth, sourceHeight);
        // First we will scale down the original page and position it on the
        // new page, aligned to the left but centred vertically.
        float scale = 0.75f;
        float scaledWidth = sourceWidth * scale;
        float scaledHeight = sourceHeight * scale;
        float yoffset = (sourceHeight - scaledHeight) / 2f;
        // Flatten all the annotations onto a clone of the page to get their
        // appearance
        PDFPage flattenedPage = new PDFPage(sourcePage);
        for (Iterator i = flattenedPage.getAnnotations().iterator(); i.hasNext(); ) {
            PDFAnnotation annot = (PDFAnnotation) i.next();
            annot.flatten();
        }
        flattenedPage.flush();
        PDFCanvas canvas = new PDFCanvas(flattenedPage);
        commentPage.drawCanvas(canvas, 0, yoffset, scaledWidth, yoffset + scaledHeight);
        // Next calculate the space for the layout boxes to hold the comment
        // information to the right of the page.
        float lbavailx = sourceWidth - scaledWidth;
        float lbwidth = lbavailx * 0.85f;
        float lbx = scaledWidth + ((lbavailx - lbwidth) / 2f);
        float lbheight = sourceHeight * 0.85f;
        float lby = sourceHeight - ((sourceHeight - lbheight) / 2f);
        float fontSize = 10f;
        PDFStyle lineStyle = new PDFStyle();
        lineStyle.setLineColor(Color.gray);
        // Now iterate over the annotations
        for (Iterator i = sourcePage.getAnnotations().iterator(); i.hasNext(); ) {
            PDFAnnotation annot = (PDFAnnotation) i.next();
            if ("Popup".equals(annot.getType())) {
                continue; // don't process Popup annotations
            }
            // First create the comment text in a layout box and add that
            LayoutBox box = new LayoutBox(lbwidth);
            createComment(annot, box, fontSize);
            commentPage.drawLayoutBox(box, lbx, lby);
            // Then draw a line from the annotation box top right to the
            // left of the comment (centred vertically on the first line)
            float[] rect = annot.getRectangle();
            if (rect != null) {
                float rx = rect[2], ry = rect[3];
                if (rx < sourceWidth && ry < sourceHeight) {
                    // Scale and translate to match the scaled version
                    rx = rx * scale;
                    ry = (ry * scale) + yoffset;
                    commentPage.setStyle(lineStyle);
                    commentPage.drawLine(rx, ry, lbx, lby - (fontSize / 2f));
                }
            }
            // Add some vertical whitespace
            lby -= box.getHeight() + fontSize;
        }
    }

    void createComment(PDFAnnotation annot, LayoutBox box, float fontSize) {
        PDFStyle normal = new PDFStyle();
        normal.setFont(HELVETICA, fontSize);
        normal.setFillColor(Color.black);

        PDFStyle bold = new PDFStyle();
        bold.setFont(HELVETICABOLD, fontSize);
        bold.setFillColor(Color.black);

        String author = annot.getAuthor();
        if (author == null) {
            author = "unknown";
        }
        Calendar date = annot.getCreationDate();
        String created = (date == null) ? "unknown" : DATEFORMAT.format(date.getTime());
        String contents = annot.getContents();

        box.addText("Author: ", bold, null);
        box.addText(author, normal, null);
        box.addText("\u00a0\u00a0\u00a0Date: ", bold, null);
        box.addText(created, normal, null);
        if (contents != null) {
            box.addLineBreak(normal);
            box.addText(contents, normal, null);
        }
        box.flush();
    }

    void print(PDF pdf) throws PrintException, PrinterException {
        final PDFParser parser = new PDFParser(pdf);
        PrintRequestAttributeSet atts = new HashPrintRequestAttributeSet();
        DocFlavor flavor = DocFlavor.SERVICE_FORMATTED.PAGEABLE;
        PrintService[] services = PrintServiceLookup.lookupPrintServices(flavor, atts);
        PrintService service = ServiceUI.printDialog(null, 50, 50, services, PrintServiceLookup.lookupDefaultPrintService(), flavor, atts);
        final PrinterJob job = PrinterJob.getPrinterJob();
        job.setPrintService(service);
        job.setPageable(new Pageable() {
            public int getNumberOfPages() {
                return parser.getNumberOfPages();
            }
            public Printable getPrintable(int pagenumber) {
                return parser.getPrintable(pagenumber);
            }
            public PageFormat getPageFormat(int pagenumber) {
                PageFormat format = parser.getPageFormat(pagenumber);
                Paper paper = job.defaultPage(format).getPaper();
                paper.setImageableArea(0, 0, paper.getWidth(), paper.getHeight());
                format.setPaper(paper);
                return format;
            }
        });
        job.print(atts);
    }

}