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ý

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 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;

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;    // 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;

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

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;

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

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