Articolo tecnico

Renderizzare pagine PDF in Bitmap in Delphi con HotPDF

HotPDF renderizza una pagina PDF caricata in un TBitmap di Delphi attraverso una singola chiamata: RenderLoadedPageToBitmap(PageIndex, DPI). La funzione interpreta il flusso di contenuti della pagina e restituisce una bitmap RGB a 24 bit di proprietà del chiamante alla risoluzione scelta, che è esattamente ciò di cui ha bisogno una striscia di miniature, un'anteprima di stampa o una pipeline di esportazione da PDF a immagine. Questo articolo illustra l'API, per poi passare alla parte che distingue un renderer utilizzabile da un giocattolo: disegnare il testo a partire dai programmi dei font incorporati stessi anziché da font di sistema simili

Perché renderizzare una pagina PDF è più difficile che disegnare un'immagine?

Una pagina PDF non è un'immagine. È un programma: un flusso di operatori che creano percorsi, selezionano font, impostano colori e posizionano glifi, eseguiti in base al modello grafico definito in ISO 32000-1 §8. Niente nel file indica l'aspetto di un singolo pixel. Per produrre una bitmap è necessario eseguire quel programma — mantenere una matrice di trasformazione corrente, uno stack di stati grafici per q/Q, un percorso di ritaglio, spazi colore per riempimento e tratto — e rasterizzare il risultato. Ecco perché "mostrare semplicemente la pagina 3 come immagine" richiede un interprete del flusso di contenuti, non una conversione del formato di file

Il renderer di HotPDF, introdotto nella v2.253.0, è strutturato in sei unità disaccoppiate che rispecchiano questo modello: un core di matrice affine per l'algebra di trasformazione PDF [a b c d e f], uno stack di stati grafici, un risolutore di spazio colore (DeviceRGB, DeviceGray, DeviceCMYK, Indexed), un builder di percorsi che collega gli operatori di percorso PDF a GDI, un livello di metriche dei font che legge gli array /Widths per avanzamenti corretti, e l'interprete che smista gli operatori e guida gli altri cinque. Gli XObject immagine passano attraverso lo stesso stack di decodifica che la libreria utilizza per l'estrazione, quindi qualsiasi filtro immagine che HotPDF può decodificare per l'estrazione — comprese le immagini JPEG 2000 compresse con JPXDecode — appare anche nell'output renderizzato

Architettura di rendering di pagina HotPDF: un interprete di stream di contenuto PDF guida sei unità disaccoppiate e produce un TBitmap RGB a 24 bit di proprietà del chiamante
L'interprete smista gli operatori mentre le sei unità portano il lavoro su matrice, stato, colore, percorso, metriche e glifi

Renderizzare una pagina caricata in un TBitmap

RenderLoadedPageToBitmap accetta un indice di pagina a base zero e un valore DPI, dove 72 DPI mappano un'unità dello spazio utente PDF in un pixel. Restituisce nil in caso di errore (indice fuori intervallo, risorse mancanti) anziché sollevare un'eccezione, in modo che un visualizzatore possa saltare una pagina danneggiata e continuare. Il chiamante possiede la bitmap restituita e deve liberarla

var
  Pdf: THotPDF;
  Bmp: TBitmap;
begin
  Pdf := THotPDF.Create(nil);
  try
    if Pdf.LoadFromFile('report.pdf') > 0 then
    begin
      Bmp := Pdf.RenderLoadedPageToBitmap(0, 144);  // pagina 1 a 144 DPI
      if Bmp <> nil then
      try
        Image1.Picture.Assign(Bmp);
      finally
        Bmp.Free;  // il chiamante possiede la bitmap
      end;
    end;
  finally
    Pdf.Free;
  end;
end;

Il parametro DPI esegue il ridimensionamento per ogni scenario comune. Una striscia di miniature viene renderizzata a 36 o 48 DPI ottenendo bitmap piccole e veloci; un'anteprima sullo schermo a 96 o 144 DPI corrisponde alla tipica densità del display; un percorso di esportazione a 300 DPI produce immagini di qualità di stampa. La rotazione della pagina indicata nella voce /Rotate e l'inversione dell'origine di /MediaBox (PDF posiziona l'origine in basso a sinistra, GDI in alto a sinistra) sono gestite all'interno della matrice pagina-dispositivo, in modo che una pagina US Letter a 72 DPI venga restituita esattamente come 612×792 pixel con il corretto orientamento

Perché le miniature PDF renderizzate mostrano glifi errati?

Glifi errati o approssimativi nell'output PDF renderizzato indicano quasi sempre che il renderer sta sostituendo un font di sistema invece di utilizzare il font incorporato nel file. Il primo renderer di HotPDF faceva esattamente questo: rimuoveva il prefisso del subset da /BaseFont (trasformando ABCDEF+Arial in Arial), richiedeva a GDI un font di sistema con quel nome e disegnava il testo utilizzandolo. Per un documento che utilizza Arial o Times New Roman con codifica standard, il risultato appare simile. Ma si tratta di un'approssimazione che fallisce in scenari ben definiti

I font incorporati come subset rappresentano il caso peggiore. Un font subset può contenere solo i quaranta glifi effettivamente utilizzati da un documento, con codici di caratteri assegnati in un ordine privato per quel file — il codice 1 potrebbe essere "T", il codice 2 "h" e così via. Un font di sistema non sa nulla di tale assegnazione privata, quindi il testo scompare o viene visualizzato con caratteri completamente errati. Le codifiche personalizzate, i font di simboli, i font di codici a barre e qualsiasi carattere non installato sulla macchina di rendering falliscono allo stesso modo. Un renderer che si limita alla sostituzione con font di sistema produce miniature che richiamano la pagina — finché la pagina non utilizza i font che hanno reso necessario l'incorporamento in primo luogo

HotPDF: la sostituzione con font di sistema rimuove il prefisso subset da ABCDEF+Arial e disegna glifi sbagliati, mentre il rendering dei glifi incorporati riproduce i contorni del FontFile per forme esatte
La sostituzione sopravvive solo sui font di sistema comuni e fallisce proprio sui font subset che hanno reso necessario l'incorporamento

Rendering dei glifi incorporati: disegnare a partire dal programma del font stesso

HotPDF ha colmato questo divario nel corso di cinque versioni (dalla v2.268.0 alla v2.272.0) analizzando i programmi dei font incorporati e riproducendo i contorni dei loro glifi come percorsi vettoriali GDI riempiti. Il testo in una pagina renderizzata proviene ora dagli stessi dati di contorno utilizzati da un visualizzatore conforme, il che significa che i font subset, le codifiche personalizzate e i caratteri non installati vengono renderizzati con le loro forme esatte. La copertura è stata sviluppata in base alla variante del font:

Per i font Type0/CIDFontType2 con un programma TrueType incorporato (FontFile2), il renderer analizza direttamente le tabelle glyf e loca: i contorni quadratici vengono convertiti nelle curve di Bézier cubiche interpretate da GDI, i punti su curva impliciti tra punti fuori curva consecutivi vengono ricostruiti e i glifi compositi vengono riprodotti ricorsivamente. Sono supportati sia i layout Identity sia i flussi espliciti CIDToGIDMap, e gli avanzamenti CID rispettano le voci di larghezza /W e /DW, garantendo il corretto avanzamento del testo a due byte Identity-H

I programmi CFF (FontFile3, sia CIDFontType0C, Type1C, o un wrapper OpenType) beneficiano di un interprete charstring Type 2 completo: linee, curve, la famiglia flex, maschere di hint e chiamate di subroutine locali/globali con la corretta distorsione (bias) della subroutine. I programmi CFF basati su CID mappano i codici dei caratteri attraverso il set di caratteri del font (charset), aspetto importante per i font subset il cui ordine dei glifi differisce dall'ordine CID, e viene rispettata la selezione font-DICT per glifo tramite FDArray/FDSelect. I font TrueType semplici (non CID) risolvono i codici a un byte tramite la tabella cmap interna del font incorporato con una robusta catena di sottotabelle — formati Unicode 4 e 12 per primi, poi sottotabelle dei simboli con specchio ad uso privato F000, infine formati Macintosh legacy — mentre i font Type1 semplici risolvono tramite la codifica integrata del programma CFF

Due perfezionamenti completano il quadro. In primo luogo, i dizionari /Encoding dei font semplici vengono risolti in base alla priorità prescritta da ISO 32000-1 §9.6.6: gli array /Differences hanno la precedenza sulla codifica di base, che a sua volta ha la precedenza sulla mappa interna del programma del font — il percorso su cui si basano le toolchain derivate da TeX e PostScript, con nomi di glifi risolti tramite la Adobe Glyph List, il charset CFF o la cmap TrueType. In secondo luogo, i font Type3, i cui glifi sono a loro volta piccoli flussi di contenuto, vengono riprodotti attraverso il renderer con la composizione di matrice del font, dimensione del font e matrice del testo; le larghezze nello spazio dei glifi (/Widths) vengono interpretate attraverso la /FontMatrix como richiesto da ISO 32000-1 §9.6.5, e le procedure dei glifi che dichiarano un riquadro di delimitazione d1 vengono ritagliate su di esso, impedendo a un glifo di codice a barre malformato di disegnare all'esterno della sua cella. Quando un codice non può essere mappato — un programma danneggiato, un carattere non mappato — il renderer ricorre al disegno con font di sistema per quel glifo anziché saltare l'intera sequenza di testo

Come velocizzare i rendering ripetuti?

La risposta inclusa in HotPDF è una cache delle pagine utilizzate più di recente: RenderLoadedPageToBitmapCached conserva fino a RenderCacheCapacity pagine renderizzate (predefinito 8) indicizzate per indice di pagina e DPI, e un successo di cache (hit) restituisce una nuova copia di proprietà del chiamante senza toccare il flusso di contenuti — in genere migliaia di volte più veloce rispetto alla reinterpretazione della pagina. Questo schema si adatta perfettamente ai visualizzatori: un utente che passa da una pagina all'altra o un evento di ridimensionamento che richiede la stessa pagina agli stessi DPI colpirà la cache ogni volta

// Striscia di miniature: la prima passata renderizza, lo scorrimento indietro colpisce la cache
for I := 0 to ThumbCount - 1 do
begin
  Bmp := Pdf.RenderLoadedPageToBitmapCached(I, 48);
  if Bmp <> nil then
  try
    ThumbList.AddThumbnail(I, Bmp);
  finally
    Bmp.Free;
  end;
end;

// Dopo aver modificato sul posto una pagina caricata:
Pdf.InvalidateRenderedPageCache;  // il render successivo riflette la modifica

Valuta attentamente l'impatto sulla memoria prima di aumentare la capacità. Una pagina US Letter a 300 DPI equivale a 2550×3300 pixel, circa 25 MB come bitmap a 24 bit, quindi otto pagine in cache a risoluzione di esportazione occupano circa 200 MB. Ai DPI delle miniature, le stesse otto voci pesano molto meno di un megabyte. Dimensiona RenderCacheCapacity in base ai DPI effettivi a cui esegui il caching e chiama InvalidateRenderedPageCache dopo ogni modifica sul posto — la cache è indicizzata solo per pagina e DPI, e non può rilevare che il contenuto sottostante è cambiato. Il caricamento di un nuovo documento la cancella automaticamente

Una seconda cache opera al di sotto della cache di pagina: gli XObject immagine decodificati vengono conservati in un archivio limitato in byte regolato da ImageCacheMaxBytes (predefinito 32 MB) con eliminazione degli elementi meno usati di recente. Un logo o un'immagine di intestazione ripetuta su ogni pagina viene decodificata una sola volta per caricamento del documento anziché una volta per operatore Do, dimezzando approssimativamente il tempo di rendering per le pagine con immagini condivise e velocizzando l'esportazione TIFF multipagina nella stessa misura. Anche InvalidateRenderedPageCache svuota questa cache

Cosa viene ancora renderizzato in modo approssimativo

Il renderer ha come target il subset comune di documenti PDF, ed è utile sapere dove si trovano i limiti. Gli spazi colore CalRGB, Lab e basati su ICC sono approssimati anziché gestiti a livello di colore — gli spazi colore del dispositivo, le tavolozze indicizzate e le ricerche di colore per funzioni campionate di tipo 0 sono gestiti, ma un file destinato alla produzione di stampa basato su intenti di rendering ICC non sarà colorimetricamente esatto. I pattern di sfumatura (sh) e le modalità di fusione oltre il semplice canale alfa sono ugualmente esclusi, e la ricorsione di Form XObject è limitata in profondità per evitare loop infiniti. Per fatture, report, contratti e moduli — pagine costituite da testo, tracciati e immagini — l'output è fedele; per una bozza grafica ricca di gradienti e gruppi di trasparenza, considera la bitmap come un'anteprima, non come una prova

HotPDF: flusso della cache MRU delle pagine renderizzate: un hit restituisce una copia fresca del TBitmap senza reinterpretare la pagina, un miss renderizza e memorizza fino a RenderCacheCapacity pagine
Un hit di cache salta del tutto lo stream di contenuto, mentre invalidazione e budget di memoria tengono i render ripetuti corretti e sicuri

Lettura pratica: se la tua pipeline genera documenti con HotPDF o consuma PDF aziendali tipici, RenderLoadedPageToBitmap li restituisce con le forme esatte dei glifi incorporati, avanzamenti CID corretti e geometria di pagina corretta. Le approssimazioni risiedono negli angoli del modello grafico che i documenti aziendali visitano raramente

RenderLoadedPageToBitmap, la sua variante con cache e la pipeline di rendering dei glifi incorporati descritta qui sono fornite come parte del componente HotPDF Delphi Component per Delphi e C++Builder — una libreria VCL nativa senza dipendenze da DLL esterne, che copre la creazione di PDF, la modifica, l'estrazione del testo e il rendering delle pagine in un unico pacchetto