It's Brent

Printing / Converting UIWebView to PDF

Posted on June 01, 2011

Please see my newer post Update to iOS HTML to PDF Conversion. It contains my open source project BNHtmlPdfKit. This post is only being kept for historical purposes.

Note: My solution isn’t perfect. It lacks crispness since the document is rendered at 72 ppi. You will also have to work with your HTML so that it doesn’t get cut off when going to a new page.

I had a client that requested printing in their iPad app, which is no problem now with iOS 4. The app was a set of forms with inputs like text fields, switches, etc. Pretty standard stuff. I decided the easy way with so much dynamic content was to render everything to HTML and use UIMarkupTextPrintFormatter. This would handle displaying text without calculating metrics on everything at the very least. Easy enough and it worked well. Then the client also wanted to be able to print to PDF and email the document. This is where the fun started.

I figured that I would be able to use the same UIMarkupTextPrintFormatter to generate out my PDF for me, after all it was a UIPrintFormatter subclass. UIPrintFormatter contains the method, - (void)drawInRect:(CGRect)rect forPageAtIndex:(NSInteger)pageIndex. I played around and got nothing but blank pages. Even the pageCount property on my UIMarkupTextPrintFormatter was returning 0. After a bunch of Google searches I still kept coming back to the same question on StackOverflow. Which the answer seemed to suggest that nothing is drawn till the system takes over.

Next plan of action was to see if I could convert a UIWebView to PDF. I was brought back once again to another question on StackOverflow. Which technically works, but doesn’t really handle multiple pages. This is when I decided to roll my own solution.

First thing, I need my HTML, nothing fancy here just a NSString. Next I need to create a UIWebView and load it with my HTML string. I also need a UIWebViewDelegate. We are going to assume that we are printing on a 8.5” x 11” sheet of paper, using 1” margins all around. So 6.5 * 72 for width and 9 * 72 for height.

NSString *html = ...;

UIWebView *webView = [[UIWebView alloc] initWithFrame: CGRectMake(0, 0, 6.5 * 72, 9 * 72)];
[webView setDelegate: self];
[webView loadHTMLString: html baseURL: nil];

I needed to make a UIWebViewDelegate because if we try to do anything with our UIWebView now, it would still be empty. The system needs to parse and render our HTML. Now we need to implement the -(void)webViewDidFinishLoad:(UIWebView *)webView method in our delegate. This will let us know when the system is done rendering. The first thing we will do is get the height of our UIWebView. I tried adding the heights of the UIWebView subviews but it was never quite right, so I decided to use good ol’ JavaScript. We also have to make sure we aren’t drawing beyond the height of our HTML or else we get that grey background you are used to when scrolling beyond the bounds of the page in Mobile Safari. Next we need to change the currentOffset of the UIWebView for each page, which is now kind of a hack using the lastObject of the subviews which should be a UIScrollView. Then lastly render.

-(void)webViewDidFinishLoad:(UIWebView *)webView {
	// Get the height of our webView
	NSString *heightStr = [webView stringByEvaluatingJavaScriptFromString:@"document.body.scrollHeight;"];

	int height = [heightStr intValue];

	// Get the number of pages needed to print. 9 * 72 = 648
	int pages = ceil(height / 648.0);

	NSMutableData *pdfData = [NSMutableData data];
	UIGraphicsBeginPDFContextToData( pdfData, CGRectZero, nil );

	for (int i = 0; i < pages; i++) {
		// Check to see if page draws more than the height of the UIWebView
		if ((i+1) * 648 > height) {
			CGRect f = [webView frame];
			f.size.height -= (((i+1) * 648.0) - height);
			[webView setFrame: f];
		}

		UIGraphicsBeginPDFPage();
		CGContextRef currentContext = UIGraphicsGetCurrentContext();
		CGContextTranslateCTM(currentContext, 72, 72); // Translate for 1" margins
		[[[webView subviews] lastObject] setContentOffset:CGPointMake(0, 648 * i) animated:NO];
		[webView.layer renderInContext:currentContext];
	}

	UIGraphicsEndPDFContext();

	...
}

From here you can use our NSData object pdfData to save out to file, or perhaps in my case attach to an email.

Now as the note at the beginning of the post said, this isn’t an ideal solution. The generated content doesn’t look the best but it works. I could have done the better thing and drawn out all of my content using Core Graphics / Quartz.

Please see my newer post Update to iOS HTML to PDF Conversion. It contains my open source project BNHtmlPdfKit. This post is only being kept for historical purposes.


ABOUT BRENT

I am a software developer living in Shreveport, LA with my wife, daughter, and 2 dogs. Most of my time is spent developing for the web using Rails, on mobile for iOS, and sometimes for the Mac. When I'm not programming I follow the Texas Rangers through the ups and downs of all 162 games.

You can find me on Twitter and Github.

Follow @BrentNycum