vADC Docs

Watermarking PDF documents with Stingray and Java Extensions

by on ‎03-20-2013 04:35 AM - edited on ‎06-02-2015 01:44 PM by PaulWallace (3,367 Views)

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.

 

pdfwatermark.png

 

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:

 

watermark.png

 

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:

374i4AA0A66727831D36.jpg

 

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  

 

fatjar1.png

4. Set the main class:

fatjar2.png

5. Hit 'CREATE FAT JAR' to generate your single fat jar:

fatjar3.png

You can upload the resulting Jar file to the Stingray Java catalog, and Stingray will identify the PdfWatermark.class within.

Contributors