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 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. PDFlibPas 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
// 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;
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 PDFlibPas 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.
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);
// 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;
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; // 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;
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.
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.