frameworker

May 6, 2008

A Cocoa recipe to edit PDF Forms

Filed under: Uncategorized — frameworker @ 11:14 pm

This recipe, based on John Calhoun’s PDFKitViewer2 example program, lets you edit, save and restore PDFAnnotationWidget values with PDFKit 10.5. Pretty much everything is shoehorned into a category of MyPDFDocument to minimize change to the original code base

(Note that PDFAnnotationWidget isn’t itself a class; it’s a reference to the widget classes like PDFAnnotationTextWidget.)

The recipe simply calls “restorePDFWidgets” when a document is opened and “cachePDFWidgets” when the document is closed.

As you can see, I’m using a free text annotation named “widgetArchive” to hold the PDFAnnotationWidgets. It has a zero dimension, doesn’t display or print and caches the PDFAnnotationWidgets in its contents field.

1. In MyPDFDocument->windowControllerDidLoadNib call the category method [self restorePDFWidgets].

A good place to do this is just after you’ve set the delegate.

2. Complete dataRepresentationOfType to archive the PDFAnnotationWidgets:

- (NSData *) dataRepresentationOfType: (NSString *) aType
{
	// For each non-empty widget append the type, fieldName and value to theWidgetCache

	[self cachePDFWidgets]; // <-- Call the category method to archive widgets.

	NSData * pdfData = &#91;&#91;_pdfView document&#93; dataRepresentation&#93;;

	return pdfData;
}
&#91;/sourcecode&#93;

<strong>3.</strong> Add a category for the PDFAnnotationWidget persistence methods:


//  MyPDFDocument+PDFForms.m

#import "MyPDFDocument+PDFAdditions.h"
#import "NSString+PDFAdditions.h"

@implementation MyPDFDocument (PDFForms)

4. Implement a method to restore PDFAnnotationWidgets when a document is opened:

- (void) restorePDFWidgets
{	
	PDFAnnotation * widgetArchive = [self findWidgetArchive];
	
	// Don't do anything here if widgetArchive doesn't exist.
	if (widgetArchive != NULL)
	{
		// internalize widgets from archive.
		
		NSString * widgetCache = [widgetArchive contents];
		NSCharacterSet *tokenSeperatorSet = [NSCharacterSet characterSetWithCharactersInString:@"\t"];
		NSArray *fieldArray = [widgetCache tokensSeparatedByCharactersFromSet: tokenSeperatorSet];

		int i;
		int cacheCount = [fieldArray count]/3;

		for (i = 0; i/3 < cacheCount; i = i + 3)
		{
			// fieldArray&#91;i  &#93; -> fieldname
			// fieldArray[1+1] -> "Text" or "Button"
			// fieldArray[i+2] -> stringValue or state

			// Find annot with fieldName
			
			NSString * theFieldName = (NSString *) [fieldArray objectAtIndex: i];
			PDFAnnotation * annot = [self annotWithFieldname: theFieldName];
			if (annot != NULL)
			{
				NSString * widgetType =  (NSString *) [fieldArray objectAtIndex: i + 1];
				
				if ([widgetType isEqualToString: @"Text"])
				{
					NSString * theStringValue =  (NSString *) [fieldArray objectAtIndex: i + 2];
					[(PDFAnnotationTextWidget *) annot setStringValue: theStringValue];
				}
				else
				{
					NSString * buttonState =  (NSString *) [fieldArray objectAtIndex: i + 2];
					if ([buttonState isEqualToString: @"NSOnState"])
					{
						[(PDFAnnotationButtonWidget *) annot setState: NSOnState];
					}
					else
					{
						[(PDFAnnotationButtonWidget *) annot setState: NSOffState];
					}
				}				
			}
		}
	}
}

5. Implement a method to find the PDFAnnotation that contains the widget archive.

- (PDFAnnotation *) findWidgetArchive
{
	PDFAnnotation * theWidgetArchive = NULL;
	
	// Iterate the list of pages.
	int pageIndex;
	int pageCount = [[_pdfView document] pageCount];
	for (pageIndex = 0; pageIndex < pageCount; pageIndex++)
	{
		PDFPage * currentPage = &#91;&#91;_pdfView document&#93; pageAtIndex: pageIndex&#93;;

		// Iterate the array of annotations.
		
		int annotIndex;
		NSArray * annotations = &#91;currentPage annotations&#93;;
		int annotCount = &#91;annotations count&#93;;
		for (annotIndex = 0; annotIndex < annotCount; annotIndex++)
		{
			PDFAnnotation * annotation = &#91;annotations objectAtIndex: annotIndex&#93;;
			
			if (&#91;&#91;annotation userName&#93; isEqualToString: @"widgetArchive"&#93;)
			{
				theWidgetArchive = annotation;
				break;
			}
		}
	}
	
	return theWidgetArchive;
}
&#91;/sourcecode&#93;

<strong>6.</strong> Implement a method to get the PDFAnnotationWidget by name that we want to restore.


- (PDFAnnotation *) annotWithFieldname: (NSString *) fieldName
{
	PDFAnnotation * annot = NULL;

	// Iterate the list of pages.
	int pageIndex;
	int pageCount = [[_pdfView document] pageCount];
	for (pageIndex = 0; pageIndex < pageCount; pageIndex++)
	{
		PDFPage * currentPage = &#91;&#91;_pdfView document&#93; pageAtIndex: pageIndex&#93;;

		// Iterate the array of annotations.
		
		int annotIndex;
		NSArray * annotations = &#91;currentPage annotations&#93;;
		int annotCount = &#91;annotations count&#93;;
		for (annotIndex = 0; annotIndex < annotCount; annotIndex++)
		{
			PDFAnnotation * annotation = &#91;annotations objectAtIndex: annotIndex&#93;;
			
			if (&#91;annotation isKindOfClass: &#91;PDFAnnotationTextWidget class&#93;&#93;)
			{
				NSString * theFieldName = &#91;(PDFAnnotationTextWidget *) annotation fieldName&#93;;
				
				if (&#91;theFieldName isEqualToString: fieldName&#93;)
				{
					annot = annotation;
					break;
				}
			}
		}
	}
	
	return annot;
}
&#91;/sourcecode&#93;

<strong>7.</strong> Implement a method to save PDFAnnotationWidgets when a document is closed:


- (void) cachePDFWidgets
{	
	PDFAnnotation * widgetArchive = [self findWidgetArchive];
	
	if (widgetArchive != NULL)
	{
		[[_pdfView currentPage] removeAnnotation: widgetArchive];
	}
	
	NSString * theCache = [self widgetsToCache];

	// Don't do anything here if there aren't any annots to cache.
	if ([theCache length] > 0)
	{
		// Create new widgetArchive
		
		widgetArchive = [[PDFAnnotationFreeText alloc] initWithBounds: NSMakeRect(0, 0, 0, 0)];
		
		[widgetArchive setUserName: @"widgetArchive"];
		[widgetArchive setShouldDisplay:  NO];
		[widgetArchive setShouldPrint:    NO];
		[widgetArchive setContents: theCache];
		
		[[_pdfView currentPage] addAnnotation: widgetArchive];
	}
}

8. Implement a method to get the widgets to cache:

- (NSString *) widgetsToCache
{
	NSString * theWidgetCache = @"";
	
	// Iterate the list of pages.
	int pageIndex;
	int pageCount = [[_pdfView document] pageCount];
	for (pageIndex = 0; pageIndex < pageCount; pageIndex++)
	{
		PDFPage * currentPage = &#91;&#91;_pdfView document&#93; pageAtIndex: pageIndex&#93;;

		// Iterate the array of annotations.
		
		int annotIndex;
		NSArray * annotations = &#91;currentPage annotations&#93;;
		int annotCount = &#91;annotations count&#93;;
		for (annotIndex = 0; annotIndex < annotCount; annotIndex++)
		{
			PDFAnnotation * annotation = &#91;annotations objectAtIndex: annotIndex&#93;;
			
			NSString * theFieldName	    = @"";
			NSString * theWidgetString	= @"";
			
			if (&#91;annotation isKindOfClass: &#91;PDFAnnotationTextWidget class&#93;&#93;)
			{
				theFieldName = &#91;(PDFAnnotationTextWidget *) annotation fieldName&#93;;
				theWidgetString = &#91;(PDFAnnotationTextWidget *) annotation stringValue&#93;;
				
				if (&#91;theWidgetString length&#93; > 0)
				{
					theWidgetCache = [theWidgetCache stringByAppendingString: theFieldName];
					theWidgetCache = [theWidgetCache stringByAppendingString: @"\t"];
					theWidgetCache = [theWidgetCache stringByAppendingString: @"Text"];
					theWidgetCache = [theWidgetCache stringByAppendingString: @"\t"];
					theWidgetCache = [theWidgetCache stringByAppendingString: theWidgetString];
					theWidgetCache = [theWidgetCache stringByAppendingString: @"\t"];
				}
			}
			else
			if ([annotation isKindOfClass: [PDFAnnotationButtonWidget class]])
			{
				theFieldName = [(PDFAnnotationButtonWidget *) annotation fieldName];
				int theWidgetstate = [(PDFAnnotationButtonWidget *) annotation state];
				
				if (theWidgetstate ==  NSOnState)
				{
					theWidgetCache = [theWidgetCache stringByAppendingString: theFieldName];
					theWidgetCache = [theWidgetCache stringByAppendingString: @"\t"];
					theWidgetCache = [theWidgetCache stringByAppendingString: @"Button"];
					theWidgetCache = [theWidgetCache stringByAppendingString: @"\t"];
					theWidgetCache = [theWidgetCache stringByAppendingString: @"ON"];
					theWidgetCache = [theWidgetCache stringByAppendingString: @"\t"];
				}
			}
		}
	}
	
	return theWidgetCache;
}
@end // End of MyPDFDocument (PDFForms)

9. Finally, implement an NSString category containing this utility method that tokenizes the widget string.

//  NSString+PDQAdditions.m

#import "NSString+PDQAdditions.h"

@implementation NSString (PDQAdditions)

- (NSArray *)tokensSeparatedByCharactersFromSet:(NSCharacterSet *)separatorSet
{
  NSScanner      *scanner      = [NSScanner scannerWithString:self];
  NSCharacterSet *tokenSet     = [separatorSet invertedSet];
  NSMutableArray *tokens       = [NSMutableArray array];

  [scanner setCharactersToBeSkipped:separatorSet];

  while (![scanner isAtEnd])
  {
    NSString  *destination = [NSString string];

    if ([scanner scanCharactersFromSet:tokenSet intoString:&destination])
    {
      [tokens addObject:[NSString stringWithString:destination]];
    }
  }

  return [NSArray arrayWithArray:tokens];
}

@end

And one more thing…

In order to enable the Save menu command, it is necessary to change the “Role” from Viewer to Editor in the Target Info Properties panel!

Advertisements

Leave a Comment »

No comments yet.

RSS feed for comments on this post. TrackBack URI

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

Create a free website or blog at WordPress.com.

%d bloggers like this: