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; // 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;
Č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); // 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;
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
// 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;
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é generují dokumenty prakticky neustále, je lepším řešením zapisovat vždy každý výstup pod unikátním jménem (např. časovým razítkem či přes identifikátor konkrétního jobu), aby si dva souběžně běžící úkoly navzájem o tutéž cestu už nikdy nekonkurovaly. Staré soubory pak může mazat některá k tomu určená politika k udržování dat (retention policy). Tento vzor je otázkou jediného řádku k uspořádání politiky po pojmenovávání podle provedených požadavků
// 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);
Identifikátor dotazu (request id) nebo úlohy (job id) funguje úplně stejně dobře jako unikátní klíč GUID ve chvíli, kdy vám jej obalující aplikační framework přímo přidělí; k dobru navíc nabízí i to, že se takový soubor dokáže zcela zdarma zpětně sám dopátrat k řádku do protokolů logu (traceable back to a log line for free). V každém ohledu ale myšlenka funguje nastejno: designujte řešení tak, aby zapisovaný soubor patřil pouze vám v okamžiku, kdy ho zapisujete. Zámek jako takový ve skutečnosti neodejde z důvodu zavření okna cizímu programu, ale díky tomu, že na uložená bajtová data v tu chvíli zkrátka nesahá nic jiného
Tvar opravy
Očistěte oba problémy na holou dřeň k jejich kořenům a uvidíte, že oba se týkají respektování hranic. Stavový automat (state-machine) po vás požaduje respektování hranic samotné instance: jeden THotPDF na jeden jediný dokument, následně komponentu opusťte a utvořte znovu zcela novou. Chyba souborových zámků zas touží po ohleduplném přistupování k hranicím od souboru: zapisujte data zrovna tam, kde u toho neprobíhá od nikoho jiného paralelně k tomu ještě i jejich načítání, potom přesuňte obdržený výsledek rovnou na žádoucí určené cílové podloží. Ani jeden ze způsobů nepotřebuje obcházet patche knihoven ani složitě spouštět externí ovládací (scripting) příkazy do sítě v desktopu. Oba postupy tak vypadávají ven na stůl už ze samotného prostého uchopení dokumentu jakožto (self-contained) kompletní uspořádané kompaktní samostatné struktury pracovních povelů a jednotek se zařazením do čisté čerstvé a zapsané tvářnosti po následném vyclení (released), což taktéž jako způsob dodává chování složce samotné zbylé odhadnutelnosti podle norem z komponenty
Volání metod BeginDoc, EndDoc, LoadFromFile a SaveLoadedDocument zobrazená výše jsou součástí komponenty HotPDF pro Delphi a C++Builder