Chyba znie Please load the document before using BeginDoc (Pred použitím BeginDoc načítajte dokument) a takmer vždy sa zobrazí na druhýkrát. Prvý dokument sa zapíše v poriadku. Potom je tá istá inštancia THotPDF požiadaná, aby začala druhý, vyvolá sa BeginDoc a správa ukazuje na načítanie dokumentu, čo je opak toho, o čo sa kód snaží. Nesúlad medzi symptómom a správou je to, kvôli čomu to zostane v pamäti. Skutočným predmetom je životný cyklus komponentu a keď vám to docvakne, chyba prestane byť záhadou

Inštancia THotPDF je jeden dokument, nie továreň na dokumenty
Lákavým mentálnym modelom je, že THotPDF je servisný objekt, ktorý raz naštartujete a posielate doň dokumenty, podobne ako by ste mohli udržiavať otvorené pripojenie k databáze a spúšťať cez ňu jeden dopyt za druhým. Tak to nie je. Inštancia modeluje jediný dokument, ktorý sa práve vytvára, a jej interný stavový stroj nesie predpoklad, že prejde cestou len raz: z prázdneho stavu cez otvorený dokument k uloženému súboru. BeginDoc otvorí túto cestu a označí inštanciu, že má prebiehajúci dokument. EndDoc všetko serializuje do FileName a uzavrie to. Opätovné volanie BeginDoc na rovnakej dokončenej inštancii žiada, aby znovu vstúpila do stavu, z ktorého nikdy čisto nevyšla, a ochrana, ktorá sa spustí, je tá, ktorej správa náhodou spomína načítanie, pretože interne sa podmienky "pripravený začať" a "má načítaný dokument" kontrolujú spoločne
Takže správa je zavádzajúca, ale ochrana robí svoju prácu. Odmieta vám dovoliť začať nový dokument na vrchu komponentu, ktorý si stále myslí, že je uprostred dokumentu. Opravou nie je poraziť ochranu. Je potrebné prestať opätovne používať vyčerpanú inštanciu
Životný cyklus v poradí, v akom sa musí udiať
Každý dokument, ktorý HotPDF zapíše od nuly, sleduje rovnaké štyri kroky a poradie nie je predmetom vyjednávania. Create alokuje komponent. BeginDoc otvorí dokument a zafixuje štrukturálne voľby, takže všetko, čo ovplyvňuje celý súbor (veľkosť stránky, kompresia, šifrovanie, názov výstupného súboru), musí byť nastavené medzi Create a BeginDoc. Potom kreslíte. Potom EndDoc zapíše bajty na disk. Free uvoľní inštanciu. Volania kreslenia umiestnené pred BeginDoc nemajú žiadnu stránku, na ktorej by mohli pristáť; vlastnosti celého dokumentu priradené za ním sú ignorované bez sťažovania
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;
Prečítajte si to ako jednotku práce. Jedno Create, jedno BeginDoc, jedno EndDoc, jedno Free, jeden súbor na disku. V momente, keď chcete druhý súbor, začínate novú jednotku práce, čo znamená novú inštanciu
Čo by "opätovné použitie" malo znamenať: nová inštancia pre každý súbor
Verzia, ktorá sa zlomí, sa snaží byť šetrná k alokácii: vytvorí komponent raz, prejde cez dávku v slučke, zavolá BeginDoc a EndDoc vnútri slučky. Druhá iterácia zlyhá. Verzia, ktorá funguje, zaobchádza s každým výstupom ako s vlastným krátko žijúcim objektom a náklady na alokáciu vytvorenia komponentu sú triviálne v porovnaní s prácou na usporiadaní a serializácii PDF, takže neexistuje nič, čo by sa dalo ušetriť hromadením inštancií
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 sediaci vnútri slučky je tou časťou, ktorú sa oplatí brániť pri revízii. Ak BeginDoc alebo akékoľvek volanie na kreslenie vyvolá chybu v polovici jedného dokumentu, inštancia tejto iterácie je stále uvoľnená pred začiatkom ďalšej, takže jeden zlý záznam nezanechá napoly vytvorený komponent a neotrávi zvyšok behu. Ak presuniete Create nad slučku, aby ste to "optimalizovali", ste späť pri pôvodnej chybe, ktorá má teraz na sebe dávkovú slučku
Úprava existujúceho súboru je iný vstupný bod
Existuje druhé chápanie "opätovného použitia", ktoré je úplne legitímne: nechcete prázdny dokument, chcete otvoriť PDF, ktoré už existuje, a upraviť ho. Táto cesta vôbec neprechádza cez BeginDoc, čo je presne dôvod, prečo chybová správa spomína načítanie. Načítate súbor, upravíte ho a uložíte pod akýmkoľvek menom, ktoré si vyberiete
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 vráti počet stránok a hodnota nula alebo menej znamená, že načítanie zlyhalo, takže stojí za to skontrolovať to predtým, ako sa dotknete CurrentPage. Párovanie je dôležité: dokument, ktorý ste otvorili pomocou LoadFromFile, sa uloží pomocou SaveLoadedDocument, nie pomocou páru BeginDoc/EndDoc, ktorý patrí dokumentom, ktoré vytvárate z ničoho. Miešanie týchto dvoch postupov je najčastejším spôsobom, ako zmiasť ten istý stavový stroj, ktorý vyprodukoval pôvodnú chybu. Udržujte tieto dva toky mentálne oddelené: BeginDoc ... EndDoc vytvára, LoadFromFile ... SaveLoadedDocument upravuje
Problém so zámkom súboru je skutočný a riešením nie je zabíjanie okien prehliadača
Chyba z opätovného použitia často cestuje s druhou sťažnosťou a obe sa prepletajú, pretože sa objavujú v rovnakom pracovnom postupe pregenerovania súboru. Používateľ otvorí PDF, ktoré ste práve vytvorili, nechá ho otvorené v Acrobat alebo Foxit a potom spustí opätovné zostavenie. EndDoc sa pokúsi zapísať na rovnakú cestu, operačný systém to odmietne, pretože prehliadač drží zdieľanie pre čítanie, ktoré blokuje zapisovateľov, a dostanete chybu prístup odopretý. Toto je skutočne problém so zamykaním súborov vo Windows a nie problém so stavom komponentu, a zaslúži si skutočnú odpoveď namiesto obchádzky
Obchádzka, ktorá koluje, enumeruje okná najvyššej úrovne a posiela WM_CLOSE každému, koho nadpis vyzerá ako prehliadač PDF, čo je nesprávny inštinkt. Siaha to cez hranice procesov a zatvára okná, ktoré váš program nevlastní, háda prehliadače podľa textu nadpisu a môže zahodiť neuložené anotácie používateľa bez toho, aby sa ho spýtal. Považujte celý tento prístup za zlozvyk. Spoľahlivou opravou je nikdy nezapisovať na cestu, ktorú by mohol držať iný proces. Serializujte do dočasného súboru v rovnakom adresári a potom ho prehoďte na miesto s atomickým premenovaním po tom, ako uspeje EndDoc. Ak má prehliadač stále otvorený starý súbor, premenovanie buď čisto uspeje, alebo hlasno zlyhá, a vy zobrazíte jasnú správu namiesto boja so zámkom
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 úprimné poznámky pod čiarou k tomuto kódu. TFile.Move a klasické RenameFile obe mapujú na to isté premenovanie Windows, ktoré je atomické iba vtedy, keď zdroj a cieľ ležia na rovnakom zväzku, a presne preto ide dočasný súbor do cieľového adresára namiesto TPath.GetTempPath. A pár vymazať-potom-presunúť nie je sám o sebe jedným atomickým krokom: existuje krátke okno, v ktorom neexistuje ani jeden súbor. Pre desktopovú aplikáciu generujúcu report to okno nie je relevantné; čitatelia, ktorí potrebujú silnejší kontrakt na rovnakom zväzku, môžu volať Win32 API ReplaceFile alebo MoveFileEx s MOVEFILE_REPLACE_EXISTING priamo, čím sa prehodenie zredukuje do jedného volania
Pre server s vysokým objemom, ktorý neustále generuje dokumenty, je čistejšou disciplínou zapísať každý výstup pod jedinečným menom (časová pečiatka alebo ID úlohy), aby dve spustenia nikdy nesúperili o jednu cestu, a nechať oddelenú politiku uchovávania vyčistiť staré súbory. Vzorom je jeden riadok disciplíny pomenovania na požiadavku
// 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 požiadavky alebo ID úlohy funguje rovnako dobre ako GUID, keď vám ho okolitý rámec už odovzdá, a robí názov súboru sledovateľným späť do riadku protokolu zadarmo. Tak či onak, princíp je rovnaký: navrhnite to tak, aby súbor, ktorý zapisujete, patril v momente zápisu iba vám. Zámok nezmizne preto, že ste násilne zatvorili okno, ale preto, že sa bajtov nedotýka nič iné
Tvar opravy
Obnažte oba problémy až na ich korene a oba sú o rešpektovaní hraníc. Chyba stavového stroja chce, aby ste rešpektovali hranicu inštancie: jedno THotPDF, jeden dokument, a potom ho nechať ísť a vytvoriť ďalší. Chyba zámku súboru chce, aby ste rešpektovali hranicu súboru: zapisovať tam, kde nič iné nečíta, potom presunúť výsledok na miesto. Ani jedno nevyžaduje záplaty knižnice ani skriptovanie desktopu. Obe vyplynú z prístupu k prístupu ku každému dokumentu ako k samostatnej pracovnej jednotke, vytvorenej nanovo, zapísanej čisto a uvoľnenej, čo je ten istý vzorec, vďaka ktorému je zvyšok komponentu predvídateľný
Volania BeginDoc, EndDoc, LoadFromFile a SaveLoadedDocument zobrazené tu sú súčasťou komponentu HotPDF pre Delphi a C++Builder