nz.ac.waikato.cms.doc.SimplePDFOverlay.java Source code

Java tutorial

Introduction

Here is the source code for nz.ac.waikato.cms.doc.SimplePDFOverlay.java

Source

/*
 *   This program is free software: you can redistribute it and/or modify
 *   it under the terms of the GNU General Public License as published by
 *   the Free Software Foundation, either version 3 of the License, or
 *   (at your option) any later version.
 *
 *   This program is distributed in the hope that it will be useful,
 *   but WITHOUT ANY WARRANTY; without even the implied warranty of
 *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *   GNU General Public License for more details.
 *
 *   You should have received a copy of the GNU General Public License
 *   along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

/**
 * SimplePDFOverlay.java
 * Copyright (C) 2017 University of Waikato, Hamilton, NZ
 */

package nz.ac.waikato.cms.doc;

import com.itextpdf.text.BaseColor;
import com.itextpdf.text.Element;
import com.itextpdf.text.Font;
import com.itextpdf.text.FontFactory;
import com.itextpdf.text.Phrase;
import com.itextpdf.text.Utilities;
import com.itextpdf.text.pdf.ColumnText;
import com.itextpdf.text.pdf.PdfContentByte;
import com.itextpdf.text.pdf.PdfReader;
import com.itextpdf.text.pdf.PdfStamper;
import net.sourceforge.argparse4j.ArgumentParsers;
import net.sourceforge.argparse4j.inf.ArgumentParser;
import net.sourceforge.argparse4j.inf.Namespace;
import nz.ac.waikato.cms.core.FileUtils;
import nz.ac.waikato.cms.core.Utils;

import java.awt.Color;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.FileReader;
import java.util.logging.Logger;

/**
 * Uses a instructions from a text file for overlaying text on a PDF.
 * Use -h on the commandline to see more details on instructions set.
 *
 * @author FracPete (fracpete at waikato dot ac dot nz)
 * @version $Revision$
 */
public class SimplePDFOverlay {

    public static final String PDF = "pdf";

    public static final String INSTRUCTIONS = "instructions";

    public static final String OUTPUT = "output";

    public static final String PREFIX_COMMENT = "#";

    public static final String PREFIX_UNITS = "units:";

    public static final String PREFIX_PAGE = "page:";

    public static final String PREFIX_FONT = "font:";

    public static final String PREFIX_TEXT = "text:";

    public static final String PREFIX_LINE = "line:";

    public static final String PREFIX_RECT = "rect:";

    public static final String PREFIX_OVAL = "oval:";

    public static final String FORMAT_FONT = "<name> <size> <color>";

    public static final String FORMAT_TEXT = "<llx> <lly> <urx> <ury> <leading> <alignment> <text>";

    public static final String FORMAT_LINE = "<x1> <y1> <x2> <x2> <line-width> <stroke-color>";

    public static final String FORMAT_RECT = "<x> <y> <w> <h> <line-width> <stroke-color> [fill-color]";

    public static final String FORMAT_OVAL = "<x1> <y1> <x2> <y2> <line-width> <stroke-color> [fill-color]";

    public static final String VALUES_ALIGN = "UNDEFINED|LEFT|CENTER|RIGHT|JUSTIFIED";

    /** for logging. */
    protected Logger m_Logger;

    /** the pdf input. */
    protected File m_Pdf;

    /** the file with the instructions. */
    protected File m_Instructions;

    /** the pdf output. */
    protected File m_Output;

    /**
     * Initializes the overlay.
     *
     * @param pdf             the template to use
     * @param instructions    the text file with the instructions
     * @param output           the output directory for the generated files
     */
    public SimplePDFOverlay(File pdf, File instructions, File output) {
        if (!pdf.exists())
            throw new IllegalArgumentException("PDF does not exist: " + pdf);
        if (pdf.isDirectory())
            throw new IllegalArgumentException("PDF points to a directory: " + pdf);

        if (!instructions.exists())
            throw new IllegalArgumentException("Instructions file does not exist: " + instructions);
        if (instructions.isDirectory())
            throw new IllegalArgumentException("Instructions file points to a directory: " + instructions);

        if (output.isDirectory())
            throw new IllegalArgumentException("Output file points to a directory: " + output);

        m_Pdf = pdf;
        m_Instructions = instructions;
        m_Output = output;
        m_Logger = Logger.getLogger(this.getClass().getName());
    }

    /**
     * Parses the color string (#RRGGBB).
     *
     * @param str      the color string
     * @return      the color, BLACK if failed to parse
     */
    protected Color parseColor(String str) {
        Color result;

        str = str.replaceAll("#", "");
        if (str.length() == 6) {
            result = new Color(Integer.parseInt(str.substring(0, 2), 16), Integer.parseInt(str.substring(2, 4), 16),
                    Integer.parseInt(str.substring(4, 6), 16));
        } else {
            m_Logger.warning("Failed to parse color, falling back to black: " + str);
            result = Color.BLACK;
        }

        return result;
    }

    /**
     * Parses the alignment string.
     *
     * @param str      the alignment string
     * @return      the alignment, see {@link Element}
     */
    protected int parseAlignment(String str) {
        switch (str) {
        case "UNDEFINED":
            return Element.ALIGN_UNDEFINED;
        case "LEFT":
            return Element.ALIGN_LEFT;
        case "CENTER":
            return Element.ALIGN_CENTER;
        case "RIGHT":
            return Element.ALIGN_RIGHT;
        case "JUSTIFIED":
            return Element.ALIGN_JUSTIFIED;
        default:
            m_Logger.warning("Unhandled alignment, falling back to LEFT: " + str);
            return Element.ALIGN_LEFT;
        }
    }

    /**
     * Parses the location string.
     *
     * @param str      the string to parse
     * @param max      the maximum to use if position string is -1
     * @param units   the units the location is in (in|mm|pt)
     * @return      the position in points
     */
    protected float parseLocation(String str, float max, String units) {
        float result;

        result = Float.parseFloat(str);

        if (result >= 0) {
            switch (units) {
            case "mm":
                result = Utilities.millimetersToPoints(result);
                break;
            case "in":
                result = Utilities.inchesToPoints(result);
                break;
            case "pt":
                // nothing to do
                break;
            default:
                m_Logger.warning("Unknown units: " + units);
            }
        } else {
            result = max;
        }

        return result;
    }

    /**
     * Applies the instructions to the input PDF.
     *
     * @return      null if successful, otherwise error message
     */
    public String execute() {
        String result;
        String line;
        BufferedReader breader;
        FileReader freader;
        int i;
        int lineNo;
        String units;
        int pageNo;
        PdfReader reader;
        PdfStamper stamper;
        PdfContentByte cb;
        ColumnText ct;
        Font font;
        String[] parts;
        StringBuilder text;

        result = null;

        freader = null;
        breader = null;
        try {
            reader = new PdfReader(new FileInputStream(m_Pdf.getAbsolutePath()));
            stamper = new PdfStamper(reader, new FileOutputStream(m_Output.getAbsolutePath()));
            freader = new FileReader(m_Instructions);
            breader = new BufferedReader(freader);
            lineNo = 0;
            units = "pt";
            pageNo = 1;
            cb = stamper.getOverContent(pageNo);
            font = null;
            while ((line = breader.readLine()) != null) {
                lineNo++;
                if (line.trim().startsWith(PREFIX_COMMENT))
                    continue;
                if (line.trim().length() == 0)
                    continue;
                if (line.startsWith(PREFIX_UNITS)) {
                    units = line.substring(PREFIX_UNITS.length()).trim().toLowerCase();
                } else if (line.startsWith(PREFIX_PAGE)) {
                    pageNo = Integer.parseInt(line.substring(PREFIX_PAGE.length()).trim());
                    cb = stamper.getOverContent(pageNo);
                } else if (line.startsWith(PREFIX_FONT)) {
                    parts = line.substring(PREFIX_FONT.length()).trim().split(" ");
                    if (parts.length == 3)
                        font = FontFactory.getFont(parts[0], Float.parseFloat(parts[1]),
                                new BaseColor(parseColor(parts[2]).getRGB()));
                    else
                        m_Logger.warning("Font instruction not in expected format (" + FORMAT_FONT + "):\n" + line);
                } else if (line.startsWith(PREFIX_TEXT)) {
                    parts = line.substring(PREFIX_TEXT.length()).trim().split(" ");
                    if (parts.length >= 7) {
                        ct = new ColumnText(cb);
                        ct.setSimpleColumn(parseLocation(parts[0], reader.getPageSize(pageNo).getWidth(), units), // llx
                                parseLocation(parts[1], reader.getPageSize(pageNo).getHeight(), units), // lly
                                parseLocation(parts[2], reader.getPageSize(pageNo).getWidth(), units), // urx
                                parseLocation(parts[3], reader.getPageSize(pageNo).getHeight(), units), // ury
                                Float.parseFloat(parts[4]), // leading
                                parseAlignment(parts[5])); // alignment
                        text = new StringBuilder();
                        for (i = 6; i < parts.length; i++) {
                            if (text.length() > 0)
                                text.append(" ");
                            text.append(parts[i]);
                        }
                        if (font == null)
                            ct.setText(new Phrase(text.toString()));
                        else
                            ct.setText(new Phrase(text.toString(), font));
                        ct.go();
                    } else {
                        m_Logger.warning("Text instruction not in expected format (" + FORMAT_TEXT + "):\n" + line);
                    }
                } else if (line.startsWith(PREFIX_LINE)) {
                    parts = line.substring(PREFIX_LINE.length()).trim().split(" ");
                    if (parts.length >= 6) {
                        cb.saveState();
                        cb.setLineWidth(Float.parseFloat(parts[4])); // line width
                        cb.setColorStroke(new BaseColor(parseColor(parts[5]).getRGB())); // color
                        cb.moveTo(parseLocation(parts[0], reader.getPageSize(pageNo).getWidth(), units), // x
                                parseLocation(parts[1], reader.getPageSize(pageNo).getWidth(), units)); // y
                        cb.lineTo(parseLocation(parts[2], reader.getPageSize(pageNo).getWidth(), units), // w
                                parseLocation(parts[3], reader.getPageSize(pageNo).getWidth(), units)); // h
                        cb.stroke();
                        cb.restoreState();
                    } else {
                        m_Logger.warning("Line instruction not in expected format (" + FORMAT_LINE + "):\n" + line);
                    }
                } else if (line.startsWith(PREFIX_RECT)) {
                    parts = line.substring(PREFIX_RECT.length()).trim().split(" ");
                    if (parts.length >= 6) {
                        cb.saveState();
                        cb.rectangle(parseLocation(parts[0], reader.getPageSize(pageNo).getWidth(), units), // x
                                parseLocation(parts[1], reader.getPageSize(pageNo).getWidth(), units), // y
                                parseLocation(parts[2], reader.getPageSize(pageNo).getWidth(), units), // w
                                parseLocation(parts[3], reader.getPageSize(pageNo).getWidth(), units) // h
                        );
                        cb.setLineWidth(Float.parseFloat(parts[4])); // line width
                        cb.setColorStroke(new BaseColor(parseColor(parts[5]).getRGB())); // stroke
                        if (parts.length >= 7) {
                            cb.setColorFill(new BaseColor(parseColor(parts[6]).getRGB())); // fill
                            cb.fillStroke();
                        } else {
                            cb.stroke();
                        }
                        cb.restoreState();
                    } else {
                        m_Logger.warning(
                                "Rectangle instruction not in expected format (" + FORMAT_RECT + "):\n" + line);
                    }
                } else if (line.startsWith(PREFIX_OVAL)) {
                    parts = line.substring(PREFIX_OVAL.length()).trim().split(" ");
                    if (parts.length >= 6) {
                        cb.saveState();
                        cb.ellipse(parseLocation(parts[0], reader.getPageSize(pageNo).getWidth(), units), // x1
                                parseLocation(parts[1], reader.getPageSize(pageNo).getWidth(), units), // y1
                                parseLocation(parts[2], reader.getPageSize(pageNo).getWidth(), units), // x2
                                parseLocation(parts[3], reader.getPageSize(pageNo).getWidth(), units) // y2
                        );
                        cb.setLineWidth(Float.parseFloat(parts[4])); // line width
                        cb.setColorStroke(new BaseColor(parseColor(parts[5]).getRGB())); // stroke
                        if (parts.length >= 7) {
                            cb.setColorFill(new BaseColor(parseColor(parts[6]).getRGB())); // fill
                            cb.fillStroke();
                        } else {
                            cb.stroke();
                        }
                        cb.restoreState();
                    } else {
                        m_Logger.warning("Oval instruction not in expected format (" + FORMAT_OVAL + "):\n" + line);
                    }
                } else {
                    m_Logger.warning("Unknown command on line #" + lineNo + ":\n" + line);
                }
            }
            stamper.close();
        } catch (Exception e) {
            result = "Failed to process!\n" + Utils.throwableToString(e);
        } finally {
            FileUtils.closeQuietly(breader);
            FileUtils.closeQuietly(freader);
        }

        return result;
    }

    /**
     * Runs the PDF overlay from command-line.
     *
     * @param args   the arguments, use -h for help
     * @throws Exception   if something goes wrong
     */
    public static void main(String[] args) throws Exception {
        ArgumentParser parser;

        parser = ArgumentParsers.newArgumentParser("SimplePDFOverlay");
        parser.description("Applies the specified instructions file to the PDF as overlay.\n\n" + "Instructions:\n"
                + "- empty lines and lines starting with # are ignored\n"
                + "- The units used for the locations (can be supplied multiple times):\n" + "  units: pt|mm|in\n"
                + "- Selecting a page (page numbers are 1-based):\n" + "  page: <int>\n" + "- Setting a font:\n"
                + "  font: " + FORMAT_FONT + "\n"
                + "- Placing text in a rectangle (ll=lower left, ur=upper right):\n" + "  text: " + FORMAT_TEXT
                + "\n" + "  align: " + VALUES_ALIGN + "\n" + "- Drawing a line (color in #RRGGBB or RRGGBB):\n"
                + "  line: " + FORMAT_LINE + "\n" + "- Drawing a rectangle (colors in #RRGGBB or RRGGBB):\n"
                + "  rect: " + FORMAT_RECT + "\n" + "- Drawing an oval (colors in #RRGGBB or RRGGBB):\n"
                + "  oval: " + FORMAT_OVAL + "\n");
        parser.addArgument(PDF).metavar(PDF).type(String.class).help("The PDF file to overlay.");
        parser.addArgument(INSTRUCTIONS).metavar(INSTRUCTIONS).type(String.class)
                .help("The text file with the overlay instructions.");
        parser.addArgument(OUTPUT).metavar(OUTPUT).type(String.class)
                .help("The output to store the generate PDF in.");

        Namespace namespace;
        try {
            namespace = parser.parseArgs(args);
        } catch (Exception e) {
            parser.printHelp();
            return;
        }

        SimplePDFOverlay overlay = new SimplePDFOverlay(new File(namespace.getString(PDF)),
                new File(namespace.getString(INSTRUCTIONS)), new File(namespace.getString(OUTPUT)));
        String result = overlay.execute();
        if (result != null)
            throw new Exception("Failed to process:\n" + result);
    }
}