PDF memorizza le immagini come oggetti di prima classe all'interno dei suoi flussi di contenuto (content stream). Quando una pagina fa riferimento a una fotografia, a una scansione o a un diagramma, i dati dei pixel risiedono in un dizionario XObject insieme alla geometria della pagina. PDFium VCL espone queste informazioni tramite due proprietà di TPdf: BitmapCount, che restituisce il numero di bitmap incorporate nella pagina corrente, e Bitmap[Index], che decodifica una di esse in un TBitmap di proprietà del chiamante, il quale deve liberarlo. Questo è l'intero modello di estrazione. Il ciclo è di quattro righe; ciò che richiede giudizio è la struttura di contorno.
Apertura del documento
La prima cosa da sapere su TPdf è che l'assegnazione Active := True non genera mai eccezioni. Errori di caricamento, password errate, file danneggiati: tutti questi problemi vengono ignorati internamente e il componente rimane semplicemente inattivo. È necessario controllare lo stato di questa proprietà dopo l'assegnazione, altrimenti si procederà nel ciclo delle pagine con PageCount che restituisce zero, chiedendosi perché non sia stato estratto nulla.
var
Pdf: TPdf;
begin
Pdf := TPdf.Create(nil);
try
Pdf.FileName := 'report.pdf';
Pdf.Active := True;
if not Pdf.Active then
begin
Writeln('Failed to open: ', Pdf.FileName);
Exit;
end;
Writeln(Pdf.PageCount, ' pages');
// proceed to extraction
finally
Pdf.Free;
end;
end;
I file protetti da password seguono lo stesso schema: assegnare Pdf.Password prima di impostare Active := True. Se la password è errata, Active rimane False e non viene generata alcuna eccezione da intercettare. In uno strumento batch che elabora centinaia di file, questo comportamento silenzioso è in realtà utile: consente di accumulare gli errori in un elenco anziché dover gestire lo srotolamento dello stack di chiamate per ciascun file.
Iterazione delle pagine ed estrazione delle bitmap
La proprietà BitmapCount è per pagina, quindi è necessario impostare Pdf.PageNumber prima di leggerla. I numeri di pagina sono in base 1; il valore predefinito è 0, il che significa che non è caricata alcuna pagina. La proprietà Bitmap[Index] è in base 0 e restituisce un TBitmap di proprietà del chiamante. È obbligatorio liberarlo. Se si trascura di liberare la memoria all'interno di un ciclo lungo su un documento di grandi dimensioni, il consumo di memoria aumenta rapidamente, poiché ogni bitmap può contenere diversi megabyte di dati pixel non compressi.
procedure ExtractAllImages(Pdf: TPdf; const OutputDir: string);
var
Page, Idx: Integer;
Bmp: TBitmap;
OutPath: string;
begin
for Page := 1 to Pdf.PageCount do
begin
Pdf.PageNumber := Page;
for Idx := 0 to Pdf.BitmapCount - 1 do
begin
Bmp := Pdf.Bitmap[Idx];
if not Assigned(Bmp) then
Continue;
try
OutPath := Format('%s\p%d_img%d.bmp', [OutputDir, Page, Idx + 1]);
Bmp.SaveToFile(OutPath);
finally
Bmp.Free;
end;
end;
end;
end;
Il controllo con Assigned è importante. Un piccolo numero di generatori PDF scrive oggetti XObject di immagini con dimensioni in pixel pari a zero o con dati altrimenti non validi; in questi casi il componente restituisce nil invece di una bitmap vuota. Trattare una restituzione nil come un errore e interrompere l'estrazione è un riflesso errato: ignorate l'immagine, registrate la pagina e l'indice nel log se avete bisogno di una traccia di controllo, e continuate. Il resto della pagina potrebbe comunque contenere immagini valide.
Si noti che il ciclo esterno imposta Pdf.PageNumber ad ogni iterazione. Questa assegnazione è ciò che carica la pagina nello stato interno del componente e rende significativo il valore di BitmapCount. Se la si salta, si leggerà ripetutamente il conteggio della stessa pagina. Il pattern può sembrare ridondante durante la scrittura, ma è così che l'API è stata progettata: la pagina è un cursore, non una collezione.
Scelta del formato di output
Il formato BMP è senza perdita (lossless) e sempre disponibile senza unità aggiuntive, il che lo rende un valore predefinito sicuro quando non si conosce ancora il contenuto dell'immagine. Quando la dimensione del file è importante, il formato dei pixel del TBitmap restituito indica quale codec sia il più appropriato. Una bitmap a 32 bit contiene un canale alfa; il formato PNG lo preserva senza perdite. Un'immagine grande a 24 bit con tono continuo è candidata per il formato JPEG. Le immagini più piccole o quelle disegnate con una tavolozza di colori limitata sono generalmente preferibili in formato BMP anziché essere convertite in JPEG, che aggiunge artefatti di compressione a impostazioni di qualità bassa e offre un risparmio limitato a impostazioni elevate.
procedure SaveBitmap(Bmp: TBitmap; const FileName: string);
var
Jpg: TJPEGImage;
begin
case UpperCase(ExtractFileExt(FileName)) of
'.JPG', '.JPEG':
begin
Jpg := TJPEGImage.Create;
try
Jpg.Assign(Bmp);
Jpg.CompressionQuality := 85;
Jpg.SaveToFile(FileName);
finally
Jpg.Free;
end;
end;
else
Bmp.SaveToFile(FileName); // BMP: lossless, no extra units
end;
end;
Nella pratica, la scelta del formato è guidata da Bmp.PixelFormat e dalle dimensioni. Se PixelFormat = pf32bit, è necessario un formato che supporti il canale alfa; PNG is la scelta ovvia, sebbene richieda l'unità PNGImage nelle versioni precedenti di Delphi. Per le immagini a 24 bit con larghezza superiore a circa 300 pixel, il formato JPEG con qualità a 85 offre una riduzione delle dimensioni di tre a uno rispetto al formato BMP, senza perdite percepibili nella maggior parte dei contenuti fotografici. Al di sotto di questa soglia, il formato BMP ha dimensioni comparabili ed evita completamente qualsiasi decisione sulla qualità.
Cosa conta e cosa non conta BitmapCount
Il formato PDF distingue tra gli oggetti XObject immagine e la grafica vettoriale disegnata con operatori di tracciato (path). Una pagina dall'aspetto visivamente complesso può restituire un valore BitmapCount pari a zero se ogni elemento è vettoriale. Le pagine scansionate restituiscono quasi sempre esattamente uno: lo scanner scrive l'intera scansione come un singolo XObject immagine a pagina intera alla risoluzione impostata sullo scanner. Le pagine che mescolano testo tipografico con fotografie incorporate restituiscono una voce per ogni fotografia. Le linee decorative, gli sfondi sfumati e i bordi delle tabelle di solito non compaiono affatto nel conteggio delle bitmap.
Il conteggio non include inoltre le immagini inline, un costrutto PDF usato raramente in cui i dati dell'immagine sono incorporati direttamente nel flusso di contenuto della pagina anziché come XObject con nome. Queste rientrano al di fuori di ciò che questa API espone; sono abbastanza insolite nei documenti reali che la maggior parte degli strumenti di estrazione semplicemente non le gestisce.
Un dettaglio importante da tenere a mente: il valore di BitmapCount letto si riferisce alla pagina corrente a partire dall'ultima assegnazione di PageNumber. Se il codice effettua diramazioni o chiama funzioni che modificano PageNumber tra il conteggio e il recupero, potreste leggere meno immagini dello spazio allocato o superare l'indice finale. Mantenete la lettura del conteggio e il ciclo su Bitmap[] sulla stessa pagina senza modificare PageNumber nel mezzo.
Utilizzo di TPdfView in un'applicazione form
Memoria e prestazioni nei processi batch
Su un archivio di grandi dimensioni, il budget di memoria è l'elemento principale da monitorare. Ogni chiamata a Bitmap[] alloca un nuovo TBitmap nell'heap e, su una pagina scansionata a 300 DPI, questo può facilmente corrispondere a 25 MB di dati pixel non compressi prima di qualsiasi codifica. Se si elaborano le pagine in un ciclo stretto senza liberare la memoria tra le iterazioni, il working set cresce in modo lineare con il numero di immagini. La struttura corretta é sempre: recuperare una bitmap, eseguire le operazioni necessarie, liberarla e quindi recuperare la successiva. Se si ha la necessità di mantenere riferimenti a più bitmap contemporaneamente per una fase di confronto, contarle prima con BitmapCount e allocare il contenitore di conseguenza, quindi liberare ciascuna bitmap non appena si è terminato di utilizzarla, anziché rimandare la pulizia alla fine del documento. Su un documento con 500 pagine scansionate, questa distinzione può fare la differenza tra 25 MB e 12 GB di RSS di picco.
Il componente TPdfView espone le stesse proprietà BitmapCount e Bitmap[], ma la pagina da cui effettua la lettura è quella attualmente visualizzata nel viewer, non TPdf.PageNumber. I due puntatori di pagina sono indipendenti; l'impostazione di uno non sposta l'altro. In un'applicazione form VCL con un visualizzatore attivo, è possibile chiamare Pdf.PageNumber := N per gestire l'estrazione tramite TPdf mentre il visualizzatore rimane sulla pagina in cui l'utente ha effettuato l'ultimo scorrimento. Questa separazione è intenzionale e mantiene pulito lo stato di visualizzazione del viewer mentre viene eseguita un'estrazione in background.
Le proprietà BitmapCount e Bitmap[] mostrate qui fanno parte del PDFium VCL Component per Delphi e C++Builder.