Articolo tecnico

Confronto PDF affiancato in Delphi con PDFium Component

Due documenti aperti contemporaneamente, stesso numero di pagina, ciascuno nel proprio pannello scorrevole: questo è il nucleo di un visualizzatore di confronto. PDFium Component offre questa funzionalità attraverso un modello a oggetti lineare in cui TPdf gestisce il file e TPdfView gestisce la visualizzazione. Un documento, un TPdf, un TPdfView. Se si desiderano tre pannelli, si avranno tre coppie. Le parti complesse non sono le chiamate API, bensì i calcoli aritmetici per il layout durante il ridimensionamento della finestra e la logica di sincronizzazione delle pagine quando si decide quale vista debba seguire l'altra

Layout del Form

Il form VCL contiene tre contenitori TScrollBox affiancati, ciascuno con un componente TPdfView all'interno allineato come alClient per riempire il box. Due componenti TSplitter sono posizionati tra i box per consentire all'utente di regolare la larghezza delle colonne a runtime. Una barra degli strumenti sopra i pannelli ospita i pulsanti di apertura, i controlli dello zoom e il selettore per passare dalla visualizzazione a due a quella a tre pannelli

La modalità a tre viste è gestita tramite un valore booleano tracciato internamente dal form. Quando questo stato cambia, vengono ricalcolate le larghezze e viene mostrata o nascosta la terza colonna. L'approccio più semplice consiste nel rimuovere tutte le proprietà Align, nascondere gli splitter e quindi impostare le posizioni assolute:

procedure TFormMain.UpdateLayout;
var
  TotalWidth: Integer;
begin
  TotalWidth := ClientWidth;

  if ThreeViewMode then
  begin
    ScrollBox3.Visible := True;
    ScrollBox1.Left   := 0;
    ScrollBox1.Width  := TotalWidth div 3;
    ScrollBox2.Left   := ScrollBox1.Width;
    ScrollBox2.Width  := TotalWidth div 3;
    ScrollBox3.Left   := ScrollBox2.Left + ScrollBox2.Width;
    ScrollBox3.Width  := TotalWidth - ScrollBox3.Left;
    // Apply the same (ClientHeight - toolbar height) to all three Height values
  end
  else
  begin
    ScrollBox3.Visible := False;
    ScrollBox1.Left   := 0;
    ScrollBox1.Width  := TotalWidth div 2;
    ScrollBox2.Left   := ScrollBox1.Width;
    ScrollBox2.Width  := TotalWidth - ScrollBox2.Left;
  end;
end;

Impostare Align := alNone su tutti e tre i box prima di eseguire i calcoli interi evita che il motore dei vincoli VCL vada in conflitto con le assegnazioni. Ripristina la visibilità degli splitter dopo il posizionamento se si desidera consentire il ridimensionamento tramite trascinamento nella modalità a due viste

L'altezza di ciascun scroll box corrisponde all'area client meno l'altezza del pannello della barra degli strumenti. Poiché la barra degli strumenti è ancorata in alto con alTop, ClientHeight - PanelButtons.Height restituisce lo spazio verticale utilizzabile. Assegna questo valore a tutti e tre i box all'interno della stessa chiamata UpdateLayout per evitare che in un qualsiasi frame un box risulti più alto degli altri, causando uno sfarfallio del layout

Apertura di un Documento

Ciascuna coppia di pannelli necessita della propria procedura di apertura. Il pattern è breve: disattivare il componente, impostare il nome del file, tentare l'attivazione e catturare l'eccezione EPdfError se il file richiede una password. Si noti che TPdfView.Active controlla il rendering, mentre TPdf.Active è ciò che effettivamente apre il file; i due stati sono indipendenti. Impostare PdfView.Active := True quando il relativo componente TPdf non è ancora attivo è innocuo ma non mostra nulla a schermo

procedure TFormMain.OpenPdfFile(PdfComponent: TPdf;
  PdfViewComponent: TPdfView);
var
  Password: string;
begin
  if not OpenDialog.Execute then
    Exit;

  PdfComponent.Active   := False;
  PdfComponent.FileName := OpenDialog.FileName;
  PdfComponent.Password := '';

  try
    PdfComponent.Active := True;
  except
    on E: EPdfError do
    begin
      if InputQuery('Password', 'Enter document password:', Password) then
      begin
        PdfComponent.Password := Password;
        PdfComponent.Active   := True;
      end
      else
        raise;
    end;
  end;

  if PdfComponent.Active then
  begin
    PdfViewComponent.PageNumber := 1;
    SetActivePdfView(PdfViewComponent);
  end;
end;

Verificare sempre lo stato di PdfComponent.Active dopo l'assegnazione; un file danneggiato o una password errata possono far fallire il caricamento in modo silenzioso senza sollevare un'eccezione nel percorso predefinito. Impostare esplicitamente PdfViewComponent.PageNumber := 1 dopo un'apertura andata a buon fine evita di mantenere il numero di pagina del documento precedente

Il codice di gestione della password sopra indicato solleva un'eccezione per qualsiasi errore diverso dal messaggio di password noto. Questo comportamento è intenzionale: è preferibile che i file corrotti o non supportati vengano segnalati immediatamente invece di essere nascosti dietro un pannello vuoto e silenzioso. Un utente che non vede nulla non saprebbe se il file è stato caricato ed è semplicemente vuoto, o se il componente lo ha rifiutato. Sollevare l'eccezione mantiene l'errore visibile

Tracciamento del Pannello Attivo

Quando l'utente fa clic all'interno di un pannello, quel pannello diventa attivo. Il form traccia lo stato tramite un campo privato FActivePdfView: TPdfView. Il feedback visivo consiste nel cambio del colore del bordo del contenitore TScrollBox: impostandolo su clHighlight per quello attivo e su clWindow per gli altri. Collega questo comportamento a ciascun evento TPdfView.OnClick e alla procedura di apertura in modo che il focus segua il documento appena aperto

Alcune operazioni si applicano a tutti i pannelli visibili anziché solo a quello attivo. Una variabile booleana FAllViewsMode sul form gestisce questa diramazione. Quando è impostata su true, le modifiche allo zoom e la navigazione delle pagine si estendono a tutti i pannelli che hanno un documento attivo:

procedure TFormMain.ApplyZoomToAll(NewZoom: Double);
begin
  if PdfView1.Active then PdfView1.Zoom := NewZoom;
  if PdfView2.Active then PdfView2.Zoom := NewZoom;
  if ThreeViewMode and PdfView3.Active then PdfView3.Zoom := NewZoom;
end;

Navigazione Sincronizzata delle Pagine

La navigazione sincronizzata è opzionale ma utile per i flussi di lavoro di revisione dei documenti in cui entrambi i file coprono lo stesso intervallo di pagine. La logica risiede in un gestore di eventi che si attiva dopo che l'utente ha navigato all'interno di una vista. Quando una vista sorgente modifica la sua proprietà PageNumber, il gestore propaga tale numero alle altre viste, a condizione che la vista di destinazione contenga almeno quel numero di pagine, altrimenti l'operazione viene saltata

Le proprietà PageNumber di TPdfView e di TPdf sono indipendenti. TPdf.PageNumber traccia quale pagina il componente del documento considera corrente; TPdfView.PageNumber traccia ciò che viene effettivamente mostrato sullo schermo. Ai fini della navigazione, occorre utilizzare la proprietà della vista, non quella del documento

Una casella di controllo con un'etichetta simile a "Sincronizza pagine" fornisce il controllo all'utente. Quando è deselezionata, ogni pannello viene navigato in modo indipendente e il gestore termina immediatamente. Questa indipendenza è importante per i casi d'uso in cui i due documenti hanno un numero di pagine differente, o quando l'utente desidera trovare il passaggio equivalente in una traduzione che inizia su una pagina diversa. Forzare sempre la sincronizzazione renderebbe lo strumento più difficile da usare rispetto a una semplice disposizione a due finestre sul desktop

Un aspetto da tenere a mente: l'impostazione programmatica di PdfView.PageNumber all'interno del gestore di sincronizzazione attiverà a sua volta l'evento di modifica su quella vista. Per evitare una ricorsione infinita, si utilizza un flag booleano impostato prima dell'assegnazione e azzerato subito dopo. Il flag è associato al form, non alla singola vista, poiché tutte e tre le viste condividono lo stesso gestore eventi

Zoom per Pannello

Ogni componente TPdfView include la propria proprietà Zoom, un valore Double espresso in percentuale in cui 1.0 corrisponde al 100%. L'impostazione di questo valore sovrascrive qualsiasi FitMode attivo. Per un pulsante "adatta alla larghezza" sul pannello attivo, leggi lo zoom di adattamento da PdfView.PageWidthZoom[PdfView.PageNumber] e assegnalo. Per "adatta alla pagina", utilizza PageZoom[PageNumber]. Entrambe sono proprietà di tipo array indicizzate in base 1 con il numero di pagina, pertanto occorre verificare che il numero di pagina non sia zero prima di accedervi

Quando si esporta la pagina corrente come immagine, leggi la rotazione dalla vista ma chiama RenderPage sul componente TPdf, non sulla vista. La versione bitmap di TPdf.RenderPage accetta dimensioni in pixel esplicite oltre a un valore TRotation e un set di TRenderOptions. La variante funzione restituisce un oggetto TBitmap di proprietà del chiamante che occorre liberare manualmente dopo il salvataggio:

procedure TFormMain.SaveActiveViewAsImage;
var
  Pdf: TPdf;
  Bmp: TBitmap;
  Jpeg: TJpegImage;
begin
  if not Assigned(FActivePdfView) or not FActivePdfView.Active then
    Exit;

  Pdf := FActivePdfView.Pdf;
  Pdf.PageNumber := FActivePdfView.PageNumber;

  Bmp := Pdf.RenderPage(
    0, 0,
    Round(Pdf.PageWidth * 2),
    Round(Pdf.PageHeight * 2),
    FActivePdfView.Rotation, [], clWhite);
  try
    if SavePictureDialog.Execute then
    begin
      Jpeg := TJpegImage.Create;
      try
        Jpeg.Assign(Bmp);
        Jpeg.CompressionQuality := 90;
        Jpeg.SaveToFile(SavePictureDialog.FileName);
      finally
        Jpeg.Free;
      end;
    end;
  finally
    Bmp.Free;
  end;
end;

Il moltiplicatore 2x su larghezza e altezza consente di ottenere un output più nitido per i documenti con testo fine. Il blocco try/finally per liberare la bitmap è obbligatorio: anche se l'utente annulla l'operazione nella finestra di dialogo TSaveDialog, il blocco finally viene comunque eseguito, garantendo il corretto rilascio della bitmap

Requisiti delle DLL

PDFium Component racchiude la libreria nativa pdfium. Un processo host a 32 bit richiede pdfium32.dll, mentre un host a 64 bit richiede pdfium64.dll. Le varianti dotate del motore JavaScript V8 aggiungono il suffisso v8 e hanno una dimensione di circa 23-27 MB, rispetto ai 5-6 MB delle build standard. Per un visualizzatore di confronto che disabilita la compilazione dei moduli (Pdf.FormFill := False), la build standard senza V8 è sufficiente e permette di mantenere più leggera la distribuzione

Posiziona la DLL nella stessa directory dell'eseguibile o in qualsiasi cartella inclusa nella variabile di sistema PATH. Il componente la carica su richiesta quando viene attivato il primo componente TPdf, pertanto l'eventuale assenza della DLL si manifesterà in quel momento anziché all'avvio dell'applicazione. Se distribuisci un programma di installazione, l'approccio più affidabile consiste nel copiare la DLL nella cartella dell'applicazione durante l'installazione, invece di fare affidamento su una directory di sistema che un amministratore potrebbe ripulire in seguito

Le build con motore V8 sono utili principalmente quando è necessario interagire con azioni JavaScript del PDF, ad esempio per attivare campi di calcolo o gestori di invio dati. Un visualizzatore di confronto passivo non ha motivo di eseguire JavaScript; l'impostazione di Pdf.FormFill := False prima di Active := True esclude interamente l'ambiente di compilazione dei moduli, il che significa che nessun motore JS verrà inizializzato, anche se si utilizza la build standard. Questa è l'impostazione predefinita corretta per un visualizzatore in sola lettura, indipendentemente dalla variante della DLL distribuita

Per ulteriori dettagli sul componente PDFium Component e sulla sua API completa, visita la pagina del prodotto Componente Delphi PDFium Component

Il confronto aggiornato supporta due o tre pannelli indipendenti, conserva il pannello attivo per la navigazione sincronizzata e applica lo zoom per pannello tramite `TPdfView.Zoom`, mantenendo le DLL richieste nel `PATH`