• Blog
  • Categories
    • App Store
    • Benchmarks
    • iOS
    • Objective-C
    • OS X
    • PHP
    • RV
    • Swift
    • tvOS
    • Web
  • Apps
    • Portfolio
    • Studio Pro
    • Sun & Moon
  • Photography

Projects

Remote Working, iOS, Mac OS X, and more

iOS

Correctly Drawing PDFs in Cocoa

Nearly every example available online for rasterizing a PDF document into an image on iOS or OS X is wrong. Most of those take this approach:

//Document Loading
CGPDFDocumentRef pdfDocument = CGPDFDocumentCreateWithURL(#URL#);
CGPDFPageRef page = CGPDFDocumentGetPage(pdfDocument, 1);

//Sizing
CGRect mediaBox = CGPDFPageGetBoxRect(page, kCGPDFMediaBox);
size_t width = (size_t) CGRectGetWidth(mediaBox);
size_t height = (size_t) CGRectGetHeight(mediaBox);
size_t bytesPerRow = ((width * 4) + 0x0000000F) & ~0x0000000F;

//Drawing context
CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
CGContextRef context =  CGBitmapContextCreate(NULL, width, height, 8, bytesPerRow, colorSpace, (kCGImageAlphaPremultipliedFirst | kCGBitmapByteOrder32Little));
CGColorSpaceRelease(colorSpace);

//Fill the background color
CGContextSetFillColorWithColor(context, UIColor.whiteColor.CGColor);
CGContextFillRect(context, CGRectMake(0.0, 0.0, width, height));

//Draw the page
CGContextDrawPDFPage(context, page);

//Get the image
CGImageRef result = CGBitmapContextCreateImage(context);
UIImage *resultUIImage = [UIImage imageWithCGImage:result];

//Clean up
CGContextRelease(context);
CGImageRelease(result);
CGPDFDocumentRelease(pdfDocument);

Error and bounds checking omitted for brevity.

This generally works, but where it really falls apart is when the PDF document has something unexpected, whether it’s a drawing affine transformation not equal to the identity matrix or a media box not equal to the crop box.

PDF tests of the incorrect rendering algorithm

As you can see, anything beyond a simple document renders completely wrong. Creating an algorithm to get the correct result means incorporating both the document transform and using the correct page box.

//This is defined previously and is the size
//we want to draw the document at. After all,
//PDF documents are meant to be vector content
//so they can be scaled at will.
CGSize drawingSize;

//Document Loading
CGPDFDocumentRef pdfDocument = CGPDFDocumentCreateWithURL(#URL#);
CGPDFPageRef page = CGPDFDocumentGetPage(pdfDocument, 1);

//Start by getting the crop box since only 
//its contents should be drawn
CGRect cropBox = CGPDFPageGetBoxRect(page, kCGPDFCropBox);

//Account for rotation of the page to figure
//out the size to create the context. Like images, 
//rotation can be represented by one of two 
//ways in a PDF: the contents can be pre-rotated
//in which case nothing needs to be done or the
//document can have its rotation value set and
//it means we need to apply the rotation as an
//affine transformation when drawing
NSInteger rotationAngle = CGPDFPageGetRotationAngle(page);
CGFloat angleInRadians = -rotationAngle * (M_PI / 180);
CGAffineTransform transform = CGAffineTransformMakeRotation(angleInRadians);
CGRect rotatedCropRect = CGRectApplyAffineTransform(cropBox, transform);

//Here we're figuring out the closest size we
//can draw the PDF at that's no larger than
//drawingSize
CGRect bestFit = BMBestFitFrameForSizeInRect(rotatedCropRect.size, CGRectMake(0.0, 0.0, drawingSize.width, drawingSize.height));
CGFloat scale = CGRectGetHeight(bestFit) / CGRectGetHeight(rotatedCropRect);
size_t width = (size_t) roundf(CGRectGetWidth(bestFit));
size_t height = (size_t) roundf(CGRectGetHeight(bestFit));
size_t bytesPerRow = ((width * 4) + 0x0000000F) & ~0x0000000F;

//Create the drawing context
CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
CGContextRef context =  CGBitmapContextCreate(NULL, width, height, 8, bytesPerRow, colorSpace, (kCGImageAlphaPremultipliedFirst | kCGBitmapByteOrder32Little));
CGColorSpaceRelease(colorSpace);

//Fill the background color
CGContextSetFillColorWithColor(context, UIColor.whiteColor.CGColor);
CGContextFillRect(context, CGRectMake(0.0, 0.0, width, height));

//This portion is the core of drawing the PDF
//correctly and is seldom seen in any examples
//found online. This is where we create the
//affine transformation matrix to align the
//PDF's CropBox to our drawing context.
transform = CGPDFPageGetDrawingTransform(page, kCGPDFCropBox, CGRectMake(0, 0, CGRectGetWidth(bestFit), CGRectGetHeight(bestFit)), 0, true);

if (scale > 1)
{
    //Since CGPDFPageGetDrawingTransform won't
    //scale up, we need to do it manually
    transform = CGAffineTransformTranslate(transform, CGRectGetMidX(cropBox), CGRectGetMidY(cropBox));
    transform = CGAffineTransformScale(transform, scale, scale);
    transform = CGAffineTransformTranslate(transform, -CGRectGetMidX(cropBox), -CGRectGetMidY(cropBox));
}

CGContextConcatCTM(context, transform);

//Clip the drawing to the CropBox
CGContextAddRect(context, cropBox);
CGContextClip(context);

CGContextDrawPDFPage(context, page);

CGImageRef result = CGBitmapContextCreateImage(context);
UIImage *resultUIImage = [UIImage imageWithCGImage:result];

CGContextRelease(context);
CGImageRelease(result);
CGPDFDocumentRelease(pdfDocument);

Again, error and bounds checking omitted for brevity.

Now, let’s test the result:

PDF tests of the incorrect rendering algorithm

Everything draws exactly as it should. This example has used the UI* classes rather than their NS* counterparts, but as of OS X 10.8 they’re almost fully API-compatible, so it’s a quick change to use one or the other. Beyond this it’s also relatively easy to add on support for interactive functionality, such as a table of contents or embedded links.

Previous
Next

Copyright 2025 Ryan Britton