Vai al contenuto

Generare file PDF tramite API Remote

Una necessità che può capitare di avere è generare dei file PDF direttamente da Xojo. Tramite questo controllo è possibile sfruttare delle API remote per ottenere velocemente il PDF su qualsiasi piattaforma, iOS compreso.

Chiaramente il controllo del documento e le possibilità sono limitate rispetto all’uso di un plugin, come ad esempio quelli della MBS, ma per farlo su una piattaforma come iOS questo approccio rimane il più semplice in assoluto.

L’idea di questo post deriva da quello pubblicato da Javier Rodriguez (Xojo Developer Evangelist per la lingua spagnola) sul blog di Xojo.

Nella sua implementazione, Javier ha utilizzato  HTTPSocket del framework classico. In questa versione utilizzo  HTTPSocket del nuovo framework in modo da poterlo utilizzare realmente in tutte le piattaforme compresa iOS.
Il servizio che andiamo ad utilizzare è lo stesso di quello utilizzato da Javier, per cui possiamo utilizzarlo per convertire il nostro documento in vari formati, non solo in PDF; così come l’approccio per realizzare il componente sarà lo stesso.

Creare la classe

Iniziamo creando una nuova classe che andiamo a nominare HTTPNetDocConverter, per distinguere la variazione della classe base che utilizziamo.

Dopo aver assegnato il nome selezioniamo Xojo.Net.HTTPSocket come super della nostra classe.

Per farlo possiamo scriverlo nell’apposito campo o premere il pulsante e scegliere la classe dalla lista di quelle disponibili.

Impostiamo ora la classe definendo alcune caratteristiche:

Prima di tutto una costante privata che indica l’indirizzo del servizio che andremo a utilizzare:

Private Const kServiceUrl as Text = "http://c.docverter.com/convert"

Poi, a differenza dell’approccio di Javier, creiamo una enumerazione per i tipi di conversione. L’uso dell’enumerazione ci permette di essere sicuri che il tipo di conversione richiesta sia una di quelle disponibili.

Public Enum conversionTypes
  PDF
  DOCX
  ePub
  MOBI
  rtf
End Enum

Visto che ci serviranno i nomi di queste conversioni creiamo una funzione shared (quindi funzione della classe) che ci restituisce il tipo della conversione come Text:

Private Shared Function conversionType(type as conversionTypes) as Text
  Select Case type
  Case conversionTypes.DOCX
    Return "docx"
  Case conversionTypes.ePub
    Return "ePub"
  Case conversionTypes.MOBI
    Return "mobi"
  Case conversionTypes.PDF
    Return "pdf"
  Case conversionTypes.rtf
    Return "rtf"
  Case Else
    Return "pdf"
  End Select
End Function

Come nell’approccio di Javier gestiremo tutto internamente e utilizzeremo una funzione delegate per ottenere il risultato. Definiamo quindi due delegate una per ottenere il file convertito, l’altra per ottenere il messaggio d’errore. Si potrebbe utilizzarne una sola con il tipo Auto, ma questo complicherebbe la gestione del risultato (bisognerebbe sempre verificare il tipo del risultato o dovremmo utilizzare una parametro per indicare se la conversione è riuscita o meno e di conseguenza sapere il tipo del parametro informativo; gli approcci sono equivalenti, utilizziamo qui quello più esplicito)

//Delegate con argomento il motivo del fallimento
Public Sub failedCallback(reason as Text)
 
//Delegate con il riferimento al file convertito
Public Sub successfulCallback(resultFile as Xojo.IO.FolderItem)

A questo punto possiamo definire le proprietà private della nostra classe:

//il metodo da richiamare in caso di fallimento
Private Property koCB as failedCallback
 
//Il metodo da chiamare in caso di successo
Private Property okCB as successfulCallback
 
//Il file originale da convertire
Private Property origin as Xojo.IO.FolderItem
 
//Il tipo di conversione richiesta
Private Property type as conversionTypes

Non ci resta che creare il costruttore della classe che imposterà i parametri e lancerà la conversione:

Public Sub Constructor(src as Xojo.IO.FolderItem, convertTo as conversionTypes, success as successfulCallback, fail as failedCallback=Nil)
  // Calling the overridden superclass constructor.
  Super.Constructor
  origin=src
  type=convertTo
  okCB=success
  koCB=fail
 
End Sub

Ho impostato che il parametro del fallimento può essere opzionale (nel caso in cui non mi interessa gestirlo per qualche motivo come metodo ma come eccezione.

Richiedere la conversione

Nel costruttore manca il comando per richiamare automaticamente la conversione. In realtà è buona norma verificare prima se tutto sia ok e poi richiamare la conversione per cui al costruttore aggiungiamo:

If checkForConversion Then sendForConversion

Ovvero prima verifichiamo che sia tutto ok e poi inviamo la richiesta.
La funzione di verifica è molto semplice ma ci permette di vedere come gestire la scelta di utilizzare o meno la callback per il fallimento:

Private Function checkForConversion() as Boolean
//Se il file di origine non è definito o non esiste o non si hanno i permessi di lettura
//allora non posso convertire
  If origin=Nil Or Not origin.Exists Or Not origin.IsReadable Then
    //Se la callback per il fallimento esiste la utilizziamo
    // altrimenti creiamo una eccezione
    If koCB<>Nil Then
      koCB.Invoke("No data to convert")
    Else
      Dim e As New Xojo.Core.InvalidArgumentException
      e.Reason="No data to convert"
      Raise e
    End If
    Return False
  End If
 
  Return True
End Function

Il metodo che richiede la conversione è più complesso in quanto questo servizio richiede di comporre la richiesta in modalità multipart/form-data. Visto che diversi elementi saranno utilizzati nelle varie richieste li dichiariamo come static in modo da inizializzarli una sola volta:

Private Sub sendForConversion()
  //Definiamo il boundary (deve essere una stringa che difficilmente può apparire nel contenuto)
  //random o predefinita in modo opportuno
  Static boundary As Text="ThisIsTheHTTPNextConvertBoundaryLIMit"
 
  //Gli elementi che andremo ad utilizzare spesso per comporre la richiesta
  // il doppio meno
  Static mDash As Xojo.Core.MemoryBlock=Xojo.Core.TextEncoding.UTF8.ConvertTextToData("--")
 
  //Il ritorno a capo CRLF
  Static mCRLF As Xojo.Core.MemoryBlock=Xojo.Core.TextEncoding.UTF8.ConvertTextToData(Text.FromUnicodeCodepoint(13)+Text.FromUnicodeCodepoint(10))
 
  //Il delimitatore --boundaryCRLF
  Static mBound As Xojo.Core.MemoryBlock=Xojo.Core.TextEncoding.UTF8.ConvertTextToData("--"+boundary+Text.FromUnicodeCodepoint(13)+Text.FromUnicodeCodepoint(10))
 
  //Gli elementi per l'intestazione del form
  Static mDataBefore As Xojo.Core.MemoryBlock=Xojo.Core.TextEncoding.UTF8.ConvertTextToData("Content-Disposition: form-data; name=""")
  Static mDataBeforeFile As Xojo.Core.MemoryBlock=xojo.Core.TextEncoding.UTF8.ConvertTextToData("""; filename=""")
  Static mDataPost As Xojo.Core.MemoryBlock=Xojo.Core.TextEncoding.UTF8.ConvertTextToData(""""+Text.FromUnicodeCodepoint(13)+Text.FromUnicodeCodepoint(10))
 
  //Il content type
  Static mDataHTML As Xojo.Core.MemoryBlock=Xojo.Core.TextEncoding.UTF8.ConvertTextToData("Content-Type: text/html"+Text.FromUnicodeCodepoint(13)+Text.FromUnicodeCodepoint(10))
 
  //Creiamo un oggetto MutableMemoryBlock in cui scriveremo i nostri dati
  Dim mData As New Xojo.Core.MutableMemoryBlock(0)
 
  //Parametro per il tipo di file di origine, nel nostro caso convertiamo file HTML
  mData.Append mBound
  mData.Append mDataBefore
  mData.Append Xojo.Core.TextEncoding.UTF8.ConvertTextToData("from")
  mData.Append mDataPost
  mData.Append mCRLF
  mData.Append Xojo.Core.TextEncoding.UTF8.ConvertTextToData("html")
  mData.Append mCRLF
 
  //Parametro per il tipo di conversione richiesta
  mData.Append mBound
  mData.Append mDataBefore
  mData.Append Xojo.Core.TextEncoding.UTF8.ConvertTextToData("to")
  mData.Append mDataPost
  mData.Append mCRLF
  mData.Append Xojo.Core.TextEncoding.UTF8.ConvertTextToData(conversionType(type))
  mData.Append mCRLF
 
  //Nello stesso modo potremmo aggiungere altri parametri opzionali utili alla conversione
  //Nel caso verificare quali sono disponibili nella descrizione dell'API
 
 
  //Aggiugiamo l'intestazione per il contenuto del file
  mData.Append mBound
  mData.Append mDataBefore
  mData.Append Xojo.Core.TextEncoding.UTF8.ConvertTextToData("input_files[]")
  mData.Append mDataBeforeFile
  mData.Append Xojo.Core.TextEncoding.UTF8.ConvertTextToData(origin.Name)
  mData.Append mDataPost
  mData.Append mDataHTML
  mData.Append mCRLF
 
  //Leggiamo il file a lo appendiamo alla nostra richiesta
  Dim bs As Xojo.IO.BinaryStream=Xojo.io.BinaryStream.Open(origin, Xojo.io.BinaryStream.LockModes.Read)
  mData.Append bs.Read(bs.Length)
  bs.Close
  mData.Append mCRLF
 
  //Aggiungiamo la chiusura del corpo della richiesta
  mData.Append mDash
  mData.Append Xojo.Core.TextEncoding.UTF8.ConvertTextToData(boundary)
  mData.Append mDash
  mData.Append mCRLF
 
  //Utilizziamo il metodo originale della classe per impostare la richiesta
  Super.SetRequestContent(mData, "multipart/form-data; boundary=" + boundary)
 
  //Definiamo il file di uscita come file con lo stesso nome di quello originale
  //ma con estensione del tipo richiesto e lo memorizziamo nella cartella dei file temporanei
  Dim out As Xojo.IO.FolderItem=Xojo.IO.SpecialFolder.Temporary.Child(origin.Name.ReplaceAll(".", "_")+"."+conversionType(type))
 
  //Chiediamo alla classe originale di eseguire la conversione
  Super.Send("POST", kServiceUrl, out)
End Sub

Per terminare la nostra classe manca solo l’implementazione di due eventi della classe:

//Se abbiamo un errore nella comunicazione
Sub Error(err as RuntimeException) Handles Error
  If koCB<>Nil Then
    koCB.Invoke(err.Reason)
  Else
    Raise err
  End If
End Sub
 
//Come gestire il risultato ottenuto
//Se tutto è andato a buon fine l'HTTPStatus varrà 200
//E possiamo richiamare la callback per usare il risultato
//Altrimenti gestiamo l'errore
Sub FileReceived(URL as Text, HTTPStatus as Integer, File as xojo.IO.FolderItem) Handles FileReceived
  #Pragma Unused URL
  If HTTPStatus=200 Then
    okCB.Invoke(file)
  Else
    Dim msg As Text="Got a "+HTTPStatus.ToText+" reply"
    If koCB<>Nil Then
      koCB.Invoke(msg)
    Else
      Dim e As New Xojo.Core.ErrorException
      e.Reason=msg
      Raise e
    End If
  End If
End Sub

Ecco un esempio della classe su iOS:

Esempio su iOS
Gestione mancata conversione

Il servizio offre la possibilità di utilizzare nel CSS i @media print, anche se solo una piccola parte, è possibile utilizzare font remoti (nel sito ci sono i parametri da utilizzare nel CSS) e non può gestire le immagini SVG.

Ma è comunque discretamente efficiente e utilizzabile per applicazioni per tutte le piattaforme.

Se si desiderano risultati più complessi esistono altre API web che fanno lo stesso servizio ma non gratuitamente e per utilizzarle basta semplicemente creare una sottoclasse di questa, con un nuovo constructor (per i parametri dell’API) e una nuova versione del metodo sendForConversion, in modo da rendere la richiesta conforme a quanto previsto dall’API.

La classe realizzata risulta quindi flessibile per ulteriori servizi.

Tra le cose che si possono aggiungere a questa classe è renderla utilizzabile come Factory, ovvero non rendere necessario l’inserimento di un istanza in una Window (o View) ma poterla richiamare direttamente.

Per fare questo basta creare una proprietà condivisa di tipo Xojo.Core.Dictionary; un metodo condiviso per lanciare le richieste e un paio di metodi per la gestione come chiamata interna delle callback.

Una soluzione possibile potrebbe essere:

Private Shared Property calls as Xojo.Core.Dictionary
Public Shared Sub convertHTMLtoPDF(f as Xojo.IO.folderItem, fail as failedCallback, success as successfulCallback)
  Dim d As New Xojo.Core.Dictionary
  d.Value("ok")=success
  d.Value("ko")=fail
  Dim id As Text=//Generazione di un ID unico, ad esempio come UUID
  Dim h As HTTPNetDocConverter=New HTTPNetDocConverter(id, f, HTTPNetDocConverter.conversionTypes.PDF)
  d.Value("htp")=h
  If calls=Nil Then calls=New Xojo.Core.Dictionary
 
  calls.Value(id)=d
 
  h.start
End Sub
Private Sub internalFail(reason as Text)
  Dim d As Dictionary=calls.Value(id)
  Dim c As failedCallback=d.Value("ko")
  calls.Remove id
  If c<>Nil Then
      c.Invoke(reason)
  Else
      Dim e As New Xojo.Core.ErrorException
      e.Reason=msg
      Raise e
  End If
End Sub
 
Private Sub internalSuccess(file as Xojo.IO.FolderItem)
  Dim d As Dictionary=calls.Value(id)
  Dim c As successfulCallback=d.Value("ok")
  calls.Remove id
  If c<>Nil Then c.Invoke(file)
End Sub
 
Private Sub start()
  If checkForConversion Then sendForConversion
End Sub
 
Private Sub Constructor(ref as text, src as Xojo.IO.FolderItem, convertTo as conversionTypes)
  // Calling the overridden superclass constructor.
  Super.Constructor
  origin=src
  type=convertTo
  id=ref
  okCB=WeakAddressOf internalSuccess
  koCB=WeakAddressOf internalFail
End Sub

In questo modo basta richiamare il metodo HTTPNetDocConverter.convertHTMLtoPDF(fileDaConvertire, callBackPositiva, callBackNegativa) e la classe si occuperà di creare il socket, gestirne la risposta ed eliminare il socket.

Per soluzioni più sofisticate, che richiedono una gestione del PDF nei minimi dettagli, si può creare la propria API su una webapp Xojo e utilizzare come motore della webapp qualcosa come DynaPDF o prodotti simili per generare il file e una sottoclasse di questa che abbiamo visto per richiedere ed ottenere il PDF.