PDF Library for Delphi (PDFlibPas) записывает метаданные команд Web Capture в каталог документа через AddWebCaptureCommand, заполняя словарь /SpiderInfo, определённый в ISO 32000-1 §14.10. Каждая команда фиксирует URL, который был загружен, на сколько уровней вглубь ушла загрузка и небольшой набор флагов. Это запись намерения — сама библиотека ничего не загружает
Именно это различие — причина, по которой этот API так легко прочитать неправильно. Инженер поддержки приносит вам PDF, собранный из онлайн-документации вендора, и спрашивает, откуда взялась страница 40. Если производитель заполнил /SpiderInfo, ответ лежит в каталоге: URL, глубина обхода, возможно, тело POST-запроса, из которого родилась страница. Если нет — сколько ни читай поток содержимого, ничего не узнаешь. Запись этого происхождения дешёвая, структурно капризная и почти всегда пропускается
Что на самом деле записывает словарь /SpiderInfo каталога?
/SpiderInfo — это словарь информации Web Capture, висящий на каталоге документа, и в нём ровно две вещи, которые волнуют PDF Library for Delphi: /V — числовая версия базы данных Web Capture, которую библиотека всегда пишет как 1.0, и /C — массив словарей команд. Номер версии — это не версия PDF: он идентифицирует саму модель данных Web Capture, и 1.0 — единственное значение, определённое в ISO 32000-1 §14.10. Всё интересное живёт в /C. Каждый словарь команды несёт /URL (адрес запроса), /L (сколько уровней ссылок было пройдено от него) и /F (флаги обхода). Три необязательных ключа описывают сам запрос, когда это не был обычный GET: /P для данных POST, /CT для типа содержимого этих данных и /H для дополнительных заголовков запроса. PDF Library for Delphi отображает все шесть на одну запись, TPDFlibWebCaptureCommand, поля которой — URL, Levels, Flags, PostData, ContentType и Headers, все AnsiString, кроме двух счётчиков Integer. Наличие /SpiderInfo также поднимает минимальную версию документа: AddWebCaptureCommand вызывает EnsureMinVersion('1.3', 'Catalog /SpiderInfo Web Capture command'), а правила определения версии в PDF Library for Delphi регистрируют соответствующее правило PDF 1.3, так что документ, прочитанный позже, сообщает тот же минимум
Почему команда Web Capture должна быть косвенным объектом
Единственное структурное правило, на котором все спотыкаются: словарь команды обязан быть зарегистрирован как косвенный объект до того, как попадёт в /C, потому что ISO 32000-1 §14.10 определяет /C как массив косвенных ссылок на словари команд, а не массив словарей. Запихните в этот массив прямой словарь — и получите то, что парсится как валидный PDF, но ничего не значит как запись Web Capture: вьюер или архивный инструмент, обходящий список команд, найдёт инлайн-объект там, где должна быть ссылка, и запись просто не будет командой. PDF Library for Delphi делает это внутри AddWebCaptureCommand: строит словарь, передаёт его OnNewIndObj, чтобы получить номер объекта и номер поколения, оборачивает эту пару в косвенную ссылку и добавляет ссылку в массив. Если OnNewIndObj не выдаёт положительный номер объекта, функция возвращает 0 и массив остаётся нетронутым. Спецификация настаивает на косвенности потому, что одна команда может разделяться — один и тот же загруженный ресурс можно ссылать из нескольких мест структуры Web Capture, — а разделяемые объекты в PDF по определению косвенные
Как добавить команду Web Capture из Delphi?
Заполните запись целиком и вызовите AddWebCaptureCommand, который возвращает новый индекс команды с единицы или 0, если валидация её отклонила. Полнота здесь важна больше обычного. Delphi инициализирует управляемые поля AnsiString локальной записи пустыми, а поля Integer — Levels и Flags — такой обработки не получают, так что запись, которую вы забыли заполнить, приходит с тем, что лежало на стеке. AddWebCaptureCommand требует, чтобы Levels был не меньше 1, а Flags — неотрицательным, и мусор со стека либо будет отклонён с порога, либо, что хуже, молча принят как правдоподобная глубина
var
Lib: TPDFlib;
Command: TPDFlibWebCaptureCommand;
Index: Integer;
begin
Lib := TPDFlib.Create;
try
if Lib.LoadFromFile('captured-site.pdf', '') = 1 then
begin
// Заполните каждое поле: Levels и Flags — обычные Integers,
// и если их пропустить, там окажется мусор со стека.
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 := '';
// Возвращает новый индекс команды с единицы или 0 при отклонении.
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;
Первый успешный вызов и создаёт структуру. AddWebCaptureCommand просит у внутреннего хелпера WebCaptureCommands(True) массив команд, и этот хелпер создаёт /SpiderInfo с записью /V, а затем пустой массив /C, только если их нет. Он намеренно консервативен к тому, что находит: если в каталоге уже есть ключ /SpiderInfo, значение которого не словарь, или ключ /C, значение которого не массив, хелпер возвращает nil, а не перезаписывает существующий объект. Битая унаследованная структура приводит к провалу добавления; она не заставляет PDF Library for Delphi уничтожать то, что написал предыдущий производитель
Белый список флагов: три определённых бита и ничего больше
AddWebCaptureCommand валидирует Flags по whitelist и отклоняет любое значение с битом вне трёх констант, определённых в PDF Library for Delphi: PDF_WEB_CAPTURE_SAME_SITE = 1 (следовать только за ссылками, остающимися на том же сайте), PDF_WEB_CAPTURE_SAME_PATH = 2 (следовать только за ссылками в пределах того же пути) и PDF_WEB_CAPTURE_SUBMIT = 4 (этот запрос был отправкой формы, а не обычным получением). Передайте 8, или 16, или случайное -1 — и вызов вернёт 0, не коснувшись документа
Это строже, чем обычно бывают PDF-парсеры, и намеренно. /F — битовое поле без самописывающей структуры, так что неопределённый бит неотличим от испорченного, и потребитель, читающий значение позже, не сможет отличить будущее расширение от опечатки. Отказ при записи убирает двусмысленность из файла целиком. Заметьте также, что /P, /CT и /H пишутся только когда соответствующие поля записи непустые, так что обычный GET даёт словарь команды из трёх ключей — /URL, /L, /F — без загромождающих его пустых строк, и именно этого читатель файла ожидает для запроса без тела
// Отправка формы: тело POST плюс его тип содержимого.
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 плюс same-path — легальная комбинация: 1 или 2 = 3.
Command.Flags := PDF_WEB_CAPTURE_SAME_SITE or PDF_WEB_CAPTURE_SAME_PATH;
// Бит 3 спецификацией не определён, так что этот вариант до файла не дойдёт.
Command.Flags := 8;
if Lib.AddWebCaptureCommand(Command) <> 0 then
raise Exception.Create('undefined flag bit should have been refused');
Чтение команд обратно: один ключ и два допустимых представления
GetWebCaptureCommand возвращает 1 при успехе и 0 иначе, и он обязан быть терпимее писателя, потому что читает файлы, написанные другими производителями. Здесь важны две асимметрии. Первая: читатель принимает запись команды и как косвенную ссылку, и как прямой словарь, разыменовывая при необходимости — PDF Library for Delphi всегда пишет косвенную форму, требуемую спецификацией, но отказ читать неряшливый файл не помогает никому. Вторая: /P может быть и строкой, и потоком, и это обобщение выходит далеко за пределы Web Capture. ISO 32000-1 многократно разрешает одному ключу держать либо строку, либо поток, когда значение может быть сколь угодно большим — тела POST здесь, и тот же паттерн встречается в формате в метаданных и встроенных полезных нагрузках. Внутренний хелпер ReadString в GetWebCaptureCommand поэтому принимает флаг AllowStream, и только чтение /P передаёт True: значение-поток декодируется через свою цепочку фильтров и возвращается как поле PostData, тогда как /URL, /CT и /H остаются только строками, потому что спецификация не предлагает им альтернативу-поток. Если вы пишете свой код чтения PDF, эту привычку стоит позаимствовать — проверяйте по спецификации, какие ключи двутипизированы, вместо того чтобы предполагать строку
var
Lib: TPDFlib;
Command: TPDFlibWebCaptureCommand;
I, Total, Removed: Integer;
begin
Total := Lib.GetWebCaptureCommandCount;
// Публичная индексация с единицы, как и в остальном 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)]));
// Удаляет весь словарь /SpiderInfo и сообщает, сколько команд
// было удалено; возвращает 0, когда очищать нечего.
Removed := Lib.ClearWebCaptureCommands;
Writeln(Format('cleared %d command(s)', [Removed]));
end;
ClearWebCaptureCommands считает команды и затем вычищает весь ключ /SpiderInfo из каталога, а не просто опустошает /C. Оставить /SpiderInfo с пустым массивом команд означало бы утверждать, что документ имеет происхождение Web Capture и что ничего не было загружено, — а это другое и ложное заявление, нежели отсутствие записанного происхождения. Тот же инстинкт применим к метаданным каталога в целом: пустой словарь-оболочка — это утверждение, а не нейтральный остаток. Если вы аудируете, что документ уже заявляет, перед переписыванием, паттерн интроспекции из статьи про чтение структуры оглавления, аннотаций и действий из существующего PDF естественно сочетается здесь с проходом «прочитай — затем очисти»
Запись метаданных — это не реализация Web Capture
Будьте честны с собой насчёт границ: AddWebCaptureCommand сохраняет запись о намеренной или выполненной загрузке — и только. PDF Library for Delphi не выполняет HTTP-запросы, не следует за ссылками, которые описывает ваше значение Levels, и не соблюдает флаги SAME_SITE или SAME_PATH — эти флаги документируют, как вёл себя какой-то краулер, а не инструкции, которые выполняет библиотека. Ваш собственный код загрузки, какой бы он ни был, остаётся полностью вашей проблемой
Ещё два куска ISO 32000-1 §14.10 тоже вне этого API. Web Capture определяет наборы содержимого — структуры /Contents, которые привязывают отдельные части страниц к исходному материалу, из которого они пришли, — и дерево имён, отображающее цифровые идентификаторы на эти наборы. PDF Library for Delphi не ведёт ни того, ни другого. Что вы получаете — это список команд: верхнеуровневый ответ на вопрос «откуда был загружен этот документ и насколько глубоко ушёл обход», который для происхождения, аудита и архивного приёма обычно и есть та часть, о которой вообще спрашивают. Если URL нужны кликабельными для читателей, а не только записанными для аудиторов, это отдельная работа — см. статью про построение действий GoToR, GoToE и Launch для стороны действий и про триггеры действий жизненного цикла документа и страницы для поведения при открытии или закрытии. Метаданные команд /SpiderInfo каталога, описанные здесь, — часть PDF Library for Delphi, нативной PDF-библиотеки для Delphi и C++Builder