PDF Rendering Library for iOS

If you want to render an image of a page or part of a page, RenderQueue is what you need. This article guides you through the necessary steps to use the render queue and the render cache.

Rendering a Page to an Image

You can render a page from a document to a UIImage like so:

let document = ...
let pageImageSize = ...
let pageIndex = ...

// Create a render request from your `Document`.
let request = MutableRenderRequest(document: document)
request.imageSize = pageImageSize
request.pageIndex = pageIndex

do {
    // Create a render task using the `MutableRenderRequest`.
    let task = try RenderTask(request: request)
    task.priority = .utility
    task.completionHandler = { image, error in
        if let error {
            // Handle rendering error.
        } else if let image {
            // Use the `UIImage` of the page.
        }
    }
    PSPDFKit.SDK.shared.renderManager.renderQueue.schedule(task)
} catch {
    // Handle `RenderTask` creation error. For example, this
    // may happen if the `imageSize` contains negative numbers.
}
PSPDFDocument *document = ...
PSPDFPageIndex pageIndex = ...
CGSize pageImageSize = ...

// Create a render request from your `PSPDFDocument`.
PSPDFMutableRenderRequest *request = [[PSPDFMutableRenderRequest alloc] initWithDocument:document];
request.imageSize = pageImageSize;
request.pageIndex = pageIndex;

// Create a render task using the `PSPDFMutableRenderRequest`.
NSError *error;
PSPDFRenderTask *task = [[PSPDFRenderTask alloc] initWithRequest:request error:&error];
if (task == nil) {
    // Handle task creation error. For example, this may
    // happen if the `imageSize` contains negative numbers.
}
task.priority = PSPDFRenderQueuePriorityUtility;
task.completionHandler = ^(UIImage *_Nullable image, NSError *_Nullable renderError) {
    if (renderError != nil) {
        // Handle rendering error.
    } else if (image != nil) {
        // Use the `UIImage` of the page.
    }
};
[PSPDFKitGlobal.sharedInstance.renderManager.renderQueue scheduleTask:task];

Rendering Architecture

The entry point for all image requests is a RenderRequest, no matter whether you want to obtain an image from the cache or render a new image. The render request contains all the information that specifies the image you want to obtain.

With the render request, you’re able to create a RenderTask. You can use the task object to alter the priority of a task, cancel a task, or receive the rendered image once a task has completed.

To execute a render task, you must schedule this task in the global RenderQueue. You can think of a task and its queue like Operation and OperationQueue. The concept is similar, yet there are a couple of differences and optimizations going on under the hood.

Rendering Architecture

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

let request = MutableRenderRequest(document: document)
request.imageSize = pageImageSize

for pageIndex in UInt(0)..<document.pageCount {
    request.pageIndex = pageIndex
    do {
        let task = try RenderTask(request: request)
        task.priority = .utility
        task.delegate = self
        PSPDFKit.SDK.shared.renderManager.renderQueue.schedule(task)
    } catch {
        // Handle error.
    }
}
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;
    [PSPDFKitGlobal.sharedInstance.renderManager.renderQueue scheduleTask:task];
}

Render Requests

RenderRequest is a pair of two classes: a mutable and an immutable version. Usually you create a MutableRenderRequest, configure it, and then pass it on to RenderTask(request:). This will make a copy of the request so that you can no longer modify the request in a task. It also gives PSPDFKit the possibility of making many optimizations that will result in the fast execution of your task, and it means you can reuse your mutable render request in case you need to create a batch of render tasks, as is done in the example above.

The example also illustrates the minimum amount of information needed to specify for both the request and the task. The request needs to be created with a Document and requires an imageSize to be set. The imageSize specifies the resulting size of the UIImage in points — or rather, more precisely, it specifies the bounding box in which the resulting image will fit. If the aspect ratio of the specified image size doesn’t match the one of the page, you’ll get an image that is smaller on one of the axes, as the image is always rendered by fitting its aspect ratio 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, the request assumes the page is 0, which might be fine if you just want to render a cover image.

The setup above 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 contain the pdfRect you specified. The resulting image contains the content of the specified PDF rectangle, drawn by fitting its content inside the rectangle while keeping its aspect ratio. As a result, the image might be smaller than the requested size in one axis and will contain exactly the content of the specified PDF rectangle.

Render Tasks

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

You can set a task’s priority at any time, even after scheduling it 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, such as other tasks requesting the same image and whether or not a given request produces a cache hit. 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 don’t rely on execution orders.

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

Render Cache

You don’t need to worry about PDFCache under normal conditions. Scheduling a task in a render queue will automatically ensure that a new image is only rendered if there’s 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’ll 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, RenderTask is always asynchronous. If you, for some reason, need to render an image synchronously, you can use the same RenderRequest you’d use to create a task to retrieve an image from the cache as well. All you need to do is call PDFCache.image(for:imageSizeMatching:) with the request and a size-matching strategy. With size matching, you can tell the cache whether you’d 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, you should keep the memory implications of this in mind, especially when requesting larger images.

Before directly querying the cache, think about your approach and whether it’s really necessary to synchronously request images from the cache. If the cache doesn’t contain an image for your request, you’d need to schedule a render task anyway, so usually your code should be written to work with asynchronous requests. You shouldn’t use PDFCache as your data store; instead, store images you’re actively using in another data structure.

While you have no control over whether or not a rendered image will be stored in the cache, you can control if a request accesses the cache and is able to perform an actual rendering. By default, RenderRequest 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 didn’t produce a result, or only check the cache and not render an image if the cache doesn’t 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:

let cacheDirectory = PSPDFKit.SDK.shared.cache.diskCache.cacheDirectory
NSString *cacheDirectory = PSPDFKitGlobal.sharedInstance.cache.diskCache.cacheDirectory;
Information

This directory is automatically created upon first cache write. Clearing the cache will delete all subdirectories but not the directory itself. To learn more, read the Instant guide about iOS data protection.

Also, refer to the PSPDFKit SDK security guide for an overview of SDK security options.

Disabling the Disk Cache per Document

The useDiskCache property on Document controls whether or not the disk cache is used for a certain document. It defaults to true unless any data provider disables useDiskCache, or if any document provider is encrypted, in which case it returns false:

document.useDiskCache = false
document.useDiskCache = NO;

Disabling the Disk Cache Globally

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 PDFCache object:

PSPDFKit.SDK.shared.cache.diskCache.allowedDiskSpace = 0
PSPDFKitGlobal.sharedInstance.cache.diskCache.allowedDiskSpace = 0;
Information

Disabling the cache may result in performance issues, especially on older devices.

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, call PDFCache.invalidateImage(from:pageIndex:). This removes all images for the given page from both the in-memory cache and the disk cache. All render requests executed after this method has been called will no longer produce a cache hit and will then render a new image for the page, which is then added to the cache again.

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 RenderRequest to PSPDFRenderRequestCachePolicyReloadIgnoringCacheData. This will result in a new rendering that ignores eventual cache hits. The new image will then override any existing image in the cache that matches the given request. Keep in mind 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 request above might in fact execute before it, resulting in the retrieval of the old image from the cache.

Synchronous Rendering

Synchronous page rendering is generally discouraged because it blocks the main thread when rendering a large and complex page. You can request a page image using Document.imageForPage(at:size:clippedTo:annotations:options:). You won’t need to create a render request or task, as the image is rendered synchronously. We recommend you always use render requests and tasks and only use the synchronous method if there’s no other way out:

let document = // `Document` object.
let pageInfo = document.pageInfoForPage(at: 0)!
do {
    let pageImage = try document.imageForPage(at: 0, size: pageInfo.size, clippedTo: .zero, annotations: nil, options: nil)
} catch {
    // Handle error.
}
PSPDFDocument *document = // PSPDFDocument object
PSPDFPageInfo *pageInfo = [document pageInfoForPageAtIndex:0];
UIImage *pageImage = [document imageForPageAtIndex:0 size:pageInfo.size clippedToRect:CGRectZero annotations:nil options:nil error:&error];

Debugging

If you’re 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… at 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 Settings

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

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’re 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.