Technický článek

Zastaralý text po editaci: cache FPDF_TEXTPAGE v PDFium

Zavoláte AddText, abyste na stránku PDF pomocí PDFiumPas otiskli řádek, pak okamžitě zavoláte FindFirst, abyste potvrdili, že razítko přistálo, a hledání se vrátí prázdné. Text na stránce je — Acrobat jej ukazuje — ale komponenta TPdf v PDFiumPas drží samostatnou cachovanou strukturu FPDF_TEXTPAGE, naparsovanou jednou z proudu obsahu stránky, a úprava tuto strukturu sama o sobě zpětně neaktualizuje. Dotážete se na ni dřív, než byla obnovena, a přečtete si stránku přesně tak, jak vypadala před vaší změnou, ne po ní

Proč PDFium hned po úpravě vrací zastaralý text?

PDFiumPas obaluje vykreslovací engine PDFium od Googlu pro Delphi a C++Builder, a jeho volání pro text a editaci sahají do dvou odlišných podsystémů uvnitř tohoto enginu. FPDF_TEXTPAGE patří ke čtecí straně: FPDFText_LoadPage jednou projde proud obsahu stránky a sestaví textovou stránku — kódy znaků, pozice, metriky fontu, hranice slov — a PDFiumPas drží tuto strukturu cachovanou tak dlouho, dokud stránka zůstává načtená. Editační volání jako FPDFPage_InsertObject nebo FPDFPage_GenerateContent operují na úplně jiné reprezentaci, grafu objektů a proudu obsahu stránky, a PDFium tyto změny samo o sobě netlačí do už otevřené textové stránky. Přestavovat ji při každé úpravě by dávkovou editaci nepřijatelně zpomalilo, takže návrh tento náklad místo toho vyměňuje za pravidlo — kdokoli drží handle, jej po úpravě měnící obsah zavře, a další čtení sestaví čerstvý

Edity PDFium zapisují do obsahového streamu strany, zatímco cachovaný FPDF_TEXTPAGE zůstává snímkem z času načtení, takže dotaz FindFirst v Delphi hned po AddText čte stranu před editací a minou razítko
Úpravy a čtení jsou dva oddělené podsystémy uvnitř PDFium; kešovaná textová stránka je snímek z času načtení a žádná úprava ji sama neobnoví

Uvnitř cache textu TPdf: FTextPage, LoadTextPage a UnloadTextPage

TPdf sleduje cachovaný handle v jediném soukromém poli, FTextPage, a obaluje jeho životní cyklus do dvou metod. LoadTextPage zkontroluje, zda je FTextPage nil, a jen v tom případě zavolá FPDFText_LoadPage proti aktuální stránce; pokud handle už existuje, LoadTextPage jej znovu použije, aniž by se ptala, zda se stránka od jeho sestavení změnila. UnloadTextPage je druhá polovina: zavře nativní handle přes FPDFText_ClosePage, nastaví FTextPage zpátky na nil, a také zahodí cachovaný seznam webových odkazů a jakoukoli probíhající relaci hledání, protože oba byly odvozeny ze stejné textové stránky a zastarají ze stejného důvodu

Chování LoadTextPage — znovupoužití bez kontroly — je přesně důvod, proč na pořadí záleží. Každý textový dotaz na TPdfText, FindFirst, GetWebLinks — prochází nejdřív přes LoadTextPage, takže dokud FTextPage pořád drží handle před úpravou, žádné z těchto volání nemá způsob, jak zjistit, že se stala změna. Navigace stránek nikdy nebyla to riziko: UnloadPage, která běží při přepnutí stránky, opětovném načtení a zavření dokumentu, vždy zavírala textovou stránku spolu se samotnou stránkou. Otevřená otázka byla vždy o úpravách aplikovaných na stránku, na které pořád sedíte

Které metody PDFiumPas obnovují cache automaticky?

Vlastní editační metody TPdfAddText, SetText, SetTextPositions, AddPath, RemoveObject a InsertFormObjectFromXObject — každá zavolá UnloadTextPage dřív, než zavolá UpdatePage (PDFium FPDFPage_GenerateContent), aby serializovala změnu do proudu obsahu. Zavolejte kteroukoli z nich a úplně další volání Text, FindFirst, nebo GetWebLinks přestaví textovou stránku z obsahu tak, jak právě stojí, bez jakéhokoli dodatečného volání na vaší straně

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 už zavřel cachovanou textovou stránku, takže toto FindFirst
    // ji před hledáním znovu postaví čerstvou
    Index := Pdf.FindFirst('Reviewed by J. Alvarez');
    if Index >= 0 then
      ShowMessage('Stamp confirmed at character ' + IntToStr(Index));
  finally
    Pdf.Free;
  end;
end;

Vzor, který se pořád rozbije: cachování surového handle TextPage

TPdf vystavuje živý handle přes vlastnost TextPage jen pro čtení, pro vzácný případ, kdy potřebujete zavolat funkci FPDFText_*, kterou PDFiumPas neobaluje. Tento únikový poklop je zároveň jediné místo, kde automatická invalidace nemůže pomoct: jakmile hodnotu FPDF_TEXTPAGE zkopírujete z vlastnosti do lokální proměnné, PDFiumPas nemá způsob, jak zjistit, že ji pořád držíte, a žádný způsob, jak aktualizovat vaši kopii, když UnloadTextPage proběhne někde jinde ve vašem kódu

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;    // handle FPDFText_LoadPage, cachovaný v FTextPage
    Pdf.SetText(0, 'Amended Clause 4.2');
    // SetText už zavřel RawHandle a nastavil Pdf.TextPage zpět na nil.
    // Volání libovolné funkce FPDFText_* proti staré hodnotě se nyní dotýká
    // handlu, který PDFium už uvolnil — nedefinované chování, ne chyba, kterou
    // byste zachytili kontrolou nil
    StaleCount := FPDFText_CountChars(RawHandle);
  finally
    Pdf.Free;
  end;
end;

Použití handle poté, co na něm proběhlo FPDFText_ClosePage, je nedefinované chování v samotném PDFium, ne konvence PDFiumPas, kterou byste si mohli dovolit ignorovat — může vrátit poslední známá data, nevrátit nic, nebo spadnout proces, a co z toho se stane na daném sestavení, není něco, na čem by kód aplikace měl záviset. Bezpečné pravidlo je úzké: přečtěte si Pdf.TextPage čerstvě, těsně před voláním FPDFText_*, které jej potřebuje, a nikdy nedržte kopii přes příkaz, který by mohl stránku upravit

Dávkujte své úpravy, pak se dotažte jednou

Nic z tohoto neznamená, že každé volání AddText nebo RemoveObject potřebuje hned po sobě obranný textový dotaz pro kontrolu výsledku. Každá editační metoda už platí náklad za zavření textové stránky jednou; dotazovat se po každé jednotlivé úpravě uvnitř smyčky platí tento náklad znovu bez žádného přínosu, protože FPDFText_LoadPage při každém spuštění znovu prochází celý proud obsahu

Editační metody TPdf jako AddText, SetText a RemoveObject zavolají UnloadTextPage před UpdatePage, takže další dotaz Delphi Text, FindFirst nebo GetWebLinks znovu postaví FPDF_TEXTPAGE z upraveného obsahu
Každá zabalená úprava nejprve upustí zastaralou textovou stránku a teprve potom generuje obsah; další textový dotaz pak znovu postaví FPDF_TEXTPAGE automaticky
var
  Pdf: TPdf;
  I: Integer;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.FileName := 'watermarked.pdf';
    Pdf.Active := True;
    Pdf.PageNumber := 1;

    // Odstraňte každý textový objekt, který vypadá jako koncept vodoznaku. Každé
    // volání RemoveObject samo o sobě cache zneplatní, takže
    // mezi iteracemi není potřeba nic ručně obnovovat
    for I := Pdf.ObjectCount - 1 downto 0 do
      if (Pdf.ObjectType[I] = otText) and (Pdf.ObjectBounds[I].Top > 700) then
        Pdf.RemoveObject(I, True);

    // Dotážete se jednou, až je celá dávka hotová, ne po každém odstranění
    if Pdf.FindFirst('DRAFT') < 0 then
      ShowMessage('Watermark cleared');
  finally
    Pdf.Free;
  end;
end;

Stejná logika dávkování platí konkrétně i pro stav hledání. FindNext a FindPrevious pokračují v relaci zahájené FindFirst, a tato relace se strhne přes UnloadTextPage spolu se vším ostatním, takže volání FindNext znovu po úpravě — místo opětovného volání FindFirst — vyvolá výjimku místo toho, aby tiše obnovila hledání proti obsahu, který už neexistuje. Berte jakoukoli úpravu jako tvrdou hranici jak pro textový obsah, tak pro pozici hledání, a nechte jedno čerstvé FindFirst na druhé straně vašich úprav hledání znovu zvednout

Zkopírování surového handle FPDF_TEXTPAGE z vlastnosti TPdf TextPage a volání FPDFText_CountChars na něm po SetText nechá kód Delphi používat PDFium handle už uvolněný, což je nedefinované chování
Zkopírovaná hodnota FPDF_TEXTPAGE dál ukazuje na handle, který editační cesta už zavřela; čtěte místo toho Pdf.TextPage čerstvé těsně před každým nezabaleným voláním FPDFText_*

Kam to zapadá s extrakcí a prací s anotacemi

Obyčejná extrakce textu — čtení textu stránky bez čehokoli měnění — na nic z tohoto nikdy nenarazí, protože nic neinvaliduje handle, kterého se žádná úprava nedotkla. Jak fungují Text, obdélníky znaků a hranice slov na nezměněné stránce, popisuje doprovodný článek o extrakci textu s PDFiumPas, který pokrývá tuto půdu bez životního cyklu cache textové stránky, který tento článek přidává navrch

Životní cyklus cache je nejdůležitější v pracovních postupech, které upraví a pak okamžitě jednají na výsledku: otisknutí opravy a její vyhledání, redakce odstavce a potvrzení, že je pryč, nebo vyhledání fráze pro ukotvení anotace značkování hned po vložení textu poblíž ní. Ten poslední případ stojí za samostatné zmínění — anotace značkování textu s quad-points se umísťují podle obdélníků znaků přečtených z textové stránky, takže anotace postavená ze souřadnic zachycených před úpravou skončí zvýrazňující špatné místo, jakmile úprava přistane

API pro editaci a text v TPdf je součástí komponenty PDFium pro Delphi a C++Builder, a stránka produktu nese úplnou referenci metod pro editaci, extrakci a vyhledávací plochy popsané zde