Artigo Técnico

Buscar e Substituir Texto em um PDF Existente com Delphi

O HotPDF Component pode buscar e substituir texto dentro de um PDF existente a partir do Delphi e C++Builder. O SearchLoadedPageText e o SearchLoadedDocumentText localizam cada ocorrência de uma string com precisão a nível de glifo, e o ReplaceLoadedPageText e o ReplaceLoadedDocumentText regravam os bytes correspondentes no local — desde que cada caractere de substituição possa ser codificado novamente através da fonte original, uma restrição física que este artigo trata com honestidade em vez de esconder em uma nota de rodapé

A solicitação por trás desse recurso é sempre comum. Uma empresa muda de nome e três mil faturas arquivadas ainda trazem o nome antigo. Um modelo de contrato foi enviado com a data de validade do ano passado. Um código de produto foi descontinuado e cada folha de dados (datasheet) que o menciona precisa do código sucessor em seu lugar. Em um processador de texto, cada uma dessas tarefas leva trinta segundos. Em um PDF, é um problema genuinamente difícil, e entender o porquê faz a diferença entre usar bem a API ou registrar um relatório de bug que na verdade é apenas uma citação da especificação

Por que substituir texto em um PDF é tão difícil?

Substituir texto em um PDF é difícil porque uma página PDF não contém texto editável — ela contém glifos posicionados. Sob o modelo de exibição de texto da ISO 32000-1 §9.4, um fluxo de conteúdo direciona operadores como Tj and TJ que desenham sequências de códigos de caracteres nas coordenadas estabelecidas pela matriz de texto. Esses códigos não são Unicode; eles são índices para qualquer codificação que a fonte da página declare, e o mapeamento de volta para caracteres legíveis pode residir em um /ToUnicode CMap, uma matriz de diferenças de codificação ou uma cadeia de mapeamento CID. Não há objeto de parágrafo, nem fluxo de texto e nenhuma garantia de que uma palavra visual seja armazenada como uma única string

Buscando texto: busca a nível de glifo com rastreamento de deslocamento de bytes

O SearchLoadedDocumentText do HotPDF encontra cada ocorrência de um termo de busca correspondendo-o com a sequência decodificada de glifos Unicode de cada página, e não com os bytes brutos do fluxo, de modo que uma correspondência ocorra independentemente de como a fonte a codificou. A infraestrutura subjacente foi introduzida na v2.251.0: o tokenizador de fluxo de conteúdo (content stream tokenizer) registra um intervalo de bytes StartOfs/EndOfs para cada operando de string — incluindo seus delimitadores ( ) ou < > — e cada glifo decodificado carrega uma tripla TokenIndex/ItemIndex/ByteOffset que aponta de volta para o operando exato, item de matriz TJ e unidade de código que o produziu. O mesmo interpretador de glifos alimenta a API de extração descrita em extração de texto de um PDF carregado no Delphi; a busca simplesmente mantém a procedência que a extração descarta

Each match comes back as a THPDFTextMatch record carrying the page index, the inclusive glyph range, the user-space X/Y origin and width of the hit, the source token and item index, and the matched text itself. That is enough to drive a highlight overlay, a review UI, or the replace step. A search that finds nothing returns an empty array rather than failing, so the calling pattern stays simple

var
  Pdf: THotPDF;
  Matches: THPDFTextMatchArray;
  I: Integer;
begin
  Pdf := THotPDF.Create(nil);
  try
    if Pdf.LoadFromFile('invoices-2025.pdf') > 0 then
    begin
      if Pdf.SearchLoadedDocumentText('Acme Corp', False, Matches) then
        for I := 0 to Length(Matches) - 1 do
          WriteLn(Format('page %d at (%.1f, %.1f): "%s"',
            [Matches[I].PageIndex, Matches[I].X, Matches[I].Y,
             Matches[I].Text]));
    end;
  finally
    Pdf.Free;
  end;
end;

Uma escolha de design deliberada merece destaque. Quando CaseSensitive é False, a comparação converte maiúsculas/minúsculas apenas para caracteres ASCII, por design: a conversão completa de maiúsculas/minúsculas Unicode se comporta de maneira diferente nas cadeias de ferramentas do Delphi 5 ao XE suportadas pelo HotPDF, e uma API de busca que encontra correspondências diferentes dependendo de qual compilador construiu sua aplicação é pior do que uma com um limite documentado e previsível. Para texto comercial latino — nomes, códigos, datas —, a conversão ASCII cobre os casos práticos

Substituindo texto: codificação reversa e emenda cirúrgica

O ReplaceLoadedDocumentText, adicionado no HotPDF v2.252.0, regravar cada ocorrência de um termo executando o mecanismo de decodificação no sentido inverso. A função HPDFEncodeUnicode é o inverso do decodificador de código de caracteres: ela percorre a mesma cadeia de estratégias em ordem reversa — busca de bfchar e bfrange /ToUnicode, mapeamento CID de fluxo de codificação, mapeamentos de identidade do Tipo0 e as tabelas WinAnsi e MacRoman predefinidas — para transformar cada caractere de substituição de volta nos bytes de código de caracteres que a fonte original espera. Os bytes codificados novamente são então serializados em uma string literal bem-formada ou string hexadecimal, espelhando as próprias regras de escape do tokenizador para que uma viagem de ida e volta de análise → reserialização seja estável

A emenda em si é cirúrgica, e não por atacado. Apenas o intervalo de bytes de código coberto pelo resultado é substituído dentro do operando de string; bytes não correspondentes no mesmo operando, o espaço em branco entre tokens e cada operador adjacente são preservados na íntegra, byte por byte. Substituir bca dentro de abcabc resulta em a + substituição + bc, e não em um operando corrompido. As substituições podem ser mais curtas ou mais longas do que o termo original — o literal é reserializado e o comprimento /Length do fluxo é atualizado — e cada fluxo /Contents de uma página de múltiplos fluxos é processado isoladamente para que a página permaneça bem-formada

var
  Pdf: THotPDF;
  ReplaceCount: Integer;
begin
  Pdf := THotPDF.Create(nil);
  try
    if Pdf.LoadFromFile('contract-draft.pdf') > 0 then
    begin
      if Pdf.ReplaceLoadedDocumentText('2025-12-31', '2026-12-31',
        True, ReplaceCount) then
        WriteLn(Format('%d operand rewrites performed', [ReplaceCount]));
      Pdf.SaveLoadedDocument('contract-final.pdf');
    end;
  finally
    Pdf.Free;
  end;
end;

Observe o que a API não faz: ela não redefine a diagramação da página. O PDF não possui refluxo (reflow), portanto uma substituição que seja visualmente mais larga do que a original simplesmente ocupará mais espaço horizontal e poderá sobrepor o que quer que tenha sido desenhado à sua direita. Substituições de mesmo comprimento ou comprimentos aproximados — datas, strings de versão, números de peça, correções de nomes — são o cenário ideal. A reformulação completa do texto pertence ao documento de origem, e não ao PDF

Por que você não pode substituir texto por caracteres que o subconjunto de fonte nunca incluiu?

Você não pode substituir texto por um caractere que o subconjunto da fonte incorporada nunca incluiu, porque a sequência de bytes que selecionaria esse caractere simplesmente não existe nas tabelas de mapeamento da fonte. Quando um gerador de PDF incorpora uma fonte de subconjunto, o seu /ToUnicode CMap e as estruturas de codificação cobrem apenas os glifos que o documento original realmente utilizou. O HPDFEncodeUnicode só pode reverter um mapeamento que esteja presente: se o documento nunca conteve a letra E naquela fonte, não haverá código de caractere para E ao qual reverter. Esta é uma propriedade física do arquivo, e não uma limitação de uma biblioteca específica — nenhuma ferramenta pode evocar um mapeamento de glifo que nunca foi incorporado

O HotPDF trata a falha de forma conservadora. Se qualquer caractere individual da substituição não puder ser codificado novamente, toda a ocorrência daquele termo de busca será ignorada — sem exceções, sem textos parciais ilegíveis, e a ocorrência simplesmente não é contabilizada no ReplaceCount. A consequência prática: verifique o ReplaceCount em relação à contagem de correspondências de uma busca anterior e trate qualquer diferença como um sinal. No exemplo de data acima, o dígito 6 deve aparecer em algum lugar no texto do documento na mesma fonte para que a regravação tenha sucesso — provável em uma fatura, mas nunca garantido no caso geral. Quando os caracteres que você precisa simplesmente não estão disponíveis e o objetivo é remover texto sensível em vez de reformulá-lo, a remoção real de conteúdo é a melhor ferramenta; consulte o artigo sobre redação e reestruturação de PDFs carregados no Delphi para essa rota

var
  Matches: THPDFTextMatchArray;
  Expected, Replaced: Integer;
begin
  Pdf.SearchLoadedDocumentText('Acme Corp', True, Matches);
  Expected := Length(Matches);
  Pdf.ReplaceLoadedDocumentText('Acme Corp', 'Apex Corp', True, Replaced);
  if Replaced < Expected then
    WriteLn(Format('%d occurrence(s) skipped: characters missing ' +
      'from the font subset, or match spans multiple operands',
      [Expected - Replaced]));
end;

A segunda condição de omissão naquela mensagem é o outro limite documentado: um termo de busca que se estende por múltiplos operandos de string — por exemplo, Hello dividido nos itens [(He)(llo)] TJ — é encontrado pela busca, pois ela corresponde à sequência decodificada de glifos, mas é ignorado pela substituição, porque regravar através de limites de operandos exigiria a fusão de intervalos de bytes adjacentes. Buscar e depois verificar torna ambos os limites visíveis em vez de silenciosos

O que muda no arquivo quando você salva?

Um fluxo /Contents substituído é salvo sem compactação. Os fluxos compactados por FlateDecode são descompactados para edição e, quando o HotPDF grava os bytes reconstruídos, ele remove a entrada /Filter do fluxo e atualiza /Length em vez de compactar novamente. O PDF resultante é totalmente válido e renderizado normalmente nos visualizadores mais comuns; a desvantagem é um arquivo maior para cada fluxo editado. Para um pipeline de lote que processa milhares de documentos, planeje esse crescimento ou execute uma passagem de compactação separada posteriormente. Como objetos regravados interagem com a estrutura de referência cruzada do documento ao salvar é um assunto próprio, abordado na fluxos de objetos e atualizações incrementais no HotPDF

Tudo o mais sobre o arquivo permanece inalterado. Os fluxos intocados mantêm sua compactação, fontes e imagens não são regravadas, e a emenda ao nível do operando significa que até os fluxos editados diferem do original apenas onde ocorreu a correspondência. Esse conservadorismo é deliberado: quanto mais partes de um documento carregado uma biblioteca regrava, mais oportunidades ela tem de quebrar alguma particularidade do gerador que ela não antecipou

A busca e substituição de texto junta-se à extração, redação e renderização de página no conjunto de ferramentas para documentos carregados do HotPDF, tudo direcionado pelo mesmo interpretador de fluxo de conteúdo e disponível do Delphi 5 às versões atuais do RAD Studio sem dependências externas. A referência completa da API e o download da versão de avaliação estão na página do produto HotPDF Component