Műszaki cikk

PDF rétegek Delphi-ben: Opcionális tartalomcsoportok (OCG)

Egy földmérő megnyit egy helyszínrajzot, és szeretné, ha a szintvonalak rejtve maradnának, miközben a közművek látszódnak. Egy ellenőr azt szeretné, ha a piros jelölések (redline annotations) láthatóak lennének a képernyőn, de eltűnnének a nyomtatásból. Egy termékismertető három nyelven érkezik egyetlen fájlban, és az olvasó választja ki, melyik nyelv jelenjen meg. Mindhárom ugyanaz a PDF funkció, és a panel, amely az Acrobatban vezérli őket, a Layers (Rétegek). A panel mögött álló funkció az opcionális tartalom (optional content), és ez teszi lehetővé, hogy egyetlen oldal több független vizuális réteget hordozzon, amelyeket a megjelenítő be- és kikapcsolhat

Opcionális tartalom az ISO 32000-1 8.11. szakaszában van meghatározva. A láthatóság egysége az opcionális tartalomcsoport (optional content group), azaz OCG, amely egy /OCG típusú, névvel ellátott szótár. Az oldalon lévő megjelölt tartalom (marked content) egy csoporthoz van társítva, és a megjelenítő dönti el, hogy az a csoport éppen látható-e. Egy kapcsolódó konstrukció, az opcionális tartalomtagsági szótár (optional content membership dictionary) vagy OCMD lehetővé teszi, hogy a láthatóság több csoport logikai kombinációjától függjön, de a mindennapi eset egyetlen nevesített csoport, amely egyetlen réteget képvisel. A dokumentum a teljes mechanizmust egyetlen katalógusbejegyzésen, az /OCProperties szótáron keresztül köti össze, amelyet a következőkben ismertetünk

Amit a katalógusnak tartalmaznia kell

Az OCG önmagában inaktív. Ahhoz, hogy a megjelenítő listázza a réteget és emlékezzen az állapotára, a dokumentumkatalógusnak szüksége van egy /OCProperties szótárra, és a 8.11.4. szakasz pontosan részletezi, mi kerül ebbe. Tartalmaz egy /OCGs tömböt, amely megnevezi a fájlban lévő összes csoportot, és egy /D bejegyzést, amely az alapértelmezett konfigurációt hordozza. Az alapértelmezett konfiguráció az a rész, amelyet az olvasó alkalmaz a fájl első megnyitásakor. Rögzíti, hogy mely csoportok indulnak bekapcsolva és melyek kikapcsolva, mely bejegyzések vannak zárolva a felhasználói kapcsolás ellen, és egy /Order tömbön keresztül azt, hogyan vannak elrendezve és egymásba ágyazva a rétegek nevei a panelen

A PDF opcionális tartalomcsoportok szerkezete a PDF Library for Delphi mögött: a katalógus OCProperties bejegyzése felsorolja az összes OCG réteget és alapértelmezett konfigurációját kezdőállapotokkal, zárolásokkal és panelsorrenddel, miközben az oldal jelölt tartalma a rajzokat csoportokhoz köti
A rétegek csak a katalóguson keresztül címezhetők meg: az OCProperties felsorolja az összes OCG-t, az alapértelmezett konfiguráció pedig rögzíti a kezdőállapotokat, a zárolásokat és a panel sorrendjét

A gyakorlati következmény az, hogy egy réteg létrehozása soha nem tisztán helyi művelet. A csoportot rá kell rajzolni az oldalra, és regisztrálni kell egy katalógusszintű struktúrában is, amely korábban nem létezett. PDF Library for Delphi mindkettőt elvégzi Ön helyett. Az első hívás, amely létrehoz egy csoportot, hozzáadja az /OCProperties bejegyzést a katalógushoz, és elhelyezi az alapértelmezett konfigurációt, így a réteg mind kirajzolásra, mind listázásra kerül az Ön részéről történő külön könyvelés nélkül

Miért tilthatja meg a megfelelőségi mód ezt a funkciót

Mielőtt bármilyen rétegkezelő kód lefutna, a dokumentum megfelelőségi célja eldönti, hogy az opcionális tartalom egyáltalán megengedett-e. A PDF/A-1, az ISO 19005-1 szabványban meghatározott archív profil, a 6.1.13. szakaszban teljesen megtiltja az /OCProperties bejegyzést. Az érvelés illeszkedik a formátum céljához. Egy archív fájlnak a távoli jövőben is azonos módon kell megjelennie minden olvasó számára, és az a tartalom, amelynek láthatóságát a megjelenítő megváltoztathatja, nem fix megjelenésű, így a profil tiltja a konstrukciót ahelyett, hogy megengedne egy kétértelmű archívumot. A PDF/A-2 és PDF/A-3 (ISO 19005-2 és ISO 19005-3) ellentétes álláspontot képvisel a 6.9. szakaszban, és engedélyezi az opcionális tartalmat, a default láthatóságra vonatkozó szabályokkal

Ez a különbség közvetlenül megmutatkozik az API-ban. Ha a dokumentum PDF/A-1 módban van, a NewOptionalContentGroup megtagadja a csoport létrehozását és nullát ad vissza, mert a kérés teljesítése olyan fájlt eredményezne, amely megbukik a saját maga által deklarált megfelelőségi szinten. PDF/A-2 vagy PDF/A-3 módban, valamint a hagyományos, korlátozás nélküli PDF esetén ugyanaz a hívás sikeres, és nem nulla csoportazonosítót ad vissza. A nulla eredmény tehát nem egy általános hiba, amelyet később kellene vizsgálni; a könyvtár ezzel jelzi, hogy az aktív megfelelőségi szinten nincs hely ennek a funkciónak

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
      // PDF/A-1 alatt elutasítva; nem átmeneti hiba, ez a mód tiltja a rétegeket
      ShowMessage('Optional content is not available in PDF/A-1 mode.');
  finally
    Pdf.Free;
  end;
end;

Rétegenként két állapot, nem egy

Egy réteg nem egyszerűen látható vagy láthatatlan. Az alapértelmezett konfiguráció rögzíti a képernyőn megjelenő állapotát és egy különálló nyomtatási állapotot, mivel a 8.11.4. szakasz megkülönbözteti azt, amit a megjelenítő mutat, attól, amit a nyomtatási folyamat kiad. A kettő szándékosan független egymástól. Egy vázlat vízjel látható a képernyőn és elhagyható a papírról, a vágóvonal réteg pedig elrejthető a képernyőn, mégis elküldhető egy plotternek. A kettő összevonása arra kényszerítené az egyiket, hogy kövesse a másikat, és pont azt az ellenőrzést veszítené el, amelynek biztosítására a funkció létezik

A PDF Library for Delphi a párost két beállító (setter) függvényen keresztül teszi elérhetővé. A SetOptionalContentGroupVisible átveszi a csoportazonosítót és egy jelzőt (ahol az egy láthatót, a nulla rejtettet jelent), és szabályozza a képernyő alapértelmezett állapotát. A SetOptionalContentGroupPrintable átveszi a csoportazonosítót és egy jelzőt arra vonatkozóan, hogy a réteg kiadásra kerül-e a dokumentum nyomtatásakor. A hozzájuk tartozó lekérdező (getter) függvények, a GetOptionalContentGroupVisible and a GetOptionalContentGroupPrintable egyaránt egyet vagy nullát adnak vissza, így külön-külön olvashatja vissza egy réteg képernyő- és nyomtatási beállításait ahelyett, hogy az egyikből következtetne a másikra

Független képernyős és nyomtatási láthatósági állapotok mátrixa a Delphi PDF Library-vel készített PDF rétegekhez: a segédrétegek mindenhol láthatók maradnak, a lektori jelölések képernyőn jelennek meg, de sosem nyomtatódnak, a plotter vágóvonal pedig a képernyőn rejtett, mégis kinyomtatódik
A képernyős láthatóság és a nyomtatott kibocsátás csoportonként állítható, így a segédrétegek mindenhol felbukkanhatnak, miközben a lektori jelölés képernyőre korlátozott jegyzet marad

Két réteg felépítése egy oldalon

Egy réteg létrehozása és feltöltése meghatározott sorrendet követ. Rárajzolja a réteg tartalmát az aktuális oldalra, majd meghívja a SetContentStreamOptional függvényt a csoportazonosítóval, ami becsomagolja az oldal aktuális tartalomfolyamát (content stream), így minden addig rajzolt elem ahhoz a csoporthoz fog tartozni. Mivel a hívás rögzíti mindazt, ami az adott pillanatban a folyamon van, a szabály az, hogy elhelyezzük az egyik réteg jelöléseit, hozzárendeljük őket, és csak ezután indítjuk el a következő réteget. Az alábbi példa a közműveket az első oldalra helyezi, az ellenőri piros jelölést pedig egy második oldalra, beállítja az egyes rétegek képernyő- és nyomtatási állapotát, majd ment

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

    // 1. réteg: segédobjektumok, megrajzolva, majd saját csoportjukhoz rendelve
    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);   // képernyőn megjelenítve
    Pdf.SetOptionalContentGroupPrintable(UtilLayer, 1); // és papíron

    // 2. réteg: felülvizsgálói javítás egy új oldalon
    Pdf.InsertPages(2, 1);                     // egy oldal hozzáfűzése az 1. oldal után
    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);    // felülvizsgálat közben látható
    Pdf.SetOptionalContentGroupPrintable(RedlineLayer, 0);  // never printed

    Pdf.SaveToFile('SitePlan_Layers.pdf');
  finally
    Pdf.Free;
  end;
end;

A piros jelölő (redline) réteg az az eset, amelyet érdemes megjegyezni. Megjelenik a képernyőn, hogy a felülvizsgáló lássa a megjegyzést, és a nyomtatható (printable) jelzője nulla, így ugyanannak a fájlnak a nyomtatása nem tartalmazza a felülvizsgálati szöveget. Ez az aszimmetria a lényege a két állapot elkülönítésének

A konfiguráció visszaolvasása

A rétegek olvasása egy másik bejárást jelent ugyanazon a struktúrán keresztül. A fájl betöltése után a GetOptionalContentConfigCount jelzi, hogy a dokumentum hány konfigurációs szótárat tartalmaz; az első alapértelmezett konfiguráció az 1-es konfigurációs azonosító. Egy konfiguráción belül a GetOptionalContentConfigOrderCount megadja a sorrendfa (order tree) bejegyzéseinek számát, és ezeket 1-től indexeljük. Minden egyes bejegyzésnél a GetOptionalContentConfigOrderItemLabel visszaadja a megjelenített szöveget, a GetOptionalContentConfigOrderItemLevel pedig a beágyazási mélységet, így egy fejlécek alá behúzott alrétegeket tartalmazó panel vázlata szó szerint rekonstruálható

Minden bejegyzésnek van típusa is. A GetOptionalContentConfigOrderItemType megkülönbözteti a tényleges opcionális tartalomcsoportot a sima szöveges címkétől (plain text label), amely csak a fa egy szakaszának megjelölésére szolgál. Ez a megkülönböztetés azért fontos, mert a csoportonkénti állapotlekérdezéseknek csak valós csoportok esetében van értelmük. Csoportbejegyzés esetén a GetOptionalContentConfigState jelzi, hogy a konfiguráció bekapcsolva, kikapcsolva indítja-e, vagy változatlanul hagyja, a GetOptionalContentConfigLocked pedig azt mutatja, hogy a felhasználó el van-e tiltva a kapcsolásától. Az alábbi ciklus kirajzolja a sorrendfát az egyes csoportok állapotával és zárolási státuszával, a szintek szerint behúzva

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;                                  // az alapértelmezett konfiguráció
    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 = opcionális tartalomcsoport
      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;
      // Az ItemType = 2 szöveges címkefejléc; nincs csoportonkénti állapota

      Writeln(Line);
    end;
  finally
    Pdf.Free;
  end;
end;

Két részlet tartja helyesen ezt a ciklust. A sorrendindex egyalapú (one-based), 1-től a darabszámig terjed, igazodva ahhoz, ahogyan a könyvtár belsőleg számozza a fát. A csoportonkénti hívások pedig csak akkor futnak le, ha az elemtípus csoport, mivel a szöveges címke egy névvel és szinttel rendelkező fejléc, de nincs lekérdezhető be-, ki- vagy zárolt állapota. Ha elhagyja ezt a védelmet, olyan állapotot kér le egy címkétől, amellyel az nem rendelkezik

Visszaolvasási hurok az opcionális tartalomkonfigurációkhoz a PDF Library for Delphi-ben: konfigurációk felsorolása, order bejegyzések bejárása egyalapúan, elágazás ItemType szerint, így csak a valódi csoportok kérdezhetők le be-ki-zárolt állapotra, a szövegcímkék kihagyva
A rétegek visszaolvasása egyalapúan járja a sorrendbejegyzéseket, és csak valódi csoportoknál kérdezi le az állapotot, mert egy felirat fejlécnek nincs be-, kikapcsolt vagy zárolt állapota, amit jelenteni lehetne

Ahol ez elhelyezkedik

A rétegek megjelenítési mechanizmusok, így a motornak minden olyan útvonalon tiszteletben kell tartania őket, amely leképez egy oldalt; a leképezési oldalt a Delphi-ben történő többmotoros leképezésről szóló útmutatónk tárgyalja. A dokumentum struktúrájával is érintkeznek, mivel a réteg neve a szerző felé megjelenő szöveg, az olvasó pedig profitál a strukturált rétegvázlatból, ami kapcsolódik a címkézett PDF-ről és akadálymentesítési struktúráról szóló cikkünkben bemutatott munkához. Mindkettő párosul az itt leírt opcionális tartalom API-kkal, amelyek a Delphi PDF Library részeként érhetők el, a blogunkon máshol tárgyalt oldal-, szöveg-, betűtípus- és megfelelőségi funkciók mellett