Technischer Artikel

SpiderInfo-Web-Capture-Befehle im PDF-Catalog in Delphi

PDF Library for Delphi (PDFlibPas) schreibt über AddWebCaptureCommand Web-Capture-Befehlsmetadaten in den Katalog eines Dokuments und füllt dabei das in ISO 32000-1 §14.10 definierte /SpiderInfo-Dictionary. Jeder Befehl hält fest, welche URL abgerufen wurde, wie viele Ebenen tief der Abruf ging und einen kleinen Satz von Flags. Er dokumentiert die Absicht – abgerufen wird nichts

Genau diese Unterscheidung macht die API so leicht misszuverstehen. Ein Support-Ingenieur reicht Ihnen ein PDF, das aus der Online-Dokumentation eines Anbieters zusammengestellt wurde, und fragt, woher Seite 40 stammt. Hat der Erzeuger /SpiderInfo ausgefüllt, steht die Antwort im Katalog: eine URL, eine Crawl-Tiefe, vielleicht der POST-Body, aus dem die Seite entstand. Hat er es nicht, sagt Ihnen auch das vollständige Lesen des Content-Streams nichts. Diese Provenienz zu schreiben ist billig, strukturell heikel – und wird fast immer übersprungen

Was hält das Catalog-/SpiderInfo-Dictionary tatsächlich fest?

/SpiderInfo ist ein Web-Capture-Informationsdictionary am Dokumentkatalog und enthält genau zwei Dinge, die PDF Library for Delphi betreffen: /V, eine numerische Web-Capture-Datenbankversion, die die Bibliothek immer als 1.0 schreibt, und /C, ein Array aus Kommandodictionaries. Die Versionsnummer ist nicht die PDF-Version – sie identifiziert das Web-Capture-Datenmodell selbst, und 1.0 ist der einzige Wert, den ISO 32000-1 §14.10 definiert. Alles Interessante steckt in /C. Jedes Kommandodictionary trägt /URL (die angeforderte Adresse), /L (wie viele Link-Ebenen von dort verfolgt wurden) und /F (Traversal-Flags). Drei optionale Schlüssel beschreiben die Anfrage selbst, wenn es kein schlichter GET war: /P für POST-Daten, /CT für den Content-Type dieser Daten und /H für zusätzliche Request-Header. PDF Library for Delphi bildet alle sechs auf einen einzigen Record ab, TPDFlibWebCaptureCommand, dessen Felder URL, Levels, Flags, PostData, ContentType und Headers heißen – alles AnsiString außer den zwei Integer-Zählern. Die Präsenz von /SpiderInfo hebt außerdem die Mindestversion des Dokuments an: AddWebCaptureCommand ruft EnsureMinVersion('1.3', 'Catalog /SpiderInfo Web Capture command'), und die Versionserkennungsregeln von PDF Library for Delphi registrieren eine passende PDF-1.3-Regel, sodass ein später zurückgelesenes Dokument dieselbe Untergrenze meldet

Struktur des von PDFlibPas geschriebenen Catalog-SpiderInfo-Dictionarys: /SpiderInfo trägt /V mit der Web-Capture-Datenbankversion 1.0 und /C als Array indirekter Referenzen, wobei jedes Kommandodictionary die Schlüssel /URL, /L, /F und die optionalen /P, /CT, /H hält, die auf die Record-Felder von TPDFlibWebCaptureCommand abbilden
Die Kommandoliste hängt am Dokumentkatalog als ein Versionseintrag plus ein Array indirekter Referenzen, sodass ein gemeinsamer Abruf von mehreren Stellen referenziert werden kann, während die Versionsuntergrenze auf PDF 1.3 steigt

Warum ein Web-Capture-Befehl ein indirektes Objekt sein muss

Die eine Strukturregel, über die alle stolpern: Ein Kommandodictionary muss als indirektes Objekt registriert sein, bevor es in /C landet, denn ISO 32000-1 §14.10 legt /C als Array aus indirekten Referenzen auf Kommandodictionaries fest – nicht als Array von Dictionaries. Schieben Sie ein direktes Dictionary in das Array, bekommen Sie etwas, das als gültiges PDF parst und als Web-Capture-Eintrag nichts bedeutet: Ein Viewer oder Archivierungswerkzeug, das die Kommandoliste durchläuft, findet ein Inline-Objekt, wo eine Referenz stehen sollte, und der Eintrag ist schlicht kein Befehl. PDF Library for Delphi erledigt das intern in AddWebCaptureCommand: Es baut das Dictionary, übergibt es an OnNewIndObj, um Objekt- und Generationsnummer zu erhalten, verpackt das Paar in eine indirekte Referenz und hängt die Referenz an das Array an. Liefert OnNewIndObj keine positive Objektnummer, gibt die Funktion 0 zurück und das Array bleibt unangetastet. Dass die Spezifikation auf Indirektion besteht, hat einen Grund: Ein einzelner Befehl kann geteilt werden – derselbe Abruf kann an mehreren Stellen einer Web-Capture-Struktur referenziert werden – und geteilte Objekte sind in PDF per Definition indirekt

Wie fügen Sie aus Delphi einen Web-Capture-Befehl hinzu?

Füllen Sie den Record vollständig aus und rufen dann AddWebCaptureCommand auf, das den 1-basierten Index des neuen Befehls oder 0 zurückgibt, wenn die Validierung ihn ablehnt. Vollständigkeit zählt hier mehr als sonst. Delphi initialisiert die verwalteten AnsiString-Felder eines lokalen Records mit leer – die Integer-Felder Levels und Flags dagegen bekommen keine solche Behandlung, sodass ein Record, den Sie vergessen haben zu füllen, mit ankommt, was gerade auf dem Stack lag. AddWebCaptureCommand verlangt für Levels mindestens 1 und für Flags einen nicht negativen Wert, und ein Stack-Garbage-Wert wird entweder rundweg abgelehnt oder – schlimmer – stillschweigend als plausibel aussehende Tiefe akzeptiert

Validierungsablauf von AddWebCaptureCommand in PDFlibPas: alle Record-Felder füllen, weil Levels und Flags schlichte Integers sind und Stack-Garbage tragen, Levels mit mindestens 1 verlangen, Flags außerhalb der 1-2-4-Whitelist ablehnen, /SpiderInfo lazy erzeugen, den Befehl über OnNewIndObj als indirektes Objekt registrieren und den 1-basierten Index oder 0 zurückgeben
Jede Ablehnung passiert, bevor das Dokument angefasst wird: Ein Record, der die Zähler- oder Flag-Prüfungen nicht besteht, gibt 0 zurück, lässt den Katalog exakt wie er war und schreibt nie einen leeren Platzhalter in die Datei
var
  Lib: TPDFlib;
  Command: TPDFlibWebCaptureCommand;
  Index: Integer;
begin
  Lib := TPDFlib.Create;
  try
    if Lib.LoadFromFile('captured-site.pdf', '') = 1 then
    begin
      // Alle Felder zuweisen: Levels und Flags sind schlichte Integers und
      // tragen Stack-Garbage, wenn Sie sie auslassen.
      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 := '';

      // Gibt den neuen 1-basierten Befehlsindex zurück, oder 0 bei Ablehnung.
      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;

Auch die Struktur entsteht erst mit dem ersten erfolgreichen Aufruf. AddWebCaptureCommand fragt beim internen Helfer WebCaptureCommands(True) das Befehlsarray an, und der Helfer legt /SpiderInfo mit dem /V-Eintrag und anschließend ein leeres /C-Array nur an, wenn sie fehlen. Was er vorfindet, behandelt er bewusst konservativ: Hat der Katalog bereits einen /SpiderInfo-Schlüssel, dessen Wert kein Dictionary ist, oder einen /C-Schlüssel, dessen Wert kein Array ist, gibt der Helfer nil zurück, statt das vorhandene Objekt zu überschreiben. Eine kaputte vererbte Struktur lässt das Hinzufügen scheitern – sie bringt PDF Library for Delphi nicht dazu, zu zerstören, was ein früherer Erzeuger geschrieben hat

Die Flag-Whitelist: drei definierte Bits und sonst nichts

AddWebCaptureCommand validiert Flags gegen eine Whitelist und lehnt jeden Wert ab, der ein Bit außerhalb der drei Konstanten setzt, die PDF Library for Delphi definiert: PDF_WEB_CAPTURE_SAME_SITE = 1 (nur Links folgen, die auf derselben Site bleiben), PDF_WEB_CAPTURE_SAME_PATH = 2 (nur Links unter demselben Pfad folgen) und PDF_WEB_CAPTURE_SUBMIT = 4 (die Anfrage war eine Formularübermittlung statt eines schlichten Abrufs). Übergeben Sie 8, 16 oder versehentlich -1, gibt der Aufruf 0 zurück, ohne das Dokument anzufassen

Von AddWebCaptureCommand in PDFlibPas durchgesetzte Flag-Whitelist: Das /F-Bitfeld akzeptiert genau drei definierte Bits, PDF_WEB_CAPTURE_SAME_SITE mit 1, PDF_WEB_CAPTURE_SAME_PATH mit 2 und PDF_WEB_CAPTURE_SUBMIT mit 4, während jedes undefinierte Bit wie 8 oder ein versehentliches minus 1 beim Schreiben verweigert wird
Ein undefiniertes Bit ist von einem korrupten nicht zu unterscheiden, daher hält die Verweigerung beim Schreiben die Mehrdeutigkeit aus der Datei, während eine legale Kombination wie 1 oder 2 zu 3 weiterhin durchgeht

Das ist strenger, als PDF-Parser üblicherweise sind – und zwar mit Absicht. /F ist ein Bitfeld ohne selbstbeschreibende Struktur, daher ist ein undefiniertes Bit von einem korrupten nicht zu unterscheiden, und ein Konsument, der den Wert später zurückliest, kann eine künftige Erweiterung nicht von einem Tippfehler trennen. Die Verweigerung beim Schreiben hält die Mehrdeutigkeit vollständig aus der Datei. Beachten Sie außerdem: /P, /CT und /H werden nur geschrieben, wenn die zugehörigen Record-Felder nicht leer sind, sodass ein gewöhnlicher GET ein Kommandodictionary mit drei Schlüsseln erzeugt – /URL, /L, /F – ohne dass leere String-Platzhalter es zumüllen, was genau das ist, was ein Leser beim Parsen der Datei für eine Anfrage ohne Body erwartet

// Eine Formularübermittlung: POST-Body plus 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 plus same-path ist eine legale Kombination: 1 oder 2 = 3.
Command.Flags := PDF_WEB_CAPTURE_SAME_SITE or PDF_WEB_CAPTURE_SAME_PATH;

// Bit 3 ist in der Spezifikation nicht definiert, daher erreicht dieser Wert nie die Datei.
Command.Flags := 8;
if Lib.AddWebCaptureCommand(Command) <> 0 then
  raise Exception.Create('undefined flag bit should have been refused');

Befehle zurücklesen: ein Schlüssel, zwei legale Darstellungen

GetWebCaptureCommand gibt bei Erfolg 1 zurück, sonst 0, und es muss toleranter sein als der Writer, denn es liest Dateien, die andere Erzeuger geschrieben haben. Zwei Asymmetrien zählen. Erstens akzeptiert der Reader einen Befehlseintrag, ob er nun eine indirekte Referenz oder ein direktes Dictionary ist, und dereferenziert bei Bedarf – PDF Library for Delphi schreibt immer die von der Spezifikation geforderte indirekte Form, aber eine schlampige Datei nicht lesen zu wollen, hilft niemandem. Zweitens darf /P entweder ein String oder ein Stream sein, und diese Regel generalisiert weit über Web Capture hinaus. ISO 32000-1 lässt an vielen Stellen einen einzelnen Schlüssel wahlweise einen String oder einen Stream halten, wenn der Wert beliebig groß werden kann – POST-Bodies hier, dasselbe Muster taucht im Format sonst bei Metadaten und eingebetteten Nutzlasten auf. Der interne ReadString-Helfer in GetWebCaptureCommand nimmt deshalb ein AllowStream-Flag, und nur der /P-Read übergibt True: Ein Stream-Wert wird durch seine Filterkette dekodiert und als PostData-Feld zurückgegeben, während /URL, /CT und /H rein string bleiben, weil die Spezifikation ihnen die Stream-Alternative nicht anbietet. Wenn Sie eigenen PDF-Lesecode schreiben, ist das die Gewohnheit, die sich zu übernehmen lohnt – prüfen Sie in der Spezifikation, welche Schlüssel dual typisiert sind, statt einen String anzunehmen

var
  Lib: TPDFlib;
  Command: TPDFlibWebCaptureCommand;
  I, Total, Removed: Integer;
begin
  Total := Lib.GetWebCaptureCommandCount;
  // Öffentliche Indizierung ist 1-basiert, wie im Rest von 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)]));

  // Entfernt das gesamte /SpiderInfo-Dictionary und meldet, wie viele
  // Befehle verworfen wurden; gibt 0 zurück, wenn nichts zu löschen war.
  Removed := Lib.ClearWebCaptureCommands;
  Writeln(Format('cleared %d command(s)', [Removed]));
end;

ClearWebCaptureCommands zählt die Befehle und entfernt dann den gesamten /SpiderInfo-Schlüssel aus dem Katalog, statt nur /C zu leeren. Ein /SpiderInfo mit leerem Befehlsarray zurückzulassen würde behaupten, dass das Dokument Web-Capture-Provenienz hat und nichts abgerufen wurde – eine andere und falsche Aussage als gar keine Provenienz zu haben. Derselbe Instinkt gilt allgemein für Katalogmetadaten: Ein leeres Schalendictionary ist eine Aussage, kein neutraler Rest. Wenn Sie prüfen, was ein Dokument bereits behauptet, bevor Sie es umschreiben, passt das Introspektionsmuster aus dem Artikel zum Zurücklesen von Outline- und Annotations-Aktionen aus einem bestehenden PDF natürlich zu einem Read-dann-Clear-Durchgang hier

Metadaten schreiben ist kein Web Capture implementieren

Seien Sie sich über den Umfang im Klaren: AddWebCaptureCommand speichert eine Aufzeichnung eines geplanten oder abgeschlossenen Abrufs – und das ist alles. PDF Library for Delphi führt keine HTTP-Requests aus, folgt nicht den Links, die Ihr Levels-Wert beschreibt, und berücksichtigt die SAME_SITE- oder SAME_PATH-Flags nicht – diese Flags dokumentieren, wie sich irgendein Crawler verhalten hat, sie sind keine Anweisungen, die die Bibliothek ausführt. Ihr eigener Fetch-Code, wie auch immer er aussieht, bleibt vollständig Ihr Problem

Zwei weitere Teile von ISO 32000-1 §14.10 liegen ebenfalls außerhalb dieser API. Web Capture definiert Content-Sets – die /Contents-Strukturen, die einzelne Seitenbausteine an das Quellmaterial koppeln, aus dem sie stammen – und einen Namensbaum, der digitale Kennungen auf diese Content-Sets abbildet. PDF Library for Delphi pflegt keins von beiden. Sie bekommen die Kommandoliste: die Antwort auf die Frage, woher dieses Dokument abgerufen wurde und wie tief der Crawl ging, was für Provenienz-, Audit- und Archivierungszwecke meist genau der Teil ist, nach dem irgendjemand fragt. Wenn die URLs für Leser klickbar sein sollen statt nur für Auditoren aufgezeichnet, ist das ein separates Thema – siehe Erzeugen von GoToR-, GoToE- und Launch-Aktionen für die Aktionsseite und Trigger für Dokument- und Seitenlebenszyklus-Aktionen für Verhalten beim Öffnen und Schließen. Die hier beschriebene Catalog-/SpiderInfo-Befehlsmetadaten sind Teil von PDF Library for Delphi, der nativen PDF-Bibliothek für Delphi und C++Builder