Man kalder AddText for at stemple en linje ind på en PDF-side med PDFiumPas, og kalder derefter straks FindFirst for at bekræfte, at stemplen landede, og søgningen kommer tom tilbage. Teksten er på siden — Acrobat viser den — men PDFiumPas' TPdf-komponent holder en separat cachet FPDF_TEXTPAGE-struktur, parset én gang fra sidens content-stream, og en redigering opdaterer ikke retroaktivt den struktur af sig selv. Forespørg den, før den er blevet opdateret, og man læser siden præcis som den så ud før ens ændring, ikke bagefter
Hvorfor returnerer PDFium forældet tekst lige efter en redigering?
PDFiumPas wrapper Googles PDFium-gengivelsesmotor til Delphi og C++Builder, og dens tekst- og redigeringskald rækker ind i to forskellige undersystemer inde i den motor. FPDF_TEXTPAGE tilhører læse-siden: FPDFText_LoadPage gennemgår sidens content-stream én gang og bygger tekst-siden — tegnkoder, positioner, skrifttype-metrik, ordgrænser — og PDFiumPas holder den struktur cachet, så længe siden forbliver indlæst. Redigeringskald såsom FPDFPage_InsertObject eller FPDFPage_GenerateContent opererer på en helt anden repræsentation, sidens objekt- og content-stream-graf, og PDFium skubber ikke de ændringer ind i en allerede-åben tekst-side på egen hånd. At genopbygge den ved hver redigering ville gøre batch-redigering uacceptabelt langsom, så designet bytter den omkostning for en regel i stedet — hvem der end holder handlet, lukker det efter en indholds-ændrende redigering, og næste læsning bygger et frisk et
Inde i TPdfs tekst-cache: FTextPage, LoadTextPage og UnloadTextPage
TPdf sporer det cachede handle i et enkelt privat felt, FTextPage, og pakker dets livscyklus ind i to metoder. LoadTextPage tjekker, om FTextPage er nil, og kun i så fald kalder den FPDFText_LoadPage mod den aktuelle side; hvis et handle allerede findes, genbruger LoadTextPage det uden at spørge, om siden ændrede sig, siden det blev bygget. UnloadTextPage er den anden halvdel: den lukker det native handle med FPDFText_ClosePage, sætter FTextPage tilbage til nil, og dropper også den cachede web-link-liste og enhver igangværende find-session, da begge var afledt af den samme tekst-side og bliver forældede af den samme grund
LoadTextPage's genbrug-uden-at-tjekke-opførsel er præcis, hvorfor rækkefølgen betyder noget. Hver tekst-forespørgsel på TPdf — Text, FindFirst, GetWebLinks — render gennem LoadTextPage først, så så længe FTextPage stadig holder før-redigerings-handlet, har ingen af de kald nogen måde at vide, at en ændring skete. Sidenavigation var aldrig risikoen her: UnloadPage, som kører ved side-skift, genindlæsninger og dokumentlukning, har altid lukket tekst-siden sammen med selve siden. Det åbne spørgsmål handlede altid om redigeringer anvendt på den side, man stadig sidder på
Hvilke PDFiumPas-metoder opdaterer cachen automatisk?
TPdf's egne side-redigerings-metoder — AddText, SetText, SetTextPositions, AddPath, RemoveObject og InsertFormObjectFromXObject — kalder hver UnloadTextPage, før de kalder UpdatePage (PDFiums FPDFPage_GenerateContent) for at serialisere ændringen ind i content-strømmen. Kald en af disse, og selve det næste Text-, FindFirst- eller GetWebLinks-kald genopbygger tekst-siden fra indholdet, som det nu står, uden noget ekstra kald krævet fra din side
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;
Mønsteret der stadig går i stykker: at cache det rå TextPage-handle
TPdf eksponerer det levende handle gennem en skrivebeskyttet TextPage-egenskab, til det sjældne tilfælde hvor man har brug for at kalde en FPDFText_*-funktion, PDFiumPas ikke har wrappet. Den nødudgang er også det ene sted, den automatiske invalidering ikke kan hjælpe: så snart man kopierer FPDF_TEXTPAGE-værdien ud af egenskaben ind i en lokal variabel, har PDFiumPas ingen måde at vide, at man stadig holder den, og ingen måde at opdatere ens kopi, når UnloadTextPage kører et andet sted i ens kode
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;
At bruge et handle efter FPDFText_ClosePage har kørt på det, er udefineret opførsel i selve PDFium, ikke en PDFiumPas-konvention man kan vælge at ignorere — det kan returnere den sidst-kendte data, returnere ingenting, eller crashe processen, og hvilken af de ting der sker på en given build, er ikke noget applikationskode bør afhænge af. Den sikre regel er snæver: læs Pdf.TextPage friskt, umiddelbart før det FPDFText_*-kald der har brug for det, og hold aldrig en kopi på tværs af en sætning, der måtte redigere siden
Batch dine redigeringer, forespørg så én gang
Intet af dette betyder, at hvert AddText- eller RemoveObject-kald har brug for en defensiv tekst-forespørgsel lige efter for at tjekke resultatet. Hver redigeringsmetode betaler allerede omkostningen ved at lukke tekst-siden én gang; at forespørge efter hver eneste redigering inde i en løkke betaler den omkostning igen uden gevinst, da FPDFText_LoadPage gennemgår hele content-strømmen på ny hver gang, den kører
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;
Den samme batch-logik gælder for søgetilstand specifikt. FindNext og FindPrevious fortsætter en session startet af FindFirst, og den session rives ned af UnloadTextPage sammen med alt andet, så at kalde FindNext igen efter en redigering — i stedet for at kalde FindFirst igen — kaster en undtagelse frem for i stilhed at genoptage en søgning mod indhold, der ikke længere findes. Behandl enhver redigering som en hård grænse for både tekstindhold og søgeposition, og lad ét frisk FindFirst på den anden side af ens redigeringer tage søgningen op igen
Hvor dette passer med udtræknings- og annotationsarbejde
Almindelig tekstudtrækning — at læse en sides tekst uden at ændre noget — støder aldrig ind i noget af dette, fordi intet invaliderer et handle, ingen redigering har rørt. For hvordan Text, tegn-rektangler og ordgrænser fungerer på en umodificeret side, dækker følgeartiklen om at udtrække tekst med PDFiumPas det terræn uden den tekst-side-cache-livscyklus, denne artikel lægger oven på
Cache-livscyklussen betyder mest i workflows der redigerer og derefter straks handler på resultatet: at stemple en rettelse og søge efter den, at redigere et afsnit sort og bekræfte det er væk, eller at lokalisere en frase for at forankre en markup-annotation lige efter at have indsat tekst nær den. Det sidste tilfælde er værd at flage for sig selv — quad-point-markup-annotationer positioneres fra tegn-rektangler læst fra tekst-siden, så en annotation bygget fra koordinater fanget før en redigering ender med at fremhæve det forkerte sted, når redigeringen lander
TPdf's redigerings- og tekst-API'er er en del af PDFium-komponenten til Delphi og C++Builder, og produktsiden bærer den fulde metode-reference til de redigerings-, udtræknings- og søge-flader dækket her