Technical Article

Vrstvy PDF v Delphi: Volitelné skupiny obsahu (OCG)

Geodet otevře plán staveniště a chce skrýt vrstevnice, zatímco inženýrské sítě zůstanou zobrazené. Recenzent požaduje, aby červeně vyznačené anotace byly viditelné na obrazovce, ale zmizely z výtisku. Produktový list se dodává ve třech jazycích v rámci jednoho souboru a čtenář si vybírá, který jazyk se zobrazí. Všechny tři případy představují stejnou funkci PDF a panel, který je v Acrobat řídí, se nazývá Vrstvy. Funkce pod tímto panelem se nazývá volitelný obsah (optional content) a právě ona umožňuje, aby jedna stránka nesla několik nezávislých vizuálních vrstev, které prohlížeč zapíná a vypíná.

Volitelný obsah je specifikován v ISO 32000-1 §8.11. Jednotkou viditelnosti je volitelná skupina obsahu, OCG (optional content group), což je adresář typu /OCG s názvem. Označený obsah na stránce je spojen se skupinou a prohlížeč rozhoduje o tom, zda je tato skupina aktuálně zobrazena. Související konstrukce, adresář členství ve volitelném obsahu neboli OCMD (optional content membership dictionary), umožňuje, aby viditelnost závisela na booleovské kombinaci několika skupin, ale běžným případem je jedna pojmenovaná skupina představující jednu vrstvu. Dokument spojuje celý mechanismus dohromady prostřednictvím jedné položky katalogu /OCProperties, která je popsána dále.

Co musí katalog obsahovat

Samotná skupina OCG je neaktivní. Aby prohlížeč mohl vypsat vrstvu a zapamatovat si její stav, katalog dokumentu potřebuje adresář /OCProperties a §8.11.4 přesně popisuje, co do něj patří. Nachází se zde pole /OCGs pojmenovávající každou skupinu v souboru a položka /D obsahující výchozí konfiguraci. Výchozí konfigurace je ta část, kterou čtečka použije při prvním otevření souboru. Zaznamenává, které skupiny začínají jako zapnuté a které jako vypnuté, které položky jsou uzamčeny proti přepnutí uživatelem a jak jsou názvy vrstev uspořádány a zanořeny v panelu prostřednictvím pole /Order.

Praktickým důsledkem je, že vytvoření vrstvy není nikdy čistě lokální záležitostí. Skupina musí být vykreslena na stránce a také musí být zaregistrována ve struktuře na úrovni katalogu, která dříve neexistovala. PDFlibPas dělá obojí za vás. První volání, které vytvoří skupinu, přidá položku /OCProperties do katalogu a připraví výchozí konfiguraci, takže vrstva je vykreslena i zařazena do seznamu bez nutnosti dalšího účtování na vaší straně.

Proč může režim kompatibility tuto funkci odepřít

Předtím, než se spustí jakýkoli kód vrstvy, cílová kompatibilita dokumentu rozhodne, zda je volitelný obsah vůbec legální. PDF/A-1, archivační profil definovaný v ISO 19005-1, zakazuje položku /OCProperties přímo v §6.1.13. Odůvodnění odpovídá účelu formátu. Archivační soubor se musí zobrazovat identicky pro každého čtenáře i v daleké budoucnosti a obsah, jehož viditelnost může prohlížeč měnit, je obsahem, jehož vzhled není pevný, takže profil tuto konstrukci raději zakazuje, než aby povolil nejednoznačný archiv. PDF/A-2 a PDF/A-3, definované v ISO 19005-2 a ISO 19005-3, zastávají v §6.9 opačný názor a volitelný obsah povolují s pravidly pro výchozí viditelnost.

Tento rozdíl se projevuje přímo v rozhraní API. Když je dokument v režimu PDF/A-1, NewOptionalContentGroup odmítne vytvořit skupinu a vrátí nulu, protože vyhovění požadavku by vytvořilo soubor, který nesplňuje deklarovanou kompatibilitu. V režimu PDF/A-2 nebo PDF/A-3 a v běžném neomezeném PDF stejné volání uspěje a vrátí nenulové ID skupiny. Nulový výsledek tedy není obecnou chybou, kterou by bylo nutné zkoumat později; knihovna vám tím sděluje, že aktivní úroveň kompatibility nemá pro tuto funkci prostor.

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;

Dva stavy na vrstvu, nikoli jeden

Vrstva není jednoduše viditelná nebo neviditelná. Výchozí konfigurace zaznamenává její stav na obrazovce a samostatný tiskový stav, protože §8.11.4 rozlišuje to, co zobrazuje prohlížeč, od toho, co generuje tisková pipeline. Tyto dva stavy jsou záměrně nezávislé. Vodoznak konceptu (draft watermark) může být zobrazen na obrazovce a vynechán z papíru, a vrstva s řeznými liniemi může být na obrazovce skrytá, ale přesto odeslána na plotter. Sloučení těchto dvou stavů by vynutilo, aby jeden následoval druhý, čímž by se ztratila přesně ta kontrola, kvůli které tato funkce existuje.

PDFlibPas vystavuje tuto dvojici prostřednictvím dvou setterů. SetOptionalContentGroupVisible přebírá ID skupiny a příznak, kde jednička znamená viditelný a nula skrytý, a řídí výchozí stav na obrazovce. SetOptionalContentGroupPrintable přebírá ID skupiny a příznak určující, zda je vrstva emitována při tisku dokumentu. Odpovídající gettery GetOptionalContentGroupVisible a GetOptionalContentGroupPrintable vracejí jedničku nebo nulu, takže můžete číst nastavení obrazovky a tisku vrstvy odděleně, namísto odvozování jednoho z druhého.

Vytvoření dvou vrstev na stránce

Vytvoření vrstvy a její naplnění probíhá v pevném pořadí. Vykreslíte obsah vrstvy na aktuální stránku a poté zavoláte SetContentStreamOptional s ID skupiny, což zabalí aktuální proud obsahu stránky, takže vše dosud nakreslené patří do této skupiny. Protože volání zachycuje cokoli, co je v proudu v daném okamžiku, pravidlem je vykreslit prvky jedné vrstvy, přiřadit je a teprve poté začít další vrstvu. Níže uvedený příklad umisťuje inženýrské sítě na první stránku a recenzentské poznámky na druhou stránku, nastavuje stav obrazovky a tisku každé vrstvy a ukládá soubor.

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;

Vrstvu poznámek stojí za to si všimnout. Zobrazuje se na obrazovce, aby recenzent viděl poznámku, a její tiskový příznak je nula, takže výtisk stejného souboru neobsahuje žádný text recenze. Tato asymetrie je celým důvodem pro oddělení těchto dvou stavů.

Zpětné čtení konfigurace

Čtení vrstev je odlišný průchod stejnou strukturou. Po načtení souboru hlásí GetOptionalContentConfigCount, kolik adresářů konfigurací dokument obsahuje; první výchozí konfigurace má ID 1. V rámci konfigurace udává GetOptionalContentConfigOrderCount počet položek ve stromu řazení, které indexujete od 1. Pro každou položku vrací GetOptionalContentConfigOrderItemLabel její zobrazovaný text a GetOptionalContentConfigOrderItemLevel vrací hloubku jejího zanoření, takže obrys panelu s podvrstvami odsazenými pod nadpisy lze rekonstruovat doslovně.

Každá položka má také typ. GetOptionalContentConfigOrderItemType rozlišuje skutečnou volitelnou skupinu obsahu od prostého textového štítku, který existuje pouze jako nadpis sekce stromu. Tento rozdíl je důležitý, protože dotazy na stav jednotlivých skupin mají smysl pouze u skutečných skupin. Pro položku skupiny GetOptionalContentConfigState uvádí, zda ji konfigurace spouští jako zapnutou, vypnutou nebo ji ponechává beze změny, a GetOptionalContentConfigLocked uvádí, zda je uživateli zakázáno ji přepínat. Níže uvedený cyklus vykresluje strom řazení se stavem každé skupiny a stavem uzamčení, s odsazením podle úrovně.

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;

Správnost tohoto cyklu zajišťují dva detaily. Index řazení začíná od jedničky, od 1 do celkového počtu, což odpovídá tomu, jak knihovna čísluje strom interně. A volání pro jednotlivé skupiny se spouštějí pouze tehdy, když je typ položky skupina, protože textový štítek je nadpis s názvem a úrovní, ale nemá žádný stav zapnuto, vypnuto nebo uzamčeno k dotazování. Pokud tuto ochranu vynecháte, dotazujete se štítku na stav, který nemá.

Kam to zapadá

Vrstvy jsou prezentačním mechanismem, takže engine je musí respektovat při každém způsobu vykreslování stránky; oblast vykreslování je popsána v našem průvodci vykreslováním s více enginy v Delphi. Vrstvy se also prolínají se strukturou dokumentu, protože název vrstvy je text určený uživateli a čtenáři prospívá strukturovaný obrys vrstev, což souvisí s prací v našem článku o tagovaném PDF a struktuře přístupnosti. Obojí se pojí s rozhraními API pro volitelný obsah popsanými zde, která jsou dodávána jako součást Delphi PDF Library spolu s možnostmi stránek, textu, písem a kompatibility popsanými na jiných místech tohoto blogu.