Technical Article

PDF vrstvy v Delphi: Voliteľné skupiny obsahu (OCG)

Geodet otvorí plán staveniska a chce, aby boli vrstevnice skryté, zatiaľ čo inžinierske siete zostanú zobrazené. Recenzent chce, aby boli červené poznámky viditeľné na obrazovke a zmizli z výtlačku. Produktový list sa dodáva v troch jazykoch z jedného súboru a čitateľ si vyberie, ktorý jazyk sa zobrazí. Všetky tri prípady sú rovnakou funkciou PDF a panel, ktorý ich ovláda v programe Acrobat, sa nazýva Vrstvy. Funkciou pod týmto panelom je voliteľný obsah, a to je to, čo umožňuje jednej stránke niesť niekoľko nezávislých vizuálnych vrstiev, ktoré prehliadač zapína a vypína.

Voliteľný obsah je špecifikovaný v ISO 32000-1 §8.11. Jednotkou viditeľnosti je voliteľná skupina obsahu, OCG, slovník typu /OCG, ktorý nesie názov. Označený obsah na stránke je priradený k skupine a prehliadač rozhoduje, či je táto skupina momentálne zobrazená. Súvisiaci konštrukt, slovník členstva vo voliteľnom obsahu alebo OCMD, umožňuje, aby viditeľnosť závisela od boolovskej kombinácie niekoľkých skupín, ale každodenným prípadom je jedna pomenovaná skupina predstavujúca jednu vrstvu. Dokument spája celý mechanizmus prostredníctvom jednej položky katalógu, /OCProperties, opísanej nižšie.

Čo musí katalóg obsahovať

Skupina OCG je sama o sebe neaktívna. Aby prehliadač mohol uviesť vrstvu a zapamätať si jej stav, katalóg dokumentu potrebuje slovník /OCProperties a §8.11.4 presne uvádza, čo doň patrí. Obsahuje pole /OCGs pomenúvajúce každú skupinu v súbore a položku /D s predvolenou konfiguráciou. Predvolená konfigurácia je časť, ktorú čítačka aplikuje pri prvom otvorení súboru. Zaznamenáva, ktoré skupiny začínajú ako zapnuté a ktoré ako vypnuté, ktoré položky sú uzamknuté proti prepínaniu používateľom a prostredníctvom poľa /Order určuje, ako sú názvy vrstiev usporiadané a vnorené v paneli.

Praktickým dôsledkom je, že vytvorenie vrstvy nikdy nie je čisto lokálnym aktom. Skupina sa musí nakresliť na stránku a musí byť tiež zaregistrovaná v štruktúre na úrovni katalógu, ktorá predtým neexistovala. PDFlibPas robí oboje za vás. Prvé volanie, ktoré vytvorí skupinu, pridá položku /OCProperties do katalógu a nasadí predvolenú konfiguráciu, takže vrstva je vykreslená aj uvedená v zozname bez samostatného účtovníctva na vašej strane.

Prečo môže režim zhody túto funkciu odoprieť

Predtým, ako sa spustí akýkoľvek kód vrstvy, cieľová zhoda dokumentu rozhodne, či je voliteľný obsah vôbec legálny. PDF/A-1, archívny profil definovaný v ISO 19005-1, úplne zakazuje položku /OCProperties v §6.1.13. Odôvodnenie zodpovedá účelu formátu. Archívny súbor sa musí zobrazovať identicky pre každého čitateľa ďaleko do budúcnosti a obsah, ktorého viditeľnosť môže prehliadač meniť, je obsahom, ktorého vzhľad nie je fixný, takže profil zakazuje tento konštrukt, namiesto toho, aby povolil nejednoznačný archív. Normy PDF/A-2 a PDF/A-3, definované v ISO 19005-2 a ISO 19005-3, majú opačný názor v §6.9 a povoľujú voliteľný obsah s pravidlami o predvolenej viditeľnosti.

Tento rozdiel sa prejavuje priamo v API. Keď je dokument v režime PDF/A-1, NewOptionalContentGroup odmietne vytvoriť skupinu a vráti nulu, pretože vyhovenie požiadavke by viedlo k súboru, ktorý nespĺňa svoju vlastnú deklarovanú zhodu. V režime PDF/A-2 alebo PDF/A-3 a v bežnom neobmedzenom PDF rovnaké volanie uspeje a vráti nenulové ID skupiny. Nulový výsledok preto nie je všeobecným zlyhaním, ktoré by sa malo skúmať neskôr; je to knižnica, ktorá vám oznamuje, že aktívna úroveň zhody nemá pre túto funkciu miesto.

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, nie jeden

Vrstva nie je jednoducho viditeľná alebo neviditeľná. Predvolená konfigurácia zaznamenáva jej stav na obrazovke a samostatný stav tlače, pretože §8.11.4 rozlišuje to, čo prehliadač zobrazuje, od toho, čo vysiela tlačový kanál. Tieto dva stavy sú zámerne nezávislé. Vodoznak konceptu môže byť zobrazený na obrazovke a vynechaný z papiera a vrstva orezových čiar môže byť skrytá na obrazovke, no odoslaná na plotter. Zlúčenie týchto dvoch stavov by prinútilo jeden sledovať druhý a stratila by sa presne tá kontrola, pre ktorú táto funkcia existuje.

PDFlibPas odkrýva túto dvojicu prostredníctvom dvoch nastavovacích metód. SetOptionalContentGroupVisible berie ID skupiny a príznak, kde jedna znamená viditeľný a nula skrytý, a riadi predvolený stav na obrazovke. SetOptionalContentGroupPrintable berie ID skupiny a príznak pre to, či sa vrstva odošle pri tlači dokumentu. Zodpovedajúce čítacie metódy GetOptionalContentGroupVisible a GetOptionalContentGroupPrintable vracajú jednu alebo nulu, takže môžete prečítať nastavenie obrazovky a tlače vrstvy oddelene, namiesto odvodzovania jedného od druhého.

Vytvorenie dvoch vrstiev na stránke

Vytváranie vrstvy a jej napĺňanie sa riadi pevným poradím. Nakreslíte obsah pre vrstvu na aktuálnu stránku, potom zavoláte SetContentStreamOptional s ID skupiny, čo obalí aktuálny tok obsahu stránky, takže všetko doteraz nakreslené patrí do tejto skupiny. Keďže volanie zachytáva to, čo je v toku v danom momente, pravidlom je umiestniť značky jednej vrstvy, priradiť ich a až potom začať ďalšiu vrstvu. Nasledujúci príklad umiestňuje inžinierske siete na prvú stránku a červené poznámky recenzenta na druhú stránku, nastavuje stav obrazovky a tlače každej vrstvy a ukladá súbor.

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 červených poznámok (redline) stojí za to si všimnúť. Zobrazuje sa na obrazovke, aby recenzent videl poznámku, a jej príznak tlače (printable) je nula, takže výtlačok toho istého súboru neobsahuje žiadny text recenzie. Táto asymetria je hlavným dôvodom oddelenia oboch stavov.

Spätné čítanie konfigurácie

Čítanie vrstiev je inou prechádzkou tou istou štruktúrou. Po načítaní súboru GetOptionalContentConfigCount nahlási, koľko slovníkov konfigurácie dokument obsahuje; prvá predvolená konfigurácia má ID 1. V rámci konfigurácie GetOptionalContentConfigOrderCount poskytuje počet položiek v strome usporiadania a indexujete ich od 1. Pre každú položku GetOptionalContentConfigOrderItemLabel vráti jej zobrazovaný text a GetOptionalContentConfigOrderItemLevel vráti hĺbku jej vnorenia, takže osnovu panelu s podvrstvami odsadenými pod nadpismi je možné rekonštruovať doslova.

Každá položka má tiež svoj typ. GetOptionalContentConfigOrderItemType rozlišuje skutočnú voliteľnú skupinu obsahu od obyčajného textového štítku, ktorý existuje len ako nadpis sekcie stromu. Na tomto rozdiele záleží, pretože dotazy na stav jednotlivých skupín majú zmysel len pre skutočné skupiny. Pre položku skupiny GetOptionalContentConfigState uvádza, či ju konfigurácia spúšťa ako zapnutú, vypnutú alebo ju ponecháva nezmenenú, a GetOptionalContentConfigLocked uvádza, či má používateľ zakázané ju prepínať. Nižšie uvedený cyklus vykresľuje strom usporiadania so stavom každej skupiny a stavom uzamknutia, pričom odsadzuje podľa úrovne.

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;

Tento cyklus udržiavajú správny dva detaily. Index usporiadania začína od jednej, od 1 po celkový počet, čo zodpovedá tomu, ako knižnica vnútorne čísluje strom. A volania pre konkrétne skupiny bežia len vtedy, keď je typ položky skupina, pretože textový štítok je nadpis s názvom a úrovňou, ale nemá žiadny stav zapnutia, vypnutia alebo uzamknutia, na ktorý by sa dalo pýtať. Vynechajte túto ochranu a opýtate sa štítku na stav, ktorý nemá.

Kam to zapadá

Vrstvy sú prezentačným mechanizmom, takže engine ich musí rešpektovať na každej ceste, ktorá vykresľuje stránku, a téma vykresľovania je popísaná v našom sprievodcovi vykresľovaním pomocou viacerých enginov v Delphi. Prepájajú sa aj so štruktúrou dokumentu, pretože názov vrstvy je text určený pre autora a čitateľ ťaží zo štruktúrovanej osnovy vrstiev, čo nadväzuje na prácu v našom článku o tagovanom PDF a štruktúre prístupnosti. Obe sa dopĺňajú s rozhraniami API pre voliteľný obsah, ktoré sa dodávajú ako súčasť Delphi PDF Library spolu s funkciami pre stránky, text, písma a zhodu, o ktorých sa píše inde na tomto blogu.