Artículo técnico

Comandos Web Capture SpiderInfo de catálogo en PDF de Delphi

PDF Library for Delphi (PDFlibPas) escribe los metadatos de comandos de Web Capture en el catálogo del documento a través de AddWebCaptureCommand, que rellena el diccionario /SpiderInfo definido en ISO 32000-1 §14.10. Cada comando registra la URL que se capturó, cuántos niveles de profundidad recorrió la captura y un pequeño conjunto de flags. Registra la intención: no captura nada

Esa distinción es exactamente lo que hace fácil malinterpretar esta API. Un ingeniero de soporte te pasa un PDF armado con la documentación en línea de un proveedor y te pregunta de dónde salió la página 40. Si el productor rellenó /SpiderInfo, la respuesta está en el catálogo: una URL, la profundidad del rastreo y quizá el cuerpo POST que generó la página. Si no lo hizo, por más que leas el content stream no lo vas a averiguar. Escribir esa procedencia es barato, estructuralmente quisquilloso y casi siempre se saltea

¿Qué registra realmente el diccionario /SpiderInfo del catálogo?

/SpiderInfo es un diccionario de información de Web Capture que cuelga del catálogo del documento y guarda exactamente dos cosas que le importan a PDF Library for Delphi: /V, una versión numérica de la base de datos de Web Capture que la librería siempre escribe como 1.0, y /C, un arreglo de diccionarios de comando. El número de versión no es la versión del PDF: identifica el modelo de datos de Web Capture en sí, y 1.0 es el único valor que define ISO 32000-1 §14.10. Todo lo interesante vive en /C. Cada diccionario de comando lleva /URL (la dirección que se solicitó), /L (cuántos niveles de enlaces se siguieron desde ella) y /F (flags de recorrido). Tres claves opcionales describen la petición cuando no fue un GET simple: /P para los datos POST, /CT para el content type de esos datos y /H para headers adicionales. PDF Library for Delphi mapea las seis a un único record, TPDFlibWebCaptureCommand, cuyos campos son URL, Levels, Flags, PostData, ContentType y Headers, todos AnsiString salvo los dos contadores Integer. La presencia de /SpiderInfo también empuja la versión mínima del documento: AddWebCaptureCommand llama a EnsureMinVersion('1.3', 'Catalog /SpiderInfo Web Capture command'), y las reglas de detección de versión en PDF Library for Delphi registran una regla PDF 1.3 equivalente para que un documento releído más tarde reporte el mismo piso

Estructura del diccionario Catalog SpiderInfo que escribe PDFlibPas: /SpiderInfo lleva /V con la versión 1.0 de la base de datos de Web Capture y /C como arreglo de referencias indirectas, con cada diccionario de comando guardando /URL, /L, /F y las claves opcionales /P, /CT, /H que se mapean a los campos del record TPDFlibWebCaptureCommand
La lista de comandos cuelga del catálogo del documento como una entrada de versión más un arreglo de referencias indirectas, de modo que una captura compartida se puede referenciar desde más de un lugar mientras el piso de versión sube a PDF 1.3

¿Por qué un comando de Web Capture tiene que ser un objeto indirecto?

La única regla estructural que hace tropezar a la gente: un diccionario de comando tiene que registrarse como objeto indirecto antes de entrar en /C, porque ISO 32000-1 §14.10 especifica /C como un arreglo de referencias indirectas a diccionarios de comando, no como un arreglo de diccionarios. Mete un diccionario directo en ese arreglo y obtienes algo que parsea como PDF válido pero no significa nada como registro de Web Capture: un visor o una herramienta de archivo que recorra la lista de comandos encuentra un objeto inline donde debería haber una referencia, y la entrada simplemente no es un comando. PDF Library for Delphi maneja esto dentro de AddWebCaptureCommand: construye el diccionario, se lo pasa a OnNewIndObj para obtener el número de objeto y el número de generación, envuelve ese par en una referencia indirecta y agrega la referencia al arreglo. Si OnNewIndObj falla y no produce un número de objeto positivo, la función devuelve 0 y el arreglo queda intacto. La razón por la que la spec insiste en la indirección es que un comando se puede compartir: la misma captura puede referenciarse desde más de un lugar en una estructura de Web Capture, y los objetos compartidos en PDF son, por definición, indirectos

¿Cómo se agrega un comando de Web Capture desde Delphi?

Llena el record por completo y luego llama a AddWebCaptureCommand, que devuelve el índice 1-based del comando nuevo o 0 si la validación lo rechazó. Completar los campos importa más de lo habitual acá. Delphi inicializa los campos gestionados AnsiString de un record local a vacío, pero los campos Integer — Levels y Flags — no reciben ese tratamiento, así que un record que olvidas llenar llega cargando lo que había en el stack. AddWebCaptureCommand exige que Levels sea al menos 1 y que Flags sea no negativo, y un valor de basura del stack o se rechaza de plano o, peor, se acepta en silencio como una profundidad que parece plausible

Flujo de validación de AddWebCaptureCommand en PDFlibPas: llena cada campo del record porque Levels y Flags son Integers simples que arrastran basura del stack, exige Levels de al menos 1, rechaza Flags fuera de la whitelist 1, 2, 4, crea /SpiderInfo de forma perezosa, registra el comando como objeto indirecto vía OnNewIndObj y devuelve el índice 1-based o 0
Cada rechazo ocurre antes de tocar el documento: un record que falla los chequeos de contadores o flags devuelve 0, deja el catálogo exactamente como estaba y nunca escribe un placeholder vacío en el archivo
var
  Lib: TPDFlib;
  Command: TPDFlibWebCaptureCommand;
  Index: Integer;
begin
  Lib := TPDFlib.Create;
  try
    if Lib.LoadFromFile('captured-site.pdf', '') = 1 then
    begin
      // Asigna cada campo: Levels y Flags son Integers simples y
      // arrastran basura del stack si los salteas.
      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 := '';

      // Devuelve el índice 1-based del comando nuevo, o 0 si lo rechazan.
      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;

La primera llamada exitosa también es la que crea la estructura. AddWebCaptureCommand le pide el arreglo de comandos al helper interno WebCaptureCommands(True), y ese helper crea /SpiderInfo con su entrada /V y luego un arreglo /C vacío solo si faltan. Es deliberadamente conservador con lo que encuentra: si el catálogo ya tiene una clave /SpiderInfo cuyo valor no es un diccionario, o una clave /C cuyo valor no es un arreglo, el helper devuelve nil en lugar de sobrescribir el objeto existente. Una estructura heredada malformada hace fallar el add; no hace que PDF Library for Delphi destruya lo que escribió el productor anterior

La whitelist de flags: tres bits definidos y nada más

AddWebCaptureCommand valida Flags contra una whitelist y rechaza cualquier valor con un bit fuera de las tres constantes que define PDF Library for Delphi: PDF_WEB_CAPTURE_SAME_SITE = 1 (seguir solo enlaces que se queden en el mismo sitio), PDF_WEB_CAPTURE_SAME_PATH = 2 (seguir solo enlaces bajo el mismo path) y PDF_WEB_CAPTURE_SUBMIT = 4 (esta petición fue un envío de formulario, no una recuperación simple). Pasa un 8, un 16 o un -1 accidental, y la llamada devuelve 0 sin tocar el documento

Whitelist de flags que aplica AddWebCaptureCommand en PDFlibPas: el campo de bits /F acepta exactamente tres bits definidos, PDF_WEB_CAPTURE_SAME_SITE con valor 1, PDF_WEB_CAPTURE_SAME_PATH con valor 2 y PDF_WEB_CAPTURE_SUBMIT con valor 4, mientras que cualquier bit no definido como el 8 o un menos 1 accidental se rechaza al escribir
Un bit no definido es indistinguible de uno corrupto, así que rechazar el valor al escribir saca la ambigüedad del archivo y una combinación legal como 1 y 2 con valor 3 sigue pasando

Esto es más estricto de lo que suelen ser los parsers de PDF, y a propósito. /F es un campo de bits sin estructura autodescriptiva, así que un bit no definido es indistinguible de uno corrupto, y un consumidor que luego lea el valor no tiene forma de distinguir una extensión futura de un typo. Rechazar al escribir saca la ambigüedad del archivo por completo. Nota también que /P, /CT y /H se escriben solo cuando sus campos de record correspondientes no están vacíos, así que un GET ordinario produce un diccionario de comando de tres claves — /URL, /L, /F — sin placeholders de string vacío ensuciándolo, que es lo que un lector que parsea el archivo espera para una petición sin cuerpo

// Un envío de formulario: cuerpo POST más su content type.
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 más same-path es una combinación legal: 1 o 2 = 3.
Command.Flags := PDF_WEB_CAPTURE_SAME_SITE or PDF_WEB_CAPTURE_SAME_PATH;

// El bit 3 no lo define la spec, así que este nunca llega al archivo.
Command.Flags := 8;
if Lib.AddWebCaptureCommand(Command) <> 0 then
  raise Exception.Create('undefined flag bit should have been refused');

Leer comandos de vuelta: una clave, dos representaciones legales

GetWebCaptureCommand devuelve 1 en éxito y 0 en cualquier otro caso, y tiene que ser más tolerante que el escritor, porque lee archivos que escribieron otros productores. Importan dos asimetrías. Primero, el lector acepta una entrada de comando ya sea referencia indirecta o diccionario directo, dereferenciando cuando hace falta — PDF Library for Delphi siempre escribe la forma indirecta que exige la spec, pero negarse a leer un archivo descuidado no ayuda a nadie. Segundo, /P puede ser un string o un stream, y eso se generaliza mucho más allá de Web Capture. ISO 32000-1 deja repetidamente que una sola clave tenga un string o un stream cuando el valor puede ser arbitrariamente grande: cuerpos POST acá, y el mismo patrón aparece en otros lugares del formato para metadatos y payloads embebidos. El helper interno ReadString de GetWebCaptureCommand toma por eso un flag AllowStream, y solo la lectura de /P pasa True: un valor de stream se decodifica a través de su cadena de filtros y se devuelve como el campo PostData, mientras que /URL, /CT y /H quedan solo string porque la spec no les ofrece la alternativa de stream. Si escribes tu propio código de lectura de PDF, este es el hábito que vale la pena robar: revisa en la spec qué claves son de doble tipo en lugar de asumir un string

var
  Lib: TPDFlib;
  Command: TPDFlibWebCaptureCommand;
  I, Total, Removed: Integer;
begin
  Total := Lib.GetWebCaptureCommandCount;
  // El indexado público es 1-based, como el resto de 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)]));

  // Elimina todo el diccionario /SpiderInfo y reporta cuántos
  // comandos se descartaron; devuelve 0 cuando no había nada que limpiar.
  Removed := Lib.ClearWebCaptureCommands;
  Writeln(Format('cleared %d command(s)', [Removed]));
end;

ClearWebCaptureCommands cuenta los comandos y luego purga la clave /SpiderInfo entera del catálogo en lugar de solo vaciar /C. Dejar un /SpiderInfo con un arreglo de comandos vacío afirmaría que el documento tiene procedencia de Web Capture y que no se capturó nada, que es una afirmación distinta y falsa respecto a no tener procedencia registrada. El mismo instinto aplica a los metadatos de catálogo en general: un diccionario cascarón vacío es una declaración, no un residuo neutral. Si estás auditando lo que un documento ya afirma antes de reescribirlo, el patrón de introspección de leer outline y annotation actions de un PDF existente combina naturalmente con una pasada de leer y luego limpiar acá

Escribir metadatos no es implementar Web Capture

Sé honesto contigo mismo sobre el alcance: AddWebCaptureCommand guarda un registro de una captura prevista o completada, y eso es todo lo que hace. PDF Library for Delphi no realiza peticiones HTTP, no sigue los enlaces que describe tu valor de Levels y no honra los flags SAME_SITE ni SAME_PATH — esos flags son documentación de cómo se comportó algún crawler, no instrucciones que la librería ejecute. Tu propio código de captura, sea el que sea, sigue siendo enteramente problema tuyo

Otras dos piezas de ISO 32000-1 §14.10 también quedan fuera de esta API. Web Capture define content sets — las estructuras /Contents que atan piezas individuales de página al material de origen del que vinieron — y un name tree que mapea identificadores digitales a esos content sets. PDF Library for Delphi no mantiene ninguno de los dos. Lo que obtienes es la lista de comandos: la respuesta de primer nivel a «de dónde se capturó este documento y qué tan profundo llegó el rastreo», que para procedencia, auditoría e ingesta de archivo suele ser la parte que cualquiera pregunta. Si necesitas que las URLs sean clicables para los lectores en lugar de solo registradas para los auditores, ese es otro trabajo — mira cómo construir acciones GoToR, GoToE y Launch para el lado de las acciones, y los disparadores de acciones del ciclo de vida del documento y la página para el comportamiento al abrir o cerrar. Los metadatos de comandos de catálogo /SpiderInfo que se describen acá son parte de PDF Library for Delphi, la librería de PDF nativa para Delphi y C++Builder