PDFlib Cookbook

cookbook

pdfvt/starter_pdfvt2s

Download Java Code     Show Output      Show Input Files

/* $Id: starter_pdfvt2s.java,v 1.20 2016/12/21 17:20:44 tm Exp $ */

package com.pdflib.cookbook.pdflib.pdfvt;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.io.StringWriter;
import java.io.UnsupportedEncodingException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.text.DateFormat;
import java.text.DecimalFormat;
import java.text.NumberFormat;
import java.util.Date;
import java.util.Calendar;
import java.util.HashMap;
import java.util.Locale;
import java.util.Random;

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

/**
 * Starter sample for PDF/VT-2s
 *
 * This is a variation of the starter_pdfvt2 sample program. It wraps all the 
 * data into a PDF/VT-2s-compliant MIME package, according to the 
 * ISO 16612-2:2010 specification, Annex A, "Use of multipart MIME for
 * streamed generation of PDF/VT data".
 * 
 * For demonstration purposes the MIME package is stored in the file system.
 * In a real-world application it could be streamed directly to the
 * consuming output device.
 * 
 * Experiments showed that various tools that can be used to inspect MIME
 * packages, like WinZip or Mozilla Thunderbird, do not handle MIME parts
 * correctly that have content transfer encoding set to "binary". The tools
 * performed line-end conversion on the binary data and rendered the files
 * unusable. Therefore this sample program writes the MIME parts by default
 * with content transfer encoding "base64". To produce MIME parts with
 * content transfer encoding "binary", set the variable "USE_BASE64_ENC" to
 * false. Content transfer encoding "binary" is the encoding that is
 * recommended by ISO 16612-2:2010.
 * 
 * For performing the base64 encoding some code from the Apache Commons
 * library is included (see below).
 *
 * Required software: PDFlib+PDI/PPS 9
 * Required data: PDF/X-4p input documents, fonts
 */
public class starter_pdfvt2s {

    /**
     * Stationery document
     */
    private static final String STATIONERY = "stationery_pdfx4p.pdf";

    /**
     * Name of the referenced ICC profile. Must be the same for all PDF/VT-2
     * documents in the MIME package.
     */
    private static final String ICC_PROFILE_FILENAME = "ISOcoated_v2_eci.icc";

    /**
     * Nodenamelist for the document part hierarchy in the PDF/VT-2 files. Must
     * be the same for all PDF/VT-2 files in the MIME package.
     */
    private static final String NODENAMELIST = "root recipient";
    
    /**
     * Output file name for PDF/VT-2s MIME package.
     */
    private static final String MIME_FILE_NAME = "starter_pdfvt2s.uue";

    /**
     * Character set for writing the MIME headers.
     */
    private static final String MIME_HEADER_CHARSET = "US-ASCII";

    /*
     * Some constants for producing the MIME headers.
     */
    private static final String PDFVT_STREAM_VERSION_HEADER_FIELD =
                                        "X-PDFVT-Stream-version";
    private static final String CONTENT_TYPE_HEADER_FIELD = "Content-Type";
    private static final String CONTENT_DISPOSITION_HEADER_FIELD =
                                        "Content-Disposition";
    private static final String CONTENT_TRANSFER_ENCODING_HEADER_FIELD =
                                        "Content-Transfer-Encoding";
    private static final String MIME_VERSION_HEADER_FIELD = "MIME-Version";
    private static final String BOUNDARY_MARKER = "--";

    /**
     * If set to true, use content transfer encoding "base64", otherwise
     * use "binary". The latter is recommended by ISO 16612-2:2010. 
     */
    private static final boolean USE_BASE64_ENC = true;
    
    /**
     * Where to find the input files.
     */
    private static final String INPUT_DIR = "../input";

    /**
     * Number of PDF/VT-2 files in the PDF/VT-2s stream.
     */
    private static final int PDFVT2_COUNT = 3;
    
    /**
     * Map of file names to hash values for adding unique identifiers to the
     * Reference XObjects.
     */
    static HashMap<String, String> filenameXidMap =
                                new HashMap<String, String>();
    
    /**
     * Digest object used for the computation of unique ids for the referenced
     * files.
     */
    static MessageDigest digest;
    
    public static void main(String argv[])
            throws UnsupportedEncodingException, NoSuchAlgorithmException {
        pdflib p = null;
        digest = MessageDigest.getInstance("MD5");
	int exitcode = 0;

        /* This is where font/image/PDF input files live. Adjust as necessary. */
        String searchpath = INPUT_DIR;

        try {
            p = new pdflib();
            
            /* This means we must check return values of load_font() etc. */
            p.set_option("errorpolicy=return");
            
            p.set_option("searchpath={" + searchpath + "}");

            /*
             * The MIME package is written to a file, but it could be streamed
             * to a socket as well.
             */
            OutputStream mime_file = new FileOutputStream(MIME_FILE_NAME);
            make_file(mime_file, p);
            mime_file.close();
        }
        catch (FileNotFoundException e) {
            System.err.println("Unable to open file: " + e.getMessage());
	    exitcode = 1;
        }
        catch (IOException e) {
            System.err.println("IOException occurred: " + e.getMessage());
	    exitcode = 1;
        }
        catch (Exception e) {
            System.err.println("Exception occurred: " + e.getMessage());
	    exitcode = 1;
        }
        finally {
            if (p != null) {
                p.delete();
            }
	    System.exit(exitcode);
        }
    }

    /**
     * Produce the MIME package and write it to the given OutputStream.
     * 
     * @param mime_package
     *            the OutputStream for the MIME package
     * @param p 
     *            
     * @throws Exception
     * @throws PDFlibException
     */
    private static void make_file(OutputStream mime_package, pdflib p)
        throws Exception, PDFlibException {
        final String main_boundary = new_boundary();

        write_header_field(mime_package, MIME_VERSION_HEADER_FIELD, "1.0");
        write_header_field(mime_package, PDFVT_STREAM_VERSION_HEADER_FIELD, "1");
        write_header_field(mime_package, CONTENT_TYPE_HEADER_FIELD,
            "multipart/mixed; boundary=\"" + main_boundary + "\"");

        crlf(mime_package);

        /*
         * Write the ICC profile.
         */
        begin_part(mime_package, main_boundary);
        write_resource_part(mime_package, "application/vnd.iccprofile",
            ICC_PROFILE_FILENAME);

        /*
         * Write the stationery document.
         */
        begin_part(mime_package, main_boundary);
        write_resource_part(mime_package, "application/pdf", STATIONERY);

        /*
         * Write the sales rep PDF documents.
         */
        int i;
        for (i = 0; i < salesrepnames.length; i++) {
            begin_part(mime_package, main_boundary);
            write_resource_part(mime_package, "application/pdf",
                get_salesrep_filename(i));
        }

        /*
         * Write several PDF/VT-2 documents to the PDF/VT-2s stream, that
         * reference the ICC profile and the sales rep documents. They need to
         * be written with content disposition "inline".
         * 
         * ISO 16612-2:2010 requires that these PDF/VT-2 documents have the
         * same nodenamelist and are prepared for the same characterized
         * printing condition. This must be ensured by the application, as
         * this is outside of the scope of PDFlib.
         */
        for (i = 1; i <= PDFVT2_COUNT; i++) {
            begin_part(mime_package, main_boundary);
    
            write_header_field(mime_package, CONTENT_TYPE_HEADER_FIELD,
                "application/pdf");
            
            /*
             * The PDF/VT-2 files containing content to be directly interpreted
             * are identifed by the names "pdfvt2_direct_<n>.pdf", with <n>
             * in the range 1 to PDFVT2_COUNT.
             */
            write_header_field(mime_package, CONTENT_DISPOSITION_HEADER_FIELD,
                "inline; filename=pdfvt2_direct_" + i + ".pdf");
            
            write_content_transfer_encoding_header_field(mime_package);
            crlf(mime_package);
            
            /*
             * Write each PDF/VT-2 document with a unique Subject entry in
             * the document information dictionary, and vary the number of
             * pages.
             */
            write_main_file(mime_package, p, "PDF/VT-2 document #" + i, i * 10);
        }

        end_multipart(mime_package, main_boundary);
    }

    /**
     * Write the header field "Content-Transfer-Encoding", depending on the
     * setting of the USE_BASE64_ENCODING setting.
     * 
     * @param os        the OutputStream to write to
     * 
     * @throws UnsupportedEncodingException
     * @throws IOException
     */
    private static void write_content_transfer_encoding_header_field(
            OutputStream os)
        throws UnsupportedEncodingException, IOException {
        write_header_field(os,
            CONTENT_TRANSFER_ENCODING_HEADER_FIELD,
                        USE_BASE64_ENC ? "base64" : "binary");
    }

    /**
     * Write a resource file as a MIME part to the MIME package.
     * 
     * The file is written as an attachment with content transfer encoding
     * "binary".
     * 
     * The file is expected to exist in the directory named INPUT_DIR.
     * 
     * @param os
     *            the OutputStream to write to
     * @param content_type
     *            value for the "Content-Type" MIME header field
     * @param filename
     *            name of attachment file
     *            
     * @throws Exception 
     */
    private static void write_resource_part(OutputStream os,
        String content_type, String filename) throws Exception {
        write_header_field(os, CONTENT_TYPE_HEADER_FIELD, content_type);
        write_header_field(os, CONTENT_DISPOSITION_HEADER_FIELD,
            "attachment; filename=" + filename);
        write_content_transfer_encoding_header_field(os);
        crlf(os);

        copy(os, filename);
    }

    private static void copy(OutputStream os, String filename)
            throws IOException, FileNotFoundException, Exception {
        if (USE_BASE64_ENC) {
            copyBase64(os, filename);
        }
        else {
            copyBinary(os, filename);
        }
    }
    
    /**
     * For staying Java 1.4 compliant, we convert bytes to hex digits directly.
     */
    private final static String HEX_DIGITS = "0123456789abcdef";
    
    /**
     * Copy the given file in binary form to the output stream, and create
     * a hash of the file contents which is stored in filenameXidMap. 
     * 
     * @param os
     *            target OutputStream of copy operation
     * @param filename
     *            file to copy from INPUT_DIR to the output stream
     * @throws FileNotFoundException 
     * @throws Exception 
     * 
     * @throws NoSuchAlgorithmException
     */
    private static void copyBinary(OutputStream os, String filename)
        throws IOException, FileNotFoundException, Exception {
        /*
         * Perform check that a file with the same name was not yet copied
         * to the output stream. This is considered a fatal error, as it is
         * not clear which file should be referenced by the name.
         */
        if (filenameXidMap.containsKey(filename)) {
            throw new Exception("Duplicate input filename detected");
        }
        
        FileInputStream infile = new FileInputStream(new File(INPUT_DIR, filename));
        final byte buffer[] = new byte[1024];
        int bytes_read;

        digest.reset();
        
        while ((bytes_read = infile.read(buffer)) != -1) {
            digest.update(buffer);
            os.write(buffer, 0, bytes_read);
        }
        
        infile.close();
        
        byte hash[] = digest.digest();
        
        /*
         * Convert byte array to a string that looks like a
         * "uuid-schemed URI as defined in RFC 4122". This is the form that is
         * recommended in ISO 16612-2:2010,
         * "6.7.2 Unique identification of an XObject".
         */
        StringWriter uuidString = new StringWriter();
        uuidString.write("uuid:");
        for (int i = 0; i < hash.length; i += 1) {
            int hi_nibble = (int) (hash[i] & 0xf0) >> 4;
            uuidString.write(HEX_DIGITS.substring(hi_nibble, hi_nibble + 1));
            int lo_nibble = (int) hash[i] & 0xf;
            uuidString.write(HEX_DIGITS.substring(lo_nibble, lo_nibble + 1));
            if (i == 3 || i == 5 || i == 7 || i == 9)
            {
                uuidString.write("-");
            }
        }
        
        filenameXidMap.put(filename, uuidString.toString());
    }
    
    /**
     * Copy the given file in binary form to the output stream, and create
     * a hash of the file contents which is stored in filenameXidMap. 
     * 
     * @param os
     *            target OutputStream of copy operation
     * @param filename
     *            file to copy from INPUT_DIR to the output stream
     * @throws Exception 
     * 
     * @throws NoSuchAlgorithmException 
     */
    private static void copyBase64(OutputStream os, String filename)
                throws Exception {
        /*
         * Perform check that a file with the same name was not yet copied
         * to the output stream. This is considered a fatal error, as it is
         * not clear which file should be referenced by the name.
         */
        if (filenameXidMap.containsKey(filename)) {
            throw new Exception("Duplicate input filename detected");
        }
        
        FileInputStream infile = new FileInputStream(new File(INPUT_DIR, filename));
        final byte buffer[] = new byte[1024];
        int bytes_read;
        ByteArrayOutputStream intermediate = new ByteArrayOutputStream();
        
        digest.reset();
        
        while ((bytes_read = infile.read(buffer)) != -1) {
            digest.update(buffer);
            intermediate.write(buffer, 0, bytes_read);
        }
        
        infile.close();
        
        os.write(encodeBase64Chunked(intermediate.toByteArray()));
        
        byte hash[] = digest.digest();
        
        /*
         * Convert byte array to a string that looks like a
         * "uuid-schemed URI as defined in RFC 4122". This is the form that is
         * recommended in ISO 16612-2:2010,
         * "6.7.2 Unique identification of an XObject".
         */
        StringWriter uuidString = new StringWriter();
        uuidString.write("uuid:");
        for (int i = 0; i < hash.length; i += 1) {
            int hi_nibble = (int) (hash[i] & 0xf0) >> 4;
            uuidString.write(HEX_DIGITS.substring(hi_nibble, hi_nibble + 1));
            int lo_nibble = (int) hash[i] & 0xf;
            uuidString.write(HEX_DIGITS.substring(lo_nibble, lo_nibble + 1));
            if (i == 3 || i == 5 || i == 7 || i == 9)
            {
                uuidString.write("-");
            }
        }
        
        filenameXidMap.put(filename, uuidString.toString());
    }

    /**
     * Write the MIME marker for the beginning of a MIME part.
     * 
     * @param mime_package
     *            OutputStream for MIME package
     * @param boundary
     *            boundary string
     * 
     * @throws UnsupportedEncodingException
     * @throws IOException
     */
    private static void begin_part(OutputStream mime_package, String boundary)
        throws UnsupportedEncodingException, IOException {
        crlf(mime_package);
        mime_package.write(BOUNDARY_MARKER.getBytes(MIME_HEADER_CHARSET));
        mime_package.write(boundary.getBytes(MIME_HEADER_CHARSET));
        crlf(mime_package);
    }

    /**
     * Write the final MIME marker for the end of the MIME multipart package.
     * 
     * @param mime_package
     *            OutputStream for MIME package
     * @param boundary
     *            boundary string
     * 
     * @throws UnsupportedEncodingException
     * @throws IOException
     */
    private static void end_multipart(OutputStream mime_package, String boundary)
        throws UnsupportedEncodingException, IOException {
        crlf(mime_package);
        mime_package.write(BOUNDARY_MARKER.getBytes(MIME_HEADER_CHARSET));
        mime_package.write(boundary.getBytes(MIME_HEADER_CHARSET));
        mime_package.write(BOUNDARY_MARKER.getBytes(MIME_HEADER_CHARSET));
        crlf(mime_package);
    }

    /**
     * Write a single MIME header field.
     * 
     * @param os
     *            OutputStream for MIME package
     * @param name
     *            field name
     * @param value
     *            value of the field
     * 
     * @throws UnsupportedEncodingException
     * @throws IOException
     */
    private static void write_header_field(OutputStream os, String name,
        String value) throws UnsupportedEncodingException, IOException {
        os.write(name.getBytes(MIME_HEADER_CHARSET));
        os.write(": ".getBytes(MIME_HEADER_CHARSET));
        os.write(value.getBytes(MIME_HEADER_CHARSET));
        crlf(os);
    }

    /**
     * Write a CRLF sequence to the output stream.
     * 
     * @param os
     *            the OutputStream
     * 
     * @throws IOException
     * @throws UnsupportedEncodingException
     */
    private static void crlf(OutputStream os) throws IOException,
        UnsupportedEncodingException {
        os.write("\r\n".getBytes(MIME_HEADER_CHARSET));
    }

    /**
     * Characters for generating a pseudo-random MIME boundary string.
     */
    private static final String BOUNDARY_CHARS =
        "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";

    /**
     * Length of the pseudo-random MIME boundary string.
     */
    private static final int BOUNDARY_LENGTH = 20;

    /**
     * Return a new boundary string.
     */
    private static String new_boundary() {
        final int charset_size = BOUNDARY_CHARS.length();
        int i;

        StringWriter result = new StringWriter();

        result.write("----------");
        for (i = 0; i < BOUNDARY_LENGTH - 1; i += 1) {
            final int index = get_random(charset_size);
            result.write(BOUNDARY_CHARS.substring(index, index + 1));
        }

        return result.toString();
    }
    
    final static Random random = new Random();

    static class articledata_s {
        articledata_s(String name, double price) {
            this.name = name;
            this.price = price;
        }

        String name;
        double price;
    };

    static class addressdata_s {
        addressdata_s(String firstname, String lastname, String flat,
                String street, String city) {
            this.firstname = firstname;
            this.lastname = lastname;
            this.flat = flat;
            this.street = street;
            this.city = city;
        }

        String firstname;
        String lastname;
        String flat;
        String street;
        String city;
    };

    private static final String[] salesrepnames = { 
        "Charles Ragner", "Hugo Baldwin",
        "Katie Blomock", "Ernie Bastel", "Lucy Irwin", "Bob Montagnier",
        "Chuck Hope", "Pierre Richard" 
    };
    
    private static final String[] headers = {
        "ITEM", "DESCRIPTION", "QUANTITY", "PRICE", "AMOUNT"
    };

    private static final String[] alignments = {
        "right", "left", "right", "right", "right"
    };
    
    static final int MATRIXROWS = 32;
    static final int MATRIXDATASIZE = 4 * MATRIXROWS;

    /**
     * Write a PDF/VT-2s file to the MIME stream.
     * 
     * @param os
     *            the OutputStream for the MIME package
     * @param p
     *            the pdflib object
     * @param subject
     *            value for the "Subject" field in the document information
     *            dictionary
     * @param records
     *            number of records to generate
     *            
     * @throws Exception 
     * @throws IOException 
     * @throws PDFlibException 
     */
    private static void write_main_file(OutputStream os,
            pdflib p, String subject, int records)
            throws PDFlibException, IOException, Exception {
        int i;
        int record;
        int barcodeimage;
        String fontname = "DejaVuSerif";
        final String title = "Starter PDF/VT-2s";

        double left = 55;
        double right = 530;
        double bottom = 822;

        double fontsize = 12, leading, x, y;
        String buf;
        String optlist;
        String fontoptions;

        String closingtext =
            "Terms of payment: <save fillcolor={cmyk 0 1 1 0}>30 days net<restore>. "
                + "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.";

        articledata_s articledata[] = {
            new articledata_s("Super Kite", 20),
            new articledata_s("Turbo Flyer", 40),
            new articledata_s("Giga Trash", 180),
            new articledata_s("Bare Bone Kit", 50),
            new articledata_s("Nitty Gritty", 20),
            new articledata_s("Pretty Dark Flyer", 75),
            new articledata_s("Large Hadron Glider", 85),
            new articledata_s("Flying Bat", 25),
            new articledata_s("Simple Dimple", 40),
            new articledata_s("Mega Sail", 95),
            new articledata_s("Tiny Tin", 25),
            new articledata_s("Monster Duck", 275),
            new articledata_s("Free Gift", 0)
        };

        addressdata_s addressdata[] = {
            new addressdata_s("Edith", "Poulard", "Suite C", "Main Street",
                    "New York"),
            new addressdata_s("Max", "Huber", "", "Lipton Avenue",
                    "Albuquerque"),
            new addressdata_s("Herbert", "Pakard", "App. 29", "Easel",
                    "Duckberg"),
            new addressdata_s("Charles", "Fever", "Office 3", "Scenic Drive",
                    "Los Angeles"),
            new addressdata_s("D.", "Milliband", "", "Old Harbour", "Westland"),
            new addressdata_s("Lizzy", "Tin", "Workshop", "Ford", "Detroit"),
            new addressdata_s("Patrick", "Black", "Backside",
                    "Woolworth Street", "Clover")
        };

        int dpm = 0, cip4_root, cip4_metadata;

        leading = fontsize + 2;

        /*
         * Produce document in memory for writing in chunks to the OutputStream.
         * ISO 16612-2:2010 requires that the same nodenamelist value is used
         * for all PDF/VT-2 files in the PDF/VT-2s stream. This must be ensured
         * by the application and is outside of the scope of PDFlib.
         */
        if (p.begin_document("",
                "pdfvt=PDF/VT-2 pdfx=PDF/X-5pg usestransparency=true "
                + "nodenamelist={" + NODENAMELIST + "} "
                + "recordlevel=1") == -1) {
            throw new Exception("Error: " + p.get_errmsg());
        }

        p.set_info("Creator", "PDFlib Cookbook");
        p.set_info("Title", title + " $Revision: 1.20 $");
        p.set_info("Subject", subject);

        fontoptions = "fontname=" + fontname + " fontsize=" + fontsize
            + " embedding encoding=unicode";
        
        /*
         * Define output intent profile. ISO 16612-2:2010 requires that the same
         * output intent is used for all PDF/VT-2 files in the PDF/VT-2s stream.
         * This must be ensured by the application and is outside of the scope
         * of PDFlib.
         */
        if (p.load_iccprofile(ICC_PROFILE_FILENAME,
                "usage=outputintent urls={http://www.color.org}") == -1) {
            System.err.print("Error: " + p.get_errmsg() + "\n");
            System.err.print("Please install the ICC profile package from "
                    + "www.pdflib.com to run the PDF/VT-2 starter sample.\n");
            throw new Exception("ICC profile not found");
        }

        /*
         * ----------------------------------- 
         * Load company stationery as background (used 
         * on first page for each recipient) by reference and 
         * construct proxy for it
         * 
         * For demonstration purposes we assume that there is a specific
         * PDF/VT environment context for stationery documents, and
         * therefore pass the string "Stationery Environment Context" for
         * the PDF/VT environment context.
         * -----------------------------------
         */
        final int proxy_stationery = make_proxy(p, STATIONERY,
                                        "Proxy for stationery",
                                        "Stationery Environment Context");
        if (proxy_stationery == -1)
        {
            throw new Exception("Error: " + p.get_errmsg());
        }

        /*
         * ----------------------------------- 
         * Preload PDF images of all local sales reps (used on first page
         * for each recipient) by reference and construct proxy for it
         * -----------------------------------
         */
        final int proxy_salesrepimage[] = new int[salesrepnames.length];
        for (i = 0; i < salesrepnames.length; i++) {
            final String description = "Proxy for sales rep image " + i;
            final String salesrepfilename = get_salesrep_filename(i);
            
            /*
             * For demonstration purposes we assume that theres a separate
             * PDF/VT environment context for the sales rep image
             * documents.
             */
            proxy_salesrepimage[i] =
                make_proxy(p, salesrepfilename, description,
                                "Sales Rep Image Environment Context");

            if (proxy_salesrepimage[i] == -1) {
                throw new Exception("Proxy error: " + p.get_errmsg());
            }
        }

        final int ARTICLECOUNT = articledata.length;
        final int ADDRESSCOUNT = addressdata.length;
        final int COLUMNCOUNT = headers.length;
        final int SALESREPS = salesrepnames.length;
        
        /*
         * ----------------------------------- 
         * Construct DPM metadata for the DPart 
         * root node
         * -----------------------------------
         */
        dpm = p.poca_new("containertype=dict usage=dpm");
        cip4_root = p.poca_new("containertype=dict usage=dpm");
        cip4_metadata = p.poca_new("containertype=dict usage=dpm");

        optlist = "type=dict key=CIP4_Root value=" + cip4_root;
        p.poca_insert(dpm, optlist);

        optlist = "type=dict key=CIP4_Metadata value=" + cip4_metadata;
        p.poca_insert(cip4_root, optlist);

        p.poca_insert(cip4_metadata,
                "type=string key=CIP4_Conformance value=base");
        p.poca_insert(cip4_metadata,
                "type=string key=CIP4_Creator value=starter_pdfvt2");
        p.poca_insert(cip4_metadata,
                "type=string key=CIP4_JobID value={Kraxi Systems invoice}");

        /* Create root node in the DPart hierarchy and add DPM metadata */
        optlist = "dpm=" + dpm;
        p.begin_dpart(optlist);

        p.poca_delete(dpm, "recursive=true");

        /*
         * If base64 encoding is switched on, buffer the output for being able
         * to encode the whole output as base64 at the end.
         */
        ByteArrayOutputStream bos = null;
        OutputStream intermediate;
        if (USE_BASE64_ENC)
        {
            intermediate = bos = new ByteArrayOutputStream();
        }
        else
        {
            intermediate = os;
        }
        intermediate.write(p.get_buffer());
        
        DecimalFormat zip_code_format = new DecimalFormat("00000");
        
        NumberFormat priceFormat = NumberFormat.getInstance(Locale.US);
        priceFormat.setMaximumFractionDigits(2);
        priceFormat.setMinimumFractionDigits(2);
        
        for (record = 0; record < records; record++) {
            byte datamatrix[] = new byte[MATRIXDATASIZE];
            int cip4_recipient, cip4_contact, cip4_person;
            String firstname, lastname, result;
            int pagecount=0;
            int item;

            firstname = addressdata[get_random(ADDRESSCOUNT)].firstname;
            lastname = addressdata[get_random(ADDRESSCOUNT)].lastname;

            /*
             * ----------------------------------- 
             * Construct DPM metadata for the next 
             * DPart node (i.e. the page)
             * -----------------------------------
             */
            dpm = p.poca_new("containertype=dict usage=dpm");
            cip4_root = p.poca_new("containertype=dict usage=dpm");
            cip4_recipient = p.poca_new("containertype=dict usage=dpm");
            cip4_contact = p.poca_new("containertype=dict usage=dpm");
            cip4_person = p.poca_new("containertype=dict usage=dpm");

            optlist = "type=dict key=CIP4_Root value=" + cip4_root;
            p.poca_insert(dpm, optlist);

            optlist = "type=dict key=CIP4_Recipient value="
                    + cip4_recipient;
            p.poca_insert(cip4_root, optlist);

            optlist = "type=string key=CIP4_UniqueID value={ID_" + record
                    + "}";
            p.poca_insert(cip4_recipient, optlist);

            optlist = "type=dict key=CIP4_Contact value=" + cip4_contact;
            p.poca_insert(cip4_recipient, optlist);

            optlist = "type=dict key=CIP4_Person value=" + cip4_person;
            p.poca_insert(cip4_contact, optlist);

            optlist = "type=string key=CIP4_Firstname value={" + firstname
                    + "}";
            p.poca_insert(cip4_person, optlist);

            optlist = "type=string key=CIP4_Lastname value={" + lastname
                    + "}";
            p.poca_insert(cip4_person, optlist);

            /*
             * Create a new node in the document part hierarchy and add DPM
             * metadata
             */
            optlist = "dpm=" + dpm;
            p.begin_dpart(optlist);

            p.poca_delete(dpm, "recursive=true");

            /* -----------------------------------
             * Create and place table with article list
             * -----------------------------------
             */
            /* ---------- Header row */
            int row = 1, col;
            int tbl = -1;

            for (col=1; col <= COLUMNCOUNT; col++)
            {
                optlist = "fittextline={position={" + alignments[col-1]
                    + " center} " + fontoptions + "} margin=2";
                tbl = p.add_table_cell(tbl, col, row, headers[col-1],
                                        optlist);
            }
            row++;

            /* ---------- Data rows: one for each article */
            double total = 0;

            /* -----------------------------------
             * Print variable-length article list
             * -----------------------------------
             */
            for (i = 0, item = 0; i < ARTICLECOUNT; i++) {
                int quantity = get_random(9) + 1;
                double sum;

                if ((get_random(2) % 2) != 0)
                    continue;

                col = 1;

                item++;
                sum = articledata[i].price * quantity;

                /* column 1: ITEM */
                buf = "" + item;
                optlist = "fittextline={position={" + alignments[col-1]
                        + " center} " + fontoptions
                        + "} colwidth=5% margin=2";
                tbl = p.add_table_cell(tbl, col++, row, buf, optlist);

                /* column 2: DESCRIPTION */
                optlist = "fittextline={position={" + alignments[col-1]
                        + " center} " + fontoptions
                        + "} colwidth=50% margin=2";
                tbl = p.add_table_cell(tbl, col++, row,
                        articledata[i].name, optlist);

                /* column 3: QUANTITY */
                buf = "" + quantity;
                optlist = "fittextline={position={" + alignments[col-1]
                        + " center} " + fontoptions + "} margin=2";
                tbl = p.add_table_cell(tbl, col++, row, buf, optlist);

                /* column 4: PRICE */
                buf = priceFormat.format(articledata[i].price);
                optlist = "fittextline={position={" + alignments[col-1]
                        + " center} " + fontoptions + "} margin=2";
                tbl = p.add_table_cell(tbl, col++, row, buf, optlist);

                /* column 5: AMOUNT */
                buf = priceFormat.format(sum);
                optlist = "fittextline={position={" + alignments[col-1]
                        + " center} " + fontoptions + "} margin=2";
                tbl = p.add_table_cell(tbl, col++, row, buf, optlist);

                total += sum;
                row++;
            }

            /* ---------- Print total in the rightmost column */
            buf = priceFormat.format(total);
            optlist = "fittextline={position={" + alignments[COLUMNCOUNT-1]
                    + " center} " + fontoptions + "} margin=2";
            tbl = p.add_table_cell(tbl, COLUMNCOUNT, row++, buf, optlist);


            /* ---------- Footer row with terms of payment */
            optlist = fontoptions + " alignment=justify leading=120%";
            int tf = p.create_textflow(closingtext, optlist);

            optlist = "rowheight=1 margin=2 margintop=" + 2*fontsize
                    + " textflow=" + tf + " colspan=" + COLUMNCOUNT;
            tbl = p.add_table_cell(tbl, 1, row++, "", optlist);


            /* ----- Place the table instance(s), creating pages as required */
            do {
                double top;

                p.begin_page_ext(0, 0,
                        "topdown=true width=a4.width height=a4.height");

                if (++pagecount == 1)
                {
                    /*
                     * ----------------------------------- 
                     * Place company stationery / proxy (template) as
                     * background on the page
                     * -----------------------------------
                     */
                    p.fit_image(proxy_stationery, 0, 842, "");

                    /* -----------------------------------
                     * Place name and image of local sales rep on first page
                     * for each recipient
                     * -----------------------------------
                     */
                    y = 177;
                    x = 455;

                    optlist = "fontname=" + fontname
                            + " encoding=winansi embedding fontsize=9";
                    p.fit_textline("Local sales rep:", x, y, optlist);
                    p.fit_textline(salesrepnames[record % SALESREPS],
                            x, y+9, optlist);

                    y = 280;
                    
                    /* Place the proxy on the page */
                    p.fit_image(
                        proxy_salesrepimage[record % salesrepnames.length],
                        x, y, "boxsize={90 90} fitmethod=meet");


                    /* -----------------------------------
                     * Address of recipient
                     * -----------------------------------
                     */
                    y = 170;

                    optlist = "fontname=" + fontname
                            + " encoding=winansi embedding fontsize="
                            + fontsize;
                    buf = firstname + " " + lastname;
                    p.fit_textline(buf, left, y, optlist);

                    y += leading;
                    p.fit_textline(addressdata[get_random(ADDRESSCOUNT)].flat,
                            left, y, optlist);

                    y += leading;
                    buf = get_random(999) + " "
                        + addressdata[get_random(ADDRESSCOUNT)].street;
                    p.fit_textline(buf, left, y, optlist);

                    y += leading;
                    buf = zip_code_format.format(get_random(99999))
                        + " " + addressdata[get_random(ADDRESSCOUNT)].city;
                    p.fit_textline(buf, left, y, optlist);


                    /*
                     * ----------------------------------- 
                     * Individual barcode image for each recipient
                     * -----------------------------------
                     */
                    create_datamatrix(datamatrix, record);
                    p.create_pvf("barcode", datamatrix, "");

                    /*
                     * The "mask" option helps us achieve GTS_Encapsulated
                     * status
                     */
                    barcodeimage = p.load_image("raw", "barcode",
                            "bpc=1 components=1 width=32 height=32 invert "
                                    + "pdfvt={scope=singleuse} mask");
                    if (barcodeimage == -1) {
                        throw new Exception("Error: " + p.get_errmsg());
                    }

                    p.fit_image(barcodeimage, 280.0, 200.0, "scale=1.5");
                    p.close_image(barcodeimage);
                    p.delete_pvf("barcode");


                    /* -----------------------------------
                     * Print header and date
                     * -----------------------------------
                     */
                    y = 300;
                    buf = "INVOICE "
                            + Calendar.getInstance().get(Calendar.YEAR)
                            + "-" + (record+1);
                    optlist = "fontname=" + fontname
                            + " encoding=winansi embedding fontsize="
                            + fontsize;
                    p.fit_textline(buf, left, y, optlist);
                    
                    buf = DateFormat.getDateInstance(DateFormat.LONG,
                                    Locale.US).format(new Date());
                    optlist = "fontname=" + fontname
                            + " encoding=winansi fontsize=" + fontsize
                            + " embedding position {100 0}";
                    p.fit_textline(buf, right, y, optlist);

                    top = y + 2*leading;
                }
                else
                {
                    top = 50;
                }

                /*
                 * Place the table on the page.
                 * Shade every other row, except the footer row.
                 */
                result = p.fit_table(tbl,
                        left, bottom, right, top,
                        "header=1 "
                        + "fill={{area=rowodd fillcolor={gray 0.9}} "
                            + "{area=rowlast fillcolor={gray 1}}} "
                        + "rowheightdefault=auto colwidthdefault=auto");

                if (result.equals("_error")) {
                    throw new Exception("Couldn't place table: "
                                            + p.get_errmsg());
                }

                p.end_page_ext("");
            } while (result.equals("_boxfull"));

            p.delete_table(tbl, "");

            /* Close node in the document part hierarchy */
            p.end_dpart("");
            
            intermediate.write(p.get_buffer());
        }

        /* Close root node in the document part hierarchy */
        p.end_dpart("");

        p.end_document("");
        
        intermediate.write(p.get_buffer());
        
        /*
         * For base64 encoding, the whole output was only buffered up to now.
         */
        if (USE_BASE64_ENC) {
            os.write(encodeBase64Chunked(bos.toByteArray()));
            bos.close();
        }
    }

    /**
     * Generate filename for the referenced PDF files containing the sales rep
     * images.
     * 
     * @param i
     *            numer of sales rep
     * 
     * @return generated filename
     */
    private static String get_salesrep_filename(int i) {
        return "sales_rep" + i + ".pdf";
    }

    /**
     * Load page 1 of the specified PDF and use it as reference for
     * a proxy which consists of a transparent crossed-out rectangle
     * of the same size.
     * 
     * @throws Exception 
     */
    static int
    make_proxy(pdflib p, String targetname, String description, String environment) throws Exception
    {
        String optlist;
        int proxy;
        double linewidth = 2;
        double width, height;
        double x1, y1, x2, y2, x3, y3, x4, y4;
        int gstate;
        
        /*
         * We use the hashes that were computed from the file contents in
         * the "copy" function for specifying unique identifiers for the
         * XObjects.
         */
        final String unique_id = filenameXidMap.get(targetname);
        if (unique_id == null) {
            throw new Exception(
                "Internal error: no unique id found for filename \"" 
                + targetname + "\"");
        }
        
        /* Create the template which will serve as proxy. The referenced
         * page (the target) is attached to the proxy.
         * The width and height parameters will be set in PDF_end_template_ext()
         * after we queried the size of the target page.
         * The "transparencygroup" option is provided to achieve GTS_Encapsulated
         * status.
         * 
         * As the referenced files are referenced from all PDF/VT-2 files
         * in the PDF/VT-2s stream, the right setting for the scope is "stream"
         * here. Specifying "scope=stream" implies that the "environment"
         * option must be specified as well.
         */
        optlist = "reference={filename=" + targetname 
            + " pagenumber=1} "
            + "pdfvt={scope=stream environment={" + environment 
                                            + "} xid={" + unique_id + "}} "
            + "transparencygroup={colorspace=devicecmyk isolated=true}";
        proxy = p.begin_template_ext(0, 0, optlist);

        if (proxy == -1)
        {
            return proxy;
        }

        /* Determine the coordinates of the target; we use it for
         * dimensioning the proxy appropriately.
         */
        x1 = p.info_image(proxy, "targetx1", "");
        y1 = p.info_image(proxy, "targety1", "");
        x2 = p.info_image(proxy, "targetx2", "");
        y2 = p.info_image(proxy, "targety2", "");
        x3 = p.info_image(proxy, "targetx3", "");
        y3 = p.info_image(proxy, "targety3", "");
        x4 = p.info_image(proxy, "targetx4", "");
        y4 = p.info_image(proxy, "targety4", "");

        width = x2 - x1;
        height = y4 - y1;

        /* Draw a transparent crossed-out rectangle to visualize the proxy.
         * Attention: if we use the exact corner points, one half of the
         * linewidth would end up outside the template, and therefore be
         * clipped.
         */
        p.setlinewidth(linewidth);
        p.set_graphics_option("dasharray={10 5}");

        /* Make the dashed crossed-out rectangle transparent so that the proxy
         * does not obscure the underlying page contents.
         */
        gstate = p.create_gstate("opacitystroke=0.25 opacityfill=0.25");
        p.set_gstate(gstate);

        p.moveto(x1 + linewidth / 2, y1 + linewidth / 2);
        p.lineto(x2 - linewidth / 2, y2 + linewidth / 2);
        p.lineto(x3 - linewidth / 2, y3 - linewidth / 2);
        p.lineto(x4 + linewidth / 2, y4 - linewidth / 2);
        p.lineto(x1 + linewidth / 2, y1 + linewidth / 2);
        p.lineto(x3 - linewidth / 2, y3 - linewidth / 2);
        p.moveto(x2 - linewidth / 2, y2 + linewidth / 2);
        p.lineto(x4 + linewidth / 2, y4 - linewidth / 2);
        p.stroke();

        double fontsize = width > 550 ? 24.0 : 48.0;
        optlist = "fontname=LuciduxSans-Oblique encoding=winansi embedding " 
                + "fontsize=" + fontsize + " fitmethod=auto position=center "
                + "boxsize={" + width + " " + height + "}";
        p.fit_textline(description, 0, 0, optlist);

        /* Make the proxy template the same size as the target page */
        p.end_template_ext(width, height);

        return proxy;
    }
    
    /**
     * Get a pseudo random number between 0 and n-1
     */
    static int get_random(int n) {
        return random.nextInt(n);
    }

    /**
     * Simulate a datamatrix barcode
     */
    static void create_datamatrix(byte datamatrix[], int record) {
        int i;

        for (i = 0; i < MATRIXROWS; i++) {
            datamatrix[4 * i + 0] = (byte) ((0xA3 + 1 * record + 17 * i) % 0xFF);
            datamatrix[4 * i + 1] = (byte) ((0xA2 + 3 * record + 11 * i) % 0xFF);
            datamatrix[4 * i + 2] = (byte) ((0xA0 + 5 * record + 7 * i) % 0xFF);
            datamatrix[4 * i + 3] = (byte) ((0x71 + 7 * record + 9 * i) % 0xFF);
        }
        for (i = 0; i < MATRIXROWS; i++) {
            datamatrix[4 * i + 0] |= 0x80;
            datamatrix[4 * i + 2] |= 0x80;
            if ((i % 2) != 0)
                datamatrix[4 * i + 3] |= 0x01;
            else
                datamatrix[4 * i + 3] &= 0xFE;
        }
        for (i = 0; i < 4; i++) {
            datamatrix[4 * (MATRIXROWS / 2 - 1) + i] = (byte) 0xFF;
            datamatrix[4 * (MATRIXROWS - 1) + i] = (byte) 0xFF;
        }
    }
    
    /*
     * The rest of the class is taken in slightly modified form from class
     * org.apache.commons.codec.binary.Base64 from the Apache Commons project.
     * 
     * Copyright 2001-2004 The Apache Software Foundation.
     *
     * 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.
     */
    
    /**
     * Chunk size per RFC 2045 section 6.8.
     *
     * <p>The {@value} character limit does not count the trailing CRLF, but counts
     * all other characters, including any equal signs.</p>
     *
     * @see <a href="http://www.ietf.org/rfc/rfc2045.txt">RFC 2045 section 6.8</a>
     */
    private static final int CHUNK_SIZE = 76;

    /**
     * Chunk separator per RFC 2045 section 2.1.
     *
     * @see <a href="http://www.ietf.org/rfc/rfc2045.txt">RFC 2045 section 2.1</a>
     */
    private static final byte[] CHUNK_SEPARATOR = "\r\n".getBytes();

    /**
     * The base length.
     */
    private static final int BASELENGTH = 255;

    /**
     * Lookup length.
     */
    private static final int LOOKUPLENGTH = 64;

    /**
     * Used to calculate the number of bits in a byte.
     */
    private static final int EIGHTBIT = 8;

    /**
     * Used when encoding something which has fewer than 24 bits.
     */
    private static final int SIXTEENBIT = 16;

    /**
     * Used to determine how many bits data contains.
     */
    private static final int TWENTYFOURBITGROUP = 24;

    /**
     * Used to test the sign of a byte.
     */
    private static final int SIGN = -128;

    /**
     * Byte used to pad output.
     */
    private static final byte PAD = (byte) '=';

    // Create arrays to hold the base64 characters and a
    // lookup for base64 chars
    private static byte[] base64Alphabet = new byte[BASELENGTH];
    private static byte[] lookUpBase64Alphabet = new byte[LOOKUPLENGTH];

    // Populating the lookup and character arrays
    static {
        for (int i = 0; i < BASELENGTH; i++) {
            base64Alphabet[i] = (byte) -1;
        }
        for (int i = 'Z'; i >= 'A'; i--) {
            base64Alphabet[i] = (byte) (i - 'A');
        }
        for (int i = 'z'; i >= 'a'; i--) {
            base64Alphabet[i] = (byte) (i - 'a' + 26);
        }
        for (int i = '9'; i >= '0'; i--) {
            base64Alphabet[i] = (byte) (i - '0' + 52);
        }

        base64Alphabet['+'] = 62;
        base64Alphabet['/'] = 63;

        for (int i = 0; i <= 25; i++) {
            lookUpBase64Alphabet[i] = (byte) ('A' + i);
        }

        for (int i = 26, j = 0; i <= 51; i++, j++) {
            lookUpBase64Alphabet[i] = (byte) ('a' + j);
        }

        for (int i = 52, j = 0; i <= 61; i++, j++) {
            lookUpBase64Alphabet[i] = (byte) ('0' + j);
        }

        lookUpBase64Alphabet[62] = (byte) '+';
        lookUpBase64Alphabet[63] = (byte) '/';
    }

    /**
     * Encodes binary data using the base64 algorithm and chunks
     * the encoded output into 76 character blocks
     *
     * @param binaryData binary data to encode
     * @return Base64 characters chunked in 76 character blocks
     */
    private static byte[] encodeBase64Chunked(byte[] binaryData) {
        return encodeBase64(binaryData, true);
    }
    
    /**
     * Encodes binary data using the base64 algorithm, optionally
     * chunking the output into 76 character blocks.
     *
     * @param binaryData Array containing binary data to encode.
     * @param isChunked if isChunked is true this encoder will chunk
     *                  the base64 output into 76 character blocks
     * @return Base64-encoded data.
     */
    private static byte[] encodeBase64(byte[] binaryData, boolean isChunked) {
        int lengthDataBits = binaryData.length * EIGHTBIT;
        int fewerThan24bits = lengthDataBits % TWENTYFOURBITGROUP;
        int numberTriplets = lengthDataBits / TWENTYFOURBITGROUP;
        byte encodedData[] = null;
        int encodedDataLength = 0;
        int nbrChunks = 0;

        if (fewerThan24bits != 0) {
            //data not divisible by 24 bit
            encodedDataLength = (numberTriplets + 1) * 4;
        } else {
            // 16 or 8 bit
            encodedDataLength = numberTriplets * 4;
        }

        // If the output is to be "chunked" into 76 character sections,
        // for compliance with RFC 2045 MIME, then it is important to
        // allow for extra length to account for the separator(s)
        if (isChunked) {

            nbrChunks =
                (CHUNK_SEPARATOR.length == 0 ? 0 : (int) Math.ceil((float) encodedDataLength / CHUNK_SIZE));
            encodedDataLength += nbrChunks * CHUNK_SEPARATOR.length;
        }

        encodedData = new byte[encodedDataLength];

        byte k = 0, l = 0, b1 = 0, b2 = 0, b3 = 0;

        int encodedIndex = 0;
        int dataIndex = 0;
        int i = 0;
        int nextSeparatorIndex = CHUNK_SIZE;
        int chunksSoFar = 0;

        //log.debug("number of triplets = " + numberTriplets);
        for (i = 0; i < numberTriplets; i++) {
            dataIndex = i * 3;
            b1 = binaryData[dataIndex];
            b2 = binaryData[dataIndex + 1];
            b3 = binaryData[dataIndex + 2];

            //log.debug("b1= " + b1 +", b2= " + b2 + ", b3= " + b3);

            l = (byte) (b2 & 0x0f);
            k = (byte) (b1 & 0x03);

            byte val1 =
                ((b1 & SIGN) == 0) ? (byte) (b1 >> 2) : (byte) ((b1) >> 2 ^ 0xc0);
            byte val2 =
                ((b2 & SIGN) == 0) ? (byte) (b2 >> 4) : (byte) ((b2) >> 4 ^ 0xf0);
            byte val3 =
                ((b3 & SIGN) == 0) ? (byte) (b3 >> 6) : (byte) ((b3) >> 6 ^ 0xfc);

            encodedData[encodedIndex] = lookUpBase64Alphabet[val1];
            //log.debug( "val2 = " + val2 );
            //log.debug( "k4   = " + (k<<4) );
            //log.debug(  "vak  = " + (val2 | (k<<4)) );
            encodedData[encodedIndex + 1] =
                lookUpBase64Alphabet[val2 | (k << 4)];
            encodedData[encodedIndex + 2] =
                lookUpBase64Alphabet[(l << 2) | val3];
            encodedData[encodedIndex + 3] = lookUpBase64Alphabet[b3 & 0x3f];

            encodedIndex += 4;

            // If we are chunking, let's put a chunk separator down.
            if (isChunked) {
                // this assumes that CHUNK_SIZE % 4 == 0
                if (encodedIndex == nextSeparatorIndex) {
                    System.arraycopy(
                        CHUNK_SEPARATOR,
                        0,
                        encodedData,
                        encodedIndex,
                        CHUNK_SEPARATOR.length);
                    chunksSoFar++;
                    nextSeparatorIndex =
                        (CHUNK_SIZE * (chunksSoFar + 1)) +
                        (chunksSoFar * CHUNK_SEPARATOR.length);
                    encodedIndex += CHUNK_SEPARATOR.length;
                }
            }
        }

        // form integral number of 6-bit groups
        dataIndex = i * 3;

        if (fewerThan24bits == EIGHTBIT) {
            b1 = binaryData[dataIndex];
            k = (byte) (b1 & 0x03);
            //log.debug("b1=" + b1);
            //log.debug("b1<<2 = " + (b1>>2) );
            byte val1 =
                ((b1 & SIGN) == 0) ? (byte) (b1 >> 2) : (byte) ((b1) >> 2 ^ 0xc0);
            encodedData[encodedIndex] = lookUpBase64Alphabet[val1];
            encodedData[encodedIndex + 1] = lookUpBase64Alphabet[k << 4];
            encodedData[encodedIndex + 2] = PAD;
            encodedData[encodedIndex + 3] = PAD;
        } else if (fewerThan24bits == SIXTEENBIT) {

            b1 = binaryData[dataIndex];
            b2 = binaryData[dataIndex + 1];
            l = (byte) (b2 & 0x0f);
            k = (byte) (b1 & 0x03);

            byte val1 =
                ((b1 & SIGN) == 0) ? (byte) (b1 >> 2) : (byte) ((b1) >> 2 ^ 0xc0);
            byte val2 =
                ((b2 & SIGN) == 0) ? (byte) (b2 >> 4) : (byte) ((b2) >> 4 ^ 0xf0);

            encodedData[encodedIndex] = lookUpBase64Alphabet[val1];
            encodedData[encodedIndex + 1] =
                lookUpBase64Alphabet[val2 | (k << 4)];
            encodedData[encodedIndex + 2] = lookUpBase64Alphabet[l << 2];
            encodedData[encodedIndex + 3] = PAD;
        }

        if (isChunked) {
            // we also add a separator to the end of the final chunk.
            if (chunksSoFar < nbrChunks) {
                System.arraycopy(
                    CHUNK_SEPARATOR,
                    0,
                    encodedData,
                    encodedDataLength - CHUNK_SEPARATOR.length,
                    CHUNK_SEPARATOR.length);
            }
        }

        return encodedData;
    }
}