Technical Article

PDF-tasot Delphissä: valinnaiset sisältöryhmät (OCG)

Mittaaja avaa tonttipiirroksen ja haluaa korkeuskäyrät piiloon, kun taas vesi- ja sähköjohdot jäävät näkyviin. Tarkastaja haluaa korjausmerkinnät näkyviin näytölle mutta pois tulosteesta. Tuoteseloste toimitetaan kolmella kielellä yhdessä tiedostossa, ja lukija valitsee, mikä kieli näytetään. Kaikki kolme ovat saman PDF-ominaisuuden sovelluksia, ja Acrobatissa niitä ohjaavaa paneelia kutsutaan nimellä Tasot (Layers). Tämän paneelin taustalla oleva ominaisuus on valinnainen sisältö (optional content), ja se mahdollistaa sen, että yksittäinen sivu voi sisältää useita itsenäisiä visuaalisia tasoja, joita katseluohjelma kytkee päälle ja pois päältä.

Valinnainen sisältö määritellään ISO 32000-1 -standardin kohdassa §8.11. Näkyvyyden yksikkö on valinnainen sisältöryhmä eli OCG (optional content group), joka on tyyppiä /OCG oleva nimen kantava hakemisto. Sivulla oleva merkitty sisältö (marked content) liitetään ryhmään, ja katseluohjelma päättää, näytetäänkö kyseinen ryhmä parhaillaan. Tähän liittyvä rakenne, valinnaisen sisällön jäsenyyssanakirja eli OCMD (optional content membership dictionary), sallii näkyvyyden riippuvan useiden ryhmien loogisesta yhdistelmästä, mutta jokapäiväinen tapaus on yksittäinen nimetty ryhmä, joka vastaa yhtä tasoa. Dokumentti sitoo koko mekanismin yhteen yhden katalogimerkinnän, /OCProperties, kautta, joka kuvataan seuraavaksi.

Mitä katalogin on sisällettävä

OCG itsessään on toimimaton. Jotta katseluohjelma voisi luetella tason ja muistaa sen tilan, dokumenttiluettelo (catalog) tarvitsee /OCProperties-sanakirjan, ja kohta §8.11.4 määrittelee tarkasti, mitä siihen kuuluu. Siellä on /OCGs-taulukko, joka nimeää tiedoston jokaisen ryhmän, ja /D-merkintä, joka sisältää oletuskonfiguraation. Oletuskonfiguraatio on se osa, jota lukija soveltaa, kun tiedosto avataan ensimmäisen kerran. Se tallentaa, mitkä ryhmät alkavat päällä ja mitkä pois päältä, mitkä merkinnät on lukittu käyttäjän muokkauksilta ja miten tasojen nimet on järjestetty ja sisäkkäin paneelissa /Order-taulukon kautta.

Käytännön seuraus on se, että tason luominen ei ole koskaan puhtaasti paikallinen teko. Ryhmä on piirrettävä sivulle, ja se on myös rekisteröitävä katalogitason rakenteeseen, jota ei aiemmin ollut olemassa. PDFlibPas tekee molemmat puolestasi. Ensimmäinen kutsu, joka luo ryhmän, lisää /OCProperties-merkinnän katalogiin ja alustaa oletuskonfiguraation, joten taso sekä piirretään että luetellaan ilman erillistä kirjanpitoa puoleltasi.

Miksi yhteensopivuustila voi evätä ominaisuuden

Ennen kuin mitään tasokoodia suoritetaan, dokumentin vaatimustenmukaisuuskohde ratkaisee, onko valinnainen sisältö edes laillista. PDF/A-1, ISO 19005-1 -standardissa määritelty arkistointiprofiili, kieltää /OCProperties-merkinnän kokonaan kohdassa §6.1.13. Perustelu sopii formaatin tarkoitukseen. Arkistotiedoston on toistuttava täysin samalla tavalla jokaiselle lukijalle kaukana tulevaisuudessa, ja sisältö, jonka näkyvyyttä katseluohjelma voi muuttaa, on sisältöä, jonka ulkoasu ei ole kiinteä, joten profiili kieltää rakenteen sen sijaan, että 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 kannan kohdassa §6.9 ja sallivat valinnaisen sisällön oletusnäkyvyyttä koskevin säännöin.

Tämä ero näkyy suoraan API-liittymässä. Kun dokumentti on PDF/A-1-tilassa, NewOptionalContentGroup kieltäytyy luomasta ryhmää ja palauttaa nollan, koska pyynnön kunnioittaminen tuottaisi tiedoston, joka epäonnistuu omassa ilmoitetussa vaatimustenmukaisuudessaan. PDF/A-2- tai PDF/A-3-tilassa sekä tavallisessa rajoittamattomassa PDF-tiedostossa sama kutsu onnistuu ja palauttaa nollasta poikkeavan ryhmätunnuksen (ID). Nollatulos ei siis ole yleinen virhe, vaan kirjasto kertoo sinulle, ettei aktiivisella yhteensopivuustasolla 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. Oletuskonfiguraatio tallentaa sen näytöllä näkyvän tilan ja erillisen tulostustilan, koska §8.11.4 erottaa sen, mitä katseluohjelma näyttää näytöllä siitä, mitä tulostusputki tuottaa. Nämä kaksi ovat itsenäisiä tarkoituksella. Luonnosvesileima voidaan näyttää näytöllä ja jättää pois paperilta, ja leikkausviivataso voidaan piilottaa näytöllä mutta lähettää plotterille. Nämä kaksi yhdistämällä menettäisi juuri sen hallinnan, jonka vuoksi ominaisuus on olemassa.

PDFlibPas paljastaa parin kahden asetusfunktion kautta. SetOptionalContentGroupVisible ottaa ryhmätunnuksen ja lipun, jossa yksi tarkoittaa näkyvää ja nolla piilotettua, ja hallitsee oletusarvoista näytön tilaa. SetOptionalContentGroupPrintable ottaa ryhmätunnuksen ja lipun sille, tulostuuko taso, kun dokumentti tulostetaan. Vastaavat hakuperheet, GetOptionalContentGroupVisible and GetOptionalContentGroupPrintable, palauttavat kukin joko ykkösen tai nollan, joten voit lukea tason näyttö- ja tulostustilan erikseen sen sijaan, että päättelisit toisen toisesta.

Kahden tason rakentaminen sivulle

Tason luominen ja täyttäminen noudattaa kiinteää järjestystystä. Piirrät tason sisällön nykyiselle sivulle ja kutsut sitten SetContentStreamOptional-funktiota ryhmätunnuksella, mikä käärii sivun nykyisen sisältövirran niin, että kaikki tähän mennessä piirretty kuuluu kyseiseen ryhmään. Koska kutsu kaappaa kaiken, mitä virrassa on sillä hetkellä, kurinalaisuus vaatii piirtämään yhden tason merkit, määrittämään ne ja vasta sitten aloittamaan seuraavan tason. Alla oleva esimerkki asettaa johtoverkot ensimmäiselle sivulle ja tarkastajan korjausmerkinnät toiselle sivulle, asettaa kunkin tason näyttö- ja tulostustilan ja tai 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;

Konfiguraation lukeminen takaisin

Tasojen lukeminen on erilainen polku saman rakenteen läpi. Kun tiedosto on ladattu, GetOptionalContentConfigCount ilmoittaa, kuinka monta konfiguraatiosanakirjaa dokumentti sisältää; ensimmäinen oletuskonfiguraatio on konfiguraatio-ID 1. Konfiguraation sisällä GetOptionalContentConfigOrderCount antaa järjestyspuun alkioiden määrän, ja indeksoit ne arvosta 1 alkaen. Kullekin alkiolle GetOptionalContentConfigOrderItemLabel palauttaa sen näyttötekstin ja GetOptionalContentConfigOrderItemLevel palauttaa sen sisäkkäisyyssyvyyden, joten paneelin jäsennys, jossa alitasot on sisennetty otsikoiden alle, voidaan rekonstruoida täsmällisesti.

Jokaisella alkiolla on myös tyyppi. GetOptionalContentConfigOrderItemType erottaa todellisen valinnaisen sisältöryhmän tavallisesta tekstiotsikosta, joka on olemassa vain puun osion otsikkona. Tämä erottelu on tärkeä, koska ryhmäkohtaiset tilakyselyt ovat järkeviä vain todellisille ryhmille. Ryhmäalkiolle GetOptionalContentConfigState ilmoittaa, käynnistääkö konfiguraatio sen päällä, pois päältä vai jättääkö se muuttumattomaksi, ja GetOptionalContentConfigLocked ilmoittaa, onko käyttäjältä evätty sen kytkeminen. Alla oleva silmukka renderöi järjestyspuun kunkin ryhmän tilan ja lukitusstatuksen kanssa, sisentaen 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;

Mihin tämä sopii

Tasot ovat esitysmuoto, joten moottorin on kunnioitettava niitä jokaisella polulla, joka renderöi sivun, ja renderöintipuoli käsitellään ohjeessamme Delphi-monimoottorirenderöinnistä. Ne risteävät myös dokumentin rakenteen kanssa, koska tason nimi on tekijälle näkyvää tekstiä ja lukija hyötyy jäsennellystä tasorakenteesta, mikä liittyy työhön artikkelissamme merkityistä PDF-tiedostoista ja saavutettavuusrakenteesta. Molemmat pariutuvat tässä kuvattujen valinnaisen sisällön API-liittymien kanssa, jotka toimitetaan osana Delphin PDF-kirjastoa sivu-, teksti-, fontti- ja vaatimuksenmukaisuusominaisuuksien rinnalla, joita käsitellään muualla tässä blogissa.