Watermarking a Report

Going beyond XML, and custom Blend Modes

The problem

We recently had a customer present us with the following problem: "we need to be able to apply a watermark to a Report that doesn't obscure the images, but we can't use transparent images".

Transparency, but without transparency. We like a challenge.

Beyond XML - using the PDF Library API

The first thing that was obvious is that we can't do this with the XML - the syntax just isn't flexible enough. The Report Generator functions as a layer on top of the PDF Library however, so it's a very simple matter to edit the PDF after it's been generated. Take this simple code

import java.io.*;
import java.awt.*;
import org.xml.sax.*;
import org.faceless.pdf2.*;
import org.faceless.report.*;

ReportParser parser = ReportParser.getInstance();
File file = new File("/path/to/file.xml");
InputSource in = new InputSource(new FileInputStream(file));
in.setSystemId(file.toURI().toString());
PDF pdf = parser.parse(in);
pdf.render(new FileOutputStream("out.pdf"));

(as an aside, we always recommend setting the SystemId on an InputSource so the ReportParser can resolve relative URLs in the XML).

If we want to edit that PDF in some fashion before we render it, just insert the code in the correct spot. For example, here's how to add some pages from an existing PDF - the new lines are highlighted.

ReportParser parser = ReportParser.getInstance();
File file = new File("/path/to/file.xml");
InputSource in = new InputSource(new FileInputStream(file));
in.setSystemId(file.toURI().toString());
PDF pdf = parser.parse(in);
PDF trailer = new PDF(new PDFReader(new File("disclaimer.pdf"));
pdf.getPages().addAll(trailer.getPages());
pdf.render(new FileOutputStream("out.pdf"));

(another aside - if you're doing this to multiple documents, it's quicker to load the trailer once at the beginning then add the pages from a clone of that document, eg pdf.getPages().addAll(((new PDF(trailer)).getPages());

If you need to do this from the Servlet Filter, the source code is supplied in the docs/extra folder of the Report Generator download, and is easy to follow. Remember to change the package name of your modified filter so it's no longer org.faceless.report2

Adding the watermark

In order to add a transparent-but-not-transparent watermark to the PDF, one approach which we think works quite well is setting a blend mode. This controls how the color from an object is merged with the existing color on the page - AWT experts might be familiar with the Composite class which does the same thing.

Normally, if you paint in red on a blue background you'll get red, but there are a whole raft of mathematical functions that can be used instead which are documented on the Adobe site. We're going to use "Multiply" with a gray of about 80%. Here's our code, new lines highlighted.

PDFStyle style = new PDFStyle();
style.setFillColor(new Color(0xCCCCCC, false));
style.setFont(new StandardFont(StandardFont.HELVETICA), 80);
style.setTextAlign(PDFStyle.TEXTALIGN_CENTER);
style.setBlendMode("Multiply");

ReportParser parser = ReportParser.getInstance();
File file = new File("/path/to/file.xml");
InputSource in = new InputSource(new FileInputStream(file));
in.setSystemId(file.toURI().toString());
PDF pdf = parser.parse(in);

for (int i=0;i<pdf.getNumberOfPages();i++) {
   PDFPage page = pdf.getPage(i);
   page.save();
   page.setStyle(style);
   float x = page.getWidth() / 2;
   float y = page.getHeight() / 2;
   page.rotate(x, y, 20);
   page.drawText("CONFIDENTIAL", x, y);
   page.restore();
}
pdf.render(new FileOutputStream("out.pdf"));

The "Multiply" blend mode will multiply the color with the color already on the page, so painting with 80% gray will darken the underlying paint to 80% of its original value. If you're painting over a dark background you can use a dark paint and the "Screen" mode to lighten it, and many other possibilities are available.

Conclusion

Editing a PDF in this fashion adds a huge amount of flexibilty to the Report Generator. It's also a great trick for working with very large documents - the original report XML can be split into smaller chunks, and the resulting PDFs concatenated together with the API to reduce the memory footprint.