Artigo Técnico

Reutilizar Instância THotPDF Entre Documentos no Delphi

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

Reutilização de THotPDF no Delphi: uma instância executa BeginDoc, desenho e EndDoc para cada arquivo de saída entre um único Create e Free, ou um loop em lote cria uma instância nova para cada arquivo
Todo documento executa BeginDoc, desenho, EndDoc; uma instância de THotPDF pode repetir esse ciclo antes do Free, ou cada arquivo de saída pode ganhar uma instância própria

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