Il rendering di una pagina PDF in JPEG consiste in due operazioni che in genere si eseguono insieme per poi esserne eseguito il debug separatamente. Per prima cosa si rasterizza la pagina in una bitmap di pixel a una risoluzione scelta. Poi si passa quella bitmap a un codificatore JPEG e si sceglie una qualità. PDFium Component possiede la prima metà tramite RenderPage; la seconda metà è puro VCL, TJPEGImage da Vcl.Imaging.jpeg. Il punto di giunzione tra di esse è dove risiedono le decisioni interessanti, perché la risoluzione che scegli sul lato del rendering e la qualità che scegli sul lato della codifica si controbilanciano a vicenda e con le dimensioni del file in modi in cui è facile sbagliare
La cosa da assimilare prima di qualsiasi codice: una pagina PDF non ha pixel. È descritta in punti, dove un punto è 1/72 di pollice, e la pagina è un disegno vettoriale misurato in quei punti. Quando chiedi a PDFium di eseguire il rendering, stai scegliendo su quanti pixel proiettare quel disegno, e quella scelta sono i DPI. Se sbagli i calcoli, finirai per eseguire il rendering di una miniatura sfocata quando volevi un master di stampa, o assegnerai una bitmap da 200 megapixel per qualcosa destinato a essere un'anteprima da 120 pixel
Dai DPI alle dimensioni in pixel
RenderPage richiede coordinate in pixel interi per Width e Height, non i DPI. Quindi il primo lavoro è la conversione. Una pagina riporta le sue dimensioni in punti tramite PageWidth e PageHeight (entrambi Double), e la conversione è la stessa che usa ogni rasterizzatore: i pixel sono uguali ai punti moltiplicati per i DPI di destinazione divisi per 72. Una pagina US Letter misura 612 per 792 punti. A 150 DPI diventano 1275 per 1650 pixel; a 72 DPI rimane 612 per 792, un pixel per punto, che è il caso in cui ci si dimentica che è semplicemente l'identità
// Pdf.PageNumber deve già puntare alla pagina che desideri.
PixelW := Round(Pdf.PageWidth * Dpi / 72);
PixelH := Round(Pdf.PageHeight * Dpi / 72);
Bitmap := Pdf.RenderPage(0, 0, PixelW, PixelH, ro0, [], clWhite);
// ... usa Bitmap ...
Bitmap.Free; // la forma a funzione di RenderPage ti cede la proprietà
Due dettagli in quelle quattro righe decidono se il codice è corretto. Il primo è che la forma a funzione di RenderPage restituisce una TBitmap di cui tu sei il proprietario. PDFium l'ha allocata e se n'è andato; se non la liberi (Free) ad ogni iterazione, un'elaborazione in batch su alcune centinaia di pagine causa un leak di alcune centinaia di bitmap e il processo si gonfia fino a collassare. Il secondo è l'argomento Color, qui clWhite. Le pagine PDF vengono solitamente disegnate assumendo un substrato bianco opaco, e una pagina con trasparenza di cui viene eseguito il rendering sul colore di sfondo sbagliato produce bordi fangosi o aloni scuri vaganti. Il bianco è l'impostazione predefinita corretta per quasi ogni documento; il parametro esiste per i rari casi in cui non lo è
Gli offset 0, 0 sono Left e Top nella pagina, nello spazio delle coordinate scalate, e si lasciano a zero a meno che non si stia ritagliando. L'opzione ro0 è la rotazione: lasciala a zero e PDFium onorerà qualsiasi rotazione la pagina dichiari già nella sua voce /Rotate, cosicché una pagina creata in orizzontale esce in orizzontale senza che tu faccia nulla
Codifica della bitmap come JPEG
Una volta che la bitmap esiste, il formato JPEG è la parte facile ed è puro codice Delphi. TJPEGImage.Assign vi copia la bitmap, CompressionQuality imposta la qualità su una scala da 1 a 100, e SaveToFile scrive il file. L'unica regola di ordinamento è che la qualità deve essere impostata prima del salvataggio, perché governa la codifica attivata da SaveToFile
uses
Vcl.Graphics, Vcl.Imaging.jpeg, PDFium;
procedure SavePageAsJpeg(Pdf: TPdf; PageNumber, Dpi, Quality: Integer;
const FileName: string);
var
Bitmap: TBitmap;
Jpeg: TJPEGImage;
begin
Pdf.PageNumber := PageNumber;
Bitmap := Pdf.RenderPage(0, 0,
Round(Pdf.PageWidth * Dpi / 72),
Round(Pdf.PageHeight * Dpi / 72),
ro0, [], clWhite);
try
Jpeg := TJPEGImage.Create;
try
Jpeg.Assign(Bitmap);
Jpeg.CompressionQuality := Quality; // 1..100
Jpeg.SaveToFile(FileName);
finally
Jpeg.Free;
end;
finally
Bitmap.Free;
end;
end;
Quel try/finally annidato sembra pignolo per un helper di una pagina, ed è esattamente ciò che serve per un'elaborazione in batch. Il blocco interno libera il codificatore, il blocco esterno libera la bitmap, e chiunque dei due si attivi in caso di eccezione rilascia comunque ciò che possiede. Se li riassumi in uno solo, un'eccezione durante la codifica può bloccare la bitmap. Su un'esecuzione lunga, questa è la differenza tra un convertitore che termina e uno che muore alla pagina 300 con un file corrotto e una finestra di dialogo di memoria esaurita
Scegliere insieme DPI e qualità
I due controlli non sono indipendenti dallo scopo dell'output, e l'errore comune è di aumentarli entrambi per precauzione. Una miniatura per il web renderizzata a 300 DPI e salvata con qualità 95 sarà di svariate centinaia di kilobyte che fingono di essere un'immagine da 120 pixel; il browser ne scarterà la maggior parte durante il ridimensionamento verso il basso. Abbina la risoluzione ai pixel di cui l'output ha effettivamente bisogno, quindi scegli una qualità che sopravviva alla compressione con perdita del formato JPEG senza artefatti visibili
| Output | DPI | Qualità JPEG |
|---|---|---|
| Miniatura per l'elenco | 72 | 60-70 |
| Anteprima a schermo | 96-150 | 80-85 |
| Visualizzazione ad alto dettaglio | 200-300 | 85-95 |
| Master di stampa | 300-600 | 90-100 |
La qualità JPEG merita un avvertimento a sé stante. Non è una scala lineare. Il salto da 70 a 85 garantisce un reale miglioramento visivo a fronte di un modesto aumento del file; il salto da 95 a 100 raddoppia circa il file per una differenza che quasi nessuno può notare, perché la qualità 100 non è ancora priva di perdite, semplicemente smette di scartare molto. Per le pagine cariche di testo, la compressione basata su blocchi del JPEG sfuma i bordi netti dei glifi in un debole effetto eco (ringing), ed è per questo che una qualità al di sotto dell'80 causerà un testo dall'aspetto scansionato su un output che dovrebbe essere nitido. Se le pagine sono principalmente testo e puoi cambiare formato, il PNG esegue il rendering di quel testo senza effetto eco; il JPEG si guadagna il suo posto sui contenuti fotografici e misti in cui la sua compressione è genuinamente inferiore
Miniature più veloci e leggere
Quando l'obiettivo è una miniatura piuttosto che una riproduzione fedele, si può dire al renderer di fare meno lavoro. Il parametro Options accetta un insieme di flag TRenderOption e alcuni di essi barattano la fedeltà in cambio di velocità esattamente come richiesto da una piccola anteprima. L'opzione reGrayscale elimina i colori, il che rende il rendering più veloce e produce una bitmap più piccola da codificare. reNoSmoothImage e reNoSmoothPath omettono l'anti-aliasing, che è comunque invisibile su scala miniatura
function RenderThumbnail(Pdf: TPdf; PageNumber, MaxW, MaxH: Integer): TBitmap;
var
Scale: Double;
begin
Pdf.PageNumber := PageNumber;
// Adatta la pagina all'interno di MaxW x MaxH preservando le proporzioni.
Scale := Min(MaxW / Pdf.PageWidth, MaxH / Pdf.PageHeight);
Result := Pdf.RenderPage(0, 0,
Round(Pdf.PageWidth * Scale),
Round(Pdf.PageHeight * Scale),
ro0, [reGrayscale, reNoSmoothImage], clWhite);
end;
Il caso della miniatura mostra anche il modo più pulito di pensare al dimensionamento. Invece di passare attraverso i DPI, si calcola un singolo fattore di scala che adatti la pagina all'interno di un riquadro di delimitazione e conservi le proporzioni, che è ciò che fa il Min dei due rapporti. Sia una pagina in verticale sia una in orizzontale finiscono all'interno dello stesso riquadro senza distorsioni, e non si deve mai ragionare su quanti DPI corrispondano a "adatta in 200 per 280". C'è un avvertimento su reGrayscale: converte i contenuti delle immagini raster in scala di grigi, ma i riempimenti vettoriali e il testo mantengono i loro valori di colore nel motore, pertanto una pagina che è per lo più grafica vettoriale potrebbe tornare indietro meno monocromatica di quanto suggerisca il nome del flag. Per un vero risultato interamente in scala di grigi, convertire la bitmap resa con GrayscalePdfBitmap è la strada affidabile
Elaborazione in batch per l'intero documento
Per mettere tutto insieme su un documento completo, occorre un ciclo su PageCount spostando PageNumber di una pagina alla volta. Le pagine hanno base 1: la pagina uno è PageNumber := 1, e il ciclo scorre fino a PageCount compreso, non fino a PageCount - 1. L'altra cosa che l'elaborazione in batch deve rispettare è il contratto del caricamento silenzioso. L'impostazione di Active := True non genera mai un errore su un file danneggiato o su una password errata; lascia semplicemente Active su False. Verificalo prima di eseguire il rendering di una singola pagina, altrimenti il primo RenderPage lavorerà contro un documento mai aperto
procedure ExportAllPages(const PdfPath, OutDir: string; Dpi, Quality: Integer);
var
Pdf: TPdf;
I, Digits: Integer;
begin
Pdf := TPdf.Create(nil);
try
Pdf.FileName := PdfPath;
Pdf.Active := True;
if not Pdf.Active then
raise Exception.Create('Impossibile aprire ' + PdfPath);
Digits := Length(IntToStr(Pdf.PageCount)); // aggiunge gli zeri affinché i file siano ordinati correttamente
for I := 1 to Pdf.PageCount do
SavePageAsJpeg(Pdf, I, Dpi, Quality,
Format('%s\page_%.*d.jpg', [OutDir, Digits, I]));
finally
Pdf.Active := False;
Pdf.Free;
end;
end;
L'aggiunta di zeri (zero-padding) tramite Digits è una piccola cosa che poi ti fa risparmiare un pomeriggio. Se nomini i file da page_1.jpg a page_10.jpg qualsiasi strumento che li classifichi come stringhe metterà page_10 subito dopo page_1 scombinandone l'ordine. Riempirli fino alla larghezza del numero di pagina più alto, in modo che un documento da 300 pagine produca page_001.jpg, mantiene l'ordine lessicale e quello della pagina identici per tutte le fasi successive
Per documenti sufficientemente grandi in cui la conversione impiega un tempo considerevole, eseguila fuori dal thread dell'interfaccia utente o trasmetti i messaggi (pump messages) tra una pagina e l'altra per far restare l'applicazione reattiva e dai all'utente un modo per fermarla. Se stai eseguendo il rendering di pagine molto grandi e vuoi un annullamento che abbia effetto a metà pagina piuttosto che tra una pagina e l'altra, PDFium Component ha un percorso di rendering progressivo con un token di annullamento; è un meccanismo più pesante di quanto serva per la maggior parte delle esportazioni batch, ma è presente qualora una singola pagina a 600 DPI fosse lenta al punto tale da causare un blocco
Un'ultima combinazione che vale la pena conoscere. Rasterizzare una pagina elimina il suo livello di testo: il formato JPEG è composto di pixel, e le parole in esso contenute non sono più né selezionabili né ricercabili. Quando hai bisogno sia dell'immagine sia del testo sottostante, esegui il rendering per l'immagine ed estrai il testo separatamente, argomento trattato dall'articolo complementare sull'estrazione di testo da documenti PDF con PDFium Component. I sovraccarichi (overloads) di RenderPage e le opzioni di rendering mostrate qui fanno parte del Componente PDFium per Delphi e C++Builder