Tehnički članak

Zastarjeli tekst nakon izmjene: PDFiumova predmemorija FPDF_TEXTPAGE

Pozovete AddText kako biste pomoću PDFiumPas-a utisnuli redak na PDF stranicu, zatim odmah pozovete FindFirst da potvrdite da je pečat upisan, ali pretraga ne vraća ništa. Tekst je na stranici — Acrobat ga prikazuje — no komponenta TPdf u PDFiumPas-u čuva zasebnu predmemoriranu strukturu FPDF_TEXTPAGE, jednom raščlanjenu iz toka sadržaja stranice, a sama izmjena tu strukturu naknadno ne ažurira. Ako je upitate prije osvježavanja, pročitat ćete stranicu upravo onako kako je izgledala prije promjene, a ne nakon nje

Zašto PDFium vraća zastarjeli tekst odmah nakon izmjene?

PDFiumPas obavija Googleov mehanizam za iscrtavanje PDFium za Delphi i C++Builder, a njegovi pozivi za tekst i izmjene dosežu dva različita podsustava unutar tog mehanizma. FPDF_TEXTPAGE pripada strani za čitanje: FPDFText_LoadPage jednom prolazi kroz tok sadržaja stranice i gradi tekstualnu stranicu — kodove znakova, položaje, metriku fonta i granice riječi — a PDFiumPas tu strukturu drži u predmemoriji dok je stranica učitana. Pozivi za izmjenu poput FPDFPage_InsertObject ili FPDFPage_GenerateContent rade nad potpuno drugačijim prikazom, grafom objekata stranice i toka sadržaja, a PDFium sam ne prenosi te promjene u već otvorenu tekstualnu stranicu. Ponovna izgradnja nakon svake izmjene batch obradu učinila bi neprihvatljivo sporom, pa dizajn tu cijenu zamjenjuje pravilom — onaj tko drži ručku zatvara je nakon izmjene sadržaja, a sljedeće čitanje izgrađuje novu

Unutar tekstne predmemorije TPdf: FTextPage, LoadTextPage i UnloadTextPage

TPdf prati predmemoriranu ručku u jednom privatnom polju, FTextPage, a njezin životni ciklus obuhvaća kroz dvije metode. LoadTextPage provjerava je li FTextPage nil i samo tada poziva FPDFText_LoadPage nad trenutačnom stranicom; ako ručka već postoji, LoadTextPage je ponovno upotrebljava bez provjere je li se stranica promijenila nakon izgradnje. UnloadTextPage čini drugu polovicu: zatvara izvornu ručku pomoću FPDFText_ClosePage, ponovno postavlja FTextPage na nil te odbacuje predmemorirani popis web-poveznica i svaku aktivnu sesiju pretrage jer su oba izvedena iz iste tekstualne stranice i zastarijevaju iz istog razloga

Ponašanje metode LoadTextPage koja ponovno upotrebljava ručku bez provjere upravo je razlog zašto je redoslijed važan. Svaki tekstualni upit nad TPdfText, FindFirst, GetWebLinks — najprije prolazi kroz LoadTextPage, pa dok god FTextPage drži ručku prije izmjene, nijedan od tih poziva ne može znati da se promjena dogodila. Kretanje po stranicama ovdje nikada nije bilo rizik: UnloadPage, koji se izvršava pri promjeni stranice, ponovnom učitavanju i zatvaranju dokumenta, oduvijek je zatvarao tekstualnu stranicu zajedno sa samom stranicom. Pitanje se uvijek odnosilo na izmjene primijenjene na stranici na kojoj se još nalazite

Koje metode PDFiumPas automatski osvježavaju predmemoriju?

Vlastite metode TPdf-a za izmjenu stranice — AddText, SetText, SetTextPositions, AddPath, RemoveObject i InsertFormObjectFromXObject — svaka poziva UnloadTextPage prije poziva UpdatePage (FPDFPage_GenerateContent u PDFiumu) kako bi serijalizirala promjenu u tok sadržaja. Pozovite bilo koju od njih i sljedeći poziv Text, FindFirst ili GetWebLinks ponovno će izgraditi tekstualnu stranicu iz trenutačnog sadržaja, bez potrebe za dodatnim pozivom s vaše strane

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;

Obrazac koji i dalje puca: predmemoriranje sirove TextPage ručke

TPdf izlaže aktivnu ručku kroz svojstvo samo za čitanje TextPage, za rijedak slučaj kada morate pozvati funkciju FPDFText_* koju PDFiumPas nije obuhvatio. Taj izlaz iz apstrakcije ujedno je jedino mjesto na kojem automatska invalidacija ne može pomoći: čim vrijednost FPDF_TEXTPAGE kopirate iz svojstva u lokalnu varijablu, PDFiumPas nema načina znati da je još držite ni ažurirati vašu kopiju kada se UnloadTextPage izvrši negdje drugdje u kodu

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;

Korištenje ručke nakon što je nad njom izvršen FPDFText_ClosePage nedefinirano je ponašanje u samom PDFiumu, a ne konvencija PDFiumPas-a koju možete zanemariti — može vratiti posljednje poznate podatke, ne vratiti ništa ili srušiti proces, a aplikacijski se kod ne bi smio oslanjati na to koja će se mogućnost dogoditi u određenoj izgradnji. Sigurno je pravilo jednostavno: učitajte Pdf.TextPage iznova neposredno prije poziva FPDFText_* kojem je potreban i nikada ne držite kopiju preko naredbe koja bi mogla izmijeniti stranicu

Grupirajte izmjene, zatim upitajte jednom

Ništa od ovoga ne znači da nakon svakog poziva AddText ili RemoveObject treba odmah izvršiti zaštitni tekstualni upit radi provjere rezultata. Svaka metoda za izmjenu već jednom plaća cijenu zatvaranja tekstualne stranice; upit nakon svake pojedinačne izmjene u petlji tu cijenu plaća ponovno bez koristi jer FPDFText_LoadPage svaki put iznova prolazi kroz cijeli tok sadržaja

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;

Ista logika grupiranja posebno vrijedi za stanje pretrage. FindNext i FindPrevious nastavljaju sesiju koju je započeo FindFirst, a UnloadTextPage tu sesiju ruši zajedno sa svime ostalim, pa ponovni poziv FindNext nakon izmjene — umjesto novog poziva FindFirst — izaziva iznimku, a ne tiho nastavlja pretragu nad sadržajem koji više ne postoji. Svaku izmjenu tretirajte kao čvrstu granicu i za tekstualni sadržaj i za položaj pretrage, pa jednim novim pozivom FindFirst nakon izmjena ponovno pokrenite pretragu

Kako se ovo uklapa u izdvajanje teksta i rad s bilješkama

Izdvajanje običnog teksta — čitanje teksta stranice bez ikakvih izmjena — ne nailazi ni na jedan od ovih problema jer nijedna izmjena nije dotaknula ručku. Način rada značajki Text, pravokutnika znakova i granica riječi na neizmijenjenoj stranici objašnjen je u pratećem članku o izdvajanju teksta pomoću PDFiumPas-a, bez životnog ciklusa predmemorije tekstualne stranice koji ovaj članak dodaje

Životni ciklus predmemorije najvažniji je u tijekovima rada koji izmijene sadržaj i odmah zatim djeluju na rezultat: utisnu ispravak pa ga pretraže, redigirate odlomak i potvrdite da je nestao ili pronađete izraz za usidravanje bilješke odmah nakon umetanja teksta u njegovoj blizini. Posljednji slučaj vrijedi posebno istaknuti — bilješke s oznakama quad-point postavljaju se iz pravokutnika znakova pročitanih s tekstualne stranice, pa će bilješka izgrađena iz koordinata uhvaćenih prije izmjene nakon nje istaknuti pogrešno mjesto

API-ji za izmjenu i tekst u TPdf-u dio su komponente PDFium Component za Delphi i C++Builder, a stranica proizvoda sadržava potpuni referentni opis metoda za izmjene, izdvajanje i pretragu obrađene ovdje