Skip to content

Creating a PDF with iOS

CreatePDFExample

If you find yourself in the position where you need to generate a report that can be printed from your iOS application, you could either integrate directly with the AirPrint APIs, or you can alternatively generate a PDF file so the report can be emailed as well as printed. This is a relatively easy process, and is very similar to rendering graphics in a standard view context.

The key methods in this example are:

  • UIGraphicsBeginPDFContextToFile: instruct the system to generate a new graphics context backed by a file.
  • UIGraphicsGetCurrentContext: Retrieve the context that was just created
  • CGContext...: Any of the CGContext methods that can render to a graphics context
  • UIGraphicsBeginPDFPageWithInfo:Begin a new page in the PDF
  • UIGraphicsEndPDFContext: Finish the PDF and write it to disk (to the file specified in the call to UIGraphicsBeginPDFContextToFile

You’ll notice in the attached example that I use a CGRectZero to initialize the the PDF in the call to UIGraphicsBeginPDFContextToFile. This will create a PDF of the default 8 1/2″ x 11″ paper size, at 612 x 792 pixels. This is documented in the platform documentation at: http://developer.apple.com/library/ios/#documentation/2DDrawing/Conceptual/DrawingPrintingiOS/GeneratingPDF/GeneratingPDF.html

Other items to note: we are keeping track of our Y position on the page so we can render text line by line until we run out of space. We determine how much space we are going to use by using the NSString’s measurement functions, which are probably the easiest way to get simple text metrics from the system. For more advanced text rendering you could always use Core Text.

After we generate our PDF file, we present the user with the option of emailing the file or displaying it. To display it, we use the QuickLook view controller, which provides an easy way to preview any content the system knows how to render (this includes web, pdf, images, etc). From the quick look view controller, the user can then print the file. When emailing, the file is added as an attachment using the MFMailComposeViewController.

Here’s the code that makes it all possible (and a link to the sample project CreatePDFExample):


// create some sample data. In a real application, this would come from the database or an API.
NSString* path = [[NSBundle mainBundle] pathForResource:@"sampleData" ofType:@"plist"];
NSDictionary* data = [NSDictionary dictionaryWithContentsOfFile:path];
NSArray* students = [data objectForKey:@"Students"];

// get a temprorary filename for this PDF
path = NSTemporaryDirectory();
self.pdfFilePath = [path stringByAppendingPathComponent:[NSString stringWithFormat:@"%d.pdf", [[NSDate date] timeIntervalSince1970] ]];

// Create the PDF context using the default page size of 612 x 792.
// This default is spelled out in the iOS documentation for UIGraphicsBeginPDFContextToFile
UIGraphicsBeginPDFContextToFile(self.pdfFilePath, CGRectZero, nil);

// get the context reference so we can render to it.
CGContextRef context = UIGraphicsGetCurrentContext();

int currentPage = 0;

// maximum height and width of the content on the page, byt taking margins into account.
CGFloat maxWidth = kDefaultPageWidth - kMargin * 2;
CGFloat maxHeight = kDefaultPageHeight - kMargin * 2;

// we're going to cap the name of the class to using half of the horizontal page, which is why we're dividing by 2
CGFloat classNameMaxWidth = maxWidth / 2;

// the max width of the grade is also half, minus the margin
CGFloat gradeMaxWidth = (maxWidth / 2) - kColumnMargin;

// only create the fonts once since it is a somewhat expensive operation
UIFont* studentNameFont = [UIFont boldSystemFontOfSize:17];
UIFont* classFont = [UIFont systemFontOfSize:15];

CGFloat currentPageY = 0;

// iterate through out students, adding to the pdf each time.
for (NSDictionary* student in students)
{
// every student gets their own page
// Mark the beginning of a new page.
UIGraphicsBeginPDFPageWithInfo(CGRectMake(0, 0, kDefaultPageWidth, kDefaultPageHeight), nil);
currentPageY = kMargin;

// draw the student's name at the top of the page.
NSString* name = [NSString stringWithFormat:@"%@ %@",
[student objectForKey:@"FirstName"],
[student objectForKey:@"LastName"]];

CGSize size = [name sizeWithFont:studentNameFont forWidth:maxWidth lineBreakMode:UILineBreakModeWordWrap];
[name drawAtPoint:CGPointMake(kMargin, currentPageY) forWidth:maxWidth withFont:studentNameFont lineBreakMode:UILineBreakModeWordWrap];
currentPageY += size.height;

// draw a one pixel line under the student's name
CGContextSetStrokeColorWithColor(context, [[UIColor blueColor] CGColor]);
CGContextMoveToPoint(context, kMargin, currentPageY);
CGContextAddLineToPoint(context, kDefaultPageWidth - kMargin, currentPageY);
CGContextStrokePath(context);

// iterate through the list of classes and add these to the PDF.
NSArray* classes = [student objectForKey:@"Classes"];
for(NSDictionary* class in classes)
{
NSString* className = [class objectForKey:@"Name"];
NSString* grade = [class objectForKey:@"Grade"];

// before we render any text to the PDF, we need to measure it, so we'll know where to render the
// next line.
size = [className sizeWithFont:classFont constrainedToSize:CGSizeMake(classNameMaxWidth, MAXFLOAT) lineBreakMode:UILineBreakModeWordWrap];

// if the current text would render beyond the bounds of the page,
// start a new page and render it there instead
if (size.height + currentPageY > maxHeight) {
// create a new page and reset the current page's Y value
UIGraphicsBeginPDFPageWithInfo(CGRectMake(0, 0, kDefaultPageWidth, kDefaultPageHeight), nil);
currentPageY = kMargin;
}

// render the text
[className drawInRect:CGRectMake(kMargin, currentPageY, classNameMaxWidth, maxHeight) withFont:classFont lineBreakMode:UILineBreakModeWordWrap alignment:UITextAlignmentLeft];

// print the grade to the right of the class name
[grade drawInRect:CGRectMake(kMargin + classNameMaxWidth + kColumnMargin, currentPageY, gradeMaxWidth, maxHeight) withFont:classFont lineBreakMode:UILineBreakModeWordWrap alignment:UITextAlignmentLeft];

currentPageY += size.height;

}

// increment the page number.
currentPage++;

}

// end and save the PDF.
UIGraphicsEndPDFContext();

// Ask the user if they'd like to see the file or email it.
UIActionSheet* actionSheet = [[[UIActionSheet alloc] initWithTitle:@"Would you like to preview or email this PDF?"
delegate:self
cancelButtonTitle:@"Cancel"
destructiveButtonTitle:nil
otherButtonTitles:@"Preview", @"Email", nil] autorelease];
[actionSheet showInView:self.view];

 

{ 10 } Comments

  1. Coder | July 13, 2011 at 2:30 am | Permalink

    Thank you very much. Saved my day!

  2. Joe Andris | August 16, 2011 at 6:02 pm | Permalink

    Wow, thank you for the post! Just saved me a ton of time!

  3. srihari | August 24, 2011 at 9:52 am | Permalink

    Hi

    Great work, It has helped me very much

  4. Dave Hoebe | August 28, 2011 at 6:39 pm | Permalink

    Great post !! helped me to understand a lot !!!

  5. Chris | October 6, 2011 at 1:45 am | Permalink

    Thank you for such a great tutorial. This is something I’ve really been trying to figure out and you’ve explained it very clearly and well

  6. Shiela | October 31, 2011 at 6:44 am | Permalink

    This is just great! I may not be a programmer but how you explained it made sense. Thanks for making my life easier.:)

  7. Billy | March 9, 2012 at 3:39 am | Permalink

    Wow – fantastic! Thank you for a clear explanation!

  8. Kevin | March 28, 2012 at 1:42 pm | Permalink

    Very nice, perfect!! I’m glad you aren’t too reluctant :)

  9. Clément Wehrung | November 17, 2012 at 7:04 am | Permalink

    I created a class based on every good advice I found around. I’ve been digging a lot and I hope my class will offer some good start for anyone trying to create multi-page PDF directly out of some HTML source.

    You’ll find the whole code here with some basic sample code : https://github.com/iclems/iOS-htmltopdf

    I had just the same issue as you and my requirements were:
    – full PDF (real text, no bitmap)
    – smart multi-pages (compared to cutting a full height webview every X pixels…)

    Thus, the solution I use is pretty nice as it resorts to the same tools iOS uses to split pages for print.

    Let me explain, I setup a UIPrintPageRenderer based on the web view print formatter (first tip) :

    UIPrintPageRenderer *render = [[UIPrintPageRenderer alloc] init];

    [render addPrintFormatter:webView.viewPrintFormatter startingAtPageAtIndex:0];

    CGRect printableRect = CGRectMake(self.pageMargins.left,
    self.pageMargins.top,
    self.pageSize.width – self.pageMargins.left – self.pageMargins.right,
    self.pageSize.height – self.pageMargins.top – self.pageMargins.bottom);

    CGRect paperRect = CGRectMake(0, 0, self.pageSize.width, self.pageSize.height);

    [render setValue:[NSValue valueWithCGRect:paperRect] forKey:@”paperRect”];
    [render setValue:[NSValue valueWithCGRect:printableRect] forKey:@”printableRect”];

    NSData *pdfData = [render printToPDF];

    [pdfData writeToFile: self.PDFpath atomically: YES];

    In the meantime, I have created a category on UIPrintPageRenderer to support:

    -(NSData*) printToPDF
    {
    [self doNotRasterizeSubviews:self.view];

    NSMutableData *pdfData = [NSMutableData data];

    UIGraphicsBeginPDFContextToData( pdfData, CGRectZero, nil );

    [self prepareForDrawingPages: NSMakeRange(0, self.numberOfPages)];

    CGRect bounds = UIGraphicsGetPDFContextBounds();

    for ( int i = 0 ; i < self.numberOfPages ; i++ )
    {
    UIGraphicsBeginPDFPage();

    [self drawPageAtIndex: i inRect: bounds];
    }

    UIGraphicsEndPDFContext();

    return pdfData;
    }

  10. Adam | February 16, 2013 at 7:24 am | Permalink

    Thanks for the info/example! It got me jump started in a direction that may work out.

    Have you tried including an image per Student or per class?

    I’m working on rendering a PDF from Core Data. Each row has an image and 4 test elements.

    Should be 2-3 rows per page because each image I’m making a fixed size (sort of large).

Post a Comment

Your email is never published nor shared. Required fields are marked *