Techninis straipsnis

Pasenęs tekstas po redagavimo: PDFium FPDF_TEXTPAGE podėlis

AddText pakeičia PDF puslapį, tačiau iškart po to iškviestas FindFirst gali nematyti pakeitimo ir grąžinti tuščią rezultatą. Tekstas puslapyje yra — Acrobat jį rodo — tačiau PDFiumPas TPdf komponentas laiko atskirą, iš puslapio turinio srauto vieną kartą sukurtą podėliuotą FPDF_TEXTPAGE struktūrą, kurios redagavimas savaime atgaline data neatnaujina. Užklauskite jos prieš atnaujinimą, ir perskaitysite puslapį tiksliai tokį, koks jis buvo prieš pakeitimą, o ne po jo

Kodėl PDFium iškart po redagavimo grąžina pasenusį tekstą

PDFiumPas apgaubia „Google“ PDFium atvaizdavimo variklį, skirtą Delphi ir C++Builder, o jo teksto ir redagavimo iškvietimai pasiekia dvi skirtingas šio variklio posistemes. FPDF_TEXTPAGE priklauso skaitymo pusei: FPDFText_LoadPage vieną kartą perbėga puslapio turinio srautą ir sukuria teksto puslapį — simbolių kodus, pozicijas, šrifto metriką, žodžių ribas — o PDFiumPas šią struktūrą laiko podėlyje tol, kol puslapis lieka įkeltas. Redagavimo iškvietimai, tokie kaip FPDFPage_InsertObject ar FPDFPage_GenerateContent, veikia visai kitokį atvaizdavimą — puslapio objektų ir turinio srauto grafiką, — ir PDFium savaime šių pakeitimų nepersiunčia į jau atidarytą teksto puslapį. Perkurti jį po kiekvieno redagavimo padarytų paketinį redagavimą nepriimtinai lėtą, todėl vietoje šios kainos pasirinkta taisyklė — rankeną turintis kodas ją uždaro po turinį keičiančio redagavimo, o kitas skaitymas sukuria naują

PDFium redagavimai rašo į puslapio turinio srautą, kol podėlio FPDF_TEXTPAGE lieka pakrovimo metu padaryta momentine kopija, todėl Delphi FindFirst užklausa iškart po AddText skaito iki-redagavimo puslapį ir neaptinka antspaudo
Redagavimas ir skaitymas yra dvi atskiros PDFium posistemės; podėlyje laikomasis teksto puslapis yra momentinė kopija iš įkėlimo laiko, ir joks redagavimas jo neatnaujina savaime

TPdf teksto podėlyje: FTextPage, LoadTextPage ir UnloadTextPage

TPdf podėliuotą rankeną seka viename privačiame lauke FTextPage, o jo gyvavimo ciklą apgaubia dviem metodais. LoadTextPage patikrina, ar FTextPage yra nil, ir tik tokiu atveju iškviečia FPDFText_LoadPage dabartiniam puslapiui; jei rankena jau yra, LoadTextPage ją tiesiog pakartotinai naudoja, neklausdama, ar puslapis nuo jos sukūrimo nepasikeitė. UnloadTextPage yra antroji pusė: ji uždaro natyvią rankeną per FPDFText_ClosePage, atstato FTextPage atgal į nil, taip pat panaikina podėliuotą saitynų sąrašą bei bet kokį vykstantį paieškos seansą, nes abu buvo gauti iš to paties teksto puslapio ir pasensta dėl tos pačios priežasties

Būtent dėl šio LoadTextPage pakartotinio naudojimo be tikrinimo elgesio ir svarbi iškvietimų seka. Kiekviena TPdf teksto užklausa — Text, FindFirst, GetWebLinks — pirmiausia praeina per LoadTextPage, todėl kol FTextPage vis dar laiko rankeną iš prieš redagavimą buvusios būsenos, nė vienas iš šių iškvietimų neturi jokio būdo sužinoti, kad įvyko pakeitimas. Puslapio navigacija čia niekada nebuvo rizika: UnloadPage, kuris paleidžiamas perjungiant puslapius, iš naujo įkeliant ar uždarant dokumentą, teksto puslapį kartu su pačiu puslapiu uždarydavo visada. Atviras klausimas visada buvo tik apie redagavimus, atliktus tame pačiame puslapyje, kuriame vis dar esate

Kurie PDFiumPas metodai podėlį atnaujina automatiškai

TPdf nuosavi puslapio redagavimo metodai — AddText, SetText, SetTextPositions, AddPath, RemoveObject ir InsertFormObjectFromXObject — kiekvienas iškviečia UnloadTextPage prieš iškviesdami UpdatePage (PDFium FPDFPage_GenerateContent), kad pakeitimas būtų įrašytas į turinio srautą. Iškvietus bet kurį iš jų, kitas Text, FindFirst ar GetWebLinks iškvietimas teksto puslapį atkuria iš dabartinio turinio, ir jums nereikia jokio papildomo veiksmo

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 jau uždarė podėlyje esantį teksto puslapį, todėl šis FindFirst
    // iškvietimas jį atkuria iš naujo prieš pradėdamas paiešką
    Index := Pdf.FindFirst('Reviewed by J. Alvarez');
    if Index >= 0 then
      ShowMessage('Stamp confirmed at character ' + IntToStr(Index));
  finally
    Pdf.Free;
  end;
end;

Šablonas, kuris vis dar sugenda: podėliuojama neapdorota TextPage rankena

TPdf gyvą rankeną atveria per tik skaitymui skirtą savybę TextPage — retam atvejui, kai reikia iškviesti FPDFText_* funkciją, kurios PDFiumPas dar neapgaubė. Ši išeitis yra ir vienintelė vieta, kur automatinis podėlio panaikinimas nebepadeda: kai FPDF_TEXTPAGE reikšmę iš savybės nukopijuojate į lokalų kintamąjį, PDFiumPas nebeturi jokio būdo žinoti, kad ją vis dar laikote, nei kaip atnaujinti jūsų kopiją, kai UnloadTextPage paleidžiamas kitoje jūsų kodo vietoje

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 rankena, podėliuota FTextPage
    Pdf.SetText(0, 'Amended Clause 4.2');
    // SetText jau uždarė RawHandle ir nustatė Pdf.TextPage atgal į nil.
    // Bet kokios FPDFText_* funkcijos iškvietimas prieš seną reikšmę dabar
    // liečia rankeną, kurią PDFium jau atlaisvino — neapibrėžtas elgesys,
    // ne klaida, kurią galima pagauti nil patikra
    StaleCount := FPDFText_CountChars(RawHandle);
  finally
    Pdf.Free;
  end;
end;

Rankenos naudojimas po to, kai jai buvo pritaikytas FPDFText_ClosePage, pačiame PDFium yra neapibrėžtas elgesys, o ne PDFiumPas susitarimas, kurio galima nepaisyti — ji gali grąžinti paskutinius žinomus duomenis, negrąžinti nieko arba nulaužti procesą, ir kuris iš šių variantų įvyks konkrečioje versijoje, programos kodas neturėtų numatyti iš anksto. Saugi taisyklė paprasta: Pdf.TextPage skaitykite iš naujo tiesiai prieš FPDFText_* iškvietimą, kuriam jos reikia, ir niekada nelaikykite kopijos per sakinį, galintį redaguoti puslapį

Grupuokite redagavimus, tada atlikite vieną užklausą

Tai nereiškia, kad po kiekvieno AddText ar RemoveObject iškvietimo reikia papildomos apsauginės teksto užklausos rezultatui patikrinti. Kiekvienas redagavimo metodas jau vieną kartą sumoka teksto puslapio uždarymo kainą; užklausiant po kiekvieno pavienio redagavimo cikle ta kaina sumokama dar kartą be jokios naudos, nes FPDFText_LoadPage kaskart iš naujo perbėga visą turinio srautą

TPdf redagavimo metodai, tokie kaip AddText, SetText ir RemoveObject, kviečia UnloadTextPage prieš UpdatePage, kad kita Delphi Text, FindFirst arba GetWebLinks užklausa perstatytų FPDF_TEXTPAGE iš redaguoto turinio
Kiekvienas suvyniotasis redagavimas pirmiausia meta pasenusįjį teksto puslapį, o turinį generuoja antra; kita teksto užklausa tada perstatą FPDF_TEXTPAGE automatiškai
var
  Pdf: TPdf;
  I: Integer;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.FileName := 'watermarked.pdf';
    Pdf.Active := True;
    Pdf.PageNumber := 1;

    // Pašalinkite kiekvieną teksto objektą, kuris atrodo kaip juodraščio
    // vandens ženklas. Kiekvienas RemoveObject iškvietimas jau pats savaime
    // panaikina podėlį, todėl tarp iteracijų nieko rankiniu būdu atnaujinti nereikia
    for I := Pdf.ObjectCount - 1 downto 0 do
      if (Pdf.ObjectType[I] = otText) and (Pdf.ObjectBounds[I].Top > 700) then
        Pdf.RemoveObject(I, True);

    // Užklauskite vieną kartą, kai visas paketas baigtas, o ne po kiekvieno pašalinimo
    if Pdf.FindFirst('DRAFT') < 0 then
      ShowMessage('Watermark cleared');
  finally
    Pdf.Free;
  end;
end;

Ta pati paketinio apdorojimo logika galioja ir paieškos būsenai. FindNext ir FindPrevious tęsia FindFirst pradėtą seansą, o tą seansą kartu su viskuo kitu nutraukia UnloadTextPage, todėl iškvietus FindNext po redagavimo — vietoje pakartotinio FindFirst iškvietimo — gaunama išimtis, o ne tyliai tęsiama paieška turinyje, kurio jau nebėra. Laikykite bet kokį redagavimą griežta riba ir teksto turiniui, ir paieškos pozicijai, ir tegul vienas naujas FindFirst po redagavimų vėl pradeda paiešką

Žalios FPDF_TEXTPAGE rankenėlės iškopijavimas iš TPdf TextPage savybės ir FPDFText_CountChars iškvietimas jai po SetText palieka Delphi kodą naudojantį PDFium rankenėlę, kuri jau atlaisvinta, kas yra neapibrėžta elgsena
Nukopijuota FPDF_TEXTPAGE reikšmė toliau rodo į rankeną, kurią redagavimo kelias jau uždarė; skaitykite Pdf.TextPage šviežią iškart prieš kiekvieną neapvyniotą FPDFText_* kvietimą

Kaip tai susiję su išskleidimu ir anotacijomis

Paprastas teksto išskleidimas — puslapio teksto skaitymas nieko nekeičiant — su šia problema niekada nesusiduria, nes joks veiksmas nepanaikina rankenos, kurios nepalietė joks redagavimas. Apie tai, kaip Text, simbolių stačiakampiai ir žodžių ribos veikia nepakeistame puslapyje, papasakoja susijęs straipsnis apie teksto išskleidimą su PDFiumPas, neliesdamas šiame straipsnyje pridedamo teksto puslapio podėlio gyvavimo ciklo

Podėlio gyvavimo ciklas svarbiausias darbo eigose, kai redaguojama ir iškart veikiama pagal rezultatą: uždedant taisymo žymę ir jos ieškant, ištrinant pastraipą ir patvirtinant, kad jos nebeliko, arba surandant frazę, prie kurios pritvirtinama žymėjimo anotacija, iškart įterpus tekstą šalia jos. Šis paskutinis atvejis vertas atskiro paminėjimo — keturių taškų žymėjimo anotacijos pozicionuojamos pagal simbolių stačiakampius, nuskaitytus iš teksto puslapio, todėl anotacija, sukurta iš koordinačių, užfiksuotų prieš redagavimą, po redagavimo galiausiai paryškina ne tą vietą

TPdf redagavimo ir teksto API priklauso PDFium Component Delphi ir C++Builder aplinkoms, o produkto puslapyje pateikiama pilna metodų nuoroda apie čia aprašytą redagavimo, išskleidimo ir paieškos funkcionalumą