Artigo Técnico

Comandos de Web Capture do Catalog SpiderInfo em PDF Delphi

A PDF Library for Delphi (PDFlibPas) grava metadados de comandos Web Capture no catálogo do documento via AddWebCaptureCommand, preenchendo o dicionário /SpiderInfo definido na ISO 32000-1 §14.10. Cada comando registra a URL que foi buscada, quantos níveis de profundidade a busca percorreu e um pequeno conjunto de flags. Ela registra intenção — não busca nada

Essa distinção é exatamente o motivo de essa API ser fácil de interpretar errado. Um engenheiro de suporte te entrega um PDF montado a partir da documentação online de um fornecedor e pergunta de onde veio a página 40. Se o produtor preencheu /SpiderInfo, a resposta está no catálogo: uma URL, uma profundidade de crawl, talvez o corpo do POST que gerou a página. Se o produtor não preencheu, nenhuma leitura do content stream vai te dizer isso. Gravar essa procedência é barato, estruturalmente chato e quase sempre pulado

O que o dicionário /SpiderInfo do catálogo registra de fato?

/SpiderInfo é um dicionário de informações de Web Capture pendurado no catálogo do documento e guarda exatamente duas coisas que a PDF Library for Delphi considera: /V, uma versão numérica do banco de dados de Web Capture que a biblioteca sempre grava como 1.0, e /C, um array de dicionários de comando. O número de versão não é a versão do PDF — ele identifica o próprio modelo de dados de Web Capture, e 1.0 é o único valor que a ISO 32000-1 §14.10 define. Tudo que interessa vive em /C. Cada dicionário de comando carrega /URL (o endereço que foi solicitado), /L (quantos níveis de links foram seguidos a partir dele) e /F (flags de travessia). Três chaves opcionais descrevem a própria requisição quando ela não era um GET simples: /P para dados de POST, /CT para o content type desses dados e /H para cabeçalhos adicionais. A PDF Library for Delphi mapeia todos os seis em um único record, TPDFlibWebCaptureCommand, cujos campos são URL, Levels, Flags, PostData, ContentType e Headers, todos AnsiString exceto os dois contadores Integer. A presença de /SpiderInfo também eleva a versão mínima do documento: AddWebCaptureCommand chama EnsureMinVersion('1.3', 'Catalog /SpiderInfo Web Capture command'), e as regras de detecção de versão da PDF Library for Delphi registram uma regra PDF 1.3 correspondente, de modo que um documento relido mais tarde reporte o mesmo piso

Estrutura do dicionário Catalog SpiderInfo gravado pela PDFlibPas: /SpiderInfo carrega /V definido como a versão 1.0 do banco de dados de Web Capture e /C como um array de referências indiretas, cada dicionário de comando guardando /URL, /L, /F e as chaves opcionais /P, /CT, /H que mapeiam para os campos do record TPDFlibWebCaptureCommand
A lista de comandos fica pendurada no catálogo do documento como uma entrada de versão mais um array de referências indiretas, então uma busca compartilhada pode ser referenciada de mais de um lugar enquanto o piso de versão sobe para PDF 1.3

Por que um comando de Web Capture precisa ser um objeto indireto

A única regra estrutural que derruba todo mundo: um dicionário de comando precisa ser registrado como objeto indireto antes de ir para /C, porque a ISO 32000-1 §14.10 especifica /C como um array de referências indiretas a dicionários de comando, não um array de dicionários. Empurre um dicionário direto para esse array e você obtém algo que faz parse como PDF válido e não significa nada como registro de Web Capture — um viewer ou uma ferramenta de arquivamento percorrendo a lista de comandos encontra um objeto inline onde deveria haver uma referência, e a entrada simplesmente não é um comando. A PDF Library for Delphi cuida disso dentro de AddWebCaptureCommand: ela monta o dicionário, entrega a OnNewIndObj para obter um object number e um generation number, embrulha esse par em uma referência indireta e anexa a referência ao array. Se OnNewIndObj não produzir um object number positivo, a função retorna 0 e o array fica intocado. O motivo de a spec insistir em indireção é que um único comando pode ser compartilhado — a mesma busca pode ser referenciada de mais de um lugar em uma estrutura de Web Capture — e objetos compartilhados em PDF são, por definição, indiretos

Como você adiciona um comando de Web Capture a partir do Delphi?

Preencha o record por completo e então chame AddWebCaptureCommand, que retorna o índice 1-based do novo comando ou 0 se a validação o rejeitou. Completude importa mais do que o normal aqui. O Delphi inicializa os campos gerenciados AnsiString de um record local como vazios, mas os campos Integer — Levels e Flags — não recebem esse tratamento, então um record que você esqueceu de preencher chega carregando o que estivesse na stack. AddWebCaptureCommand exige Levels de pelo menos 1 e Flags não negativo, e um valor-lixo de stack ou é rejeitado na hora ou, pior, aceito silenciosamente como uma profundidade que parece plausível

Fluxo de validação de AddWebCaptureCommand na PDFlibPas: preencha todos os campos do record porque Levels e Flags são Integers puros que carregam lixo de stack, exija Levels de pelo menos 1, rejeite Flags fora da whitelist 1, 2, 4, crie /SpiderInfo de forma lazy, registre o comando como objeto indireto via OnNewIndObj e retorne o índice 1-based ou 0
Toda rejeição acontece antes de o documento ser tocado: um record que falha nas checagens de contador ou flags retorna 0, deixa o catálogo exatamente como estava e nunca grava um placeholder vazio no arquivo
var
  Lib: TPDFlib;
  Command: TPDFlibWebCaptureCommand;
  Index: Integer;
begin
  Lib := TPDFlib.Create;
  try
    if Lib.LoadFromFile('captured-site.pdf', '') = 1 then
    begin
      // Atribua todos os campos: Levels e Flags são Integers comuns e
      // carregam lixo de pilha se você os pular.
      Command.URL := 'https://example.com/docs/index.html';
      Command.Levels := 2;
      Command.Flags := PDF_WEB_CAPTURE_SAME_SITE or PDF_WEB_CAPTURE_SAME_PATH;
      Command.PostData := '';
      Command.ContentType := '';
      Command.Headers := '';

      // Retorna o novo índice de comando 1-based, ou 0 em caso de rejeição.
      Index := Lib.AddWebCaptureCommand(Command);
      if Index = 0 then
        raise Exception.Create('Web Capture command rejected');

      Lib.SaveToFile('captured-site.pdf');
    end;
  finally
    Lib.Free;
  end;
end;

A primeira chamada bem-sucedida também é o que cria a estrutura. AddWebCaptureCommand pede o array de comandos ao helper interno WebCaptureCommands(True), e esse helper cria /SpiderInfo com sua entrada /V e depois um array /C vazio apenas se eles não existirem. Ele é deliberadamente conservador sobre o que encontra: se o catálogo já tem uma chave /SpiderInfo cujo valor não é um dicionário, ou uma chave /C cujo valor não é um array, o helper retorna nil em vez de sobrescrever o objeto existente. Uma estrutura herdada malformada faz a adição falhar; ela não faz a PDF Library for Delphi destruir o que o produtor anterior gravou

A whitelist de flags: três bits definidos e nada mais

AddWebCaptureCommand valida Flags contra uma whitelist e rejeita qualquer valor com um bit definido fora das três constantes que a PDF Library for Delphi define: PDF_WEB_CAPTURE_SAME_SITE = 1 (seguir apenas links que permanecem no mesmo site), PDF_WEB_CAPTURE_SAME_PATH = 2 (seguir apenas links sob o mesmo path) e PDF_WEB_CAPTURE_SUBMIT = 4 (esta requisição foi um submit de formulário, não uma recuperação simples). Passe 8, ou 16, ou um -1 acidental, e a chamada retorna 0 sem tocar no documento

Whitelist de flags imposta por AddWebCaptureCommand na PDFlibPas: o campo de bits /F aceita exatamente três bits definidos, PDF_WEB_CAPTURE_SAME_SITE definido como 1, PDF_WEB_CAPTURE_SAME_PATH definido como 2 e PDF_WEB_CAPTURE_SUBMIT definido como 4, enquanto qualquer bit indefinido como 8 ou um menos 1 acidental é recusado no momento da escrita
Um bit indefinido é indistinguível de um corrompido, então recusar o valor no momento da escrita mantém a ambiguidade fora do arquivo e uma combinação legal como 1 ou 2 definida como 3 ainda passa

Isso é mais rígido do que parsers de PDF normalmente são, e de propósito. /F é um campo de bits sem estrutura autodescritiva, então um bit indefinido é indistinguível de um corrompido, e um consumidor que depois relê o valor não tem como distinguir uma extensão futura de um typo. Recusar no momento da escrita mantém a ambiguidade totalmente fora do arquivo. Note também que /P, /CT e /H só são gravados quando os campos correspondentes do record não estão vazios, então um GET comum produz um dicionário de comando de três chaves — /URL, /L, /F — sem placeholders de string vazia poluindo, que é o que um leitor fazendo parse do arquivo espera para uma requisição que não carregou corpo

// Um envio de formulário: corpo POST mais o tipo de conteúdo.
Command.URL := 'https://example.com/search';
Command.Levels := 1;
Command.Flags := PDF_WEB_CAPTURE_SUBMIT;          // 4
Command.PostData := 'q=iso32000&scope=all';
Command.ContentType := 'application/x-www-form-urlencoded';
Command.Headers := 'Accept-Language: en-US';
if Lib.AddWebCaptureCommand(Command) = 0 then
  raise Exception.Create('submission command rejected');

// Same-site mais same-path é uma combinação válida: 1 ou 2 = 3.
Command.Flags := PDF_WEB_CAPTURE_SAME_SITE or PDF_WEB_CAPTURE_SAME_PATH;

// O bit 3 não é definido pela especificação, então este nunca chega ao arquivo.
Command.Flags := 8;
if Lib.AddWebCaptureCommand(Command) <> 0 then
  raise Exception.Create('undefined flag bit should have been refused');

Lendo comandos de volta: uma chave, duas representações legais

GetWebCaptureCommand retorna 1 em caso de sucesso e 0 caso contrário, e precisa ser mais tolerante que o writer, porque está lendo arquivos que outros produtores gravaram. Duas assimetrias importam. Primeiro, o reader aceita uma entrada de comando seja ela uma referência indireta ou um dicionário direto, dereferenciando quando necessário — a PDF Library for Delphi sempre grava a forma indireta que a spec exige, mas recusar ler um arquivo relaxado não ajuda ninguém. Segundo, /P pode ser tanto uma string quanto um stream, e isso generaliza bem além de Web Capture. A ISO 32000-1 repetidamente permite que uma única chave guarde uma string ou um stream quando o valor pode ser arbitrariamente grande — corpos de POST aqui, e o mesmo padrão aparece em outros lugares do formato para metadados e payloads embutidos. O helper interno ReadString em GetWebCaptureCommand portanto recebe uma flag AllowStream, e apenas a leitura de /P passa True: um valor de stream é decodificado pela sua cadeia de filtros e retornado como o campo PostData, enquanto /URL, /CT e /H continuam apenas strings porque a spec não oferece a alternativa de stream para eles. Se você escreve seu próprio código de leitura de PDF, esse é o hábito que vale roubar — cheque na spec quais chaves são dual-typed em vez de assumir uma string

var
  Lib: TPDFlib;
  Command: TPDFlibWebCaptureCommand;
  I, Total, Removed: Integer;
begin
  Total := Lib.GetWebCaptureCommandCount;
  // A indexação pública é 1-based, igual ao resto da PDF Library para Delphi.
  for I := 1 to Total do
    if Lib.GetWebCaptureCommand(I, Command) = 1 then
      Writeln(Format('%s  levels=%d  flags=%d  post=%d bytes',
        [Command.URL, Command.Levels, Command.Flags, Length(Command.PostData)]));

  // Remove o dicionário /SpiderInfo inteiro e informa quantos
  // comandos foram descartados; retorna 0 quando não havia nada a limpar.
  Removed := Lib.ClearWebCaptureCommands;
  Writeln(Format('cleared %d command(s)', [Removed]));
end;

ClearWebCaptureCommands conta os comandos e então purga a chave /SpiderInfo inteira do catálogo em vez de apenas esvaziar /C. Deixar um /SpiderInfo com um array de comandos vazio afirmaria que o documento tem procedência de Web Capture e que nada foi buscado, o que é uma afirmação diferente e falsa em relação a não ter procedência registrada. O mesmo instinto vale para metadados de catálogo em geral — um dicionário casca vazio é uma declaração, não um resto neutro. Se você está auditando o que um documento já afirma antes de reescrevê-lo, o padrão de introspecção em lendo ações de outline e anotação de volta de um PDF existente combina naturalmente com uma passada de ler-e-limpar aqui

Gravar metadados não é implementar Web Capture

Seja franco com você mesmo sobre o escopo: AddWebCaptureCommand salva um registro de uma busca pretendida ou concluída, e é só isso que ele faz. A PDF Library for Delphi não executa requisições HTTP, não segue os links que seu valor de Levels descreve e não honra as flags SAME_SITE ou SAME_PATH — essas flags são documentação de como algum crawler se comportou, não instruções que a biblioteca executa. Seu próprio código de busca, seja ele qual for, continua sendo inteiramente problema seu

Mais duas partes da ISO 32000-1 §14.10 também ficam fora dessa API. Web Capture define content sets — as estruturas /Contents que amarram pedaços individuais de página de volta ao material de origem — e uma name tree que mapeia identificadores digitais para esses content sets. A PDF Library for Delphi não mantém nenhuma das duas. O que você recebe é a lista de comandos: a resposta de nível superior para "de onde este documento foi buscado, e quão fundo o crawl foi", que para fins de procedência, auditoria e ingestão de arquivamento costuma ser a parte que qualquer um de fato pergunta. Se você precisa que as URLs sejam clicáveis para os leitores em vez de meramente registradas para auditores, isso é um trabalho separado — veja construindo ações GoToR, GoToE e Launch para o lado das ações, e gatilhos de ações de ciclo de vida de documento e página para comportamento de disparo ao abrir ou fechar. Os metadados de comando do catálogo /SpiderInfo descritos aqui fazem parte da PDF Library for Delphi, a biblioteca de PDF nativa para Delphi e C++Builder