Odborný článok

Zastaraný text po úprave: vyrovnávacia pamäť FPDF_TEXTPAGE v PDFium

Zavoláte AddText, aby ste na stránku PDF pomocou PDFiumPas vytlačili riadok, potom okamžite zavoláte FindFirst, aby ste potvrdili, že pečiatka dorazila, a vyhľadávanie sa vráti prázdne. Text je na stránke — Acrobat ho zobrazí — no komponent TPdf v PDFiumPas drží samostatnú vyrovnávaciu pamäť štruktúry FPDF_TEXTPAGE, naparsovanú raz z obsahového prúdu stránky, a úprava túto štruktúru sama osebe spätne neaktualizuje. Dopytujte ju ešte pred jej obnovením a prečítate stránku presne tak, ako vyzerala pred vašou zmenou, nie po nej

Prečo PDFium hneď po úprave vráti zastaraný text?

PDFiumPas obaľuje vykresľovací engine PDFium od Googlu pre Delphi a C++Builder, a jeho volania pre text a úpravu siahajú do dvoch odlišných subsystémov vnútri tohto enginu. FPDF_TEXTPAGE patrí čítacej strane: FPDFText_LoadPage raz prejde obsahový prúd stránky a postaví textovú stránku — kódy znakov, pozície, metriky písma, hranice slov — a PDFiumPas túto štruktúru drží vo vyrovnávacej pamäti počas celej doby, kým stránka zostáva načítaná. Volania úpravy ako FPDFPage_InsertObject alebo FPDFPage_GenerateContent pracujú na úplne inej reprezentácii, na grafe objektov a obsahového prúdu stránky, a PDFium tieto zmeny sám od seba netlačí do už otvorenej textovej stránky. Znovupostavenie pri každej úprave by urobilo dávkovú úpravu neprijateľne pomalou, takže návrh túto cenu vymieňa za pravidlo namiesto toho — ktokoľvek drží handle, ho po úprave meniacej obsah zatvorí, a ďalšie čítanie postaví čerstvý

Úpravy PDFium zapisujú do obsahového toku strany, zatiaľ čo cachovaná FPDF_TEXTPAGE zostáva snímkou z času načítania, takže dotaz FindFirst Delphi hneď po AddText číta stranu pred úpravou a minie pečiatku
Úpravy a čítanie sú dva oddelené subsystemy vo vnútri PDFium; cachovaná textová strana je snapshot z času načítania a žiadna úprava ju neobnoví sama

Vnútri textovej vyrovnávacej pamäte TPdf: FTextPage, LoadTextPage, a UnloadTextPage

TPdf sleduje uložený handle v jedinom súkromnom poli, FTextPage, a obaľuje jeho životný cyklus do dvoch metód. LoadTextPage skontroluje, či je FTextPage nil, a iba v tom prípade zavolá FPDFText_LoadPage voči aktuálnej stránke; ak už handle existuje, LoadTextPage ho znova použije bez toho, aby sa pýtal, či sa stránka od jeho postavenia zmenila. UnloadTextPage je druhá polovica: zatvorí natívny handle cez FPDFText_ClosePage, nastaví FTextPage späť na nil, a tiež zahodí uložený zoznam webových odkazov a akúkoľvek prebiehajúcu reláciu vyhľadávania, keďže oba boli odvodené z tej istej textovej stránky a zastarávajú z toho istého dôvodu

Správanie znovupoužitia-bez-kontroly v LoadTextPage je presne dôvod, prečo záleží na poradí. Každý textový dopyt na TPdfText, FindFirst, GetWebLinks — prechádza najprv cez LoadTextPage, takže pokým FTextPage stále drží handle spred úpravy, žiadne z týchto volaní nemá žiadny spôsob, ako vedieť, že sa zmena stala. Navigácia stránok tu nikdy nebola rizikom: UnloadPage, ktorá beží pri prepínaní stránok, opätovnom načítaní, a zatvorení dokumentu, vždy zatvorila textovú stránku spolu so samotnou stránkou. Otvorená otázka bola vždy o úpravách aplikovaných na stránku, na ktorej stále sedíte

Ktoré metódy PDFiumPas obnovujú vyrovnávaciu pamäť automaticky?

Vlastné metódy úpravy stránky TPdfAddText, SetText, SetTextPositions, AddPath, RemoveObject, a InsertFormObjectFromXObject — každá zavolá UnloadTextPage ešte pred tým, než zavolá UpdatePage (FPDFPage_GenerateContent z PDFium), aby zmenu serializovala do obsahového prúdu. Zavolajte ktorékoľvek z nich a hneď ďalšie volanie Text, FindFirst, alebo GetWebLinks znova postaví textovú stránku z obsahu tak, ako práve stojí, bez akéhokoľvek extra volania z vašej strany

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ž zatvorila vyrovnávaciu pamäť textovej stránky, takže toto
    // volanie FindFirst ju pred vyhľadávaním znova čerstvo postaví
    Index := Pdf.FindFirst('Reviewed by J. Alvarez');
    if Index >= 0 then
      ShowMessage('Stamp confirmed at character ' + IntToStr(Index));
  finally
    Pdf.Free;
  end;
end;

Vzor, ktorý sa stále láme: ukladanie surového handlu TextPage

TPdf sprístupňuje živý handle cez vlastnosť TextPage iba na čítanie, pre vzácny prípad, keď potrebujete zavolať funkciu FPDFText_*, ktorú PDFiumPas neobalil. Táto únikový poklop je aj jediné miesto, kde automatická invalidácia nedokáže pomôcť: len čo skopírujete hodnotu FPDF_TEXTPAGE z vlastnosti do lokálnej premennej, PDFiumPas nemá žiadny spôsob, ako vedieť, že ju stále držíte, a žiadny spôsob, ako aktualizovať vašu kópiu, keď niekde inde vo vašom kóde beží UnloadTextPage

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, uložený vo FTextPage
    Pdf.SetText(0, 'Amended Clause 4.2');
    // SetText už zatvorila RawHandle a nastavila Pdf.TextPage späť na nil.
    // Volanie akejkoľvek funkcie FPDFText_* voči starej hodnote teraz siaha na
    // handle, ktorý PDFium už uvoľnil — nedefinované správanie, nie chyba, ktorú
    // by ste zachytili kontrolou na nil
    StaleCount := FPDFText_CountChars(RawHandle);
  finally
    Pdf.Free;
  end;
end;

Použitie handlu po tom, čo naň už bežal FPDFText_ClosePage, je nedefinované správanie priamo v samotnom PDFium, nie konvencia PDFiumPas, ktorú by ste sa mohli rozhodnúť ignorovať — môže vrátiť naposledy známe dáta, nevrátiť nič, alebo padnúť s procesom, a ktorá z týchto možností nastane na danom builde, nie je niečo, na čo by mal kód aplikácie spoliehať. Bezpečné pravidlo je úzke: čítajte Pdf.TextPage čerstvo, tesne pred volaním FPDFText_*, ktoré ho potrebuje, a nikdy nedržte kópiu naprieč príkazom, ktorý by mohol stránku upraviť

Dávkujte svoje úpravy, potom dopytujte raz

Nič z toho neznamená, že každé volanie AddText alebo RemoveObject potrebuje hneď po sebe obranný textový dopyt na kontrolu výsledku. Každá metóda úpravy už platí cenu za jedno zatvorenie textovej stránky; dopytovanie po každej jednotlivej úprave vnútri slučky platí túto cenu znova bez akéhokoľvek prínosu, keďže FPDFText_LoadPage pri každom behu znova prechádza celý obsahový prúd

Metódy úprav TPdf ako AddText, SetText a RemoveObject zavolajú UnloadTextPage pred UpdatePage, takže ďalší dotaz Delphi Text, FindFirst alebo GetWebLinks prestaví FPDF_TEXTPAGE z upraveného obsahu
Každá zbalená úprava najprv zahodí zastaralú textovú stranu a až potom generuje obsah; ďalší textový dotaz potom automaticky znovu vybuduje FPDF_TEXTPAGE
var
  Pdf: TPdf;
  I: Integer;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.FileName := 'watermarked.pdf';
    Pdf.Active := True;
    Pdf.PageNumber := 1;

    // Odstráni každý textový objekt, ktorý vyzerá ako vodoznak draftu. Každé
    // volanie RemoveObject už samo osebe invaliduje vyrovnávaciu pamäť, takže
    // medzi iteráciami netreba nič ručne obnovovať
    for I := Pdf.ObjectCount - 1 downto 0 do
      if (Pdf.ObjectType[I] = otText) and (Pdf.ObjectBounds[I].Top > 700) then
        Pdf.RemoveObject(I, True);

    // Dopytujte raz, po dokončení celej dávky, nie raz na každé odstránenie
    if Pdf.FindFirst('DRAFT') < 0 then
      ShowMessage('Watermark cleared');
  finally
    Pdf.Free;
  end;
end;

Rovnaká logika dávkovania platí konkrétne aj pre stav vyhľadávania. FindNext a FindPrevious pokračujú v relácii spustenej pomocou FindFirst, a táto relácia sa zruší spolu so všetkým ostatným cez UnloadTextPage, takže opätovné volanie FindNext po úprave — namiesto opätovného volania FindFirst — vyvolá výnimku namiesto toho, aby ticho pokračovalo vo vyhľadávaní voči obsahu, ktorý už neexistuje. Berte akúkoľvek úpravu ako tvrdú hranicu ako pre obsah textu, tak pre pozíciu vyhľadávania, a nechajte jeden čerstvý FindFirst na druhej strane vašich úprav vyhľadávanie znova zdvihnúť

Skopírovanie surového handle FPDF_TEXTPAGE z vlastnosti TextPage TPdf a volanie FPDFText_CountChars na ňom po SetText nechá kód Delphi používať handle PDFium, ktorý je už uvoľnený, čo je nedefinované správanie
Skopírovaná hodnota FPDF_TEXTPAGE stále míri na handle, ktorý cesta úpravy už zatvorila; namiesto toho čítajte Pdf.TextPage čerstvé bezprostredne pred každým nezabaleným volaním FPDFText_*

Kde to zapadá do práce s extrakciou a anotáciami

Obyčajná extrakcia textu — čítanie textu stránky bez akejkoľvek zmeny — sa s ničím z tohto nikdy nestretne, pretože nič neinvaliduje handle, ktorého sa žiadna úprava nedotkla. Ako fungujú Text, obdĺžniky znakov, a hranice slov na nezmenenej stránke, pokrýva sprievodný článok o extrakcii textu s PDFiumPas bez životného cyklu vyrovnávacej pamäte textovej stránky, ktorý tento článok pridáva navrch

Životný cyklus vyrovnávacej pamäte je najdôležitejší v pracovných postupoch, ktoré upravia a potom okamžite konajú na výsledku: napečiatkovanie opravy a jej vyhľadanie, redakcia odseku a potvrdenie, že je preč, alebo nájdenie frázy na ukotvenie anotácie značkovania hneď po vložení textu vedľa nej. Práve ten posledný prípad sa oplatí označiť samostatne — anotácie značkovania s bodmi štvoruholníka sú polohované z obdĺžnikov znakov prečítaných z textovej stránky, takže anotácia postavená zo súradníc zachytených pred úpravou skončí zvýrazňujúc nesprávne miesto vo chvíli, keď úprava dorazí

API pre úpravu a text v TPdf sú súčasťou komponentu PDFium pre Delphi a C++Builder, a stránka produktu nesie plnú referenciu metód pre plochy úpravy, extrakcie a vyhľadávania opísané tu