Artykuł techniczny

Polecenia Web Capture katalogu /SpiderInfo w PDF w Delphi

PDF Library for Delphi (PDFlibPas) zapisuje metadane poleceń Web Capture do katalogu dokumentu przez AddWebCaptureCommand, wypełniając słownik /SpiderInfo zdefiniowany w ISO 32000-1 §14.10. Każde polecenie rejestruje pobrany adres URL, liczbę poziomów, na jaką zeszło pobieranie, oraz niewielki zestaw flag. Zapisuje intencję — nie pobiera niczego

Ta różnica to cały powód, dla którego ten API łatwo źle odczytać. Inżynier wsparcia wręcza ci PDF złożony z dokumentacji online dostawcy i pyta, skąd wzięła się strona 40. Jeśli producent wypełnił /SpiderInfo, odpowiedź siedzi w katalogu: adres URL, głębokość indeksowania, może nawet treść POST, która wygenerowała stronę. Jeśli nie, czytanie strumienia treści nic ci nie powie. Zapisywanie tej proweniencji jest tanie, strukturalnie wymagające i prawie zawsze pomijane

Co właściwie zapisuje słownik /SpiderInfo katalogu?

/SpiderInfo to słownik informacji Web Capture zwisający z katalogu dokumentu, trzymający dokładnie dwie rzeczy, na których zależy PDF Library for Delphi: /V, numeryczną wersję bazy Web Capture, którą biblioteka zawsze zapisuje jako 1.0, oraz /C, tablicę słowników poleceń. Numer wersji to nie wersja PDF — identyfikuje sam model danych Web Capture, a 1.0 to jedyna wartość zdefiniowana w ISO 32000-1 §14.10. Wszystko, co interesujące, siedzi w /C. Każdy słownik polecenia niesie /URL (adres, który został zażądany), /L (ile poziomów linków prześledzono od niego) oraz /F (flagi przechodzenia). Trzy opcjonalne klucze opisują samo żądanie, gdy nie było zwykłym GET: /P dla danych POST, /CT dla typu zawartości tych danych i /H dla dodatkowych nagłówków żądania. PDF Library for Delphi mapuje wszystkie sześć na pojedynczy rekord, TPDFlibWebCaptureCommand, którego pola to URL, Levels, Flags, PostData, ContentType i Headers — wszystko AnsiString oprócz dwóch liczników Integer. Obecność /SpiderInfo podbija też minimalną wersję dokumentu: AddWebCaptureCommand woła EnsureMinVersion('1.3', 'Catalog /SpiderInfo Web Capture command'), a reguły wykrywania wersji w PDF Library for Delphi rejestrują pasującą regułę PDF 1.3, więc późniejszy odczyt dokumentu raportuje to samo dno

Struktura słownika SpiderInfo katalogu zapisywana przez PDFlibPas: /SpiderInfo niesie /V ustawione na wersję 1.0 bazy Web Capture oraz /C jako tablicę odwołań pośrednich, a każdy słownik polecenia trzyma /URL, /L, /F oraz opcjonalne klucze /P, /CT, /H mapujące się na pola rekordu TPDFlibWebCaptureCommand
Lista poleceń wisi na katalogu dokumentu jako jeden wpis wersji plus tablica odwołań pośrednich, więc wspólne pobranie może być przywoływane z więcej niż jednego miejsca, a podłoga wersji rośnie do PDF 1.3

Dlaczego polecenie Web Capture musi być obiektem pośrednim

Jedyna reguła strukturalna, o którą ludzie się potykają: słownik polecenia musi zostać zarejestrowany jako obiekt pośredni, zanim trafi do /C, bo ISO 32000-1 §14.10 określa /C jako tablicę odwołań pośrednich do słowników poleceń, a nie tablicę słowników. Wepchnij bezpośredni słownik do tej tablicy, a dostaniesz coś, co parsuje się jako poprawny PDF i nic nie znaczy jako rekord Web Capture — przeglądarka albo narzędzie archiwizacyjne przechodzące listę poleceń znajduje obiekt inline tam, gdzie powinno być odwołanie, a wpis po prostu nie jest poleceniem. PDF Library for Delphi załatwia to wewnątrz AddWebCaptureCommand: buduje słownik, przekazuje go do OnNewIndObj, żeby zdobyć numer obiektu i numer generacji, zawija tę parę w odwołanie pośrednie i dopina odwołanie do tablicy. Jeśli OnNewIndObj nie wyprodukuje dodatniego numeru obiektu, funkcja zwraca 0, a tablica zostaje nietknięta. Powód, dla którego specyfikacja nalega na pośredniość, to możliwość współdzielenia jednego polecenia — to samo pobranie może być przywoływane z więcej niż jednego miejsca w strukturze Web Capture — a obiekty współdzielone w PDF są z definicji pośrednie

Jak dodać polecenie Web Capture z poziomu Delphi?

Wypełnij rekord w całości, potem zawołaj AddWebCaptureCommand, która zwraca indeks nowego polecenia liczony od 1 albo 0, jeśli walidacja go odrzuciła. Kompletność ma tu większe znaczenie niż zwykle. Delphi inicjalizuje zarządzane pola AnsiString lokalnego rekordu pustymi wartościami, ale pola Integer — Levels i Flags — nie dostają takiego traktowania, więc rekord, którego nie wypełnisz, przyjeżdża ze śmieciami ze stosu. AddWebCaptureCommand wymaga Levels co najmniej 1 i Flags nieujemnego, a wartość-śmieć ze stosu zostanie albo odrzucona wprost, albo, co gorsza, po cichu przyjęta jako wiarygodnie wyglądająca głębokość

Przebieg walidacji AddWebCaptureCommand w PDFlibPas: wypełnij każde pole rekordu, bo Levels i Flags to zwykłe Integer niosące śmieci ze stosu, wymagaj Levels co najmniej 1, odrzucaj Flags spoza białej listy 1, 2, 4, twórz /SpiderInfo leniwie, rejestruj polecenie jako obiekt pośredni przez OnNewIndObj i zwracaj indeks liczony od 1 albo 0
Każde odrzucenie następuje, zanim dokument zostanie dotknięty: rekord, który nie przejdzie kontroli liczników albo flag, zwraca 0, zostawia katalog dokładnie takim, jakim był, i nigdy nie zapisuje pustego placeholderu do pliku
var
  Lib: TPDFlib;
  Command: TPDFlibWebCaptureCommand;
  Index: Integer;
begin
  Lib := TPDFlib.Create;
  try
    if Lib.LoadFromFile('captured-site.pdf', '') = 1 then
    begin
      // Przypisz każde pole: Levels i Flags to zwykłe Integer i
      // niosą śmieci ze stosu, jeśli je pominiesz.
      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 := '';

      // Zwraca indeks nowego polecenia liczony od 1, albo 0 przy odrzuceniu.
      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;

Pierwsze udane wywołanie jest też tym, które tworzy strukturę. AddWebCaptureCommand prosi wewnętrzny helper WebCaptureCommands(True) o tablicę poleceń, a ten helper tworzy /SpiderInfo z wpisem /V i dopiero potem pustą tablicę /C — ale tylko wtedy, gdy ich brakuje. Świadomie konserwatywnie podchodzi do tego, co zastanie: jeśli katalog ma już klucz /SpiderInfo, którego wartość nie jest słownikiem, albo klucz /C, którego wartość nie jest tablicą, helper zwraca nil zamiast nadpisywać istniejący obiekt. Zniekształcona odziedziczona struktura powoduje niepowodzenie dodawania; nie powoduje, że PDF Library for Delphi niszczy to, co napisał poprzedni producent

Biała lista flag: trzy zdefiniowane bity i nic więcej

AddWebCaptureCommand waliduje Flags względem białej listy i odrzuca każdą wartość z ustawionym bitem poza trzema stałymi, które definiuje PDF Library for Delphi: PDF_WEB_CAPTURE_SAME_SITE = 1 (podążaj tylko za linkami zostającymi na tej samej stronie), PDF_WEB_CAPTURE_SAME_PATH = 2 (podążaj tylko za linkami pod tą samą ścieżką) oraz PDF_WEB_CAPTURE_SUBMIT = 4 (to żądanie było wysłaniem formularza, a nie zwykłym pobraniem). Przekaż 8, 16 albo przypadkowe -1, a wywołanie zwróci 0, nie dotykając dokumentu

Biała lista flag egzekwowana przez AddWebCaptureCommand w PDFlibPas: pole bitowe /F przyjmuje dokładnie trzy zdefiniowane bity, PDF_WEB_CAPTURE_SAME_SITE ustawione na 1, PDF_WEB_CAPTURE_SAME_PATH na 2 i PDF_WEB_CAPTURE_SUBMIT na 4, a każdy niezdefiniowany bit, jak 8 albo przypadkowe minus 1, jest odrzucany w momencie zapisu
Niezdefiniowany bit jest nieodróżnialny od uszkodzonego, więc odmowa wartości przy zapisie trzyma dwuznaczność z dala od pliku, a legalna kombinacja, jak 1 albo 2 ustawione na 3, nadal przechodzi

To surowsze, niż zwykle bywają parsery PDF, i to celowo. /F to pole bitowe bez samoopisującej się struktury, więc niezdefiniowany bit jest nieodróżnialny od uszkodzonego, a konsument, który później odczyta tę wartość, nie ma jak odróżnić przyszłego rozszerzenia od literówki. Odmowa przy zapisie trzyma dwuznaczność całkowicie z dala od pliku. Zauważ też, że /P, /CT i /H są zapisywane tylko wtedy, gdy odpowiadające im pola rekordu są niepuste, więc zwykły GET produkuje słownik polecenia z trzema kluczami — /URL, /L, /F — bez zaśmiecania go pustymi placeholderami w postaci pustych stringów, czego czytelnik parsujący plik oczekuje od żądania bez ciała

// Wysłanie formularza: ciało POST plus jego typ zawartości.
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');

// Ta sama strona plus ta sama ścieżka to legalna kombinacja: 1 lub 2 = 3.
Command.Flags := PDF_WEB_CAPTURE_SAME_SITE or PDF_WEB_CAPTURE_SAME_PATH;

// Bit 3 nie jest zdefiniowany w specyfikacji, więc ten nigdy nie trafi do pliku.
Command.Flags := 8;
if Lib.AddWebCaptureCommand(Command) <> 0 then
  raise Exception.Create('undefined flag bit should have been refused');

Odczyt poleceń z powrotem: jeden klucz, dwie legalne reprezentacje

GetWebCaptureCommand zwraca 1 przy sukcesie i 0 w przeciwnym razie, a musi być bardziej tolerancyjna niż zapis, bo czyta pliki napisane przez innych producentów. Liczą się dwie asymetrie. Po pierwsze, czytelnik przyjmuje wpis polecenia niezależnie od tego, czy jest odwołaniem pośrednim, czy bezpośrednim słownikiem, rozwikłując go, gdy trzeba — PDF Library for Delphi zawsze zapisuje formę pośrednią wymaganą przez specyfikację, ale odmowa czytania niechlujnego pliku nikomu nie pomaga. Po drugie, /P może być albo stringiem, albo strumieniem, i ta jedna rzecz generalizuje się daleko poza Web Capture. ISO 32000-1 wielokrotnie pozwala, by pojedynczy klucz trzymał albo string, albo strumień, gdy wartość może być dowolnie duża — tutaj ciała POST, a ten sam wzorzec pojawia się w formacie gdzie indziej dla metadanych i osadzonych payloadów. Wewnętrzny helper ReadString w GetWebCaptureCommand przyjmuje zatem flagę AllowStream, a tylko odczyt /P przekazuje True: wartość strumieniowa jest dekodowana przez swój łańcuch filtrów i zwracana jako pole PostData, podczas gdy /URL, /CT i /H zostają string-only, bo specyfikacja nie daje im alternatywy strumieniowej. Jeśli piszesz własny kod czytający PDF, to nawyk wart podkradnięcia — sprawdź w specyfikacji, które klucze są dual-typed, zamiast zakładać string

var
  Lib: TPDFlib;
  Command: TPDFlibWebCaptureCommand;
  I, Total, Removed: Integer;
begin
  Total := Lib.GetWebCaptureCommandCount;
  // Publiczne indeksowanie jest liczone od 1, jak w reszcie 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)]));

  // Usuwa cały słownik /SpiderInfo i raportuje, ile poleceń
  // wyleciało; zwraca 0, gdy nie było czego czyścić.
  Removed := Lib.ClearWebCaptureCommands;
  Writeln(Format('cleared %d command(s)', [Removed]));
end;

ClearWebCaptureCommands liczy polecenia, a potem czyści cały klucz /SpiderInfo z katalogu zamiast tylko opróżniać /C. Zostawienie /SpiderInfo z pustą tablicą poleceń twierdziłoby, że dokument ma proweniencję Web Capture i że nic nie pobrano, co jest innym i fałszywym twierdzeniem niż brak zapisanej proweniencji. Ten sam instynkt odnosi się ogólnie do metadanych katalogu — pusty słownik-szkielet to oświadczenie, nie neutralna pozostałość. Jeśli audytujesz to, co dokument już twierdzi, zanim go przepiszesz, wzorzec introspekcji z odczytywania akcji zakładek i adnotacji z istniejącego PDF naturalnie łączy się z przejściem read-then-clear tutaj

Zapisywanie metadanych to nie implementacja Web Capture

Bądź z sobą szczery co do zakresu: AddWebCaptureCommand zapisuje rekord zamierzonego albo zakończonego pobrania i to wszystko. PDF Library for Delphi nie wykonuje żądań HTTP, nie podąża za linkami, które opisuje twoja wartość Levels, i nie respektuje flag SAME_SITE ani SAME_PATH — te flagi to dokumentacja tego, jak zachowywał się jakiś crawler, nie instrukcje wykonywane przez bibliotekę. Twój własny kod pobierania, czymkolwiek jest, pozostaje w całości twoim problemem

Dwie dalsze części ISO 32000-1 §14.10 też są poza tym API. Web Capture definiuje zestawy zawartości — struktury /Contents wiążące poszczególne fragmenty stron z materiałem źródłowym, z którego powstały — oraz drzewo nazw mapujące identyfikatory cyfrowe na te zestawy zawartości. PDF Library for Delphi nie utrzymuje żadnego z nich. To, co dostajesz, to lista poleceń: odpowiedź najwyższego poziomu na pytanie „skąd pobrano ten dokument i jak głęboko zeszło indeksowanie", co w zastosowaniach proweniencyjnych, audytowych i archiwizacyjnych zwykle jest właśnie tą częścią, o którą ktokolwiek pyta. Jeśli chcesz, żeby adresy URL były klikalne dla czytelników, a nie tylko zapisane dla audytorów, to osobna robota — zobacz budowanie akcji GoToR, GoToE i Launch po stronie akcji oraz wyzwalacze akcji cyklu życia dokumentu i stron do zachowania przy otwieraniu i zamykaniu. Metadane poleceń katalogu /SpiderInfo opisane tutaj to część PDF Library for Delphi, natywnej biblioteki PDF dla Delphi i C++Builder