Uma instância de THotPDF pode escrever qualquer número de documentos, um após o outro: um BeginDoc depois de um EndDoc inicia um documento novo e independente. A reutilização multi-documento de uma única instância está disponível desde o HotPDF v2.764.0. A mensagem Please load the document before using BeginDoc pertence a uma guarda vizinha: o LoadFromFile a levanta quando um documento iniciado com BeginDoc ainda não foi fechado com EndDoc. Os dois comportamentos decorrem do ciclo de vida do componente, e uma vez que ele se encaixa na cabeça, nenhum dos dois é misterioso
Uma instância THotPDF é um documento ou uma fábrica de documentos?
Uma instância de THotPDF é uma fábrica de documentos: você pode criá-la uma vez, por exemplo como um componente solto em um form, e alimentá-la com documentos do mesmo jeito que você manteria uma conexão de banco de dados aberta e dispararia query após query por ela. O BeginDoc abre um documento e marca a instância como tendo um documento em andamento. O EndDoc serializa tudo para FileName e depois descarta tudo o que foi registrado para aquele arquivo, incluindo imagens, fontes, campos de formulário, outlines, JavaScript de documento, named destinations, page labels, perfis ICC e output intents. As páginas e objetos do documento escrito continuam legíveis até o próximo BeginDoc liberá-los, então ValidatePDFUA2Structure, CurrentPageNumber e PagesCount ainda funcionam depois do EndDoc. Com ReproducibleOutput ligado, um segundo documento de uma instância reutilizada é byte a byte igual àquele documento vindo de uma instância nova
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. Em uma instância reutilizada, configurações de propriedades como FileName, Compression, PDFACompliance, Author e Title carregam para o próximo documento, então defina-as de novo antes de cada BeginDoc quando forem diferentes
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 BeginDoc, um EndDoc, um arquivo no disco. Um segundo arquivo é um segundo par BeginDoc e EndDoc, seja na mesma instância ou em uma nova
O que “reutilizar” significa: uma instância só, ou uma instância nova por arquivo
A versão econômica funciona: construa o componente uma vez, faça um loop sobre um lote e chame BeginDoc e EndDoc dentro do loop. Cada iteração produz um arquivo independente, e nada registrado para um documento chega ao seguinte. Resta uma armadilha para saída protegida: o EndDoc limpa UserPassword e OwnerPassword, então com ActivateProtection ainda ligado você precisa definir as senhas de novo antes do próximo BeginDoc, que de outro modo levanta uma exceção em vez de proteger o arquivo com senhas vazias. A versão abaixo toma o outro caminho e trata cada saída como seu próprio objeto de vida curta. O custo de alocação de criar um componente é trivial perto do trabalho de paginar e serializar um PDF, então não há quase nada a economizar acumulando a instância de qualquer 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); // 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 levantar uma exceção no meio de um documento, a instância daquela iteração ainda é liberada antes do próximo começar, então um registro ruim não deixa um componente pela metade para envenenar o resto da execução. Puxar o Create para cima do loop também funciona, mas um documento abandonado no meio por uma exceção é uma boa razão para dar ao próximo registro uma instância nova
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 a documentos que você cria do nada. Misturar os dois é o jeito mais comum de tropeçar na guarda de ciclo de vida descrita no topo deste artigo. Mantenha os dois fluxos mentalmente separados: BeginDoc ... EndDoc cria, LoadFromFile ... SaveLoadedDocument edita
O problema do bloqueio de arquivo é real, e a resposta não é matar janelas do visualizador
Erros de ciclo de vida costumam viajar com 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 Foxit, e então dispara uma reconstrução. O EndDoc tenta escrever o mesmo caminho, o sistema operacional recusa porque o visualizador segura um share de leitura que bloqueia escritores, e você recebe uma falha de acesso negado. Este é genuinamente um problema de file locking do Windows e não um problema 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 do documento: conclua um documento com EndDoc antes de iniciar ou carregar o próximo. O erro de bloqueio de arquivo quer que você honre o limite do arquivo: escreva onde nada mais está lendo, e depois mova o resultado para o lugar. Nenhum dos dois pede para contornar o componente ou scriptar o desktop. Ambos decorrem 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 Delphi Component para Delphi e C++Builder