Articolo tecnico

Rendering PDF multi-motore in Delphi: integrato, Cairo e PDFium con PDF Library for Delphi

Tre rasterizzatori possono leggere lo stesso PDF e non concordare su ciò che contiene. Il motore integrato di PDF Library for Delphi è quello distribuito senza file aggiuntivi e rende correttamente ogni elemento, perciò occupa il posto predefinito. Cairo offre una pipeline diversa per trasparenza e anti-aliasing ed è spesso la scelta quando maschere morbide o modalità di fusione risultano errate altrove. PDFium incorpora il codice di rendering di Chrome, quindi una pagina che appare corretta in un browser di solito lo appare anche con PDFium, al costo di una DLL consistente e dell'obbligo di corrispondenza della bitness. Nessuno dei tre è corretto in astratto. La correttezza dipende dal documento e l'unico modo onesto per capire quale motore gestisce un determinato corpus è elaborare quel corpus con ciascuno di essi

Questo è il motivo per trattare il motore come scelta di runtime anziché di compilazione. PDF Library for Delphi, la libreria PDF per Delphi e C++Builder di losLab, colloca tutti e tre dietro un'unica superficie di rendering, quindi la decisione richiede un intero invece di un ramo di codice. Il resto riguarda la selezione sicura tra i motori, la conferma di quali motori siano effettivamente inclusi in un binario distribuito e l'impedire che lo stato di rendering comprometta silenziosamente il lavoro successivo

Tre rasterizzatori dietro un'unica superficie di chiamata

La libreria assegna un numero ai motori. Il motore 1 è il renderer integrato, quello predefinito, con opzioni di smussamento GDI+ in Windows. Il motore 2 è Cairo e il motore 3 è PDFium; entrambi vengono selezionati in fase di esecuzione tramite SelectRenderer. I due motori esterni vengono caricati da DLL i cui percorsi sono forniti con SetCairoFileName e SetPDFiumFileName prima di selezionarli. Qualunque sia il motore attivo, il lavoro passa attraverso le stesse chiamate: RenderPageToFile, RenderPageToStream, RenderDocumentToFile. Per cambiare motore si modifica un numero; il resto del codice di rendering non se ne accorge

Il modello di destinazione va ben oltre le bitmap. La classe del renderer supporta anche metafile (WMF, EMF, EMF+), EPS, contesti di dispositivo diretti, stampanti e HTML5, mentre Cairo e PDFium compaiono come destinazioni aggiuntive solo quando sono stati compilati. L'output raster è quello in cui i tre motori divergono più visibilmente, quindi è ciò che usano gli esempi di questo articolo

Tre motori di rendering PDF dietro un'unica superficie di chiamata: SelectRenderer commuta tra il motore incorporato, Cairo e PDFium mentre il codice applicativo continua a chiamare le stesse funzioni di render
SelectRenderer scambia un intero per spostare il lavoro tra i motori integrato, Cairo e PDFium. Il codice applicativo continua a chiamare RenderPageToFile e simili a prescindere da quale motore ha prodotto i pixel

Non presumere mai che un motore esista: verifica all'avvio

Cairo e PDFium sono funzioni di compilazione condizionale, quindi un binario può essere creato interamente senza di essi. In tal caso, richiedere il motore 2 o 3 non solleva eccezioni. SelectRenderer restituisce semplicemente un valore diverso dall'ID richiesto e il codice che ignora il valore di ritorno continua a eseguire il rendering con il motore precedentemente attivo. La difesa consiste in una verifica all'avvio che chieda a ogni motore di identificarsi e ne registri la risposta:

function ProbeEngines(PDF: TPDFlib): string;
begin
  Result := 'built-in';                        // il motore 1 è sempre presente
  if (PDF.SetCairoFileName('cairo.dll') = 1) and (PDF.SelectRenderer(2) = 2) then
    Result := Result + ', cairo';
  if (PDF.SetPDFiumFileName('pdfium.dll') = 1) and (PDF.SelectRenderer(3) = 3) then
    Result := Result + ', pdfium';
  PDF.SelectRenderer(1);                       // ripristina il default prima del lavoro vero
end;

Esegui questa verifica una volta all'avvio e scrivine il risultato nel log accanto a ogni lavoro di rendering. La domanda più comune quando un cliente segnala una differenza di rendering è quali motori abbia realmente la sua installazione; una risposta in una riga già presente nel log risolve la questione senza una sessione di desktop remoto. Vi è anche un effetto utile: se SetPDFiumFileName restituisce 0, sai già che il problema è la DLL (percorso errato, bitness errata o dipendenza mancante) e non un binario compilato senza supporto PDFium, perché la chiamata al percorso non ha risolto nulla prima ancora che SelectRenderer venisse eseguito

Dieci formati di output dietro un unico intero Options

Il parametro Options delle chiamate di rendering seleziona la codifica di output: 0 è BMP, 1 JPEG, 2 WMF, 3 EMF, 4 EPS, 5 PNG, 6 GIF, 7 TIFF, 8 EMF+ e 9 HTML5. PNG (5) è la scelta sensata per anteprime e immagini di pagina archiviate. JPEG (1), insieme a SetJPEGQuality, è preferibile per scansioni fotografiche in cui la dimensione del file conta più dei contorni nitidi

Un formato nasconde un requisito relativo allo stream di destinazione. Il percorso BMP scrive prima i dati dell'immagine, poi torna all'offset 0x26 per correggere i campi di risoluzione nell'intestazione. Se lo indirizzi a uno stream a sola lettura in avanti, a un wrapper di compressione o a un socket di rete, la chiamata fallisce in un modo che sembra un errore del motore ma non lo è. Quando una destinazione non ricercabile è inevitabile, esegui invece il rendering in PNG oppure passa il BMP attraverso uno stream in memoria e copialo in avanti una volta completato

Il DPI passato non è il DPI ottenuto

Ogni chiamata di rendering accetta un argomento DPI, ma la risoluzione effettiva è quel valore moltiplicato per la scala di rendering globale. SetRenderScale inizia da 1.0 e, dopo averlo modificato, il nuovo fattore si applica silenziosamente a ogni rendering successivo su quell'istanza:

PDF.SetRenderScale(2.0);                    // ogni render successivo viene raddoppiato
PDF.RenderPageToFile(150, 1, 5, 'p1.png');  // effettivamente 300 DPI
PDF.SetRenderScale(1.0);                    // resetta, altrimenti le tue miniature arrivano enormi

La stessa persistenza vale per SetRenderCropType e per l'impostazione della qualità JPEG. In un servizio che produce miniature, anteprime e immagini adatte alla stampa da un'istanza condivisa, queste impostazioni residue sono la vera origine della sporadica segnalazione «le miniature sono improvvisamente di 40 MB». Esistono due soluzioni pulite: reimpostare lo stato pertinente all'inizio di ogni operazione oppure dedicare un'istanza distinta a ogni profilo di output, così nulla trapela tra le operazioni

PDF Library for Delphi: diagramma di flusso delle sonde dei motori all'avvio: ogni renderer conferma il suo percorso DLL e la sua risposta SelectRenderer prima che una sintesi di disponibilità venga registrata accanto a ogni lavoro di render
Una chiamata di percorso fallita incrimina la DLL, mentre un risultato di SelectRenderer non corrispondente significa che il binario non ha mai compilato il motore. La sonda gira una volta e il suo riepilogo di una riga chiude la maggior parte delle domande di rendering dei clienti

Regolare il motore predefinito prima di passare a un altro

Una quota sorprendente delle richieste «ci serve un motore diverso» si rivela essere un problema di impostazioni sotto mentite spoglie. Il renderer integrato espone il proprio comportamento di smussamento tramite SetGDIPlusOptions e la più ampia famiglia SetRenderOptions, mentre SetGDIPlusFileName consente di puntare a uno specifico runtime GDI+ quando un ambiente di distribuzione ne include uno insolito. Disegni al tratto frastagliati a basso DPI, testo sfocato nelle miniature, bande nei gradienti: tutti rispondono a questi controlli e attivarli non costa nulla nel programma di installazione. Aggiungere Cairo o PDFium, al contrario, significa distribuire più DLL, gestire una seconda o terza variante di bitness e assumersi l'obbligo di aggiornarle

Perciò un reclamo sulla qualità ha un ordine naturale di operazioni. Riproducilo anzitutto con il DPI e la scala esatti del cliente, poiché metà delle volte la differenza svanisce quando questi coincidono. Prova poi le opzioni di smussamento del motore integrato. Solo allora confronta la pagina affiancata tra i motori mantenendo costante ogni altra variabile: esegui il rendering in PNG con i motori 1, 2 e 3 allo stesso DPI e allega tutte e tre le immagini. Di solito due su tre concordano e questa maggioranza indica se il valore anomalo dipende da un'interpretazione diversa del documento o da un'aspettativa di base errata. Tre immagini concrete risolvono una disputa sul rendering errato molto più rapidamente di un paragrafo di aggettivi

Una catena di fallback che si spiega da sola

Una volta predisposte la verifica e la disciplina dello stato, la catena di fallback è breve. Il rilevamento di un errore si basa su LastRenderError, che contiene il testo del messaggio del motore per il rendering più recente ed è vuoto quando il rendering è riuscito:

procedure RenderPageWithFallback(PDF: TPDFlib; Page: Integer; const OutFile: string);
begin
  PDF.SelectRenderer(1);                            // prima il built-in
  PDF.RenderPageToFile(200, Page, 5, OutFile);      // 5 = PNG
  if PDF.LastRenderError = '' then Exit;
  LogEngineFailure('built-in', Page, PDF.LastRenderError);
  if PDF.SelectRenderer(3) = 3 then                 // PDFium come fallback pesante
  begin
    PDF.RenderPageToFile(200, Page, 5, OutFile);
    if PDF.LastRenderError = '' then Exit;
    LogEngineFailure('pdfium', Page, PDF.LastRenderError);
  end;
  raise Exception.CreateFmt('Page %d failed on all available engines', [Page]);
end;

Due aspetti progettuali hanno peso. La catena registra perché è avvenuto ogni cambio, perché una riga di log come «questa pagina ha usato PDFium come fallback dalla versione 3.7» è un segnale di regressione da monitorare, non da perdere. Anche l'ordine di fallback è una politica da scegliere per ciascun carico di lavoro. Il motore integrato non richiede DLL aggiuntive, che lo rende la prima scelta corretta nella maggior parte delle installazioni, mentre documenti ricchi di gruppi di trasparenza o sfumature insolite sono il motivo tipico per cui un team integra un motore alternativo. Nessun motore è il più rapido in generale, ed è proprio questo il motivo della scelta per chiamata: confronta ciascuno con un campione dei documenti reali al DPI reale e ripeti la misurazione ogni volta che cambiano le DLL del motore o la composizione dei documenti. Il corpus decide sempre la discussione

Catena di fallback del render PDF: il motore incorporato prova per primo, i fallimenti vengono registrati, PDFium riprova, e un'eccezione sollevata riporta quando tutti i motori disponibili falliscono una pagina
Ogni tentativo controlla LastRenderError e registra il motivo prima di cambiare motore. Solo quando ogni motore installato ha fallito la catena solleva l'eccezione, con le cause raccolte già nel log

Oltre le singole pagine: batch TIFF e contesti di dispositivo live

Due chiamate vicine a quelle per singola pagina completano gli strumenti disponibili. RenderAsMultipageTIFFToFile esegue direttamente un'espressione di intervallo di pagine in un TIFF multipagina, la forma naturale per consegne archivistiche a sistemi di gestione documentale antecedenti al PDF. RenderPageToDC disegna direttamente in un contesto di dispositivo Windows per i controlli di anteprima, regolato da una propria triade di impostazioni persistenti (SetRenderDCOffset, SetRenderDCErasePage, oltre al tipo di ritaglio), che richiedono la stessa disciplina di ripristino del fattore di scala. Il rendering per l'anteprima a schermo e per il percorso di stampa contiene abbastanza insidie da meritare un articolo dedicato, collegato qui sotto

Dove proseguire

Un'abitudine da mantenere: poiché SelectRenderer ha effetto su ogni chiamata successiva dell'istanza, una singola pagina problematica può essere ritentata con un altro motore mentre il resto del documento rimane su quello predefinito. Per il disegno delle anteprime, la selezione della stampante e la gestione di DevMode, prosegui con l'articolo sull'anteprima di stampa e sul contesto di dispositivo. Quando i rendering alimentano una pipeline ad alto volume su file molto grandi, l'approccio basato su handle descritto in la guida all'accesso diretto si abbina naturalmente al rendering per pagina tramite DARenderPageToFile

Il packaging dei motori, i formati supportati e le versioni di prova sono descritti nella pagina del prodotto PDF Library for Delphi