How To Create a PDF with Quartz 2D in iOS 5 – Part 2

This is a blog post by iOS Tutorial Team member Tope Abayomi, an iOS developer and Founder of App Design Vault, your source for iPhone App Design. Welcome to the second part of the tutorial series on how to generate a PDF using Quartz 2D! In Part One, we set up an app framework for […] By Tope Abayomi.

Leave a rating/review
Save for later
Share

Contents

Hide contents

This is a blog post by iOS Tutorial Team member Tope Abayomi, an iOS developer and Founder of App Design Vault, your source for iPhone App Design.

Welcome to the second part of the tutorial series on how to generate a PDF using Quartz 2D!

In Part One, we set up an app framework for the PDF, and we drew some rudimentary text and lines with Quartz 2D inside our PDF.

Since our PDF will be an invoice document, it needs to look professional. To accomplish this, here in Part Two we’ll be adding a logo image and drawing a table to hold the invoice data. By the end of this article, our PDF will be complete.

Let’s jump back in!

Adding An Image

The image we’re going to draw to our PDF is the logo of none other than RayWenderlich.com. All part of Ray’s scheme to take over the world! ;]

Go ahead and download the image, then add it to the project. To do this, control-click on the PDFRenderer group in the Project navigator and choose “Add Files To iOSPDFRenderer.” Select the ‘ray-logo.png’ image you downloaded and then click Add.

Next open PDFRenderer.m and add this method:

 +(void)drawImage:(UIImage*)image inRect:(CGRect)rect
 {
    
    [image drawInRect:rect];
    
 }

Yes, drawing an image with Core Graphics is that easy! All we need is this one-liner to draw an image to the current context.

The method takes in the image we want to draw and the frame where it will be drawn, and draws the image into the context.

Next add the definition of this method in PDFRenderer.h:

 +(void)drawImage:(UIImage*)image inRect:(CGRect)rect;

Now we need to call the method so that it’s displayed on the PDF. Add the following lines of code to the drawPDF method on the PDFRenderer.m file (right before the call to UIGraphicsEndPDFContext):

 UIImage* logo = [UIImage imageNamed:@"ray-logo.png"];
 CGRect frame = CGRectMake(20, 100, 300, 60);
 [PDFRenderer drawImage:logo inRect:frame];

In the code above, we create a UIImage from the image file, define the location and size of the image to be drawn, then call the drawImage method we created above with these two parameters.

Here’s the complete version of the drawPDF method:

 +(void)drawPDF:(NSString*)fileName
 {
    // Create the PDF context using the default page size of 612 x 792.
    UIGraphicsBeginPDFContextToFile(fileName, CGRectZero, nil);
    // Mark the beginning of a new page.
    UIGraphicsBeginPDFPageWithInfo(CGRectMake(0, 0, 612, 792), nil);
    
    [self drawText];
    
    CGPoint from = CGPointMake(0, 0);
    CGPoint to = CGPointMake(200, 300);
    [PDFRenderer drawLineFromPoint:from toPoint:to];
    
    UIImage* logo = [UIImage imageNamed:@"ray-logo.png"];
    CGRect frame = CGRectMake(20, 100, 300, 60);
    
    [PDFRenderer drawImage:logo inRect:frame];
    
    // Close the PDF context and write the contents out.
    UIGraphicsEndPDFContext();
 }

Now if you run the application in the simulator, you’ll see our PDF as we left it in Part One, only now with Ray’s logo. Of course, it still looks pretty crappy. Time to fix that!

Drawing the Labels

At this point, we know how to draw the basic elements of the invoice: text, lines and images. The next step is to combine all the elements to create a polished layout.

To do this, we’ll use a little trick. We’ll create a View in the interface builder and add the elements for the invoice. Then we’ll use the locations of the views to make the layout.

This will make things a lot easier because we can visually layout the PDF, and won’t have to hard-code as many coordinates for drawing. Don’t worry, it will begin to make sense as we do it!

Create a new View in the application by choosing File\New\New File. Select the iOS\User Interface\View template and click on Next. Make sure the Device family is set to iPhone and click Next.

Give the new View the name InvoiceView and click Create. Select the View on the Interface Builder and delete it by hitting the backspace button.

Add a new View from the Objects tab onto the canvas. Resize the view to be 612 wide and 792 tall. These are the default dimensions for an A4 PDF file.

Add eight labels to the View and give them the following names:

  • Recipient [Name]
  • Recipient’s Address
  • Recipient’s City
  • Recipient’s Postal Code
  • Invoicer [Name]
  • Invoicer’s Address
  • Invoicer’s City
  • Invoicer’s Postal Code

The positions of these labels will form our layout for the invoice. Give each label a tag from 0-7. For example, the label “Recipient” will have a tag of 0, “Recipient’s Address” will have a tag of 1, and so on.

We’re done with the layout view for now. We’ll come back to it in a bit.

But first we need to open PDFRenderer.m and refactor the drawText method. We want to pass in the text to be drawn and the frame that will encompass it, rather than hard-coding it in.

Go ahead and replace drawText with the following (pretty much the same except pulling out the hardcoded string and frame):

 +(void)drawText:(NSString*)textToDraw inFrame:(CGRect)frameRect
 {
    CFStringRef stringRef = (__bridge CFStringRef)textToDraw;
    // Prepare the text using a Core Text Framesetter.
    CFAttributedStringRef currentText = CFAttributedStringCreate(NULL, stringRef, NULL);
    CTFramesetterRef framesetter = CTFramesetterCreateWithAttributedString(currentText);
      
    CGMutablePathRef framePath = CGPathCreateMutable();
    CGPathAddRect(framePath, NULL, frameRect);
    
    // Get the frame that will do the rendering.
    CFRange currentRange = CFRangeMake(0, 0);
    CTFrameRef frameRef = CTFramesetterCreateFrame(framesetter, currentRange, framePath, NULL);
    CGPathRelease(framePath);
    
    // Get the graphics context.
    CGContextRef    currentContext = UIGraphicsGetCurrentContext();
    
    // Put the text matrix into a known state. This ensures
    // that no old scaling factors are left in place.
    CGContextSetTextMatrix(currentContext, CGAffineTransformIdentity);
        
    // Core Text draws from the bottom-left corner up, so flip
    // the current transform prior to drawing.
    CGContextTranslateCTM(currentContext, 0, 100);
    CGContextScaleCTM(currentContext, 1.0, -1.0);
    
    // Draw the frame.
    CTFrameDraw(frameRef, currentContext);
    
    CFRelease(frameRef);
    CFRelease(stringRef);
    CFRelease(framesetter);
 }

Then add the new definition to PDFRenderer.h:

 +(void)drawText:(NSString*)textToDraw inFrame:(CGRect)frameRect;

The next step is to load the labels from the InvoiceView and use the text and the frame to draw to the PDF. Add this new method to PDFRenderer.m, right above drawPDF:

 +(void)drawLabels
 {    
   NSArray* objects = [[NSBundle mainBundle] loadNibNamed:@"InvoiceView" owner:nil options:nil];
    
    UIView* mainView = [objects objectAtIndex:0];
    
    for (UIView* view in [mainView subviews]) {
        if([view isKindOfClass:[UILabel class]])
        {
            UILabel* label = (UILabel*)view;
            
            [self drawText:label.text inFrame:label.frame];
        }
    }
 }

This will load the labels from the InvoiceView, loop through all the labels and call the drawText method with its text and frame variables.

Next we’ll modify the drawPDF method call this method and remove all the test drawing code we used previously. Replace drawPDF with the following:

 +(void)drawPDF:(NSString*)fileName
 {
    // Create the PDF context using the default page size of 612 x 792.
    UIGraphicsBeginPDFContextToFile(fileName, CGRectZero, nil);
    // Mark the beginning of a new page.
    UIGraphicsBeginPDFPageWithInfo(CGRectMake(0, 0, 612, 792), nil);
    
    [self drawText:@"Hello World" inFrame:CGRectMake(0, 0, 300, 50)];
    
    [self drawLabels];
    
    // Close the PDF context and write out the contents.
    UIGraphicsEndPDFContext();
 }

Let’s try it out! Run the application in the simulator, and you will see something similar to the image below.

Yikes – it looks like garbage, right? That’s because the text is being flipped and translated multiple times in the the drawText method. Up until now, we’ve only drawn one line of text, but the problem arises when we want to draw multiple lines of text.

To fix this, we need to modify the drawText method to flip the current context back to its original coordinates. Change the following lines in the drawText method to add the reverse-flip actions and take the origin into consideration:

    // Core Text draws from the bottom-left corner up, so flip
    // the current transform prior to drawing.
    // Modify this to take into consideration the origin.
    CGContextTranslateCTM(currentContext, 0, frameRect.origin.y*2);
    CGContextScaleCTM(currentContext, 1.0, -1.0);
    
    // Draw the frame.
    CTFrameDraw(frameRef, currentContext);
    

    // Add these two lines to reverse the earlier transformation.
    CGContextScaleCTM(currentContext, 1.0, -1.0);
    CGContextTranslateCTM(currentContext, 0, (-1)*frameRect.origin.y*2);

Now run the application again. You should see a nicely laid-out PDF with all our labels mapped.

Tope Abayomi

Contributors

Tope Abayomi

Author

Over 300 content creators. Join our team.