Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to render a complex UIView into a PDF Context with high resolution?

There are several questions on SO asking how to render a UIView into a PDF context, but they all use view.layer.renderInContext(pdfContext), which results in a 72 DPI image (and one that looks terrible when printed). What I'm looking for is a technique to somehow get the UIView to render at something like 300 DPI.

like image 551
David H Avatar asked Feb 16 '16 20:02

David H


1 Answers

In the end, I was able to take hints from several prior posts and put together a solution. I'm posting this since it took me a long time to get working, and I really hope to save someone else time and effort doing the same.

This solution uses two basic techniques:

  1. Render the UIView into a scaled bitmap context to produce a large image
  2. Draw the image into a PDF Context which has been scaled down, so that the drawn image has a high resolution

Build your view:

let v = UIView()
... // then add subviews, constraints, etc

Create the PDF Context:

UIGraphicsBeginPDFContextToData(data, docRect, stats.headerDict) // zero == (612 by 792 points)

defer { UIGraphicsEndPDFContext() }

UIGraphicsBeginPDFPage();

guard let pdfContext = UIGraphicsGetCurrentContext() else { return nil }

// I tried 300.0/72.0 but was not happy with the results
let rescale: CGFloat = 4 // 288 DPI rendering of VIew

// You need to change the scale factor on all subviews, not just the top view!
// This is a vital step, and there may be other types of views that need to be excluded

Then create a large bitmap of the image with an expanded scale:

func scaler(v: UIView) {
   if !v.isKindOfClass(UIStackView.self) {
      v.contentScaleFactor = 8
   }
   for sv in v.subviews {
      scaler(sv)
   }
}
scaler(v)

// Create a large Image by rendering the scaled view
let bigSize = CGSize(width: v.frame.size.width*rescale, height: v.frame.size.height*rescale)
UIGraphicsBeginImageContextWithOptions(bigSize, true, 1)
let context = UIGraphicsGetCurrentContext()!

CGContextSetFillColorWithColor(context, UIColor.whiteColor().CGColor)
CGContextFillRect(context, CGRect(origin: CGPoint(x: 0, y: 0), size: bigSize))

// Must increase the transform scale
CGContextScaleCTM(context, rescale, rescale)

v.layer.renderInContext(context)

let image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()

Now we have a large image with each point representing one pixel. To get it drawn into the PDF at high resolution, we need to scale the PDF down while drawing the image at its large size:

CGContextSaveGState(pdfContext)
CGContextTranslateCTM(pdfContext, v.frame.origin.x, v.frame.origin.y) // where the view should be shown

CGContextScaleCTM(pdfContext, 1/rescale, 1/rescale)

let frame = CGRect(origin: CGPoint(x: 0, y: 0), size: bigSize)
image.drawInRect(frame)

CGContextRestoreGState(pdfContext)

... // Continue with adding other items

You can see that the left "S" contained in the cream colored bitmap looks pretty nice compared to a "S" drawn but an attributed string:

enter image description here

When the same PDF is viewed by a simple rendering of the PDF without all the scaling, this is what you would see:

enter image description here

like image 173
David H Avatar answered Nov 07 '22 17:11

David H