Artigo Técnico

Comandos Web Capture SpiderInfo do Catalog em PDFs Delphi

A PDF Library for Delphi (PDFlibPas) escreve os metadados de comandos Web Capture no catalog do documento através de AddWebCaptureCommand, preenchendo o dicionário /SpiderInfo definido na ISO 32000-1 §14.10. Cada comando regista o URL que foi obtido, quantos níveis de profundidade a obtenção percorreu e um pequeno conjunto de flags. Regista a intenção — não obtém nada

Essa distinção é exatamente a razão pela qual esta API é fácil de ler mal. Um engenheiro de suporte entrega-lhe 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 catalog: um URL, uma profundidade de crawl, talvez o corpo POST que gerou a página. Se não preencheu, nenhuma leitura do content stream lhe vai dizer isso. Escrever essa proveniência é barato, estruturalmente melindroso e quase sempre é ignorado

O que regista realmente o dicionário /SpiderInfo do Catalog?

/SpiderInfo é um dicionário de informação Web Capture pendurado no catalog do documento, e guarda exatamente duas coisas que interessam à PDF Library for Delphi: /V, uma versão numérica da base de dados Web Capture que a biblioteca escreve sempre como 1.0, e /C, um array de dicionários de comandos. O número de versão não é a versão do PDF — identifica o próprio modelo de dados Web Capture, e 1.0 é o único valor que a ISO 32000-1 §14.10 define. Tudo o que interessa vive em /C. Cada dicionário de comando transporta /URL (o endereço que foi pedido), /L (quantos níveis de ligações foram seguidos a partir dele) e /F (flags de travessia). Três chaves opcionais descrevem o próprio pedido quando não foi um GET simples: /P para dados POST, /CT para o tipo de conteúdo desses dados e /H para cabeçalhos de pedido adicionais. A PDF Library for Delphi mapeia os seis num ú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 empurra também a versão mínima do documento: AddWebCaptureCommand chama EnsureMinVersion('1.3', 'Catalog /SpiderInfo Web Capture command'), e as regras de deteção de versão da PDF Library for Delphi registam uma regra PDF 1.3 correspondente, para que um documento relido mais tarde reporte o mesmo mínimo

Estrutura do dicionário SpiderInfo do Catalog escrita pela PDFlibPas: /SpiderInfo transporta /V definido como a versão 1.0 da base de dados Web Capture e /C como um array de referências indiretas, cada dicionário de comando com as chaves /URL, /L, /F e as opcionais /P, /CT, /H que mapeiam para os campos do record TPDFlibWebCaptureCommand
A lista de comandos fica pendurada no catalog do documento como uma entrada de versão mais um array de referências indiretas, pelo que uma obtenção partilhada pode ser referenciada de mais do que um sítio enquanto o mínimo de versão sobe para PDF 1.3

Porque é que um comando Web Capture tem de ser um objeto indireto

A única regra estrutural que apanha toda a gente: um dicionário de comando tem de ser registado como objeto indireto antes de entrar em /C, porque a ISO 32000-1 §14.10 especifica /C como um array de referências indiretas para dicionários de comandos, não como um array de dicionários. Empurre um dicionário direto para esse array e obtém algo que faz parse como PDF válido e não significa nada como registo Web Capture — um visualizador ou uma ferramenta de arquivo que percorra a lista de comandos encontra um objeto inline onde devia estar uma referência, e a entrada simplesmente não é um comando. A PDF Library for Delphi trata disto dentro de AddWebCaptureCommand: constrói o dicionário, entrega-o a OnNewIndObj para obter um número de objeto e um número de geração, envolve esse par numa referência indireta e anexa a referência ao array. Se OnNewIndObj não produzir um número de objeto positivo, a função devolve 0 e o array fica intocado. A razão pela qual a spec insiste na indireção é que um único comando pode ser partilhado — a mesma obtenção pode ser referenciada de mais do que um sítio numa estrutura Web Capture — e objetos partilhados em PDF são, por definição, indiretos

Como é que se adiciona um comando Web Capture a partir do Delphi?

Preencha o record por completo e chame depois AddWebCaptureCommand, que devolve o índice do novo comando a partir de 1, ou 0 se a validação o rejeitar. A completude importa mais aqui do que o habitual. O Delphi inicializa os campos AnsiString geridos de um record local como vazios, mas os campos Integer — Levels e Flags — não recebem esse tratamento, pelo que um record que se esqueça de preencher chega com o que estava na stack. AddWebCaptureCommand exige que Levels seja pelo menos 1 e que Flags seja não negativo, e um valor de lixo da stack ou é rejeitado liminarmente ou, pior, é aceite silenciosamente como uma profundidade com aspeto plausível

Fluxo de validação do AddWebCaptureCommand na PDFlibPas: preencha todos os campos do record porque Levels e Flags são Integers simples que transportam lixo da stack, exija Levels de pelo menos 1, rejeite Flags fora da lista branca 1, 2, 4, crie o /SpiderInfo de forma preguiçosa, registe o comando como objeto indireto através do OnNewIndObj e devolva o índice a partir de 1 ou 0
Toda a rejeição acontece antes de o documento ser tocado: um record que falhe as verificações dos contadores ou das flags devolve 0, deixa o catalog exatamente como estava e nunca escreve um placeholder vazio no ficheiro
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 simples e
      // transportam lixo da stack se os saltar.
      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 := '';

      // Devolve o índice do novo comando a partir de 1, 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 a entrada /V e depois um array /C vazio, apenas se estes faltarem. É deliberadamente conservador em relação ao que encontra: se o catalog já tem uma chave /SpiderInfo cujo valor não é um dicionário, ou uma chave /C cujo valor não é um array, o helper devolve nil em vez de sobrescrever o objeto existente. Uma estrutura herdada malformada faz a adição falhar; não faz a PDF Library for Delphi destruir o que o produtor anterior escreveu

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

AddWebCaptureCommand valida Flags contra uma lista branca e rejeita qualquer valor com um bit ativo fora das três constantes que a PDF Library for Delphi define: PDF_WEB_CAPTURE_SAME_SITE = 1 (seguir apenas ligações que fiquem no mesmo site), PDF_WEB_CAPTURE_SAME_PATH = 2 (seguir apenas ligações sob o mesmo caminho) e PDF_WEB_CAPTURE_SUBMIT = 4 (este pedido foi um envio de formulário em vez de uma obtenção simples). Passe 8, ou 16, ou um -1 acidental, e a chamada devolve 0 sem tocar no documento

Lista branca de flags imposta pelo 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 como 2 e PDF_WEB_CAPTURE_SUBMIT como 4, enquanto qualquer bit não definido, como 8 ou um menos 1 acidental, é recusado na altura da escrita
Um bit não definido é indistinguível de um corrompido, pelo que recusar o valor na altura da escrita mantém a ambiguidade fora do ficheiro, e uma combinação legal como 1 ou 2 definidos como 3 continua a passar

Isto é mais estrito do que os parsers de PDF costumam ser, e é de propósito. /F é um campo de bits sem estrutura auto-descritiva, pelo que um bit não definido é indistinguível de um corrompido, e um consumidor que mais tarde leia o valor de volta não tem forma de distinguir uma extensão futura de um erro de escrita. Recusar na altura da escrita mantém a ambiguidade totalmente fora do ficheiro. Note também que /P, /CT e /H só são escritos quando os campos correspondentes do record não estão vazios, pelo que um GET normal produz um dicionário de comando com três chaves — /URL, /L, /F — sem placeholders de string vazia a poluí-lo, que é o que um leitor que faça parse do ficheiro espera para um pedido sem corpo

// Um envio de formulário: corpo POST mais o seu 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');

// Mesmo site mais mesmo caminho é uma combinação legal: 1 ou 2 = 3.
Command.Flags := PDF_WEB_CAPTURE_SAME_SITE or PDF_WEB_CAPTURE_SAME_PATH;

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

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

GetWebCaptureCommand devolve 1 em caso de sucesso e 0 caso contrário, e tem de ser mais tolerante do que o escritor, porque está a ler ficheiros escritos por outros produtores. Duas assimetrias importam. Primeiro, o leitor aceita uma entrada de comando quer seja uma referência indireta quer um dicionário direto, fazendo dereferência quando necessário — a PDF Library for Delphi escreve sempre a forma indireta que a spec exige, mas recusar ler um ficheiro descuidado não ajuda ninguém. Segundo, /P pode ser uma string ou um stream, e isso generaliza muito para além do Web Capture. A ISO 32000-1 permite repetidamente que uma única chave contenha uma string ou um stream quando o valor pode ser arbitrariamente grande — corpos POST aqui, e o mesmo padrão aparece noutros locais do formato para metadados e payloads embutidos. O helper interno ReadString em GetWebCaptureCommand recebe por isso um flag AllowStream, e apenas a leitura de /P passa True: um valor stream é descodificado através da sua cadeia de filtros e devolvido como o campo PostData, enquanto /URL, /CT e /H ficam só-string porque a spec não lhes oferece a alternativa de stream. Se escrever o seu próprio código de leitura de PDF, este é o hábito que vale a pena roubar — verifique na spec quais as chaves de tipo duplo em vez de assumir uma string

var
  Lib: TPDFlib;
  Command: TPDFlibWebCaptureCommand;
  I, Total, Removed: Integer;
begin
  Total := Lib.GetWebCaptureCommandCount;
  // A indexação pública começa em 1, como no resto da PDF Library for 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 reporta quantos comandos
  // foram descartados; devolve 0 quando não havia nada para limpar.
  Removed := Lib.ClearWebCaptureCommands;
  Writeln(Format('cleared %d command(s)', [Removed]));
end;

ClearWebCaptureCommands conta os comandos e depois purga a chave /SpiderInfo inteira do catalog em vez de apenas esvaziar /C. Deixar um /SpiderInfo com um array de comandos vazio afirmaria que o documento tem proveniência Web Capture e que nada foi obtido, o que é uma alegação diferente e falsa de não ter proveniência registada. O mesmo instinto aplica-se aos metadados do catalog em geral — um dicionário-casca vazio é uma declaração, não um resto neutro. Se está a auditar o que um documento já afirma antes de o reescrever, o padrão de introspeção em ler ações de outlines e anotações de volta de um PDF existente combina naturalmente com uma passagem de ler-e-limpar aqui

Escrever metadados não é implementar Web Capture

Seja franco consigo próprio sobre o âmbito: AddWebCaptureCommand guarda um registo de uma obtenção pretendida ou concluída, e é só isso que faz. A PDF Library for Delphi não executa pedidos HTTP, não segue as ligações que o 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. O seu próprio código de obtenção, seja ele qual for, continua a ser inteiramente problema seu

Duas outras peças da ISO 32000-1 §14.10 estão também fora desta API. O Web Capture define content sets — as estruturas /Contents que ligam pedaços individuais de página de volta ao material de origem de onde vieram — 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 obtém é a lista de comandos: a resposta de topo para "de onde foi obtido este documento, e até que profundidade foi o crawl", que para efeitos de proveniência, auditoria e entrada em arquivo costuma ser a parte que alguém realmente pergunta. Se precisa que os URLs sejam clicáveis para os leitores em vez de apenas registados para auditores, isso é outro trabalho — veja construir ações GoToR, GoToE e Launch para o lado das ações, e gatilhos de ações do ciclo de vida de documentos e páginas para disparar comportamento ao abrir ou fechar. Os metadados de comandos Web Capture do /SpiderInfo do Catalog descritos aqui fazem parte da PDF Library for Delphi, a biblioteca PDF nativa para Delphi e C++Builder