Making a PDF from a UIWebView

I don’t write here anymore. You can now find me at coderchrismills.com.

Update – If Brent Nycum made a great little helper class to make printing PDFs from WebViews easy. Check it out over on GitHub

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.

27 thoughts on “Making a PDF from a UIWebView

  1. Pingback: Making a PDF from a UIWebView – Updated « Chris Mills

    • 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];
      }
      
      Reply
  2. 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.

    Reply
    • 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.

      Reply
  3. 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.

    Reply
  4. 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?

    Reply
    • 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.

      Reply
  5. 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??

    Reply
    • 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.

      Reply
  6. Where does the generated PDF reside? I am using Brent’s class, and while all seem to be in order, I can not find the actual pdf. Actually, I need to email that. Thanks in advance for any help…

    Reply
    • It depends. In Brent’s class you have the option to write to a file or write to an NSData object.

      - (void)saveHtmlAsPdf:(NSString *)html toFile:(NSString *)file

      is what you’re looking for. You could say write to a temp directory,

      NSString *tmp_dir = NSTemporaryDirectory();
      NSString *file_path = [tmp_dir stringByAppendingPathComponent:[NSString stringWithFormat:@"%f.pdf", [[NSDate date] timeIntervalSince1970] ]];
      [brentsClass saveHtmlAsPdf:your_html toFile:file_path];
      

      The file is then in the temp directory. If you’re running in the Simulator it can be found in your ~/Library/Application Support/iPhone Simulator/x.x/guid/tmp folder. Where x.x is the version of the SDK you’re testing against and the guid is well, a guid. You can dig through the x.x folder to find the app you’re testing.

      Writing all this code in the reply window and has been untested, but that is generally how you would go about that part. Now to email it.

      Assuming you saved the file path or have some way of getting it, you can just present the user with a way to share the pdf using the UIActivityViewController.

      NSArray *activityItems = @[[NSData file_path]];
      UIActivityViewController *activityViewController = [[UIActivityViewController alloc] initWithActivityItems:activityItems
                                                                                               applicationActivities:nil];
          [self presentViewController:activityViewController animated:YES completion:NULL];
      

      If you only want to allow for email you’ll have to use the MailComposeViewController

      MFMailComposeViewController* mailComposer = [[MFMailComposeViewController alloc] init];
      	mailComposer.mailComposeDelegate = self;
      	[mailComposer addAttachmentData:[NSData dataWithContentsOfFile:file_path]
      						   mimeType:@"application/pdf" fileName:@"myPDF.pdf"];
      	[self presentModalViewController:mailComposer animated:YES];
      

      You’ll want to do this stuff this delegate method. That way you get the file it made and also know that it completed without error.

      - (void)htmlPdfKit:(BNHtmlPdfKit *)htmlPdfKit didSavePdfFile:(NSString *)file;
      

      Hope this helps. Let me know if anything is unclear.

      Reply
      • Chris,

        Thank you for your quick and thorough reply. I am now anxious to try your suggestions, but unfortunately I will not be able to do it until tomorrow night. I will let you know how it turned out.

        M. Bayona

      • Chris, I apologize for not being able to follow through with my own question, but I had been out of town and as soon as I got back to my app, I discovered a nasty bug that I am still trying to fix. I will most definitely work on my PDF generation as soon as I have my app once again under control.

        M. Bayona

      • Chris,

        I have tried the code to generate the PDF files and send the via email, with no success. The following is the situation:

        I have a button that brings in a popOverView. In it I need to display a webView. This webView will host a html string that is dynamically generated. So fat the output in this webView looks the way I want. I need then to email this “web page” as a PDF file. I followed your suggestions as follows:

        my web view is called: mainWebView
        my HTML string is called: htmlString

        // Load webView //
        NSString *path = [[NSBundle mainBundle] bundlePath];
        NSURL *baseURL = [NSURL fileURLWithPath:path];
        [mainWebView loadHTMLString:htmlString baseURL:baseURL];

        // Create PDF //
        NSString *tmp_dir = NSTemporaryDirectory();
        NSString *file_path = [tmp_dir stringByAppendingPathComponent:[NSString stringWithFormat:@”%f.pdf”, [[NSDate date] timeIntervalSince1970]]];
        BNHtmlPdfKit *htmlPdfKit = [[BNHtmlPdfKit alloc] init];
        htmlPdfKit.delegate = self;
        [htmlPdfKit saveHtmlAsPdf:htmlString toFile:file_path];

        // Mail PDF //
        MFMailComposeViewController* mailComposer = [[MFMailComposeViewController alloc] init];
        mailComposer.mailComposeDelegate = self;
        [mailComposer addAttachmentData:[NSData dataWithContentsOfFile:file_path]
        mimeType:@”application/pdf” fileName:@”myPDF.pdf”];
        [self presentModalViewController:mailComposer animated:YES];

        ///////////////////

        I have not yet been able to find the generated PDF, searching everywhere for it.
        The MailComposer does appear, with an icon of a pdf file, and even though it seems unresponsive, it did send me a blank email (with no attachment).

        Sorry for the length of this email. Any further help and suggestions will be greatly appreciated.

        Miguel

      • Hey Miguel,

        Made a new Project (Pdf Test 2) that should help you with the file location issue as well well as the mailing of the pdf. Think there might be a problem with the Brent’s class, so I added another method that makes it all work. Let me know if this doesn’t work for you. Feel free to email me at coderchrismills[at]gmail[dot]com.

        Edit : Should mention that I’m using the Xcode beta to build this so you’ll need that.

  7. Is there the possibility to break pages in certain points? For example I would add a page-break css property to some divs, and get a similar page break in the pdf. Is it possibile?

    Reply
    • Yes. The stringByEvaluatingJavaScriptFromString could be modified to get the bounds of the div, or query the page-break properties. If you can get some raw js commands to work, you should have little problem using this method.

      Reply
  8. Hi Chris, Thank you for this great piece of code. It works fine. However I am running into issues with page refresh/update. I am creating the pdf file based on the the input from the User. As the input changes, the PDF should display this change. However I am running into an issue with this. I get the updated data, at first it displays always the previous data then only when I click the PDF generation button again does it update to the changed data. Can you please help me with this. I need it very urgently… I appreciate your help.

    Thanks !!

    Reply
    • This method uses a UIWebView to generate the pdf. When called it will render the UIWebView to a render context and commit that to the pdf. For real-time updating which it sounds like you want, you should cache the information and show it to them in a way that can be updated while typing, say a UITextView. Then the UIWebView is a backing field that can be used to generate the pdf. I haven’t had much time to look lately, but iOS 8 does offer some new html in UITextView features that may help you.

      Reply
  9. This technique can be dangerous when the content of the web view is very long. In this case UIGraphicsBeginPDFPageWithInfo generates memory spikes. Have you ever experienced such kind of problem?

    Reply
    • In my experience there have been a number of memory issues with UIGraphicsBeginPDFPageWithInfo. Since you’re making a call to UIGraphicsBeginPDFPageWithInfo for each page you’re planning to render, and the page size is known before hand, the memory spikes that do happen shouldn’t go over time. If you’re content is really long, you probably want to have a step that breaks it into page size chunks, then process those individually.

      Reply
  10. Pingback: iOS create pdf from UIWebview content | DL-UAT

  11. Pingback: How can I make a PDF document containing a table of data on iPhone?

Leave a reply to Vic Cancel reply