Hi all

As part of some ground-testing work I've been doing for pdf embedding enhancements in Apache FOP, I've modified PDFBox's Overlay.java to support translation, rotation and scaling of the PDF being overlaid.

This may be a useful feature for PDFBox, so I thought I'd send it in.

Right now it uses the target PDF document's co-ordinate units for translations, so it's hard for users to know what translation to specify without trial and error. I'm not sure what the best way to remedy this is to make for a better user interface, but I suspect that adding the ability to set an origin (TL, BL, TR, BR, or center) that'd help. That way it's easier for them to say "position the overlay on the top left" for example. The actual translation amount could be given as a percentage of the target document size in that axis, maybe, or accept different unit suffixes like "%" or "cm". I'm not sure what the best approach is, and I won't need to explore it for my current project, but it should be easy enough to add.

I hope this is useful. If you'd prefer the patch to be added to a tracker, let me know.

diffstat:

Overlay.java | 168 +++++++++++++++++++++++++++++++++++++++++++----------------
 1 file changed, 123 insertions(+), 45 deletions(-)


--
Craig Ringer


POST Newspapers
276 Onslow Rd, Shenton Park
Ph: 08 9381 3088     Fax: 08 9388 2258
ABN: 50 008 917 717
http://www.postnewspapers.com.au/
diff --git a/pdfbox/src/main/java/org/apache/pdfbox/Overlay.java b/pdfbox/src/main/java/org/apache/pdfbox/Overlay.java
index bb0b255..775b696 100644
--- a/pdfbox/src/main/java/org/apache/pdfbox/Overlay.java
+++ b/pdfbox/src/main/java/org/apache/pdfbox/Overlay.java
@@ -16,6 +16,8 @@
  */
 package org.apache.pdfbox;
 
+import java.awt.geom.AffineTransform;
+import java.io.*;
 import org.apache.pdfbox.cos.COSArray;
 import org.apache.pdfbox.cos.COSBase;
 import org.apache.pdfbox.cos.COSDictionary;
@@ -30,18 +32,13 @@ import org.apache.pdfbox.pdmodel.PDDocumentCatalog;
 import org.apache.pdfbox.pdmodel.PDPage;
 import org.apache.pdfbox.pdmodel.PDResources;
 
-import java.io.FileInputStream;
-import java.io.FileOutputStream;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.ByteArrayOutputStream;
-import java.io.OutputStream;
 
 import java.util.ArrayList;
 import java.util.Iterator;
 import java.util.List;
 import java.util.TreeMap;
 import java.util.Map;
+import org.apache.pdfbox.cos.*;
 
 /**
  * Overlay on document with another one.<br>
@@ -82,9 +79,8 @@ public class Overlay
     private PDDocument pdfOverlay;
     private PDDocument pdfDocument;
     private int pageCount = 0;
-    private COSStream saveGraphicsStateStream;
-    private COSStream restoreGraphicsStateStream;
-
+    private COSStream saveGraphicsStateStream, restoreGraphicsStateStream, transformCoordsStream;
+    
     /**
      * This will overlay a document and write out the results.<br/><br/>
      *
@@ -97,38 +93,90 @@ public class Overlay
      */
     public static void main( String[] args ) throws IOException, COSVisitorException
     {
-        if( args.length != 3 )
+        if( args.length < 3 )
         {
             usage();
             System.exit(1);
         }
-        else
-        {
-            PDDocument overlay = null;
-            PDDocument pdf = null;
+        
+        double  tx = 0d, ty = 0d,
+                xscale = 1d, yscale = 1d,
+                rotate = 0d;
+        
+        for (int i = 0; i < args.length - 3; i++) {
+            String opt = args[i], optVal = null;
+            if (!opt.startsWith(("--"))) {
+                System.err.println("Too many non-option arguments");
+                usage();
+                System.exit(1);
+            }
+            int separator = opt.indexOf('=');
+            if (separator > 0) {
+                optVal = opt.substring(separator+1, opt.length());
+                opt = opt.substring(2, separator);
+            }
+            try {
+                if (opt.equals("x")) {
+                    tx = Double.parseDouble(optVal);
+                } else if (opt.equals("y")) {
+                    ty = Double.parseDouble(optVal);
+                } else if (opt.equals("xs")) {
+                    xscale = Double.parseDouble(optVal);
+                } else if (opt.equals("ys")) {
+                    yscale = Double.parseDouble(optVal);
+                } else if (opt.equals("r")) {
+                    rotate = Double.parseDouble(optVal);
+                } else {
+                    System.err.println("Unknown command-line option --" + opt);
+                    usage();
+                    System.exit(1);
+                }
+            } catch (NullPointerException ex) {
+                System.err.println("Option " + opt + " requires a value after the = sign");
+                usage();
+                System.exit(1);
+            } catch (NumberFormatException ex) {
+                System.err.println("Option " + opt + ": could not parse number " + optVal + ": " + ex);
+                usage();
+                System.exit(1);
+            }
+        }
+        if (xscale == 0 || yscale == 0) {
+            System.err.println("WARNING: X and/or Y scaling factors are zero. Overlay will be invisible.");            
+        }
+        AffineTransform transform = AffineTransform.getTranslateInstance(tx, ty);
+        transform.concatenate(AffineTransform.getRotateInstance(- Math.toRadians(rotate)));
+        transform.concatenate(AffineTransform.getScaleInstance(xscale, yscale));
+        
+        String overlayFile = args[args.length - 3];
+        String targetFile = args[args.length - 2];
+        String outputFile = args[args.length - 1];
+        
+        // Now do the actual overlay work.
+        PDDocument overlay = null;
+        PDDocument pdf = null;
 
-            try
+        try
+        {
+            overlay = getDocument( overlayFile );
+            pdf = getDocument( targetFile );
+            Overlay overlayer = new Overlay();
+            overlayer.overlay( overlay, transform, pdf);
+            writeDocument( pdf, outputFile );
+        }
+        finally
+        {
+            if( overlay != null )
             {
-                overlay = getDocument( args[0] );
-                pdf = getDocument( args[1] );
-                Overlay overlayer = new Overlay();
-                overlayer.overlay( overlay, pdf );
-                writeDocument( pdf, args[2] );
+                overlay.close();
             }
-            finally
+            if( pdf != null )
             {
-                if( overlay != null )
-                {
-                    overlay.close();
-                }
-                if( pdf != null )
-                {
-                    pdf.close();
-                }
+                pdf.close();
             }
         }
     }
-
+            
     private static void writeDocument( PDDocument pdf, String filename ) throws IOException, COSVisitorException
     {
         FileOutputStream output = null;
@@ -176,7 +224,16 @@ public class Overlay
 
     private static void usage()
     {
-        System.err.println( "usage: java -jar pdfbox-app-x.y.z.jar Overlay <overlay.pdf> <document.pdf> <result.pdf>" );
+        System.err.println("usage: java -jar pdfbox-app-x.y.z.jar Overlay [opts] <overlay.pdf> <document.pdf> <result.pdf>" );
+        System.err.println("");
+        System.err.println("\t--x=n  Offset of left edge of overlay from left edge of page.");
+        System.err.println("\t--y=n  Offset of bottom edge of overlay from bottom edge of page.");
+        System.err.println("\t--xs=n Horizontal scale factor, 1 (default) = no scaling");
+        System.err.println("\t--ys=n Vertical scale factor, 1 (default) = no scaling");
+        System.err.println("\t--r=n  Clockwise rotation.");
+        System.err.println("");
+        System.err.println("\tLengths are in target page units. Rotation is in degrees.");
+        System.err.println("\tScale is a multiplier. Negative scale factors mirror that axis.");
     }
 
     /**
@@ -215,31 +272,46 @@ public class Overlay
      *
      * @throws IOException If there is an error accessing data.
      */
-    public PDDocument overlay( PDDocument overlay, PDDocument destination ) throws IOException
+    public PDDocument overlay( PDDocument overlay, AffineTransform transform, PDDocument destination) throws IOException
     {
         pdfOverlay = overlay;
         pdfDocument = destination;
 
         PDDocumentCatalog overlayCatalog = pdfOverlay.getDocumentCatalog();
         collectLayoutPages( overlayCatalog.getAllPages() );
-
-        COSDictionary saveGraphicsStateDic = new COSDictionary();
-        saveGraphicsStateStream = new COSStream( saveGraphicsStateDic, pdfDocument.getDocument().getScratchFile() );
-        OutputStream saveStream = saveGraphicsStateStream.createUnfilteredStream();
-        saveStream.write( " q\n".getBytes("ISO-8859-1") );
-        saveStream.flush();
-
-        restoreGraphicsStateStream = new COSStream( saveGraphicsStateDic, pdfDocument.getDocument().getScratchFile() );
-        OutputStream restoreStream = restoreGraphicsStateStream.createUnfilteredStream();
-        restoreStream.write( " Q\n".getBytes("ISO-8859-1") );
-        restoreStream.flush();
-
-
+        
+        saveGraphicsStateStream = makeStreamFromBytes(pdfDocument, " q\n".getBytes("ISO-8859-1"));
+        restoreGraphicsStateStream = makeStreamFromBytes(pdfDocument, " Q\n".getBytes("ISO-8859-1"));
+        transformCoordsStream = makeStreamFromBytes(pdfDocument, affineTransformToCM(transform));
+        
         PDDocumentCatalog pdfCatalog = pdfDocument.getDocumentCatalog();
         processPages( pdfCatalog.getAllPages() );
 
         return pdfDocument;
     }
+    
+    private COSStream makeStreamFromBytes(PDDocument doc, byte[] streamBytes) throws IOException {
+        COSStream st = new COSStream( doc.getDocument().getScratchFile() );
+        OutputStream restoreStream = st.createUnfilteredStream();
+        restoreStream.write(streamBytes);
+        restoreStream.flush();
+        return st;
+    }
+    
+    private byte[] affineTransformToCM(AffineTransform transform) throws UnsupportedEncodingException {
+        // PDF expects the matrix to be given as the 6 variable parts,
+        // as standalone numbers, in the order:
+        //   . . . . xtranslate ytranslate
+        
+        double[] matrix = new double[6];
+        transform.getMatrix(matrix);
+        StringBuilder b = new StringBuilder();
+        for (int i = 0; i < 6; i++){
+            b.append(matrix[i]).append(' ');
+        }
+        b.append("cm\n");
+        return b.toString().getBytes("ISO-8859-1");
+    }
 
     private void collectLayoutPages( List pages) throws IOException
     {
@@ -276,6 +348,7 @@ public class Overlay
         }
     }
 
+    // FIXME: This is a scary, scary hack
     private COSStream makeUniqObjectNames(Map objectNameMap, COSStream stream) throws IOException
     {
         ByteArrayOutputStream baos = new ByteArrayOutputStream(10240);
@@ -420,9 +493,14 @@ public class Overlay
         //<save graphics state>
         //<all existing content streams>
         //<restore graphics state>
+        //<translate, rotate, scale> (cm operator)
         //<overlay content>
+        //
+        // TODO: Clip overlay document to its transformed clip region, PDF:2008 8.5.4
+
         array.add(0, saveGraphicsStateStream );
         array.add( restoreGraphicsStateStream );
+        array.add( transformCoordsStream );
         array.add(layoutPage.contents);
     }
 
-- 
1.7.7.6

Reply via email to