gov.samhsa.c2s.common.pdfbox.enhance.HexPdf.java Source code

Java tutorial

Introduction

Here is the source code for gov.samhsa.c2s.common.pdfbox.enhance.HexPdf.java

Source

/*
 * Copyright 2014 Frank J. ynes, heksemann@gmail.com
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 * 
 * 
 * HexPDF is a simple Java class making it easier to use Apache PDFBox
 * for creating pdf documents in from your Java application or web-service.
 * 
 * HexPDF adds stuff like automatic page adding, word-wrap, newline awareness,
 * left/right/center text alignment, table creation and image insertion.
 * Tables may contain images and text.
 * It also has support for text colour and page footers.
 * 
 */
package gov.samhsa.c2s.common.pdfbox.enhance;

import org.apache.fontbox.util.BoundingBox;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDPage;
import org.apache.pdfbox.pdmodel.PDPageContentStream;
import org.apache.pdfbox.pdmodel.PDPageTree;
import org.apache.pdfbox.pdmodel.common.PDRectangle;
import org.apache.pdfbox.pdmodel.font.PDFont;
import org.apache.pdfbox.pdmodel.font.PDType1Font;
import org.apache.pdfbox.pdmodel.graphics.image.JPEGFactory;
import org.apache.pdfbox.pdmodel.graphics.image.PDImageXObject;

import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
 * HexPdf
 *
 * https://github.com/heksemann/HexPDF
 *
 *
 */

/**
 * Simple class for generating pdf documents with support for tables and images,
 * uses Apache pdfBox for basic pdf creation functions.
 * <p>
 * <p>
 * The class adds automatic word wrap, left, right, center alignment of text,
 * control over page borders, a simple table function with control over text
 * alignment for each column, and inclusion of images in the document.</p>
 * <p>
 * <p>
 * Example usage</p>
 * <pre>
 * <code>
 *     HexPdf doc = new HexPdf();
 *
 *     BufferedImage basemap = (your function for retrieving the image)
 *     BufferedImage overlay = (your function for retrieving the image)
 *     String title = "A simple pdf document";
 *     String exampleText = "Lorem ipsum dolor.......";
 *
 *     Object[][] table = {
 *                          {"Country",  "Area", "Population", "Info"},
 *                          {"Norway",   "col2", "col2", "col4"},
 *                          {"Sweden",   "col2", "col2", "col4"},
 *                          {"Denmark",  "col2", "col2", "col4"},
 *                          {"Vietnam",  "col2", "col2", "col4"},
 *                          {"Thaland",  "col2", "col2", "col4"},
 *                          {"Burma",    "col2", "col2", "col4"},
 *                          {"USA",      "col2", "col2", "col4"},
 *                          {"Germany",  "col2", "col2", "col4"}
 *                        };
 *
 *     // Create the first page. IMPORTANT!
 *     doc.newPage();
 *
 *     // Add a title
 *     doc.title1Style();
 *     doc.drawText("My simple document\n", HexPdf.CENTER);
 *
 *     doc.normalStyle();
 *     doc.drawText(exampleText); // Default left aligned
 *
 *     doc.drawImage(basemap, HexPdf.CENTER);
 *     doc.drawImage(overlay, HexPdf.CENTER | HexPdf.NEWLINE);
 *     doc.drawText("Figure 1: An example figure with overlay\n", HexPdf.CENTER);
 *
 *     doc.drawText("\n\n" + exampleText, HexPdf.RIGHT);
 *
 *     doc.drawTable(table,
 *                   new int[]{100, 50, 50, 300},
 *                   new int[]{HexPdf.LEFT, HexPdf.LEFT, HexPdf.LEFT, HexPdf.LEFT},
 *                   HexPdf.CENTER);
 *     doc.drawText("Table 1: Some countries I've been to\n", HexPdf.CENTER);
 *
 *     doc.drawText("\n\n" + exampleText + exampleText, HexPdf.LEFT);
 *
 *     doc.title1Style();
 *     doc.drawText("-- END OF DOCUMENT --", HexPdf.CENTER);
 *
 *     doc.getDocumentAsBytArray("myfile.pdf");
 * </code>
 * </pre>
 *
 * @author Frank J. ynes, heksemann@gmail.com
 */
public class HexPdf extends PDDocument {
    private int orientation;
    private PDPageContentStream cs;
    private PDPage currentPage = null;
    private int numPages;
    private Footer footer = null;
    private Color normalColor;
    private Color titleColor;
    // Page setup
    private PDRectangle pageSize;
    private PDFont font;
    private float fontSize;
    private float topMargin;
    private float bottomMargin;
    private float leftMargin;
    private float rightMargin;
    private boolean ignorePagebleed;

    // Calculated dimensions
    private float pageWidth;
    private float pageHeight;

    /**
     * Page orientation portrait (default).
     *
     * @see #setOrientation(int)
     */
    public final static int PORTRAIT = 0;

    /**
     * Page orientation Landscape.
     *
     * @see #setOrientation(int)
     */
    public final static int LANDSCAPE = 1;

    /**
     * The height, in points, of the writable area between bottom and top
     * margins
     */
    protected float contentHeight;

    /**
     * The width, in points, of the writable area between left and right margins
     */
    protected float contentWidth;
    private float lineSep;

    /**
     * The starting (leftmost, lowest value) x position of writable area. Equal
     * to <code>leftMargin</code>
     */
    protected float contentStartX;

    /**
     * The starting (topmost, highest value) y position of writable area. Equal
     * to <code>bottomMargin + contentHeight</code>
     */
    protected float contentStartY;

    /**
     * The end (rightmost, highest value) x position of writable area. Equal to
     * <code>leftMargin + contentWidth</code>
     */
    protected float contentEndX;

    /**
     * The end (bottommost, lowest value) y position of writable area. Equal to
     * <code>bottomMargin</code>
     */
    protected float contentEndY;

    /**
     * The x-position in points of the current cursor position.
     */
    protected float cursorX;

    /**
     * The y-position in points of the current cursor position. Note that y
     * values are increasing towards the top of the page in pdf.
     */
    protected float cursorY;

    // Flags
    /**
     * Flag for image and text alignment and alignment of text in table cells.
     *
     * @see #LEFT
     * @see #RIGHT
     * @see #JUSTIFY
     */
    public static final int CENTER = 1;

    /**
     * Flag for image and text alignment and alignment of text in table cells.
     *
     * @see #CENTER
     * @see #RIGHT
     * @see #JUSTIFY
     */
    public static final int LEFT = 2;

    /**
     * Flag for image and text alignment and alignment of text in table cells.
     *
     * @see #LEFT
     * @see #CENTER
     * @see #JUSTIFY
     */
    public static final int RIGHT = 4;

    /**
     * Flag for image and text alignment and alignment of text in table cells.
     *
     * @see #LEFT
     * @see #RIGHT
     * @see #CENTER
     */
    public static final int JUSTIFY = 8;
    /**
     * Flag for drawImage - if set, move cursor to bottom after placement.
     */
    public static final int NEWLINE = 16;
    public static final int NEWPAGE = 32;
    // Text processing
    private static final String TXT_NEWLINE = "@@NeWlInE@@";

    // Styling
    private float normalFontSize = 11;
    private float title1FontSize = 20;
    private float title2FontSize = 15;

    /**
     * Default font size used for normalStyle.
     *
     * @see #normalStyle()
     * @see #setNormalFontSize(float)
     */
    public static final float DEFAULT_NORMAL_FONT_SIZE = 10;

    /**
     * Default font size used for title1Style.
     *
     * @see #title1Style()
     * @see #setTitle1FontSize(float)
     */
    public static final float DEFAULT_TITLE1_FONT_SIZE = 20;

    /**
     * Default font size used for title2Style.
     *
     * @see #title2Style()
     * @see #setTitle2FontSize(float)
     */
    public static final float DEFAULT_TITLE2_FONT_SIZE = 15;

    // Table
    private float tableCellMargin = 5;

    /**
     * Default margin in points between table cell border and table cell text.
     *
     * @see #settableCellMargin(float)
     * @see #drawTable(Object[][], float[], int[], int)
     */
    public static final float DEFAULT_TABLE_CELL_MARGIN = 5;

    /**
     * Sole constructor, creates a new instance of HexPdf.
     */
    public HexPdf() {
        super();
        this.ignorePagebleed = false;
        this.numPages = 0;
        this.rightMargin = 50f;
        this.leftMargin = 50f;
        this.bottomMargin = 50f;
        this.topMargin = 50f;
        this.fontSize = 10;
        this.font = PDType1Font.TIMES_ROMAN;
        this.pageSize = PDRectangle.A4;
        this.normalColor = Color.black;
        this.titleColor = Color.black;
        this.orientation = HexPdf.PORTRAIT;
        //firstPage();
    }

    /**
     * Recalculate page boundaries after a change of margins or page style.
     * Automatically called after margin changes.
     *
     * @see #contentHeight
     * @see #contentWidth
     * @see #contentStartX
     * @see #contentStartY
     * @see #contentEndX
     * @see #contentEndY
     * @see #cursorX
     * @see #cursorY
     */
    protected void setDimensions() {

        contentHeight = pageHeight - topMargin - bottomMargin;
        contentWidth = pageWidth - leftMargin - rightMargin;

        contentStartX = leftMargin;
        contentEndX = leftMargin + contentWidth;
        contentStartY = pageHeight - topMargin;
        contentEndY = bottomMargin;

        // Make sure cursor is inside writable area
        cursorX = (cursorX < leftMargin) ? leftMargin : cursorX;
        cursorX = (cursorX > contentEndX) ? contentEndX : cursorX;
        cursorY = (cursorY > contentStartY) ? contentStartY : cursorY;
        cursorY = (cursorY < contentEndX) ? contentEndX : cursorY;

        // Set line separation according to current font-size
        lineSep = font.getFontDescriptor().getFontBoundingBox().getHeight() / 1000 * fontSize;
    }

    /**
     * Returns the width of the pdf representation of the given string in
     * points.
     *
     * @param txt The text to calulate pdf-width of
     * @return The width given currently selected font and fontsize
     */
    protected float textWidth(String txt) {
        try {
            return (font.getStringWidth(txt) * fontSize / 1000);
        } catch (IOException ex) {
            Logger.getLogger(HexPdf.class.getName()).log(Level.SEVERE, null, ex);
            return 0;
        }
    }

    private void savedoc(String filename) throws IOException {
        closePage();
        super.save(filename);
    }

    /**
     * Close the current page and add it to the document.
     *
     * @see #newPage()
     */
    protected void closePage() {
        if (currentPage != null) {
            try {
                cs.close();
                addPage(currentPage);
                currentPage = null;
            } catch (IOException ex) {
                Logger.getLogger(HexPdf.class.getName()).log(Level.SEVERE, null, ex);
            }
        }
    }

    private String replaceBookmarks(String str, int pagenum, int numpages) {
        String ret = str;

        String today = new SimpleDateFormat("dd MMM yyyy").format(Calendar.getInstance().getTime());
        String user = System.getProperty("user.name");

        ret = ret.replace(Footer.PAGENUM, "" + pagenum);
        ret = ret.replace(Footer.NUMPAGES, "" + numpages);
        ret = ret.replace(Footer.DATE, today);
        ret = ret.replace(Footer.USER, user);
        return ret;
    }

    private void drawFooters() {
        if (footer != null) {
            ignorePagebleed = true; // Now new pages while writing footers!

            int pg;
            int pagecounter = 0;
            int total = (footer.isCOUNT_FIRSTPAGE()) ? numPages : numPages - 1;
            PDPageTree pages = this.getDocumentCatalog().getPages();
            for (PDPage page : pages) {
                pagecounter++;
                try {
                    cs = new PDPageContentStream(this, page, true, true);
                    setTextColor(footer.getTextColor());
                    setFont(footer.getFont());
                    setFontSize(footer.getFontsize());
                    if (pagecounter > 1 || false == footer.isOMIT_FIRSTPAGE()) {
                        pg = (footer.isCOUNT_FIRSTPAGE()) ? pagecounter : pagecounter - 1;

                        //noinspection Since15
                        if (footer.getLeftText() != null && !footer.getLeftText().isEmpty()) {
                            String left = replaceBookmarks(footer.getLeftText(), pg, total);
                            setCursor(contentStartX, contentEndY - lineSep);
                            drawText(left, HexPdf.LEFT);
                        }
                        //noinspection Since15
                        if (footer.getRightText() != null && !footer.getRightText().isEmpty()) {
                            String right = replaceBookmarks(footer.getRightText(), pg, total);
                            setCursor(contentStartX, contentEndY - lineSep);
                            drawText(right, HexPdf.RIGHT);
                        }
                        //noinspection Since15
                        if (footer.getCenterText() != null && !footer.getCenterText().isEmpty()) {
                            String right = replaceBookmarks(footer.getCenterText(), pg, total);
                            setCursor(contentStartX, contentEndY - lineSep);
                            drawText(right, HexPdf.CENTER);
                        }
                    }
                    cs.close();
                } catch (IOException ex) {
                    Logger.getLogger(HexPdf.class.getName()).log(Level.SEVERE, null, ex);
                }
            }
            ignorePagebleed = false;
        }
    }

    /**
     * Close the current page (if any), and open a new one. Cursor position is
     * reset to top-left corner of writable area (within margins)
     *
     * @see #closePage()
     */
    public void newPage() {

        numPages++;
        if (currentPage != null) {
            closePage();
        }

        currentPage = new PDPage();
        float x1 = this.pageSize.getLowerLeftX();
        float y1 = this.pageSize.getLowerLeftY();
        float x2 = this.pageSize.getUpperRightX();
        float y2 = this.pageSize.getUpperRightY();
        float w = this.pageSize.getWidth();
        float h = this.pageSize.getHeight();
        if (orientation == HexPdf.PORTRAIT) {
            pageWidth = w;
            pageHeight = h;
            currentPage.setMediaBox(new PDRectangle(new BoundingBox(x1, y1, x2, y2)));
        } else {
            pageWidth = h;
            pageHeight = w;
            currentPage.setMediaBox(new PDRectangle(new BoundingBox(y1, x1, y2, x2)));
        }

        setDimensions();
        cursorX = contentStartX;
        cursorY = contentStartY;
        try {
            cs = new PDPageContentStream(this, currentPage);
            cs.setFont(font, fontSize);
        } catch (IOException ex) {
            Logger.getLogger(HexPdf.class.getName()).log(Level.SEVERE, null, ex);
        }
    }

    /**
     * Save and close the document.
     *
     * @return byte[] the pdf document as byte array
     * @see #save(String)
     * @see #close()
     */
    public byte[] getDocumentAsBytArray() {
        ByteArrayOutputStream pdfOutputStream = new ByteArrayOutputStream();
        try {
            setTextColor(footer.getTextColor());
            setFont(footer.getFont());
            setFontSize(footer.getFontsize());
            closePage();
            drawFooters();
            closePage();
            super.save(pdfOutputStream);
            close();
        } catch (IOException ex) {
            Logger.getLogger(HexPdf.class.getName()).log(Level.SEVERE, null, ex);
        } finally {
            return pdfOutputStream.toByteArray();
        }
    }

    /**
     * Move cursor to a new position on the current page.
     *
     * @param x cursor x-position (horizontal)
     * @param y cursor y-position (vertical)
     * @see #setCursor(float[])
     */
    public void setCursor(float x, float y) {
        cursorX = x;
        cursorY = y;
    }

    /**
     * Move cursor to a new position on the current page.
     *
     * @param point a two-element float array giving x and y position of cursor
     * @see #setCursor(float, float)
     */
    public void setCursor(float[] point) {
        setCursor(point[0], point[1]);
    }

    /**
     * Get current cursor position on current page.
     *
     * @return two-element float array giving x and y position of cursor
     */
    public float[] getCursor() {
        float[] ret = { cursorX, cursorY };
        return ret;
    }

    /**
     * Retrieve the current cursors horizontal position.
     *
     * @return x value of current cursor position
     * @see #getCursor()
     */
    public float getCursorX() {
        return cursorX;
    }

    /**
     * Retrieve the current cursor vertical position.
     *
     * @return y value of current cursor position
     * @see #getCursor()
     */
    public float getCursorY() {
        return cursorY;
    }

    private int makeLine(String[] words, int first, float maxlen) {
        int num = 0;
        String result = "";
        if (words[first].equals(HexPdf.TXT_NEWLINE)) {
            return -1;
        }
        for (int i = first; i < words.length; i++) {
            if (words[i].equals(HexPdf.TXT_NEWLINE)) {
                return num;
            }
            String word = words[i].trim();
            if (num == 0) {
                if (textWidth(" " + word) > maxlen) {
                    return 0;
                } else {
                    result = word;
                    num++;
                }
            } else if (textWidth(result + " " + word) > maxlen) {
                return num;
            } else {
                result += " " + word;
                num++;
            }
        }
        return num;
    }

    private void doDrawText(String line) {
        try {
            cs.beginText();
            cs.moveTextPositionByAmount(cursorX, cursorY);
            cs.drawString(line);
            cs.endText();
            cursorX += font.getStringWidth(line) * fontSize / 1000;
        } catch (IOException ex) {
            Logger.getLogger(HexPdf.class.getName()).log(Level.SEVERE, null, ex);
        }
    }

    private String join(String[] words, int first, int num) {
        String ret = "";
        String sep = "";
        for (int i = first; i < first + num; i++) {
            if (i < words.length) {
                ret += sep + words[i];
                sep = " ";
            }
        }
        return ret;
    }

    // Estimate height of a table cell. Used by rowHeight to determine
    // if page break should be inserted in a table
    private float elemHeight(Object elem, float startx, float endx, int flags) {
        String txt;
        if (elem instanceof BufferedImage) {
            return ((BufferedImage) elem).getHeight();
        }
        txt = (String) elem;

        float cystart = cursorY;
        float cxstart = cursorX;

        int align = HexPdf.LEFT;
        if ((flags & HexPdf.CENTER) > 0) {
            align = HexPdf.CENTER;
        } else if ((flags & HexPdf.RIGHT) > 0) {
            align = HexPdf.RIGHT;
        } else if ((flags & HexPdf.JUSTIFY) > 0) {
            align = HexPdf.JUSTIFY;
        }
        //noinspection Since15
        if (txt == null || txt.isEmpty()) {
            return 0;
        }
        txt = txt.replace("\n", " " + HexPdf.TXT_NEWLINE + " ");
        String[] words = txt.split("\\s+");
        int i = 0;
        float height = 0;
        while (i < words.length) {
            int num = makeLine(words, i, endx - cursorX);
            if (num == -1) { // newline
                i++;
                cursorX = startx;
                cursorY -= lineSep;
                height += lineSep;
            } else if (num == 0) {
                if (cursorX > startx) {
                    // Something on line from start. Try a newline first, then recheck.
                    cursorX = startx;
                    cursorY -= lineSep;
                } else {
                    // a single word is too big for the box. Draw it!
                    cursorY -= lineSep;
                    cursorX = startx;
                    i++;
                }
            } else {
                String toDraw = join(words, i, num);
                float strlen = textWidth(toDraw);
                if ((cursorX == startx)
                        && (align == HexPdf.RIGHT || align == HexPdf.CENTER || align == HexPdf.JUSTIFY)) {
                    boolean newline_after = ((i + num) >= words.length
                            || words[i + num].equals(HexPdf.TXT_NEWLINE));
                    if (align == HexPdf.JUSTIFY) {
                        if (newline_after == false) {
                            cursorX = endx;
                        } else {
                            cursorX += strlen;
                        }
                    } else {
                        cursorX += (align == HexPdf.RIGHT) ? endx : cursorX + strlen;
                    }
                } else {
                    cursorX += strlen;
                }

                i += num;
            }
        }
        height = lineSep + cystart - cursorY;
        setCursor(cxstart, cystart);
        return height;
    }

    // Estimate heght of a table row. Used for determining page breaks within
    // a table
    private float rowHeight(float x, float y, float[] w, Object[] cells, int[] flags) {
        float maxh = 0, thish, cellx = x;
        for (int i = 0; i < cells.length; i++) {
            thish = (elemHeight(cells[i], cellx, w[i], flags[i]));
            cellx += w[i];
            maxh = (thish > maxh) ? thish : maxh;
        }
        return maxh;
    }

    /**
     * Draw a text from the current cursor position. The text can be multi-line
     * and even multi-page. When crossing page boundaries
     * (<code>contentEndY</code>) a new page is created. Newline characters
     * force explicit lineshift. Normal word-wrap is performed to keep the text
     * between the stated boundaries <code>startx</code> and <code>endx</code>
     *
     * @param txt    The text to be drawn
     * @param startx Left edge of area available for drawing
     * @param endx   Right edge of area available for drawing
     * @param flags  One of
     *               <code>HexPdf.LEFT | HexPdf.CENTER | HexPdf.RIGHT | HexPdf.JUSTIFY</code>
     *               for text alignment between startx and endx
     * @return Actual height of the string drawn
     * @see #drawText(String, int)
     * @see #drawText(String, float, float, int)
     */
    protected float _drawText(String txt, float startx, float endx, int flags) {
        float cystart = cursorY;
        int align = HexPdf.LEFT;
        if ((flags & HexPdf.CENTER) > 0) {
            align = HexPdf.CENTER;
        } else if ((flags & HexPdf.RIGHT) > 0) {
            align = HexPdf.RIGHT;
        } else if ((flags & HexPdf.JUSTIFY) > 0) {
            align = HexPdf.JUSTIFY;
        }
        //noinspection Since15
        if (txt == null || txt.isEmpty()) {
            return 0;
        }
        txt = txt.replace("\n", " " + HexPdf.TXT_NEWLINE + " ");
        String[] words = txt.split("\\s+");
        int i = 0;
        float height = 0;
        while (i < words.length) {
            int num = makeLine(words, i, endx - cursorX);
            if (num == -1) { // newline
                i++;
                cursorX = startx;
                cursorY -= lineSep;
                // New page?
                if (ignorePagebleed == false && ((cursorY - lineSep) < contentEndY)) {
                    newPage();
                    cursorX = startx;
                }
                height += lineSep;
            } else if (num == 0) {
                if (cursorX > startx) {
                    // Something on line from start. Try a newline first, then recheck.
                    cursorX = startx;
                    cursorY -= lineSep;
                    // New page?
                    if (ignorePagebleed == false && ((cursorY - lineSep) < contentEndY)) {
                        newPage();
                        cursorX = startx;
                    }
                } else {
                    // a single word is too big for the box. Draw it!
                    doDrawText(words[i]);
                    cursorY -= lineSep;
                    cursorX = startx;
                    i++;
                    // New page?
                    if (ignorePagebleed == false && ((cursorY - lineSep) < contentEndY)) {
                        newPage();
                        cursorX = startx;
                    }
                }
            } else {
                String toDraw = join(words, i, num);
                if ((cursorX == startx)
                        && (align == HexPdf.RIGHT || align == HexPdf.CENTER || align == HexPdf.JUSTIFY)) {
                    float strlen = textWidth(toDraw);
                    float space = endx - startx - strlen;
                    boolean newline_after = ((i + num) >= words.length
                            || words[i + num].equals(HexPdf.TXT_NEWLINE));
                    if (align == HexPdf.JUSTIFY) {
                        if (newline_after == false) {
                            // Only justify if this is the not last line of the paragraph.
                            try {
                                cs.appendRawCommands(
                                        String.format("%f Tc\n", space / (toDraw.length() - 1)).replace(',', '.'));
                                doDrawText(toDraw);
                                toDraw = null;
                                cs.appendRawCommands("0 Tc\n");
                            } catch (IOException ex) {
                                Logger.getLogger(HexPdf.class.getName()).log(Level.SEVERE, null, ex);
                            }
                        }
                    } else {
                        cursorX += (align == HexPdf.RIGHT) ? space : space / 2;
                    }
                }
                if (toDraw != null) {
                    doDrawText(toDraw);
                }
                i += num;
            }
        }
        return lineSep + cystart - cursorY;
    }

    /**
     * Draw a text from the current cursor position. The text can be multi-line
     * and even multi-page. When crossing page boundaries
     * (<code>contentEndY</code>) a new page is automatically created. Newline
     * characters force explicit lineshift. Normal word-wrap is performed to
     * keep the text between the current <code>leftMargin</code> and
     * <code>rightMargin</code>
     *
     * @param txt   The text to be drawn
     * @param flags One of
     *              <code>HexPdf.LEFT | HexPdf.CENTER | HexPdf.RIGHT | HexPdf.JUSTIFY</code>
     *              for text alignment between startx and endx
     * @return Actual height of the string drawn
     * @see #drawText(String, float, float, int)
     * @see #_drawText(String, float, float, int)
     */
    public float drawText(String txt, int flags) {
        return _drawText(txt, contentStartX, contentEndX, flags);
    }

    /**
     * Draw a left-aligned text from the current cursor position. The text can
     * be multi-line and even multi-page. When crossing page boundaries
     * (<code>contentEndY</code>) a new page is automatically created. Newline
     * characters force explicit lineshift. Normal word-wrap is performed to
     * keep the text between the current <code>leftMargin</code> and
     * <code>rightMargin</code>
     *
     * @param txt The text to be drawn
     * @return height of text drawn
     * @see #drawText(String, int)
     * @see #_drawText(String, float, float, int)
     */
    public float drawText(String txt) {
        return drawText(txt, HexPdf.LEFT);
    }

    /**
     * Draw a text from the specified position. The text can be multi-line and
     * even multi-page. When crossing page boundaries (<code>contentEndY</code>)
     * a new page is automatically created. Newline characters force explicit
     * lineshift. Normal word-wrap is performed to keep the text between the
     * current <code>leftMargin</code> and <code>rightMargin</code>
     *
     * @param txt   The text to be drawn
     * @param x     Starting x-position for the text
     * @param y     Starting y-position for the text
     * @param flags One of
     *              <code>HexPdf.LEFT | HexPdf.CENTER | HexPdf.RIGHT | HexPdf.JUSTIFY</code>
     *              for text alignment between startx and endx
     * @return Actual height of the string drawn
     * @see #drawText(String, int)
     * @see #_drawText(String, float, float, int)
     */
    public float drawText(String txt, float x, float y, int flags) {
        setCursor(x, y);
        return drawText(txt, flags);
    }

    /**
     * Draw an image starting at current cursor location. If no flags are given,
     * the top-left corner of the image is positioned at the current cursor
     * position. If one of the flags
     * <code>HexPdf.LEFT | HexPdf.CENTER | HexPdf.RIGHT</code> is set, the image
     * is adjusted between <code>leftMargin, rightMargin</code> accordingly. The
     * cursor location is kept unchanged unless the flag
     * <code>HexPdf.NEWLINE</code> is set. This is useful for adding pictures as
     * layers. If <code>HexPdf.NEWLINE</code> is set, the cursor is positioned
     * at <code>leftMargin</code> immediately below the image.
     *
     * @param image the image to be added
     * @param flags see description
     */
    public void drawImage(BufferedImage image, int flags) {
        PDImageXObject ximage = null;
        float imW = 0;
        float imH = 0;
        try {
            ximage = JPEGFactory.createFromImage(this, image);
            imW = ximage.getWidth();
            imH = ximage.getHeight();
        } catch (IOException ex) {
            Logger.getLogger(HexPdf.class.getName()).log(Level.SEVERE, null, ex);
        }
        // newpage if image cannot fit on rest of current page
        if ((cursorY - imH) < contentEndY) {
            newPage();
        }
        float imgX = cursorX;
        float imgY = cursorY - imH;
        if ((flags & HexPdf.CENTER) > 0) {
            imgX = contentStartX + ((contentWidth - imW) / 2);
        } else if ((flags & HexPdf.LEFT) > 0) {
            imgX = contentStartX;
        } else if ((flags & HexPdf.RIGHT) > 0) {
            imgX = contentEndX - imW;
        }

        try {
            cs.drawXObject(ximage, imgX, imgY, imW, imH);
        } catch (IOException ex) {
            Logger.getLogger(HexPdf.class.getName()).log(Level.SEVERE, null, ex);
        }

        if ((flags & HexPdf.NEWLINE) > 0) {
            setCursor(contentStartX, imgY - lineSep);
        }
    }

    /**
     * Draw an image at the given location. If no flags are given, the top-left
     * corner of the image is positioned at the specified <code>x, y</code>
     * location. If one of the flags
     * <code>HexPdf.LEFT | HexPdf.CENTER | HexPdf.RIGHT</code> is set, the image
     * is adjusted horizontally between <code>leftMargin, rightMargin</code>
     * accordingly. The top of the image will always be at the given
     * <code>y</code> position. The cursor location is kept unchanged unless the
     * flag <code>HexPdf.NEWLINE</code> is set. This is useful for adding
     * pictures as layers. If <code>HexPdf.NEWLINE</code> is set, the cursor is
     * positioned at <code>leftMargin</code> immediately below the image.
     *
     * @param image The image to be added
     * @param x     wanted x-value of image top-left corner on the page
     * @param y     wanted y-value of image top-left corner on the page
     * @param flags see description
     */
    public void drawImage(BufferedImage image, float x, float y, int flags) {
        setCursor(x, y);
        drawImage(image, flags);
    }

    // TABLE functions

    // Add a text cell to table
    private float addCell(float x, float y, float w, String txt, int flags) {
        setCursor(x + tableCellMargin, y - 0.8f * lineSep);
        return _drawText(txt, x + tableCellMargin, x + w - tableCellMargin, flags);
    }

    // Add an image cell to table
    private float addCell(float x, float y, float w, BufferedImage image, int flags) {
        if ((flags & HexPdf.CENTER) > 0) {
            setCursor(x + 0.5f * (w - image.getWidth()), y);// - tableCellMargin);
        } else if ((flags & HexPdf.RIGHT) > 0) {
            setCursor(x + w - image.getWidth(), y);// - tableCellMargin);
        } else {
            setCursor(x + tableCellMargin, y);// - tableCellMargin);
        }
        drawImage(image, 0);
        return (image.getHeight());// + 2 * tableCellMargin);
    }

    private void addCellBorder(float x, float y, float w, float h) {
        try {
            cs.drawLine(x, y, x + w, y);
            cs.drawLine(x + w, y, x + w, y - h);
            cs.drawLine(x + w, y - h, x, y - h);
            cs.drawLine(x, y - h, x, y);
        } catch (IOException ex) {
            Logger.getLogger(HexPdf.class.getName()).log(Level.SEVERE, null, ex);
        }
    }

    private float addRow(float x, float y, float[] w, Object[] cells, int[] flags) {
        float maxh = 0;
        float thish = 0;
        float cellx = x;
        int i;
        for (i = 0; i < w.length; i++) {
            thish = 0;
            if (cells[i] instanceof String) {
                thish = addCell(cellx, y, w[i], (String) cells[i], flags[i]);
            } else if (cells[i] instanceof BufferedImage) {
                thish = addCell(cellx, y, w[i], (BufferedImage) cells[i], flags[i]);
            }
            cellx += w[i];
            maxh = (thish > maxh) ? thish : maxh;
        }
        cellx = x;
        for (i = 0; i < w.length; i++) {
            addCellBorder(cellx, y, w[i], maxh);
            cellx += w[i];
        }
        return maxh;
    }

    /**
     * Add a table to the document starting at current cursor location. The
     * input table must be a two-dimensional array of <code>Object</code>, all
     * rows must have the same number of columns.
     * <p>
     * If the table extends beyond the page boundary <code>contentEndY</code> a
     * new page is automatically created and the table continues.
     * <p>
     * Normal word-wrap is performed within each cell if the text is longer than
     * the column designated width.
     * <p>
     * A table element (Object) can be either a String or a BufferedImage
     *
     * @param table        the table data. Objects should be String or BufferedImage
     * @param column_width array of column widths
     * @param column_flag  array of flags for text alignment within columns, one
     *                     of <code>HexPdf.LEFT | HexPdf.CENTER | HexPdf.RIGHT</code>
     * @param table_align  flag for alignment of the table itself
     * @return the height of the table - if multipage then return the height on
     * the last page.
     */
    public float drawTable(Object[][] table, float[] column_width, int[] column_flag, int table_align) {
        float tabheight = 0;
        float rowheight = 0;
        float table_width = 0;
        boolean oldIgnoreBleed = ignorePagebleed;
        ignorePagebleed = true; // No unintended page-breaks while adding table
        for (float colwidth : column_width) {
            table_width += colwidth;
        }
        float free_space = this.contentWidth - table_width;

        float x = cursorX;
        float y = cursorY;

        if (table_align == HexPdf.CENTER || table_align == HexPdf.RIGHT) {
            x += ((table_align == HexPdf.CENTER) ? free_space / 2 : free_space);
        }
        int rownum = 1;
        for (Object[] row : table) {
            if (row != null) {
                // Can the next row it fit on same page? Find the height of next
                // row and make a new page after adding this row if necessary.
                float guessRowHeight = 0;
                if (rownum < table.length) {
                    guessRowHeight = rowHeight(x, contentStartY, column_width, table[rownum], column_flag);
                }
                rowheight = addRow(x, y - tabheight, column_width, row, column_flag);
                tabheight += rowheight;
                // Ne page before next row?
                if ((y - tabheight - guessRowHeight) < contentEndY) {
                    newPage();
                    tabheight = 0;
                    y = contentStartY;
                }
            }
            rownum++;
        }
        cursorX = contentStartX;
        cursorY -= (rowheight + tableCellMargin);
        ignorePagebleed = oldIgnoreBleed;
        return tabheight;
    }

    // Setters and getters

    /**
     * Set current font-size and color to a title-1 style.
     *
     * @see #title1FontSize
     * @see #titleColor
     */
    public void title1Style() {
        setFontSize(title1FontSize);
        setTextColor(titleColor);
    }

    /**
     * Set current font-size and color to a title-2 style.
     *
     * @see #title2FontSize
     * @see #titleColor
     */
    public void title2Style() {
        setFontSize(title2FontSize);
        setTextColor(titleColor);
    }

    /**
     * Set current font-size and color to normal style.
     *
     * @see #normalFontSize
     * @see #normalColor
     */
    public void normalStyle() {
        setFontSize(normalFontSize);
        setTextColor(normalColor);
    }

    /**
     * Retrieve the currently active font size in points.
     *
     * @return current font size
     */
    public float getFontSize() {
        return fontSize;
    }

    /**
     * Set font size in points.
     * Normally you should not use this directly.
     * Instead, do setNormalFontSize() followed by normalStyle()
     *
     * @param fs font size in points
     */
    public void setFontSize(float fs) {
        this.fontSize = fs;
        if (currentPage != null) {
            try {
                cs.setFont(font, fontSize);
            } catch (IOException ex) {
                Logger.getLogger(HexPdf.class.getName()).log(Level.SEVERE, null, ex);
            }
        }
        lineSep = font.getFontDescriptor().getFontBoundingBox().getHeight() / 1000 * fontSize;
    }

    /**
     * Retrieve the currently active font.
     *
     * @return current font as PDFont
     * @see PDFont
     */
    public PDFont getFont() {
        return font;
    }

    /**
     * Set font type to use.
     *
     * @param font font to use, PDFont type
     * @see PDFont
     */
    public void setFont(PDFont font) {
        this.font = font;
        try {
            cs.setFont(font, fontSize);
        } catch (IOException ex) {
            Logger.getLogger(HexPdf.class.getName()).log(Level.SEVERE, null, ex);
        }
        lineSep = font.getFontDescriptor().getFontBoundingBox().getHeight() / 1000 * fontSize;
    }

    /**
     * Retrieve the height of the top margin.
     *
     * @return height of top margin in points
     */
    public float getTopMargin() {
        return topMargin;
    }

    /**
     * Set height of top margin. Height of writable area (contentHeight) is
     * adjusted accordingly.
     *
     * @param topMargin new height of top margin in points
     */
    public void setTopMargin(float topMargin) {
        this.topMargin = topMargin;
        setDimensions();
    }

    /**
     * Retrieve the height of bottom margin in points.
     *
     * @return height of bottom margin in points
     */
    public float getBottomMargin() {
        return bottomMargin;
    }

    /**
     * Set height of bottom margin. Height of writable area (contentHeigth) is
     * adjusted accordingly.
     *
     * @param bottomMargin new height of top margin in points
     */
    public void setBottomMargin(float bottomMargin) {
        this.bottomMargin = bottomMargin;
        setDimensions();
    }

    /**
     * Retrieve the width of left margin in points.
     *
     * @return width of left margin in points
     */
    public float getLeftMargin() {
        return leftMargin;
    }

    /**
     * Set width of left margin. Width of writable area (contentWidth) is
     * adjusted accordingly.
     *
     * @param leftMargin new height of top margin in points
     */
    public void setLeftMargin(float leftMargin) {
        this.leftMargin = leftMargin;
        setDimensions();
    }

    /**
     * Retrieve the width of right margin in points.
     *
     * @return width of right margin in points
     */
    public float getRightMargin() {
        return rightMargin;
    }

    /**
     * Retrieve the height of current page.
     *
     * @return page height in points
     */
    public float getPageHeight() {
        return pageHeight;
    }

    /**
     * Retrieve the width of current page.
     *
     * @return page width in points
     */
    public float getPageWidth() {
        return pageWidth;
    }

    /**
     * Retrieve the height of writable area (witin margins) of current page.
     *
     * @return height of page content area in points
     */
    public float getContentHeight() {
        return contentHeight;
    }

    /**
     * Retrieve the width of writable area (witin margins) of current page.
     *
     * @return width of page content area in points
     */
    public float getContentWidth() {
        return contentWidth;
    }

    /**
     * Set width of right margin. Width of writable area (contentWidth) is
     * adjusted accordingly.
     *
     * @param rightMargin new height of top margin in points
     */
    public void setRightMargin(float rightMargin) {
        this.rightMargin = rightMargin;
        setDimensions();
    }

    /**
     * Set the font size to be used for normal style.
     *
     * @param fontSize font size in points.
     * @see #normalStyle()
     * @see #DEFAULT_NORMAL_FONT_SIZE
     */
    public void setNormalFontSize(float fontSize) {
        normalFontSize = fontSize;
    }

    /**
     * Set the font size to be used for title 1 style.
     *
     * @param fontSize font size in points.
     * @see #title1Style()
     * @see #DEFAULT_TITLE1_FONT_SIZE
     */
    public void setTitle1FontSize(float fontSize) {
        title1FontSize = fontSize;
    }

    /**
     * Set the font size to be used for title 2 style.
     *
     * @param fontSize font size in points.
     * @see #title2Style()
     * @see #DEFAULT_TITLE2_FONT_SIZE
     */
    public void setTitle2FontSize(float fontSize) {
        title2FontSize = fontSize;
    }

    /**
     * Set the margin to be used between table borders and table text
     *
     * @param cellMargin table margin in points
     * @see #DEFAULT_TABLE_CELL_MARGIN
     */
    public void settableCellMargin(float cellMargin) {
        tableCellMargin = cellMargin;
    }

    /**
     * Returns the underlying PDPageContentStream from pdfBox. Note that the
     * PDPageContentStream will change whenever a new page is created, it is
     * hence important to call getPDPageContentStream() before any direct
     * operation on the content stream.
     *
     * @return PDPageContentStream in use
     */
    public PDPageContentStream getPDPageContentStream() {
        return cs;
    }

    /**
     * Set text color.
     * This will be reset if you change style.
     *
     * @param color the new text color
     * @see #setTitleColor(Color)
     * @see #setNormalColor(Color)
     */
    public void setTextColor(Color color) {
        try {
            cs.setNonStrokingColor(color);
            //textColor = color;
        } catch (IOException ex) {
            Logger.getLogger(HexPdf.class.getName()).log(Level.SEVERE, null, ex);
        }
    }

    /**
     * Get the footer set for the document.
     *
     * @return Footer
     */
    public Footer getFooter() {
        return footer;
    }

    /**
     * Set footer to use on all pages.
     * The footer is added when finalizing the document.
     *
     * @param footer Footer to use
     * @see Footer
     */
    public void setFooter(Footer footer) {
        this.footer = footer;
    }

    /**
     * Get the color currently used for title style writing.
     *
     * @return Color
     */
    public Color getTitleColor() {
        return titleColor;
    }

    /**
     * Set the color to use for title style writing.
     *
     * @param titleColor Color
     */
    public void setTitleColor(Color titleColor) {
        this.titleColor = titleColor;
    }

    /**
     * Get the color currently used for normal style writing.
     *
     * @return color    Color
     */
    public Color getNormalColor() {
        return normalColor;
    }

    /**
     * Set the color to use for normal style writing.
     *
     * @param color Color
     */
    public void setNormalColor(Color color) {
        this.normalColor = color;
    }

    /**
     * Return the left boundary of the page content area.
     *
     * @return x-coordinate points
     */
    public float getContentStartX() {
        return contentStartX;
    }

    /**
     * Return the upper boundary of the page content area.
     *
     * @return y-coordinate points
     */
    public float getContentStartY() {
        return contentStartY;
    }

    /**
     * Return the right boundary of the page content area.
     *
     * @return x-coordinate points
     */
    public float getContentEndX() {
        return contentEndX;
    }

    /**
     * Return the lower boundary of the page content area.
     *
     * @return y-coordinate points
     */
    public float getContentEndY() {
        return contentEndY;
    }

    /**
     * Set page orientation.
     * Takes effect only next tine a newpage() is explicitly or implicitly called.
     *
     * @param orientation HexPdf.LANDSCAPE | HexPdf.PORTRAIT
     */
    public void setOrientation(int orientation) {
        this.orientation = orientation;
    }

    /**
     * Set page size to be used.
     * Takes effect on the next explicit or automatic newPage()
     * Default is <code>PDPage.PAGE_SIZE_A4</code>
     *
     * @param pageSize page dimension as <code>PDRectangle</code>
     * @see PDRectangle
     */
    public void setPageSize(PDRectangle pageSize) {
        this.pageSize = pageSize;
    }
}