Artículo técnico

Reutilizar una instancia THotPDF entre documentos en Delphi

Una instancia de THotPDF puede escribir cualquier número de documentos uno tras otro: un 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 guarda vecina: LoadFromFile lo lanza cuando un documento iniciado con BeginDoc todavía no se ha cerrado con EndDoc. Ambos comportamientos se siguen del ciclo de vida del componente, y en cuanto eso encaja ninguno de los dos tiene misterio

Reutilización de THotPDF en Delphi: una sola instancia ejecuta BeginDoc, dibujar y EndDoc por cada archivo de salida entre un único Create y Free, o un bucle por lotes crea una instancia nueva por archivo
Cada documento ejecuta BeginDoc, dibujar, EndDoc; una instancia de THotPDF puede repetir ese ciclo antes del Free, o cada archivo de salida puede llevarse una instancia nueva propia

¿Es una instancia de THotPDF un documento o una fábrica de documentos?

Una instancia de THotPDF es una fábrica de documentos: puede crearla una vez, por ejemplo como componente depositado en un formulario, e irle 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. BeginDoc abre un documento y marca la instancia como que tiene un documento en curso. EndDoc serializa todo en FileName y después suelta todo lo registrado para ese archivo, incluidas sus imágenes, fuentes, campos de formulario, 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 idéntico al que produciría una instancia nueva

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. En una instancia reutilizada, los ajustes de propiedades como FileName, Compression, PDFACompliance, Author y Title se arrastran al documento siguiente, así que vuelva a fijarlos 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;

Léalo 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

Qué significa «reutilizar»: una sola instancia, o una instancia nueva por archivo

La versión frugal funciona: construya el componente una vez, recorra un lote en bucle y llame a BeginDoc y EndDoc dentro del bucle. Cada iteración produce un archivo independiente, y nada de lo registrado para un documento alcanza al siguiente. Queda una trampa para la salida protegida: EndDoc borra UserPassword y OwnerPassword, así que con ActivateProtection todavía activado tiene que volver a fijar 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 el otro camino y trata cada salida como su propio objeto de vida corta. El coste de asignación de crear un componente es insignificante frente al trabajo de maquetar y serializar un PDF, así que tampoco hay mucho 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. Sacar el Create por encima del bucle también funciona, pero un documento abandonado a mitad por una excepción es una buena razón para darle al siguiente registro una instancia nueva

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 en absoluto por BeginDoc, 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 tropezar con la guarda del ciclo de vida descrita al principio de este artículo. 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

Los errores del ciclo de vida suelen 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 al 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 del documento: termine un documento con EndDoc antes de iniciar o cargar el siguiente. 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 trabajar alrededor del componente ni automatizar el escritorio. Ambos se derivan de tratar 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