Una istanza di THotPDF può scrivere quanti documenti vuoi, uno dopo l'altro: BeginDoc dopo EndDoc avvia un documento nuovo e indipendente. Il riutilizzo multi-documento di una singola istanza è disponibile da HotPDF v2.764.0. Il messaggio Please load the document before using BeginDoc appartiene a una guardia vicina: LoadFromFile lo solleva quando un documento avviato con BeginDoc non è ancora stato chiuso con EndDoc. Entrambi i comportamenti discendono dal ciclo di vita del componente, e una volta che ti entra in testa nessuno dei due è misterioso
Una istanza THotPDF è un documento o una fabbrica di documenti?
Una istanza di THotPDF è una fabbrica di documenti: puoi crearla una volta, per esempio come componente posato su una form, e assegnarle documenti come terrestri aperta una connessione a database facendogli girare query dietro query. BeginDoc apre un documento e marca l'istanza come con un documento in corso. EndDoc serializza tutto su FileName e poi butta giù tutto ciò che è registrato per quel file, comprese immagini, font, campi form, outline, JavaScript del documento, destinazioni con nome, etichette di pagina, profili ICC e output intent. Le pagine e gli oggetti del documento scritto restano leggibili finché il prossimo BeginDoc non li libera, così ValidatePDFUA2Structure, CurrentPageNumber e PagesCount funzionano ancora dopo EndDoc. Con ReproducibleOutput attivo, un secondo documento da una istanza riusata è byte per byte identico a quel documento prodotto da una istanza nuova
Il ciclo di vita, nell'ordine in cui deve avvenire
Ogni documento che HotPDF scrive da zero segue gli stessi quattro tempi, e l'ordine non è negoziabile. Create alloca il componente. BeginDoc apre il documento e fissa le scelte strutturali, perciò tutto ciò che riguarda il file intero (dimensione della pagina, compressione, cifratura, nome del file di uscita) va impostato fra Create e BeginDoc. Poi disegnate. Poi EndDoc scrive i byte su disco. Free rilascia l'istanza. Le chiamate di disegno collocate prima di BeginDoc non hanno una pagina su cui atterrare; le proprietà valide per l'intero documento assegnate dopo vengono ignorate senza un fiato. Su una istanza riusata, le impostazioni di proprietà come FileName, Compression, PDFACompliance, Author e Title si tramandano al documento successivo, quindi risettatele prima di ogni BeginDoc quando cambiano
var
Pdf: THotPDF;
begin
Pdf := THotPDF.Create(nil);
try
Pdf.FileName := 'invoice.pdf';
Pdf.BeginDoc; // apre il documento
Pdf.CurrentPage.SetFont('Arial', [], 11);
Pdf.CurrentPage.TextOut(50, 760, 0, 'Invoice 2026-042');
Pdf.EndDoc; // scrive invoice.pdf e lo chiude
finally
Pdf.Free; // una istanza, un documento
end;
end;
Leggilo come l'unità di lavoro: un BeginDoc, un EndDoc, un file su disco. Un secondo file è una seconda coppia BeginDoc e EndDoc, sulla stessa istanza o su una nuova
Che cosa significa «riutilizzo»: una istanza sola, o una istanza nuova per ogni file
La versione frugale funziona: costruisci il componente una volta, cicla su un batch e chiama BeginDoc e EndDoc dentro il ciclo. Ogni iterazione produce un file indipendente, e nulla di ciò che è registrato per un documento raggiunge il successivo. Resta una trappola per l'output protetto: EndDoc pulisce UserPassword e OwnerPassword, quindi con ActivateProtection ancora attivo devi reimpostare le password prima del prossimo BeginDoc, che altrimenti solleva un'eccezione invece di proteggere il file con password vuote. La versione qui sotto prende la strada opposta e tratta ogni output come un oggetto di vita breve. Il costo di allocazione della creazione di un componente è banale accanto al lavoro di layout e serializzazione di un PDF, quindi accumulare l'istanza non salva granché in nessun caso
procedure WriteBatch(const Names: TArray<string>);
var
I: Integer;
Pdf: THotPDF;
begin
for I := 0 to High(Names) do
begin
Pdf := THotPDF.Create(nil); // nuova istanza a ogni passaggio
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;
Il try/finally seduto dentro il ciclo è la parte da difendere in review. Se BeginDoc o una chiamata di disegno qualsiasi solleva un'eccezione con un documento a metà, l'istanza di quell'iterazione viene comunque liberata prima che inizi la successiva, così un record brutto non lascia in vita un componente mezzo costruito e non avvelena il resto dell'esecuzione. Tirare la Create sopra il ciclo funziona pure, ma un documento abbandonato a metà da un'eccezione è un buon motivo per dare al record successivo una istanza fresca
Modificare un file esistente è un punto di ingresso diverso
C'è una seconda lettura di «riutilizzo» del tutto legittima: non volete un documento vuoto, volete aprire un PDF che esiste già e cambiarlo. Quel percorso non passa affatto per BeginDoc, ed è esattamente per questo che il messaggio di errore nomina il caricamento. Caricate il file, lo modificate e lo salvate con il nome che preferite
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 restituisce il numero di pagine, e un valore pari a zero o sotto significa che il caricamento è fallito, quindi vale la pena controllarlo prima di toccare CurrentPage. Il pairing conta: un documento aperto con LoadFromFile si salva con SaveLoadedDocument, non con la coppia BeginDoc/EndDoc, che appartiene ai documenti che scrivi da zero. Mescolare le due cose è il modo più comune di beccare la guardia del ciclo di vita descritta in cima a questo articolo. Tieni i due flussi mentalmente separati: BeginDoc ... EndDoc crea, LoadFromFile ... SaveLoadedDocument modifica
Il problema del lock sul file è reale, e la risposta non è chiudere a forza le finestre dei visualizzatori
Gli errori di ciclo di vita viaggiano spesso con una seconda lamentela, e le due si aggrovigliano perché affiorano nello stesso flusso di lavoro rigenera-il-file. Un utente apre il PDF appena prodotto, lo lascia aperto in Acrobat o Foxit, poi fa scattare una ricostruzione. EndDoc tenta di scrivere sullo stesso percorso, il sistema operativo rifiuta perché il viewer tiene una share di lettura che blocca gli scrittori, e tu becci un fallimento di accesso negato. Questo è davvero un problema di file locking di Windows e non di stato del componente, e merita una risposta vera invece di un espediente
L'espediente che circola, enumerare le finestre di primo livello e inviare WM_CLOSE a qualunque titolo somigli a un visualizzatore PDF, è l'istinto sbagliato. Attraversa i confini di processo per chiudere finestre che il vostro programma non possiede, indovina i visualizzatori dal testo del titolo e può buttare via le annotazioni non salvate di un utente senza chiedere. Considerate tutto quell'approccio un cattivo odore. La correzione affidabile è non scrivere mai su un percorso che un altro processo potrebbe tenere occupato. Serializzate su un file temporaneo nella stessa directory, poi mettetelo al suo posto con una rinomina atomica appena EndDoc è andato a buon fine. Se un visualizzatore tiene ancora aperto il vecchio file, la rinomina o riesce in modo pulito o fallisce rumorosamente, e voi mostrate un messaggio chiaro invece di combattere contro il lock
uses
System.SysUtils, System.IOUtils;
procedure WritePdfAtomically(const FinalPath: string);
var
Pdf: THotPDF;
TempPath: string;
begin
// File temporaneo nella STESSA directory del bersaglio: una rinomina
// dentro un solo volume NTFS scambia il nome in modo atomico, mentre uno
// spostamento fra volumi degrada a copia-più-cancella e perde la garanzia
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; // qui il temporaneo è completo su disco
finally
Pdf.Free;
end;
// Scambio in posizione. TFile.Move rifiuta di sovrascrivere: eliminate
// prima un bersaglio obsoleto; se un visualizzatore tiene ancora il
// vecchio file, fallisce la cancellazione, rumorosamente, prima dei byte buoni
if TFile.Exists(FinalPath) then
TFile.Delete(FinalPath);
TFile.Move(TempPath, FinalPath); // oppure: RenameFile(TempPath, FinalPath)
except
if TFile.Exists(TempPath) then
TFile.Delete(TempPath); // mai lasciare un temporaneo a metà
raise;
end;
end;
Due note oneste su quel codice. TFile.Move e il classico RenameFile mappano entrambi sulla stessa rinomina di Windows, atomica solo quando origine e destinazione stanno sullo stesso volume, ed è proprio per questo che il file temporaneo finisce nella directory di destinazione anziché in TPath.GetTempPath. E la coppia cancella-poi-sposta non è a sua volta un unico passo atomico: c'è una breve finestra in cui nessuno dei due file esiste. Per una applicazione desktop che rigenera un rapporto quella finestra è irrilevante; chi ha bisogno di una garanzia più forte sullo stesso volume può chiamare direttamente la Win32 ReplaceFile oppure MoveFileEx con MOVEFILE_REPLACE_EXISTING, che condensa lo scambio in una sola chiamata
Per un server ad alto volume che rigenera documenti di continuo, la disciplina più pulita è scrivere ogni uscita sotto un nome univoco (una marca temporale o un identificativo di job) così che due esecuzioni non contendano mai lo stesso percorso, lasciando a una politica di conservazione separata il compito di ripulire i vecchi file. Lo schema costa una riga di disciplina sui nomi per ogni richiesta
// Un percorso di uscita per richiesta: due job concorrenti non possono mai
// contendersi lo stesso nome, niente balletto di rinomine e nessun lock perso
OutName := Format('statement-%s-%s.pdf',
[CustomerId, TGUID.NewGuid.ToString.Trim(['{', '}'])]);
Pdf.FileName := TPath.Combine(OutputDir, OutName);
Un identificativo di richiesta o di job funziona bene quanto il GUID quando il framework circostante ve ne consegna già uno, e rende il nome del file tracciabile fino a una riga di log senza costi aggiuntivi. In un modo o nell'altro il principio è lo stesso: progettate in modo che il file che state scrivendo sia soltanto vostro nel momento in cui lo scrivete. Il lock sparisce non perché avete forzato la chiusura di una finestra ma perché nessun altro sta toccando quei byte
La forma della soluzione
Riporta i due problemi alle loro radici e sono entrambi una questione di rispetto dei confini. L'errore di macchina a stati vuole che tu onori il confine del documento: chiudi un documento con EndDoc prima di avviare o caricare il successivo. L'errore di lock del file vuole che tu onori il confine del file: scrivi dove nessun altro sta leggendo, poi sposta il risultato al suo posto. Nessuno dei due chiede di aggirare il componente o di scriptare il desktop. Entrambi discendono dal trattare ogni documento come una unità di lavoro autocontenuta, creata fresca, scritta pulita e rilasciata, che è lo stesso schema che rende prevedibile il resto del componente
Le chiamate BeginDoc, EndDoc, LoadFromFile e SaveLoadedDocument mostrate qui fanno parte del HotPDF Delphi Component per Delphi e C++Builder