Chybová hláška zní Please load the document before using BeginDoc (Prosím, načtěte dokument před použitím BeginDoc) a téměř vždy se objeví, až když to zkoušíte podruhé. První dokument se zapíše bez problémů. Pak je stejná instance THotPDF požádána o zahájení zpracování druhého dokumentu, metoda BeginDoc vyvolá výjimku a chybová zpráva poukazuje na nenačtení dokumentu, což je přesný opak toho, o co se kód právě snaží. Nesoulad mezi příznakem a hlášením je to, co dělá tuto chybu nepříjemně vtíravou. Skutečným problémem je tu ale samotný životní cyklus komponenty, a jakmile to pochopíte, přestane být tato chyba záhadná

Instance THotPDF představuje jeden dokument, nikoli továrnu na dokumenty
Lákavým mentálním modelem je představa, že THotPDF je servisní objekt, který si jednou vytvoříte a pak do něj sypete dokumenty podobně, jako byste udržovali otevřené připojení k databázi a prováděli přes něj jeden dotaz za druhým. Tak to ale není. Instance modeluje tvorbu jediného dokumentu a její vnitřní stavový automat (state machine) vychází z předpokladu, že touto cestou projde jen jednou: od prázdného stavu, přes otevřený dokument až po uložený soubor. Volání BeginDoc otevírá tuto cestu a označuje instanci tak, že v ní aktuálně probíhá tvorba dokumentu. Metoda EndDoc vše serializuje do FileName a celý proces uzavírá. Pokud zavoláte BeginDoc znovu nad tou samou (už dokončenou) instancí, požadujete po ní, aby znovu vstoupila do stavu, který nikdy čistě neopustila. Ochranný mechanismus, který se v tu chvíli spustí, je pak shodou okolností ten, jehož chybová hláška zmiňuje načítání – protože interně se podmínky "připraveno začít" a "má načtený dokument" kontrolují společně
Takže hláška je sice zavádějící, ale ochranný mechanismus (guard) dělá svou práci. Odmítá vám dovolit zahájit nový dokument nad komponentou, která je stále přesvědčena, že se nachází uprostřed tvorby dokumentu. Řešením není tento mechanismus obcházet. Řešením je přestat znovu používat "vyčerpanou" instanci
Životní cyklus v pořadí, v jakém se musí dít
Každý dokument, který HotPDF vytváří od nuly, se řídí těmi samými čtyřmi kroky a toto pořadí je neměnné. Create alokuje komponentu. BeginDoc otevírá dokument a upevňuje strukturální volby, takže cokoli, co ovlivňuje celý soubor (velikost stránky, komprese, šifrování, název výstupního souboru), musí být nastaveno mezi Create a BeginDoc. Následně provádíte kreslení (draw). Pak metoda EndDoc zapíše bajty na disk. Free instanci uvolní. Volání pro kreslení umístěná před BeginDoc nemají žádnou stránku, na které by mohla přistát; vlastnosti celého dokumentu přiřazené až po jeho zahájení jsou pak bez reptání ignorovány
var
Pdf: THotPDF;
begin
Pdf := THotPDF.Create(nil);
try
Pdf.FileName := 'invoice.pdf';
Pdf.BeginDoc; // otevírá dokument
Pdf.CurrentPage.SetFont('Arial', [], 11);
Pdf.CurrentPage.TextOut(50, 760, 0, 'Invoice 2026-042');
Pdf.EndDoc; // zapíše invoice.pdf a dokument uzavře
finally
Pdf.Free; // jedna instance, jeden dokument
end;
end;
Čtěte to jako jednu pracovní jednotku (unit of work). Jedno Create, jedno BeginDoc, jedno EndDoc, jedno Free, jeden soubor na disku. Ve chvíli, kdy chcete druhý soubor, zahajujete novou pracovní jednotku, což znamená novou instanci
Co by mělo znamenat „znovupoužití“: pro každý soubor nová instance
Kód, který obvykle havaruje, se snaží být v ohledu na paměť příliš spořivý: sestavit komponentu jednou, v cyklu procházet dávku a volat BeginDoc a EndDoc uvnitř smyčky. Druhá iterace s chybou spadne. Funkční řešení totiž přistupuje ke každému výstupu jako k samostatnému objektu s krátkou životností. Alokační náklady na vytvoření komponenty jsou naprosto zanedbatelné v porovnání s prací, kterou představuje rozvržení a serializace PDF. Tím pádem není na recyklování instance vlastně vůbec co ušetřit
procedure WriteBatch(const Names: TArray<string>);
var
I: Integer;
Pdf: THotPDF;
begin
for I := 0 to High(Names) do
begin
Pdf := THotPDF.Create(nil); // nová instance pro každý průchod
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;
Blok try/finally sedící uvnitř cyklu je částí, kterou se při kontrole kódu vyplatí bránit. Pokud BeginDoc nebo jakékoli volání pro vykreslování vyvolá výjimku uprostřed práce na jednom dokumentu, je instance pro tuto iteraci stále uvolněna dřív, než začne další. Díky tomu jeden chybný záznam nezanechá napůl rozpracovanou komponentu viset a nezničí i zbytek běhu celé dávky. Pokud „v rámci optimalizace“ vytáhnete Create před smyčku, vracíte se rázem zpět k původní chybě, která si navíc oblékla kabát dávkového cyklu
Změna existujícího souboru je jiný vstupní bod
Existuje však ještě druhý výklad slova „znovupoužití“, který je naprosto oprávněný: nechcete prázdný dokument, chcete otevřít už existující PDF a upravit jej. Tato cesta ale vůbec nevede přes metodu BeginDoc, což je přesně důvod, proč se v té chybové hlášce mluví o načítání. Zkrátka načtete soubor, upravíte ho a uložíte pod libovolným jménem, jaké si zvolíte
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;
Metoda LoadFromFile vrací počet stránek a hodnota nula nebo nižší znamená, že načtení selhalo, proto se vyplatí to zkontrolovat ještě předtím, než se dotknete vlastnosti CurrentPage. Párování má velký význam: dokument, který jste otevřeli pomocí LoadFromFile, se ukládá metodou SaveLoadedDocument, a nikoliv párem metod BeginDoc / EndDoc (ten je určen jen pro dokumenty, které vytváříte z ničeho). Míchání těchto dvou cest je tím nejčastějším způsobem, jak zmást stejný stavový automat (state machine), který způsobil původní chybovou hlášku. Udržujte si proto tyto dva toky mentálně jasně oddělené: BeginDoc ... EndDoc dokument vytváří (creates), LoadFromFile ... SaveLoadedDocument dokument upravuje (edits)
Problém se zámkem souboru je reálný a řešením není zabíjet okna prohlížečů
Chyba se znovupoužíváním často přichází ruku v ruce s dalším problémem a oba se do sebe zamotají, protože se objevují ve stejném workflow pro „přegenerování“ souboru. Uživatel si otevře nově vygenerované PDF, nechá si ho rozečtené v aplikaci jako je Acrobat nebo Foxit, a pak spustí jeho opětovné sestavení. Metoda EndDoc se snaží zapsat na tu samou cestu, operační systém to odmítne, protože prohlížeč drží zámek pro čtení, který blokuje jakékoli procesy pro zápis. Následně dostanete chybu kvůli odepření přístupu (access-denied). Tohle je skutečný problém na úrovni zámků souborů v systému Windows (file-locking) a ne už problém stavu komponenty; proto si spíše než jen další obezličku (workaround) zaslouží reálnou odpověď
Šířící se obezlička spočívající ve výčtu oken nejvyšší úrovně (top-level windows) a zasílání zprávy WM_CLOSE všemu, jehož název připomíná PDF prohlížeč, je špatný instinkt. Překračuje to hranice procesů k zavírání oken, která váš program nevlastní, tipuje prohlížeče podle textu v titulku a může beze slova zahodit neuložené poznámky uživatele. K celému tomuto přístupu se stavte s nedůvěrou. Spolehlivou opravou je nikdy nezapisovat na cestu k souboru, kterou by mohl držet jiný proces. Proveďte serializaci do dočasného souboru ve stejném adresáři, a jakmile metoda EndDoc úspěšně doběhne, prohoďte jej (swap) na původní místo pomocí atomického přejmenování (atomic rename). Pokud má prohlížeč starý soubor stále otevřený, přejmenování buď bez problémů projde, nebo okamžitě a hlasitě selže. Vy tak můžete zobrazit naprosto jasnou chybovou hlášku a se zámkem samotným bojovat nemusíte
uses
System.SysUtils, System.IOUtils;
procedure WritePdfAtomically(const FinalPath: string);
var
Pdf: THotPDF;
TempPath: string;
begin
// Dočasný soubor ve STejNÉM adresáři jako cíl: přejmenování v rámci jednoho
// svazku NTFS vymění název atomicky, zatímco přesun přes svazky
// se zhorší na kopírování-plus-mazání a tuto záruku ztratí
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; // dočasný soubor je zde na disku kompletní
finally
Pdf.Free;
end;
// Proveďte výměnu. TFile.Move odmítá přepisovat, proto odstraňte zastaralý
// cíl jako první; pokud prohlížeč stále drží starý soubor, je to mazání,
// co selže hlasitě, dříve než se dotknete platných bajtů
if TFile.Exists(FinalPath) then
TFile.Delete(FinalPath);
TFile.Move(TempPath, FinalPath); // nebo: RenameFile(TempPath, FinalPath)
except
if TFile.Exists(TempPath) then
TFile.Delete(TempPath); // nikdy nezanechte napůl zapsaný dočasný soubor
raise;
end;
end;
K tomuto kódu patří dvě upřímné poznámky. Zápis TFile.Move i klasické RenameFile se v obou případech mapují na stejnou přejmenovávací funkci systému Windows, a to je atomické pouze tehdy, když se zdroj a cíl nacházejí na tom stejném diskovém svazku (volume). To je přesně ten důvod, proč temp soubor končí v cílovém adresáři a nikoliv někde pod TPath.GetTempPath. Dvojice mazání-pak-přesun sama o sobě jako jeden atomický krok navíc ani nefunguje: na kratičkou chvíli se ocitnete ve stavu, kdy ani jeden ze souborů neexistuje. Pro desktopovou aplikaci, která si přegenerovává nějaký svůj report, je tohle sice irelevantní, ale čtenáři, co vyžadují silnější kontrakt na tom samém svazku disku, by měli zavolat nativně Win32 funkce ReplaceFile nebo pak MoveFileEx ve spojení přímo na MOVEFILE_REPLACE_EXISTING, čímž dojde ke sloučení prohození přímo v jednom zavolání
Pro serverové provozy s vysokou zátěží, které dokumenty průběžně regenerují, je čistší disciplínou zapisovat každý výstup pod unikátním názvem (časové razítko nebo identifikátor úlohy), aby dvě souběžná spuštění nikdy nesoupeřila o jednu cestu, a staré soubory nechat mazat samostatnou politikou uchovávání dat (retention policy). Tento vzor představuje jediný řádek pojmenovací disciplíny na každý požadavek
// Jedna výstupní cesta na požadavek: dva souběžné úlohy si nikdy nekonkurují
// o stejné jméno, takže žádné přejmenovávání a žádný zámek, o který by se dalo přijít
OutName := Format('statement-%s-%s.pdf',
[CustomerId, TGUID.NewGuid.ToString.Trim(['{', '}'])]);
Pdf.FileName := TPath.Combine(OutputDir, OutName);
Identifikátor požadavku (request id) nebo úlohy (job id) funguje stejně dobře jako klíč GUID ve chvíli, kdy vám jej obalující aplikační framework sám přidělí; navíc zdarma umožňuje vystopovat název souboru zpět až k řádku v logu. V každém ohledu ale myšlenka funguje nastejno: navrhněte řešení tak, aby zapisovaný soubor patřil v okamžiku zápisu pouze vám. Zámek zmizí ne proto, že byste násilím zavřeli cizí okno, ale proto, že se v tu chvíli bajtů nedotýká nikdo jiný
Tvar opravy
Oloupejte oba problémy až ke kořenům a ukáže se, že oba se týkají respektování hranic. Chyba stavového automatu vyžaduje, abyste respektovali hranici instance: jeden THotPDF, jeden dokument, pak jej pusťte a vytvořte další. Chyba souborových zámků vyžaduje, abyste respektovali hranici souboru: zapisujte tam, kde nikdo jiný nečte, a poté výsledek přesuňte na místo. Ani jedno nevyžaduje patchování knihovny ani skriptování desktopu. Obojí vyplývá ze zacházení s každým dokumentem jako se samostatnou pracovní jednotkou (unit of work), vytvořenou čerstvě, čistě zapsanou a uvolněnou, což je stejný vzor, který dělá zbytek komponenty předvídatelným
Volání metod BeginDoc, EndDoc, LoadFromFile a SaveLoadedDocument zobrazená výše jsou součástí komponenty HotPDF pro Delphi a C++Builder