Pozovete AddText da preko PDFiumPas-a utisnete red teksta na PDF stranicu, a zatim odmah pozovete FindFirst da potvrdite da je žig upisan, ali pretraga ne pronađe ništa. Tekst jeste na stranici — Acrobat ga prikazuje — ali komponenta TPdf u PDFiumPas-u čuva zasebnu keširanu strukturu FPDF_TEXTPAGE, jednom izgrađenu iz sadržajnog toka stranice, pa sama izmena ne ažurira tu strukturu naknadno. Ako je upitate pre osvežavanja, pročitaćete stranicu tačno onako kako je izgledala pre izmene, a ne posle nje
Zašto PDFium odmah posle izmene vraća zastareo tekst
PDFiumPas obavija Google-ov PDFium mehanizam za prikazivanje u Delphiju i C++Builderu, a pozivi za tekst i izmene dopiru do dva različita podsistema unutar tog mehanizma. FPDF_TEXTPAGE pripada strani za čitanje: FPDFText_LoadPage jednom prolazi kroz sadržajni tok stranice i gradi tekstualnu stranicu — kodove znakova, položaje, metriku fonta i granice reči — a PDFiumPas tu strukturu kešira dok je stranica učitana. Pozivi za izmene, kao što su FPDFPage_InsertObject ili FPDFPage_GenerateContent, rade nad potpuno drugačijim prikazom, grafom objekata stranice i sadržajnog toka, a PDFium sam ne prenosi te izmene u već otvorenu tekstualnu stranicu. Ponovna izgradnja posle svake izmene učinila bi grupno uređivanje neprihvatljivo sporim, pa dizajn taj trošak menja za jedno pravilo — onaj ko drži rukohvat zatvara ga posle izmene sadržaja, a sledeće čitanje gradi nov rukohvat
Unutar tekstualnog keša TPdf-a: FTextPage, LoadTextPage i UnloadTextPage
TPdf prati keširani rukohvat u jednom privatnom polju, FTextPage, i njegov životni ciklus obuhvata kroz dve metode. LoadTextPage proverava da li je FTextPage nil i samo tada poziva FPDFText_LoadPage nad trenutnom stranicom; ako rukohvat već postoji, LoadTextPage ga ponovo koristi bez provere da li se stranica promenila od njegove izgradnje. Drugu polovinu čini UnloadTextPage: ona zatvara izvorni rukohvat pomoću FPDFText_ClosePage, ponovo postavlja FTextPage na nil, a zatim uklanja i keširanu listu veb-veza i tekuću sesiju pretrage, jer su obe izvedene iz iste tekstualne stranice i iz istog razloga postaju zastarele
Ponašanje metode LoadTextPage koja ponovo koristi rukohvat bez provere upravo objašnjava zašto je redosled važan. Svaki tekstualni upit nad TPdf — Text, FindFirst i GetWebLinks — prvo prolazi kroz LoadTextPage, pa dok FTextPage još drži rukohvat iz vremena pre izmene nijedan od tih poziva ne može znati da se promena dogodila. Navigacija kroz stranice nikada nije bila rizik: UnloadPage, koji se izvršava pri promeni stranice, ponovnom učitavanju i zatvaranju dokumenta, oduvek je zatvarao tekstualnu stranicu zajedno sa samom stranicom. Pitanje se uvek odnosilo na izmene primenjene na stranici na kojoj se i dalje nalazite
Koje PDFiumPas metode automatski osvežavaju keš
Metode TPdf-a za izmenu stranice — AddText, SetText, SetTextPositions, AddPath, RemoveObject i InsertFormObjectFromXObject — svaka poziva UnloadTextPage pre poziva UpdatePage (PDFium-ovog FPDFPage_GenerateContent) da bi izmenu upisala u sadržajni tok. Posle bilo koje od njih sledeći poziv Text, FindFirst ili GetWebLinks ponovo gradi tekstualnu stranicu iz trenutno važećeg sadržaja, bez dodatnog poziva sa vaše strane
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;
Obrazac koji se i dalje kvari: keširanje sirovog TextPage rukohvata
TPdf izlaže živi rukohvat kroz svojstvo TextPage samo za retke slučajeve kada morate da pozovete funkciju FPDFText_* koju PDFiumPas nije obavio. Taj izlaz iz apstrakcije ujedno je jedino mesto na kojem automatska invalidacija ne može pomoći: čim vrednost FPDF_TEXTPAGE kopirate iz svojstva u lokalnu promenljivu, PDFiumPas nema način da zna da je još držite niti da ažurira vašu kopiju kada se UnloadTextPage izvrši na drugom mestu u kodu
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;
Korišćenje rukohvata nakon što je nad njim izvršen FPDFText_ClosePage predstavlja nedefinisano ponašanje u samom PDFium-u, a ne konvenciju PDFiumPas-a koju možete zanemariti — poziv može vratiti poslednje poznate podatke, ne vratiti ništa ili oboriti proces, a aplikacioni kod ne sme zavisiti od toga šta će se dogoditi u određenoj izgradnji. Bezbedno pravilo je usko: čitajte Pdf.TextPage iznova neposredno pre poziva FPDFText_* kojem je potreban i nikada ne zadržavajte kopiju preko naredbe koja bi mogla izmeniti stranicu
Grupisanje izmena, pa jedno pretraživanje
To ne znači da svaki poziv AddText ili RemoveObject zahteva zaštitni tekstualni upit odmah nakon izmene radi provere rezultata. Svaka metoda za izmenu već jednom plaća cenu zatvaranja tekstualne stranice; upit posle svake pojedinačne izmene u petlji tu cenu plaća ponovo bez koristi, jer FPDFText_LoadPage svaki put iznova prolazi kroz ceo sadržajni tok
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;
Istа logika grupisanja važi i za stanje pretrage. FindNext i FindPrevious nastavljaju sesiju koju je započeo FindFirst, a UnloadTextPage tu sesiju prekida zajedno sa ostalim podacima, pa ponovni poziv FindNext posle izmene — umesto novog poziva FindFirst — izaziva izuzetak umesto da nečujno nastavi pretragu nad sadržajem koji više ne postoji. Svaku izmenu tretirajte kao čvrstu granicu i za tekstualni sadržaj i za poziciju pretrage, a zatim jednim novim FindFirst nastavite pretragu s druge strane izmena
Kako se ovo uklapa sa ekstrakcijom i anotacijama
Obično izdvajanje teksta — čitanje teksta stranice bez ikakve izmene — ne nailazi ni na jedan od ovih problema, jer nijedna izmena nije dirala rukohvat. Za objašnjenje rada Text-a, pravougaonika znakova i granica reči na neizmenjenoj stranici pogledajte prateći članak o izdvajanju teksta pomoću PDFiumPas-a, koji obrađuje tu temu bez životnog ciklusa keša tekstualne stranice dodatog u ovom članku
Životni ciklus keša najvažniji je u tokovima rada koji menjaju dokument, a zatim odmah koriste rezultat: upis korekcije i pretraga tog teksta, redigovanje pasusa i potvrda da je nestao ili pronalaženje izraza za sidrenje napomene neposredno posle umetanja teksta pored njega. Poslednji slučaj posebno vredi naglasiti — napomene za označavanje sa kvadratnim tačkama postavljaju se prema pravougaonicima znakova pročitanim iz tekstualne stranice, pa napomena izgrađena iz koordinata snimljenih pre izmene na kraju ističe pogrešno mesto
API-ji za izmenu i tekst komponente TPdf deo su proizvoda PDFium Component za Delphi i C++Builder, a stranica proizvoda sadrži potpunu referencu metoda za izmenu, izdvajanje i pretragu obrađene ovde