Artigo Técnico

Reutilizando uma Instância THotPDF Entre Documentos no Delphi

O erro diz Please load the document before using BeginDoc, e quase sempre aparece na segunda vez. O primeiro documento é escrito corretamente. Então a mesma instância THotPDF é solicitada a iniciar um segundo, o BeginDoc gera exceção e a mensagem aponta para o carregamento de um documento, que é o oposto do que o código está tentando fazer. É a incompatibilidade entre o sintoma e a mensagem que torna esse erro persistente. O assunto real é o ciclo de vida do componente, e assim que isso faz sentido, o erro deixa de ser um mistério

Ciclo de vida do documento THotPDF mostrando Create, BeginDoc, EndDoc e Free por arquivo de saída
Uma instância THotPDF corresponde a um documento: Create, BeginDoc, desenhar, EndDoc, Free

Uma instância THotPDF é um documento, não uma fábrica de documentos

O modelo mental tentador é que THotPDF é um objeto de serviço que você inicializa uma vez e alimenta com documentos, da mesma forma que você poderia manter uma conexão de banco de dados aberta e executar consulta após consulta por meio dela. Não é isso. Uma instância modela a construção de um único documento, e sua máquina de estado interna carrega a premissa de que ela percorre o caminho uma vez só: do vazio, passando por um documento aberto, até um arquivo salvo. O BeginDoc abre esse caminho e marca a instância como tendo um documento em andamento. O EndDoc serializa tudo em FileName e o encerra. Chamar BeginDoc novamente na mesma instância já finalizada pede a ela que reentre em um estado que ela nunca deixou de forma limpa, e o guarda que dispara é aquele cuja mensagem por acaso menciona carregamento, porque internamente as condições 'pronto para começar' e 'tem um documento carregado' são verificadas juntas

Portanto a mensagem é enganosa, mas o guarda está fazendo o seu trabalho. Ele está se recusando a deixar você iniciar um documento novo sobre um componente que ainda acredita estar no meio de um documento. A correção não é derrotar o guarda. É parar de reutilizar uma instância já gasta

O ciclo de vida, na ordem em que precisa acontecer

Todo documento que o HotPDF escreve do zero segue as mesmas quatro etapas, e a ordem não é negociável. O Create aloca o componente. O BeginDoc abre o documento e fixa as escolhas estruturais, então qualquer coisa que afete o arquivo inteiro (tamanho da página, compactação, criptografia, nome do arquivo de saída) tem de ser definida entre o Create e o BeginDoc. Depois você desenha. Depois o EndDoc grava os bytes no disco. O Free libera a instância. Chamadas de desenho colocadas antes do BeginDoc não têm página onde aterrissar; propriedades de documento inteiro atribuídas depois dele são ignoradas sem reclamação

var
  Pdf: THotPDF;
begin
  Pdf := THotPDF.Create(nil);
  try
    Pdf.FileName := 'invoice.pdf';
    Pdf.BeginDoc;                        // abre o documento
    Pdf.CurrentPage.SetFont('Arial', [], 11);
    Pdf.CurrentPage.TextOut(50, 760, 0, 'Invoice 2026-042');
    Pdf.EndDoc;                          // grava invoice.pdf e o encerra
  finally
    Pdf.Free;                            // uma instância, um documento
  end;
end;

Leia isso como a unidade de trabalho. Um Create, um BeginDoc, um EndDoc, um Free, um arquivo no disco. No instante em que você quer um segundo arquivo, você está iniciando uma nova unidade de trabalho, o que significa uma nova instância

O que "reutilizar" deveria significar: uma instância nova por arquivo

A versão que quebra tenta ser econômica com alocação: construa o componente uma vez, faça um loop sobre um lote, chame BeginDoc e EndDoc dentro do loop. A segunda iteração gera exceção. A versão que funciona trata cada saída como seu próprio objeto de vida curta, e o custo de alocação de criar um componente é trivial perto do trabalho de compor e serializar um PDF, então não há nada a economizar acumulando a instância

procedure WriteBatch(const Names: TArray<string>);
var
  I: Integer;
  Pdf: THotPDF;
begin
  for I := 0 to High(Names) do
  begin
    Pdf := THotPDF.Create(nil);         // uma instância nova a cada passagem
    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;

O try/finally posicionado dentro do loop é a parte que vale a pena defender na revisão. Se o BeginDoc ou qualquer chamada de desenho gerar exceção no meio de um documento, a instância daquela iteração ainda é liberada antes de a próxima começar, então um registro defeituoso não deixa encalhado um componente montado pela metade nem envenena o resto da execução. Puxe o Create para fora, acima do loop, para 'otimizar', e você volta ao bug original, agora vestido de loop de lote

Modificar um arquivo existente é um ponto de entrada diferente

Há uma segunda leitura de "reutilizar" que é inteiramente legítima: você não quer um documento em branco, você quer abrir um PDF que já existe e alterá-lo. Esse caminho não passa pelo BeginDoc de forma alguma, que é exatamente por que a mensagem de erro menciona carregamento. Você carrega o arquivo, edita-o e salva com o nome que escolher

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;

O LoadFromFile retorna a contagem de páginas, e um valor de zero ou menos significa que o carregamento falhou, então vale a pena verificar antes de tocar em CurrentPage. O pareamento importa: um documento que você abriu com LoadFromFile é salvo com SaveLoadedDocument, não com o par BeginDoc/EndDoc, que pertence aos documentos que você cria a partir do nada. Misturar os dois é a maneira mais comum de confundir a mesma máquina de estado que produziu o erro original. Mantenha os dois fluxos separados mentalmente: BeginDoc ... EndDoc cria, LoadFromFile ... SaveLoadedDocument edita

O problema do bloqueio de arquivo é real, e a resposta não é matar janelas do visualizador

O erro de reutilização costuma vir acompanhado de uma segunda reclamação, e as duas se enredam porque afloram no mesmo fluxo de regenerar o arquivo. Um usuário abre o PDF que você acabou de produzir, deixa-o aberto no Acrobat ou no Foxit e então dispara uma reconstrução. O EndDoc tenta escrever no mesmo caminho, o sistema operacional recusa porque o visualizador mantém um compartilhamento de leitura que bloqueia quem escreve, e você recebe uma falha de acesso negado. Essa é genuinamente uma questão de bloqueio de arquivo do Windows, e não uma questão de estado do componente, e merece uma resposta de verdade em vez de uma solução de contorno

A solução de contorno que circula, enumerar as janelas de nível superior e postar WM_CLOSE para qualquer uma cujo título pareça um visualizador de PDF, é o instinto errado. Ela cruza os limites de processo para fechar janelas que o seu programa não possui, ela adivinha visualizadores pelo texto do título e pode descartar as anotações não salvas de um usuário sem perguntar. Trate toda essa abordagem como um cheiro ruim. A correção confiável é nunca escrever em um caminho que outro processo possa estar segurando. Serialize para um arquivo temporário no mesmo diretório e depois troque-o de lugar com uma renomeação atômica assim que o EndDoc for bem-sucedido. Se um visualizador ainda tiver o arquivo antigo aberto, a renomeação ou sucede de forma limpa ou falha ruidosamente, e você exibe uma mensagem clara em vez de brigar com o bloqueio

uses
  System.SysUtils, System.IOUtils;

procedure WritePdfAtomically(const FinalPath: string);
var
  Pdf: THotPDF;
  TempPath: string;
begin
  // Arquivo temporário no MESMO diretório do destino: uma renomeação dentro de
  // um volume NTFS troca o nome de forma atômica, enquanto uma movimentação
  // entre volumes degenera em copiar-e-apagar e perde essa garantia
  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;                    // o arquivo temporário está completo no disco aqui
    finally
      Pdf.Free;
    end;

    // Troca de lugar. TFile.Move se recusa a sobrescrever, então limpe um
    // destino obsoleto primeiro; se um visualizador ainda segurar o arquivo
    // antigo, é a exclusão que falha, ruidosamente, antes de os bytes bons
    // serem tocados
    if TFile.Exists(FinalPath) then
      TFile.Delete(FinalPath);
    TFile.Move(TempPath, FinalPath); // ou: RenameFile(TempPath, FinalPath)
  except
    if TFile.Exists(TempPath) then
      TFile.Delete(TempPath);        // nunca deixe encalhado um temporário escrito pela metade
    raise;
  end;
end;

Duas ressalvas honestas sobre esse código. O TFile.Move e o clássico RenameFile mapeiam ambos para a mesma renomeação do Windows, que é atômica somente quando origem e destino ficam no mesmo volume, e é exatamente por isso que o arquivo temporário vai para o diretório de destino em vez de TPath.GetTempPath. E o par apagar-depois-mover não é em si um único passo atômico: há uma breve janela na qual nenhum dos arquivos existe. Para um aplicativo desktop regenerando um relatório essa janela é irrelevante; leitores que precisem de um contrato mais forte no mesmo volume podem chamar diretamente o ReplaceFile do Win32 ou o MoveFileEx com MOVEFILE_REPLACE_EXISTING, que colapsa a troca em uma única chamada

Para um servidor de alto volume que regenera documentos constantemente, a disciplina mais limpa é escrever cada saída sob um nome exclusivo (um carimbo de data/hora ou um id de tarefa) para que duas execuções nunca disputem um caminho, e deixar uma política de retenção separada limpar os arquivos antigos. O padrão é uma linha de disciplina de nomenclatura por requisição

// Um caminho de saída por requisição: duas tarefas concorrentes nunca podem
// disputar o mesmo nome, então sem dança de renomeação e sem bloqueio a perder
OutName := Format('statement-%s-%s.pdf',
  [CustomerId, TGUID.NewGuid.ToString.Trim(['{', '}'])]);
Pdf.FileName := TPath.Combine(OutputDir, OutName);

Um id de requisição ou id de tarefa funciona tão bem quanto o GUID quando o framework ao redor já lhe entrega um, e isso torna o nome do arquivo rastreável de volta a uma linha de log de graça. De qualquer forma o princípio é o mesmo: projete de modo que o arquivo que você está escrevendo seja só seu no momento em que você o escreve. O bloqueio desaparece não porque você forçou o fechamento de uma janela, mas porque nada mais está tocando nos bytes

O formato da correção

Reduza os dois problemas às suas raízes e ambos têm a ver com respeitar limites. O erro da máquina de estado quer que você honre o limite da instância: um THotPDF, um documento, depois deixe-o ir e faça outro. O erro de bloqueio de arquivo quer que você honre o limite do arquivo: escreva onde nada mais esteja lendo e depois mova o resultado para o lugar. Nenhum dos dois pede corrigir a biblioteca com patch nem programar a área de trabalho por script. Ambos resultam de tratar cada documento como uma unidade de trabalho autocontida, criada do zero, escrita de forma limpa e liberada, que é o mesmo padrão que torna o resto do componente previsível

As chamadas BeginDoc, EndDoc, LoadFromFile e SaveLoadedDocument mostradas aqui fazem parte do HotPDF Component para Delphi e C++Builder