O erro apresenta Please load the document before using BeginDoc e surge quase sempre à segunda vez. O primeiro documento é escrito sem problemas. Depois, a mesma instância de THotPDF é chamada a iniciar um segundo documento, BeginDoc gera uma exceção e a mensagem aponta para o carregamento de um documento, o que é o oposto do que o código está a tentar fazer. É o desfasamento entre o sintoma e a mensagem que torna este erro tão difícil de esquecer. O verdadeiro tema é o ciclo de vida do componente e, assim que isso fica claro, o erro deixa de ser misterioso

Uma instância de THotPDF é um documento, não uma fábrica de documentos
O modelo mental tentador é pensar que THotPDF é um objeto de serviço que se cria uma vez e ao qual se vão enviando documentos, tal como se poderia manter uma ligação a uma base de dados aberta e executar consulta atrás de consulta através dela. Não é esse o caso. Uma instância modela um único documento em construção, e a sua máquina de estados interna assume que o percurso é percorrido apenas uma vez: de vazio, passando por um documento aberto, até um ficheiro guardado. BeginDoc abre esse percurso e marca a instância como tendo um documento em curso. EndDoc serializa tudo para FileName e fecha o processo. Chamar BeginDoc novamente na mesma instância já concluída pede-lhe que reentre num estado do qual nunca saiu de forma limpa, e a verificação que dispara é precisamente aquela cuja mensagem menciona o carregamento, porque, internamente, as condições "pronto para iniciar" e "tem um documento carregado" são verificadas em conjunto
Portanto, a mensagem é enganadora, mas a verificação está a cumprir a sua função: recusa-se a permitir iniciar um novo documento sobre um componente que ainda acredita estar a meio de um documento. A correção não passa por contornar essa verificação, mas sim por deixar de reutilizar uma instância já esgotada
O ciclo de vida, pela ordem em que tem de acontecer
Todos os documentos que o HotPDF escreve de raiz seguem os mesmos quatro passos, e a ordem não é negociável. Create aloca o componente. BeginDoc abre o documento e fixa as escolhas estruturais, pelo que tudo o que afeta o ficheiro como um todo (tamanho de página, compressão, encriptação, nome do ficheiro de saída) tem de ser definido entre Create e BeginDoc. Depois desenha-se. Depois EndDoc escreve os bytes em disco. Free liberta a instância. As chamadas de desenho colocadas antes de BeginDoc não têm nenhuma página onde assentar; as propriedades do documento inteiro atribuídas depois dele são ignoradas sem qualquer aviso
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; // escreve invoice.pdf, fecha o processo
finally
Pdf.Free; // uma instância, um documento
end;
end;
Interpretar isto como a unidade de trabalho: um Create, um BeginDoc, um EndDoc, um Free, um ficheiro em disco. No momento em que se pretende um segundo ficheiro, está a iniciar-se uma nova unidade de trabalho, o que significa uma nova instância
O que "reutilizar" deveria significar: uma instância nova por ficheiro
A versão que falha tenta ser poupada na alocação: construir o componente uma vez, percorrer um lote em ciclo, chamando BeginDoc e EndDoc dentro do ciclo. A segunda iteração gera uma exceção. A versão que funciona trata cada ficheiro de saída como um objeto próprio e de vida curta, e o custo de alocação de criar um componente é irrisório face ao trabalho de compor e serializar um PDF, pelo que não há nada a ganhar em reter 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); // nova instância 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 colocado dentro do ciclo é a parte que vale a pena defender numa revisão de código. Se BeginDoc ou qualquer chamada de desenho gerar uma exceção a meio de um documento, a instância dessa iteração é ainda assim libertada antes de a seguinte começar, pelo que um registo com problemas não deixa um componente meio construído a bloquear e a contaminar o resto da execução. Retirar o Create para fora do ciclo a pretexto de "otimizar" faz regressar ao erro original, agora disfarçado de ciclo em lote
Modificar um ficheiro existente é um ponto de entrada diferente
Existe uma segunda leitura de "reutilização" totalmente legítima: não se quer um documento em branco, mas sim abrir um PDF já existente e alterá-lo. Esse percurso não passa de todo por BeginDoc, o que explica exatamente por que a mensagem de erro menciona o carregamento. Carrega-se o ficheiro, edita-se e guarda-se com o nome que se 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;
LoadFromFile devolve o número de páginas, e um valor igual ou inferior a zero significa que o carregamento falhou, pelo que vale a pena verificar isso antes de tocar em CurrentPage. O emparelhamento importa: um documento aberto com LoadFromFile é guardado com SaveLoadedDocument, não com o par BeginDoc/EndDoc, que pertence aos documentos criados a partir do nada. Misturar os dois é a forma mais comum de confundir a mesma máquina de estados que produziu o erro original. Convém manter os dois fluxos mentalmente separados: BeginDoc ... EndDoc cria, LoadFromFile ... SaveLoadedDocument edita
O problema do bloqueio de ficheiros é real, e a resposta não é fechar à força as janelas do visualizador
O erro de reutilização surge muitas vezes acompanhado de uma segunda queixa, e as duas acabam por se confundir porque aparecem no mesmo fluxo de trabalho de regeneração do ficheiro. Um utilizador abre o PDF que acabou de ser produzido, deixa-o aberto no Acrobat ou no Foxit e, em seguida, desencadeia uma reconstrução. EndDoc tenta escrever no mesmo caminho, o sistema operativo recusa porque o visualizador mantém uma partilha de leitura que bloqueia escritores, e obtém-se uma falha de acesso negado. Este último é genuinamente um problema de bloqueio de ficheiros do Windows, e não um problema de estado do componente, e merece uma resposta real em vez de uma solução de recurso
A solução de recurso que circula por aí — enumerar as janelas de topo e enviar WM_CLOSE a tudo o que tenha um título com aspeto de visualizador de PDF — é o instinto errado. Atravessa os limites de outros processos para fechar janelas que o programa não possui, adivinha os visualizadores pelo texto do título e pode deitar fora anotações não guardadas de um utilizador sem sequer perguntar. É de evitar toda esta abordagem. A correção fiável é nunca escrever num caminho que outro processo possa estar a manter aberto. Serializar para um ficheiro temporário na mesma pasta e, depois, trocá-lo para o lugar definitivo com uma renomeação atómica assim que EndDoc for bem-sucedido. Se um visualizador ainda tiver o ficheiro antigo aberto, a renomeação ou é bem-sucedida de forma limpa ou falha ruidosamente, apresentando uma mensagem clara em vez de lutar contra o bloqueio
uses
System.SysUtils, System.IOUtils;
procedure WritePdfAtomically(const FinalPath: string);
var
Pdf: THotPDF;
TempPath: string;
begin
// Ficheiro temporário na MESMA pasta do destino: uma renomeação dentro
// do mesmo volume NTFS troca o nome de forma atómica, enquanto uma
// mudança entre volumes degrada para copiar-e-eliminar 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 ficheiro temporário está completo em disco aqui
finally
Pdf.Free;
end;
// Troca para o lugar definitivo. TFile.Move recusa-se a substituir,
// por isso elimina-se primeiro um destino desatualizado; se um visualizador
// ainda mantiver o ficheiro antigo aberto, é a eliminação que falha, ruidosamente
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 deixar um ficheiro temporário meio escrito
raise;
end;
end;
Duas notas honestas sobre esse código. TFile.Move e o clássico RenameFile mapeiam ambos para a mesma renomeação do Windows, que só é atómica quando a origem e o destino estão no mesmo volume, e é exatamente por isso que o ficheiro temporário fica na pasta de destino em vez de TPath.GetTempPath. E o par eliminar-depois-mover não é, em si, um único passo atómico: existe uma pequena janela em que nenhum dos ficheiros existe. Para uma aplicação de ambiente de trabalho que regenera um relatório, essa janela é irrelevante; quem precisar de uma garantia mais forte no mesmo volume pode chamar diretamente o ReplaceFile ou o MoveFileEx do Win32 com MOVEFILE_REPLACE_EXISTING, o que reduz a troca a uma única chamada
Num servidor de grande volume que regenera documentos constantemente, a disciplina mais limpa é escrever cada ficheiro de saída com um nome único (uma marca temporal ou um id de tarefa), de modo a que duas execuções nunca disputem o mesmo caminho, deixando que uma política de retenção separada limpe os ficheiros antigos. O padrão resume-se a uma linha de disciplina de nomenclatura por pedido
// Um caminho de saída por pedido: duas tarefas simultâneas nunca podem disputar
// o mesmo nome, por isso não há dança de renomeações nem bloqueio a perder
OutName := Format('statement-%s-%s.pdf',
[CustomerId, TGUID.NewGuid.ToString.Trim(['{', '}'])]);
Pdf.FileName := TPath.Combine(OutputDir, OutName);
Um id de pedido ou de tarefa funciona tão bem como o GUID quando a framework envolvente já fornece um, e torna o nome do ficheiro rastreável até uma linha de registo sem qualquer custo adicional. De qualquer forma, o princípio é o mesmo: conceber o sistema de modo a que o ficheiro que se está a escrever pertença exclusivamente a quem o escreve no momento em que o escreve. O bloqueio desaparece não porque se forçou o fecho de uma janela, mas porque mais nada está a tocar nos bytes
O formato da correção
Reduzidos à sua raiz, ambos os problemas resumem-se a respeitar limites. O erro da máquina de estados pede que se respeite o limite da instância: um THotPDF, um documento, depois libertá-lo e criar outro. O erro de bloqueio de ficheiros pede que se respeite o limite do ficheiro: escrever onde mais nada está a ler, depois mover o resultado para o lugar definitivo. Nenhum dos dois exige corrigir a biblioteca ou automatizar o ambiente de trabalho. Ambos decorrem de tratar cada documento como uma unidade de trabalho autocontida, criada de novo, escrita de forma limpa e libertada, que é o mesmo padrão que torna o resto do componente previsível
As chamadas BeginDoc, EndDoc, LoadFromFile e SaveLoadedDocument aqui apresentadas fazem parte do HotPDF Delphi Component para Delphi e C++Builder