Technical Article

Reutilizar una instancia de THotPDF en varios 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 de THotPDF que inicie un segundo, BeginDoc lanza la excepción y el mensaje apunta a cargar un documento, que es lo opuesto a lo que intenta hacer el código. La discrepancia entre el síntoma y el mensaje es lo que hace que este error se quede grabado. El verdadero tema es el ciclo de vida del componente, y una vez que eso queda claro, el error deja de ser misterioso.

THotPDF document lifecycle showing Create, BeginDoc, EndDoc, and Free per output file
Una instancia de THotPDF corresponde a un documento: Create, BeginDoc, dibujar, EndDoc, Free.

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

El modelo mental tentador es que THotPDF es un objeto de servicio que se pone en marcha una vez y al que se le van enviando documentos, de la misma manera que se mantiene una conexión de base de datos abierta y se ejecutan consultas una tras otra. No es así. Una instancia modela un único documento en construcción, y su máquina de estados interna lleva la suposición de que recorre el camino una sola vez: desde vacío, a través de un documento abierto, hasta un fichero guardado. BeginDoc abre ese camino y marca la instancia como si tuviera un documento en curso. EndDoc serializa todo en FileName y lo cierra. Llamar de nuevo a BeginDoc en la misma instancia terminada le pide que vuelva a entrar en un estado que nunca abandonó limpiamente, y la guardia que se dispara es la cuyo mensaje menciona la carga, porque internamente las condiciones «listo para comenzar» y «tiene un documento cargado» se comprueban juntas.

Así que el mensaje es engañoso, pero la guardia está haciendo su trabajo. Se niega a dejaros iniciar un documento nuevo encima de un componente que todavía cree que está a mitad de un documento. La solución no es derrotar a la guardia. Es dejar de reutilizar una instancia agotada.

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

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

var
  Pdf: THotPDF;
begin
  Pdf := THotPDF.Create(nil);
  try
    Pdf.FileName := 'invoice.pdf';
    Pdf.BeginDoc;                        // opens the document
    Pdf.CurrentPage.SetFont('Arial', [], 11);
    Pdf.CurrentPage.TextOut(50, 760, 0, 'Invoice 2026-042');
    Pdf.EndDoc;                          // writes invoice.pdf, closes it out
  finally
    Pdf.Free;                            // one instance, one document
  end;
end;

Entendedlo como la unidad de trabajo. Un Create, un BeginDoc, un EndDoc, un Free, un fichero en disco. En cuanto queréis un segundo fichero, estáis comenzando una nueva unidad de trabajo, lo que significa una nueva instancia.

Lo que «reutilizar» debería significar: una instancia nueva por fichero

La versión que falla intenta ser frugal con la asignación de memoria: construir el componente una vez, iterar sobre un lote, llamar a BeginDoc y EndDoc dentro del bucle. La segunda iteración lanza la excepción. La versión que funciona trata cada salida como su propio objeto de corta duración, y el coste de crear un componente es insignificante comparado con el trabajo de componer y serializar un PDF, así que no hay nada que ganar acumulando 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);         // new instance each pass
    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 dentro del bucle es la parte que vale la pena defender en una revisión de código. Si BeginDoc o cualquier llamada de dibujo lanza una excepción a mitad de un documento, la instancia de esa iteración se libera antes de que comience la siguiente, de modo que un registro defectuoso no deja abandonado un componente a medio construir ni envenena el resto de la ejecución. Sacar el Create fuera del bucle para «optimizar» os devuelve al error original, ahora envuelto en un bucle de lote.

Modificar un fichero existente es un punto de entrada distinto

Existe una segunda lectura de «reutilizar» que es completamente legítima: no queréis un documento en blanco, sino abrir un PDF que ya existe y modificarlo. Ese camino no pasa por BeginDoc en absoluto, que es exactamente la razón por la que el mensaje de error menciona la carga. Cargáis el fichero, lo editáis y lo guardáis con el nombre que elijáis.

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 número de páginas, y un valor de cero o inferior significa que la carga ha fallado, por lo que conviene comprobarlo antes de tocar CurrentPage. El emparejamiento importa: un documento abierto con LoadFromFile se guarda con SaveLoadedDocument, no con el par BeginDoc/EndDoc, que corresponde a documentos creados desde cero. Mezclar los dos es la forma más habitual de confundir la misma máquina de estados que produjo el error original. Mantened los dos flujos separados mentalmente: BeginDoc ... EndDoc crea, LoadFromFile ... SaveLoadedDocument edita.

El problema del bloqueo de ficheros es real, y la solución no es cerrar las ventanas del visor

El error de reutilización suele venir acompañado de una segunda queja, y los dos se mezclan porque afloran en el mismo flujo de trabajo de regeneración del fichero. Un usuario abre el PDF que acabáis de producir, lo deja abierto en Acrobat o Foxit, y luego desencadena una reconstrucción. EndDoc intenta escribir en la misma ruta, el sistema operativo lo rechaza porque el visor mantiene un acceso de lectura que bloquea la escritura, y se obtiene un error de acceso denegado. Este es genuinamente un problema de bloqueo de ficheros de Windows, no un problema del estado del componente, y merece una respuesta real en lugar de un parche.

El remedio que circula, enumerar las ventanas de nivel superior y enviar WM_CLOSE a cualquiera cuyo título parezca el de un visor PDF, es el instinto equivocado. Cruza los límites de proceso para cerrar ventanas que vuestro programa no posee, adivina los visores por el texto del título y puede descartar las anotaciones no guardadas de un usuario sin pedirle permiso. Tratad todo ese enfoque como una señal de alarma. La solución fiable es no escribir nunca en una ruta que otro proceso pueda estar usando. Serializad en un fichero temporal en el mismo directorio y luego intercambiadlo mediante un renombrado atómico una vez que EndDoc tenga éxito. Si un visor todavía tiene el fichero antiguo abierto, el renombrado o tiene éxito sin problemas o falla de forma clara, y podéis mostrar un mensaje de error en lugar de luchar contra el bloqueo.

Para un servidor de alto volumen que regenera documentos constantemente, la disciplina más limpia es escribir cada salida con un nombre único (una marca de tiempo o un identificador de tarea) de modo que dos ejecuciones nunca compitan por la misma ruta, y dejar que una política de retención independiente limpie los ficheros antiguos. En cualquier caso el principio es el mismo: diseñad de modo que el fichero que estáis escribiendo sea exclusivamente vuestro en el momento de escribirlo. El bloqueo desaparece no porque hayáis forzado el cierre de una ventana, sino porque nada más está tocando los bytes.

La forma de la solución

Si se despojan los dos problemas hasta su raíz, ambos tienen que ver con respetar los límites. El error de la máquina de estados pide que respetéis el límite de instancia: un THotPDF, un documento, luego liberadlo y cread otro. El error de bloqueo de ficheros pide que respetéis el límite del fichero: escribid donde nada más esté leyendo y luego moved el resultado a su lugar definitivo. Ninguno requiere parchear la biblioteca ni automatizar el escritorio. Ambos se resuelven tratando cada documento como una unidad de trabajo autónoma, creada limpia, escrita correctamente y liberada, que es el mismo patrón que hace que el resto del componente sea predecible.

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