PDFium Component aggiunge un livello di testo ricercabile alle pagine PDF scansionate da Delphi tramite ApplyOcrSearchLayer. Renderizza ogni pagina selezionata, consegna i pixel a un provider OCR fornito dall'utente, e riscrive le parole riconosciute come oggetti di testo invisibili posizionati sopra le parole nella scansione. L'immagine di pagina originale non viene mai decodificata, ricodificata o sostituita, quindi il risultato visivo è byte per byte la pagina di partenza
Il motore di riconoscimento non fa deliberatamente parte della libreria. PDFium espone il rendering delle pagine, la mappatura delle coordinate, il caricamento dei font, la creazione di oggetti di testo e le modalità di render invisibili, ma non contiene alcun motore OCR, e far finta del contrario significherebbe integrare il prodotto di riconoscimento di qualcun altro in un componente PDF. Il riconoscimento risiede invece dietro l'interfaccia IPdfOcrProvider: la libreria passa pixel BGRA a layout fisso con origine in alto, e il provider restituisce testo Unicode, valori di confidenza e quadrilateri delle parole
Cos'è esattamente un livello di testo ricercabile?
Un PDF scansionato è la fotografia di un documento. Il contenuto della pagina è un'unica grande immagine, e non c'è nulla da selezionare, cercare, copiare o indicizzare. Un livello di testo ricercabile aggiunge veri oggetti di testo sopra quell'immagine con la modalità di render impostata su invisibile, così i visualizzatori non disegnano nulla ma selezione, ricerca ed estrazione trovano le parole esattamente dove compaiono
Il posizionamento è tutto. Se il testo invisibile è spostato di qualche punto, l'evidenziazione della selezione cade accanto alle parole anziché sopra di esse, e copiare un paragrafo produce testo nell'ordine sbagliato. Ecco perché la geometria deve provenire dalle stesse trasformazioni che PDFium usa per renderizzare la pagina, anziché da una stima proporzionale
Implementare il provider
Il contratto del provider è un unico metodo. Riceve un record di immagine di pagina che porta dimensioni, stride, DPI, formato pixel e i byte dei pixel stessi, più un token di cancellazione, e restituisce le parole o un messaggio di errore:
uses
PDFium;
type
TMyOcrProvider = class(TInterfacedObject, IPdfOcrProvider)
public
function RecognizePage(const Image: TPdfOcrImage;
const CancellationToken: IPdfCancellationToken;
out Words: TPdfOcrWords; out ErrorMessage: string): Boolean;
end;
function TMyOcrProvider.RecognizePage(const Image: TPdfOcrImage;
const CancellationToken: IPdfCancellationToken;
out Words: TPdfOcrWords; out ErrorMessage: string): Boolean;
var
I: Integer;
begin
// Image.Pixels contiene righe BGRA con origine in alto di Image.Stride byte.
// Passale al tuo motore, poi riempi una voce per ogni parola riconosciuta
SetLength(Words, RecognisedCount);
for I := 0 to RecognisedCount - 1 do
begin
Words[I].Text := EngineWordText(I);
Words[I].Confidence := EngineWordConfidence(I); // 0..1
Words[I].Quad := TPdfOcrQuad.FromRectangle(
EngineLeft(I), EngineTop(I), EngineRight(I), EngineBottom(I));
end;
ErrorMessage := '';
Result := True;
end;
Quadrilateri anziché rettangoli, perché una scansione è raramente perfettamente allineata alla pagina. Una parola su una pagina leggermente ruotata occupa un parallelogramma, e TPdfOcrQuad porta quattro punti d'angolo così che parole inclinate e ruotate mantengano una regione di selezione accurata. I motori che riportano solo riquadri allineati agli assi possono usare FromRectangle, che costruisce il quadrilatero degenere
Perché le posizioni delle parole non possono essere scalate proporzionalmente?
È allettante convertire una coordinata pixel in una coordinata di pagina dividendo per la larghezza di rendering e moltiplicando per la larghezza della pagina. Questo funziona solo per pagine senza rotazione, con un CropBox identico al MediaBox, e un'origine a zero, e molti documenti scansionati falliscono almeno una di queste condizioni
PDFium Component mappa ognuno dei quattro angoli del quadrilatero individualmente tramite FPDF_DeviceToPage, la stessa mappatura usata dal renderer per produrre i pixel, così le voci /Rotate e i crop box con offset vengono gestiti per costruzione. La matrice affine per l'oggetto di testo viene poi costruita da tre dei punti mappati, gli angoli in basso a sinistra, in basso a destra e in alto a sinistra, il che è esattamente sufficiente per esprimere posizione, scala, rotazione e distorsione
L'oggetto di testo stesso viene creato con dimensione del font unitaria così da poterne misurare i veri limiti del font, e i limiti misurati dell'oggetto vengono poi mappati sul quadrilatero di destinazione. Dimensionare con un corpo tipografico indovinato sperando che corrisponda alla parola scansionata deriverebbe a ogni sostituzione di font; misurare prima rende l'adattamento indipendente da quale font usa il livello
Eseguirlo su un documento
Il record delle opzioni controlla risoluzione, filtraggio e ogni budget. Il filtraggio per confidenza conta più di quanto sembri: parole spazzatura a bassa confidenza inquinano permanentemente i risultati di ricerca, e a differenza di un rendering sbagliato, nessuno se ne accorge finché una ricerca non restituisce assurdità:
var
Pdf: TPdf;
Options: TPdfOcrOptions;
Report: TPdfOcrReport;
I: Integer;
begin
Pdf := TPdf.Create(nil);
try
Pdf.FileName := 'scanned-contract.pdf';
Pdf.LoadDocument;
Options := TPdfOcrOptions.Default;
Options.Dpi := 300; // risoluzione di riconoscimento
Options.MinConfidence := 0.60; // scarta le parole incerte
Options.SkipPagesWithText := True; // lascia intatte le pagine nate digitali
Options.ContinueOnError := True; // una pagina difettosa non deve fermare il lavoro
Options.MaxPixelsPerPage := 40 * 1000 * 1000;
if Pdf.ApplyOcrSearchLayer(TMyOcrProvider.Create, Options, Report) then
Pdf.SaveAs('scanned-contract-searchable.pdf');
for I := 0 to High(Report.Pages) do
if Report.Pages[I].Status = popsFailed then
Writeln(Format('page %d failed: %s',
[Report.Pages[I].PageNumber, Report.Pages[I].ErrorMessage]));
Writeln(Format('%d word(s) inserted, %d rejected, %d page(s) skipped',
[Report.InsertedWordCount, Report.RejectedWordCount,
Report.SkippedPageCount]));
finally
Pdf.Free;
end;
end;
SkipPagesWithText merita di essere sottolineato negli archivi misti. Un PDF che già porta testo reale, sia esso nato digitale o precedentemente elaborato, riceve un secondo livello di testo se si esegue l'OCR alla cieca su di esso, e il duplicato fa sì che l'estrazione restituisca ogni parola due volte. Lo stato per pagina popsSkippedExistingText indica esattamente quali pagine sono state lasciate intatte
Budget, cancellazione e contenimento dei fallimenti
Ogni quantità che un documento malevolo o semplicemente enorme può gonfiare ha un limite massimo: pixel per pagina e in totale, parole per pagina e in totale, e caratteri per parola. Tutti vengono controllati prima che la pagina venga scritta, non dopo, e la stima dei pixel viene calcolata dalle dimensioni della pagina e dal DPI prima che venga allocata qualsiasi bitmap. Aumentare il DPI da 150 a 300 quadruplica la memoria per pagina, quindi il limite per pagina è il parametro da regolare per primo quando un job batch inizia a fallire su formati di grandi dimensioni
Il token di cancellazione attraversa l'intero percorso: rendering progressivo, chiamata al provider e ciclo di inserimento per parola. Questo significa che un utente che annulla durante il riconoscimento di un file di 400 pagine si ferma entro una pagina anziché alla fine del documento, e lo stesso pattern di token usato altrove nel componente, descritto in rendering progressivo annullabile, si applica qui invariato
Il contenimento dei fallimenti è per pagina. La libreria raccoglie gli handle degli oggetti che ha inserito in una pagina e chiama FPDFPage_GenerateContent una volta, dopo che tutte le parole sono state posizionate. Se qualcosa fallisce a metà, sia un errore del provider sia un problema di font, gli oggetti inseriti su quella pagina vengono rimossi in ordine inverso e il contenuto della pagina viene rigenerato, così una pagina fallita torna al proprio stato originale anziché conservare metà di un livello di testo. Il ciclo del documento poi continua o si ferma secondo ContinueOnError, e la pagina attiva viene sempre ripristinata
Verificare che l'immagine sia davvero rimasta intatta
Il controllo più forte disponibile è anche il più semplice: renderizza la pagina prima e dopo l'applicazione del livello alla stessa dimensione e confronta le bitmap. Dovrebbero essere identiche byte per byte, perché il testo invisibile non disegna nulla e lo stream dell'immagine non è mai stato decodificato. Qualsiasi differenza significa che qualcosa di diverso dal livello di testo ha cambiato la pagina
Dopodiché, verifica il lato testo estraendo dal file elaborato e confermando che le posizioni delle parole cadono sulla scansione. Il percorso di estrazione è lo stesso descritto in estrarre testo da documenti PDF, e per un rapido controllo visivo dell'allineamento, renderizzare le pagine in immagini come in convertire pagine PDF in JPEG permette di sovrapporre i riquadri delle parole sulla scansione
La stratificazione OCR, il rendering, l'estrazione e la modifica funzionano tutti sullo stesso oggetto documento in Delphi, C++Builder e Lazarus; l'intera superficie dell'API è descritta nella pagina di PDFium Component per Delphi