Artykuł techniczny

Nieaktualny tekst po edycji: pamięć podręczna FPDF_TEXTPAGE w PDFium

Wywołujesz AddText, aby wbić linię na stronę PDF za pomocą PDFiumPas, a następnie natychmiast wywołujesz FindFirst, aby potwierdzić, że pieczątka wylądowała, a wyszukiwanie wraca puste. Tekst jest na stronie — Acrobat go pokazuje — ale komponent TPdf w PDFiumPas przechowuje osobną, buforowaną strukturę FPDF_TEXTPAGE, sparsowaną raz ze strumienia zawartości strony, a edycja nie aktualizuje tej struktury wstecznie sama z siebie. Odpytaj ją, zanim została odświeżona, a odczytasz stronę dokładnie tak, jak wyglądała przed twoją zmianą, nie po niej

Dlaczego PDFium zwraca nieaktualny tekst zaraz po edycji?

PDFiumPas opakowuje silnik renderowania PDFium od Google dla Delphi i C++Buildera, a jego wywołania tekstowe i edycyjne sięgają do dwóch różnych podsystemów wewnątrz tego silnika. FPDF_TEXTPAGE należy do strony odczytu: FPDFText_LoadPage przechodzi raz przez strumień zawartości strony i buduje stronę tekstową — kody znaków, pozycje, metryki czcionek, granice słów — a PDFiumPas przechowuje tę strukturę w buforze tak długo, jak strona pozostaje wczytana. Wywołania edycyjne, takie jak FPDFPage_InsertObject czy FPDFPage_GenerateContent, operują na zupełnie innej reprezentacji, na grafie obiektów i strumienia zawartości strony, a PDFium nie wpycha tych zmian do już otwartej strony tekstowej samodzielnie. Odbudowywanie jej przy każdej edycji uczyniłoby edycję wsadową niedopuszczalnie wolną, więc projekt wymienia ten koszt na regułę: kto trzyma uchwyt, zamyka go po edycji zmieniającej zawartość, a kolejny odczyt buduje świeży

Wnętrze pamięci podręcznej tekstu w TPdf: FTextPage, LoadTextPage i UnloadTextPage

TPdf śledzi buforowany uchwyt w jednym prywatnym polu, FTextPage, i opakowuje jego cykl życia w dwie metody. LoadTextPage sprawdza, czy FTextPage jest nil, i tylko w tym przypadku wywołuje FPDFText_LoadPage na bieżącej stronie; jeśli uchwyt już istnieje, LoadTextPage ponownie go wykorzystuje, nie pytając, czy strona zmieniła się od czasu jego zbudowania. UnloadTextPage to druga połowa: zamyka natywny uchwyt przez FPDFText_ClosePage, ustawia FTextPage z powrotem na nil, a także porzuca buforowaną listę linków webowych i wszelką trwającą sesję wyszukiwania, ponieważ obie zostały wyprowadzone z tej samej strony tekstowej i stają się nieaktualne z tego samego powodu

Zachowanie ponownego wykorzystania bez sprawdzania w LoadTextPage to dokładnie dlatego, dlaczego kolejność ma znaczenie. Każde zapytanie tekstowe na TPdfText, FindFirst, GetWebLinks — przechodzi najpierw przez LoadTextPage, więc dopóki FTextPage wciąż trzyma uchwyt sprzed edycji, żadne z tych wywołań nie ma sposobu, by wiedzieć, że zmiana się wydarzyła. Nawigacja po stronach nigdy nie była tu ryzykiem: UnloadPage, uruchamiany przy przełączaniu stron, przeładowaniach i zamknięciu dokumentu, zawsze zamykał stronę tekstową razem z samą stroną. Otwartym pytaniem zawsze były edycje zastosowane do strony, na której wciąż siedzisz

Które metody PDFiumPas automatycznie odświeżają pamięć podręczną?

Własne metody edycji strony w TPdfAddText, SetText, SetTextPositions, AddPath, RemoveObject i InsertFormObjectFromXObject — każda wywołuje UnloadTextPage, zanim wywoła UpdatePage (funkcję PDFium FPDFPage_GenerateContent), aby zserializować zmianę do strumienia zawartości. Wywołaj którąkolwiek z nich, a sam następny Text, FindFirst czy GetWebLinks odbuduje stronę tekstową z zawartości w jej obecnym stanie, bez żadnego dodatkowego wywołania z twojej strony

var
  Pdf: TPdf;
  Index: Integer;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.FileName := 'invoice.pdf';
    Pdf.Active := True;
    Pdf.PageNumber := 1;

    Pdf.AddText('Reviewed by J. Alvarez', 'Helvetica', 10, 72, 40, clBlack, 255, 0);
    // AddText already closed the cached text page, so this FindFirst
    // call rebuilds it fresh before it searches
    Index := Pdf.FindFirst('Reviewed by J. Alvarez');
    if Index >= 0 then
      ShowMessage('Stamp confirmed at character ' + IntToStr(Index));
  finally
    Pdf.Free;
  end;
end;

Wzorzec, który wciąż się psuje: buforowanie surowego uchwytu TextPage

TPdf udostępnia żywy uchwyt przez tylko-do-odczytu właściwość TextPage, na rzadki przypadek, gdy musisz wywołać funkcję FPDFText_*, której PDFiumPas nie opakował. Ta furtka ucieczkowa to też jedyne miejsce, w którym automatyczna inwalidacja nie może pomóc: gdy tylko skopiujesz wartość FPDF_TEXTPAGE z właściwości do zmiennej lokalnej, PDFiumPas nie ma sposobu, by wiedzieć, że wciąż ją trzymasz, ani sposobu, by zaktualizować twoją kopię, gdy UnloadTextPage uruchomi się gdzie indziej w twoim kodzie

var
  Pdf: TPdf;
  RawHandle: FPDF_TEXTPAGE;
  StaleCount: Integer;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.FileName := 'contract.pdf';
    Pdf.Active := True;
    Pdf.PageNumber := 1;

    RawHandle := Pdf.TextPage;    // FPDFText_LoadPage handle, cached in FTextPage
    Pdf.SetText(0, 'Amended Clause 4.2');
    // SetText already closed RawHandle and set Pdf.TextPage back to nil.
    // Calling any FPDFText_* function against the old value now touches a
    // handle PDFium has already freed — undefined behavior, not a bug you
    // can catch with a nil check
    StaleCount := FPDFText_CountChars(RawHandle);
  finally
    Pdf.Free;
  end;
end;

Użycie uchwytu po tym, jak uruchomiono na nim FPDFText_ClosePage, to niezdefiniowane zachowanie w samym PDFium, nie konwencja PDFiumPas, którą można wybrać zignorować — może zwrócić ostatnio znane dane, może zwrócić nic, albo zawiesić proces, i to, co z tego się zdarzy w danym buildzie, nie jest czymś, na czym kod aplikacji powinien polegać. Bezpieczna reguła jest wąska: odczytaj Pdf.TextPage na świeżo, bezpośrednio przed wywołaniem FPDFText_*, które go potrzebuje, i nigdy nie trzymaj kopii przez instrukcję, która mogłaby edytować stronę

Grupuj edycje wsadowo, potem odpytaj raz

Nic z tego nie oznacza, że każde wywołanie AddText czy RemoveObject potrzebuje obronnego zapytania tekstowego zaraz po sobie, aby sprawdzić wynik. Każda metoda edycji już płaci koszt jednorazowego zamknięcia strony tekstowej; odpytywanie po każdej pojedynczej edycji wewnątrz pętli płaci ten koszt ponownie bez żadnej korzyści, ponieważ FPDFText_LoadPage za każdym uruchomieniem przechodzi na nowo przez cały strumień zawartości

var
  Pdf: TPdf;
  I: Integer;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.FileName := 'watermarked.pdf';
    Pdf.Active := True;
    Pdf.PageNumber := 1;

    // Strip every text object that looks like a draft watermark. Each
    // RemoveObject call already invalidates the cache on its own, so
    // nothing needs refreshing by hand between iterations
    for I := Pdf.ObjectCount - 1 downto 0 do
      if (Pdf.ObjectType[I] = otText) and (Pdf.ObjectBounds[I].Top > 700) then
        Pdf.RemoveObject(I, True);

    // Query once, after the whole batch is done, not once per removal
    if Pdf.FindFirst('DRAFT') < 0 then
      ShowMessage('Watermark cleared');
  finally
    Pdf.Free;
  end;
end;

Ta sama logika grupowania odnosi się konkretnie do stanu wyszukiwania. FindNext i FindPrevious kontynuują sesję rozpoczętą przez FindFirst, a ta sesja zostaje zdemontowana przez UnloadTextPage razem ze wszystkim innym, więc wywołanie FindNext ponownie po edycji — zamiast ponownego wywołania FindFirst — zgłasza wyjątek, zamiast po cichu wznawiać wyszukiwanie względem zawartości, która już nie istnieje. Traktuj każdą edycję jako twardą granicę zarówno dla zawartości tekstowej, jak i pozycji wyszukiwania, i pozwól, aby jedno świeże FindFirst po drugiej stronie twoich edycji podjęło wyszukiwanie na nowo

Gdzie to pasuje do pracy z ekstrakcją i adnotacjami

Zwykłe wyodrębnianie tekstu — czytanie tekstu strony bez zmieniania czegokolwiek — nigdy nie natrafia na nic z tego, ponieważ nic nie unieważnia uchwytu, którego żadna edycja nie dotknęła. To, jak Text, prostokąty znaków i granice słów działają na niezmodyfikowanej stronie, omawia towarzyszący artykuł o wyodrębnianiu tekstu za pomocą PDFiumPas, bez cyklu życia pamięci podręcznej strony tekstowej, który dodaje ten artykuł

Cykl życia pamięci podręcznej ma największe znaczenie w przepływach pracy, które edytują, a następnie natychmiast działają na wyniku: wbijanie poprawki i wyszukiwanie jej, redagowanie akapitu i potwierdzanie, że zniknął, lub lokalizowanie frazy, aby zakotwiczyć adnotację znacznikową zaraz po wstawieniu obok niej tekstu. Ten ostatni przypadek warto oznaczyć osobno — adnotacje znacznikowe z punktami czworokąta są pozycjonowane na podstawie prostokątów znaków odczytanych ze strony tekstowej, więc adnotacja zbudowana ze współrzędnych przechwyconych przed edycją kończy podświetlaniem niewłaściwego miejsca, gdy tylko edycja wyląduje

API edycji i tekstu TPdf jest częścią komponentu PDFium dla Delphi i C++Buildera, a strona produktu zawiera pełne odniesienie do metod dla powierzchni edycji, ekstrakcji i wyszukiwania opisanych tutaj