Exit code: 0 Wall time: 12.4 seconds Output: Zastarelo besedilo po urejanju

Tehnični članak

Zastarelo besedilo po urejanju: predpomnilnik PDFium FPDF_TEXTPAGE

S AddText dodate vrstico na stran PDF z uporabo PDFiumPas, nato pa takoj pokličete FindFirst, da preverite, ali je bil žig dodan, vendar iskanje ne vrne ničesar. Besedilo je na strani — Acrobat ga prikaže — toda komponenta TPdf v PDFiumPas hrani ločeno predpomnjeno strukturo FPDF_TEXTPAGE, enkrat razčlenjeno iz vsebinskega toka strani, urejanje pa te strukture samo po sebi ne posodobi za nazaj. Če jo poizvedete, preden je osvežena, preberete stran natanko tako, kot je bila videti pred spremembo, ne po njej

Zakaj PDFium takoj po urejanju vrne zastarelo besedilo

PDFiumPas ovije Googlov pogon za izris PDFium za Delphi in C++Builder, njegovi klici za besedilo in urejanje pa dosežejo dva različna podsistema v tem pogonu. FPDF_TEXTPAGE pripada bralni strani: FPDFText_LoadPage enkrat prehodi vsebinski tok strani in zgradi besedilno stran — kode znakov, položaje, metrike pisav, meje besed — PDFiumPas pa to strukturo predpomni, dokler je stran naložena. Klici za urejanje, kot sta FPDFPage_InsertObject in FPDFPage_GenerateContent, delujejo na povsem drugačni predstavitvi, grafu predmetov strani in vsebinskega toka, PDFium pa teh sprememb sam ne potisne v že odprto besedilno stran. Ponovna gradnja po vsakem urejanju bi paketno urejanje naredila nesprejemljivo počasno, zato zasnova ta strošek zamenja za pravilo — kdor ima ročico, jo po spremembi vsebine zapre, naslednje branje pa zgradi svežo

Urejanja PDFium zapišejo v vsebinski tok strani, medtem ko predpomnjeni FPDF_TEXTPAGE ostane posnetek ob nalaganju, tako da poizvedba FindFirst v Delphi takoj po AddText prebere stran pred urejanjem in zgreši žig
Urejanje in branje sta dve ločeni podsistemi znotraj PDFium; predpomnjena besedilna stran je posnetek iz časa nalaganja in nobeno urejanje je ne osveži samo po sebi

V besedilnem predpomnilniku TPdf: FTextPage, LoadTextPage in UnloadTextPage

TPdf predpomnjeno ročico spremlja v enem zasebnem polju, FTextPage, njen življenjski cikel pa ovije v dve metodi. LoadTextPage preveri, ali je FTextPage nil, in samo v tem primeru pokliče FPDFText_LoadPage za trenutno stran; če ročica že obstaja, jo LoadTextPage znova uporabi, ne da bi preverila, ali se je stran od njene izdelave spremenila. UnloadTextPage je druga polovica: izvorno ročico zapre z FPDFText_ClosePage, FTextPage znova nastavi na nil ter odstrani tudi predpomnjen seznam spletnih povezav in morebitno tekočo sejo iskanja, saj oba izhajata iz iste besedilne strani in iz istega razloga zastarata

Prav vedenje metode LoadTextPage, ki ročico znova uporabi brez preverjanja, pojasni, zakaj je zaporedje pomembno. Vsaka poizvedba besedila v TPdfText, FindFirst, GetWebLinks — najprej poteka skozi LoadTextPage, zato dokler FTextPage še vsebuje ročico izpred urejanja, noben od teh klicev ne more vedeti, da se je zgodila sprememba. Krmarjenje po straneh tu nikoli ni bilo tveganje: UnloadPage, ki se izvede pri menjavi strani, ponovnem nalaganju in zapiranju dokumenta, je vedno zaprl besedilno stran skupaj s samo stranjo. Odprto vprašanje so bila vedno urejanja strani, na kateri se še vedno nahajate

Katere metode PDFiumPas samodejno osvežijo predpomnilnik

Lastne metode TPdf za urejanje strani — AddText, SetText, SetTextPositions, AddPath, RemoveObject in InsertFormObjectFromXObject — vsaka pokliče UnloadTextPage, preden pokliče UpdatePage (FPDFPage_GenerateContent v PDFium), da spremembo serializira v vsebinski tok. Pokličite katero koli od njih in naslednji klic Text, FindFirst ali GetWebLinks bo besedilno stran znova zgradil iz trenutno obstoječe vsebine, brez dodatnega klica z vaše strani

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 je predpomnjeno besedilno stran že zaprl, zato ta klic
    // FindFirst pred iskanjem zgradi svežo
    Index := Pdf.FindFirst('Reviewed by J. Alvarez');
    if Index >= 0 then
      ShowMessage('Stamp confirmed at character ' + IntToStr(Index));
  finally
    Pdf.Free;
  end;
end;

Vzorec, ki se še vedno zlomi: predpomnjenje surove ročice TextPage

TPdf živo ročico razkrije prek lastnosti TextPage, ki je samo za branje, za redek primer, ko morate poklicati funkcijo FPDFText_*, ki je PDFiumPas ne ovija. Ta izhod v sili je tudi edino mesto, kjer samodejna razveljavitev ne more pomagati: ko vrednost FPDF_TEXTPAGE iz lastnosti kopirate v lokalno spremenljivko, PDFiumPas ne more vedeti, da jo še vedno držite, niti ne more posodobiti vaše kopije, ko se UnloadTextPage izvede drugje v vaši kodi

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;    // ročica FPDFText_LoadPage, predpomnjena v FTextPage
    Pdf.SetText(0, 'Amended Clause 4.2');
    // SetText je RawHandle že zaprl in Pdf.TextPage nastavil nazaj na nil.
    // Klic katerekoli funkcije FPDFText_* proti stari vrednosti zdaj dotakne
    // ročice, ki jo je PDFium že sprostil — nedoločeno vedenje, ne hrošč,
    // ki bi ga lahko ujeli s preverjanjem nil
    StaleCount := FPDFText_CountChars(RawHandle);
  finally
    Pdf.Free;
  end;
end;

Uporaba ročice po tem, ko se je nad njo izvedel FPDFText_ClosePage, je nedoločeno vedenje v samem PDFium, ne konvencija PDFiumPas, ki bi jo lahko prezrli — lahko vrne zadnje znane podatke, ne vrne ničesar ali sesuje proces, na to, kaj se bo zgodilo v določeni gradnji, pa se programska koda ne sme zanašati. Varno pravilo je ozko: tik pred klicem FPDFText_*, ki jo potrebuje, na novo preberite Pdf.TextPage in nikoli ne hranite kopije čez stavek, ki bi lahko uredil stran

Paketno izvedite urejanja, nato poizvedite enkrat

To ne pomeni, da vsak klic AddText ali RemoveObject potrebuje takojšnjo obrambno poizvedbo besedila za preverjanje rezultata. Vsaka metoda za urejanje že plača strošek enkratnega zaprtja besedilne strani; poizvedba po vsakem posameznem urejanju v zanki ta strošek plača znova brez koristi, saj FPDFText_LoadPage ob vsakem izvajanju znova prehodi celoten vsebinski tok

Urejevalne metode TPdf, kot so AddText, SetText in RemoveObject, pokličejo UnloadTextPage pred UpdatePage, tako da naslednja poizvedba Text, FindFirst ali GetWebLinks v Delphi znova zgradi FPDF_TEXTPAGE iz urejene vsebine
Vsako ovito urejanje najprej spusti zastarelo besedilno stran in šele nato generira vsebino; naslednja besedilna poizvedba pa znova zgradi FPDF_TEXTPAGE samodejno
var
  Pdf: TPdf;
  I: Integer;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.FileName := 'watermarked.pdf';
    Pdf.Active := True;
    Pdf.PageNumber := 1;

    // Odstranite vsak besedilni objekt, ki je videti kot vodni žig osnutka. Vsak
    // klic RemoveObject predpomnilnik že sam po sebi izniči, zato med
    // iteracijami ni treba ničesar ročno osveževati
    for I := Pdf.ObjectCount - 1 downto 0 do
      if (Pdf.ObjectType[I] = otText) and (Pdf.ObjectBounds[I].Top > 700) then
        Pdf.RemoveObject(I, True);

    // Poizvedujte enkrat, po koncu celotnega paketa, ne enkrat na odstranitev
    if Pdf.FindFirst('DRAFT') < 0 then
      ShowMessage('Watermark cleared');
  finally
    Pdf.Free;
  end;
end;

Enaka paketna logika velja posebej za stanje iskanja. FindNext in FindPrevious nadaljujeta sejo, začeto z FindFirst, UnloadTextPage pa jo skupaj z vsem drugim prekine, zato ponovni klic FindNext po urejanju — namesto ponovnega klica FindFirst — sproži izjemo, namesto da bi tiho nadaljeval iskanje po vsebini, ki ne obstaja več. Vsako urejanje obravnavajte kot strogo mejo za vsebino besedila in položaj iskanja, nato pa naj en svež FindFirst na drugi strani vaših urejanj znova začne iskanje

Kopiranje surovega ročaja FPDF_TEXTPAGE iz lastnosti TextPage TPdf in klic FPDFText_CountChars nanj po SetText pusti kodo Delphi, ki uporablja ročaj PDFium, ki je že sproščen, kar je nedoločeno obnašanje
Kopirana vrednost FPDF_TEXTPAGE še naprej kaže na ročaj, ki ga je pot urejanja že zaprla; berite Pdf.TextPage sveže takoj pred vsakim neovitim klicem FPDFText_*

Kako se to umešča v pridobivanje besedila in delo z opombami

Pridobivanje navadnega besedila — branje besedila strani brez kakršne koli spremembe — se temu nikoli ne izpostavi, saj nič ne razveljavi ročice, ki se je ni dotaknilo nobeno urejanje. Za delovanje Text, pravokotnikov znakov in meja besed na nespremenjeni strani poskrbi spremljevalni članek o pridobivanju besedila s PDFiumPas, ne da bi dodal življenjski cikel predpomnilnika besedilne strani, ki ga obravnava ta članek

Življenjski cikel predpomnilnika je najpomembnejši pri potekih dela, ki stran uredijo in nato takoj ukrepajo na rezultatu: dodajo popravek in ga poiščejo, prekrijejo odstavek in preverijo, ali je izginil, ali poiščejo besedno zvezo, na katero pripnejo opombo, takoj po vstavljanju besedila v njeni bližini. Zadnji primer je vredno posebej izpostaviti — opombe z označbami quad-point so postavljene iz pravokotnikov znakov, prebranih z besedilne strani, zato opomba, zgrajena iz koordinat, zajetih pred urejanjem, po izvedeni spremembi označi napačno mesto

API-ji za urejanje in besedilo v TPdf so del komponente PDFium Component za Delphi in C++Builder, stran izdelka pa vsebuje celoten referenčni pregled metod za urejanje, pridobivanje in iskanje, ki jih obravnava ta članek