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.
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:
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.