Геодезист отваря генерален план и иска хоризонталите да бъдат скрити, докато подземните комуникации останат показани. Рецензент иска червените анотации за корекции да се виждат на екрана, но да изчезнат от разпечатката. Продуктов лист се доставя на три езика от един файл, а читателят избира кой език да се покаже. И трите са една и съща функция на PDF, а панелът, който ги управлява в Acrobat, се нарича Слоеве (Layers). Функцията под този панел е незадължително съдържание (optional content) и именно тя позволява на една страница да носи няколко независими визуални нива, които четецът включва и изключва.
Незадължителното съдържание е специфицирано в ISO 32000-1 §8.11. Единицата за видимост е незадължителна група съдържание (optional content group - OCG), която представлява речник от тип /OCG, носещ име. Маркираното съдържание на дадена страница се свързва с група, а четецът решава дали тази група да се показва в момента. Свързана конструкция, речникът за членство в незадължително съдържание (optional content membership dictionary - 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 разкрива двойката чрез два сетера (setters). SetOptionalContentGroupVisible приема идентификатора на групата и флаг, където едно означава видим, а нула означава скрит, и управлява състоянието на екрана по подразбиране. SetOptionalContentGroupPrintable приема идентификатора на групата и флаг за това дали слоят се извежда при печат на документа. Съответстващите гетери (getters), 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 заедно с възможностите за страници, текст, шрифтове и съвместимост, обсъждани на други места в този блог.