Maanmittari avaa asemapiirroksen ja haluaa korkeuskäyrät piiloon, kun taas kunnallistekniikka pysyy näkyvissä. Tarkastaja haluaa punakynämerkinnät näkyviin näytölle, mutta pois tulosteesta. Tuote-esite toimitetaan kolmella kielellä yhdessä tiedostossa, ja lukija valitsee, mikä kieli näytetään. Kaikki kolme ovat samaa PDF-ominaisuutta, ja paneelia, joka ohjaa niitä Acrobatissa, kutsutaan tasoiksi (Layers). Paneelin alla oleva ominaisuus on valinnainen sisältö (optional content), ja sen avulla yksi sivu voi kantaa useita itsenäisiä visuaalisia kerrostumia, joita katseluohjelma kytkee päälle ja pois
Valinnainen sisältö on määritelty ISO 32000-1 §8.11:ssä. Näkyvyyden yksikkö on valinnainen sisältöryhmä (optional content group), OCG, tyyppiä /OCG oleva sanakirja, jolla on nimi. Sivulla oleva merkitty sisältö (marked content) liitetään ryhmään, ja katseluohjelma päättää, näytetäänkö kyseinen ryhmä tällä hetkellä. Siihen liittyvä rakenne, valinnaisen sisällön jäsenyyssanakirja tai OCMD (optional content membership dictionary), antaa näkyvyyden riippua useiden ryhmien loogisesta yhdistelmästä (boolean combination), mutta jokapäiväisessä tapauksessa yksi nimetty ryhmä edustaa yhtä tasoa. Asiakirja sitoo koko mekanismin yhteen yhden luettelomerkinnän, /OCProperties, kautta, jota kuvataan seuraavaksi
Mitä luettelon (catalog) on kannettava
OCG on itsessään inaktiivinen. Jotta katseluohjelma voisi luetella tason ja muistaa sen tilan, asiakirjan luettelo (document catalog) tarvitsee /OCProperties-sanakirjan, ja §8.11.4 esittää tarkasti, mitä siihen menee. Siellä on /OCGs-taulukko, joka nimeää jokaisen ryhmän tiedostossa, ja siellä on /D-merkintä, joka sisältää oletuskokoonpanon. Oletuskokoonpano on se osa, jota lukija soveltaa, kun tiedosto avautuu ensimmäisen kerran. Se tallentaa, mitkä ryhmät alkavat päällä ja mitkä poissa päältä, mitkä merkinnät on lukittu käyttäjää vastaan, ja /Order-taulukon kautta, kuinka tasojen nimet on järjestetty ja sisäkkäin (nested) paneelissa
Käytännön seuraus on, että tason luominen ei ole koskaan puhtaasti paikallinen teko. Ryhmä on piirrettävä sivulle, ja se on myös rekisteröitävä luettelotason (catalog-level) rakenteeseen, jota ei aiemmin ollut. PDFlibPas tekee molemmat puolestasi. Ensimmäinen kutsu, joka tekee ryhmän, lisää /OCProperties-merkinnän luetteloon ja kylvää oletuskokoonpanon, joten taso sekä maalataan (painted) että luetellaan ilman erillistä kirjanpitoa sinun puoleltasi
Miksi vaatimustenmukaisuustila voi evätä ominaisuuden
Ennen kuin mikään tasokoodi suoritetaan, asiakirjan vaatimustenmukaisuustavoite päättää, onko valinnainen sisältö edes laillista. PDF/A-1, ISO 19005-1:ssä määritelty arkistointiprofiili, kieltää /OCProperties-merkinnän suoraan kohdassa §6.1.13. Perustelu sopii formaatin tarkoitukseen. Arkistotiedoston on renderöidyttävä identtisesti jokaiselle lukijalle kauas tulevaisuuteen, ja sisältö, jonka näkyvyyttä katseluohjelma voi muuttaa, on sisältöä, jonka ulkonäkö ei ole kiinteä, joten profiili kieltää rakenteen sen sijaan, että se sallisi epäselvän arkiston. PDF/A-2 ja PDF/A-3, jotka on määritelty standardeissa ISO 19005-2 ja ISO 19005-3, ottavat päinvastaisen näkemyksen kohdassa §6.9 ja sallivat valinnaisen sisällön, oletusnäkyvyyttä koskevien sääntöjen kera
Tämä ero näkyy suoraan API:ssa (ohjelmointirajapinnassa). Kun asiakirja on PDF/A-1 -tilassa, NewOptionalContentGroup kieltäytyy luomasta ryhmää ja palauttaa nollan, koska pyynnön noudattaminen tuottaisi tiedoston, joka ei täytä omaa ilmoitettua vaatimustenmukaisuuttaan. PDF/A-2 tai PDF/A-3 -tilassa ja tavallisessa rajoittamattomassa PDF:ssä sama kutsu onnistuu ja palauttaa nollasta poikkeavan ryhmän ID:n. Nollatulos ei siis ole yleinen virhe, joka tutkittaisiin myöhemmin; kirjasto kertoo sinulle, että aktiivisella vaatimustenmukaisuustasolla ei ole tilaa kyseiselle ominaisuudelle
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;
Kaksi tilaa tasoa kohti, ei yhtä
Taso ei ole vain näkyvä tai näkymätön. Oletuskokoonpano tallentaa sen näytöllä olevan tilan ja erillisen tulostustilan, koska §8.11.4 erottaa sen, mitä katseluohjelma näyttää, siitä, mitä tulostusputki tuottaa (emits). Nämä kaksi ovat itsenäisiä tarkoituksella. Luonnosvesileima (draft watermark) voidaan näyttää näytöllä ja pudottaa paperilta, ja leikkauslinjataso (cut-line layer) voidaan piilottaa näytöllä, mutta lähettää kuitenkin piirturille (plotter). Näiden kahden yhdistäminen pakottaisi toisen seuraamaan toista ja menettäisi juuri sen hallinnan, jonka vuoksi ominaisuus on olemassa
PDFlibPas paljastaa parin kahden asettajan (setter) kautta. SetOptionalContentGroupVisible ottaa ryhmätunnuksen ja lipun (flag), jossa yksi tarkoittaa näkyvää ja nolla tarkoittaa piilotettua, ja hallitsee oletusarvoista näytöllä olevaa tilaa. SetOptionalContentGroupPrintable ottaa ryhmätunnuksen ja lipun siitä, tuotetaanko taso, kun asiakirja tulostuu. Vastaavat hakijat (getters), GetOptionalContentGroupVisible ja GetOptionalContentGroupPrintable, palauttavat kumpikin yhden tai nollan, joten voit lukea tason näyttö- ja tulostusasenteen (disposition) erikseen sen sijaan, että päättelisit toisen toisesta
Kahden tason rakentaminen sivulle
Tason luominen ja sen täyttäminen seuraa kiinteää järjestystä. Piirrät tason sisällön nykyiselle sivulle, sitten kutsut SetContentStreamOptional ryhmätunnuksella (group ID), joka käärii sivun nykyisen sisältövirran (content stream) niin, että kaikki tähän mennessä piirretty kuuluu kyseiseen ryhmään. Koska kutsu kaappaa kaiken, mitä virrassa on sillä hetkellä, kurina on laskea alas yhden tason merkit, määrittää ne, ja vasta sitten aloittaa seuraava taso. Alla oleva esimerkki laittaa kunnallistekniikan (utilities) ensimmäiselle sivulle ja tarkistajan punakynämerkinnän (reviewer redline) toiselle sivulle, asettaa kunkin tason näyttö- ja tulostustilan ja tallentaa
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;
Punakynätaso on huomionarvoinen tapaus. Se näkyy näytöllä, jotta arvioija näkee huomautuksen, ja sen tulostettava-lippu (printable flag) on nolla, joten saman tiedoston tuloste ei sisällä tarkastustekstiä (review text). Se epäsymmetria on koko idea näiden kahden tilan erillään pitämisessä
Kokoonpanon (configuration) lukeminen takaisin
Tasojen lukeminen on erilainen kävely (walk) saman rakenteen läpi. Kun tiedosto on ladattu, GetOptionalContentConfigCount raportoi, kuinka monta konfiguraatiosanakirjaa (configuration dictionaries) asiakirja pitää sisällään; ensimmäinen oletuskokoonpano on kokoonpanon ID 1. Konfiguraation sisällä GetOptionalContentConfigOrderCount antaa järjestyspuun (order tree) merkintöjen lukumäärän, ja indeksoit ne 1:stä alkaen. Jokaiselle merkinnälle GetOptionalContentConfigOrderItemLabel palauttaa sen näyttötekstin ja GetOptionalContentConfigOrderItemLevel palauttaa sen sisäkkäisyyssyvyyden (nesting depth), joten paneelin ääriviivat, joiden alatasot (sub-layers) on sisennetty otsikoiden alle, voidaan rekonstruoida sanatarkasti
Jokaisella merkinnällä on myös tyyppi. GetOptionalContentConfigOrderItemType erottaa varsinaisen valinnaisen sisältöryhmän tavallisesta tekstiotsikosta (text label), joka on olemassa vain puun osan otsikointia varten. Tällä erolla on väliä, koska ryhmäkohtaiset tilakyselyt ovat mielekkäitä vain todellisille ryhmille. Ryhmämerkinnälle GetOptionalContentConfigState raportoi, käynnistääkö kokoonpano sen päälle, pois päältä vai jättääkö se muuttamatta, ja GetOptionalContentConfigLocked raportoi, onko käyttäjää estetty vaihtamasta sitä (toggling). Alla oleva silmukka renderöi järjestyspuun kunkin ryhmän tilan ja lukitustilan kera, sisentäen tason mukaan
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;
Kaksi yksityiskohtaa pitävät tämän silmukan oikeana. Järjestysindeksi alkaa ykkösestä (one-based), arvosta 1 lukumäärään, mikä vastaa sitä, kuinka kirjasto numeroi puun sisäisesti. Ja ryhmäkohtaiset kutsut suoritetaan vain, kun kohteen tyyppi on ryhmä, koska tekstiotsikko (text label) on otsikko, jolla on nimi ja taso, mutta ei päälle-, pois päältä- tai lukittua tilaa (on, off, or locked state) kyseltäväksi. Ohita tämä varotoimi (guard), ja kysyt otsikolta tilaa, jota sillä ei ole
Mihin tämä sopii
Tasot ovat esitysmekanismi (presentation mechanism), joten moottorin (engine) on kunnioitettava niitä jokaisella polulla, joka renderöi sivun, ja renderöintipuolta käsitellään artikkelissamme pdflibpas-multi-engine-rendering-delphi.html, läpikäynti monimoottorisesta (multi-engine) renderöinnistä Delphissä. Ne leikkaavat myös dokumentin rakenteen (document structure) kanssa, koska tason nimi on tekijälle suunnattu (author-facing) teksti, ja lukija hyötyy strukturoidusta tason ääriviivasta, joka liittyy työhön artikkelissamme pdflibpas-tagged-pdf-accessibility-structure.html, tagattu PDF ja saavutettavuusrakenne (tagged PDF and accessibility structure). Molemmat yhdistyvät tässä kuvattujen valinnaisten sisältörajapintojen (optional content APIs) kanssa, jotka toimitetaan osana Delphi PDF Library -kirjastoa muiden tässä blogissa käsiteltyjen sivu-, teksti-, fontti- ja vaatimustenmukaisuusominaisuuksien ohella