El error dice Please load the document before using BeginDoc, y casi siempre aparece en la segunda vuelta. El primer documento se escribe sin problemas. Después se le pide a la misma instancia de THotPDF que inicie un segundo documento, BeginDoc lanza una excepción y el mensaje habla de cargar un documento, que es justo lo contrario de lo que el código intenta hacer. Ese desajuste entre el síntoma y el mensaje es lo que hace que este error se quede grabado. El verdadero asunto es el ciclo de vida del componente, y en cuanto eso encaja el error deja de ser un misterio

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 arranca una vez y al que se le van pasando documentos, igual que se mantiene abierta una conexión a base de datos y se ejecuta consulta tras consulta a través de ella. No es eso. Una instancia modela un único documento en construcción, y su máquina de estados interna parte de la premisa de que recorre el camino una sola vez: desde vacío, pasando por un documento abierto, hasta un archivo guardado. BeginDoc abre ese camino y marca la instancia como que tiene un documento en curso. EndDoc serializa todo en FileName y lo cierra. Llamar de nuevo a BeginDoc sobre esa misma instancia ya terminada le pide que vuelva a entrar en un estado del que nunca salió limpiamente, y la guarda que salta es la que casualmente menciona la carga en su mensaje, porque internamente las condiciones "listo para empezar" y "tiene un documento cargado" se comprueban juntas
Así que el mensaje despista, pero la guarda hace su trabajo. Se niega a dejarle iniciar un documento nuevo encima de un componente que todavía cree estar a medio documento. La solución no es burlar la guarda. Es dejar de reutilizar una instancia ya gastada
El ciclo de vida, en el orden en que tiene que ocurrir
Todo documento que HotPDF escribe desde cero sigue los mismos cuatro compases, y el orden no es negociable. Create reserva el componente. BeginDoc abre el documento y fija las decisiones estructurales, de modo 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. Después se dibuja. Después EndDoc escribe los bytes en disco. Free libera la instancia. Las llamadas de dibujo situadas antes de BeginDoc no tienen página donde caer; las propiedades de documento completo asignadas después de él se ignoran sin la menor 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;
Léalo como la unidad de trabajo. Un Create, un BeginDoc, un EndDoc, un Free, un archivo en disco. En el instante en que quiera un segundo archivo, está iniciando una nueva unidad de trabajo, lo que significa una nueva instancia
Lo que debería significar "reutilizar": una instancia nueva por archivo
La versión que falla intenta ser frugal con la asignación de memoria: construye el componente una vez, recorre un lote en bucle y llama 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 vida corta, y el coste de crear un componente es insignificante frente 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 queda dentro del bucle es la parte que merece defenderse en la 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 igualmente antes de que empiece la siguiente, así que un registro defectuoso no deja abandonado un componente a medio construir ni envenena el resto de la ejecución. Saque el Create fuera del bucle para "optimizar" y volverá al fallo original, ahora disfrazado de bucle por lotes
Modificar un archivo existente es otro punto de entrada
Hay una segunda lectura de "reutilizar" que es del todo legítima: no quiere un documento en blanco, quiere abrir un PDF que ya existe y cambiarlo. Ese camino no pasa por BeginDoc en absoluto, y precisamente por eso el mensaje de error menciona la carga. Se carga el archivo, se edita y se guarda con el nombre que se prefiera
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 falló, así que conviene comprobarlo antes de tocar CurrentPage. El emparejamiento importa: un documento abierto con LoadFromFile se guarda con SaveLoadedDocument, no con la pareja BeginDoc/EndDoc, que pertenece a los documentos que se crean desde la nada. Mezclar ambos es la forma más habitual de confundir a la misma máquina de estados que produjo el error original. Mantenga los dos flujos separados mentalmente: BeginDoc ... EndDoc crea, LoadFromFile ... SaveLoadedDocument edita
El problema del bloqueo de archivos es real, y la respuesta no es matar ventanas de visores
El error de reutilización suele viajar con una segunda queja, y ambas acaban enredadas porque aparecen en el mismo flujo de regenerar el archivo. Un usuario abre el PDF que acaba de producirse, lo deja abierto en Acrobat o Foxit y después lanza una reconstrucción. EndDoc intenta escribir en la misma ruta, el sistema operativo lo rechaza porque el visor mantiene un modo de compartición de lectura que bloquea a los escritores, y se obtiene un fallo de acceso denegado. Este sí es un problema genuino de bloqueo de archivos en Windows y no un problema de estado del componente, y merece una respuesta de verdad en lugar de un apaño
El apaño que circula, enumerar las ventanas de primer nivel y enviar WM_CLOSE a cualquiera cuyo título parezca el de un visor de PDF, es el instinto equivocado. Cruza los límites entre procesos para cerrar ventanas que su programa no posee, adivina qué es un visor por el texto del título y puede tirar a la basura las anotaciones sin guardar de un usuario sin preguntar. Trate todo ese enfoque como una mala señal. La solución fiable es no escribir nunca en una ruta que otro proceso pueda estar reteniendo. Serialice en un archivo temporal dentro del mismo directorio y después cámbielo de sitio con un renombrado atómico una vez que EndDoc haya terminado bien. Si un visor sigue teniendo abierto el archivo antiguo, el renombrado o bien tiene éxito limpiamente o bien falla de forma ruidosa, y usted muestra un mensaje claro en lugar de pelearse 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 cambia el nombre de forma atómica, mientras que
// un movimiento entre volúmenes degenera en 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;
// Colocar en su sitio. TFile.Move se niega a sobrescribir, así que primero
// se limpia un destino obsoleto; si un visor aún retiene el archivo antiguo,
// lo que falla, y de forma ruidosa, 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 dejar abandonado un temporal a medio escribir
raise;
end;
end;
Dos notas honestas sobre ese código. TFile.Move y el clásico RenameFile se traducen ambos en el mismo renombrado de Windows, que solo es atómico cuando origen y destino están en el mismo volumen, y exactamente por eso el archivo temporal va al directorio de destino y no a TPath.GetTempPath. Y la pareja borrar-y-mover no es en sí misma un paso atómico: hay una breve ventana en la que no existe ninguno de los dos archivos. Para una aplicación de escritorio que regenera un informe esa ventana es irrelevante; quien necesite un contrato más fuerte dentro del mismo volumen puede llamar directamente a ReplaceFile o MoveFileEx de Win32 con MOVEFILE_REPLACE_EXISTING, que condensa el intercambio en una sola llamada
Para un servidor de gran 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 trabajo) para que dos ejecuciones nunca compitan por una misma ruta, y dejar que una política de retención aparte limpie los archivos antiguos. El patrón es una línea de disciplina de nombres por petición
// Una ruta de salida por petición: dos trabajos concurrentes nunca pueden
// competir por el mismo nombre, así que no hay baile de renombrados ni bloqueo que perder
OutName := Format('statement-%s-%s.pdf',
[CustomerId, TGUID.NewGuid.ToString.Trim(['{', '}'])]);
Pdf.FileName := TPath.Combine(OutputDir, OutName);
Un identificador de petición o de trabajo sirve igual de bien que el GUID cuando el framework circundante ya le entrega uno, y hace que el nombre del archivo pueda rastrearse hasta una línea de log sin coste alguno. En cualquier caso el principio es el mismo: diseñe de modo que el archivo que está escribiendo sea suyo y solo suyo en el momento de escribirlo. El bloqueo desaparece no porque haya forzado el cierre de una ventana, sino porque nada más está tocando esos bytes
La forma de la solución
Reduzca ambos problemas a sus raíces y los dos tratan de respetar límites. El error de la máquina de estados le pide respetar el límite de la instancia: un THotPDF, un documento, y después soltarlo y crear otro. El error del bloqueo de archivos le pide respetar el límite del archivo: escribir donde nadie más esté leyendo y después mover el resultado a su sitio. Ninguno de los dos exige parchear la biblioteca ni automatizar el escritorio. Ambos se resuelven tratando cada documento como una unidad de trabajo autocontenida, creada desde cero, 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 HotPDF Delphi Component para Delphi y C++Builder