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.
Progressive viewer rendering
The viewer progressively renders detailed vector pages, such as building plans and maps, and shows partial results while rendering continues. This behavior also applies to zoomed pages; text editing with the Content Editor; night, sepia, and grayscale modes; and pages with a rotation offset, opaque background color, or page tint. Progressive rendering is enabled by default and needs no code changes.
The RenderTask API still calls its completion handler when the requested image finishes rendering.
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 = pageImageSizerequest.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
All image requests start with a RenderRequest, 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 can create a RenderTask. You can use the task object to change a task’s priority, cancel it, or receive the rendered image once it completes.
To execute a render task, you must schedule it in the global RenderQueue. You can think of a task and its queue like Operation and OperationQueue. The concept is similar, but a couple of differences and optimizations apply under the hood.

Now that we’ve covered the general data flow, let’s look closer at the 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];}Rendering requests
RenderRequest is a pair of classes: a mutable and an immutable version. Usually you create a MutableRenderRequest, configure it, and pass it to RenderTask(request:). This makes a copy of the request, so you can no longer modify the request in a task. The copy enables Nutrient to make many optimizations that speed up your task. It also means you can reuse your mutable render request to create a batch of render tasks, as the example above does.
The example also shows the minimum information you need to specify for both the request and the task. The request needs a Document and an imageSize. The imageSize sets the bounding box, in points, that the resulting UIImage fits inside. If the aspect ratio of the specified image size doesn’t match the page’s, the image is smaller on one of the axes. Nutrient always fits the page’s aspect ratio inside the specified imageSize.
The request also needs a pageIndex so that it knows which page to render. If you don’t specify a pageIndex, the request assumes page 0, which might be fine if you only want to render a cover image.
The setup above produces an image of the full page. To render only part of a page, specify a pdfRect in PDF coordinates. The resulting image still has the size of imageSize but contains only the pdfRect you specified. Nutrient draws the content of that PDF rectangle by fitting it inside the requested image size while keeping its aspect ratio. As a result, the image might be smaller than the requested size on one axis and contains exactly the PDF rectangle’s content.
Rendering tasks
Use the render task to alter the priority of requests or cancel them altogether. Best practice is to cancel tasks you no longer need. If you don’t need a task now but are likely to need it again soon, reduce its priority instead.
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 doesn’t necessarily execute before a task with a lower priority. This depends on several factors, such as other tasks requesting the same image and whether a given request produces a cache hit. Nutrient executes as many tasks as possible in a timely manner. The order of execution might also change as we improve our algorithms for choosing the next most important task, so don’t rely on it.
To find out when a task completes, you have two options. A render task can have a delegate and a completionHandler. You can use either one or both. To cancel a task, call cancel on it, and you’ll no longer receive any callbacks from it.
Rendering while in the background
Nutrient pauses queued page rendering when your app enters the background by default. This behavior comes from SDK.Setting.suspendsRenderingWhenBackgrounded, which also pauses document precaching, text parsing, and page view annotation loading while your app is backgrounded. Nutrient resumes this work when your app returns to the foreground.
Most apps should keep this setting enabled to reduce the chance of iOS terminating the app for excessive background processing. If your app intentionally performs queued rendering or text parsing during a background processing task, disable this setting during SDK initialization.
This setting doesn’t affect synchronous rendering through Document.imageForPage(at:size:clippedTo:annotations:options:). Use synchronous rendering only for isolated background renders.
Rendering cache
You don’t need to worry about PDFCache under normal conditions. Scheduling a task in a render queue automatically renders a new image only if the cache has no matching image. The framework also stores images in the cache that are likely to be retrieved again. If you schedule a render task that produces a cache hit, we’ll call your completion handler and the delegate in a prioritized way. This keeps the task from waiting in the render queue unnecessarily.
However, due to its nature, RenderTask is always asynchronous. If you need to render an image synchronously, you can retrieve one from the cache with the same RenderRequest you’d use to create a task. 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 has no image for your request, you need to schedule a render task anyway, so your code should usually work asynchronously. You shouldn’t use PDFCache as your data store; instead, store images you’re actively using in another data structure.
You can’t control whether Nutrient stores a rendered image in the cache. You can control whether a request accesses the cache and whether it can render. By default, RenderRequest automatically determines the best strategy to fulfill your request. Other options ignore the cache and render a new image, or check only the cache and skip rendering when it has no match. A third option checks the cache first and renders only if it has no result.
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.cacheDirectoryNSString *cacheDirectory = PSPDFKitGlobal.sharedInstance.cache.diskCache.cacheDirectory;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 Nutrient SDK security guide for an overview of SDK security options.
Disabling the disk cache per document
The useDiskCache property on Document controls whether Nutrient uses the disk cache 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 = falsedocument.useDiskCache = NO;Disabling the disk cache globally
By default, the cache stores 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 = 0PSPDFKitGlobal.sharedInstance.cache.diskCache.allowedDiskSpace = 0;Disabling the cache may result in performance issues, especially on older devices.
Cache invalidation
The framework usually invalidates the cache itself, so 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.invalidateImages(from:pageIndex:). This removes all images for the given page from both the in-memory cache and the disk cache. Render requests you execute after this call no longer produce a cache hit. They render a new image for the page, and Nutrient adds it to the cache again.
If you only need a new rendering of a specific image size, set the cachePolicy property of RenderRequest to PSPDFRenderRequestCachePolicyReloadIgnoringCacheData when requesting the image. This produces a new rendering that ignores any cache hits. The new image then overrides any existing image in the cache that matches the given request. Keep in mind to treat the order of request execution as non-deterministic, as mentioned above. Tasks you schedule after the task with the request above might execute before it and retrieve the old image from the cache.
Synchronous rendering
We generally discourage synchronous page rendering 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, because Nutrient renders the image 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 render requests, tasks, or the cache, Nutrient has two environment variables that alter the render engine’s behavior. This makes it easier to debug certain types of problems. To set an environment variable, go to your project’s scheme settings in Xcode. Open 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.

If you run your app through Xcode now, the added variable affects the way the render engine handles things. In your scheme settings, tick or untick the box next to the environment variable to activate or deactivate it.
| 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.