Un team addetto all'elaborazione dei sinistri aveva trent'anni di pratiche cartacee da digitalizzare con uno scanner a foglio. Lo scanner produceva un file JPEG per pagina all'interno di una cartella, con nomi come 0001.jpg, 0002.jpg e così via. L'archivio richiedeva invece un PDF per ogni fascicolo, con le pagine in ordine, in modo che il revisore potesse aprire un unico documento invece di scorrere cento miniature. Questo ultimo passaggio, trasformare una pila numerata di scansioni in un singolo PDF ordinato, è il compito descritto qui.
PDFium VCL lo gestisce direttamente. Oltre al rendering e all'estrazione del testo, il componente può creare un PDF da zero: crea un documento vuoto, aggiunge una pagina bianca delle dimensioni desiderate, posiziona un'immagine su quella pagina in coordinate utente PDF, poi salva. L'intera pipeline risiede nel componente TPdf, quindi un convertitore batch si riduce a un ciclo sui nomi di file con poche chiamate.
La struttura della conversione
Per ogni scansione occorre fare tre cose: stabilire le dimensioni della pagina, posizionare l'immagine all'interno della pagina lasciando un margine e passare alla pagina successiva. PDFium VCL mette a disposizione un metodo per ciascuna operazione: AddPage crea una pagina bianca di dimensioni specificate, AddImage (oppure AddPicture se si dispone già di un oggetto TPicture) disegna la bitmap nella pagina corrente, e PageNumber indica al componente quale pagina deve ricevere i successivi comandi di disegno.
Il dettaglio che causa più confusione è il sistema di coordinate. Lo spazio utente PDF posiziona l'origine nell'angolo in basso a sinistra della pagina, con Y crescente verso l'alto, l'opposto delle coordinate schermo a cui gli sviluppatori Delphi sono abituati per riflesso. I valori X, Y passati ad AddImage indicano l'angolo in basso a sinistra del rettangolo dell'immagine, mentre Width, Height rappresentano la dimensione di posizionamento in punti, non la dimensione in pixel del file sorgente. Invertirli significa che le scansioni finiscono fuori dalla pagina o capovolte rispetto al punto atteso.
Creare il documento e una pagina per scansione
Si parte da un documento vuoto. CreateDocument alloca un nuovo PDF e lascia il componente attivo, quindi non è necessario un passaggio separato di apertura. Dopodiché si scorre l'elenco dei file scansionati: per ognuno si aggiunge una pagina, la si rende corrente e si posiziona l'immagine. Le dimensioni di pagina usate qui sono A4 in punti (595 × 842 verticale), il formato standard per la corrispondenza archiviata.
procedure TArchiveForm.ScansToPdf(const Files: TStrings; const OutputPath: string);
const
PageW = 595.0; // A4 width in points
PageH = 842.0; // A4 height in points
Margin = 36.0; // half-inch border around each scan
var
I: Integer;
Pdf: TPdf;
begin
Pdf := TPdf.Create(nil);
try
Pdf.CreateDocument; // new, empty, already active
for I := 0 to Files.Count - 1 do
begin
Pdf.AddPage(I + 1, PageW, PageH); // 1-based page index
Pdf.PageNumber := I + 1; // make the new page current
PlaceScan(Pdf, Files[I], PageW, PageH, Margin);
end;
Pdf.SaveAs(OutputPath);
finally
Pdf.Free;
end;
end;
Ogni iterazione crea una pagina e imposta immediatamente PageNumber su di essa. Questa seconda riga è fondamentale: AddPage inserisce la pagina ma i metodi di disegno agiscono sulla pagina corrente, quindi impostare PageNumber è ciò che punta AddImage alla pagina appena creata. Omettendo questa riga, le immagini si sovrappongono su qualsiasi pagina fosse caricata in precedenza.
Nel ciclo si nasconde un'assunzione: l'ordine di Files. Uno scanner nomina le pagine da 0001.jpg a 0100.jpg, ma un'enumerazione di directory non le restituisce sempre ordinate; non appena si ha page9.jpg accanto a page10.jpg, un ordinamento lessicale puro mette la pagina 10 prima della 9. Ordinare l'elenco esplicitamente prima del ciclo è indispensabile, e si preferisca usare nomi con zeri iniziali già al momento della scansione, così l'ordine lessicale corrisponde all'ordine delle pagine. La sequenza delle pagine è la cosa che un revisore nota immediatamente, ed è l'errore più economico da prevenire.
Posizionare una scansione mantenendo le proporzioni
Una scansione raramente ha la stessa forma della pagina. Allungarla fino a riempire il foglio distorce il testo; posizionarla a dimensione pixel piena comporta il fuoriuscita. La soluzione è scalare in base al minore dei due rapporti, larghezza o altezza, e centrare lo spazio rimanente. Poiché l'origine si trova in basso a sinistra, centrare significa dividere equamente lo spazio residuo e aggiungerlo sia a X che a Y.
procedure TArchiveForm.PlaceScan(Pdf: TPdf; const FileName: string;
PageW, PageH, Margin: Double);
var
Pic: TPicture;
AvailW, AvailH, Scale, DrawW, DrawH, X, Y: Double;
begin
Pic := TPicture.Create;
try
Pic.LoadFromFile(FileName); // BMP, JPG, PNG, etc. via the VCL graphics units
AvailW := PageW - 2 * Margin;
AvailH := PageH - 2 * Margin;
// Fit inside the margins without distorting the scan.
Scale := Min(AvailW / Pic.Width, AvailH / Pic.Height);
DrawW := Pic.Width * Scale;
DrawH := Pic.Height * Scale;
// Center: leftover space split evenly. Y measured from the page bottom.
X := (PageW - DrawW) / 2;
Y := (PageH - DrawH) / 2;
Pdf.AddImage(FileName, X, Y, DrawW, DrawH);
finally
Pic.Free;
end;
end;
Questo codice carica il file una volta per leggerne le dimensioni in pixel, calcola una scala uniforme e passa il rettangolo di posizionamento ad AddImage. AddImage accetta direttamente un percorso file e lo instrada attraverso la stessa pipeline di immagini usata da AddPicture, quindi qualsiasi formato riconosciuto dalle unità grafiche VCL funziona senza casi speciali. Se si dispone già dell'immagine decodificata in un oggetto TPicture proveniente da un pannello di anteprima, si chiami AddPicture(Pic, X, Y, DrawW, DrawH) con lo stesso rettangolo, evitando una seconda lettura del file.
Evitare la decodifica per le scansioni JPEG
Gli scanner emettono quasi sempre file JPEG. Caricare un JPEG in un oggetto TPicture lo decodifica in bitmap, e poi PDFium lo ricodifica al salvataggio: due cicli con perdita di qualità non necessari. AddJpegImage incorpora i byte compressi originali direttamente nella pagina da uno stream, il che è sia più veloce che visivamente più pulito per un batch ad alto volume.
var
Stream: TFileStream;
begin
// ... after AddPage + PageNumber for the current page ...
Stream := TFileStream.Create(FileName, fmOpenRead);
try
// Embeds the JPEG bytes as-is; no decode/re-encode cycle.
Pdf.AddJpegImage(Stream, X, Y, DrawW, DrawH);
finally
Stream.Free;
end;
end;
Si calcolano ugualmente X, Y, DrawW e DrawH nello stesso modo, poiché le dimensioni in pixel sono necessarie per il ridimensionamento. Si leggano dal file o tramite una rapida analisi dell'intestazione, poi si passi lo stream grezzo ad AddJpegImage. Per scansioni PNG o TIFF il percorso AddImage è quello corretto; si riservi la scorciatoia JPEG al formato a cui si applica effettivamente.
Etichettare ogni pagina
Le scansioni archiviate sono più facili da verificare quando ogni pagina riporta il nome del file sorgente. AddText disegna una stringa in una coordinata dello spazio utente, quindi una didascalia può essere posizionata appena sotto l'immagine. Si ricordi l'asse Y invertito: per inserire un'etichetta sotto la scansione, si sottrae dal bordo inferiore dell'immagine anziché aggiungere.
// Caption below the scan: Y decreases toward the page bottom.
Pdf.AddText('File: ' + ExtractFileName(FileName), 'Helvetica', 9,
X, Y - 14, clGray);
Un'ultima nota sul salvataggio. SaveAs è una funzione che restituisce un Boolean, quindi nel codice di produzione è opportuno verificarne il risultato anziché assumere che la scrittura sia riuscita: un disco pieno o un percorso di output bloccato fallisce silenziosamente in caso contrario. Una volta terminato il ciclo e scritto il file, si ottiene esattamente ciò di cui l'archivio aveva bisogno: un PDF ordinato per ogni fascicolo, con le pagine ridimensionate per adattarsi, pronto per essere letto in qualsiasi visualizzatore.
Gli stessi componenti di base coprono attività correlate. Modificando la regola di dimensionamento per pagina si ottiene un album fotografico con un'immagine per foglio; mantenendo il ciclo ma leggendo da una sorgente TIFF multipagina si ottiene un convertitore per archivi fax. Per un quadro più ampio sulla creazione di PDF a livello di codice, si veda la creazione di documenti PDF da zero con PDFium VCL; per rendere il risultato a schermo in un secondo momento, si veda la conversione di pagine PDF in immagini JPEG con PDFium VCL.
PDFium VCL Component di loslab.com raggruppa le API di creazione documenti, rendering e testo utilizzate in tutta questa serie.