Fast and Lean PDF Viewer for iPhone / iPad / iOS - tips and hints?
IosSwiftPdfCalayerApple PdfkitIos Problem Overview
There has been many Questions recently about drawing PDF's.
Yes, you can render PDF's very easily with a UIWebView
but this cant give the performance and functionality that you would expect from a good PDF viewer.
You can draw a PDF page http://www.cocoabuilder.com/archive/cocoa/196313-display-pdf-in-calayer.html">to a CALayer or https://stackoverflow.com/questions/3831304/calayer-and-off-screen-rendering/3853778#3853778"> to a UIImage. Apple even have sample code to show how draw a large PDF http://developer.apple.com/library/ios/#samplecode/ZoomingPDFViewer/Introduction/Intro.html#//apple_ref/doc/uid/DTS40010281">in a Zoomable UIScrollview
But the same issues keep cropping up.
UIImage Method:
- PDF's in a
UIImage
don't optically scale as well as a Layer approach. - The CPU and memory hit on generating
the
UIImages
from aPDFcontext
limits/prevents using it to create a real-time render of new zoom-levels.
CATiledLayer Method:
- Theres a significant Overhead (time)
drawing a full PDF page to a
CALayer
: individual tiles can be seen rendering (even with a tileSize tweak) CALayers
cant be prepared ahead of time (rendered off-screen).
Generally PDF viewers are pretty heavy on memory too. Even monitor the memory usage of apple's zoomable PDF example.
In my current project, I'm developing a PDF viewer and am rendering a UIImage
of a page in a separate thread (issues here too!) and presenting it while the scale is x1. CATiledLayer
rendering kicks in once the scale is >1. iBooks takes a similar double take approach as if you scroll the pages you can see a lower res version of the page for just less than a second before a crisp version appears.
Im rendering 2 pages each side of the page in focus so that the PDF image is ready to mask the layer before it starts drawing.Pages are destroyed again when they are +2 pages away from the focused page.
Does anyone have any insights, no matter how small or obvious to improve the performance/ memory handling of Drawing PDF's? or any other issues discussed here?
EDIT: Some Tips (Credit- Luke Mcneice,VdesmedT,Matt Gallagher,Johann):
-
Save any media to disk when you can.
-
Use larger tileSizes if rendering on TiledLayers
-
init frequently used arrays with placeholder objects, alternitively another design approach is http://cocoawithlove.com/2009/01/multiple-virtual-pages-in-uiscrollview.html">this one
-
Note that images will render faster than a
CGPDFPageRef
-
Use
NSOperations
or GCD & http://thirdcog.eu/pwcblocks/">Blocks</a> to prepare pages ahead of time. -
call
CGContextSetInterpolationQuality(ctx, kCGInterpolationHigh); CGContextSetRenderingIntent(ctx, kCGRenderingIntentDefault);
beforeCGContextDrawPDFPage
to reduce memory usage while drawing -
init'ing your
NSOperations
with a docRef is a bad idea (memory), wrap the docRef into a singleton. -
Cancel needless
NSOperations
When you can, especially if they will be using memory, beware of leaving contexts open though! -
Recycle page objects and destroy unused views
-
Close any open Contexts as soon as you don't need them
-
on receiving memory warnings release and reload the DocRef and any page Caches
Other PDF Features:
-
https://stackoverflow.com/questions/3257057/iphone-cgpdfdocument-pdf-links">Getting Links inside a PDF (and https://stackoverflow.com/questions/4080373/get-pdf-hyperlinks-on-ios-with-quartz">here</a> and http://pastebin.com/69JW1Kkc">here</a>;)
-
https://stackoverflow.com/questions/4255298/how-does-an-annot-cgpdfdictionary-rect-translate-to-objective-c-points/4255586#4255586">Understanding the PDF Rect for link positioning
-
https://stackoverflow.com/questions/4303394/converting-adobe-pdf-date-string-to-nsdate">Converting PDF annot datestrings
-
Getting the target of the link (Getting the page number from the
/Dest
array) -
https://stackoverflow.com/questions/2556344/create-a-table-of-contents-from-a-pdf-file">Getting a table of contents
-
Document title and http://www.iphonedevsdk.com/forum/iphone-sdk-development/29770-pdf-title-keywords-label.html">Keywords</a>
-
https://stackoverflow.com/questions/2556344/create-a-table-of-contents-from-a-pdf-file/3427596#3427596">Getting Raw Text (and https://stackoverflow.com/questions/2960195/extracting-pdf-text-in-objective-c">here</a> and https://stackoverflow.com/questions/3627745">Here</a> and https://stackoverflow.com/questions/3627745/getting-text-position-while-parsing-pdf-with-quartz-2d">here</a> (positioning focused))
-
Searching(and"">http://blog.random-ideas.net/?p=184">Searching</a>(and https://stackoverflow.com/questions/4097044/pdf-search-on-the-iphone">here</a>;) (doesn't work with all PDFs (some just show weird characters, I guess it's an encoding issue but I'm not sure) -Credit BrainFeeder)
-
https://stackoverflow.com/questions/3831304/calayer-and-off-screen-rendering">CALayer and Off-Screen Rendering - render the next page for fast/smooth display
Documentation
- http://developer.apple.com/library/mac/#documentation/GraphicsImaging/Reference/CGPDFObject/Reference/reference.html">Quartz PDFObjects (Used for meta info, annotations, thumbs)
- http://www.adobe.com/devnet/pdf/pdf_reference.html">Abobe PDF Spec
Example projects
- Apple/ ZoomingPDF - zooming,
UIScrollView
,CATiledLayer
- vfr/ reader - zooming, paging,
UIScrollView
,CATiledView
- brow/ leaves - paging with nice transitions
- / skim - everything it seems (PDF reader/editor for OSX)
Ios Solutions
Solution 1 - Ios
I have build such kind of application using approximatively the same approach except :
- I cache the generated image on the disk and always generate two to three images in advance in a separate thread.
- I don't overlay with a
UIImage
but instead draw the image in the layer when zooming is 1. Those tiles will be released automatically when memory warnings are issued.
Whenever the user start zooming, I acquire the CGPDFPage
and render it using the appropriate CTM. The code in - (void)drawLayer: (CALayer*)layer inContext: (CGContextRef) context
is like :
CGAffineTransform currentCTM = CGContextGetCTM(context);
if (currentCTM.a == 1.0 && baseImage) {
//Calculate ideal scale
CGFloat scaleForWidth = baseImage.size.width/self.bounds.size.width;
CGFloat scaleForHeight = baseImage.size.height/self.bounds.size.height;
CGFloat imageScaleFactor = MAX(scaleForWidth, scaleForHeight);
CGSize imageSize = CGSizeMake(baseImage.size.width/imageScaleFactor, baseImage.size.height/imageScaleFactor);
CGRect imageRect = CGRectMake((self.bounds.size.width-imageSize.width)/2, (self.bounds.size.height-imageSize.height)/2, imageSize.width, imageSize.height);
CGContextDrawImage(context, imageRect, [baseImage CGImage]);
} else {
@synchronized(issue) {
CGPDFPageRef pdfPage = CGPDFDocumentGetPage(issue.pdfDoc, pageIndex+1);
pdfToPageTransform = CGPDFPageGetDrawingTransform(pdfPage, kCGPDFMediaBox, layer.bounds, 0, true);
CGContextConcatCTM(context, pdfToPageTransform);
CGContextDrawPDFPage(context, pdfPage);
}
}
issue is the object containg the CGPDFDocumentRef
. I synchronize the part where I access the pdfDoc
property because I release it and recreate it when receiving memoryWarnings. It seems that the CGPDFDocumentRef
object do some internal caching that I did not find how to get rid of.
Solution 2 - Ios
For a simple and effective PDF viewer, when you require only limited functionality, you can now (iOS 4.0+) use the QuickLook framework:
First, you need to link against QuickLook.framework
and #import <QuickLook/QuickLook.h>;
Afterwards, in either viewDidLoad
or any of the lazy initialization methods:
QLPreviewController *previewController = [[QLPreviewController alloc] init];
previewController.dataSource = self;
previewController.delegate = self;
previewController.currentPreviewItemIndex = indexPath.row;
[self presentModalViewController:previewController animated:YES];
[previewController release];
Solution 3 - Ios
Since iOS 11, you can use the native framework called PDFKit for displaying and manipulating PDFs.
After importing PDFKit, you should initialize a PDFView
with a local or a remote URL and display it in your view.
if let url = Bundle.main.url(forResource: "example", withExtension: "pdf") {
let pdfView = PDFView(frame: view.frame)
pdfView.document = PDFDocument(url: url)
view.addSubview(pdfView)
}
Read more about PDFKit in the Apple Developer documentation.