Making a PDF from a UIWebView

While working on a project I needed a way to generate a pdf from some in app html to a for emailing and printing purposes. This seemed like a rather easy task, however the pdf generated was always empty. After digging around the web I kept coming back to this post on StackOverflow. A bit more searching I came across this great post by Brent Nycum which became my starting point. As Brent points out, this really isn’t the most ideal situation as the document is going to be rendered at 72 ppi. In a normal situation if you are in control of the data, for instance if you want to generate a PDF from images and text from within your app, you would use Core Graphics and Quartz. The advantage there is that you can control how the pages get split up. In this simple example the text gets cropped in an undesirable way. If your still interested read on and check out the code which is available on GitHub so feel free to use it however you want.

It doesn’t matter how you generate the html, whether it’s like what I needed in my project, or if it’s from an external resource like I use in the sample. So let’s start by loading our web content

[printContentWebView setDelegate:self];
NSURL *url = [NSURL URLWithString:@"http://happymaau.com/2011/05/17/weekly-update-2/"];
NSURLRequest *request = [NSURLRequest requestWithURL:url];
[printContentWebView loadRequest:request];

From there, we’re going to wait till the page is finished loading using the UIWebViewDelegate method webViewDidFinishLoad. If we don’t wait, we’re back to having an empty PDF. We get the total length of the web page using a tiny bit of JavaScript and after that we set the max width and height that each PDF page is going to be. With these values we need to set the webViews frame to match the page size. If we don’t do this then the width of the content rendered will still be the 320 pixels wide from the way the view was created in Interface Builder. If our UIWebView wasn’t something the user could see, we could set these values when the view was created which is what Brent does in his example.

// Store off the original frame so we can reset it when we're done
CGRect origframe = webView.frame;
NSString *heightStr = [webView stringByEvaluatingJavaScriptFromString:@"document.body.scrollHeight;"]; // Get the height of our webView
int height = [heightStr intValue];

// Size of the view in the pdf page
CGFloat maxHeight	= kDefaultPageHeight - 2*kMargin;
CGFloat maxWidth	= kDefaultPageWidth - 2*kMargin;
int pages = ceil(height / maxHeight);

[webView setFrame:CGRectMake(0.f, 0.f, maxWidth, maxHeight)];

Now that the view setup is done, time to actually generate the PDF. The drawing process is rather boilerplate and similar to drawing in a graphics image context (UIGraphicsBeginImageContext) as it’s wrapped with begin and end states. There’s another minor change to Brent’s original code within the loop. Rather than call UIGraphicsBeginPDFPage, I wanted to specify the size of each page I was about to draw. This will set the media box for the PDF page and define the rect we will be drawing into. Only thing left to do is to draw the portion of the web page for each page of the PDF.

// Set up we the pdf we're going to be generating is
UIGraphicsBeginPDFContextToFile(self.pdfPath, CGRectZero, nil);
int i = 0;
for ( ; i < pages; i++) 
{
     if (maxHeight * (i+1) > height) 
     { // Check to see if page draws more than the height of the UIWebView
          CGRect f = [webView frame];
          f.size.height -= (((i+1) * maxHeight) - height);
          [webView setFrame: f];
     }
     // Specify the size of the pdf page
     UIGraphicsBeginPDFPageWithInfo(CGRectMake(0, 0, kDefaultPageWidth, kDefaultPageHeight), nil);
     CGContextRef currentContext = UIGraphicsGetCurrentContext();
     // Move the context for the margins
     CGContextTranslateCTM(currentContext, kMargin, kMargin);
     // offset the webview content so we're drawing the part of the webview for the current page
     [[[webView subviews] lastObject] setContentOffset:CGPointMake(0, maxHeight * i) animated:NO];
     // draw the layer to the pdf, ignore the "renderInContext not found" warning. 
     [webView.layer renderInContext:currentContext];
}
// all done with making the pdf
UIGraphicsEndPDFContext();
// Restore the webview and move it to the top. 
[webView setFrame:origframe];
[[[webView subviews] lastObject] setContentOffset:CGPointMake(0, 0) animated:NO];

The process is very simple and in the code sample on GitHub I show how to use QuickLook for previewing the PDF as well as how to attach it to an email. Not the best solution for the problem, due to the content cut off issue and it being 72 ppi, but if you need a quick way to generate a PDF it’s not a bad one.

About these ads

11 Comments

  1. [...] those coming to the site for the Making a PDF from a UIWebView post, I’ve updated the project on GitHub to support pre iOS 5 targets. The project was [...]

  2. chris;
    Thanks, very nice material. How to get the pdf to print page number at the bottom?

    1. Hi Vic,

      Here’s the code you’re looking for, it’s from this documentation page

      - (void)drawPageNumber:(NSInteger)pageNum
      {
      	NSString* pageString = [NSString stringWithFormat:@"Page %d", pageNum];
      	UIFont* theFont = [UIFont systemFontOfSize:12];
      	CGSize maxSize = CGSizeMake(612, 72);
      	
      	CGSize pageStringSize = [pageString sizeWithFont:theFont
      								   constrainedToSize:maxSize
                                             lineBreakMode:UILineBreakModeClip];
      	CGRect stringRect = CGRectMake(((612.0 - pageStringSize.width) / 2.0),
      								   720.0 + ((72.0 - pageStringSize.height) / 2.0) ,
      								   pageStringSize.width,
      								   pageStringSize.height);
      	
      	[pageString drawInRect:stringRect withFont:theFont];
      }
      
  3. Hello, i got a error when i start PDFPrint the Application is crashing and the Only mistake i got in the Code is the renderInContext warning. i Use 5.1 Simulator and 4.3 xcode.

    1. Just updated the XCode project to add QuartzCore which was missing. You’ll need that for the renderInContext call. If you’re still seeing the crash feel free to email me the stack trace.

  4. Thanks for this info. I found a way to render the content at 144 dpi instead of 72 dpi. First set your UIWebView’s scalesPageToFit property to true. Then double the width and height of the UIWebView. Finally, add CGContextScaleCTM(currentContext, .5, .5) after the CGContextTranslateCTM line. Now the output looks much better! You can adjust the resolution for your needs by changing the scale ratio.

    1. That’s a great tip, thanks! I use similar code to this in a few projects of mine and so this tip is really helpful.

  5. After adding this functionality to my app, I’ve found that PDFs generated this way don’t display in Microsoft Outlook or Internet Explorer … both show a “missing image” (red X) icon where the PDF should be. Does this happen for you, too, or have I done something different in my app that’s causing the problem?

    1. Never mind, it looks like the problem is not with the PDF itself. I’m emailing the PDF using MFMailComposeViewController and the problem was a result of how the mail framework constructs the email message. It was adding these image-based PDFs with the multipart/related content type, which the Microsoft products didn’t like. However, it looks like iOS 6 adds these PDFs as multipart/mixed, which the Microsoft products display correctly.

  6. mattconnolly · · Reply

    It seems that the resulting PDF file has the text of the webview flattened into a screen resolution bitmap. This isn’t really good for printing, and the text cannot be selected for copying either.

    Have you seen this? If so, is there a workaround??

    1. There’s really no good workaround for making a pdf from a webview. We’re rending parts of the webview to CGContextRef and building the pdf from these renders. The page is essentially converted from text and images to just a flattened image.

      I suppose a you could extract the DOM and content of the page and rebuild it with UIKit elements like UIImageView, etc. Then you’d render those parts out like I do with the page number. That text when on the pdf is selectable. This would be a good time investment, but it seems like something that would work for you.

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 )

Connecting to %s

Follow

Get every new post delivered to your Inbox.

Join 466 other followers

%d bloggers like this: