Геодезист відкриває план ділянки і хоче приховати горизонталі рельєфу, залишивши видимими інженерні мережі. Рецензент хоче бачити червоні позначки приміток на екрані, але щоб їх не було на роздруківці. Інформаційний лист продукту постачається трьома мовами в одному файлі, і читач сам вибирає, яку мову відображати. Усі три випадки використовують одну й ту саму функцію PDF, а панель, яка керує ними в Acrobat, називається 'Шари' (Layers). Функція, яка лежить в основі цієї панелі, — це необов'язковий вміст (optional content), і саме він дозволяє одній сторінці містити кілька незалежних візуальних пластів, які засіб перегляду може вмикати та вимикати.
Необов'язковий вміст специфікований в ISO 32000-1 §8.11. Одиницею видимості є група необов'язкового вмісту, OCG (optional content group), словник типу /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;
Шар червоних приміток — це випадок, який варто виділити особливо. Він відображається на екрані, щоб рецензент бачив нотатку, а його прапорець printable встановлений у нуль, тому роздруківка того ж файлу не міститиме тексту приміток. Ця асиметрія є головною причиною розділення двох станів.
Зворотне зчитування конфігурації
Зчитування шарів — це інший шлях через ту саму структуру. Після завантаження файлу функція 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 необов'язкового вмісту, які постачаються у складі бібліотеки PDF для Delphi разом із засобами сторінок, тексту, шрифтів та перевірки відповідності стандартам, що обговорюються в інших статтях цього блогу.