The previous version works well enough, but it’s fairly inefficient memory-wise and uses a rather strange method of moving data around using streams within the class. So, now we have this one:
/* * A Simple PDF Stamper using PDFBox * * July, 2013 * @author jack */ package simplestamper; import java.awt.Color; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.lang.reflect.Field; import java.util.List; import java.util.Properties; import org.apache.pdfbox.exceptions.COSVisitorException; import org.apache.pdfbox.exceptions.CryptographyException; import org.apache.pdfbox.pdmodel.PDDocument; import org.apache.pdfbox.pdmodel.PDPage; import org.apache.pdfbox.pdmodel.common.PDRectangle; import org.apache.pdfbox.pdmodel.edit.PDPageContentStream; import org.apache.pdfbox.pdmodel.font.PDFont; import org.apache.pdfbox.pdmodel.font.PDType1Font; /** * * @author jack */ public class SimpleStamper { private static int numPages; private static Properties config = new Properties(); private static String configFile; private static String outputFN = null; private static String inputFN = null; private static String stampString = null; private static String fontFamily = null; private static Float fontSize = null; private static Integer textRot = null; private static Color color = null; private static Boolean invertY = false; /** * Our Constructor */ public SimpleStamper() { super(); } /** * The main class. It's what plants crave. * * @param args the command line arguments args[0] -> The config file args[1] * -> The PDF document to be stamped args[2] -> The string to stamp * (optional, falls back to ss.text in the config file) args[3] -> The * desired name and path of the stamped PDF (optional, also defined in * config file as ss.outputFN) */ public static void main(String[] args) throws IOException, CryptographyException, COSVisitorException, NoSuchFieldException, IllegalArgumentException, IllegalAccessException { // as if we were outside... SimpleStamper app = new SimpleStamper(); // if we don't have any parameters... if (args.length == 0) { usage(); } else { // the config file path configFile = args[0]; // load the config loadConfig(configFile, args); } // stamp app.stamp(); } /** * Loads the configuration data. * Note: If we add many more config values, this function should be revised as a fetching routine. * * @param configFile * @param args * @throws NoSuchFieldException * @throws IllegalArgumentException * @throws IllegalAccessException */ public static void loadConfig(String configFile, String[] args) throws NoSuchFieldException, IllegalArgumentException, IllegalAccessException { // the input stream InputStream is; try { is = new FileInputStream(configFile); try { // try to load the config file config.load(is); is.close(); } catch (IOException e) { System.out.println(e); } } catch (IOException e) { System.out.println(e); } // the input PDF filename/path if (args.length > 1) { inputFN = args[1]; } else { // make sure we have an input filename try { inputFN = config.getProperty("ss.inputFN"); } catch (NullPointerException e) { System.err.println("You must specify an input filename as the second argument, or in the properties file. (ss.inputFN)"); } } // the string to be stamped if (args.length > 2) { stampString = args[2]; } else { // make sure we have a string to stamp try { stampString = config.getProperty("ss.text"); } catch (NullPointerException e) { System.err.println("You must specify a string to stamp as the third argument, or in the properties file. (ss.text)"); } } // the output PDF filename/path if (args.length > 3) { outputFN = args[3]; } else { // make sure we have an output filename try { outputFN = config.getProperty("ss.outputFN"); } catch (NullPointerException e) { System.err.println("You must specify an output filename as the fourth argument, or in the properties file. (ss.outputFN)"); } } // make sure we have a font from the prop file try { fontFamily = config.getProperty("ss.fontFamily"); } catch (NullPointerException e) { System.err.println("You must specify a font in the properties file. (ss.fontFamily)"); } // make sure we have a font size from the prop file try { fontSize = Float.parseFloat(config.getProperty("ss.fontSize")); } catch (NullPointerException e) { System.err.println("You must specify a font size in the properties file. (ss.fontSize)"); } // text rotation // make sure we have a font size from the prop file try { textRot = Integer.parseInt(config.getProperty("ss.rotation")); } catch (NullPointerException e) { System.err.println("You must specify a rotation in the properties file. (ss.rotation)"); } // text color. if not set in the properties file we default to black try { Field field = Color.class.getField(config.getProperty("ss.fontColor")); color = (Color) field.get(null); } catch (NullPointerException e) { System.err.println("You must specify a font color in the properties file. (ss.fontColor)"); } // the Y value inversion bool. Do we calc yVal from the top or bottom of the page? invertY = "true".equals(config.getProperty("ss.invertY")) ? true : false; } /** * The Stamping function. Where the magic lives. * * @throws IOException * @throws COSVisitorException */ public void stamp() throws IOException, COSVisitorException { // the document PDDocument doc = null; try { // load the incoming document into a PDDcoument doc = PDDocument.load(inputFN); // make a list array of all the pages List allPages = doc.getDocumentCatalog().getAllPages(); // Create a new font object selecting one of the PDF base fonts PDFont font = PDType1Font.getStandardFont(fontFamily); // the x/y coords float xVal = Float.parseFloat(config.getProperty("ss.xVal")); float yVal = Float.parseFloat(config.getProperty("ss.yVal")); // for every page in the incoming doc, stamp for (int i = 0; i < allPages.size(); i++) { // create an empty page and a geo object to use for calcs PDPage page = (PDPage) allPages.get(i); PDRectangle pageSize = page.findMediaBox(); // are we inverting the y axis? if (invertY) { yVal = pageSize.getHeight() - yVal; } // calculate the width of the string according to the font float stringWidth = font.getStringWidth(stampString) * fontSize / 1000f; // determine the rotation stuff. Is the the loaded page in landscape mode? (for axis and string dims) int pageRot = page.findRotation(); boolean pageRotated = pageRot == 90 || pageRot == 270; // are we rotating the text? boolean textRotated = textRot != 0 || textRot != 360; // calc the diff of rotations so the text stamps int totalRot = pageRot - textRot; // calc the page dimensions float pageWidth = pageRotated ? pageSize.getHeight() : pageSize.getWidth(); float pageHeight = pageRotated ? pageSize.getWidth() : pageSize.getHeight(); // determine the axis of rotation double centeredXPosition = pageRotated ? pageHeight / 2f : (pageWidth - stringWidth) / 2f; double centeredYPosition = pageRotated ? (pageWidth - stringWidth) / 2f : pageHeight / 2f; // append the content to the existing stream PDPageContentStream contentStream = new PDPageContentStream(doc, page, true, true, true); contentStream.beginText(); // set font and font size contentStream.setFont(font, fontSize); // set the stroke (text) color contentStream.setNonStrokingColor(color); // if we are rotating, do it if (pageRotated) { // rotate the text according to the calculations above contentStream.setTextRotation(Math.toRadians(totalRot), centeredXPosition, centeredYPosition); } else if (textRotated) { // rotate the text according to the calculations above contentStream.setTextRotation(Math.toRadians(textRot), xVal, yVal); } else { // no rotate, just move it. contentStream.setTextTranslation(xVal, yVal); } // stamp the damned text already contentStream.drawString(stampString); // close and clean up contentStream.endText(); contentStream.close(); } doc.save(outputFN); } finally { // you know, PHP 5.5 now has a finally construct... // if the document isnt closed, do so if (doc != null) { doc.close(); } } } /** * Attempts to provide basic usage info. */ private static void usage() { System.err.println("usage: java -jar simplestamper \"PdfToStamp.pdf\" \"The text to stamp, in parenthesis.\" \"Output.pdf\""); } }
And the config file:
# the properties for each event #The text to be stamped. ss.text=This is the text to be stamped. #The x value of the stamped text, measured in pixels with the origin at bottom-left. ss.xVal=100 #The y value of the stamped text, measured in pixels with the origin at bottom-left. ss.yVal=200 # The counterclockwise rotation of the text in degrees (converted to radians in the app) ss.rotation=0 #The name/path of the input file ss.inputFN=pdf.pdf # the filename of the stamped pdf ss.outputFN=output.pdf # FONT OPTIONS: # more info: http://pdfbox.apache.org/apidocs/org/apache/pdfbox/pdmodel/font/PDType1Font.html # # Helvetica-BoldOblique # Times-Italic # ZapfDingbats # Symbol # Helvetica-Oblique # Courier # Helvetica-Bold # Helvetica # Courier-Oblique # Times-BoldItalic # Courier-Bold # Times-Roman # Times-Bold # Courier-BoldOblique ss.fontFamily=Helvetica ss.fontSize=18 # # The font color. # Examples: blue, red, green, black, white, yelllow, cyan, gray, lightGray # More Info: http://docs.oracle.com/javase/7/docs/api/java/awt/Color.html ss.fontColor=black # Operational Vars # # Do we want to calc text placement from the top (true) or bottom (false) of the page? # (The PDF spec. dictates that we calc all y values starting from the bottom, this is why top = true) ss.invertY=true