Articolo tecnico

Riutilizzare una istanza THotPDF su più documenti in Delphi

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

Riutilizzo di THotPDF in Delphi: una istanza esegue BeginDoc, disegno ed EndDoc per ogni file di output tra una singola Create e Free, oppure un ciclo batch crea una istanza fresca per ogni file
Ogni documento esegue BeginDoc, disegno, EndDoc; una istanza di THotPDF può ripetere il ciclo prima di Free, oppure ogni file di output può avere una istanza fresca tutta sua

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