Műszaki cikk

Elavult szöveg szerkesztés után: a PDFium FPDF_TEXTPAGE gyorsítótár

Meghívod az AddText-et, hogy egy sort bélyegezz egy PDF-oldalra a PDFiumPasszal, majd azonnal meghívod a FindFirst-et, hogy megerősítsd, a bélyeg landolt, és a keresés üresen tér vissza. A szöveg ott van az oldalon — az Acrobat mutatja —, de a PDFiumPas TPdf komponense egy külön, gyorsítótárazott FPDF_TEXTPAGE struktúrát tart, egyszer elemezve az oldal tartalomfolyamából, és egy szerkesztés önmagában nem frissíti visszamenőlegesen azt a struktúrát. Kérdezd le, mielőtt frissítve lett volna, és pontosan úgy olvasod az oldalt, ahogyan a változtatásod előtt nézett ki, nem utána

Miért ad vissza a PDFium elavult szöveget közvetlenül egy szerkesztés után?

A PDFiumPas becsomagolja a Google PDFium renderelő motorját Delphihez és C++Builderhez, és a szöveg- és szerkesztéshívásai két különböző alrendszert érnek el azon a motoron belül. Az FPDF_TEXTPAGE az olvasási oldalhoz tartozik: az FPDFText_LoadPage egyszer bejárja az oldal tartalomfolyamát, és felépíti a szövegoldalt — karakterkódokat, pozíciókat, betűtípus-metrikát, szóhatárokat —, és a PDFiumPas gyorsítótárazva tartja ezt a struktúrát, ameddig az oldal betöltve marad. A szerkesztő hívások, mint az FPDFPage_InsertObject vagy az FPDFPage_GenerateContent, egy teljesen más reprezentáción dolgoznak, az oldal objektum- és tartalomfolyam-gráfján, és a PDFium nem tolja be ezeket a változásokat magától egy már nyitva lévő szövegoldalba. Ennek minden szerkesztésnél való újraépítése elfogadhatatlanul lassúvá tenné a kötegelt szerkesztést, így a tervezés ezt a költséget egy szabályra cseréli inkább — bárki is tartja a handle-t, bezárja azt egy tartalom-módosító szerkesztés után, és a következő olvasás egy frisset épít

A TPdf szöveg-gyorsítótárának belseje: FTextPage, LoadTextPage, és UnloadTextPage

A TPdf egyetlen privát mezőben, az FTextPage-ben követi nyomon a gyorsítótárazott handle-t, és két metódusba csomagolja élettartamát. A LoadTextPage ellenőrzi, hogy az FTextPage nil-e, és csak akkor hívja meg az FPDFText_LoadPage-et az aktuális oldal ellen; ha már létezik egy handle, a LoadTextPage újrahasznosítja azt anélkül, hogy megkérdezné, változott-e az oldal, mióta felépítették. Az UnloadTextPage a másik fele: bezárja a natív handle-t az FPDFText_ClosePage-dzsel, visszaállítja az FTextPage-et nil-re, és eldobja a gyorsítótárazott weblink-listát és bármely folyamatban lévő keresési munkamenetet is, mivel mindkettő ugyanabból a szövegoldalból származott, és ugyanazon okból válik elavulttá

A LoadTextPage ellenőrzés-nélküli újrahasznosítási viselkedése pontosan az, ami miatt a sorrend számít. Minden szövegkérdés a TPdf-en — Text, FindFirst, GetWebLinks — először a LoadTextPage-en keresztül tölcsérezik, így amíg az FTextPage még mindig a szerkesztés-előtti handle-t tartja, ezen hívások egyike sem tud arról, hogy egy változás történt. A navigáció soha nem volt itt kockázat: az UnloadPage, amely oldalváltásokon, újratöltéseken, és dokumentumbezáráson fut, mindig bezárta a szövegoldalt magával az oldallal együtt. A nyitott kérdés mindig az oldalra alkalmazott szerkesztésekről szólt, amelyen még mindig ülsz

Mely PDFiumPas metódusok frissítik automatikusan a gyorsítótárat?

A TPdf saját oldal-szerkesztő metódusai — AddText, SetText, SetTextPositions, AddPath, RemoveObject, és InsertFormObjectFromXObject — mindegyik meghívja az UnloadTextPage-et, mielőtt meghívná az UpdatePage-et (a PDFium FPDFPage_GenerateContent-jét), hogy szerializálja a változást a tartalomfolyamba. Hívd meg bármelyiket, és a rögtön következő Text, FindFirst, vagy GetWebLinks hívás újraépíti a szövegoldalt a tartalomból úgy, ahogyan az most áll, semmilyen extra hívás nélkül a részedről

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;

A minta, amely még mindig elromlik: a nyers TextPage handle gyorsítótárazása

A TPdf egy csak-olvasható TextPage tulajdonságon keresztül teszi elérhetővé az élő handle-t, arra a ritka esetre, amikor egy FPDFText_* függvényt kell hívnod, amit a PDFiumPas nem burkolt be. Ez a vészkijárat egyben az egyetlen hely, ahol az automatikus érvénytelenítés nem tud segíteni: amint kimásolod az FPDF_TEXTPAGE értéket a tulajdonságból egy helyi változóba, a PDFiumPasnak nincs módja tudni, hogy még mindig tartod azt, és nincs módja frissíteni a másolatodat, amikor az UnloadTextPage fut valahol máshol a kódodban

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;

Egy handle használata azután, hogy az FPDFText_ClosePage lefutott rajta, definiálatlan viselkedés magában a PDFiumban, nem egy PDFiumPas-konvenció, amit figyelmen kívül hagyhatsz — visszaadhatja az utolsó ismert adatot, semmit nem adhat vissza, vagy összeomlaszthatja a folyamatot, és hogy ezek közül melyik történik egy adott buildben, nem olyasmi, amire az alkalmazáskódnak támaszkodnia kellene. A biztonságos szabály szűk: olvasd a Pdf.TextPage-et frissen, közvetlenül azelőtt, hogy az FPDFText_* hívás igényelné, és soha ne tarts egy másolatot egy olyan utasításon át, amely szerkesztheti az oldalt

Kötegeld a szerkesztéseidet, majd kérdezz egyszer

Ebből semmi nem jelenti azt, hogy minden AddText vagy RemoveObject hívásnak szüksége lenne egy védekező szövegkérdésre rögtön utána az eredmény ellenőrzéséhez. Minden szerkesztő metódus már megfizeti a szövegoldal egyszeri bezárásának költségét; egy ciklus minden egyes szerkesztése utáni lekérdezés újra megfizeti ezt a költséget minden előny nélkül, mivel az FPDFText_LoadPage minden futáskor újra bejárja az egész tartalomfolyamot

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;

Ugyanez a kötegelési logika kifejezetten a keresési állapotra is vonatkozik. A FindNext és a FindPrevious folytat egy munkamenetet, amit a FindFirst indított, és azt a munkamenetet lebontja az UnloadTextPage, minden mással együtt, így a FindNext ismételt hívása egy szerkesztés után — a FindFirst ismételt hívása helyett — kivételt dob, ahelyett hogy csendben folytatna egy keresést olyan tartalom ellen, amely már nem létezik. Kezelj minden szerkesztést kemény határként mind a szövegtartalomhoz, mind a keresési pozícióhoz, és hagyd, hogy egy friss FindFirst a szerkesztéseid túlsó oldalán vegye fel újra a keresést

Hova illik ez a kinyerési és annotáció-munkával

A sima szövegkinyerés — egy oldal szövegének olvasása bármi megváltoztatása nélkül — soha nem ütközik ebbe egyáltalán, mert semmi nem érvényteleníti egy olyan handle-t, amit egyetlen szerkesztés sem érintett. Arra, hogyan működik a Text, a karakter-téglalapok, és a szóhatárok egy nem módosított oldalon, a szöveg PDFiumPasszal történő kinyeréséről szóló kísérőcikk tárgyalja azt a területet, e cikk szövegoldal-gyorsítótár-élettartama nélkül

A gyorsítótár élettartama leginkább olyan munkafolyamatokban számít, amelyek szerkesztenek, majd azonnal cselekszenek az eredményen: egy javítás bélyegzése és keresése, egy bekezdés kitakarása és eltűnésének megerősítése, vagy egy kifejezés megkeresése egy markup-annotáció rögzítéséhez, közvetlenül azután, hogy szöveget szúrtunk be a közelébe. Ez az utolsó eset önmagában is jelzésre érdemes — a quad-pontos markup-annotációk a szövegoldalról leolvasott karakter-téglalapokból pozicionálódnak, így egy annotáció, amelyet egy szerkesztés előtt rögzített koordinátákból építettek, a rossz helyet emeli ki, miután a szerkesztés landolt

A TPdf szerkesztő- és szöveg-API-jai a Delphihez és C++Builderhez készült PDFium Komponens részei, és a termékoldal hordozza a teljes metódus-referenciát az itt tárgyalt szerkesztési, kinyerési, és keresési felületekhez