Геодезист открывает план участка и хочет скрыть горизонтали рельефа, оставив включенными инженерные сети. Рецензент хочет видеть красные пометки на экране, но скрыть их при печати. Информационный лист продукта поставляется на трех языках в одном файле, и читатель выбирает нужный язык. Все это функции одного и того же механизма PDF, а панель для управления ими в Acrobat называется слоями (Layers). В основе этой панели лежит концепция дополнительного (необязательного) содержимого (optional content), которая позволяет одной странице нести несколько независимых визуальных слоев, включаемых и выключаемых программой просмотра.
Дополнительное содержимое описывается в разделе §8.11 стандарта ISO 32000-1. Единицей видимости является группа дополнительного содержимого (Optional Content Group, OCG), представляющая собой словарь типа /OCG с именем группы. Помеченное содержимое на странице связывается с группой, а программа просмотра определяет, должна ли эта группа отображаться в данный момент. Смежная структура, словарь членства в дополнительном содержимом (OCMD), позволяет ставить видимость в зависимость от логической комбинации нескольких групп, однако на практике чаще всего используется одна именованная группа, представляющая один слой. Документ объединяет весь этот механизм через одну запись в каталоге, а именно /OCProperties, которая описана ниже.
Что должен содержать каталог
Сама по себе группа OCG неактивна. Чтобы программа просмотра могла перечислить слой и запомнить его состояние, каталог документа должен содержать словарь /OCProperties, и раздел §8.11.4 точно определяет его структуру. В нем есть массив /OCGs, перечисляющий каждую группу в файле, и запись /D, содержащая конфигурацию по умолчанию. Конфигурация по умолчанию применяется при первом открытии файла. Она определяет, какие группы изначально включены, а какие выключены, какие записи заблокированы от изменений пользователем, а также задает порядок отображения и вложенность имен слоев в панели с помощью массива /Order.
На практике создание слоя никогда не ограничивается локальными изменениями на странице. Группа должна быть отрисована на странице, а также зарегистрирована в структуре уровня каталога, которой раньше не существовало. PDFlibPas выполняет обе задачи за вас. Первый вызов для создания группы добавляет запись /OCProperties в каталог и инициализирует конфигурацию по умолчанию, поэтому слой отрисовывается на странице и отображается в списке слоев без дополнительной ручной настройки.
Почему режим соответствия стандартам может блокировать эту функцию
Перед выполнением кода управления слоями выбранный стандарт соответствия документа определяет, допустимо ли вообще использовать дополнительное содержимое. Спецификация PDF/A-1, представляющая собой архивный профиль в стандарте ISO 19005-1, полностью запрещает запись /OCProperties в разделе §6.1.13. Логика соответствует назначению формата. Архивный файл должен отображаться абсолютно одинаково у всех читателей в течение долгого времени, а содержимое, видимость которого пользователь может изменить, нарушает это правило. Поэтому профиль запрещает данную структуру во избежание двусмысленности. Стандарты PDF/A-2 и PDF/A-3, определенные в ISO 19005-2 и ISO 19005-3, придерживаются противоположной позиции в разделе §6.9 и разрешают дополнительное содержимое с соблюдением правил видимости по умолчанию.
Это различие напрямую отражается в API. Когда документ находится в режиме PDF/A-1, функция NewOptionalContentGroup отказывается создавать группу и возвращает ноль, так как выполнение запроса привело бы к созданию файла, нарушающего заявленный стандарт соответствия. В режимах PDF/A-2 или PDF/A-3, а также в обычном свободном PDF тот же вызов завершается успешно и возвращает ненулевой идентификатор группы. Нулевой результат означает не общую ошибку, а указывает на то, что активный уровень соответствия стандартам не поддерживает данную функцию.
var
Pdf: TPDFlib;
LayerID: Integer;
begin
Pdf := TPDFlib.Create(nil);
try
Pdf.NewDocument;
Pdf.SetPDFAMode(1); // PDF/A-1a: OCProperties forbidden
LayerID := Pdf.NewOptionalContentGroup('Utilities');
if LayerID = 0 then
// refused under PDF/A-1; not a transient error, the mode bans layers
ShowMessage('Optional content is not available in PDF/A-1 mode.');
finally
Pdf.Free;
end;
end;
Два состояния для каждого слоя вместо одного
Слой не может быть просто видимым или невидимым. Конфигурация по умолчанию сохраняет его состояние на экране и отдельное состояние для печати, так как раздел §8.11.4 разделяет то, что программа просмотра показывает на экране, и то, что выводится при печати. Эти состояния независимы. Черновой водяной знак может отображаться на экране, но отсутствовать на бумаге, а слой с линиями реза может быть скрыт на экране, но отправлен на плоттер. Объединение этих состояний лишило бы нас гибкости управления, ради которой и создавалась эта функция.
PDFlibPas открывает доступ к этой паре через два метода установки значений. Метод SetOptionalContentGroupVisible принимает идентификатор группы и флаг (единица означает видимость, ноль указывает на скрытие) и управляет состоянием на экране по умолчанию. Метод SetOptionalContentGroupPrintable принимает идентификатор группы и флаг, определяющий вывод слоя при печати документа. Соответствующие методы получения значений, GetOptionalContentGroupVisible и GetOptionalContentGroupPrintable, возвращают единицу или ноль, позволяя считывать состояние экрана и печати независимо друг от друга.
Создание двух слоев на странице
Создание и заполнение слоя выполняются в строго определенном порядке. Вы отрисовываете содержимое слоя на текущей странице, а затем вызываете метод SetContentStreamOptional с идентификатором группы. Это действие оборачивает текущий поток содержимого страницы, привязывая все нарисованное к этой группе. Поскольку вызов фиксирует состояние потока на данный момент, правильный алгоритм состоит в отрисовке элементов одного слоя, их назначении и только затем переходе к следующему слою. Пример ниже размещает инженерные сети на первой странице, а пометки рецензента размещаются на второй странице, настраивает состояния видимости и печати для каждого слоя и сохраняет документ.
var
Pdf: TPDFlib;
FontID, UtilLayer, RedlineLayer: Integer;
begin
Pdf := TPDFlib.Create(nil);
try
Pdf.NewDocument; // unconstrained PDF: layers allowed
Pdf.SetPageDimensions(595, 842); // A4 in points
FontID := Pdf.AddStandardFont(0); // Helvetica
Pdf.SelectFont(FontID);
// Layer 1: utilities, drawn then assigned to its own group
Pdf.SetTextColor(0.10, 0.30, 0.65);
Pdf.DrawText(72, 770, 'Utilities: water main, valve chamber');
UtilLayer := Pdf.NewOptionalContentGroup('Utilities');
Pdf.SetContentStreamOptional(UtilLayer);
Pdf.SetOptionalContentGroupVisible(UtilLayer, 1); // shown on screen
Pdf.SetOptionalContentGroupPrintable(UtilLayer, 1); // and on paper
// Layer 2: reviewer redline on a fresh page
Pdf.InsertPages(2, 1); // append one page after page 1
Pdf.SetTextColor(0.80, 0.10, 0.10);
Pdf.DrawText(72, 770, 'REVIEW: revise valve spec before issue');
RedlineLayer := Pdf.NewOptionalContentGroup('Reviewer markup');
Pdf.SetContentStreamOptional(RedlineLayer);
Pdf.SetOptionalContentGroupVisible(RedlineLayer, 1); // visible while reviewing
Pdf.SetOptionalContentGroupPrintable(RedlineLayer, 0); // never printed
Pdf.SaveToFile('SitePlan_Layers.pdf');
finally
Pdf.Free;
end;
end;
Слой с пометками рецензента заслуживает особого внимания. Он отображается на экране для рецензента, но его флаг печати равен нулю, поэтому при печати этого файла текст рецензии не выводится. Данная асимметрия как раз и объясняет необходимость разделения двух этих состояний.
Чтение конфигурации
Чтение слоев представляет собой обратный процесс обхода той же структуры. После загрузки файла метод GetOptionalContentConfigCount сообщает количество словарей конфигурации в документе; первая конфигурация по умолчанию получает идентификатор 1. Внутри конфигурации функция GetOptionalContentConfigOrderCount возвращает количество записей в дереве порядка, индексация которых начинается с 1. Для каждой записи функция GetOptionalContentConfigOrderItemLabel возвращает отображаемый текст, а GetOptionalContentConfigOrderItemLevel возвращает глубину вложенности, что позволяет воссоздать структуру панели со вложенными слоями под заголовками.
Каждая запись также имеет тип. Метод GetOptionalContentConfigOrderItemType позволяет отличить реальную группу дополнительного содержимого от обычной текстовой метки, служащей заголовком раздела дерева. Это разделение важно, так как запросы состояния группы имеют смысл только для реальных групп. Для записи группы метод GetOptionalContentConfigState сообщает, включает ли конфигурация ее изначально, отключает или оставляет без изменений, а GetOptionalContentConfigLocked указывает, запрещено ли пользователю переключать ее состояние. Приведенный ниже цикл выводит дерево порядка с указанием состояния и статуса блокировки каждой группы с отступами в зависимости от уровня вложенности.
var
Pdf: TPDFlib;
Cfg, Count, I, ItemType, GroupID, Indent: Integer;
Line: string;
begin
Pdf := TPDFlib.Create(nil);
try
if Pdf.LoadFromFile('SitePlan_Layers.pdf', '') = 0 then Exit;
if Pdf.GetOptionalContentConfigCount = 0 then Exit;
Cfg := 1; // the default configuration
Count := Pdf.GetOptionalContentConfigOrderCount(Cfg);
for I := 1 to Count do
begin
Indent := Pdf.GetOptionalContentConfigOrderItemLevel(Cfg, I);
Line := StringOfChar(' ', Indent * 2)
+ Pdf.GetOptionalContentConfigOrderItemLabel(Cfg, I);
ItemType := Pdf.GetOptionalContentConfigOrderItemType(Cfg, I);
if ItemType = 1 then // 1 = optional content group
begin
GroupID := Pdf.GetOptionalContentConfigOrderItemID(Cfg, I);
case Pdf.GetOptionalContentConfigState(Cfg, GroupID) of
1: Line := Line + ' [on]';
2: Line := Line + ' [off]';
3: Line := Line + ' [unchanged]';
end;
if Pdf.GetOptionalContentConfigLocked(Cfg, GroupID) = 1 then
Line := Line + ' (locked)';
end;
// ItemType = 2 is a text label heading; it has no per-group state
Writeln(Line);
end;
finally
Pdf.Free;
end;
end;
Две детали обеспечивают корректность этого цикла. Индекс порядка начинается с единицы (от 1 до общего количества), что соответствует внутреннему представлению дерева в библиотеке. Кроме того, вызовы для конкретных групп выполняются только тогда, когда тип элемента является группой, поскольку текстовая метка представляет собой просто заголовок с именем и уровнем, но не имеет состояний включения, выключения или блокировки. Без этой проверки вы запросите у текстовой метки состояние, которого у нее нет.
Сфера применения
Слои служат механизмом представления, поэтому движок должен учитывать их при любом способе рендеринга страницы. Вопросы визуализации подробно описаны в нашем руководстве по многодвижковому рендерингу в Delphi. Слои также связаны со структурой документа, поскольку имя слоя представляет собой видимый пользователю текст, а структурированное дерево слоев упрощает навигацию, что перекликается с материалом в нашей статье о тегированных PDF и структуре доступности. Обе эти темы тесно связаны с описанным здесь API дополнительного содержимого, которое поставляется в составе библиотеки Delphi PDF Library вместе с инструментами для работы со страницами, текстом, шрифтами и стандартами соответствия.