Napaka se glasi Please load the document before using BeginDoc in se skoraj vedno pojavi ob drugem poskusu. Prvi dokument se pravilno napiše. Nato se od iste instance THotPDF zahteva, da začne drugi dokument, sproži se BeginDoc in sporočilo kaže na nalaganje dokumenta, kar je ravno obratno od tistega, kar poskuša narediti koda. Zaradi neskladja med simptomom in sporočilom se ta napaka obdrži. Pravi predmet je življenjski cikel komponente, in ko vam to postane jasno, napaka preneha biti skrivnostna

Instanca THotPDF je en dokument, ne tovarna dokumentov
Mamljiv miselni model je, da je THotPDF storitveni predmet, ki ga enkrat zaženete in mu dovajate dokumente, tako kot bi lahko imeli odprto povezavo do baze podatkov in skozi njo izvajali poizvedbo za poizvedbo. Vendar ni tako. Instanca modelira en sam dokument, ki se gradi, njen notranji avtomat stanj (state machine) pa vsebuje predpostavko, da po poti stopi enkrat: od praznega, preko odprtega dokumenta, do shranjene datoteke. BeginDoc odpre to pot in označi instanco, da ima dokument v teku. EndDoc seralizira vse v FileName in ga zaključi. Če znova pokličete BeginDoc na isti končani instanci, od nje zahtevate, da znova vstopi v stanje, iz katerega nikoli ni čisto izstopila, varovalo (guard), ki se sproži, pa je tisto, katerega sporočilo slučajno omenja nalaganje, ker se interno pogoja "pripravljen na začetek" in "ima naložen dokument" preverjata skupaj
Torej je sporočilo zavajajoče, vendar varovalo opravlja svoje delo. Zavrača, da bi začeli nov dokument na vrhu komponente, ki še vedno verjame, da je sredi dokumenta. Popravek ni v tem, da premagate varovalo. Rešitev je, da prenehate s ponovno uporabo porabljene instance
Življenjski cikel v vrstnem redu, ki se mora zgoditi
Vsak dokument, ki ga HotPDF napiše od začetka, sledi istim štirim korakom in o vrstnem redu se ni mogoče pogajati. Create dodeli komponento. BeginDoc odpre dokument in določi strukturne izbire, tako da je treba vse, kar vpliva na celotno datoteko (velikost strani, stiskanje, šifriranje, izhodno ime datoteke), nastaviti med Create in BeginDoc. Potem rišete. Nato EndDoc zapiše bajte na disk. Free sprosti instanco. Klici risanja pred BeginDoc nimajo strani, na katero bi padli; lastnosti celotnega dokumenta, dodeljene po njem, se prezrejo brez pritožb
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;
Vzemite to kot enoto dela. En Create, en BeginDoc, en EndDoc, en Free, ena datoteka na disku. V trenutku, ko želite drugo datoteko, začnete novo enoto dela, kar pomeni novo instanco
Kaj naj bi pomenila "ponovna uporaba": sveža instanca na datoteko
Različica, ki se pokvari, poskuša biti varčna pri dodeljevanju (allocation): enkrat ustvarite komponento, krožite po paketu (loop over a batch), kličete BeginDoc in EndDoc znotraj zanke. Druga iteracija vrže napako. Različica, ki deluje, obravnava vsak izhod kot svoj kratkoživi predmet (short-lived object) in strošek dodelitve za ustvarjanje komponente je zanemarljiv poleg dela s postavitvijo in serializacijo 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;
try/finally v zanki je tisti del, ki ga je vredno braniti ob pregledu kode. Če se BeginDoc ali kateri koli klic risanja sproži (raise) na polovici enega dokumenta, se instanca te iteracije še vedno sprosti, preden se začne naslednja, tako da en slab zapis ne povzroči napol izgrajene komponente in zastrupi preostanka izvajanja. Izvlecite Create nad zanko, da ga "optimizirate", in spet ste na prvotnem hrošču, ki zdaj nosi paketno zanko (batch loop)
Spreminjanje obstoječe datoteke je drugačna vstopna točka
Obstaja drugo branje "ponovne uporabe", ki je povsem legitimno: ne želite praznega dokumenta, temveč želite odpreti PDF, ki že obstaja, in ga spremeniti. Ta pot sploh ne gre skozi BeginDoc, in to je točno tisti razlog, zakaj sporočilo o napaki omenja nalaganje. Datoteko naložite, 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;
LoadFromFile vrne število strani, vrednost nič ali manj pa pomeni, da nalaganje ni uspelo, zato je vredno preveriti, preden se dotaknete CurrentPage. Povezovanje je pomembno: dokument, ki ste ga odprli z LoadFromFile, se shrani s SaveLoadedDocument, ne s parom BeginDoc/EndDoc, ki pripada dokumentom, ki jih ustvarite iz nič. Mešanje obeh je najpogostejši način, da zmedete isti avtomat stanj (state machine), ki je povzročil prvotno napako. Miselno ločite ta dva tokova: BeginDoc ... EndDoc ustvari, LoadFromFile ... SaveLoadedDocument ureja
Problem z zaklepanjem datotek je resničen in rešitev ni ubijanje oken pregledovalnika
Napaka pri ponovni uporabi pogosto potuje z drugo pritožbo in obe se zapleteta skupaj, ker se pojavita v istem poteku dela pri obnavljanju datoteke. Uporabnik odpre PDF, ki ste ga pravkar ustvarili, ga pusti odprtega v programu Acrobat ali Foxit in nato sproži ponovno gradnjo (rebuild). EndDoc poskuša zapisati isto pot, operacijski sistem zavrne, ker ima pregledovalnik skupno branje (read share), ki blokira pisce, in dobite napako dostop zavrnjen (access-denied). To je dejansko težava z zaklepanjem datotek v sistemu Windows (file-locking issue) in ne težava s stanjem komponente, zato si zasluži pravo rešitev in ne obvoda (workaround)
Obvod, ki kroži naokoli – naštevanje oken najvišje ravni (top-level windows) in objavljanje WM_CLOSE vsem, katerih naslov izgleda kot pregledovalnik PDF – je napačen instinkt. Seže prek meja procesa za zapiranje oken, ki niso v lasti vašega programa, ugiba o pregledovalnikih na podlagi besedila naslova in lahko brez vprašanja zavrže uporabnikove neshranjene pripombe. Celoten pristop obravnavajte kot neprijeten vonj (smell). Zanesljiv popravek je, da nikoli ne pišete na pot, ki jo morda drži (hold) nek drug proces. Serailizirajte v začasno datoteko (temp file) v isti mapi in jo nato zamenjajte na mestu z atomičnim preimenovanjem (atomic rename), ko uspešno dokončate EndDoc. Če ima pregledovalnik še vedno odprto staro datoteko, preimenovanje bodisi uspe čisto, ali pa glasno ne uspe, vi pa sprožite jasno sporočilo namesto boja z zaklepanjem (lock)
uses
System.SysUtils, System.IOUtils;
procedure WritePdfAtomically(const FinalPath: string);
var
Pdf: THotPDF;
TempPath: string;
begin
// Temp file in the SAME directory as the target: a rename inside one
// NTFS volume swaps the name atomically, while a cross-volume move
// degrades to copy-plus-delete and loses that guarantee
TempPath := TPath.Combine(TPath.GetDirectoryName(FinalPath),
TGUID.NewGuid.ToString + '.pdf.tmp');
try
Pdf := THotPDF.Create(nil);
try
Pdf.FileName := TempPath;
Pdf.BeginDoc;
Pdf.CurrentPage.SetFont('Arial', [], 11);
Pdf.CurrentPage.TextOut(50, 760, 0, 'Invoice 2026-042');
Pdf.EndDoc; // the temp file is complete on disk here
finally
Pdf.Free;
end;
// Swap into place. TFile.Move refuses to overwrite, so clear a stale
// target first; if a viewer still holds the old file, the delete is
// what fails, loudly, before the good bytes are touched
if TFile.Exists(FinalPath) then
TFile.Delete(FinalPath);
TFile.Move(TempPath, FinalPath); // or: RenameFile(TempPath, FinalPath)
except
if TFile.Exists(TempPath) then
TFile.Delete(TempPath); // never strand a half-written temp file
raise;
end;
end;
Dve pošteni opombi o tej kodi. TFile.Move in klasični RenameFile se oba preslikata (map) v isto preimenovanje v sistemu Windows, ki je atomično samo, ko sta vir (source) in cilj (destination) na istem nosilcu podatkov (volume), in to je natanko razlog, zakaj gre začasna datoteka (temp file) v ciljno mapo, ne v TPath.GetTempPath. In par brisanje-nato-premikanje (delete-then-move) sam po sebi ni en atomičen korak: obstaja kratko okno, v katerem ne obstaja nobena datoteka. Za namizno aplikacijo, ki generira poročilo, je to okno nepomembno; bralci, ki potrebujejo strožjo pogodbo na istem nosilcu, lahko neposredno pokličejo Win32 ReplaceFile ali MoveFileEx z MOVEFILE_REPLACE_EXISTING, s čimer se zamenjava (swap) stisne v en klic
Za strežnik z velikim obsegom, ki nenehno regenerira dokumente, je čistejša disciplina zapisati vsak izhod pod edinstvenim imenom (časovni žig - timestamp ali id opravila - job id), tako da si dva zagona nikoli ne nasprotujeta glede ene poti, in dovoliti, da ločena politika hrambe (retention policy) počisti stare datoteke. Vzorec je ena vrstica discipline poimenovanja na zahtevo
// One output path per request: two concurrent jobs can never contend
// for the same name, so no rename dance and no lock to lose
OutName := Format('statement-%s-%s.pdf',
[CustomerId, TGUID.NewGuid.ToString.Trim(['{', '}'])]);
Pdf.FileName := TPath.Combine(OutputDir, OutName);
ID zahteve (request id) ali ID opravila (job id) deluje enako dobro kot GUID, ko vam ga okolje (framework) že preda, in naredi ime datoteke sledljivo (traceable) nazaj v vrstico dnevnika (log line) brezplačno. V obeh primerih je načelo isto: načrtujte (design) tako, da je datoteka, ki jo pišete, vaša lastna v trenutku, ko jo napišete. Zaklepanje (lock) ne izgine zato, ker ste na silo zaprli okno, temveč zato, ker se nič drugega ne dotika bajtov
Oblika popravka
Če oba problema ogolite (strip back) do njunih korenin, gre pri obeh za spoštovanje meja. Napaka avtomata stanj (state-machine error) želi, da spoštujete mejo instance (instance boundary): en THotPDF, en dokument, nato pa ga pustite in naredite drugega. Napaka zaklepanja datoteke (file-lock error) želi, da upoštevate mejo datoteke (file boundary): pišite tam, kjer nič drugega ne bere, in nato premaknite rezultat na svoje mesto. Niti eno niti drugo ne zahteva krpanja (patching) knjižnice ali skriptiranja namizja. Oboje izhaja iz obravnavanja vsakega dokumenta kot samostojne delovne enote (self-contained unit of work), ustvarjene na novo (created fresh), napisane čisto in sproščene (released), kar je isti vzorec, zaradi katerega je preostali del komponente predvidljiv
Klici BeginDoc, EndDoc, LoadFromFile in SaveLoadedDocument, prikazani tukaj, so del komponente HotPDF Component za Delphi in C++Builder