Rendering and Caching

If you want to render an image of a page, or of part of a page, PSPDFRenderQueue is what you need. This article explains how to use the render queue and the render cache, both of which have undergone a big refactoring in PSPDFKit 6 for iOS. No matter if you never used the cache or the render queue before or if you want to move your existing code from v5 over to v6, this article guides you through the necessary steps.

The Architecture

As of v6 the architecture has changed a bit. The entry point for all sorts of image requests is a PSPDFRenderRequest, na matter whether you want to obtain an image from the cache or render a new image. The render request contains all the information that identifies the request and has an impact on how the resulting image will look.

With the render request you then are able to create a PSPDFRenderTask. A render task controls all the things that are related to the actual rendering. You can use it to alter the priority of the task, cancel a task, and receive the rendered image once the task has completed.

In order to execute a render task you then schedule this task in the global PSPDFRenderQueue. You can think of a task and its queue like NSOperation and NSOperationQueue. The concept is very similar, yet there are a couple of differences and optimizations going on under the hood.

“Rendering

Now that we have gone through the data flow in general, let’s look a bit closer on the different parts of rendering a page into an image. We will use the following example and go through it step by step:

Copy
1
2
3
4
5
6
7
8
9
10
11
let request = PSPDFMutableRenderRequest(document: document)
request.imageSize = pageImageSize

for pageIndex in UInt(0)..<document.pageCount {
    request.pageIndex = pageIndex

    guard let task = PSPDFRenderTask(request: request) else { continue }
    task.priority = .utility
    task.delegate = self
    PSPDFKit.sharedInstance.renderManager.renderQueue.schedule(task)
}
Copy
1
2
3
4
5
6
7
8
9
10
11
PSPDFMutableRenderRequest *request = [[PSPDFMutableRenderRequest alloc] initWithDocument:document];
request.imageSize = pageImageSize;

for (int pageIndex = 0; pageIndex < document.pageCount; pageIndex++) {
    request.pageIndex = pageIndex;

    PSPDFRenderTask *task = [[PSPDFRenderTask alloc] initWithRequest:request];
    task.priority = PSPDFRenderQueuePriorityUtility;
    task.delegate = self;
    [PSPDFKit.sharedInstance.renderManager.renderQueue scheduleTask:task];
}

PSPDFRenderRequest

PSPDFRenderRequest is a pair of two classes, a mutable and an immutable version. Usually you create a PSPDFMutableRenderRequest, configure it and then pass it on to -[PSPDFRenderTask initWithRequest:]. This will make a copy of the request, so that you can no longer modify the request in a task. This gives us the possibility to do many optimizations that result in a fast execution of your task. In addition this means, that you can reuse your mutable render request in case you need to create a batch of render tasks, as it is done in the above example.

The example also illustrates the minimum amount of information you need to specify for both, the request and the task. The request needs to be created with a PSPDFDocument and requires an imageSize to be set. The imageSize specifies the resulting size of the UIImage in points, or rather – to be more precise – the bounding box in which the resulting image will fit. If the aspect ratio of the specified image size does not match the one of the page, you will get an image that is smaller on one of the axis as the image is always rendered as aspect fit inside the specified imageSize.

The request also needs to have a pageIndex so that it knows which page to render. If you don’t specify a pageIndex it assumes page 0, which might be fine if you just want to render a cover image.

The above setup results in an image that contains the full page. If you only want to render a part of a page, this can also be done by specifying a pdfRect in pdf coordinates. Note that the resulting image will still have the size of imageSize but only contains the pdfRect you specified. This is different than in previous versions of PSPDFKit. The resulting image contains the content of the specified pdf rect, drawn as aspect fit. So the image might be smaller as the requested size in one axis and will contain exactly the content of the specified pdf rect.

PSPDFRenderTask

The render task is the object that can be used to alter the priority of requests or cancel them altogether. It is considered a best practice to either cancel tasks that are no longer needed or to reduce the priority of a task that is not needed anymore but is likely to be relevant again in the near future.

You can set a tasks priority at any time, even after scheduling a task in the render queue. Note that a task with a higher priority is not necessarily executed before a task with a lower priority. This depends on a variety of factors, like other tasks requesting the same image and whether a given request produces a cache hit or not. PSPDFKit will execute as many tasks as possible in a timely manner. Also note that the order of execution might change as we improve our algorithms to determine the next most important task, so do not rely on execution orders.

To get informed of the completion of a task you have two options. A render task can have a delegate as well as a completionHandler. You can use either one of them or both. To cancel a task, simply call cancel on it and you will no longer receive any callbacks from this task.

The Cache

As of PSPDFKit v6 you do no longer need to worry about PSPDFCache under normal conditions. Scheduling a task in a render queue will automatically ensure that a new image is only rendered if there is nothing found in the cache and the framework automatically stores images in the cache that are likely to be retrieved again in the future. If you schedule a render task that produces a cache hit, we will call your completion handler and the delegate in a prioritized way so that a task does not unnecessarily need to wait in the render queue.

However due to its nature, PSPDFRenderTask is always asynchronous. If you, for some reason, need an image synchronously, you can use the same PSPDFRenderRequest you would use to create a task, to retrieve an image from the cache as well. All you need to do is call -[PSPDFCache imageForRequest:imageSizeMatching:] with the request and a size matching strategy. With the size matching you can tell the cache whether you would also be fine with images smaller or larger than your requested image size. Depending on your needs this may be helpful as it usually has a greater chance of returning an image from the cache, however, especially when requesting larger images, keep the memory implications of this in mind.

Before directly querying the cache, think about your approach and whether it is really necessary to synchronously request images from the cache. If the cache does not contain an image for your request, you would need to schedule a render task anyway, so usually your code should be written to work with asynchronous requests anyway. You should not use PSPDFCache as your data store, instead store images you are actively using in another data structure.

While you have no control over whether a rendered image will be stored in the cache or not, you can control whether a request accesses the cache and is able to perform an actual rendering. By default, PSPDFRenderRequest automatically determines the best strategy to fulfill your request. Other options are to ignore the cache and render a new image, check the cache first and render an image only if the cache did not produce a result, and only check the cache and do not render an image if the cache does not have a matching image.

iOS Data Protection for the Disk Cache

You can customize the default iOS Data Protection level by setting a new default on the cache folder. Here's how you can access this directory.

1
let cacheDirectory = PSPDFKit.sharedInstance.cache.diskCache.cacheDirectory
1
NSString *cacheDirectory = PSPDFKit.sharedInstance.cache.diskCache.cacheDirectory;

Note: This directory is automatically created upon first cache write. Clearing the cache will delete all sub-directories, but not the directory itself. To learn more, read the Instant guide about iOS Data Protection.

Disabling the Disk Cache

By default, the cache will store images in memory and on disk. You can disable the disk cache by setting its allowedDiskSpace to 0. The disk cache is accessible from the shared PSPDFCache object.

1
PSPDFKit.sharedInstance.cache.diskCache.allowedDiskSpace = 0
1
PSPDFKit.sharedInstance.cache.diskCache.allowedDiskSpace = 0;

Cache Invalidation

Cache invalidation is usually done by the framework itself and you don’t need to worry about this. However if you add custom functionality that may change the rendering of a page, you need to invalidate the cache manually. To do so you call -[PSPDFCache invalidateImageFromDocument:pageIndex:]. This removes all images for the given page from both, the in memory cache and the disk cache. All render requests that are executed after this method has been called therefore no longer produce a cache hit and will render a new image for the page which is then again added to the cache.

If, for whatever reason, you only require a new rendering of a specific image size, you can also specify this while requesting a new image by setting the cachePolicy property of PSPDFRenderRequest to PSPDFRenderRequestCachePolicyReloadIgnoringCacheData. This will result in a new rendering, ignoring eventual cache hits. The new image will then override any existing image in the cache that matches the given request. Keep in mind though, that the order in which requests are executed should be treated as non-deterministic as mentioned above. Tasks you schedule after the task with the above request might in fact execute before it and therefore still get the old image from the cache.

Synchronous Rendering

Synchronous page rendering is generally discouraged because it is blocking the main thread when rendering a large and complex page. You can request a page image using [-[PSPDFDocument imageForPageAtIndex:size:clippedToRect:annotations:options:error:]]. You won't need a create a render request or task, as the image is rendered synchronously. We recommend that you always use render requests and tasks if possible, and only use the synchronous method, if there is no other way out, in your applications' flow.

Copy
1
2
3
let document = // PSPDFDocument object
guard let pageInfo = document.pageInfoForPage(at: 0) else { return }
let pageImage = try? document.imageForPage(at: 0, size: pageInfo.rotatedRect.size, clippedTo: .zero, annotations: nil, options: nil)
Copy
1
2
3
PSPDFDocument *document = // PSPDFDocument object
PSPDFPageInfo *pageInfo = [document pageInfoForPageAtIndex:0];
UIImage *pageImage = [document imageForPageAtIndex:0 size:pageInfo.rotatedRect.size clippedToRect:CGRectZero annotations:nil options:nil error:&error];

Debugging

If you are debugging things related to render requests, tasks, or the cache PSPDFKit has a couple of environment variables you can set to alter the behavior of the render engine. This makes it easier to debug certain types of problems. To set an environment variable, go to your project’s scheme settings in Xcode by opening the scheme list next to the run and stop buttons in the top bar and select ‘Edit Scheme…’ on the bottom of the list. In the ‘Run’ section, tap the + icon in the ‘Environment Variables’ list. Set the name and the value according to the list below. Make sure the checkmark next to the new entry is activated.

“Scheme

If you run your app through Xcode now, the added variable effects the way the render engine handles things. In your scheme settings you can now just tick or untick the box next to the environment variable to control whether you want to activate the environment variable or not.

Variable Value Description
PSPDFCacheDisabled YES Disables the cache so that every request to the cache produces a cache miss. This results in the render engine always scheduling a redraw of the requested image.
PSPDFSlowRendering YES Makes the rendering slower by blocking each render call for multiple seconds.

Be aware that environment variables only have an effect if you are actually launching your app through Xcode. If you launch your application through the home screen or distribute it via the App Store, this setting has no effect.