Eén THotPDF-instantie kan na elkaar een willekeurig aantal documenten schrijven: BeginDoc na EndDoc start een nieuw, onafhankelijk document. Hergebruik van één instantie over meerdere documenten is beschikbaar sinds HotPDF v2.764.0. De melding Please load the document before using BeginDoc hoort bij een buurcontrole: LoadFromFile werpt hem op wanneer een document dat met BeginDoc is gestart nog niet met EndDoc is afgesloten. Beide gedragingen volgen uit de levenscyclus van de component, en zodra dat kwartje valt, is geen van beide mysterieus
Is een THotPDF-instantie één document of een documentfabriek?
Een THotPDF-instantie is een documentfabriek: u maakt hem eenmaal aan, bijvoorbeeld als component op een formulier, en voert hem documenten, op de manier waarop u een databaseverbinding open zou houden en er query na query doorheen zou sturen. BeginDoc opent een document en markeert de instantie als hebbende een document in uitvoering. EndDoc serialiseert alles naar FileName en gooit daarna alles weg wat voor dat bestand was geregistreerd, inclusief zijn afbeeldingen, fonts, formuliervelden, outlines, document-JavaScript, named destinations, paginalabels, ICC-profielen en output intents. De pagina's en objecten van het weggeschreven document blijven leesbaar tot de volgende BeginDoc ze vrijgeeft, dus ValidatePDFUA2Structure, CurrentPageNumber en PagesCount werken na EndDoc nog steeds. Met ReproducibleOutput aan is een tweede document van een hergebruikte instantie byte voor byte identiek aan dat document van een nieuwe instantie
De levenscyclus, in de volgorde waarin deze moet plaatsvinden
Elk document dat HotPDF vanaf nul schrijft, volgt dezelfde vier stappen, en de volgorde is niet onderhandelbaar. Create alloceert de component. BeginDoc opent het document en zet de structurele keuzes vast, dus alles wat het hele bestand raakt (paginaformaat, compressie, versleuteling, uitvoerbestandsnaam) moet tussen Create en BeginDoc worden ingesteld. Dan tekent u. Dan schrijft EndDoc de bytes naar schijf. Free geeft de instantie vrij. Tekenaanroepen vóór BeginDoc hebben geen pagina om op te landen; eigenschappen van het hele document die erna worden toegewezen worden klachtloos genegeerd. Op een hergebruikte instantie slepen eigenschapsinstellingen zoals FileName, Compression, PDFACompliance, Author en Title door naar het volgende document, dus stel ze opnieuw in vóór elke BeginDoc als ze verschillen
var
Pdf: THotPDF;
begin
Pdf := THotPDF.Create(nil);
try
Pdf.FileName := 'invoice.pdf';
Pdf.BeginDoc; // opent het document
Pdf.CurrentPage.SetFont('Arial', [], 11);
Pdf.CurrentPage.TextOut(50, 760, 0, 'Invoice 2026-042');
Pdf.EndDoc; // schrijft invoice.pdf, sluit het af
finally
Pdf.Free; // één instantie, één document
end;
end;
Lees dat als de werkeenheid: één BeginDoc, één EndDoc, één bestand op schijf. Een tweede bestand is een tweede BeginDoc- en EndDoc-paar, op dezelfde instantie of op een nieuwe
Wat "hergebruik" betekent: één instantie, of een nieuwe instantie per bestand
De zuinige versie werkt: bouw de component eenmaal, loop over een batch, en roep BeginDoc en EndDoc aan binnen de lus. Elke iteratie levert een onafhankelijk bestand op, en niets wat voor het ene document was geregistreerd, bereikt het volgende. Eén valkuil blijft er voor beveiligde uitvoer: EndDoc wist UserPassword en OwnerPassword, dus met ActivateProtection nog aan moet u de wachtwoorden opnieuw instellen vóór de volgende BeginDoc, die anders een uitzondering werpt in plaats van het bestand met lege wachtwoorden te beveiligen. De versie hieronder neemt de andere route en behandelt elke uitvoer als zijn eigen kortlevende object. De allocatiekosten van het aanmaken van een component zijn triviaal naast het werk van het opmaken en serialiseren van een PDF, dus er is hoe dan ook weinig te besparen door de instantie op te potten
procedure WriteBatch(const Names: TArray<string>);
var
I: Integer;
Pdf: THotPDF;
begin
for I := 0 to High(Names) do
begin
Pdf := THotPDF.Create(nil); // nieuwe instantie bij elke doorgang
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;
De try/finally die binnen de lus zit, is het deel dat het verdedigen waard is tijdens een review. Als BeginDoc of een tekenaanroep halverwege een document een uitzondering opwerpt, wordt de instantie van die iteratie nog steeds vrijgegeven voordat de volgende begint, zodat één foutief record geen halfgebouwde component achterlaat en de rest van de run vergiftigt. De Create boven de lus tillen werkt ook, maar een document dat halverwege door een uitzondering wordt achtergelaten, blijft een goede reden om het volgende record een verse instantie te geven
Het wijzigen van een bestaand bestand is een ander instappunt
Er is een tweede interpretatie van "hergebruik" die volkomen legitiem is: u wilt geen blanco document, u wilt een reeds bestaande PDF openen en wijzigen. Dat pad loopt helemaal niet via BeginDoc, wat precies is waarom de foutmelding het laden noemt. U laadt het bestand, bewerkt het en slaat het op onder de naam die u kiest
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 retourneert het aantal pagina's, en een waarde van nul of lager betekent dat het laden is mislukt, dus dit is het controleren waard voordat u CurrentPage aanraakt. De koppeling is belangrijk: een document dat u hebt geopend met LoadFromFile wordt opgeslagen met SaveLoadedDocument, niet met het BeginDoc/EndDoc-paar, dat hoort bij documenten die u vanuit het niets ontwerpt. De twee door elkaar halen is de meest voorkomende manier om de levenscyclusbewaking te laten afgaan die bovenaan dit artikel wordt beschreven. Houd de twee stromen mentaal gescheiden: BeginDoc ... EndDoc creëert, LoadFromFile ... SaveLoadedDocument bewerkt
Het bestandsblokkeringsprobleem is echt, en het antwoord is niet om viewervensters te sluiten
Levenscyclusfouten gaan vaak gepaard met een tweede klacht, en de twee raken met elkaar verstrikt omdat ze opduiken in dezelfde bestand-opnieuw-genereren-workflow. Een gebruiker opent de PDF die u zojuist hebt geproduceerd, laat hem open staan in Acrobat of Foxit, en triggert vervolgens een rebuild. EndDoc probeert naar hetzelfde pad te schrijven, het besturingssysteem weigert dit omdat de viewer een read-share vasthoudt die schrijvers blokkeert, en u krijgt een toegang-geweigerd-fout. Dit is oprecht een Windows-bestandsblokkeringsprobleem in plaats van een component-statusprobleem, en het verdient een echt antwoord in plaats van een omweg
De omweg die circuleert — top-level vensters opsommen en WM_CLOSE sturen naar alles waarvan de titel op een PDF-viewer lijkt — is het verkeerde instinct. Hij reikt over procesgrenzen heen om vensters te sluiten die uw programma niet bezit, hij gokt op viewers via titeltekst, en hij kan de niet-opgeslagen annotaties van een gebruiker weggooien zonder te vragen. Behandel die hele aanpak als een smell. De betrouwbare oplossing is nooit te schrijven naar een pad dat een ander proces mogelijk vasthoudt. Serialiseer naar een tijdelijk bestand in dezelfde map, en wissel hem daarna met een atomaire rename op zijn plaats zodra EndDoc slaagt. Heeft een viewer het oude bestand nog open, dan slaagt de rename ofwel netjes of faalt hij luid, en u toont een duidelijke melding in plaats van tegen de blokkering te vechten
uses
System.SysUtils, System.IOUtils;
procedure WritePdfAtomically(const FinalPath: string);
var
Pdf: THotPDF;
TempPath: string;
begin
// Tijdelijk bestand in DEZELFDE map als het doel: een hernoeming binnen één
// NTFS-volume wisselt de naam atomair, terwijl een verplaatsing over volumes heen
// degradeert tot kopiëren-plus-verwijderen en die garantie verliest
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; // het tijdelijke bestand staat hier compleet op schijf
finally
Pdf.Free;
end;
// Wissel op zijn plaats. TFile.Move weigert te overschrijven, dus verwijder eerst een
// verouderd doel; als een viewer het oude bestand nog vasthoudt, is het verwijderen
// wat luidruchtig faalt, voordat de goede bytes worden aangeraakt
if TFile.Exists(FinalPath) then
TFile.Delete(FinalPath);
TFile.Move(TempPath, FinalPath); // of: RenameFile(TempPath, FinalPath)
except
if TFile.Exists(TempPath) then
TFile.Delete(TempPath); // laat nooit een half geschreven tijdelijk bestand achter
raise;
end;
end;
Twee eerlijke voetnoten bij die code. TFile.Move en het klassieke RenameFile verwijzen beide naar dezelfde Windows rename, die alleen atomair is wanneer bron en doel zich op hetzelfde volume bevinden, en dat is precies de reden waarom het tijdelijke bestand in de doelmap terechtkomt in plaats van in TPath.GetTempPath. En de verwijderen-dan-verplaatsen-combinatie is zelf niet één atomaire stap: er is een kort venster waarin geen van beide bestanden bestaat. Voor een desktop-app die een rapport hergenereert, is dat venster irrelevant; lezers die een sterker contract nodig hebben op hetzelfde volume kunnen direct de Win32 ReplaceFile of MoveFileEx met MOVEFILE_REPLACE_EXISTING aanroepen, wat de wissel tot één enkele aanroep samenvoegt
Voor een hoogvolumeserver die constant documenten hergenereert, is de schonere discipline om elke uitvoer onder een unieke naam te schrijven (een tijdstempel of een taak-id), zodat twee runs nooit om één pad strijden, en een afzonderlijk bewaarbeleid oude bestanden te laten opruimen. Het patroon is één regel naamgevingsdiscipline per aanvraag
// Eén uitvoerpad per aanvraag: twee gelijktijdige taken kunnen nooit strijden
// om dezelfde naam, dus geen hernoemingsgedoe en geen lock om te verliezen
OutName := Format('statement-%s-%s.pdf',
[CustomerId, TGUID.NewGuid.ToString.Trim(['{', '}'])]);
Pdf.FileName := TPath.Combine(OutputDir, OutName);
Een aanvraag-id of taak-id werkt net zo goed als de GUID wanneer het omliggende raamwerk u er al één overhandigt, en het maakt de bestandsnaam gratis herleidbaar naar een logregel. Hoe dan ook is het principe hetzelfde: ontwerp zodanig dat het bestand dat u schrijft op het moment van schrijven alleen van u is. De blokkering verdwijnt niet omdat u een venster geforceerd sloot, maar omdat niets anders de bytes aanraakt
De vorm van de oplossing
Breng de twee problemen terug naar hun essentie en het gaat bij beide om het respecteren van grenzen. De statusmachinefout wil dat u de documentgrens eert: sluit één document af met EndDoc voordat u het volgende start of laadt. De bestandsblokkeringsfout wil dat u de grens van het bestand eert: schrijf waar niets anders leest, en verplaats het resultaat dan naar zijn plaats. Geen van beide vraagt om de component te omzeilen of het bureaublad te scripten. Beide vloeien voort uit het behandelen van elk document als een opzichzelfstaande werkeenheid, vers gemaakt, netjes geschreven en vrijgegeven, wat hetzelfde patroon is dat de rest van de component voorspelbaar maakt
De hier getoonde BeginDoc-, EndDoc-, LoadFromFile- en SaveLoadedDocument-aanroepen maken deel uit van de HotPDF Delphi Component voor Delphi en C++Builder