Hello World Advanced

Hello World Advanced

Joel GeraciIn our previous Hello World post, we showed how to add two simple words to a PDF Page using the Datalogics PDF Java Toolkit. Other than the text position, font, and font size, no other options were specified; we just used the defaults which resulted in solid, black text.

In this article, we expand on that concept to also create outlined or “stroked” text and solid text with an outline or “filled and stroked”. We also discuss how to draw lines and a few simple shapes. Again, we’ll use the “ContentWriter” class in conjunction with the “InstructionFactory”; just with more options.

Running the “HelloWorldAdvanced” sample results in a single 8.5 x 11 inch PDF page with the words “Hello World” repeated three times in 36 point, Helvetica-Bold starting an inch from the top-left of the page. Then there’ll be two lines; one solid, one dashed. Then five squares, all with slightly different properties. After running the sample, you should get something that looks like the image to the left.

Rendering Text in the PDF Java Toolkit:

When positioning text using the methods in the “InstructionFactory”, it’s important to remember that the coordinates specified in the “newTextPosition” method set the origin, or lower, left of the first character in the string. This means that if you want the text to start one inch from the top, left, of the page, you cannot set the text position to one inch from the top, left. You need to take the font size into account when calculating the position. This is also the position that the leading for new lines will be calculated from.
For these reasons and at the risk of repeating myself from my previous post, the “RichTextContentGenerator“ is the best way to put a block of text on a page if you don’t already know precisely where each word or line of text is going to be positioned.

The sample also demonstrates the use of the InstructionFactory’s newTextRenderingMode method to change the text rendering from solid to outline or both.

Rendering Graphics in the PDF Java Toolkit:

When drawing graphics using the InstructionFactory, you will be creating instructions for graphics operators for which there is no “reset” exactly. Instead, you save the graphics state, GSave, prior to writing the shape instructions and restore the graphics state, GRestore, after. This prevents one set of instructions from interfering with the next.

Another thing to remember is that, like fonts, the x, y coordinates in the newRectangle method of the InstructionFactory specify the lower left of the rectangle. The height and width are calculated from this point. So, if you wanted a 1 inch by 1 inch square to be positioned one inch from the top left of the page, you would set the x, y coordinates to be 1 inch from the left and 2 inches from the top.

Creating Dashed lines using Dash Patterns
Dash Patterns can allow you to create elaborate dashed lines. The line dash pattern controls the pattern of dashes and gaps used to draw lines. It has two parts, a dash array and a dash phase. The dash array specifies the lengths of alternating dashes and gaps. The dash phase specifies the distance into the dash pattern the dash starts. See Section 8.4.3.6 “Line Dash Pattern” in the PDF Reference for a more thorough explanation and table of examples.

Creating Shapes
The sample shows how to draw lines and rectangles; both simple shapes. A shape that is equally as simple yet suspiciously absent from the sample is a circle. The InstructionFactory can draw text, lines, rectangles, and Bezier curves. Unfortunately, you can’t draw a circle using Bexzier curves, you can only approximate one and while the math required to draw a circle is so simple that they teach it in grade school, the math required to approximate a circle using Bezier curves is, frankly, terrifying… at least it is to me. For this reason, we recommend drawing circles and ovals on the page by first drawing them as annotations and then flattening them… which is the topic for a future post.

The HelloWorldAdvanced Sample:

The sample takes you through the process of:

  1. Creating a new, single page PDF document.
  2. Acquiring the first page so it can be drawn on.
  3. Creating a set of resources for the drawing operators to use. In this case the font named Helvetica-Bold.
  4. Setting the font size and leading for blocks of text.
  5. Creating and writing a content stream with instructions to position and show “Hello World” on the page three times with different text rendering settings: filled, stroked and filled with a stroked outline.
  6. Adding two lines, one solid and one dashed.
  7. Adding five squares, each with different style combinations of solid, filled and with different line join properties.
  8. Overwriting the previously empty page content stream with the new content stream.
  9. Writing the PDF file to disk.

The “HelloWorldBasic” sample requires utilities that are in the samples that come with the PDF Java Toolkit Version 2.0 so you’ll want to have installed that first. You can put this sample in the src folder of the samples project.

Download HelloWorldAdvanced.java or copy the code below.

/*

 Copyright 2009-2012 Adobe Systems Incorporated. All Rights Reserved.
 Portions Copyright 2012 Datalogics Incorporated.

 NOTICE: Datalogics and Adobe permit you to use, modify, and distribute
 this file in accordance with the terms of the license agreement
 accompanying it. If you have received this file from a source other
 than Adobe or Datalogics, then your use, modification, or distribution of it
 requires the prior written permission of Adobe or Datalogics.

 */

import java.awt.Color;
import java.io.File;
import java.io.RandomAccessFile;
import com.adobe.internal.io.ByteWriter;
import com.adobe.internal.io.RandomAccessFileByteWriter;
import com.adobe.pdfjt.core.exceptions.PDFException;
import com.adobe.pdfjt.core.types.ASName;
import com.adobe.pdfjt.core.types.ASRectangle;
import com.adobe.pdfjt.core.types.ASString;
import com.adobe.pdfjt.pdf.content.Content;
import com.adobe.pdfjt.pdf.content.Instruction;
import com.adobe.pdfjt.pdf.content.InstructionFactory;
import com.adobe.pdfjt.pdf.contentmodify.ContentWriter;
import com.adobe.pdfjt.pdf.contentmodify.ModifiableContent;
import com.adobe.pdfjt.pdf.document.PDFCatalog;
import com.adobe.pdfjt.pdf.document.PDFContents;
import com.adobe.pdfjt.pdf.document.PDFDocument;
import com.adobe.pdfjt.pdf.document.PDFOpenOptions;
import com.adobe.pdfjt.pdf.document.PDFResources;
import com.adobe.pdfjt.pdf.document.PDFSaveFullOptions;
import com.adobe.pdfjt.pdf.graphics.font.PDFFont;
import com.adobe.pdfjt.pdf.graphics.font.PDFFontMap;
import com.adobe.pdfjt.pdf.graphics.font.PDFFontSimple;
import com.adobe.pdfjt.pdf.interactive.PDFViewerPreferences;
import com.adobe.pdfjt.pdf.page.PDFPage;
import com.adobe.pdfjt.pdf.page.PDFPageLayout;
import com.adobe.pdfjt.pdf.page.PDFPageMode;
import com.adobe.pdfjt.pdf.page.PDFPageTree;

/*
   Demonstrates creating a new document and placing text.
   Comments in this sample refer to the ISO 32000:2008 PDF Reference located at
   http://www.adobe.com/content/dam/Adobe/en/devnet/acrobat/pdfs/PDF32000_2008.pdf

   Output file is written to output/HelloWorldAdvanced.pdf 
 */

public class HelloWorldAdvanced {
	// One inch in PDF coordinates is 72 points. Create an INCHES constant to make measuring easier
	private static final double INCHES = 72;
	private static final double PAGE_HEIGHT = 11*INCHES;
	private static final double PAGE_WIDTH = 8.5*INCHES;
	static final String FIL‎E_PATH = "output/HelloWorldAdvanced.pdf";

	public static void main(String[] args) {

		try{
			/*
			   Create an ASRectangle object to initialize an empty PDF. This one
			   will create a page that is 8.5 x 11 INCHES based on the constants
			   above. Then use that rectangle to generate a new document with a
			   single blank page.
			 */

			double[] rectangle = {0, 0, PAGE_WIDTH, PAGE_HEIGHT};
			ASRectangle rect = new ASRectangle(rectangle);
			PDFDocument pdfDocument = PDFDocument.newInstance(rect, PDFOpenOptions.newInstance());

			/*
			   The root of a document’s object hierarchy is the catalog
			   dictionary, located by means of the Root entry in the trailer of
			   the PDF file. The catalog contains references to other objects
			   defining the document’s contents, outline, article threads, named
			   destinations, and other attributes. In addition, it contains
			   information about how the document is displayed on the screen,
			   such as whether its outline and thumbnail page images are
			   displayed automatically and whether some location other than the
			   first page will shown when the document is first opened.

			   See section 7.7.2 of the PDF specification.

			   In the case below, we are telling the viewer to... 1) Open the
			   document to single page mode 2) Set the magnification to
			   "Fit Page" 3) Show the the pages only, no additional tabs are
			   visible
			 */

			PDFCatalog catalog = pdfDocument.requireCatalog();
			catalog.setPageLayout(PDFPageLayout.SinglePage);
			PDFViewerPreferences viewerPrefs = PDFViewerPreferences.newInstance(pdfDocument);
			viewerPrefs.setFitWindow(true);
			viewerPrefs.setPageMode(PDFPageMode.PagesOnly);

			/*
			   The pages of a document are accessed through a structure known as
			   the page tree, which defines the ordering of pages in the
			   document.

			   Get the pageTree, then the first page. Pages numbers are zero
			   based.

			   See section 7.7.3 of the PDF specification, "Page Tree"
			 */

			PDFPageTree pdfPageTree = pdfDocument.requirePages();
			PDFPage pdfPage = pdfPageTree.getPage(0);

			/*
			   A content stream (PDFContents) is a PDF stream object whose data
			   consists of a sequence of "instructions" describing the graphical
			   elements to be painted on a page.

			   This will be used by the "ContentWriter" below.

			   See section 7.7.3 of the PDF specification, "Content Streams"
			 */
			PDFContents pdfContents = PDFContents.newInstance(pdfPage.getPDFDocument());

			/*
			   A content stream’s named resources (PDFResources) are defined by
			   a resource dictionary, which enumerates the named resources
			   needed by the operators in the content stream and the names by
			   which they can be referred to.

			   EXAMPLE: If a text operator appearing within the content stream
			   needs a certain font, the content stream’s resource dictionary
			   can associate the name "F42" with the corresponding font
			   dictionary. The text operator can then use this name to refer to
			   the font.

			   This will be used by the "ContentWriter" below.

			   See section 7.8.3 of the PDF specification,
			   "Resource Dictionaries"

			   For the simplicity we'll use one of the standard 14 fonts. These
			   fonts, or their font metrics and suitable substitution fonts, are
			   available to all conforming PDF readers. Any of the standard 14
			   fonts can be used without the need for font embedding.

			   The standard 14 fonts are as follows: Helvetica Helvetica-Oblique
			   Helvetica-Bold Helvetica-BoldOblique Courier Courier-Oblique
			   Courier-Bold Courier-BoldOblique Times-Roman Times-Italic
			   Times-Bold Times-BoldItalic Symbol ZapfDingbats
			 */

			PDFResources pdfResources = PDFResources.newInstance(pdfPage.getPDFDocument());	       
			//PDFFontSimple abstracts functionality common to the simple font dictionaries
			PDFFont helveticaBold = PDFFontSimple.newInstance(
					pdfPage.getPDFDocument(), ASName.k_Helvetica_Bold, ASName.k_Type1);
			PDFFontMap pdfFontMap = PDFFontMap.newInstance(pdfPage.getPDFDocument());
			pdfFontMap.set(ASName.create("Helvetica-Bold"), helveticaBold);	
			pdfResources.setFontMap(pdfFontMap);
			pdfResources.setProcSetList(new ASName[] {ASName.k_PDF,ASName.k_Text});
			pdfPage.setResources(pdfResources);

			/*
			   InstructionFactory is used to create new page content
			   "Instructions". ContentWriter adds Instructions to PDF content.
			 */

			ContentWriter contentWriter = ContentWriter.newInstance(
					ModifiableContent.newInstance(pdfContents,pdfResources));

			/*
			   The next section of code will show how to use the text rendering
			   mode to fill, stroke or fill and stroke text. we'll use the text
			   "Hello World" and the next line instruction three times so we'll
			   create that instruction ahead of time.
			 */

			Instruction showHelloWorld = InstructionFactory.newShowText(new ASString("Hello World"));
			Instruction nextLine = InstructionFactory.newTextNextLine();
			//A new text block
			contentWriter.write(InstructionFactory.newBeginText());
			//One inch from the top minus the height of the text.
			contentWriter.write(InstructionFactory.newTextPosition(
					1*INCHES, PAGE_HEIGHT-(1*INCHES)-36)); 
			// Set the font and size in points.
			contentWriter.write(InstructionFactory.newTextFont(ASName.create("Helvetica-Bold"), 36));
			// used to determine the position of the next line of text
			contentWriter.write(InstructionFactory.newTextLeading(36)); 
			/* 
			   Define the color of the text. "getColorComponents" allows us to
			   specify the color by name
			*/ 
			contentWriter.write(InstructionFactory.newColorFill(getColorComponents(Color.black))); 

			/* Solid text is the default so we don't need 
			   to set the rendering mode right now.
			   Show "Hello World"
			 */
			contentWriter.write(showHelloWorld);

			// move down by the value of the leading
			contentWriter.write(nextLine); 
			/*
			   Change the TextRenderingMode to Stroked
			   0 = Solid Fill
			   1 = Stroked, no fill
			   2 = Stroked and filled	
			 */
			contentWriter.write(InstructionFactory.newTextRenderingMode(1)); 
			//Set the stroke color
			contentWriter.write(InstructionFactory.newColorStroke(getColorComponents(Color.red)));
			//Show "Hello World" as Stroked text
			contentWriter.write(showHelloWorld); 

			contentWriter.write(nextLine); 
			contentWriter.write(InstructionFactory.newTextRenderingMode(2)); 
			contentWriter.write(InstructionFactory.newColorStroke(getColorComponents(Color.black)));
			contentWriter.write(InstructionFactory.newColorFill(getColorComponents(Color.red)));
			// Show "Hello World" as Stroked and Filled Text
			contentWriter.write(showHelloWorld); 
			// End the text block	
			contentWriter.write(InstructionFactory.newEndText()); 

			/*
			   Now it's for a few graphics instructions. The following code block
			   will add a few lines with different styles and five squares. The
			   squares will be filled and stroked similarly to the text above.

			   Additionally, because we will be changing the line dash pattern
			   and other graphics operators for which there is no reset, we wrap
			   the drawing operators in GSave and GRestore to save and restore
			   the default graphics state before and after each shape. This
			   prevents one set of instructions from interfering with the next.
			 */

			Instruction gSave = InstructionFactory.newGSave();
			Instruction gRestore = InstructionFactory.newGRestore();

			//Create a Solid line
			contentWriter.write(gSave);
			//Start of a new path
			contentWriter.write(InstructionFactory.newMoveTo(1*INCHES, PAGE_HEIGHT-(3*INCHES))); 
			//Next point on the path
			contentWriter.write(InstructionFactory.newLineTo(PAGE_WIDTH-(1*INCHES),  PAGE_HEIGHT-(3*INCHES)));  
			// a 1 point line
			contentWriter.write(InstructionFactory.newLineWidth(1)); 
			contentWriter.write(InstructionFactory.newStrokePath());
			contentWriter.write(gRestore);

			//Create a Dashed line
			contentWriter.write(gSave);
			//Start of a new path
			contentWriter.write(InstructionFactory.newMoveTo(1*INCHES, PAGE_HEIGHT-(3*INCHES)-(.125*INCHES))); 
			//Next point on the path
			contentWriter.write(InstructionFactory.newLineTo(PAGE_WIDTH-(1*INCHES),  PAGE_HEIGHT-(3*INCHES)-(.125*INCHES))); 
			// Set the line thickness to 1 point	
			contentWriter.write(InstructionFactory.newLineWidth(1)); 
			/*
			   The line dash pattern controls the pattern of dashes and
			   gaps used to stroke paths. It is specified by a dash array
			   and a dash phase. The dash array’s elements are numbers that
			   specify the lengths of alternating dashes and gaps; the numbers
			   are nonnegative and not all zero. The dash phase 
			   specifies the distance into the dash pattern at which to start the
			   dash. The elements of both the dash array and the dash phase
			   are expressed in user space units.

			   See Section 8.4.3.6 "Line Dash Pattern" in the PDF Reference

			   Add a new dashed line
			 */
			contentWriter.write(InstructionFactory.newLineDashPattern(new double[]{2, 1}, 0)); 
			contentWriter.write(InstructionFactory.newStrokePath());
			contentWriter.write(gRestore);

			/*
			   Add a rectangle where the x,y is the bottom/left and 
			   width and height move up the page and to the right.

			   Filled Square
			*/
			contentWriter.write(gSave);
			contentWriter.write(InstructionFactory.newRectangle(1*INCHES, PAGE_HEIGHT-(4.25*INCHES), 1*INCHES, 1*INCHES));   
			contentWriter.write(InstructionFactory.newColorFill(getColorComponents(Color.black)));
			// Fill the rectangle with the fill color
			contentWriter.write(InstructionFactory.newFillPath()); 
			contentWriter.write(gRestore);

			//Stroked Square
			contentWriter.write(gSave);
			contentWriter.write(InstructionFactory.newRectangle(2.25*INCHES, PAGE_HEIGHT-(4.25*INCHES), 1*INCHES, 1*INCHES));   
			contentWriter.write(InstructionFactory.newColorStroke(getColorComponents(Color.red)));
			// Stroke the rectangle with the stroke color
			contentWriter.write(InstructionFactory.newStrokePath()); 
			contentWriter.write(gRestore);

			//Filled and Stroked Square
			contentWriter.write(gSave);
			contentWriter.write(InstructionFactory.newRectangle(3.5*INCHES, PAGE_HEIGHT-(4.25*INCHES), 1*INCHES, 1*INCHES));   
			contentWriter.write(InstructionFactory.newColorFill(getColorComponents(Color.red)));
			contentWriter.write(InstructionFactory.newColorStroke(getColorComponents(Color.black)));
			// Fill and Stroke the rectangle with their respective colors
			contentWriter.write(InstructionFactory.newFillAndStrokePath()); 
			contentWriter.write(gRestore);

			/*
			Now we'll draw the same squares except change the line join type. Starting with 
			rounded corners
			*/
			contentWriter.write(gSave);
			contentWriter.write(InstructionFactory.newRectangle(1*INCHES, PAGE_HEIGHT-(5.5*INCHES), 1*INCHES, 1*INCHES));
			// a thick 10 point line, so you can see the line join better
			contentWriter.write(InstructionFactory.newLineWidth(10)); 
			//The argument for newLineJoin can be one of 0, 1, or 2 meaning respectively: miter corners, round corners, or bevel corners
			contentWriter.write(InstructionFactory.newLineJoin(1));   
			contentWriter.write(InstructionFactory.newStrokePath());
			contentWriter.write(gRestore);

			//beveled corners
			contentWriter.write(gSave);
			contentWriter.write(InstructionFactory.newRectangle(2.25*INCHES, PAGE_HEIGHT-(5.5*INCHES), 1*INCHES, 1*INCHES));
			contentWriter.write(InstructionFactory.newLineWidth(10));
			contentWriter.write(InstructionFactory.newLineJoin(2)); // beveled corners
			contentWriter.write(InstructionFactory.newStrokePath());
			contentWriter.write(gRestore);

			/*
			   "Content" represents both a content stream and the resources
			   reference by the content stream.
			 */
			Content content = contentWriter.close();

			/*
			   "setContents" overwrites the page content stream with the stream
			   passed here.
			 */

			pdfPage.setContents(content.getContents());

			/*
			   ByteWriter does not overwrite existing files, so we want to make
			   sure this file doesn't already exist beforehand by deleting
			   anything that is currently at our output path. Be careful when
			   doing this.
			 */

			File file = new File(FIL‎E_PATH); 
			file.mkdirs();
			if (file.exists()) {
				file.delete();
			}

			/*
			   Write everything we've done to the original PDF file
			 */

			RandomAccessFile outputPDFFile = new RandomAccessFile(FIL‎E_PATH, "rw");
			ByteWriter outputWriter = new RandomAccessFileByteWriter(outputPDFFile);	

			/*
			   Save the document and clean up the resources that were used to
			   create it.
			 */

			pdfDocument.saveAndClose(outputWriter, PDFSaveFullOptions.newInstance());
			System.out.println("Done!");

			try { 
				if (pdfDocument != null) {
					pdfDocument.close();
				}
			} catch (PDFException e) {
				e.printStackTrace();
			}

			if (outputWriter != null) {
				outputWriter.close();
			}
		} catch (Exception e){
			e.printStackTrace();
		}
	}

	private static double[] getColorComponents(Color color) {
		float[] colorComponents = color.getColorComponents(null);
		double[] colorComponentsDouble = new double[colorComponents.length];
		for (int i = 0; i < colorComponents.length; i++) {
			colorComponentsDouble[i] = colorComponents[i];
		}
		return colorComponentsDouble;
	}	
}

4 thoughts on “Hello World Advanced

  1. Hi Joel,

    I’ve been sharing some of your Hello World posts on my social media channels and I was wondering why you don’t have the Twitter | Facebook | Google+ buttons that are on a lot of Blog posts on your page (or did I miss them?). I had a friend tell me that he did some personal testing and blog pages with the G+ button on them shot up in the Google search results. Your content is GREAT (as always) and I just want more people to see it…

Leave a Reply

Your email address will not be published. Required fields are marked *