Fejlen lyder Please load the document before using BeginDoc, og den dukker næsten altid op anden gang. Det første dokument skrives fint. Derefter bliver den samme THotPDF-instans bedt om at starte en anden, BeginDoc kaster en undtagelse, og meddelelsen peger på at indlæse et dokument, hvilket er det modsatte af, hvad koden forsøger at gøre. Uoverensstemmelsen mellem symptomet og meddelelsen er det, der gør, at denne fejl hænger ved. Det virkelige emne er komponentens livscyklus, og når først det falder på plads, holder fejlen op med at være mystisk

En THotPDF-instans er ét dokument, ikke en dokumentfabrik
Den fristende mentale model er, at THotPDF er et serviceobjekt, som du starter op én gang og fodrer dokumenter til, på samme måde som du måske holder en databaseforbindelse åben og kører forespørgsel efter forespørgsel gennem den. Det er det ikke. En instans modellerer et enkelt dokument, der bliver bygget, og dens interne tilstandsmaskine (state machine) bærer den antagelse, at den går vejen én gang: fra tom, gennem et åbent dokument, til en gemt fil. BeginDoc åbner den vej og markerer instansen som havende et dokument i gang. EndDoc serialiserer alt til FileName og lukker den. At kalde BeginDoc igen på den samme færdige instans beder den om at genindtræde i en tilstand, den aldrig forlod rent, og den vagt (guard), der udløses, er den, hvis meddelelse tilfældigvis nævner indlæsning, fordi betingelserne "klar til at begynde" og "har et indlæst dokument" internt tjekkes sammen
Så meddelelsen er vildledende, men vagten (guard) gør sit job. Den nægter at lade dig starte et nyt dokument oven på en komponent, der stadig tror, at den er midt i et dokument. Løsningen er ikke at besejre vagten. Løsningen er at stoppe med at genbruge en udtjent instans
Livscyklussen, i den rækkefølge den skal foregå
Hvert dokument, som HotPDF skriver fra bunden, følger de samme fire trin, og rækkefølgen er ikke til forhandling. Create tildeler komponenten. BeginDoc åbner dokumentet og fastlægger de strukturelle valg, så alt, hvad der påvirker hele filen (sidestørrelse, komprimering, kryptering, outputfilnavn), skal indstilles mellem Create og BeginDoc. Derefter tegner du. Derefter skriver EndDoc bytene til disken. Free frigiver instansen. Tegnekald placeret før BeginDoc har ingen side at lande på; hele-dokument-egenskaber tildelt efter det ignoreres uden klage
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;
Læs det som arbejdsenheden (unit of work). Én Create, én BeginDoc, én EndDoc, én Free, én fil på disk. I det øjeblik du vil have en anden fil, starter du en ny arbejdsenhed, hvilket betyder en ny instans
Hvad "genbrug" bør betyde: en frisk instans pr. fil
Den version, der går i stykker, forsøger at være sparsommelig med tildeling (allocation): byg komponenten én gang, løb i en løkke over en batch, kald BeginDoc og EndDoc inde i løkken. Den anden iteration kaster (en exception). Den version, der virker, behandler hvert output som sit eget kortlivede objekt, og tildelingsomkostningen ved at oprette en komponent er triviel ved siden af arbejdet med at layoute og serialisere en PDF, så der er intet at spare ved at hamstre instansen
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, der sidder inde i løkken, er den del, der er værd at forsvare i review. Hvis BeginDoc eller ethvert tegnekald kaster (en exception) halvvejs gennem ét dokument, bliver den iterations instans stadig frigivet, før den næste begynder, så én dårlig post strander ikke en halvbygget komponent og forgifter resten af kørslen. Trækker du Create ud over løkken for at "optimere", er du tilbage ved den oprindelige fejl, der nu bærer en batchløkke
Ændring af en eksisterende fil er et andet indgangspunkt
Der er en anden læsning af "genbrug", som er helt legitim: du vil ikke have et tomt dokument, du vil åbne en PDF, der allerede eksisterer, og ændre den. Den vej går slet ikke gennem BeginDoc, hvilket præcis er grunden til, at fejlmeddelelsen nævner indlæsning. Du indlæser filen, redigerer den og gemmer under det navn, du vælger
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 returnerer sideantallet, og en værdi på nul eller derunder betyder, at indlæsningen mislykkedes, så det er værd at tjekke, før du rører ved CurrentPage. Parringen betyder noget: et dokument, du åbnede med LoadFromFile, gemmes med SaveLoadedDocument, ikke med BeginDoc/EndDoc-parret, som tilhører dokumenter, du forfatter fra ingenting. At blande de to er den mest almindelige måde at forvirre den samme tilstandsmaskine (state machine), som producerede den oprindelige fejl. Hold de to strømme mentalt adskilt: BeginDoc ... EndDoc opretter, LoadFromFile ... SaveLoadedDocument redigerer
Fil-låseproblemet er virkeligt, og svaret er ikke at dræbe fremviservinduer
Genbrugsfejlen rejser ofte med en anden klage, og de to bliver viklet sammen, fordi de dukker op i den samme regenerer-filen-arbejdsgang. En bruger åbner den PDF, du netop har produceret, lader den være åben i Acrobat eller Foxit og udløser derefter en genopbygning (rebuild). EndDoc forsøger at skrive den samme sti, operativsystemet nægter, fordi fremviseren har en læseandel (read share), der blokerer skrivere, og du får en adgang nægtet-fejl. Denne her er ægte et Windows fil-låseproblem snarere end et komponenttilstandsproblem, og det fortjener et rigtigt svar i stedet for en omvej (workaround)
Den omvej (workaround), der cirkulerer, med at opregne top-level vinduer og sende WM_CLOSE til alt, hvis titel ligner en PDF-fremviser, er det forkerte instinkt. Det rækker over procesgrænser for at lukke vinduer, dit program ikke ejer, det gætter på fremvisere ud fra titeltekst, og det kan smide en brugers ugemte anmærkninger væk uden at spørge. Betragt hele den tilgang som en dårlig vane (smell). Den pålidelige rettelse er aldrig at skrive til en sti, en anden proces muligvis holder. Serialiser til en midlertidig fil i den samme mappe, og byt den derefter på plads med et atomisk omdøb (atomic rename), når EndDoc lykkes. Hvis en fremviser stadig har den gamle fil åben, vil omdøbningen enten lykkes rent eller fejle højlydt, og du viser en klar besked i stedet for at kæmpe mod låsen
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;
To ærlige fodnoter til den kode. TFile.Move og den klassiske RenameFile knytter sig begge til det samme Windows rename, hvilket kun er atomisk, når kilde og destination sidder på den samme volumen (volume), og det er præcis derfor, temp-filen går ind i destinationsmappen snarere end TPath.GetTempPath. Og slet-derefter-flyt-parret er ikke i sig selv ét atomisk trin: der er et kort vindue, hvor ingen af filerne eksisterer. For en desktop-app, der regenererer en rapport, er det vindue irrelevant; læsere, der har brug for en stærkere kontrakt på den samme volumen, kan kalde Win32 ReplaceFile eller MoveFileEx med MOVEFILE_REPLACE_EXISTING direkte, hvilket kollapser byttet (swap) til et enkelt kald
For en højvolumen-server, der konstant regenererer dokumenter, er den renere disciplin at skrive hvert output under et unikt navn (et tidsstempel eller et job-id), så to kørsler aldrig kæmper om én sti, og lade en separat fastholdelsespolitik (retention policy) rydde op i gamle filer. Mønsteret er én linje navngivningsdisciplin pr. anmodning (request)
// 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);
Et anmodnings-id (request id) eller job-id fungerer lige så godt som GUID'en, når det omgivende framework allerede giver dig et, og det gør filnavnet sporbart tilbage til en loglinje gratis. Uanset hvad er princippet det samme: design, så den fil, du skriver, er din alene i det øjeblik, du skriver den. Låsen forsvinder ikke, fordi du tvang et vindue i, men fordi intet andet rører ved bytene
Rettelsens form
Skær de to problemer tilbage til deres rødder, og de handler begge om at respektere grænser. Tilstandsmaskinefejlen vil have dig til at ære instansgrænsen: én THotPDF, ét dokument, giv slip på den, og lav en ny. Fil-låsefejlen vil have dig til at ære filgrænsen: skriv, hvor intet andet læser, og flyt derefter resultatet på plads. Ingen af dem kræver patching af biblioteket eller scripting af skrivebordet (desktop). Begge er resultatet af at behandle hvert dokument som en selvstændig arbejdsenhed, skabt frisk, skrevet rent og frigivet, hvilket er det samme mønster, der gør resten af komponenten forudsigelig
BeginDoc, EndDoc, LoadFromFile og SaveLoadedDocument kaldene vist her er en del af HotPDF-komponenten til Delphi og C++Builder