Artykuł techniczny

Warstwy PDF w Delphi: Opcjonalne grupy zawartości (OCG)

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

Struktura grup zawartości opcjonalnej PDF za PDF Library for Delphi: wpis OCProperties katalogu wylicza każdą warstwę OCG i jej domyślną konfigurację ze stanami startowymi, blokadami i kolejnością panelu, podczas gdy oznaczona zawartość strony wiąże rysunki z grupami
Warstwy stają się adresowalne tylko przez katalog: OCProperties wylicza każdy OCG, a konfiguracja domyślna zapisuje stany początkowe, blokady i kolejność 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. PDF Library for Delphi 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
      // odrzucone w trybie PDF/A-1; to nie błąd przejściowy, ten tryb zabrania warstw
      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

PDF Library for Delphi 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

Macierz niezależnych stanów widoczności na ekranie i w druku dla warstw PDF utworzonych z Delphi PDF Library: warstwy narzędziowe pozostają widoczne wszędzie, adnotacje recenzenta pokazują się na ekranie, ale nigdy nie drukują, a linia cięcia plotera chowa się na ekranie, choć nadal się drukuje
Widoczność na ekranie i emisja do druku ustawiane są per grupa, więc warstwy pomocnicze mogą pojawiać się wszędzie, podczas gdy oznaczenia recenzenta pozostają notatką tylko na ekran

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);

    // Warstwa 1: narzędzia, narysowane, a następnie przypisane do własnej grupy
    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);   // pokazane na ekranie
    Pdf.SetOptionalContentGroupPrintable(UtilLayer, 1); // i na papierze

    // Warstwa 2: poprawki recenzenta na nowej stronie
    Pdf.InsertPages(2, 1);                     // dołącz jedną stronę po stronie 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);    // widoczne podczas przeglądu
    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;                                  // konfiguracja domyślna
    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 = opcjonalna grupa zawartości
      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 to nagłówek etykiety tekstowej; nie ma stanu dla każdej grupy

      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

Pętla odczytu konfiguracji zawartości opcjonalnej w PDF Library for Delphi: wylicz konfiguracje, przejdź wpisy kolejności od jedynki, potem rozgałęź po ItemType, tak by o stanach on, off lub locked odpytywane były tylko prawdziwe grupy, a etykiety tekstowe były pomijane
Odczyt warstw wstecz przechodzi wpisy kolejności liczone od jednego i odpytuje stan tylko dla prawdziwych grup, ponieważ nagłówek etykiety nie ma stanu włączono, wyłączono ani zablokowano do zgłoszenia

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