Articol tehnic

Text perimat după editare: cache-ul FPDF_TEXTPAGE al PDFium

Apelați AddText pentru a ștampila o linie pe o pagină PDF cu PDFiumPas, apoi apelați imediat FindFirst pentru a confirma că ștampila a aterizat, iar căutarea revine goală. Textul este pe pagină — Acrobat îl arată — dar componenta TPdf a PDFiumPas păstrează o structură FPDF_TEXTPAGE separată în cache, analizată o dată din fluxul de conținut al paginii, iar o editare nu actualizează retroactiv acea structură de la sine. Interogați-o înainte de a fi fost reîmprospătată, și citiți pagina exact așa cum arăta înainte de schimbarea dvs., nu după

De ce returnează PDFium text perimat imediat după o editare?

PDFiumPas învelește motorul de randare PDFium al Google pentru Delphi și C++Builder, iar apelurile sale de text și editare ajung la două subsisteme diferite în interiorul acelui motor. FPDF_TEXTPAGE aparține de partea de citire: FPDFText_LoadPage parcurge fluxul de conținut al paginii o dată și construiește pagina de text — coduri de caracter, poziții, metrici de font, limite de cuvinte — iar PDFiumPas păstrează acea structură în cache atâta timp cât pagina rămâne încărcată. Apelurile de editare precum FPDFPage_InsertObject sau FPDFPage_GenerateContent operează pe o reprezentare complet diferită, graful de obiecte și flux de conținut al paginii, iar PDFium nu împinge acele schimbări într-o pagină de text deja deschisă de la sine. Reconstruirea ei la fiecare editare ar face editarea în lot inacceptabil de lentă, așa că designul schimbă acel cost pentru o regulă în schimb — oricine deține handle-ul îl închide după o editare care schimbă conținutul, iar următoarea citire construiește unul proaspăt

Editările PDFium scriu în fluxul de conținut al paginii, în timp ce FPDF_TEXTPAGE-ul în cache rămâne un instantaneu de la momentul încărcării, astfel încât o interogare Delphi FindFirst imediat după AddText citește pagina de dinaintea editării și ratează ștampila
Editarea și citirea sunt două subsisteme separate în interiorul PDFium; pagina de text în cache este un instantaneu de la momentul încărcării și nicio editare nu o împrospătează de la sine

În interiorul cache-ului de text al TPdf: FTextPage, LoadTextPage și UnloadTextPage

TPdf urmărește handle-ul din cache într-un singur câmp privat, FTextPage, și învelește ciclul său de viață în două metode. LoadTextPage verifică dacă FTextPage este nil și, doar în acel caz, apelează FPDFText_LoadPage față de pagina curentă; dacă un handle există deja, LoadTextPage îl refolosește fără a întreba dacă pagina s-a schimbat de când a fost construit. UnloadTextPage este cealaltă jumătate: închide handle-ul nativ cu FPDFText_ClosePage, setează FTextPage înapoi la nil, și elimină de asemenea lista de link-uri web din cache și orice sesiune de căutare în curs, întrucât ambele au fost derivate din aceeași pagină de text și devin perimate din același motiv

Comportamentul de refolosire-fără-verificare al LoadTextPage este exact motivul pentru care secvențierea contează. Fiecare interogare de text pe TPdfText, FindFirst, GetWebLinks — se canalizează prin LoadTextPage mai întâi, așa că atâta timp cât FTextPage încă ține handle-ul dinainte de editare, niciunul din acele apeluri nu are nicio modalitate de a ști că s-a întâmplat o schimbare. Navigarea de pagină nu a fost niciodată riscul aici: UnloadPage, care rulează la schimbări de pagină, reîncărcări și închiderea documentului, a închis întotdeauna pagina de text odată cu pagina însăși. Întrebarea deschisă a fost întotdeauna despre editările aplicate pe pagina pe care încă stați

Care metode PDFiumPas reîmprospătează cache-ul automat?

Propriile metode de editare de pagină ale TPdfAddText, SetText, SetTextPositions, AddPath, RemoveObject și InsertFormObjectFromXObject — fiecare apelează UnloadTextPage înainte de a apela UpdatePage (FPDFPage_GenerateContent al PDFium) pentru a serializa schimbarea în fluxul de conținut. Apelați oricare din acestea, iar chiar următorul apel Text, FindFirst, sau GetWebLinks reconstruiește pagina de text din conținutul așa cum stă acum, fără niciun apel suplimentar necesar din partea dvs

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 a închis deja textpage-ul memorat în cache, deci acest FindFirst
    // îl reconstruiește proaspăt înainte de a căuta
    Index := Pdf.FindFirst('Reviewed by J. Alvarez');
    if Index >= 0 then
      ShowMessage('Stamp confirmed at character ' + IntToStr(Index));
  finally
    Pdf.Free;
  end;
end;

Tiparul care încă se rupe: punerea în cache a handle-ului brut TextPage

TPdf expune handle-ul viu printr-o proprietate TextPage doar-citire, pentru cazul rar în care trebuie să apelați o funcție FPDFText_* pe care PDFiumPas nu a învelit-o. Acea trapă de evacuare este de asemenea singurul loc unde invalidarea automată nu poate ajuta: odată ce copiați valoarea FPDF_TEXTPAGE din proprietate într-o variabilă locală, PDFiumPas nu are nicio modalitate de a ști că încă o dețineți, și nicio modalitate de a vă actualiza copia atunci când UnloadTextPage rulează altundeva în codul dvs

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;    // mânerul FPDFText_LoadPage, memorat în cache în FTextPage
    Pdf.SetText(0, 'Amended Clause 4.2');
    // SetText a închis deja RawHandle și a setat Pdf.TextPage înapoi la nil.
    // Apelarea oricărei funcții FPDFText_* împotriva valorii vechi atinge acum un
    // mâner pe care PDFium l-a eliberat deja — comportament nedefinit, nu un bug pe care
    // îl puteți prinde cu o verificare de nil
    StaleCount := FPDFText_CountChars(RawHandle);
  finally
    Pdf.Free;
  end;
end;

Folosirea unui handle după ce FPDFText_ClosePage a rulat pe el este comportament nedefinit în PDFium însuși, nu o convenție PDFiumPas pe care puteți alege să o ignorați — poate returna ultimele date cunoscute, poate returna nimic, sau poate prăbuși procesul, iar care din acestea se întâmplă pe un anumit build nu este ceva de care codul aplicației ar trebui să depindă. Regula sigură este restrânsă: citiți Pdf.TextPage proaspăt, imediat înainte de apelul FPDFText_* care are nevoie de el, și nu păstrați niciodată o copie peste o instrucțiune care ar putea edita pagina

Grupați-vă editările, apoi interogați o singură dată

Nimic din toate acestea nu înseamnă că fiecare apel AddText sau RemoveObject are nevoie de o interogare defensivă de text imediat după el pentru a verifica rezultatul. Fiecare metodă de editare deja plătește costul închiderii paginii de text o dată; interogarea după fiecare editare individuală într-o buclă plătește acel cost din nou fără niciun beneficiu, întrucât FPDFText_LoadPage parcurge din nou întregul flux de conținut de fiecare dată când rulează

Metodele de editare TPdf precum AddText, SetText și RemoveObject apelează UnloadTextPage înainte de UpdatePage, astfel încât următoarea interogare Delphi Text, FindFirst sau GetWebLinks reconstruiește FPDF_TEXTPAGE din conținutul editat
Fiecare editare wrap aruncă mai întâi pagina de text învechită și generează conținutul a doua; următoarea interogare de text reconstruiește apoi FPDF_TEXTPAGE automat
var
  Pdf: TPdf;
  I: Integer;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.FileName := 'watermarked.pdf';
    Pdf.Active := True;
    Pdf.PageNumber := 1;

    // Eliminați fiecare obiect text care arată ca un filigran de proiect. Fiecare
    // apel RemoveObject invalidează deja cache-ul de unul singur, deci
    // nimic nu trebuie reîmprospătat manual între iterații
    for I := Pdf.ObjectCount - 1 downto 0 do
      if (Pdf.ObjectType[I] = otText) and (Pdf.ObjectBounds[I].Top > 700) then
        Pdf.RemoveObject(I, True);

    // Interogați o singură dată, după ce tot lotul este gata, nu o dată per eliminare
    if Pdf.FindFirst('DRAFT') < 0 then
      ShowMessage('Watermark cleared');
  finally
    Pdf.Free;
  end;
end;

Aceeași logică de grupare se aplică specific stării de căutare. FindNext și FindPrevious continuă o sesiune începută de FindFirst, iar acea sesiune este demontată de UnloadTextPage odată cu tot restul, așa că apelarea din nou a FindNext după o editare — în loc să apelați din nou FindFirst — ridică o excepție, în loc să reia silențios o căutare pe conținut care nu mai există. Tratați orice editare ca o limită dură atât pentru conținutul de text, cât și pentru poziția de căutare, și lăsați un FindFirst proaspăt de partea cealaltă a editărilor dvs. să reia căutarea

Copierea handle-ului brut FPDF_TEXTPAGE din proprietatea TextPage a TPdf-ului și apelarea FPDFText_CountChars pe el după SetText lasă codul Delphi folosind un handle PDFium deja eliberat, ceea ce este comportament nedefinit
O valoare FPDF_TEXTPAGE copiată continuă să indice către un handle pe care calea de editare l-a închis deja; citește Pdf.TextPage proaspăt imediat înainte de fiecare apel FPDFText_* neînfășurat

Unde se potrivește asta cu munca de extracție și adnotare

Extracția de text simplă — citirea textului unei pagini fără a schimba nimic — nu se lovește niciodată de nimic din toate acestea, pentru că nimic nu invalidează un handle pe care nicio editare nu l-a atins. Pentru cum funcționează Text, dreptunghiurile de caractere și limitele de cuvinte pe o pagină nemodificată, articolul complementar despre extragerea de text cu PDFiumPas acoperă acel teren fără ciclul de viață al cache-ului de pagină de text pe care îl adaugă acest articol deasupra

Ciclul de viață al cache-ului contează cel mai mult în fluxuri de lucru care editează și apoi acționează imediat pe rezultat: ștampilarea unei corecții și căutarea ei, redactarea unui paragraf și confirmarea că a dispărut, sau localizarea unei fraze pentru a ancora o adnotare de marcaj chiar după inserarea de text lângă ea. Acel ultim caz merită semnalat separat — adnotările de marcaj cu puncte quad sunt poziționate din dreptunghiuri de caractere citite de pe pagina de text, așa că o adnotare construită din coordonate capturate înainte de o editare ajunge să evidențieze locul greșit odată ce editarea aterizează

API-urile de editare și text ale TPdf fac parte din componenta PDFium pentru Delphi și C++Builder, iar pagina de produs conține referința completă de metode pentru suprafețele de editare, extracție și căutare acoperite aici