Geodeta otwiera plan zagospodarowania terenu i chce ukryć poziomice, pozostawiając widoczne sieci uzbrojenia terenu. Recenzent oczekuje, że czerwone adnotacje będą widoczne na ekranie, ale znikną z wydruku. Karta produktu jest dostarczana w trzech językach w jednym pliku, a czytelnik wybiera, który język ma być wyświetlany. Wszystkie trzy przypadki to ta sama funkcja PDF, a panel, który nimi steruje w programie Acrobat, nosi nazwę Warstwy. Funkcja leżąca u podstaw tego panelu to zawartość opcjonalna (optional content) i to ona pozwala pojedynczej stronie przenosić kilka niezależnych warstw wizualnych, które przeglądarka włącza i wyłącza.
Opcjonalna zawartość jest zdefiniowana w ISO 32000-1 §8.11. Jednostką widoczności jest opcjonalna grupa zawartości, czyli OCG (Optional Content Group), będąca słownikiem typu /OCG posiadającym nazwę. Oznaczona zawartość na stronie jest powiązana z grupą, a przeglądarka decyduje, czy ta grupa jest aktualnie wyświetlana. Powiązana struktura, czyli słownik przynależności do zawartości opcjonalnej (OCMD), pozwala na uzależnienie widoczności od logicznej kombinacji kilku grup, jednak najczęstszym przypadkiem jest pojedyncza nazwana grupa reprezentująca pojedynczą warstwę. Dokument łączy cały ten mechanizm poprzez jeden wpis w katalogu głównym: /OCProperties, opisany poniżej.
Co musi zawierać katalog główny dokumentu
Sama grupa OCG jest nieaktywna. Aby przeglądarka mogła wyświetlić warstwę na liście i zapamiętać jej stan, katalog dokumentu wymaga słownika /OCProperties, a paragraf 8.11.4 precyzuje, co dokładnie się w nim znajduje. Znajdziemy tam tablicę /OCGs zawierającą nazwy wszystkich grup w pliku oraz wpis /D przechowujący konfigurację domyślną. Konfiguracja domyślna to część stosowana przez czytnik przy pierwszym otwarciu pliku. Określa ona, które grupy są początkowo włączone, a które wyłączone, które wpisy są zablokowane przed przełączeniem przez użytkownika oraz, za pośrednictwem tablicy /Order, jak nazwy warstw są uporządkowane i zagnieżdżone w panelu.
Konsekwencją w praktyce jest to, że tworzenie warstwy nigdy nie jest działaniem wyłącznie lokalnym. Grupa musi zostać narysowana na stronie, a także zarejestrowana w strukturze na poziomie katalogu dokumentu, która wcześniej nie istniała. PDFlibPas wykonuje obie te czynności za Ciebie. Pierwsze wywołanie tworzące grupę dodaje wpis /OCProperties do katalogu i inicjuje domyślną konfigurację, dzięki czemu warstwa jest zarówno rysowana, jak i wyświetlana na liście bez konieczności prowadzenia dodatkowej ewidencji z Twojej strony.
Dlaczego tryb zgodności może zablokować tę funkcję
Zanim uruchomiony zostanie jakikolwiek kod związany z warstwami, cel zgodności dokumentu decyduje o tym, czy opcjonalna zawartość jest w ogóle dozwolona. Profil archiwalny PDF/A-1, zdefiniowany w ISO 19005-1, zabrania wpisu /OCProperties wprost w §6.1.13. Argumentacja ta odpowiada celowi formatu: plik archiwalny musi być renderowany identycznie przez każdego czytelnika w odległej przyszłości, a zawartość, której widoczność użytkownik może zmieniać, nie ma stałego wyglądu, dlatego profil ten zabrania stosowania tej struktury, aby uniknąć niejednoznaczności archiwum. Z kolei formaty PDF/A-2 i PDF/A-3, zdefiniowane w normach ISO 19005-2 i ISO 19005-3, przyjmują przeciwne stanowisko w §6.9 i zezwalają na zawartość opcjonalną, określając zasady dotyczące widoczności domyślnej.
Ta różnica uwidacznia się bezpośrednio w API. Gdy dokument znajduje się w trybie PDF/A-1, funkcja NewOptionalContentGroup odmawia utworzenia grupy i zwraca zero, ponieważ spełnienie tego żądania wygenerowałoby plik niezgodny z deklarowaną zgodnością. W trybach PDF/A-2 lub PDF/A-3 oraz w zwykłym dokumencie PDF bez ograniczeń, to samo wywołanie kończy się powodzeniem i zwraca niezerowy identyfikator grupy. Wynik zero nie oznacza zatem ogólnego błędu; jest to informacja od biblioteki, że aktywny poziom zgodności nie pozwala na użycie tej funkcji.
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;
Dwa stany na warstwę, a nie jeden
Warstwa nie jest po prostu widoczna lub niewidoczna. Domyślna konfiguracja rejestruje jej stan na ekranie oraz oddzielny stan drukowania, ponieważ paragraf 8.11.4 rozróżnia to, co przeglądarka wyświetla, od tego, co generuje system drukowania. Te dwa stany są celowo niezależne. Znak wodny szkicu (draft) może być wyświetlany na ekranie i pomijany na papierze, a warstwa linii cięcia może być ukryta na ekranie, ale wysyłana do plotera. Połączenie tych dwóch stanów zmusiłoby jeden do naśladowania drugiego, co uniemożliwiłoby precyzyjną kontrolę, dla której ta funkcja powstała.
PDFlibPas udostępnia tę parę za pomocą dwóch metod ustawiających. Funkcja SetOptionalContentGroupVisible przyjmuje identyfikator grupy oraz flagę, gdzie jeden oznacza widoczność, a zero ukrycie, i steruje domyślim stanem na ekranie. Z kolei SetOptionalContentGroupPrintable przyjmuje identyfikator grupy oraz flagę określającą, czy warstwa ma być generowana przy drukowaniu dokumentu. Odpowiadające im funkcje pobierające, GetOptionalContentGroupVisible i GetOptionalContentGroupPrintable, zwracają odpowiednio jeden lub zero, co pozwala na niezależny odczyt stanu wyświetlania i drukowania warstwy zamiast wnioskowania o jednym na podstawie drugiego.
Budowanie dwóch warstw na stronie
Tworzenie warstwy i wypełnianie jej zawartością odbywa się w określonej kolejności. Rysujesz zawartość warstwy na bieżącej stronie, a następnie wywołujesz SetContentStreamOptional z identyfikatorem grupy, co opakowuje bieżący strumień zawartości strony, przypisując wszystko, co do tej pory narysowano, do tej grupy. Ponieważ wywołanie przechwytuje to, co w danym momencie znajduje się w strumieniu, zalecaną praktyką jest umieszczenie obiektów jednej warstwy, przypisanie ich, a dopiero potem rozpoczęcie kolejnej warstwy. Poniższy przykład umieszcza sieci uzbrojenia terenu na pierwszej stronie, a czerwone adnotacje recenzenta na drugiej stronie, ustawia stan ekranu i drukowania dla każdej warstwy, po czym zapisuje plik.
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;
Warstwa adnotacji (redline) jest przypadkiem wciągającym i wartym uwagi. Jest ona wyświetlana na ekranie, aby recenzent widział notatkę, ale jej flaga drukowania ma wartość zero, dzięki czemu wydruk tego samego pliku nie będzie zawierał tekstu recenzji. Ta asymetria jest głównym powodem rozdzielenia obu tych stanów.
Odczytywanie konfiguracji z powrotem
Odczytywanie warstw to inna wędrówka przez tę samą strukturę. Po załadowaniu pliku funkcja GetOptionalContentConfigCount informuje, ile słowników konfiguracji zawiera dokument; pierwsza konfiguracja domyślna ma identyfikator config ID równy 1. W obrębie konfiguracji, GetOptionalContentConfigOrderCount podaje liczbę wpisów w drzewie kolejności, które indeksuje się od 1. Dla każdego wpisu funkcja GetOptionalContentConfigOrderItemLabel zwraca jego tekst wyświetlany, a GetOptionalContentConfigOrderItemLevel zwraca głębokość zagnieżdżenia, dzięki czemu strukturę panelu z podwarstwami wciętymi pod nagłówkami można zrekonstruować kropka w kropkę.
Każdy wpis posiada również typ. Funkcja GetOptionalContentConfigOrderItemType odróżnia rzeczywistą grupę zawartości opcjonalnej od zwykłej etykiety tekstowej, która istnieje tylko jako nagłówek sekcji drzewa. To rozróżnienie jest ważne, ponieważ zapytania o stan poszczególnych grup mają sens tylko dla rzeczywistych grup. Dla wpisu grupy funkcja GetOptionalContentConfigState zgłasza, czy konfiguracja uruchamia ją jako włączoną, wyłączoną czy pozostawia bez zmian, a GetOptionalContentConfigLocked informuje, czy użytkownik ma zablokowaną możliwość jej przełączania. Poniższa pętla renderuje drzewo kolejności wraz ze stanem każdej grupy i statusem blokady, stosując wcięcia zgodnie z poziomem zagnieżdżenia.
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;
Dwa szczegóły zapewniają poprawne działanie tej pętli. Indeks kolejności zaczyna się od 1 i biegnie do wartości liczby elementów, co odpowiada wewnętrznej numeracji drzewa w bibliotece. Dodatkowo, wywołania specyficzne dla grup są wykonywane tylko wtedy, gdy typ elementu to grupa, ponieważ etykieta tekstowa to nagłówek posiadający nazwę i poziom, ale niemający stanów włączenia, wyłączenia ani blokady. Pominięcie tego zabezpieczenia spowoduje zapytanie etykiety o stan, którego ona nie posiada.
Gdzie to pasuje
Warstwy są mechanizmem prezentacji, dlatego silnik musi je uwzględniać na każdej ścieżce renderującej stronę. Kwestia renderowania została omówiona w naszym przewodniku po renderowaniu wielosilnikowym w Delphi. Warstwy łączą się także ze strukturą dokumentu, ponieważ ich nazwa to tekst widoczny dla autora, a czytelnik odnosi korzyści z uporządkowanego konspektu warstw, co nawiązuje do zagadnień z naszego artykułu na temat struktury i dostępności znaczników PDF (tagged PDF). Oba te obszary współpracują z interfejsami API opcjonalnej zawartości opisanymi tutaj, które są dostarczane jako część biblioteki PDF dla Delphi wraz z funkcjami obsługi stron, tekstu, czcionek i zgodności omawianymi w innych miejscach na tym blogu.