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.