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
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
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
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