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