Tuesday, December 18, 2007

Getting started with iText - Part 13 (TwoOnOne.java)

The next installation in Getting started with iText translates the TwoOnOne example. It demonstrates a simple but useful technique for manipulating an existing PDF file. The original PDF is condensed by merging every two pages into one. If the original PDF file contained 32 pages, the new file would contain only 16 pages.




First a word about deprecation


Technically this example does not require a newer version of iText. It is possible to run this code using only the older iText jar that is built into MX7 and CF8. But if you view the API it states that some of the methods required like height() are deprecated and scheduled to be removed entirely in a later version. So for that reason you might prefer to use a newer iText jar. For those of you that like to live dangerously I have included a MX7/CF8 compatible version at the end.

What you will need for this example


1. A recent version of iText like 2.0.7. For instructions, see How to use a newer version of iText


UPDATE: I wanted to re-emphasize an issue that is mentioned in the instructions link above. MX users that are running the JavaLoader.cfc must read the article Using a Java URLClassLoader in CFMX Can Cause a Memory Leak.

The code for this example creates a new instance of the JavaLoader on each request. This was intended only to demonstrate how to use the JavaLoader. To avoid any confusion, my future code examples will use a server scoped object instead.


2. A sample PDF file. For this example download the ChapterSection.pdf file from the iText site. Place the file in the same directory as your .cfm script.

Now onto the code


First we initialize a few variables with the paths of the files we will be using. Then we get an instance of the JavaLoader.cfc that we will use to create our iText objects.


<cfscript>
// by default outputs to current directory. change as needed
fullPathToInputFile = ExpandPath("./ChapterSection.pdf");
fullPathToOutputFile = ExpandPath("./2On1.pdf");
fullPathToITextJar = ExpandPath("./iText-2.0.7.jar");
dotNotationPathToJavaLoader = "javaloader.JavaLoader";

// cfSearching: create an instance of the javaLoader
pathsForJavaLoader = arrayNew(1);
arrayAppend(pathsForJavaLoader, fullPathToITextJar);
javaLoader = createObject('component', dotNotationPathToJavaLoader).init(pathsForJavaLoader);
</cfscript>


Next we create an instance of PdfReader to read in the input file and get the total number of pages. Then we obtain the size of the first page, and use it to create a new Document and PdfWriter object. These objects will be used to generate our new PDF file.


<cfscript>
// we create a reader for a certain document
pdfReader = javaLoader.create("com.lowagie.text.pdf.PdfReader").init(fullPathToInputFile);
// we retrieve the total number of pages
totalPages = pdfReader.getNumberOfPages();

// we retrieve the size of the first page
pageSize = pdfReader.getPageSize(1);
// cfSearching: In earlier jars (MX7/CF8), the Rectangle methods are height()/width() not getHeight()/getWidth()
width = pageSize.getHeight();
height = pageSize.getWidth();

// step 1: creation of a document-object
rectangle = javaLoader.create("com.lowagie.text.Rectangle").init(width, height);
document = javaLoader.create("com.lowagie.text.Document").init(rectangle);

// step 2: we create a writer that listens to the document
outStream = createObject("java", "java.io.FileOutputStream").init(fullPathToOutputFile);
pdfWriter = javaLoader.create("com.lowagie.text.pdf.PdfWriter").getInstance(document, outStream);
</cfscript>


Finally we use a while loop to essentially copy all pages from the original file onto the new PDF file, in groups of two. Note, I am using the term "copy" loosely here to conceptually describe the final product.

Since we are starting with a blank PDF, we first add a new page inside each iteration of the loop. Next we copy the current set of pages from the original document. The first page will be added to the left side of the current Document page using a transformation.



<cfscript>
document.newPage();
p = p + 1;
i = i + 1;

page1 = pdfWriter.getImportedPage(pdfReader, javacast("int", i));
pdfContentByte.addTemplate(page1,
javacast("float", ".5"),
javacast("float", "0"),
javacast("float", "0"),
javacast("float", ".5"),
javacast("float", "60"),
javacast("float", "120") );
</cfscript>


The second page, if one exists, will be added to the right side.


<cfscript>
if (i LT totalPages) {
i = i + 1;
page2 = pdfWriter.getImportedPage(pdfReader, javacast("int", i));
pdfContentByte.addTemplate(page2,
javacast("float", ".5"),
javacast("float", "0"),
javacast("float", "0"),
javacast("float", ".5"),
javacast("float", ((width / 2) + 60) ),
javacast("float", "120") );
}
</cfscript>


Finally we add a footer with the page numbers: page x of y.


<cfscript>
pdfContentByte.beginText();
pdfContentByte.setFontAndSize(bf, javacast("float", 14));
pdfContentByte.showTextAligned(PdfContentByte.ALIGN_CENTER,
"page "& p &" of "& ( INT(totalPages / 2) + IIF(totalPages MOD 2 GT 0, 1, 0)),
javacast("float", width / 2),
javacast("float", 40),
javacast("float", 0) );
pdfContentByte.endText();
</cfscript>


The loop will repeat these steps until all of the pages in the original document are written to the new PDF.

So there you have it. As always comments/suggestions/corrections are welcome!

Documentation: Manipulating existing PDF documents
Source: TwoOnOne.java

Complete Code (Requires JavaLoader.cfc and iText 2.0.7)

<h1>Two Pages on One Example (Using JavaLoader.cfc)</h1>

<cfscript>
savedErrorMessage = "";

// by default outputs to current directory. change as needed
fullPathToInputFile = ExpandPath("./ChapterSection.pdf");
fullPathToOutputFile = ExpandPath("./2On1.pdf");
fullPathToITextJar = ExpandPath("./iText-2.0.7.jar");
dotNotationPathToJavaLoader = "javaloader.JavaLoader";

// cfSearching: create an instance of the javaLoader
pathsForJavaLoader = arrayNew(1);
arrayAppend(pathsForJavaLoader, fullPathToITextJar);
javaLoader = createObject('component', dotNotationPathToJavaLoader).init(pathsForJavaLoader);

try {
// we create a reader for a certain document
pdfReader = javaLoader.create("com.lowagie.text.pdf.PdfReader").init(fullPathToInputFile);
// we retrieve the total number of pages
totalPages = pdfReader.getNumberOfPages();

// we retrieve the size of the first page
pageSize = pdfReader.getPageSize(1);
// cfSearching: In earlier jars (MX7/CF8), the Rectangle methods are height()/width() not getHeight()/getWidth()
width = pageSize.getHeight();
height = pageSize.getWidth();

// step 1: creation of a document-object
rectangle = javaLoader.create("com.lowagie.text.Rectangle").init(width, height);
document = javaLoader.create("com.lowagie.text.Document").init(rectangle);

// step 2: we create a writer that listens to the document
outStream = createObject("java", "java.io.FileOutputStream").init(fullPathToOutputFile);
pdfWriter = javaLoader.create("com.lowagie.text.pdf.PdfWriter").getInstance(document, outStream);

// step 3: we open the document
document.open();

// step 4: we add content
baseFont = javaLoader.create("com.lowagie.text.pdf.BaseFont");
bf = BaseFont.createFont(BaseFont.HELVETICA, BaseFont.CP1252, BaseFont.NOT_EMBEDDED);
pdfContentByte = pdfWriter.getDirectContent();

i = 0;
p = 0;
while (i LT totalPages) {
document.newPage();
p = p + 1;
i = i + 1;
// cfSearching: copy the next page
page1 = pdfWriter.getImportedPage(pdfReader, javacast("int", i));
pdfContentByte.addTemplate(page1,
javacast("float", ".5"),
javacast("float", "0"),
javacast("float", "0"),
javacast("float", ".5"),
javacast("float", "60"),
javacast("float", "120") );

if (i LT totalPages) {
i = i + 1;
page2 = pdfWriter.getImportedPage(pdfReader, javacast("int", i));
pdfContentByte.addTemplate(page2,
javacast("float", ".5"),
javacast("float", "0"),
javacast("float", "0"),
javacast("float", ".5"),
javacast("float", ((width / 2) + 60) ),
javacast("float", "120") );
}

// cfSearching: Add a "page x of y" footer
pdfContentByte.beginText();
pdfContentByte.setFontAndSize(bf, javacast("float", 14));
pdfContentByte.showTextAligned(PdfContentByte.ALIGN_CENTER,
"page "& p &" of "& ( INT(totalPages / 2) + IIF(totalPages MOD 2 GT 0, 1, 0)),
javacast("float", width / 2),
javacast("float", 40),
javacast("float", 0) );
pdfContentByte.endText();
}

WriteOutput("Finished!");
}
catch (Exception de) {
savedErrorMessage = de;
}

// step 5: we close the document
document.close();
// close the output stream
outStream.close();
</cfscript>

<!--- show any errors --->
<cfif len(savedErrorMessage) gt 0>
Error - unable to create document
<cfdump var="#savedErrorMessage#">
</cfif>


Complete Code (MX7/CF8 compatible version)

<h1>Two Pages on One Example (MX7/CF8 compatible)</h1>

<cfscript>
savedErrorMessage = "";

// cfSearching: by default all files are in the current directory. change as needed
fullPathToInputFile = ExpandPath("./ChapterSection.pdf");
fullPathToOutputFile = ExpandPath("./2On1.pdf");

try {
// we create a reader for a certain document
pdfReader = createObject("java", "com.lowagie.text.pdf.PdfReader").init(fullPathToInputFile);
// we retrieve the total number of pages
totalPages = pdfReader.getNumberOfPages();

// we retrieve the size of the first page
pageSize = pdfReader.getPageSize(1);
// cfSearching: height() and width() are deprecated in later iText versions
// cfSearching: The new methods are getHeight()/getWidth()
width = pageSize.height();
height = pageSize.width();

// step 1: creation of a document-object
rectangle = createObject("java", "com.lowagie.text.Rectangle").init(width, height);
document = createObject("java", "com.lowagie.text.Document").init(rectangle);

// step 2: we create a writer that listens to the document
outStream = createObject("java", "java.io.FileOutputStream").init(fullPathToOutputFile);
pdfWriter = createObject("java", "com.lowagie.text.pdf.PdfWriter").getInstance(document, outStream);

// step 3: we open the document
document.open();

// step 4: we add content
baseFont = createObject("java", "com.lowagie.text.pdf.BaseFont");
pdfContentByte = pdfWriter.getDirectContent();
i = 0;
p = 0;
while (i LT totalPages) {
document.newPage();
p = p + 1;
i = i + 1;
page1 = pdfWriter.getImportedPage(pdfReader, javacast("int", i));
pdfContentByte.addTemplate(page1,
javacast("float", ".5"),
javacast("float", "0"),
javacast("float", "0"),
javacast("float", ".5"),
javacast("float", "60"),
javacast("float", "120") );

if (i LT totalPages) {
i = i + 1;
page2 = pdfWriter.getImportedPage(pdfReader, javacast("int", i));
pdfContentByte.addTemplate(page2,
javacast("float", ".5"),
javacast("float", "0"),
javacast("float", "0"),
javacast("float", ".5"),
javacast("float", ((width / 2) + 60) ),
javacast("float", "120") );
}

bf = BaseFont.createFont(BaseFont.HELVETICA, BaseFont.CP1252, BaseFont.NOT_EMBEDDED);
pdfContentByte.beginText();
pdfContentByte.setFontAndSize(bf, javacast("float", 14));
pdfContentByte.showTextAligned(PdfContentByte.ALIGN_CENTER,
"page "& p &" of "& ( INT(totalPages / 2) + IIF(totalPages MOD 2 GT 0, 1, 0)),
javacast("float", width / 2),
javacast("float", 40),
javacast("float", 0) );
pdfContentByte.endText();
}

WriteOutput("Finished!");
}
catch (Exception de) {
savedErrorMessage = de;
}

// step 5: we close the document
document.close();
// close the output stream
outStream.close();
</cfscript>

<!--- show any errors --->
<cfif len(savedErrorMessage) gt 0>
Error - unable to create document
<cfdump var="#savedErrorMessage#">
</cfif>

  © Blogger templates The Professional Template by Ourblogtemplates.com 2008

Header image adapted from atomicjeep