Il porting sembrava chiuso il venerdì. Un viewer PDF Delphi, spostato su Lazarus in due giorni: qualche cambio di nome delle unit, alcuni blocchi {$IFDEF FPC}, e il progetto compilava senza errori. Il test del lunedì raccontò un'altra storia: la casella di ricerca non restituiva risultati per parole chiaramente visibili nella pagina, e un nome file con caratteri accentati si rifiutava di aprirsi. L'output del compilatore non aveva suggerito nessuno dei due guasti, perché entrambi erano problemi di codifica delle stringhe, e le incompatibilità di encoding restano invisibili finché non passa dato reale. Portare un viewer costruito su PDFium Component, che distribuisce edizioni VCL e LCL da una sola base sorgente, è perlopiù meccanico; le parti non meccaniche sono esattamente quelle trattate qui.
Stesso Pascal, payload di stringa diverso
La string nativa di Delphi è UTF-16 dal 2009. Lazarus e Free Pascal usano per impostazione predefinita UTF-8 nelle applicazioni LCL. Le API del componente rivolte al testo parlano UTF-16 tramite il tipo WString, che nella build FPC è un alias di WideString: quindi ogni confine in cui il testo passa tra la tua UI LCL e il motore PDF è un punto di conversione.
Le conversioni sono automatiche nelle assegnazioni semplici, ma due abitudini evitano la categoria di bug del lunedì mattina. Passa il testo direttamente senza manipolazioni a livello di byte: codice che taglia un termine di ricerca per offset di byte funziona in Delphi, dove un Char è una unità UTF-16, e corrompe l'UTF-8 multibyte nella LCL. E testa con dati non ASCII dal primo giorno, ad esempio un nome file tedesco o un termine di ricerca con una lineetta, perché dati di test solo ASCII rendono invisibile ogni bug di codifica.
Un piccolo livello condizionale invece di un fork
Dopo la prima dozzina di IFDEF viene la tentazione di dividere il codice per IDE. Resisti: le differenze stanno in un unico blocco di dichiarazioni condiviso, mentre un fork raddoppia ogni correzione futura. Il livello condizionale resta così piccolo:
{$IFDEF FPC}
uses
LCLType, Forms, Graphics, Controls;
type
WString = WideString; // component text APIs are UTF-16
TBytes = array of Byte;
{$ELSE}
uses
Winapi.Windows, Vcl.Forms, Vcl.Graphics, Vcl.Controls;
{$ENDIF}
Tutto sotto questo blocco, gestione dei documenti, navigazione delle pagine e chiamate di rendering, compila in modo identico in entrambi gli IDE, perché TPdf e TPdfView espongono la stessa superficie nelle edizioni VCL e LCL. La disciplina che lo mantiene vero è strutturale: la logica PDF condivisa vive in unit senza dialoghi o pannelli specifici del framework, e ciò che differisce davvero, come dialoghi di stampa o selettori file con convenzioni di piattaforma, resta dietro una piccola interfaccia implementata una volta per framework. Il blocco IFDEF sopra è anche la sede corretta per eventuali divergenze future di piattaforma, così il resto del codice resta libero da condizioni del compilatore sparse.
Costruisci il form nel codice, non in due designer
Lo streaming dei form è il punto in cui i progetti dual-IDE marciscono in silenzio: un .dfm e un .lfm che descrivono "lo stesso" form divergono proprietà dopo proprietà finché le due build si comportano diversamente per motivi che nessuno riesce a confrontare. Per il nucleo del viewer, la costruzione a runtime aggira l'intero problema: una sola sequenza di costruttori, versionata come codice ordinario:
procedure TViewerForm.FormCreate(Sender: TObject);
begin
Pdf := TPdf.Create(Self);
PdfView := TPdfView.Create(Self);
PdfView.Parent := Self;
PdfView.Align := alClient;
PdfView.Pdf := Pdf;
PdfView.FitMode := pfmFitWidth;
if ParamCount > 0 then
begin
Pdf.FileName := ParamStr(1);
Pdf.Active := True; // opens the document; PageCount valid after this
end;
end;
La sequenza conta meno del collegamento: PdfView.Pdf := Pdf lega il controllo visuale al componente documento, dopodiché la navigazione con PageNumber e lo zoom con FitMode si comportano allo stesso modo sotto VCL e LCL. C'è un comportamento cross-framework da conoscere prima che lo trovino gli utenti: assegnare manualmente Zoom riporta FitMode a pfmNone in entrambi i framework. Se la toolbar offre "adatta alla larghezza" come preferenza persistente, ripristina la modalità di adattamento dopo ogni cambio di zoom programmatico, altrimenti la preferenza smette silenziosamente di essere persistente.
Il binario di cui l'IDE non ti ha mai avvisato
Il componente avvolge il motore PDFium, distribuito come binario di piattaforma, e il caricamento binario è il punto da cui nascono i report "funziona nell'IDE, fallisce dal collegamento installato". Tre regole coprono quasi tutti i casi. La bitness deve combaciare esattamente: un eseguibile a 32 bit non può caricare una libreria pdfium a 64 bit, e il messaggio di errore prodotto dal sistema operativo, "module not found" in alcune versioni di Windows, è attivamente fuorviante perché il file è proprio lì. Risolvi il percorso della libreria rispetto all'eseguibile, mai rispetto alla working directory: i lanci dall'IDE e quelli dalla shell differiscono proprio lì. E mostra i fallimenti di caricamento prima dell'apertura del primo documento, con un messaggio che nomina percorso atteso e architettura; un ticket che dice "binario PDFium 64-bit mancante in <path>" si chiude in minuti, uno che dice "il viewer va in crash all'avvio" no.
Versiona anche il binario del motore insieme all'eseguibile. PDFium si muove rapidamente, e un installer che aggiorna l'applicazione lasciando su disco una libreria più vecchia produce crash che nessuna macchina in ufficio riesce a riprodurre, perché tutte hanno la coppia corretta. Tratta la libreria come parte dell'artefatto di build: stesso installer, stesso timbro di versione, stesso rollback.
Registrare i componenti nell'IDE Lazarus
La costruzione a runtime non richiede affatto registrazione design-time, che è la configurazione corretta più semplice per un'applicazione viewer. Se vuoi comunque i componenti nella palette di Lazarus, installa il package e lascia che la sua unit di registrazione dedicata (PDFiumLazReg) faccia il lavoro: quella unit è marcata design-time nel package proprio perché fa riferimento a interfacce di property editor dell'IDE che non devono mai essere linkate nell'eseguibile distribuito.
Il sintomo di un errore qui è un'applicazione che dipende misteriosamente da package dell'IDE, cosa che emerge come fallimento di deployment su macchine che non hanno mai visto Lazarus.
Voce e screen reader fuori da Windows
Se l'originale Delphi offriva text-to-speech, il porting eredita una decisione di piattaforma. SAPI, il backend TTS usuale su Windows, esiste solo lì. Le build Lazarus destinate a Windows mantengono pienamente SAPI e il comportamento compatibile con NVDA, quindi un porting Windows-to-Windows non perde nulla, e gli utenti NVDA interagiscono con il viewer nello stesso modo in entrambi gli IDE.
I target Linux e macOS richiedono un backend vocale diverso collegato alle stesse API di lettura, ed è un buon motivo per tenere la voce dietro un'interfaccia fin dall'inizio. La meccanica di reading-order e tracciamento delle parole è neutrale rispetto alla piattaforma; cambia solo il livello di uscita audio. L'articolo sul reader accessibile copre quella meccanica in dettaglio.
Cosa testare prima di dichiarare concluso il porting
Un passaggio di parità che ha intercettato regressioni reali, più o meno nell'ordine in cui i guasti compaiono: apri un documento il cui percorso contiene caratteri non ASCII; cerca un termine con caratteri non ASCII e conferma l'evidenziazione dei risultati; prova scroll con rotellina, selezione con trascinamento e navigazione da tastiera delle pagine su ogni widget set target, perché focus e rotellina sono tra le aree LCL più dipendenti dal widget set; controlla il rendering al 100%, 150% e 200% di scaling display; ed esegui la build installata, non quella dall'IDE, su una macchina senza IDE, che è l'unico test che esercita onestamente la risoluzione del binario.
Le caratteristiche di throughput del rendering si trasferiscono tra le edizioni, quindi la strategia di caching dell'articolo su render cache e prestazioni dello zoom si applica senza modifiche.
FAQ
L'edizione LCL è completa quanto l'edizione VCL?
La superficie principale, TPdf, TPdfView, rendering, form, estrazione del testo e API di accessibilità, è la stessa su entrambe. Le differenze reali sono vincolate alla piattaforma: l'output vocale SAPI è solo Windows, e dialoghi di stampa e file seguono le convenzioni del proprio framework.
Perché la mia build Lazarus va in crash all'avvio mentre la build Delphi funziona?
Controlla prima la risoluzione del binario: mismatch di architettura tra eseguibile e libreria pdfium, oppure un percorso di caricamento che presumeva la working directory dell'IDE. Entrambi producono fallimenti immediati all'avvio che sembrano bug del componente e sono invece bug di deployment.
Posso mantenere un solo form condiviso tra gli IDE?
I file di descrizione dei form non si trasferiscono: .dfm e .lfm sono formati diversi con set di proprietà diversi. Costruire la UI del viewer a runtime, come mostrato sopra, sostituisce due file di designer con un solo percorso di codice ed elimina del tutto il problema della deriva.
Le edizioni VCL e LCL descritte qui sono distribuite insieme come PDFium Component, con codice sorgente e API pubbliche identiche per Delphi, C++Builder e Lazarus/FPC.