Hace algún tiempo alguien preguntó en el Foro de Xojo si sería posible crear imágenes de previo (objectos Picture en Xojo) para cualquier página de cualquier archivo PDF, de modo que pudiesen mostrarse en una app iOS. ¡Claro que sí! Sigue leyendo este tutorial y te mostraré cómo puedes hacerlo.
¿Qué necesitamos?
Para conseguir nuestro objetivo necesitaremos lidiar con las siguientes piezas del puzzle:
- Un FolderItem que apunte al archivo PDF. En nuestro tutorial utilizaremos un FolderItem que apunta a un archivo previamente copiado a la carpeta Resources de la app iOS, utilizando para ello un paso de compilación “Copy To”.
- Obtener el número de páginas en el documento PDF.
- Obtener el rectángulo o dimensiones de la página en el documento PDF.
- Crear un objeto Picture para una página dada del documento PDF.
Exceptuando el primer punto, haremos uso de la potente característica Declare disponible en el lenguaje de programación Xojo. Dicha característica nos permite crear objetivos nativos de los frameworks iOS, así como llamar a los métodos / funciones de estos o cualquier otra función disponible.
Obtener el número de páginas en el Documento
¡Comencemos! Crea en primer lugar un nuevo proyecto iOS en Xojo en el caso de que no lo hubieses hecho ya. A continuación, añade un nuevo Módulo al navegador del IDE y nómbralo External
utilizando para ello el Panel Inspector asociado (puedes usar cualquier otro nombre de tu elección).
Añade ahora un nuevo Método a dicho módulo utilizando la siguiente signatura en el Panel Inspector asociado:
- Method Name: NumberOfPages
- Parameters: PDFDocFile As FolderItem
- Return Type: Integer
- Scope: Global
Y escribe luego el siguiente código en el Editor de Código para el método:
// 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 // Declares against the CoreGraphics framework 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)
Crear un Picture a partir de una página PDF
Vamos a añadir ahora el método encargado de devolver un Picture
creado a partir de la página y del documento PDF recibidos como parámetros. Al igual que hicimos con el anterior, pasaremos al método un FolderItem
que apunta al documento PDF, así como el número de página como un valor entero (todos los documentos PDF tienen como primera página la página 1).
Pero antes de crear el método necesitamos añadir antes algunas Estructuras al módulo. Necesitaremos estas tanto para pasar valores en algunas llamadas al framework CoreGraphics como a la hora de recibir este tipo de dato como valor devuelto en algunas llamadas a funciones.
Con el módulo External
seleccionado en el Navegador, añade una nueva Estructura
y nómbrala NSOrigin
usando los siguientes valores en el Editor de Estructuras:
Añade una segunda Estructura con el nombre NSSize
y con los siguientes valores:
Y una tercera Estructura nombrada NSRect
con los siguientes valores:
Añade ahora el segundo método al módulo External
utilizando los siguientes valores en el Panel Inspector:
- Method Name: GetPDFThumbnailForPage
- Parameters: PDF As FolderItem, Page As Integer
- Return Type: Picture
- Scope: Global
A continuación, escribe el siguiente código en el Editor de Código asociado con el método:
// 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
Diseñar la interfaz de usuario para la app iOS
Ya tenemos todo lo necesario para obtener las imágenes de previo correspondientes a las páginas de un documento PDF. Creemos ahora una interfaz de usuario simple para probar la funcionalidad.
Selecciona el ítem Screen1
en el Navegador de modo que se muestre el Editor de Diseño asociado en el área principal del IDE. Arrastra a continuación el control ImageViewer
desde la Librería al Editor de Diseño. Debería de verse así:
Arrastra ahora el control Table
desde la Librería y sitúalo justo debajo del ImageViewer
en el Editor de Diseño. Debería de verse así:
¡Ya hemos completado la interfaz de usuario de nuestra app iOS!
Referencia al archivo PDF
Probablemente querrás utilizar en tus apps iOS cualquier otra técnica a la hora de obtener un FolderItem
de un documento PDF; pero para mantener nuestro tutorial tan corto como sea posible, en nuestro caso utilizaremos una referencia a un archivo PDF previamente copiado a la carpeta Resources de la App.
Para ello, selecciona el icono iOS en el Navegador y elige la opción Add To "Build Settings" > Build Step > Copy Files
. Esto añadirá un nuevo objeto CopyFiles1
al Navegador, mostrando el Editor asociado.
Arrastra y suelta cualquier archivo PDF que quieras sobre el área principal del Editor (o haz clic en el icono con el símbolo más, situado en la barra de herramientas del Editor). Asegúrate de seleccionar los siguientes valores en el Panel Inspector asociado:
- Applies To: Both
- Architecture: Any
- Destination: Resources Folder
Selecciona ahora de nuevo el elemento Screen1
en el Navegador y añade una nueva Propiedad utilizando los siguientes valores en el Panel Inspector asociado:
- Name: PDFDocFile
- Type: FolderItem
- Scope: Public
¡Que empiece la función!
Nuestra app de ejemplo iOS está prácticamente terminada. Sólo necesitamos escribir la lógica que utilice los métodos añadidos en nuestro Módulo External
.
Con el item Screen1
seleccionado en el Navegador, selecciona la opción Add to "Screen1" > Event Handler…
en el menú contextual. A continuación, selecciona el evento Opening
y confirma la selección para que se añada a Screen1
.
La última acción habrá seleccionado automáticamente el evento Opening
en el Navegador, mostrando el Editor de Código asociado en el área principal del IDE. Escribe las siguientes líneas de código:
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)
Observa la variable PDFFileCopiedToResources
. En este caso tiene asignado el nombre del archivo que se ha copiado a la carpeta Resources utilizando el Build Step. Cambia dicho valor por el nombre del archivo que hayas copiado en tu caso, y asegúrate de incluir la extensión “.PDF” como parte del nombre de archivo.
Este código se limita a invocar el método NumberOfPages
para obtener la cantidad de páginas del documento. Luego utilizamos dicho valor en un bucle For…Next
para añadir tantas filas “Page x” a la tabla como páginas hay en el documento PDF.
Adicionalmente, como puedes ver, la última línea llama al segundo de nuestros métodos para que el control ImageViewer1
muestre por nosotros la primera página del documento cada vez que ejecutemos la app.
Por último, selecciona el ítem Table1
en el Navegador y selecciona la opción Add to "Table1" > Event Handler…
en el menú contextual. Selecciona la entrada SelectionChanged
en la ventana resultante y confirma la selección para que se añada al control.
La última acción seleccionará en el Navegador el Manejador de Evento recién añadido y mostrará el Editor asociado en el área principal del IDE de Xojo. Este es el evento que se disparará cada vez que el usuario cambie la fila seleccionada en la tabla. Escribe la siguiente línea de código:
ImageViewer1.Image = GetPDFThumbnailForPage(PDFDocFile,row+1)
Ejecutando la App
La aplicación ya está completa, de modo que podemos pulsar sobre el botón Run para que se ejecute en el Simulador iOS de Xcode. Como resultado deberías de ver algo similar a la imagen mostrada a continuación. Cambia la fila seleccionada de la tabla y verás como el control ImageViewer muestra la imagen de previo correspondiente a la página seleccionada… ¡y eso es todo!
Por supuesto, puedes descargar el proyecto de ejemplo Xojo desde este enlace.