Teknisk artikkel

Utdatert tekst etter redigering: PDFiums FPDF_TEXTPAGE-buffer

Du kaller AddText for å stemple en linje på en PDF-side med PDFiumPas, kaller deretter umiddelbart FindFirst for å bekrefte at stempelet landet, og søket kommer tomt tilbake. Teksten er på siden — Acrobat viser den — men PDFiumPas' TPdf-komponent holder en separat bufret FPDF_TEXTPAGE-struktur, parset én gang fra sidens innholdsstrøm, og en redigering oppdaterer ikke den strukturen retroaktivt på egen hånd. Spør den før den har blitt oppfrisket, og du leser siden nøyaktig slik den så ut før endringen din, ikke etterpå

Hvorfor returnerer PDFium utdatert tekst rett etter en redigering?

PDFiumPas pakker inn Googles PDFium-gjengivelsesmotor for Delphi og C++Builder, og dens tekst- og redigeringskall rekker inn i to forskjellige delsystemer inne i den motoren. FPDF_TEXTPAGE tilhører lesesiden: FPDFText_LoadPage går gjennom sidens innholdsstrøm én gang og bygger tekstsiden — tegnkoder, posisjoner, skrifttypemetrikker, ordgrenser — og PDFiumPas holder den strukturen bufret så lenge siden forblir innlastet. Redigeringskall som FPDFPage_InsertObject eller FPDFPage_GenerateContent opererer på en helt annen representasjon, sidens objekt- og innholdsstrøm-graf, og PDFium dytter ikke de endringene inn i en allerede-åpen tekstside på egen hånd. Å gjenoppbygge den ved hver redigering ville gjort batch-redigering uakseptabelt tregt, så designet bytter den kostnaden mot en regel i stedet — den som holder håndtaket, lukker det etter en innholdsendrende redigering, og den neste lesingen bygger en fersk en

Inne i TPdfs tekstbuffer: FTextPage, LoadTextPage, og UnloadTextPage

TPdf sporer det bufrede håndtaket i ett enkelt privat felt, FTextPage, og pakker inn livssyklusen dets i to metoder. LoadTextPage sjekker om FTextPage er nil og, bare i det tilfellet, kaller FPDFText_LoadPage mot den gjeldende siden; hvis et håndtak allerede finnes, gjenbruker LoadTextPage det uten å spørre om siden endret seg siden det ble bygget. UnloadTextPage er den andre halvparten: den lukker det native håndtaket med FPDFText_ClosePage, setter FTextPage tilbake til nil, og fjerner også den bufrede weblenke-listen og enhver pågående søkeøkt, ettersom begge ble utledet fra den samme tekstsiden og blir utdaterte av samme grunn

LoadTextPages gjenbruk-uten-å-sjekke-oppførsel er nøyaktig grunnen til at rekkefølgen betyr noe. Hver tekstspørring på TPdfText, FindFirst, GetWebLinks — kanaliseres gjennom LoadTextPage først, så så lenge FTextPage fortsatt holder før-redigering-håndtaket, har ingen av de kallene noen måte å vite at en endring skjedde. Sidenavigasjon var aldri risikoen her: UnloadPage, som kjører ved sidebytter, gjeninnlastinger, og dokumentlukking, har alltid lukket tekstsiden sammen med selve siden. Det åpne spørsmålet handlet alltid om redigeringer anvendt på siden man fortsatt sitter på

Hvilke PDFiumPas-metoder oppfrisker bufferen automatisk?

TPdfs egne side-redigerings-metoder — AddText, SetText, SetTextPositions, AddPath, RemoveObject, og InsertFormObjectFromXObject — kaller hver UnloadTextPage før de kaller UpdatePage (PDFiums FPDFPage_GenerateContent) for å serialisere endringen inn i innholdsstrømmen. Kall hvilken som helst av disse, og det aller neste Text-, FindFirst-, eller GetWebLinks-kallet gjenoppbygger tekstsiden fra innholdet slik det nå står, uten noe ekstra kall nødvendig 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 som fortsatt svikter: å bufre det rå TextPage-håndtaket

TPdf eksponerer det levende håndtaket gjennom en skrivebeskyttet TextPage-egenskap, for det sjeldne tilfellet der man trenger å kalle en FPDFText_*-funksjon PDFiumPas ikke har pakket inn. Den rømningsluken er også det ene stedet den automatiske ugyldiggjøringen ikke kan hjelpe: så snart man kopierer FPDF_TEXTPAGE-verdien ut av egenskapen inn i en lokal variabel, har PDFiumPas ingen måte å vite at man fortsatt holder den, og ingen måte å oppdatere kopien når UnloadTextPage kjører et annet sted i koden din

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;

Å bruke et håndtak etter at FPDFText_ClosePage har kjørt på det, er udefinert atferd i selve PDFium, ikke en PDFiumPas-konvensjon man kan velge å ignorere — det kan returnere den siste kjente dataen, returnere ingenting, eller krasje prosessen, og hvilken av de som skjer på et gitt bygg, er ikke noe applikasjonskode bør stole på. Den trygge regelen er snever: les Pdf.TextPage ferskt, umiddelbart før FPDFText_*-kallet som trenger den, og hold aldri en kopi over en setning som kan redigere siden

Batch redigeringene dine, spør så én gang

Ingenting av dette betyr at hvert AddText- eller RemoveObject-kall trenger en defensiv tekstspørring rett etter for å sjekke resultatet. Hver redigeringsmetode betaler allerede kostnaden av å lukke tekstsiden én gang; å spørre etter hver eneste redigering inne i en løkke betaler den kostnaden på nytt uten noen fordel, ettersom FPDFText_LoadPage går gjennom hele innholdsstrømmen på nytt hver gang den kjø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-logikken gjelder for søketilstand spesifikt. FindNext og FindPrevious fortsetter en økt startet av FindFirst, og den økten rives ned av UnloadTextPage sammen med alt annet, så å kalle FindNext igjen etter en redigering — i stedet for å kalle FindFirst igjen — kaster et unntak i stedet for stille å gjenoppta et søk mot innhold som ikke lenger finnes. Behandle enhver redigering som en hard grense for både tekstinnhold og søkeposisjon, og la én fersk FindFirst på den andre siden av redigeringene dine plukke opp søket igjen

Hvor dette passer inn med uttrekkings- og annoteringsarbeid

Ren tekstuttrekking — å lese en sides tekst uten å endre noe — støter aldri på noe av dette, fordi ingenting ugyldiggjør et håndtak ingen redigering har rørt. For hvordan Text, tegnrektangler, og ordgrenser fungerer på en umodifisert side, dekker følgeartikkelen om å trekke ut tekst med PDFiumPas den grunnen uten tekstside-buffer-livssyklusen denne artikkelen legger oppå

Buffer-livssyklusen betyr mest i arbeidsflyter som redigerer og deretter umiddelbart handler på resultatet: å stemple en korreksjon og søke etter den, å redigere bort et avsnitt og bekrefte at det er borte, eller å finne en frase for å forankre en markerings-annotering rett etter å ha satt inn tekst i nærheten av den. Det siste tilfellet er verdt å flagge på egen hånd — quad-point-markerings-annoteringer posisjoneres fra tegnrektangler lest av tekstsiden, så en annotering bygget fra koordinater fanget før en redigering, ender opp med å highlighte feil sted når redigeringen lander

TPdfs redigerings- og tekst-API-er er en del av PDFium-komponenten for Delphi og C++Builder, og produktsiden bærer den fulle metodereferansen for redigerings-, uttrekkings-, og søke-overflatene dekket her