PDFlib Cookbook

cookbook

table/table_invoice

Create an invoice using the table formatter.

Download Java Code  Switch to PHP Code  Show Output 

/*
 * Table invoice:
 * Create an invoice using the table feature
 * 
 * Create an invoice by importing a background PDF page with the company's
 * stationery header. Output the customer's address data as well as a table 
 * listing all items ordered including their quantities and prices. If the table
 * spans several pages output a subtotal after each table instance. To retrieve
 * the last row having been output in a table instance use info_table() with the
 * "lastbodyrow" option. In the last table row, output the total at the end of
 * the table. Output some final text directly after the table. Use info_table()
 * with the "height" option to retrieve the exact end position of the table. 
 *
 * Required software: PDFlib+PDI/PPS 9
 * Required data: PDF file
 */
package com.pdflib.cookbook.pdflib.table;

import java.math.BigDecimal;
import java.math.RoundingMode;
import java.text.DateFormat;
import java.util.Date;
import java.util.Locale;

import com.pdflib.pdflib;
import com.pdflib.PDFlibException;

public class table_invoice
{
    public static void main (String argv[])
    {
        /* This is where the data files are. Adjust as necessary. */
        String searchpath = "../input";

        /* By default annotations are also imported. In some cases this
         * requires the Noto fonts for creating annotation appearance streams.
         * We therefore set the searchpath to also point to the font directory.
         */
        String fontpath = "../resource/font";
        String outfile = "table_invoice.pdf";
        String title = "Table Invoice";
        
        pdflib p = null;

        String infile = "stationery.pdf";
        
        int exitcode = 0;
        int row, col, tf=-1, tbl=-1;
        int i, y, stationery, itemno, page, regularfont, boldfont;
      
        String tf_opts;
     
        double sum = 0, total = 0, subtotal = 0, tabheight = 0;
        String result;
      
        final double pagewidth = 595, pageheight = 842;
        final double fontsize = 12;
        final double capheight = 8.5;
        final double rowheight = 16;
        final int margin = 4;
        final String leading = "120%";
        final int ystart = (int) pageheight - 170;
        final int yoffset = 15;
        final int ycontinued = 40;
        final int nfooters = 1, nheaders = 1;
        
        /* The table coordinates are fixed; only the height of the table may differ
         */
        final int llx = 55, urx = 505, lly = 80;
           
        /* The widths of the individual columns is fixed */
        final int maxcol = 5;
        
        final int c1 = 30, c2 = 200, c3 = 70, c4 = 70, c5 = 80;
        
        /* Get the current date */
        Date now = new Date();
        DateFormat fulldate = 
            DateFormat.getDateInstance(DateFormat.LONG, Locale.US);
      
        /* Text to output after the table */
        final String closingtext =
            "Terms of payment: 30 days net. " +
            "90 days warranty starting at the day of sale. " +
            "This warranty covers defects in workmanship only. " +
            "Kraxi Systems, Inc. will, at its option, repair or replace the " +
            "product under the warranty. This warranty is not transferable. " +
            "No returns or exchanges will be accepted for wet products.";

        final String[][] items = {
        /*     Description,        Quantity, Price */
            { "Long Distance Glider; price includes volume discount of 20% for " +
              "more than 10 items ordered",
                                    "11",    "15.96"},
            { "Turbo Flyer",        "5",     "39.95"},
            { "Giga Trash",         "1",     "179.95"},
            { "Bare Bone Kit",      "3",     "49.95"},
            { "Nitty Gritty",       "10",    "19.95"},
            { "Pretty Dark Flyer",  "1",     "74.95"},
            { "Free Gift",          "2",     "29.95"},
            { "Giant Wing",         "2",     "29.95"},
            { "Cone Head Rocket; price includes volume discount of 30% for " +
              "more than 20 items ordered",
                                    "25",     "6.97"},
            { "Super Dart",         "2",      "29.95"},
            { "German Bi-Plane",    "6",      "9.95"},
            { "Turbo Glider; price includes volume discount of 20% for " +
              "more than 10 items ordered",
                                     "11",     "15.96"},
            { "Red Baron",           "5",      "39.95"},
            { "Mega Rocket",         "1",      "179.95"},
            { "Kit the Kat",         "3",      "49.95"},
            { "Red Wing",            "10",     "19.95"},
            { "Dark Rider",          "1",      "74.95"},
            { "Speedy Gift",         "2",      "29.95"},
            { "Giant Bingo",         "2",      "29.95"},
            { "Ready Rocket; price includes volume discount of 30% for " +
              "more than 20 items ordered",
                                     "25",     "6.97"},
        };
        
        final String [] address = {
            "John Q. Doe", "255 Customer Lane", "Suite B",
            "12345 User Town", "Everland"
        };

        /* Used to format the prices to a maximum of to fraction digits */
        BigDecimal value, roundedValue;
        
        try {
            p = new pdflib();

            p.set_option("searchpath={" + searchpath + "}");

            p.set_option("searchpath={" + fontpath + "}");

            /* This means we must check return values of load_font() etc. */
            p.set_option("errorpolicy=return");

            if (p.begin_document(outfile, "") == -1)
                throw new Exception("Error: " + p.get_errmsg());

            p.set_info("Creator", "PDFlib Cookbook");
            p.set_info("Title", title);

            /* Open the PDF document */
            stationery = p.open_pdi_document(infile, "");
            if (stationery == -1)
                throw new Exception("Error: " + p.get_errmsg());
            
            /* Open the first page of the PDF */
            page = p.open_pdi_page(stationery, 1, "");
            if (page == -1)
                throw new Exception("Error: " + p.get_errmsg());
          
            /* Load the bold and regular styles of a font */
            boldfont = p.load_font("NotoSerif-Bold", "unicode", "");
            if (boldfont == -1)
                throw new Exception("Error: " + p.get_errmsg());
            
            regularfont = p.load_font("NotoSerif-Regular", "unicode", "");
            if (regularfont == -1)
                throw new Exception("Error: " + p.get_errmsg());
            
            /* Start the output page */
            p.begin_page_ext(pagewidth, pageheight, "");

            /* Fit and close the imported PDF page */
            p.fit_pdi_page(page, 0, 0, "");
            p.close_pdi_page(page);

            /* Output the customer's address */
            y = ystart;
            
            p.setfont(regularfont, fontsize);
        
            for (i = 0; i < address.length; i++) {
                p.fit_textline(address[i], llx, y, "");
                y -= yoffset;
            }
            
            /* Print the header and the date */
            y -= 3 * yoffset;
            
            p.setfont(boldfont, fontsize);
            
            p.fit_textline("INVOICE", llx, y, "position {left top}");
            p.fit_textline(fulldate.format(now), urx, y, "position {right top}");
            
            y -= 3 * yoffset;
            
            /* ----------------------------------------------------
             * Add the first table row containing the heading cells
             * ----------------------------------------------------
             */
            
            /* Prepare the general option list for adding text line cells of the 
             * table header:
             * Define a fixed row height, and the position of the text line to be on
             * the top left with a margin of 4, for example. 
             * The text will be aligned on the top right or on the top left, 
             * respectively.
             * For an exact vertical alignment of the text line and the Textflow 
             * which will be added later note the following:
             * The height of an uppercase letter is exactly represented by the
             * capheight value of the font. For this reason use the capheight in the
             * font size specification. For example, a capheight of 8.5 will
             * approximately result in a font size of 12 points and (along with
             * "margin=4"), will sum up to an overall height of 16 points. 
            */
            final String head_opts_right = "fittextline={position={right top} " +
                " font=" + boldfont + " fontsize={capheight=" + capheight + "}} " +
                " rowheight=" + rowheight + " margin=" + margin;
            
            final String head_opts_left = "fittextline={position={left top} " +
                " font=" + boldfont + " fontsize={capheight=" + capheight + "}} " +
                " rowheight=" + rowheight + " margin=" + margin;
                 
            col = 1; row = 1;
                  
            /* Add each heading cell with the option list defined above; 
             * in addition, supply a fixed column width
             */
            tbl = p.add_table_cell(tbl, col++, row, "ITEM", 
                head_opts_right + " colwidth=" + c1);
            if (tbl == -1)
                throw new Exception("Error adding cell: " + p.get_errmsg());
            
            tbl = p.add_table_cell(tbl, col++, row, "DESCRIPTION", 
                head_opts_left + " colwidth=" + c1);
                if (tbl == -1)
                    throw new Exception("Error adding cell: " + p.get_errmsg());
            
            tbl = p.add_table_cell(tbl, col++, row, "QUANTITY", 
                head_opts_right + " colwidth=" + c3);
                if (tbl == -1)
                    throw new Exception("Error adding cell: " + p.get_errmsg());
                
            tbl = p.add_table_cell(tbl, col++, row, "PRICE", 
                head_opts_right + " colwidth=" + c4);
                if (tbl == -1)
                    throw new Exception("Error adding cell: " + p.get_errmsg());
                
            tbl = p.add_table_cell(tbl, col++, row, "SUM", 
                    head_opts_right + " colwidth=" + c5);
                if (tbl == -1)
                    throw new Exception("Error adding cell: " + p.get_errmsg());
                
            row++;
            
            
            /* -------------------------------------------
             * Add the body cells in subsequent table rows
             * -------------------------------------------
             */
            
            /* Prepare the general option list for adding text line cells of the 
             * table body; it is similar to the option list defined for header cells
             * but the font is set to a regular font instead
             */
            final String body_opts = "fittextline={position={right top} " +
                " font=" + regularfont + 
                " fontsize={capheight=" + capheight + "}} " +
                " rowheight=" + rowheight + " margin=" + margin;
            
            for (itemno = 1; itemno <= items.length; itemno++, row++) {
                col = 1;
                
                /* ---------------------------------------------------------------
                 * Add the text line cell containing the Item in the first column,
                 * with the options defined for table body cells above
                 * ---------------------------------------------------------------
                 */
                tbl = p.add_table_cell(tbl, col++, row, String.valueOf(itemno), 
                    body_opts);
                if (tbl == -1)
                    throw new Exception("Error adding cell: " + p.get_errmsg());
                
                
                /* --------------------------------------------------------------
                 * Add the Textflow cell containing the Description in the second
                 * column
                 * --------------------------------------------------------------
                 */
                          
                /* Prepare the option list for adding the Textflow.
                 * For an exact vertical alignment of the Textflow and the text
                 * lines added as well note the following:
                 * The height of an uppercase letter is exactly represented by the
                 * capheight value of the font. For this reason use the capheight in
                 * the font size specification. For example, a capheight of 8.5 will
                 * approximately result in a font size of 12 points and (along with
                 * "margin=4"), will sum up to an overall height of 16 points. 
                 */
                tf_opts = "font=" + regularfont + 
                    " fontsize={capheight=" + capheight + "} leading=" + leading; 
                
                /* Prepare the option list for adding the Textflow cell 
                 * 
                 * The first line of the Textflow should be aligned with the
                 * baseline of the text lines. At the same time, the text lines 
                 * should have the same distance from the top cell border as the 
                 * Textflow. To avoid any space from the top add the Textflow cell
                 * using "fittextflow={firstlinedist=capheight}". Then add a margin
                 * of 4 points, the same as for the text lines.
                 */
                final String bodytf_opts = "fittextflow={firstlinedist=capheight}" + 
                    " colwidth=" + c2 + " margin=" + margin;
                
                /* Add the Textflow with the options defined above */
                tf = p.add_textflow(-1, items[itemno-1][0], tf_opts);
                
                if (tf == -1)
                    throw new Exception("Error: " + p.get_errmsg());
                    
                /* Add the Textflow table cell with the options defined above */
                tbl = p.add_table_cell(tbl, col++, row, "", 
                    bodytf_opts + " textflow=" + tf);
                    
                if (tbl == -1)
                    throw new Exception("Error adding cell: " + p.get_errmsg());
                
                tf = -1;
                
                            
                /* -----------------------------------------------------------
                 * Add the text line cell containing the Quantity in the third
                 * column, with the options defined for table body cells above
                 * -----------------------------------------------------------
                 */
                tbl = p.add_table_cell(tbl, col++, row, items[itemno-1][1], 
                    body_opts);
                if (tbl == -1)
                    throw new Exception("Error adding cell: " + p.get_errmsg());
                
                
                /* -----------------------------------------------------------
                 * Add the text line cell containing the Price in the third
                 * column, with the options defined for table body cells above
                 * -----------------------------------------------------------
                 */
                tbl = p.add_table_cell(tbl, col++, row, items[itemno-1][2], 
                    body_opts);
                if (tbl == -1)
                    throw new Exception("Error adding cell: " + p.get_errmsg());
                
                
                /* ---------------------------------------------------------------
                 * Add the text line cell containing the sum with the options 
                 * defined for table body cells above. Format them to a maximum of 
                 * two fraction digits.
                 * ---------------------------------------------------------------
                 */
                sum = Double.valueOf(items[itemno-1][1]).doubleValue() * 
                      Double.valueOf(items[itemno-1][2]).doubleValue();
                
                value = new BigDecimal(sum);
                roundedValue = value.setScale(2, RoundingMode.HALF_UP);
                               
                tbl = p.add_table_cell(tbl, col, row, 
                                       roundedValue.toString(), body_opts);
                
                if (tbl == -1)
                    throw new Exception("Error adding cell: " + p.get_errmsg());
                    
                /* Calculate the overall sum */
                total += sum;
            } /* for */
            
            /* Add an empty footer row containing a matchbox called "subtotal".
             * It will be filled with the subtotal or total later. The matchbox 
             * starts in the column before last and spans two columns.
             */
            final String footer_opts = 
                "rowheight=" + rowheight + " colspan=2 margin =" + margin +
                " matchbox={name=subtotal}";
            
            tbl = p.add_table_cell(tbl, maxcol-1, row, "", footer_opts + "");
                      
            if (tbl == -1)
                throw new Exception("Error adding cell: " + p.get_errmsg());
       
                
            /* ------------------------------------
             * Place the table on one or more pages
             * ------------------------------------
             */

            /* Loop until all of the table is placed; create new pages as long as
             * more table instances need to be placed
             */
            do {
                /* The first row is the header row which will be repeated on each
                 * new page. The last row is the footer and will be repeated on each
                 * new page. The header row is filled with a light blue, and the
                 * footer row is filled with a light orange. Each odd row is filled
                 * with a light gray.
                 */
                final String fit_opts = 
                    "header=" + nheaders + " footer=" + nfooters + 
                    " fill={{area=rowodd fillcolor={gray 0.9}} " +
                    "{area=header fillcolor={rgb 0.90 0.90 0.98}} " +
                    "{area=footer fillcolor={rgb 0.98 0.92 0.84}}}";

                /* Place the table instance */
                result = p.fit_table(tbl, llx, lly, urx, y, fit_opts);
                
                /* An error occurred or the table's fitbox is too small to keep any
                 * contents 
                 */
                if (result.equals("_error"))
                    throw new Exception ("Couldn't place table : " +
                        p.get_errmsg());
                
                /* If all rows have been placed output the total in the matchbox
                 * defined for the footer row. Since the matchbox cannot be supplied
                 * directly to fit_textline(), we retrieve the matchbox coordinates
                 * and fit the text accordingly.
                 */
                if (!result.equals("_boxfull")) {
                    /* Format the total to a maximum of two fraction digits */
                    value = new BigDecimal(total);
                    roundedValue = value.setScale(2, RoundingMode.HALF_UP);
                    String contents = "total:   " + roundedValue;
                    
                    /* Retrieve the coordinates of the third (upper right) corner of
                     * the "subtotal" matchbox. The parameter "1" indicates the 
                     * first instance of the matchbox.
                     */
                    double x3 = 0, y3 = 0;
                                 
                    if ((int) p.info_matchbox("subtotal", 1, "exists") == 1) {
                        x3 = p.info_matchbox("subtotal", 1, "x3");
                        y3 = p.info_matchbox("subtotal", 1, "y3");
                    }
                    else {
                        throw new Exception("Error: " + p.get_errmsg());
                    }
                    
                    /* Start the text line at the corner coordinates retrieved
                     * (x2, y2) with a small margin. Right-align the text.
                     */
                    p.setfont(boldfont, fontsize);
                    p.fit_textline(contents, x3 - margin, y3 - margin,
                        "position={right top}");
                }
                
                /* Print the subtotal for all rows in the table instance on the
                 * current page below the last table column before we place the 
                 * remaining rows on the next page 
                 */
                else if (result.equals("_boxfull")) {
                    /* Get the last body row output in the table instance */
                    double lastrow = p.info_table(tbl, "lastbodyrow");
                    
                    /* Calculate the subtotal */
                    for (i = 0, subtotal = 0; i < lastrow - nfooters; i++) {
                        subtotal += Double.valueOf(items[i][1]).doubleValue() * 
                            Double.valueOf(items[i][2]).doubleValue();
                    }
                   
                    /* Output the subtotal in the matchbox defined for the footer 
                     * row. Since the matchbox cannot be directly referenced we 
                     * retrieve the matchbox coordinates and fit the text
                     *  accordingly.
                     */
                    
                    /* Format the subtotal to a maximum of two fraction digits*/
                    value = new BigDecimal(subtotal);
                    roundedValue = value.setScale(2, RoundingMode.HALF_UP);
                    
                    String contents = "subtotal:   " + roundedValue;
                    
                    /* Retrieve the coordinates of the third (upper right) corner of
                     * the "subtotal" matchbox. The parameter "1" indicates the 
                     * first instance of the matchbox.
                     */
                    double x3 = 0, y3 = 0;
                    
                    if ((int) p.info_matchbox("subtotal", 1, "exists") == 1) {
                        x3 = p.info_matchbox("subtotal", 1, "x3");
                        y3 = p.info_matchbox("subtotal", 1, "y3");
                    }
                    else {
                        throw new Exception("Error: " + p.get_errmsg());
                    }
                    
                    /* Start the text line at the corner coordinates retrieved in
                     * (x3, y3) with a small margin. Right-align the text.
                     */
                    p.setfont(boldfont, fontsize);
                    p.fit_textline(contents, x3 - margin, y3 - margin,
                        "position={right top}");
            
                    /* Output the "Continued" remark */               
                    p.setfont(regularfont, fontsize);
                    p.fit_textline("-- Continued --", urx, ycontinued, 
                        "position {right top}");
                    
                    p.end_page_ext("");
                    p.begin_page_ext(pagewidth, pageheight, "");
                    y = ystart;
                }
            } while (result.equals("_boxfull"));
            
            
            /* -----------------------------------------------
             * Place the closing text directly after the table
             * -----------------------------------------------
             */
            
            /* Get the table height of the current table instance */
            tabheight = p.info_table(tbl, "height");
            
            y = y - (int) tabheight - yoffset;
                    
            /* Add the closing Textflow to be placed after the table */
            tf_opts = "font=" + regularfont + " fontsize=" + fontsize +
                " leading=" + leading + " alignment=justify";
            
            tf = p.add_textflow(-1, closingtext, tf_opts);
            if (tf == -1)
                throw new Exception("Error: " + p.get_errmsg());
            
            /* Loop until all text has been fit which is indicated by the "_stop"
             * return value of fit_textflow()
             */
            do {
                /* Place the Textflow */
                result = p.fit_textflow(tf, llx, lly, urx, y, "");
                
                if (result.equals("_error"))
                    throw new Exception ("Couldn't place table : " +
                        p.get_errmsg());
                
                if (result.equals("_boxfull") || result.equals("_boxempty")) {
                    p.setfont(regularfont, fontsize);
                    p.fit_textline("-- Continued --", urx, ycontinued,
                        "position {right top}");
                    
                    p.end_page_ext("");
                    p.begin_page_ext(pagewidth, pageheight, "");
                    y = ystart;
                }
            } while (!result.equals("_stop"));
            
            p.end_page_ext("");
           
            p.end_document("");
            p.close_pdi_document(stationery);

        } catch (PDFlibException e) {
            System.err.println("PDFlib exception occurred:");
            System.err.println("[" + e.get_errnum() + "] " + e.get_apiname() +
                ": " + e.get_errmsg());
            exitcode = 1;
        } catch (Exception e) {
            System.err.println(e.toString());
            exitcode = 1;
        } finally {
            if (p != null) {
                p.delete();
            }
            System.exit(exitcode);
        }
    }
}