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ý

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

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;

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úť

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