Teknisk artikel

Inaktuell text efter redigering: PDFium FPDF_TEXTPAGE-cachen

Du anropar AddText för att stämpla en rad på en PDF-sida med PDFiumPas, sedan omedelbart anropar FindFirst för att bekräfta att stämpeln landade, och sökningen kommer tillbaka tom. Texten finns på sidan — Acrobat visar den — men PDFiumPas TPdf-komponent håller en separat cachad FPDF_TEXTPAGE-struktur, tolkad en gång från sidans innehållsström, och en redigering uppdaterar inte retroaktivt den strukturen av sig själv. Fråga den innan den uppdaterats och du läser sidan exakt som den såg ut innan din ändring, inte efteråt

Varför returnerar PDFium inaktuell text direkt efter en redigering?

PDFiumPas omsluter Googles PDFium-rendreringsmotor för Delphi och C++Builder, och dess text- och redigeringsanrop når två olika delsystem inuti den motorn. FPDF_TEXTPAGE hör till lässidan: FPDFText_LoadPage går igenom sidans innehållsström en gång och bygger textsidan — teckenkoder, positioner, typsnittsmått, ordgränser — och PDFiumPas håller den strukturen cachad så länge sidan förblir laddad. Redigeringsanrop som FPDFPage_InsertObject eller FPDFPage_GenerateContent arbetar på en helt annan representation, sidans objekt- och innehållsströmsgraf, och PDFium skjuter inte in de ändringarna i en redan öppen textsida på egen hand. Att bygga om den vid varje redigering skulle göra batchredigering oacceptabelt långsam, så designen byter den kostnaden mot en regel istället — den som håller handtaget stänger det efter en innehållsändrande redigering, och nästa läsning bygger ett nytt

Inuti TPdf:s textcache: FTextPage, LoadTextPage, och UnloadTextPage

TPdf spårar det cachade handtaget i ett enda privat fält, FTextPage, och omsluter dess livscykel i två metoder. LoadTextPage kontrollerar om FTextPage är nil och, bara i det fallet, anropar FPDFText_LoadPage mot den aktuella sidan; om ett handtag redan finns återanvänder LoadTextPage det utan att fråga om sidan ändrats sedan det byggdes. UnloadTextPage är den andra halvan: den stänger det nativa handtaget med FPDFText_ClosePage, sätter FTextPage tillbaka till nil, och släpper också den cachade webblänklistan och alla pågående sökningssessioner, eftersom båda härleddes från samma textsida och blir inaktuella av samma anledning

LoadTextPage:s återanvänd-utan-kontroll-beteende är exakt varför sekvenseringen spelar roll. Varje textfråga på TPdfText, FindFirst, GetWebLinks — går genom LoadTextPage först, så så länge FTextPage fortfarande håller det förredigerade handtaget har inget av de anropen något sätt att veta att en ändring skett. Sidnavigering var aldrig risken här: UnloadPage, som körs vid sidbyten, omladdningar, och dokumentstängning, har alltid stängt textsidan tillsammans med själva sidan. Den öppna frågan handlade alltid om redigeringar tillämpade på sidan man fortfarande sitter på

Vilka PDFiumPas-metoder uppdaterar cachen automatiskt?

TPdf:s egna sidredigeringsmetoder — AddText, SetText, SetTextPositions, AddPath, RemoveObject, och InsertFormObjectFromXObject — anropar var och en UnloadTextPage innan de anropar UpdatePage (PDFiums FPDFPage_GenerateContent) för att serialisera ändringen in i innehållsströmmen. Anropa någon av dessa och det allra nästa Text-, FindFirst-, eller GetWebLinks-anropet bygger om textsidan från innehållet som det nu ser ut, utan något extra anrop krävs från din sida

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önstret som fortfarande går sönder: att cacha det råa TextPage-handtaget

TPdf exponerar det levande handtaget genom en skrivskyddad TextPage-egenskap, för det sällsynta fallet då du behöver anropa en FPDFText_*-funktion PDFiumPas inte omslutit. Den nödutgången är också det enda stället den automatiska invalideringen inte kan hjälpa till med: när du väl kopierar FPDF_TEXTPAGE-värdet ur egenskapen till en lokal variabel har PDFiumPas inget sätt att veta att du fortfarande håller i det, och inget sätt att uppdatera din kopia när UnloadTextPage körs någon annanstans i din kod

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;

Att använda ett handtag efter att FPDFText_ClosePage körts på det är odefinierat beteende i PDFium självt, inte en PDFiumPas-konvention du kan välja att ignorera — det kan returnera senast kända data, returnera ingenting, eller krascha processen, och vilket av dessa som händer i en given byggnation är inget applikationskod bör förlita sig på. Den säkra regeln är smal: läs Pdf.TextPage färskt, omedelbart innan FPDFText_*-anropet som behöver det, och håll aldrig en kopia över en sats som kan redigera sidan

Batcha dina redigeringar, fråga sedan en gång

Inget av detta betyder att varje AddText- eller RemoveObject-anrop behöver en defensiv textfråga direkt efter för att kontrollera resultatet. Varje redigeringsmetod betalar redan kostnaden för att stänga textsidan en gång; att fråga efter varje enskild redigering inuti en loop betalar den kostnaden igen utan någon nytta, eftersom FPDFText_LoadPage går igenom hela innehållsströmmen på nytt varje gång den körs

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;

Samma batchlogik gäller sökningstillstånd specifikt. FindNext och FindPrevious fortsätter en session startad av FindFirst, och den sessionen rivs ner av UnloadTextPage tillsammans med allt annat, så att anropa FindNext igen efter en redigering — istället för att anropa FindFirst igen — kastar ett undantag snarare än att tyst återuppta en sökning mot innehåll som inte längre finns. Behandla varje redigering som en hård gräns för både textinnehåll och sökposition, och låt ett färskt FindFirst på andra sidan dina redigeringar plocka upp sökningen igen

Var det här passar in med extraktions- och anteckningsarbete

Vanlig textextraktion — att läsa en sidas text utan att ändra något — stöter aldrig på något av detta, eftersom inget invaliderar ett handtag ingen redigering rört. För hur Text, teckenrektanglar, och ordgränser fungerar på en oredigerad sida täcker följeartikeln om att extrahera text med PDFiumPas den marken utan textsidecache-livscykeln den här artikeln lägger till ovanpå

Cachelivscykeln spelar störst roll i arbetsflöden som redigerar och sedan omedelbart agerar på resultatet: stämpla en korrigering och söka efter den, redigera bort ett stycke och bekräfta att det är borta, eller lokalisera en fras för att förankra en markeringsanteckning direkt efter att ha infogat text nära den. Det sista fallet är värt att flagga för sig — quad-point-markeringsanteckningar positioneras utifrån teckenrektanglar lästa från textsidan, så en anteckning byggd från koordinater fångade innan en redigering hamnar med att markera fel plats när redigeringen landar

TPdf:s redigerings- och text-API:er är en del av PDFium-komponenten för Delphi och C++Builder, och produktsidan bär den fullständiga metodreferensen för redigerings-, extraktions-, och sökytorna som täcks här