Skip navigation

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

Leave a Reply