Artículo técnico

Reutilizar una instancia THotPDF entre documentos en Delphi

El error dice Please load the document before using BeginDoc, y casi siempre aparece la segunda vez. El primer documento se escribe bien. Luego se le pide a la misma instancia THotPDF que inicie un segundo, BeginDoc lanza la excepción, y el mensaje apunta a cargar un documento, que es lo contrario de lo que el código intenta hacer. La discrepancia entre el síntoma y el mensaje es lo que hace que este se quede pegado. El tema real es el ciclo de vida del componente, y una vez que eso hace clic el error deja de ser misterioso

Ciclo de vida de documento de THotPDF que muestra Create, BeginDoc, EndDoc y Free por archivo de salida
Una instancia THotPDF corresponde a un documento: Create, BeginDoc, dibujar, EndDoc, Free

Una instancia THotPDF es un documento, no una fábrica de documentos

El modelo mental tentador es que THotPDF es un objeto de servicio que usted levanta una vez y al que le alimenta documentos, del modo en que podría mantener abierta una conexión a base de datos y ejecutar consulta tras consulta a través de ella. No es eso. Una instancia modela un solo documento en construcción, y su máquina de estados interna carga con la suposición de que recorre el camino una vez: desde vacía, pasando por un documento abierto, hasta un archivo guardado. BeginDoc abre ese camino y marca la instancia como con un documento en curso. EndDoc serializa todo a FileName y lo cierra. Llamar a BeginDoc de nuevo sobre la misma instancia terminada le pide que vuelva a entrar en un estado del que nunca salió limpiamente, y la protección que se dispara es aquella cuyo mensaje resulta mencionar la carga, porque internamente las condiciones "lista para comenzar" y "tiene un documento cargado" se comprueban juntas

Así que el mensaje es engañoso, pero la protección hace su trabajo. Se niega a dejarle iniciar un documento nuevo encima de un componente que todavía cree estar a mitad de un documento. La solución no es burlar la protección. Es dejar de reutilizar una instancia gastada

El ciclo de vida, en el orden en que tiene que ocurrir

Cada documento que HotPDF escribe desde cero sigue los mismos cuatro tiempos, y el orden no es negociable. Create asigna el componente. BeginDoc abre el documento y fija las decisiones estructurales, así que todo lo que afecta al archivo completo (tamaño de página, compresión, cifrado, nombre del archivo de salida) tiene que establecerse entre Create y BeginDoc. Luego usted dibuja. Luego EndDoc escribe los bytes en disco. Free libera la instancia. Las llamadas de dibujo colocadas antes de BeginDoc no tienen página donde caer; las propiedades de documento completo asignadas después de él se ignoran sin queja

var
  Pdf: THotPDF;
begin
  Pdf := THotPDF.Create(nil);
  try
    Pdf.FileName := 'invoice.pdf';
    Pdf.BeginDoc;                        // abre el documento
    Pdf.CurrentPage.SetFont('Arial', [], 11);
    Pdf.CurrentPage.TextOut(50, 760, 0, 'Invoice 2026-042');
    Pdf.EndDoc;                          // escribe invoice.pdf y lo cierra
  finally
    Pdf.Free;                            // una instancia, un documento
  end;
end;

Lea eso como la unidad de trabajo. Un Create, un BeginDoc, un EndDoc, un Free, un archivo en disco. En el instante en que quiere un segundo archivo, está iniciando una nueva unidad de trabajo, lo que significa una nueva instancia

Lo que "reutilizar" debería significar: una instancia nueva por archivo

La versión que se rompe intenta ser frugal con la asignación: construir el componente una vez, iterar sobre un lote, llamar a BeginDoc y EndDoc dentro del ciclo. La segunda iteración lanza la excepción. La versión que funciona trata cada salida como su propio objeto de vida corta, y el costo de asignación de crear un componente es trivial junto al trabajo de maquetar y serializar un PDF, así que no hay nada que ahorrar acaparando la instancia

procedure WriteBatch(const Names: TArray<string>);
var
  I: Integer;
  Pdf: THotPDF;
begin
  for I := 0 to High(Names) do
  begin
    Pdf := THotPDF.Create(nil);         // instancia nueva en cada pasada
    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;

El try/finally que está dentro del ciclo es la parte que vale la pena defender en una revisión. Si BeginDoc o cualquier llamada de dibujo lanza una excepción a mitad de un documento, la instancia de esa iteración igual se libera antes de que comience la siguiente, así que un registro malo no deja varado un componente a medio construir ni envenena el resto de la ejecución. Saque el Create fuera del ciclo para "optimizar" y estará de vuelta en el error original, ahora vestido de ciclo por lotes

Modificar un archivo existente es un punto de entrada distinto

Hay una segunda lectura de "reutilizar" que es enteramente legítima: usted no quiere un documento en blanco, quiere abrir un PDF que ya existe y cambiarlo. Esa ruta no pasa por BeginDoc en absoluto, que es exactamente la razón por la que el mensaje de error menciona la carga. Usted carga el archivo, lo edita y lo guarda con el nombre que elija

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 devuelve el conteo de páginas, y un valor de cero o menos significa que la carga falló, así que vale la pena comprobarlo antes de tocar CurrentPage. El emparejamiento importa: un documento que abrió con LoadFromFile se guarda con SaveLoadedDocument, no con el par BeginDoc/EndDoc, que pertenece a los documentos que usted crea de la nada. Mezclar ambos es la forma más común de confundir la misma máquina de estados que produjo el error original. Mantenga los dos flujos mentalmente separados: BeginDoc ... EndDoc crea, LoadFromFile ... SaveLoadedDocument edita

El problema del bloqueo de archivo es real, y la respuesta no es matar ventanas de visores

El error de reutilización suele viajar con una segunda queja, y ambas se enredan porque afloran en el mismo flujo de regenerar el archivo. Un usuario abre el PDF que usted acaba de producir, lo deja abierto en Acrobat o Foxit y luego dispara una reconstrucción. EndDoc intenta escribir en la misma ruta, el sistema operativo se niega porque el visor mantiene un uso compartido de lectura que bloquea a los escritores, y usted obtiene una falla de acceso denegado. Este es genuinamente un problema de bloqueo de archivos de Windows y no un problema de estado del componente, y merece una respuesta real en lugar de un parche

El parche que circula, enumerar las ventanas de nivel superior y enviar WM_CLOSE a cualquier cosa cuyo título parezca un visor de PDF, es el instinto equivocado. Cruza los límites de proceso para cerrar ventanas que su programa no posee, adivina los visores por el texto del título y puede descartar las anotaciones sin guardar de un usuario sin preguntar. Trate todo ese enfoque como una mala señal. La solución confiable es no escribir nunca en una ruta que otro proceso podría estar sosteniendo. Serialice a un archivo temporal en el mismo directorio y luego intercámbielo en su lugar con un renombrado atómico una vez que EndDoc tenga éxito. Si un visor todavía tiene abierto el archivo viejo, el renombrado o bien tiene éxito limpiamente o falla de forma ruidosa, y usted muestra un mensaje claro en lugar de pelear con el bloqueo

uses
  System.SysUtils, System.IOUtils;

procedure WritePdfAtomically(const FinalPath: string);
var
  Pdf: THotPDF;
  TempPath: string;
begin
  // Archivo temporal en el MISMO directorio que el destino: un renombrado dentro
  // de un volumen NTFS intercambia el nombre de forma atómica, mientras que un
  // movimiento entre volúmenes degrada a copiar-y-borrar y pierde esa garantía
  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;                    // aquí el archivo temporal ya está completo en disco
    finally
      Pdf.Free;
    end;

    // Intercambiar en su lugar. TFile.Move se niega a sobrescribir, así que
    // primero limpie un destino obsoleto; si un visor aún sostiene el archivo
    // viejo, lo que falla, ruidosamente, es el borrado, antes de tocar los bytes buenos
    if TFile.Exists(FinalPath) then
      TFile.Delete(FinalPath);
    TFile.Move(TempPath, FinalPath); // o bien: RenameFile(TempPath, FinalPath)
  except
    if TFile.Exists(TempPath) then
      TFile.Delete(TempPath);        // nunca deje varado un archivo temporal a medio escribir
    raise;
  end;
end;

Dos notas honestas al pie sobre ese código. TFile.Move y el clásico RenameFile se mapean ambos al mismo renombrado de Windows, que es atómico solo cuando origen y destino están en el mismo volumen, y esa es exactamente la razón por la que el archivo temporal va al directorio de destino y no a TPath.GetTempPath. Y el par borrar-luego-mover no es en sí mismo un paso atómico: hay una breve ventana en la que ninguno de los dos archivos existe. Para una aplicación de escritorio que regenera un reporte esa ventana es irrelevante; los lectores que necesiten un contrato más fuerte en el mismo volumen pueden llamar directamente a ReplaceFile o MoveFileEx de Win32 con MOVEFILE_REPLACE_EXISTING, lo que colapsa el intercambio en una sola llamada

Para un servidor de alto volumen que regenera documentos constantemente, la disciplina más limpia es escribir cada salida bajo un nombre único (una marca de tiempo o un id de trabajo) para que dos ejecuciones nunca compitan por una misma ruta, y dejar que una política de retención aparte limpie los archivos viejos. El patrón es una línea de disciplina de nombres por solicitud

// Una ruta de salida por solicitud: dos trabajos concurrentes nunca pueden
// competir por el mismo nombre, así que no hay baile de renombrado ni bloqueo que perder
OutName := Format('statement-%s-%s.pdf',
  [CustomerId, TGUID.NewGuid.ToString.Trim(['{', '}'])]);
Pdf.FileName := TPath.Combine(OutputDir, OutName);

Un id de solicitud o un id de trabajo funciona igual de bien que el GUID cuando el framework que lo rodea ya le entrega uno, y hace que el nombre del archivo sea rastreable hasta una línea de registro sin costo. De cualquier forma el principio es el mismo: diseñe de modo que el archivo que está escribiendo sea solo suyo en el momento en que lo escribe. El bloqueo desaparece no porque usted forzó el cierre de una ventana, sino porque nada más está tocando los bytes

La forma de la solución

Reduzca los dos problemas a sus raíces y ambos tratan de respetar límites. El error de la máquina de estados quiere que usted respete el límite de la instancia: un THotPDF, un documento, y luego suéltelo y cree otro. El error de bloqueo de archivo quiere que respete el límite del archivo: escriba donde nada más está leyendo, y luego mueva el resultado a su lugar. Ninguno exige parchar la biblioteca ni programar el escritorio. Ambos se resuelven al tratar cada documento como una unidad de trabajo autocontenida, creada nueva, escrita limpiamente y liberada, que es el mismo patrón que hace predecible el resto del componente

Las llamadas BeginDoc, EndDoc, LoadFromFile y SaveLoadedDocument mostradas aquí forman parte del componente HotPDF para Delphi para Delphi y C++Builder