Artykuł techniczny

Wyszukiwanie i zastępowanie tekstu w istniejącym pliku PDF w Delphi

Komponent HotPDF potrafi wyszukiwać i zastępować tekst wewnątrz istniejącego pliku PDF z poziomu Delphi i C++Builder. Metody SearchLoadedPageText oraz SearchLoadedDocumentText lokalizują każde wystąpienie ciągu znaków z dokładnością do poziomu glifów, a ReplaceLoadedPageText i ReplaceLoadedDocumentText przepisują dopasowane bajty na miejscu — pod warunkiem, że każdy znak zastępujący może zostać ponownie zakodowany przy użyciu oryginalnego fontu, co jest fizycznym ograniczeniem, które ten artykuł traktuje rzetelnie, zamiast ukrywać je w przypisie

Zapotrzebowanie stojące za tą funkcją jest zazwyczaj prozaiczne. Firma zmienia nazwę, a trzy tysiące zarchiwizowanych faktur nadal nosi starą nazwę. Szablon umowy został wysłany z zeszłoroczną datą ważności. Kod produktu został wycofany i każda karta katalogowa, która o nim wspomina, wymaga w zamian kodu następcy. W edytorze tekstu każde z tych zadań zajmuje trzydzieści sekund. W pliku PDF jest to prawdziwie trudny problem, a zrozumienie dlaczego decyduje o tym, czy dobrze użyjesz interfejsu API, czy też zgłoszeg błąd będący w rzeczywistości cytatem ze specyfikacji

Dlaczego zastępowanie tekstu w pliku PDF jest tak trudne?

Zastępowanie tekstu w pliku PDF jest trudne, ponieważ strona PDF nie zawiera edytowalnego tekstu — zawiera wypozycjonowane glify. W modelu wyświetlania tekstu ISO 32000-1 §9.4 strumień zawartości steruje operatorami takimi jak Tj oraz TJ, które rysują sekwencje kodów znaków we współrzędnych ustalonych przez macierz tekstową. Kody te nie są kodami Unicode; są indeksami w jakimkolwiek kodowaniu, które deklaruje font strony, a odwzorowanie z powrotem na czytelne znaki może znajdować się w tabeli /ToUnicode CMap, tablicy różnic kodowania (encoding difference array) lub łańcuchu mapowania CID. Nie ma obiektu akapitu, przepływu tekstu ani gwarancji, że jedno wizualne słowo jest w ogóle przechowywane jako jeden ciąg znaków

Zastępowanie dodaje drugą warstwę trudności do dekodowania: musisz dokładnie wiedzieć, które bajty oryginalnego strumienia wyprodukowały każdy glif, aby móc wstawić nowe bajty dokładnie w ten obszar i nigdzie indziej. Moduł wyodrębniania tekstu może pozwolić sobie na odrzucenie pozycji bajtów po uzyskaniu kodu Unicode. Moduł zastępowania nie może tego zrobić. Właśnie dlatego HotPDF podzielił te prace na dwa wydania — wersja 2.251.0 przyniósła warstwę śledzenia przesunięć i wyszukiwania, a wersja 2.252.0 zbudowała na jej wierzchu warstwę przepisywania

Wyszukiwanie tekstu: wyszukiwanie na poziomie glifów ze śledzeniem przesunięcia bajtów

Metoda SearchLoadedDocumentText komponentu HotPDF znajduje każde wystąpienie poszukiwanej frazy poprzez dopasowanie jej do zdekodowanej sekwencji glifów Unicode każdej strony, a nie do surowych bajtów strumienia, więc trafienie jest trafieniem bez względu na to, jak font je zakodował. Podwaliny pod ten mechanizm wprowadzono w wersji 2.251.0: tokenizer strumienia zawartości rejestruje zakres bajtów StartOfs/EndOfs dla każdego operandu tekstowego — w tym jego ograniczników ( ) lub < > — a każdy zdekodowany glif niesie ze sobą trójkę TokenIndex/ItemIndex/ByteOffset wskazującą na dokładny operand, element tablicy TJ oraz jednostkę kodową, która go wyprodukowała. Ten sam interpreter glifów zasila interfejs API ekstrakcji opisany w wyciąganiu tekstu z załadowanego pliku PDF w Delphi; wyszukiwanie po prostu zachowuje te informacje o pochodzeniu, które ekstrakcja odrzuca

var
  Pdf: THotPDF;
  Matches: THPDFTextMatchArray;
  I: Integer;
begin
  Pdf := THotPDF.Create(nil);
  try
    if Pdf.LoadFromFile('invoices-2025.pdf') > 0 then
    begin
      if Pdf.SearchLoadedDocumentText('Acme Corp', False, Matches) then
        for I := 0 to Length(Matches) - 1 do
          WriteLn(Format('page %d at (%.1f, %.1f): "%s"',
            [Matches[I].PageIndex, Matches[I].X, Matches[I].Y,
             Matches[I].Text]));
    end;
  finally
    Pdf.Free;
  end;
end;

Jeden świadomy wybór projektowy zasługuje na uwagę. Gdy parametr CaseSensitive ma wartość False, porównanie ignoruje wielkość liter wyłącznie dla znaków ASCII: pełne ignorowanie wielkości liter w Unicode zachowuje się różnie w łańcuchach narzędzi od Delphi 5 do XE, które HotPDF obsługuje, a interfejs API wyszukiwania, który znajduje różne dopasowania w zależności od tego, który kompilator zbudował aplikację, jest gorszy niż ten z udokumentowanym, przewidywalnym ograniczeniem. Dla łacińskiego tekstu biznesowego — nazwisk, kodów, dat — ignorowanie wielkości znaków ASCII pokrywa praktyczne przypadki

Zastępowanie tekstu: wsteczne kodowanie i chirurgiczne wstawianie

Metoda ReplaceLoadedDocumentText, dodana w HotPDF w wersji 2.252.0, przepisuje każde wystąpienie szukanej frazy poprzez uruchomienie mechanizmu dekodowania wstecz. Funkcja HPDFEncodeUnicode jest odwrotnością dekodera kodów znaków: przechodzi ten sam łańcuch strategii w odwrotnej kolejności — wyszukiwanie w /ToUnicode bfchar i bfrange, mapowanie strumienia kodowania CID, mapowania Type0 identity oraz predefiniowane tabele WinAnsi i MacRoman — aby zamienić każdy znak zastępujący z powrotem na bajty kodu znakowego, których oczekuje oryginalny font. Ponownie zakodowane bajty są następnie serializowane do poprawnego literału tekstowego lub ciągu szesnastkowego, odzwierciedlając reguły unikowe tokenizera, dzięki czemu cykl analizuj → serializuj ponownie jest stabilny

Samo wstawienie ma charakter chirurgiczny, a nie hurtowy. Wewnątrz operandu tekstowego zastępowany jest tylko zakres bajtów kodu objęty dopasowaniem; bajty niedopasowane w tym samym operandzie, białe znaki między tokenami oraz każdy otaczający operator są zachowywane dosłownie, bajt po bajcie. Zastąpienie bca wewnątrz abcabc daje a + zastąpienie + bc, a nie uszkodzony operand. Zastępujące ciągi mogą być krótsze lub dłuższe niż szukana fraza — literał jest ponownie serializowany, a wartość /Length strumienia jest odświeżana — a każdy strumień /Contents na stronie wielostrumieniowej jest przetwarzany w izolacji, dzięki czemu strona pozostaje poprawna

Dlaczego nie można zastąpić tekstu znakami, których podzbiór fontu nigdy nie zawierał?

Nie można zastąpić tekstu znakiem, którego wbudowany podzbiór fontu nigdy nie zawierał, ponieważ sekwencja bajtów, która wybierałaby ten znak, po prostu nie istnieje w tabelach mapowania fontu. Gdy program generujący PDF osadza font z wydzielonym podzbiorem (subset), jego tabela /ToUnicode CMap oraz struktury kodowania obejmują wyłącznie te glify, które były faktycznie użyte w oryginalnym dokumencie. Metoda HPDFEncodeUnicode może odwrócić tylko te mapowania, które są obecne: jeśli dokument nigdy nie zawierał litery E w tym foncie, nie ma kodu znaku dla E, na który można by dokonać wstecznej konwersji. Jest to fizyczna właściwość pliku, a nie ograniczenie jakiejkolwiek konkretnej biblioteki — żadne narzędzie nie wyczaruje mapowania glifu, które nigdy nie zostało osadzone

HotPDF obsługuje to niepowodzenie w sposób konserwatywny. Jeśli choć jeden znak zamiennika nie może zostać ponownie zakodowany, całe to wystąpienie szukanej frazy jest pomijane — bez zgłaszania wyjątku i bez tworzenia częściowo zniekształconego tekstu, a wystąpienie to po prostu nie jest wliczane do ReplaceCount. Praktyczna konsekwencja: porównaj ReplaceCount z liczbą dopasowań z wcześniejszego wyszukiwania i potraktuj ewentualny niedobór jako sygnał. W powyższym przykładzie z datą, cyfra 6 musi pojawić się gdzieś w tekście dokumentu zapisanym tym samym fontem, aby przepisywanie się powiodło — co jest bardzo prawdopodobne w fakturze, ale nigdy nie jest gwarantowane ogólnie. Gdy potrzebne znaki są po prostu niedostępne, a celem jest usunięcie poufnych danych, a nie zmiana brzmienia tekstu, lepszym narzędziem jest faktyczne usunięcie zawartości; zobacz artykuł o redagowaniu i restrukturyzacji załadowanych plików PDF w Delphi, który opisuje tę ścieżkę

var
  Matches: THPDFTextMatchArray;
  Expected, Replaced: Integer;
begin
  Pdf.SearchLoadedDocumentText('Acme Corp', True, Matches);
  Expected := Length(Matches);
  Pdf.ReplaceLoadedDocumentText('Acme Corp', 'Apex Corp', True, Replaced);
  if Replaced < Expected then
    WriteLn(Format('%d occurrence(s) skipped: characters missing ' +
      'from the font subset, or match spans multiple operands',
      [Expected - Replaced]));
end;

Drugim warunkiem pominięcia w tym komunikacie jest inna udokumentowana granica: poszukiwana fraza, która rozciąga się na wiele operandów tekstowych — na przykład Hello podzielone między elementy [(He)(llo)] TJ — jest znajdowana przez wyszukiwanie, ponieważ dopasowuje ono zdekodowaną sekwencję glifów, ale jest pomijana przy zastępowaniu, ponieważ przepisanie jej ponad granicami operandów wymagałoby scalenia sąsiadujących zakresów bajtów. Wyszukiwanie, a następnie weryfikacja sprawiają, że oba te ograniczenia są widoczne zamiast cichych

Co zmienia się w pliku podczas zapisu?

Zastąpiony strumień /Contents jest zapisywany bez kompresji. Strumienie skompresowane za pomocą FlateDecode są dekompresowane na potrzeby edycji, a gdy HotPDF zapisuje przebudowane bajty, usuwa wpis /Filter strumienia i odświeża /Length zamiast ponownej kompresji. Wynikowy plik PDF jest w pełni poprawny i renderuje się normalnie w popularnych przeglądarkach; wadą jest większy rozmiar pliku dla każdego edytowanego strumienia. W przypadku potoku przetwarzającego wsadowo tysiące dokumentów należy zaplanować ten wzrost rozmiaru lub uruchomić oddzielny przebieg kompresji na dalszym etapie. To, jak przepisane obiekty wchodzą w interakcję ze strukturą odnośników dokumentu przy zapisie, to osobny temat omówiony w artykule o strumieniach obiektów i aktualizacjach przyrostowych w HotPDF

Cała reszta pliku pozostaje nienaruszona. Strumienie, które nie były modyfikowane, zachowują swoją kompresję, fonty i obrazy nie są przepisywane, a łączenie na poziomie pojedynczych operandów oznacza, że nawet edytowane strumienie różnią się od oryginału tylko w miejscach, w których wystąpiło dopasowanie. Ten konserwatyzm jest celowy: im większą część załadowanego dokumentu biblioteka przepisuje, tym więcej pojawia się okazji do uszkodzenia specyficznych cech generatora pliku, których programista biblioteki nie przewidział

Wyszukiwanie i zastępowanie tekstu dołącza do ekstrakcji, redagowania i renderowania stron w zestawie narzędzi HotPDF do pracy z załadowanymi dokumentami. Wszystkie te funkcje są sterowane przez ten sam interpreter strumienia zawartości i dostępne od Delphi 5 do najnowszych wydań RAD Studio bez zewnętrznych zależności. Pełna referencja API oraz wersja próbna do pobrania znajdują się na stronie produktu HotPDF Component