Technical Article

Ponovna uporaba instance THotPDF med dokumenti v Delphi

Napaka se glasi Please load the document before using BeginDoc in se skoraj vedno pojavi drugič. Prvi dokument se zapiše brez težav. Nato pa ista instanca THotPDF prejme zahtevo za zagon drugega dokumenta, pri čemer se sproži izjema v BeginDoc, sporočilo pa opozarja na nalaganje dokumenta, kar je ravno nasprotno od tistega, kar koda želi doseči. Neskladje med simptomom in sporočilom je tisto, kar povzroča zmedo. Dejanski vzrok leži v življenjskem ciklu komponente in ko to enkrat razumete, napaka preneha biti skrivnostna.

Življenjski cikel dokumenta THotPDF, ki prikazuje Create, BeginDoc, EndDoc in Free za vsako izhodno datoteko
Ena instanca THotPDF se preslika v en dokument: Create, BeginDoc, risanje, EndDoc, Free.

Instanca THotPDF je en dokument, ne tovarna dokumentov

Miselni model, ki nas lahko zavede, je, da je THotPDF storitveni objekt, ki ga ustvarite enkrat in vanj pošiljate dokumente, podobno kot ohranjate odprto povezavo z bazo podatkov in prek nje izvajate poizvedbo za poizvedbo. Vendar ni tako. Instanca predstavlja gradnjo enega samega dokumenta, njena notranja naprava stanja pa predpostavlja, da gre skozi pot samo enkrat: od praznega stanja, prek odprtega dokumenta do shranjene datoteke. Klic BeginDoc odpre to pot in označi, da je dokument v teku. Klic EndDoc serializira vse v datoteko FileName in jo zapre. Ponovni klic BeginDoc na isti zaključeni instanci zahteva ponoven vstop v stanje, iz katerega nikoli ni čisto izstopila. Zaščita, ki se sproži, pa je tista, katere sporočilo omenja nalaganje, saj se notranje pogoja »pripravljen za začetek« in »ima naložen dokument« preverjata skupaj.

Sporočilo je torej zavajajoče, vendar zaščita opravlja svoje delo. Knjižnica vam preprečuje, da bi začeli nov dokument na komponenti, ki še vedno meni, da je sredi dela na dokumentu. Rešitev ni v zaobidenju zaščite, temveč v prenehanju ponovne uporabe izrabljene instance.

Življenjski cikel v vrstnem redu, kot se mora zgoditi

Vsak dokument, ki ga HotPDF zapiše iz nič, sledi enakim štirim korakom in vrstni red ni predmet razprave. Klic Create dodeli pomnilnik komponenti. Klic BeginDoc odpre dokument in določi strukturne izbire, zato je treba vse, kar vpliva na celotno datoteko (velikost strani, stiskanje, šifriranje, ime izhodne datoteke), nastaviti med klicema Create in BeginDoc. Nato rišete. Nato EndDoc zapiše bajte na disk. Klic Free sprosti instanco. Klici za risanje pred klicem BeginDoc nimajo strani, na katero bi se izvedli; lastnosti celotnega dokumenta, dodeljene po njem, pa se tiho prezrejo.

var
  Pdf: THotPDF;
begin
  Pdf := THotPDF.Create(nil);
  try
    Pdf.FileName := 'invoice.pdf';
    Pdf.BeginDoc;                        // opens the document
    Pdf.CurrentPage.SetFont('Arial', [], 11);
    Pdf.CurrentPage.TextOut(50, 760, 0, 'Invoice 2026-042');
    Pdf.EndDoc;                          // writes invoice.pdf, closes it out
  finally
    Pdf.Free;                            // one instance, one document
  end;
end;

To razumite kot enoto dela. En Create, en BeginDoc, en EndDoc, en Free, ena datoteka na disku. Takoj ko želite drugo datoteko, začnete novo enoto dela, kar pomeni novo instanco.

Kaj bi morala pomeniti »ponovna uporaba«: nova instanca za vsako datoteko

Različica, ki se sesuje, skuša varčevati z dodeljevanjem: enkrat zgradi komponento, se pomika skozi zanko serije in znotraj zanke kliče BeginDoc ter EndDoc. Druga iteracija sproži izjemo. Delujoča različica obravnava vsak izhod kot svoj kratkotrajni objekt. Strošek dodelitve pomnilnika za ustvarjanje komponente je zanemarljiv v primerjavi z delom postavitve in serializacije PDF-ja, zato s kopičenjem instance ne prihranite ničesar.

procedure WriteBatch(const Names: TArray<string>);
var
  I: Integer;
  Pdf: THotPDF;
begin
  for I := 0 to High(Names) do
  begin
    Pdf := THotPDF.Create(nil);         // new instance each pass
    try
      Pdf.FileName := Names[I] + '.pdf';
      Pdf.BeginDoc;
      Pdf.CurrentPage.SetFont('Arial', [], 12);
      Pdf.CurrentPage.TextOut(50, 760, 0, 'Statement for ' + Names[I]);
      Pdf.EndDoc;
    finally
      Pdf.Free;
    end;
  end;
end;

Uporaba try/finally znotraj zanke je del, ki ga je vredno zagovarjati pri pregledu kode. Če BeginDoc ali katerikoli klic za risanje sproži izjemo sredi enega dokumenta, se instanca te iteracije še vedno sprosti pred začetkom naslednje. Tako en slab zapis ne povzroči blokade napol zgrajene komponente in ne pokvari preostalega poteka dela. Če klic Create premaknete nad zanko zaradi »optimizacije«, se vrnete k prvotni napaki, ki je zdaj le skrita znotraj zanke.

Spreminjanje obstoječe datoteke je druga vstopna točka

Obstaja tudi drugačna razlaga »ponovne uporabe«, ki je povsem legitimna: ne želite praznega dokumenta, temveč želite odpreti obstoječi PDF in ga spremeniti. Ta pot sploh ne gre skozi BeginDoc, kar je natanko razlog, zakaj sporočilo o napaki omenja nalaganje. Naložite datoteko, jo uredite in shranite pod poljubnim imenom.

var
  Pdf: THotPDF;
  PageCount: Integer;
begin
  Pdf := THotPDF.Create(nil);
  try
    PageCount := Pdf.LoadFromFile('contract.pdf');
    if PageCount > 0 then
    begin
      Pdf.CurrentPage.SetFont('Arial', [fsBold], 10);
      Pdf.CurrentPage.TextOut(40, 30, 0, 'REVIEWED');
      Pdf.SaveLoadedDocument('contract-reviewed.pdf');
    end;
  finally
    Pdf.Free;
  end;
end;

Klic LoadFromFile vrne število strani. Vrednost nič ali manj pomeni, da nalaganje ni uspelo, zato je to vredno preveriti, preden začnete uporabljati CurrentPage. Parjenje je pomembno: dokument, ki ste ga odprli z LoadFromFile, shranite s SaveLoadedDocument in ne s parom BeginDoc/EndDoc, ki pripada dokumentom, ki jih ustvarjate iz nič. Mešanje teh dveh poti je najpogostejši način, da zmedete isto napravo stanja, ki je povzročila prvotno napako. Ta dva poteka dela imejte ločena: BeginDoc ... EndDoc ustvarja, LoadFromFile ... SaveLoadedDocument ureja.

Težava z zaklepanjem datotek je resnična in rešitev ni v zapiranju oken pregledovalnika

Napaka pri ponovni uporabi se pogosto pojavi skupaj z drugo težavo in obe se zapleteta, ker se pojavita v istem poteku dela za ponovno ustvarjanje datoteke. Uporabnik odpre PDF, ki ste ga pravkar ustvarili, ga pusti odprtega v programu Acrobat ali Foxit, logo pa sproži ponovno gradnjo. Klic EndDoc poskuša pisati na isto pot, operacijski sistem pa to zavrne, ker pregledovalnik drži pravico za branje, ki blokira pisanje, zaradi česar prejmete napako o zavrnitvi dostopa (access-denied). To je dejanska težava z zaklepanjem datotek v Windowsih in ne težava s stanjem komponente, zato si zasluži pravo rešitev namesto začasnih prilagoditev.

Občasno predlagana rešitev, ki vključuje iskanje oken najvišje ravni in pošiljanje sporočila WM_CLOSE vsem oknom, katerih naslov je podoben pregledovalniku PDF, je napačen pristop. Posega namreč čez meje procesov, da bi zaprl okna, ki niso v lasti vašega programa, ugiba o pregledovalnikih na podlagi naslovnega besedila in lahko brez vprašanja zavrže uporabnikove neshranjene pripombe. Takšen pristop označite za neprimeren. Zanesljiva rešitev je, da nikoli ne pišete na pot, ki jo morda drži drug proces. Podatke shranite v začasno datoteko v istem imeniku, nato pa jo z atomskim preimenovanjem zamenjajte na pravo mesto, ko EndDoc uspešno zaključi delo. Če ima pregledovalnik še vedno odprto staro datoteko, preimenovanje bodisi uspešno uspe bodisi jasno sporoči napako, vi pa prikažete razumljivo sporočilo, namesto da se borite z zaklepanjem.

Za visoko obremenjene strežnike, ki nenehno ustvarjajo dokumente, je čistejša rešitev zapisovanje vsakega izhoda pod edinstvenim imenom (časovni žig ali ID opravila), tako da se dva zagona nikoli ne borita za isto pot, ločena politika hrambe pa poskrbi za čiščenje starih datotek. V obeh primerih je načelo enako: načrtujte tako, da je datoteka, ki jo pišete, v trenutku pisanja izključno vaša. Zaklepanje izigyra ne zato, ker ste prisilno zaprli okno, temveč zato, ker se bajtov ne dotika nič drugega.

Oblika rešitve

Če oba problema ogolimo do njunih korenin, gre pri obeh za spoštovanje meja. Napaka naprave stanja želi, da spoštujete mejo instance: en THotPDF, en dokument, nato pa ga sprostite in ustvarite novega. Napaka zaklepanja datoteke želi, da spoštujete mejo datoteke: pišite tja, kjer nič ne bere, nato pa rezultat premaknite na pravo mesto. Nobena težava ne zahteva popravkov knjižnice ali skriptiranja namizja. Obe rešitvi izhajata iz obravnave vsakega dokumenta kot samostojne enote dela, ki se ustvari na novo, zapiše čisto in sprosti, kar je enak vzorec, ki omogoča predvidljivost preostalega dela komponente.

Klici BeginDoc, EndDoc, LoadFromFile in SaveLoadedDocument, prikazani tukaj, so del komponente HotPDF Component za Delphi in C++Builder.