Articolo tecnico

Creare un visualizzatore PDF in Delphi con PDFium Component

Un visualizzatore PDF in Delphi si riduce a due componenti e al collegamento tra di essi. TPdf possiede il documento: apre il file, lo decifra e risponde alle domande sul conteggio delle pagine e sui metadati. TPdfView è il controllo visivo che disegna le pagine sullo schermo e gestisce lo scorrimento, lo zoom e la pagina che l'utente sta attualmente visualizzando. PDFium Component racchiude lo stesso motore di rendering integrato in Chrome, in modo che i glifi, l'anti-aliasing e i colori visualizzati sull'area di disegno corrispondano a quelli che gli utenti vedono già nel proprio browser. Il lavoro non consiste nel rendering, ma nel collegare l'oggetto documento alla vista, nel caricare il file senza causare crash in caso di documenti danneggiati o protetti da password, e nel fornire all'utente i pochi controlli necessari per rendere l'esperienza d'uso completa: cambiare pagina, regolare lo zoom e adattare la pagina alla finestra

Questa guida illustra tale assemblaggio nell'ordine in cui viene effettivamente costruito. Tutto ciò che viene descritto qui renderizza una sola pagina alla volta, che è ciò che la maggior parte dei flussi di lavoro documentali richiede. Se hai bisogno di pagine disposte in una colonna a scorrimento continuo, si tratta di una decisione di layout diversa trattata in un articolo separato e non è il percorso seguito qui

Collegare TPdf a TPdfView

Rilascia un TPdf e un TPdfView sul form, quindi indica alla vista quale documento mostrare. Quella singola assegnazione rappresenta l'intero collegamento tra il documento non visivo e il controllo che lo disegna

procedure TFormMain.FormCreate(Sender: TObject);
begin
  // Pdf and PdfView were dropped at design time.
  PdfView.Pdf := Pdf;                 // the view paints whatever this document holds
  PdfView.FitMode := pfmFitWidth;     // start the user at a sensible zoom
end;

Prima che tutto questo possa essere eseguito, la libreria nativa di PDFium deve essere presente sul computer. PDFium Component effettua chiamate a pdfium32.dll o pdfium64.dll a seconda della piattaforma di destinazione, e il documento rifiuterà semplicemente di aprirsi se la DLL non viene trovata. Distribuisci la DLL corrispondente accanto al tuo eseguibile o inseriscila in una cartella in cui il caricatore di sistema possa trovarla. Le build abilitate per V8 esistono solo per i PDF che contengono JavaScript che desideri eseguire (cosa che un normale visualizzatore non richiede), quindi utilizza la DLL standard a meno che tu non abbia un motivo specifico per non farlo

Caricare un documento senza fidarsi dell'input

L'istinto iniziale potrebbe essere quello di racchiudere il caricamento in un blocco try/except e trattare l'eccezione generata come un fallimento. Questo istinto è errato in questo caso, e sbagliare questo passaggio produce un visualizzatore che sembra funzionare bene finché qualcuno non gli fornisce un file corrotto. L'impostazione di Active := True non solleva eccezioni in caso di errore di caricamento. PDFium Component intercetta l'errore interno e lascia Active impostato su False, quindi l'unico modo sicuro per sapere se il documento è stato aperto è rileggere la proprietà dopo averla impostata

procedure TFormMain.OpenDocument(const FileName: string);
begin
  Pdf.FileName := FileName;
  Pdf.Active := True;                 // never raises; failure leaves Active = False
  if not Pdf.Active then
  begin
    ShowMessage('Could not open ' + FileName);
    Exit;
  end;
  PdfView.PageNumber := 1;            // the view tracks its own current page
  UpdatePageLabel;
end;

Due cose meritano attenzione. La prima è che PageNumber esiste su entrambi gli oggetti e i due valori sono indipendenti. Pdf.PageNumber rappresenta la nozione di pagina corrente per il documento; PdfView.PageNumber è la pagina effettivamente visualizzata dal controllo, ed è quella da impostare per spostare l'utente all'interno del file. L'impostazione di una non sposta l'altra, pertanto un visualizzatore deve sempre gestire la proprietà della vista. La seconda è l'indicizzazione a base 1: le pagine vanno da 1 a Pdf.PageCount, non da 0, il che potrebbe trarre in inganno chiunque sia abituato agli array a base zero

Gestione di un file crittografato

I documenti crittografati seguono lo stesso percorso di caricamento. Se la password di apertura viene impostata prima dell'attivazione, il documento viene decifrato durante l'apertura; se la password è errata o mancante, Active rimane impostato su False esattamente come avviene per un file corrotto. Pertanto, la procedura di ripristino consiste nel richiedere una password all'utente e tentare nuovamente l'attivazione

procedure TFormMain.OpenWithPassword(const FileName: string);
var
  Password: string;
begin
  Pdf.FileName := FileName;
  Pdf.Active := True;
  if not Pdf.Active then
  begin
    if InputQuery('Password required', 'Password:', Password) then
    begin
      Pdf.Password := Password;       // must be set before Active := True
      Pdf.Active := True;
    end;
    if not Pdf.Active then
    begin
      ShowMessage('Unable to open the document.');
      Exit;
    end;
  end;
  PdfView.PageNumber := 1;
end;

Poiché il fallimento è silenzioso sia in caso di password errata che di file danneggiato, non è possibile distinguerli basandosi solo sulla proprietà Active. In pratica, questo è accettabile per un visualizzatore: l'utente inserisce la password corretta oppure apprende che il file non può essere aperto, e il messaggio di errore risulta lo stesso in entrambi i casi

Navigazione tra le pagine del documento

Una volta aperto il documento, la navigazione consiste in operazioni aritmetiche su PdfView.PageNumber limitate da Pdf.PageCount. L'unico vero lavoro consiste nel limitare i valori entro l'intervallo consentito, in modo che i pulsanti non portino mai la pagina fuori dai limiti e che i pulsanti per la prima e l'ultima pagina rimangano disabilitati agli estremi del file

procedure TFormMain.GoToPage(NewPage: Integer);
begin
  if not Pdf.Active then
    Exit;
  if NewPage < 1 then
    NewPage := 1
  else if NewPage > Pdf.PageCount then
    NewPage := Pdf.PageCount;
  PdfView.PageNumber := NewPage;
  UpdatePageLabel;
end;

// the four navigation buttons reduce to one call each
procedure TFormMain.FirstClick(Sender: TObject);  begin GoToPage(1); end;
procedure TFormMain.PrevClick(Sender: TObject);   begin GoToPage(PdfView.PageNumber - 1); end;
procedure TFormMain.NextClick(Sender: TObject);   begin GoToPage(PdfView.PageNumber + 1); end;
procedure TFormMain.LastClick(Sender: TObject);   begin GoToPage(Pdf.PageCount); end;

Una casella di testo "vai alla pagina N" effettua la stessa chiamata a GoToPage passando un intero analizzato, e la limitazione del valore gestisce il caso in cui l'utente digiti 9999 in un file di dieci pagine. Mantieni UpdatePageLabel come l'unico punto che scrive "Pagina 3 di 12", in modo che l'indicatore non si sintonizzi mai in modo errato rispetto a ciò che mostra la vista

Zoom: percentuali esplicite e modalità di adattamento

Lo zoom su TPdfView è disponibile in due modalità che interagiscono tra loro, e comprenderne l'interazione fa la differenza tra un controllo dello zoom che si comporta correttamente e uno che crea problemi all'utente. La via diretta è la proprietà Zoom, una percentuale in cui 100 indica le dimensioni reali. L'altra via è FitMode, che indica alla vista di calcolare lo zoom al posto tuo e di ricalcolarlo man mano che la finestra viene ridimensionata

// fixed magnifications
PdfView.Zoom := 100;     // actual size
PdfView.Zoom := 50;      // half
PdfView.Zoom := 200;     // double

// let the view size the page to the window, and keep it sized on resize
PdfView.FitMode := pfmFitWidth;   // page width fills the control
PdfView.FitMode := pfmFitPage;    // whole page visible
PdfView.FitMode := pfmActualSize; // 1:1 with the document's points

Ecco la parte che spesso confonde gli sviluppatori. L'assegnazione diretta di Zoom reimposta FitMode su pfmNone. Si tratta del comportamento corretto, non di un bug: nel momento in cui l'utente seleziona esattamente il 150%, la vista non può più rispettare l'adattamento alla larghezza, poiché le due richieste sono in conflitto. La conseguenza per la tua interfaccia utente è che un pulsante di zoom avanti e un pulsante per l'adattamento alla pagina rappresentano stati mutuamente esclusivi, e la barra degli strumenti dovrebbe rendere visibile la modalità attiva. Quando l'utente fa clic su adatta alla pagina, imposta FitMode; quando fa clic su uno zoom numerico, imposta Zoom e lascia che questo reimposti la modalità di adattamento da solo

Se preferisci calcolare autonomamente il valore di adattamento, forse per inizializzare uno slider di zoom con la percentuale di adattamento corrente, le funzioni di supporto per pagina forniscono i numeri senza modificare la modalità corrente. PageWidthZoom[N], PageZoom[N] e ActualSizeZoom[N] restituiscono la percentuale che adatterebbe la pagina N alla larghezza, alla pagina intera o la renderizzerebbe a dimensioni reali

// seed a zoom readout from the fit-to-width value of the current page
var
  FitPercent: Double;
begin
  FitPercent := PdfView.PageWidthZoom[PdfView.PageNumber];
  ZoomEdit.Text := Format('%.0f%%', [FitPercent]);
end;

Cosa serve davvero a un visualizzatore completo

Il titolo originale sopravvaluta il lavoro necessario. Il visualizzatore sopra descritto richiede poche decine di righe di codice e svolge già tutto ciò di cui un flusso di lavoro documentale ha bisogno: aprire un file, gestire i file corrotti, mostrare una pagina, spostarsi tra le pagine e regolare l'ingrandimento manualmente o tramite adattamento. PDFium gestisce le parti difficili in modo invisibile. I font incorporati vengono risolti, le annotazioni e i campi dei moduli vengono disegnati dove previsto dal documento e la pagina visualizzata corrisponde esattamente a quella che vedrebbe un utente di Chrome, poiché il motore di rendering è lo stesso

Partendo da questa base, le aggiunte sono incrementali piuttosto che strutturali. La selezione del testo e la ricerca leggono dallo stesso livello di testo che PDFium crea già; i metadati come Pdf.Title e Pdf.Author sono leggibili tramite una semplice proprietà; la rotazione e la scala di grigi sono opzioni di rendering che puoi passare quando disegni una pagina su una bitmap. Nessuna di queste funzionalità modifica la struttura portante descritta qui, composta dall'oggetto documento, dalla vista e dal flusso di caricamento e navigazione che li collega. Una volta realizzata correttamente questa struttura, il resto è solo decorazione

I componenti TPdf e TPdfView utilizzati in questa guida fanno parte di PDFium Component per Delphi e C++Builder, che include la documentazione completa del visualizzatore sulla pagina del prodotto