Tutorial: Cómo crear previos para las páginas de PDF en iOS

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.

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *