Content protection is a key concern for many online services, and watermarking downloaded documents with a unique ID is one way to discourage and track unauthorized sharing. This article describes how to use Stingray to uniquely watermark every PDF document served from a web site.
In this example, Stingray will run a Java Extension to process all outgoing PDF documents from the web sites it is managing. The Java Extension can watermark each download with a custom message, including details such as the IP address, time of day and authentication credentials (if available) of the client:
The extension then encrypts the PDF document to make it difficult to remove the watermark.
Quick Start
Upload the attached PdfWatermark.jar file to your Java Extensions Catalog in Stingray Traffic Manager:
Create the following 'PDFWatermark' rule and apply it as a response rule to your virtual server:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
if ( http.getresponseheader( "Content-Type" ) != "application/pdf" ) break;
java.run( "PdfWatermark" ,
"x" , 10,
"y" , 20,
"textAlpha" , 30,
"textSize" , 40,
"textColor" , "0xff7f00" ,
"drawText" , "Downloaded by " .request.getRemoteIP(),
"textSize" , 26,
"drawText" , sys. gmtime . format ( "%a, %d %b %Y %T GMT" ),
"textSize" , 14,
"drawText" , http.getHostHeader() . http.getPath(),
"x" , 40,
"y" , 25,
"textAlpha" , 70,
"textColor" , "0xcccccc" ,
"textSize" , 16,
"textAngle" , 0,
"drawText" , "Copyright " .sys. time .year(),
"drawText" , "For restricted distribution"
);
Download a PDF document from your website, managed by the virtual server configured above. Verify that the PDF document has been watermarked with the URL, client IP address, and time of download.
Troubleshooting
The Java Extension applies the watermark to PDF documents, and then encrypts them to make the watermark difficult to remove.
The Java Extension will not be able to apply a watermark to PDF documents that are already encrypted, or which are served with a mime type that does not begin ‘application/pdf’.
Customizing the extension
The behaviour of the extension is controlled by the parameters passed into the Java extension by the ‘ java.run() ’ function.
The following example applies a simple watermark:
1
2
3
4
5
6
7
8
9
10
11
if ( http.getresponseheader( "Content-Type" ) != "application/pdf" ) break;
$msg1 = http.getHostHeader() . http.getPath();
$msg2 = "Downloaded by " .http.getRemoteIP();
$msg3 = sys. gmtime . format ( "%a, %d %b %Y %T GMT" );
java.run( "PdfWatermark" ,
"drawText" , $msg1 ,
"drawText" , $msg2 ,
"drawText" , $msg3 ,
);
Advanced use of the Java Extension
This Java Extension takes a list of commands to control how and where it applies the watermark text:
Command
Notes
Default
x
As a percentage between 0 and 100; places the cursor horizontally on the page.
30
y
As a percentage between 0 and 100; places the cursor vertically on the page.
30
textAngle
In degrees, sets the angle of the text. 0 is horizontal (left to right); 90 is vertical (upwards). The special value "auto" sets the text angle from bottom-left to top-right in accordance with the aspect ratio of the page.
“auto”
textAlign
Value is "L" (left), "R" (right), or "C" (center); controls the alignment of the text relative to the cursor placement.
“L”
textAlpha
As a percentage, sets the alpha of the text when drawn with drawText."0" is completely transparent, "100" is solid (opaque).
75
textColor
The color of the text when it is drawn with drawText, as hex value in a string.
“0xAAAAAA”
textSize
In points, sets the size of the text when it is drawn with drawText.
20
drawText
Draw the value (string) using the current cursor placement and text attributes; automatically moves the cursor down one line so that multiple lines of text can be rendered with successive calls to drawText.
Dependencies and Licenses
For convenience, the .jar extension contains the iText 5.4.0 library from iText software corp (http://www.itextpdf.com) and the bcprov-148 and bcmail-148 libraries from The Legion of the Bouncy Castle (http://www.bouncycastle.org), in addition to the PdfWatermark.class file. The jar file was packaged using JarSplice (http://ninjacave.com/jarsplice).
Building the extension from source
If you'd like to build the Java Extension from source, here's the code:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
import java.awt.Color;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.Hashtable;
import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import com.itextpdf.text.BaseColor;
import com.itextpdf.text.pdf.BaseFont;
import com.itextpdf.text.pdf.PdfContentByte;
import com.itextpdf.text.pdf.PdfGState;
import com.itextpdf.text.pdf.PdfReader;
import com.itextpdf.text.pdf.PdfStamper;
import com.itextpdf.text.pdf.PdfWriter;
import com.zeus.ZXTMServlet.ZXTMHttpServletResponse;
public class PdfWatermark extends HttpServlet {
private static final long serialVersionUID = 1L;
Hashtable<String, String> defaults = new Hashtable<String, String>();
public void init(ServletConfig config) throws ServletException {
super.init(config);
// Initialize defaults. These are 'commands' that are run before any commands
// passed in to the extension through the args list
defaults.put( "x" , "30" );
defaults.put( "y" , "30" );
defaults.put( "textAngle" , "auto" );
defaults.put( "textAlign" , "L" );
defaults.put( "textAlpha" , "75" );
defaults.put( "textSize" , "20" );
defaults.put( "textColor" , "0xAAAAAA" );
// Read any values defined in the ZXTM configuration for this class
// to override the defaults
Enumeration<String> e = defaults. keys ();
while (e.hasMoreElements()) {
String k = e.nextElement();
String v = config.getInitParameter(k);
if (v != null)
defaults.put(k, v);
}
}
public void doGet(HttpServletRequest req, HttpServletResponse res)
throws ServletException, IOException {
try {
ZXTMHttpServletResponse zres = (ZXTMHttpServletResponse) res;
String ct = zres.getHeader( "Content-Type" );
if (ct == null || !ct.startsWith( "application/pdf" ))
return ;
// process args
String[] args = (String[]) req.getAttribute( "args" );
if (args == null)
throw new Exception( "Missing argument list" );
if (args. length % 2 != 0)
throw new Exception(
"Malformed argument list (expected even number of args)" );
ArrayList<String[]> actions = new ArrayList<String[]>();
Enumeration<String> e = defaults. keys ();
while (e.hasMoreElements()) {
String k = e.nextElement();
actions.add(new String[] { k, defaults.get(k) });
}
for ( int i = 0; i < args. length ; i += 2) {
actions.add(new String[] { args[i], args[i + 1] });
}
InputStream is = zres.getInputStream();
OutputStream os = zres.getOutputStream();
PdfReader reader = new PdfReader(is);
int n = reader.getNumberOfPages();
PdfStamper stamp = new PdfStamper(reader, os);
stamp.setEncryption(
PdfWriter.STANDARD_ENCRYPTION_128 | PdfWriter.DO_NOT_ENCRYPT_METADATA,
null, null,
PdfWriter.ALLOW_PRINTING | PdfWriter.ALLOW_COPY
| PdfWriter.ALLOW_FILL_IN | PdfWriter.ALLOW_SCREENREADERS
| PdfWriter.ALLOW_DEGRADED_PRINTING);
for ( int i = 1; i <= n; i++) {
PdfContentByte pageContent = stamp.getOverContent(i);
com.itextpdf.text.Rectangle pageSize = reader
.getPageSizeWithRotation(i);
watermarkPage(pageContent, actions, pageSize.getWidth(),
pageSize.getHeight());
}
stamp. close ();
} catch (Exception e) {
log (req.getRequestURI() + ": " + e.toString());
e.printStackTrace();
}
}
public void doPost(HttpServletRequest req, HttpServletResponse res)
throws ServletException, IOException {
doGet(req, res);
}
private void watermarkPage(PdfContentByte pageContent,
ArrayList<String[]> actions, float width, float height)
throws Exception {
float x = 0;
float y = 0;
double textAngle = 0;
int textAlign = PdfContentByte.ALIGN_CENTER;
int fontSize = 14;
pageContent.beginText();
for ( int i = 0; i < actions.size(); i++) {
String action = actions.get(i)[0];
String value = actions.get(i)[1];
if (action.equals( "x" )) {
x = Float.parseFloat(value) / 100 * width;
continue ;
}
if (action.equals( "y" )) {
y = Float.parseFloat(value) / 100 * height;
continue ;
}
if (action.equals( "textColor" )) {
Color c = Color.decode( value );
pageContent.setColorFill(
new BaseColor( c.getRed(), c.getGreen(), c.getBlue() ) );
continue ;
}
if (action.equals( "textAlpha" )) {
PdfGState gs1 = new PdfGState();
gs1.setFillOpacity(Float.parseFloat(value) / 100f);
pageContent.setGState(gs1);
continue ;
}
if (action.equals( "textAngle" )) {
if (value.equals( "auto" )) {
textAngle = (float) Math. atan2 (height, width);
} else {
textAngle = Math.toRadians( Double.parseDouble(value) );
}
continue ;
}
if (action.equals( "textAlign" )) {
if (value.equals( "L" ))
textAlign = PdfContentByte.ALIGN_LEFT;
else if (value.equals( "R" ))
textAlign = PdfContentByte.ALIGN_RIGHT;
else
textAlign = PdfContentByte.ALIGN_CENTER;
continue ;
}
if (action.equals( "textSize" )) {
fontSize = Integer.parseInt(value);
pageContent.setFontAndSize(BaseFont
.createFont(BaseFont.HELVETICA, BaseFont.WINANSI,
BaseFont.EMBEDDED), fontSize);
continue ;
}
// x,y is top left/center/right of text, so that when we move the
// cursor at the end of a line, we can cater for subsequent fontSize
// changes
if (action.equals( "drawText" )) {
pageContent.showTextAligned(textAlign, value,
(float) (x + fontSize * Math. sin (textAngle)),
(float) (y - fontSize * Math. cos (textAngle)),
(float) Math.toDegrees(textAngle));
x += fontSize * 1.2 * Math. sin (textAngle);
y -= fontSize * 1.2 * Math. cos (textAngle);
continue ;
}
throw new Exception( "Unknown command '" + action + "'" );
}
pageContent.endText();
}
}
Compile against the Stingray servlet libraries (see Writing Java Extensions - an introduction), and the most recent versions of the iText library (http://www.itextpdf.com ) and the bcprov and bcmail libraries ( http://www.bouncycastle.org ):
$ javac -cp servlet.jar:zxtm-servlet.jar:bcprov-jdk15on-148.jar:\
bcmail-jdk15on-148.jar:itextpdf-5.4.0.jar PdfWatermark.java
You can then upload the generated PdfWatermark.class file and the three iText/bcmail/bcprov jar files to the Stingray Java Catalog.
Creating a Fat Jar
Alternatively, you can package the class files and their jar dependencies as a single Fat Jar (http://ninjacave.com/jarsplice):
1. Package the PdfWatermark.class file as a jar file
$ jar cvf PdfWatermark.jar PdfWatermark.class
2. Run JarSplice
$ java -jar ~/Downloads/jarsplice-0.40.jar
4. Set the main class:
5. Hit 'CREATE FAT JAR' to generate your single fat jar:
You can upload the resulting Jar file to the Stingray Java catalog, and Stingray will identify the PdfWatermark.class within.
View full article