Une instance THotPDF peut écrire autant de documents que voulu l'un après l'autre : un BeginDoc après un EndDoc démarre un nouveau document indépendant. La réutilisation multi-documents d'une seule instance est disponible depuis HotPDF v2.764.0. Le message Please load the document before using BeginDoc appartient à une garde voisine : LoadFromFile le lève quand un document ouvert par BeginDoc n'a pas encore été refermé par EndDoc. Les deux comportements découlent du cycle de vie du composant, et une fois celui-ci compris, aucun des deux n'a plus rien de mystérieux
Une instance THotPDF : un document ou une fabrique de documents ?
Une instance THotPDF est une fabrique de documents : vous pouvez la créer une fois, par exemple comme un composant déposé sur une fiche, et la nourrir de documents de la même manière que vous garderiez une connexion de base de données ouverte et exécuteriez des requêtes les unes après les autres à travers elle. BeginDoc ouvre un document et marque l'instance comme ayant un document en cours. EndDoc sérialise tout vers FileName, puis abandonne tout ce qui était enregistré pour ce fichier, y compris ses images, polices, champs de formulaire, signets, JavaScript de document, destinations nommées, étiquettes de page, profils ICC et output intents. Les pages et objets du document écrit restent lisibles jusqu'à ce que le prochain BeginDoc les libère, si bien que ValidatePDFUA2Structure, CurrentPageNumber et PagesCount fonctionnent encore après EndDoc. Avec ReproducibleOutput activé, un deuxième document issu d'une instance réutilisée est octet pour octet identique à ce même document produit par une nouvelle instance
Le cycle de vie, dans l'ordre où il doit se dérouler
Chaque document que HotPDF écrit à partir de zéro suit les mêmes quatre temps, et l'ordre n'est pas négociable. Create alloue le composant. BeginDoc ouvre le document et fixe les choix structurels, de sorte que tout ce qui affecte l'ensemble du fichier (taille de page, compression, chiffrement, nom de fichier de sortie) doit être défini entre Create et BeginDoc. Ensuite, vous dessinez. Puis EndDoc écrit les octets sur le disque. Free libère l'instance. Les appels de dessin placés avant BeginDoc n'ont pas de page sur laquelle atterrir ; les propriétés de l'ensemble du document assignées après celui-ci sont ignorées sans plainte. Sur une instance réutilisée, les réglages de propriétés tels que FileName, Compression, PDFACompliance, Author et Title se reportent au document suivant, donc redéfinissez-les avant chaque BeginDoc quand ils diffèrent
var
Pdf: THotPDF;
begin
Pdf := THotPDF.Create(nil);
try
Pdf.FileName := 'invoice.pdf';
Pdf.BeginDoc; // ouvre le document
Pdf.CurrentPage.SetFont('Arial', [], 11);
Pdf.CurrentPage.TextOut(50, 760, 0, 'Invoice 2026-042');
Pdf.EndDoc; // écrit invoice.pdf, le clôture
finally
Pdf.Free; // une instance, un document
end;
end;
Lisez cela comme l'unité de travail : un BeginDoc, un EndDoc, un fichier sur le disque. Un deuxième fichier est une deuxième paire BeginDoc/EndDoc, sur la même instance ou sur une nouvelle
Ce que « réutiliser » signifie : une seule instance, ou une nouvelle instance par fichier
La version frugale fonctionne : construisez le composant une fois, bouclez sur un lot, et appelez BeginDoc et EndDoc à l'intérieur de la boucle. Chaque itération produit un fichier indépendant, et rien de ce qui était enregistré pour un document n'atteint le suivant. Un piège subsiste pour les sorties protégées : EndDoc efface UserPassword et OwnerPassword, donc avec ActivateProtection toujours actif, il faut redéfinir les mots de passe avant le prochain BeginDoc, faute de quoi celui-ci lève une exception au lieu de protéger le fichier avec des mots de passe vides. La version ci-dessous prend l'autre voie et traite chaque sortie comme son propre objet éphémère. Le coût d'allocation de la création d'un composant est trivial à côté du travail de mise en page et de sérialisation d'un PDF, il y a donc peu à gagner en thésaurisant l'instance dans un cas comme dans l'autre
procedure WriteBatch(const Names: TArray<string>);
var
I: Integer;
Pdf: THotPDF;
begin
for I := 0 to High(Names) do
begin
Pdf := THotPDF.Create(nil); // nouvelle instance à chaque passage
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;
Le try/finally situé à l'intérieur de la boucle est la partie qui mérite d'être défendue en revue (de code). Si BeginDoc ou n'importe quel appel de dessin déclenche une erreur au milieu d'un document, l'instance de cette itération est tout de même libérée avant que la suivante ne commence, de sorte qu'un mauvais enregistrement n'abandonne pas un composant à moitié construit et n'empoisonne pas le reste de l'exécution. Sortir le Create au-dessus de la boucle fonctionne aussi, mais un document abandonné à mi-chemin par une exception reste une bonne raison de donner à l'enregistrement suivant une instance fraîche
Modifier un fichier existant est un point d'entrée différent
Il y a une deuxième lecture de "réutilisation" qui est tout à fait légitime : vous ne voulez pas un document vierge, vous voulez ouvrir un PDF qui existe déjà et le modifier. Ce chemin ne passe pas du tout par BeginDoc, ce qui est exactement la raison pour laquelle le message d'erreur mentionne le chargement. Vous chargez le fichier, le modifiez et l'enregistrez sous le nom que vous choisissez
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 renvoie le nombre de pages, et une valeur inférieure ou égale à zéro signifie que le chargement a échoué, il est donc utile de vérifier avant de toucher CurrentPage. L'association a de l'importance : un document que vous avez ouvert avec LoadFromFile est enregistré avec SaveLoadedDocument, et non avec la paire BeginDoc/EndDoc, qui appartient aux documents que vous créez de toutes pièces. Mélanger les deux est la façon la plus courante de buter sur la garde de cycle de vie décrite au début de cet article. Gardez les deux flux mentalement séparés : BeginDoc ... EndDoc crée, LoadFromFile ... SaveLoadedDocument édite
Le problème de verrouillage de fichier est réel, et la réponse n'est pas de tuer les fenêtres du visualiseur
Les erreurs de cycle de vie s'accompagnent souvent d'une deuxième plainte, et les deux s'entremêlent car elles font surface dans le même flux de travail de régénération de fichier. Un utilisateur ouvre le PDF que vous venez de produire, le laisse ouvert dans Acrobat ou Foxit, puis déclenche une reconstruction. EndDoc essaie d'écrire sur le même chemin, le système d'exploitation refuse car le visualiseur détient un partage en lecture (read share) qui bloque les écrivains (writers), et vous obtenez un échec d'accès refusé. Il s'agit véritablement d'un problème de verrouillage de fichier Windows plutôt que d'un problème d'état de composant, et cela mérite une vraie réponse plutôt qu'une solution de contournement
La solution de contournement qui circule, énumérant les fenêtres de niveau supérieur (top-level windows) et publiant WM_CLOSE à tout ce dont le titre ressemble à un visualiseur PDF, est un mauvais instinct. Elle franchit les frontières des processus pour fermer des fenêtres que votre programme ne possède pas, elle devine les visualiseurs par le texte du titre, et elle peut jeter les annotations non enregistrées d'un utilisateur sans demander. Considérez toute cette approche comme un code smell (une mauvaise pratique). La solution fiable consiste à ne jamais écrire sur un chemin qu'un autre processus pourrait détenir. Sérialisez dans un fichier temporaire dans le même répertoire, puis remettez-le en place avec un renommage atomique une fois que EndDoc a réussi. Si un visualiseur a toujours l'ancien fichier ouvert, le renommage réussit proprement ou échoue bruyamment, et vous affichez un message clair plutôt que de combattre le verrou
uses
System.SysUtils, System.IOUtils;
procedure WritePdfAtomically(const FinalPath: string);
var
Pdf: THotPDF;
TempPath: string;
begin
// Fichier temporaire dans le MÊME répertoire que la cible : un renommage à l'intérieur
// d'un même volume NTFS échange le nom de manière atomique, tandis qu'un déplacement
// entre volumes se dégrade en copie-puis-suppression et perd cette garantie
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; // le fichier temporaire est complet sur disque à ce stade
finally
Pdf.Free;
end;
// Échange en place. TFile.Move refuse d'écraser, donc on efface d'abord
// une cible périmée ; si un visualiseur détient encore l'ancien fichier, c'est la
// suppression qui échoue, bruyamment, avant que les bons octets ne soient touchés
if TFile.Exists(FinalPath) then
TFile.Delete(FinalPath);
TFile.Move(TempPath, FinalPath); // ou : RenameFile(TempPath, FinalPath)
except
if TFile.Exists(TempPath) then
TFile.Delete(TempPath); // ne jamais abandonner un fichier temporaire à moitié écrit
raise;
end;
end;
Deux notes de bas de page honnêtes sur ce code. TFile.Move et le classique RenameFile correspondent tous deux au même renommage Windows, qui n'est atomique que lorsque la source et la destination se trouvent sur le même volume, et c'est exactement pourquoi le fichier temporaire va dans le répertoire de destination plutôt que dans TPath.GetTempPath. Et la paire suppression-puis-déplacement (delete-then-move) n'est pas en soi une étape atomique : il y a une brève fenêtre au cours de laquelle aucun fichier n'existe. Pour une application de bureau régénérant un rapport, cette fenêtre n'a pas d'importance ; les lecteurs qui ont besoin d'un contrat plus solide sur le même volume peuvent appeler l'API Win32 ReplaceFile ou MoveFileEx avec MOVEFILE_REPLACE_EXISTING directement, ce qui condense l'échange en un seul appel
Pour un serveur à haut volume qui régénère constamment des documents, la discipline la plus propre consiste à écrire chaque sortie sous un nom unique (un horodatage ou un ID de tâche (job id)) de sorte que deux exécutions ne se disputent jamais un chemin, et de laisser une stratégie de rétention distincte nettoyer les anciens fichiers. Le modèle est une ligne de discipline de nommage par requête
// Un chemin de sortie par requête : deux tâches concurrentes ne peuvent jamais se disputer
// le même nom, donc pas de danse de renommage ni de verrou à perdre
OutName := Format('statement-%s-%s.pdf',
[CustomerId, TGUID.NewGuid.ToString.Trim(['{', '}'])]);
Pdf.FileName := TPath.Combine(OutputDir, OutName);
Un ID de requête (request id) ou un ID de tâche (job id) fonctionne tout aussi bien que le GUID lorsque le framework environnant vous en fournit déjà un, et cela permet de retracer gratuitement le nom du fichier vers une ligne de journal (log). Dans tous les cas, le principe est le même : concevez pour que le fichier que vous écrivez soit le vôtre uniquement au moment où vous l'écrivez. Le verrou disparaît non pas parce que vous avez forcé la fermeture d'une fenêtre, mais parce que rien d'autre ne touche aux octets
La forme de la solution
Dépouillez les deux problèmes de leurs racines et ils concernent tous deux le respect des frontières. L'erreur de la machine à états vous demande de respecter la limite du document : terminez un document par EndDoc avant de démarrer ou de charger le suivant. L'erreur de verrouillage de fichier vous demande d'honorer la limite du fichier : écrire là où rien d'autre ne lit, puis déplacer le résultat à sa place. Ni l'un ni l'autre ne nécessite de contourner le composant ni de scripter le bureau. Les deux découlent du traitement de chaque document comme une unité de travail autonome, créée de toutes pièces, écrite proprement et libérée, ce qui est le même modèle qui rend le reste du composant prévisible
Les appels BeginDoc, EndDoc, LoadFromFile et SaveLoadedDocument présentés ici font partie du Composant HotPDF pour Delphi et C++Builder