Una instancia THotPDF puede escribir cualquier cantidad de documentos una tras otro: BeginDoc después de EndDoc inicia un documento nuevo e independiente. La reutilización multi-documento de una sola instancia está disponible desde HotPDF v2.764.0. El mensaje Please load the document before using BeginDoc pertenece a una protección vecina: LoadFromFile lo lanza cuando un documento iniciado con BeginDoc aún no se ha cerrado con EndDoc. Ambos comportamientos se deducen del ciclo de vida del componente, y una vez que eso hace clic, ninguno es misterioso
¿Una instancia THotPDF es un documento o una fábrica de documentos?
Una instancia THotPDF es una fábrica de documentos: usted puede crearla una vez, por ejemplo como un componente soltado en un formulario, y alimentarla con 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. BeginDoc abre un documento y marca la instancia como con un documento en curso. EndDoc serializa todo a FileName y luego suelta todo lo registrado para ese archivo, incluidas sus imágenes, fuentes, campos de formulario, esquemas (outlines), JavaScript de documento, destinos con nombre, etiquetas de página, perfiles ICC y output intents. Las páginas y los objetos del documento escrito siguen siendo legibles hasta que el siguiente BeginDoc los libera, así que ValidatePDFUA2Structure, CurrentPageNumber y PagesCount siguen funcionando después de EndDoc. Con ReproducibleOutput activado, un segundo documento de una instancia reutilizada es byte a byte igual que ese documento producido por una instancia nueva
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. En una instancia reutilizada, las propiedades como FileName, Compression, PDFACompliance, Author y Title se arrastran al siguiente documento, así que vuelva a establecerlas antes de cada BeginDoc cuando difieran
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 BeginDoc, un EndDoc, un archivo en disco. Un segundo archivo es un segundo par BeginDoc y EndDoc, sobre la misma instancia o sobre una nueva
Lo que "reutilizar" significa: una sola instancia, o una instancia nueva por archivo
La versión frugal funciona: construya el componente una vez, itere sobre un lote y llame a BeginDoc y EndDoc dentro del ciclo. Cada iteración produce un archivo independiente, y nada de lo registrado para un documento llega al siguiente. Queda una trampa para la salida protegida: EndDoc limpia UserPassword y OwnerPassword, así que con ActivateProtection todavía activado hay que volver a establecer las contraseñas antes del siguiente BeginDoc, que de otro modo lanza una excepción en lugar de proteger el archivo con contraseñas vacías. La versión de abajo toma la otra ruta y trata cada salida como su propio objeto de vida corta. El costo de asignación de crear un componente es trivial junto al trabajo de maquetar y serializar un PDF, así que hay poco que ahorrar acaparando la instancia de cualquier forma
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. Sacar el Create por encima del ciclo también funciona, pero un documento abandonado a mitad de camino por una excepción es una buena razón para darle al siguiente registro una instancia nueva
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 tropezar con el guard del ciclo de vida descrito al inicio de este artículo. 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
Los errores del ciclo de vida suelen 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 del documento: termine un documento con EndDoc antes de iniciar o cargar el siguiente. 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 trabajar alrededor del componente 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 HotPDF Delphi Component para Delphi y C++Builder