Skip to content

How to: Create Thumbnails from PDFs on iOS

Someone asked in the Xojo Forum if it was possible to create thumbnails (Xojo Picture objects) from a page of a PDF in order to display it in an iOS app. Sure it is! Continue reading to learn how in this step-by-step tutorial.

What You Need

In order to do this we need the following pieces of the puzzle:

  • A FolderItem pointing to the PDF file. For our tutorial we will use a FolderItem pointing to a file that has been copied to the Resources folder of the iOS app using a “Copy To” build step.
  • The number of pages in the PDF document.
  • The rect or bounds of the page in the PDF document.
  • Create a Picture from a given page in the PDF document.

Except for the first piece, we are going to make use of the powerful Declare feature. This feature allows us to create native objects from the iOS frameworks and call methods / functions on these, or any of the available functions.

Getting the Number of Pages

First, create a new iOS project in Xojo. Next, add a new Module to the project and name it “External” using its Inspector Panel (you can choose any name).

Next, add a new Method to that module using the following signature in the Inspector Panel:

  • Method Name: NumberOfPages
  • Parameters: PDFDocFile As FolderItem
  • Return Type: Integer
  • Scope: Global

Type now the following code in the Code Editor for the method:

// Declares against the Foundation framework.
// Once "Declared" we can use them from our Xojo code

Declare Function NSClassFromString Lib "Foundation" (clsName As CFStringRef) As Ptr
Declare Function FileURLWithPath Lib "Foundation" Selector "fileURLWithPath:" (obj As ptr, path As CFStringRef) As Ptr
Declare Function CGPDFDocumentCreateWithURL Lib "CoreGraphics" (url As Ptr) As Ptr
Declare Function CGPDFDocumentGetNumberOfPages Lib "CoreGraphics" (PDF As Ptr) As Integer

// We get the native path from the received FolderItem
Var path As String = PDFDocFile.NativePath

// Here we are creating an NSURL object
Var URLClass As ptr = NSClassFromString("NSURL")

If URLClass = Nil Then Exit

// And now we get the reference to the file using the path in combination
// With the created NSURL instance
Var pathPointer As Ptr = fileURLWithPath(URLClass, path)

// We are getting here a reference to the PDF document using the
// previous reference for that.
Var docReference As Ptr = CGPDFDocumentCreateWithURL(pathPointer)

If docReference = Nil Then Exit

// Lastly, we only need to call this CoreGraphics function
// to get the number of pages from the reference to the
// PDF document we got in the previous step
Return CGPDFDocumentGetNumberOfPages(docReference)

Creating a Picture from a PDF

Add the method in charge of returning a Picture created from the page of the PDF document received as parameters. As we did previously, pass along a FolderItem pointing to the PDF document and the page number as an integer (all PDF document pages start at index 1).

Before creating the method we need to add some Structs before to our module. These are needed both for passing values to some CoreGraphics calls and also to receive this kind of types as the value returned by some of these.

With the “External” module selected in the Navigator, add a new Structure and name it as NSOrigin with following values in the Structure Editor:

Add a second Structure with the name NSSize and the following values:

And the third Structure named NSRect with the following values:

Add the second method to the “External” module using the following values in the Inspector Panel:

  • Method Name: GetPDFThumbnailForPage
  • Parameters: PDF As FolderItem, page As Integer
  • Return Type: Picture
  • Scope: Global

Next, type the following code in the Code Editor for the method:

// Declares for Foundation Calls
Declare Function NSClassFromString Lib "Foundation" (clsName As CFStringRef) As Ptr
Declare Function FileURLWithPath Lib "Foundation" Selector "fileURLWithPath:" (obj As Ptr, path As CFStringRef) As Ptr
Declare Function DataLength Lib "Foundation" Selector "length" (obj As Ptr) As Integer
Declare Sub GetDataBytes Lib "Foundation" Selector "getBytes:length:" (obj As Ptr, buff As Ptr, len As Integer)

// Declares for CoreGraphics calls
Declare Sub CGContextDrawPDFPage Lib "CoreGraphics" (ctx As Ptr, page As Ptr)
Declare Sub CGContextFillRect Lib "CoreGraphics" (ctx As Ptr, rect As NSRect)
Declare Sub CGContextRestoreGState Lib "CoreGraphics" (obj As Ptr)
Declare Sub CGContextSaveGState Lib "CoreGraphics" (ctx As Ptr)
Declare Sub CGContextScaleCTM Lib "CoreGraphics" (ctx As Ptr, x As CGFloat, y As CGFloat)
Declare Sub CGContextSetGrayFillColor Lib "CoreGraphics" (ctx As Ptr, x As CGFloat, y As CGFloat)
Declare Sub CGContextTranslateCTM Lib "CoreGraphics" (ctx As Ptr, x As CGFloat, y As CGFloat)
Declare Function CGPDFDocumentCreateWithURL Lib "CoreGraphics" (url As Ptr) As Ptr
Declare Function CGPDFDocumentGetPage Lib "CoreGraphics" (doc As Ptr, page As Integer) As Ptr
Declare Sub CGPDFDocumentRelease Lib "CoreGraphics" (PDF As Ptr)
Declare Function CGPDFPageGetBoxRect Lib "CoreGraphics" (page As Ptr, box As UInt32) As NSRect

// Declares for UIKit calls
Declare Sub UIGraphicsBeginImageContext Lib "UIKit" (size As NSSize)
Declare Sub UIGraphicsEndImageContext Lib "UIKit" ()
Declare Function UIGraphicsGetCurrentContext Lib "UIKit" () As Ptr
Declare Function UIGraphicsGetImageFromCurrentImageContext Lib "UIKit" () As Ptr
Declare Function UIImagePNGRepresentation Lib "UIKit" (img As Ptr) As Ptr

If PDF = Nil Then Exit

Var path As String = PDF.NativePath

// Getting a reference to the NSURL class
Var URLClass As ptr = NSClassFromString("NSURL")

If URLClass = Nil Then Exit

// Getting a reference to a file URL from the given path
Var pathPointer As Ptr = fileURLWithPath(URLClass, path)

// Getting a reference to the PDF document, from the URL file pointer
Var docReference As Ptr = CGPDFDocumentCreateWithURL(pathPointer)

If docReference = Nil Then Exit

// Getting a reference to the object pointing to the page in the PDF document
Var pageRef As Ptr = CGPDFDocumentGetPage(docReference, page)

// Getting the bounds for the page
Var pageBounds As NSRect = CGPDFPageGetBoxRect(pageRef, 0)

Var maxHV As Integer = Max(pageBounds.RectSize.Width, pageBounds.RectSize.Height)

If pageRef = Nil Then Exit

Var pageRect As NSRect
pageRect.Origin.X = 0
pageRect.Origin.Y = 0
pageRect.RectSize.Width = maxHV
pageRect.RectSize.Height = maxHV

// Starting an Image context
UIGraphicsBeginImageContext(pageRect.RectSize)

// And getting the reference to the current context
Var imgCtx As Ptr = UIGraphicsGetCurrentContext

// We save the graphics state
CGContextSaveGState(imgCtx)

// Matrix translation and Scale
CGContextTranslateCTM(imgCtx, 0.0, pageRect.RectSize.Height)
CGContextScaleCTM(imgCtx, 1.0, -0.95)
CGContextSetGrayFillColor(imgCtx, 1.0, 1.0)
CGContextFillRect(imgCtx, pageRect)

// Drawing the PDF Page into the graphic context
CGContextDrawPDFPage(imgCtx, pageref)

// Getting an UIImage from the graphics context
Var img As Ptr = UIGraphicsGetImageFromCurrentImageContext

// Getting an NSDATA object with the PNG representation from the image
Var pngDATA As Ptr = UIImagePNGRepresentation(img)

// We need to get the length of the raw data…
Var dlen As Integer = DataLength(pngDATA)

// …in order to create a memoryblock with the right size
Var mb As New MemoryBlock(dlen)
Var mbPtr As Ptr = mb

// And now we can dump the PNG data from the NSDATA objecto to the memoryblock
GetDataBytes(pngDATA, mbPtr, dlen)

// In order to create a Xojo Picture from it
Var p As Picture = Picture.FromData(mb)

// Clean-up
CGContextRestoreGState(imgCtx)
UIGraphicsEndImageContext
CGPDFDocumentRelease(docReference)

Return p

Designing the UI for the iOS App

Now we have everything we need in order to get the page thumbnails as Pictures. So let’s create a simple user interface to test it.

Select Screen1 in the Navigator so the Layout Editor is displayed in the IDE. Next, drag an ImageViewer from the Library to the Layout Editor. It should look like this:

Drag a Table object from the Library and put it below the ImageViewer in the Layout Editor. It should like like this:

The UI of our iOS app is completed!

Reference the PDF File

You might want to use another technique to get the FolderItem for the PDF document file; but in order to keep this tutorial as short as possible we will a reference just the one PDF file previously copied to the Resources folder of the App.

In order to do that, select the iOS icon in the Navigator and choose the Add To "Build Settings" > Build Step > Copy Files option. This will add a new CopyFiles1 object to the Navigator, displaying the associated Editor.

Simply drag and drop the PDF file you want in the main area of the Editor (or click in the icon with the plus symbol from the toolbar), and make sure to select the following values in the associated Inspector Panel:

  • Applies To: Both
  • Architecture: Any
  • Destination: Resources Folder

Select the Screen1 item in the Navigator again and add a new Property to it, using the following values in the associated Inspector Panel:

  • Name: PDFDocFile
  • Type: FolderItem
  • Scope: Public

Let’s Roll the Ball!

Our example iOS app is almost done. We only need to write the logic that will make use of the methods we wrote in the “External” Module.

With the Screen1 item selected in the Navigator, select the option Add to "Screen1" > Event Handler… from the contextual menu. Next, select the Opening event and confirm the selection so it is added to Screen1.

The last action will automatically select the Opening event in the Navigator, bringing the associated Code Editor to the main area of the IDE. Add the following lines of code:

Var PDFFileCopiedToResources As String = "Introduction to Programming with Xojo.pdf"

PDFDocFile = SpecialFolder.Resource(PDFFileCopiedToResources)

Var numberOfPages As Integer = NumberOfPages(PDFDocFile)

For x As Integer = 1 To numberOfPages

  Table1.AddRow("Page " + x.ToString)

Next x

ImageViewer1.Image = GetPDFThumbnailForPage(PDFDocFile, 1)

Observe the variable named PDFFileCopiedToResources. In this case it is assigned the name of the file copied to the Resources folder using the Build Step. Change that to the name of the file you copied and make sure to include the “.PDF” extension as part of the file name.

This code calls the NumberOfPages method in order to get the number of pages. Then we use a For…Next loop to add as many “Page x” rows to the table as there are pages in the PDF document.

Also, as you can see, the last line calls the second method so the ImageViewer1 control displays the first page of the document every time the app is run.

Lastly, select the Table1 item in the Navigator and choose the Add to "Table1" > Event Handler…  option from the contextual menu. Select the entry SelectionChanged in the resulting window and confirm the selection so it is added to the control.

The last action will select the just added Event Handler in the Navigator, bringing the associated Editor to the main area of the Xojo IDE. This is the event that will be executed every time the user changes the row selected in the table. Type the following line of code:

ImageViewer1.Image = GetPDFThumbnailForPage(PDFDocFile, row + 1)

Running the App

The app is complete now, so we can click on the Run button to launch the Xcode Simulator and run our app on it. You should be able to see something similar to the image displayed below. Tap (or click) on any row you want so the ImageViewer displays the thumbnail for the selected page and that’s all!

You can download the complete Xojo iOS project from this link. Ask me questions on Twitter @XojoES or on the Xojo Forum.