Technisch artikel

Een THotPDF-instantie hergebruiken voor meerdere documenten

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

THotPDF-hergebruik in Delphi: één instantie draait BeginDoc, tekenen en EndDoc voor elk uitvoerbestand tussen één enkele Create en Free, of een batchlus maakt voor elk bestand een verse instantie aan
Elk document doorloopt BeginDoc, tekenen, EndDoc; één THotPDF-instantie kan die cyclus herhalen vóór Free, of elk uitvoerbestand krijgt een verse instantie van eigen

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