PDFium Component dodaje z Delphi przeszukiwalną warstwę tekstu do zeskanowanych stron PDF za pomocą ApplyOcrSearchLayer. Renderuje każdą wybraną stronę, przekazuje piksele dostawcy OCR, którego sam dostarczasz, i zapisuje z powrotem rozpoznane słowa jako niewidoczne obiekty tekstowe umieszczone nad słowami na skanie. Oryginalny obraz strony nigdy nie jest dekodowany, ponownie kodowany ani zastępowany, więc wizualny wynik to bit po bicie ta sama strona, od której zacząłeś
Silnik rozpoznawania celowo nie jest częścią biblioteki. PDFium udostępnia renderowanie stron, mapowanie współrzędnych, wczytywanie fontów, tworzenie obiektów tekstowych i niewidoczne tryby renderowania, ale nie zawiera silnika OCR, a udawanie inaczej oznaczałoby wpakowanie czyjegoś produktu rozpoznającego do komponentu PDF. Zamiast tego rozpoznawanie mieszka za interfejsem IPdfOcrProvider: biblioteka przekazuje piksele BGRA o stałym układzie i początku w górnym rogu, a dostawca zwraca tekst Unicode, wartości pewności i czworokąty słów
Czym dokładnie jest przeszukiwalna warstwa tekstu?
Zeskanowany PDF to zdjęcie dokumentu. Treść strony to jeden duży obraz i nie ma tam nic do zaznaczenia, wyszukania, skopiowania ani zindeksowania. Przeszukiwalna warstwa tekstu dodaje prawdziwe obiekty tekstowe na wierzchu tego obrazu, z trybem renderowania ustawionym na niewidoczny, więc przeglądarki nic nie rysują, a mimo to zaznaczanie, wyszukiwanie i ekstrakcja znajdują słowa dokładnie tam, gdzie się pojawiają
Pozycjonowanie to cała gra. Jeśli niewidoczny tekst siedzi kilka punktów obok, podświetlenie zaznaczenia ląduje obok słów, a nie na nich, a skopiowanie akapitu daje tekst w złej kolejności. Dlatego geometria musi pochodzić z tych samych transformacji, których PDFium używa do renderowania strony, a nie z proporcjonalnego odgadywania
Implementowanie dostawcy
Kontrakt dostawcy to jedna metoda. Otrzymuje rekord obrazu strony niosący wymiary, stride, DPI, format pikseli i same bajty pikseli, plus token anulowania, a zwraca słowa albo komunikat błędu:
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 zawiera wiersze BGRA o początku w górnym rogu, po Image.Stride bajtów.
// Przekaż je swojemu silnikowi, następnie wypełnij jeden wpis na każde rozpoznane słowo
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;
Czworokąty zamiast prostokątów, ponieważ skan rzadko jest idealnie prostopadły do strony. Słowo na lekko obróconej stronie zajmuje równoległobok, a TPdfOcrQuad niesie cztery punkty narożne, dzięki czemu przekrzywione i obrócone słowa zachowują dokładny region zaznaczenia. Silniki zgłaszające tylko prostokąty wyrównane do osi mogą użyć FromRectangle, które buduje zdegenerowany czworokąt
Dlaczego pozycji słów nie można skalować proporcjonalnie?
Kusi, by przeliczyć współrzędną piksela na współrzędną strony, dzieląc przez szerokość renderowania i mnożąc przez szerokość strony. To działa tylko dla stron bez obrotu, z CropBox identycznym z MediaBox oraz początkiem w zerze, a mnóstwo zeskanowanych dokumentów nie spełnia co najmniej jednego z tych warunków
PDFium Component mapuje każdy z czterech narożników czworokąta indywidualnie przez FPDF_DeviceToPage, to samo mapowanie, którego renderer użył do wytworzenia pikseli, więc wpisy /Rotate i przesunięte pola przycięcia są obsłużone konstrukcyjnie. Macierz afiniczna dla obiektu tekstowego jest następnie budowana z trzech zmapowanych punktów — narożników dolno-lewego, dolno-prawego i górno-lewego — co jest dokładnie wystarczające, by wyrazić pozycję, skalę, obrót i pochylenie
Sam obiekt tekstowy jest tworzony przy jednostkowym rozmiarze fontu, tak by można było zmierzyć jego rzeczywiste granice, a zmierzone granice obiektu są następnie mapowane na docelowy czworokąt. Dobieranie rozmiaru przez zgadywaną wielkość punktową w nadziei, że będzie pasować do zeskanowanego słowa, dryfowałoby przy każdej podmianie fontu; wcześniejszy pomiar sprawia, że dopasowanie jest niezależne od tego, jakiego fontu używa warstwa
Uruchamianie na dokumencie
Rekord opcji kontroluje rozdzielczość, filtrowanie i każdy budżet. Filtrowanie pewności ma większe znaczenie, niż się wydaje: śmieciowe słowa o niskiej pewności trwale zanieczyszczają wyniki wyszukiwania, a w przeciwieństwie do błędnego renderowania nikt tego nie zauważa, dopóki wyszukiwanie nie zwróci bzdur:
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; // rozdzielczość rozpoznawania
Options.MinConfidence := 0.60; // odrzuć niepewne słowa
Options.SkipPagesWithText := True; // zostaw w spokoju strony natywnie cyfrowe
Options.ContinueOnError := True; // jedna zła strona nie może zatrzymać zadania
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 zasługuje na podkreślenie w mieszanych archiwach. PDF, który już niesie prawdziwy tekst, czy to natywnie cyfrowy, czy wcześniej przetworzony, dostaje drugą warstwę tekstu, jeśli uruchomisz na nim OCR bez zastanowienia, a duplikat sprawia, że ekstrakcja zwraca każde słowo dwukrotnie. Status per-stronę popsSkippedExistingText mówi dokładnie, które strony pozostawiono w spokoju
Budżety, anulowanie i ograniczanie awarii
Każda wielkość, którą wrogi albo po prostu ogromny dokument może rozdąć, ma pułap: piksele na stronę i łącznie, słowa na stronę i łącznie, oraz znaki na słowo. Wszystkie są sprawdzane przed zapisaniem strony, nie po, a szacunek pikseli jest obliczany z wymiarów strony i DPI, zanim jakakolwiek mapa bitowa zostanie zaalokowana. Podniesienie DPI ze 150 do 300 czterokrotnie zwiększa pamięć na stronę, więc pułap na stronę to parametr, który należy dostroić najpierw, gdy zadanie wsadowe zaczyna zawodzić na dużych formatach
Token anulowania przewija się przez całą ścieżkę: renderowanie progresywne, wywołanie dostawcy i pętlę wstawiania słowo po słowie. Oznacza to, że użytkownik anulujący w trakcie rozpoznawania 400-stronicowego pliku zatrzymuje się w obrębie jednej strony, a nie na końcu dokumentu, a ten sam wzorzec tokenu, użyty gdzie indziej w komponencie i opisany w anulowalnym renderowaniu progresywnym, stosuje się tutaj bez zmian
Ograniczanie awarii działa na poziomie strony. Biblioteka gromadzi uchwyty obiektów, które wstawiła na stronie, i wywołuje FPDFPage_GenerateContent raz, po umieszczeniu wszystkich słów. Jeśli w połowie coś zawiedzie, czy to błąd dostawcy, czy problem z fontem, obiekty wstawione na tej stronie są usuwane w odwrotnej kolejności, a treść strony jest regenerowana, więc strona, na której wystąpiła awaria, wraca do stanu początkowego zamiast zachować połowę warstwy tekstu. Pętla dokumentu następnie kontynuuje albo zatrzymuje się zgodnie z ContinueOnError, a aktywna strona jest zawsze przywracana
Weryfikacja, czy obraz naprawdę pozostał nietknięty
Najsilniejsze dostępne sprawdzenie jest też najprostsze: wyrenderuj stronę przed i po zastosowaniu warstwy w tym samym rozmiarze i porównaj mapy bitowe. Powinny być identyczne bit po bicie, ponieważ niewidoczny tekst niczego nie rysuje, a strumień obrazu nigdy nie został zdekodowany. Jakakolwiek różnica oznacza, że coś innego niż warstwa tekstu zmieniło stronę
Następnie zweryfikuj stronę tekstową, wyodrębniając tekst z przetworzonego pliku i potwierdzając, że pozycje słów lądują na skanie. Ścieżka ekstrakcji jest tą samą, która opisana jest w ekstrakcji tekstu z dokumentów PDF, a do szybkiego wizualnego sprawdzenia wyrównania renderowanie stron do obrazów, jak w konwersji stron PDF do JPEG, pozwala nałożyć ramki słów na skan
Nakładanie warstwy OCR, renderowanie, ekstrakcja i edycja działają na tym samym obiekcie dokumentu w Delphi, C++Builder i Lazarus; pełna powierzchnia API jest opisana na stronie PDFium Component dla Delphi