Technical Article

Plasti PDF v Delphi: Izbirne skupine vsebin (OCG)

Geodet odpre načrt lokacije in želi, da so plasti s konturami skrite, medtem ko instalacije ostanejo vidne. Ocenjevalec želi, da so rdeče korekturne pripombe vidne na zaslonu in odstranjene iz natisnjenega dokumenta. Predstavitveni list izdelka se pošlje v treh jezikih iz ene datoteke, bralec pa izbere, kateri jezik bo prikazan. Vsi trije primeri predstavljajo isto funkcijo PDF, plošča, ki jih upravlja v programu Acrobat, pa se imenuje Plasti (Layers). Funkcija pod to ploščo je izbirna vsebina (optional content) in prav to omogoča, da ena sama stran vsebuje več neodvisnih vizualnih slojev, ki jih pregledovalnik vklopi ali izklopi.

Izbirna vsebina je določena v standardu ISO 32000-1 §8.11. Enota vidnosti je izbirna skupina vsebin (optional content group), OCG, slovar vrste /OCG, ki nosi ime. Označena vsebina na strani je povezana s skupino, pregledovalnik pa se odloči, ali je ta skupina trenutno prikazana. Sorodna struktura, slovar članstva izbirne vsebine ali OCMD (optional content membership dictionary), omogoča, da je vidnost odvisna od logične kombinacije več skupin, toda vsakodnevni primer je ena sama poimenovana skupina, ki predstavlja posamezno plast. Dokument povezuje celoten mehanizem prek enega vnosa v katalogu, /OCProperties, ki je opisan v nadaljevanju.

Kaj mora katalog vsebovati

Skupina OCG je sama po sebi inertna. Da bi pregledovalnik lahko izpisal plast in si zapomnil njeno stanje, katalog dokumenta potrebuje slovar /OCProperties, pri čemer §8.11.4 natančno določa, kaj gre vanj. V njem je matrika /OCGs, ki poimenuje vsako skupino v datoteki, in vnos /D, ki vsebuje privzeto konfiguracijo. Privzeta konfiguracija je del, ki ga bralnik uveljavi ob prvem odprtju datoteke. Beleži, katere skupine se začnejo kot vklopljene in katere kot izklopljene, kateri vnosi so zaklenjeni pred uporabniškim preklapljanjem ter, prek matrike /Order, kako so imena plasti razporejena in gnezdena v plošči.

Praktična posledica tega je, da ustvarjanje plasti nikoli ni povsem lokalno dejanje. Skupino je treba izrisati na strani, poleg tega pa jo je treba registrirati v strukturi na ravni kataloga, ki prej ni obstajala. PDFlibPas stori oboje namesto vas. Prvi klic, ki ustvari skupino, doda vnos /OCProperties v katalog in zaseje privzeto konfiguracijo, so plast hkrati izrisana in uvrščena na seznam brez ločenega vodenja evidenc z vaše strani.

Zakaj lahko način skladnosti to funkcijo zavrne

Preden se zažene kakršna koli koda za plasti, cilj skladnosti dokumenta določi, ali je izbirna vsebina sploh dovoljena. PDF/A-1, arhivski profil, opredeljen v ISO 19005-1, v §6.1.13 popolnoma prepoveduje vnos /OCProperties. Utemeljitev ustreza namenu formata. Arhivska datoteka se mora za vsakega bralca v daljni prihodnosti izrisati popolnoma enako, vsebina, katere vidnost lahko pregledovalnik spreminja, pa je vsebina, katere videz ni fiksen, zato profil prepoveduje to strukturo, namesto da bi dovolil dvoumen arhiv. PDF/A-2 in PDF/A-3, opredeljena v ISO 19005-2 in ISO 19005-3, imata v svojih §6.9 nasprotno stališče in dovoljujeta izbirno vsebino s pravili o privzeti vidnosti.

Ta razlika se neposredno pokaže v API. Ko je dokument v načinu PDF/A-1, funkcija NewOptionalContentGroup zavrne ustvarjanje skupine in vrne nič, saj bi izpolnitev zahteve ustvarila datoteko, ki ne bi ustrezala svoji deklarirani skladnosti. V načinu PDF/A-2 ali PDF/A-3 ter v običajnem neomejenem PDF isti klic uspe in vrne neničelni ID skupine. Rezultat nič torej ni splošna napaka, ki bi jo morali preučiti kasneje; knjižnica vam s tem sporoča, da aktivna raven skladnosti ne dopušča te funkcije.

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;

Dve stanji na plast, ne eno

Plast ni preprosto le vidna ali nevidna. Privzeta konfiguracija beleži njeno stanje na zaslonu in ločeno stanje za tiskanje, saj §8.11.4 razlikuje med tem, kaj pregledovalnik prikazuje in kaj oddaja tiskalni cevovod. Obe stanji sta namenoma neodvisni. Vodni znak za osnutek je lahko prikazan na zaslonu in izpuščen na papirju, plast z linijami za izrez pa je lahko skrita na zaslonu, a kljub temu poslana na risalnik. Združitev obeh stanj bi prisilila eno, da sledi drugemu, s čimer bi izgubili natanko tisti nadzor, zaradi katerega funkcija sploh obstaja.

PDFlibPas izpostavlja ta par prek dveh nastavitvenih metod. SetOptionalContentGroupVisible sprejme ID skupine in zastavico (pri čemer ena pomeni vidno in nič skrito) ter upravlja privzeto stanje na zaslonu. SetOptionalContentGroupPrintable sprejme ID skupine in zastavico za to, ali se plast odda pri tiskanju dokumenta. Ustrezni bralni metodi, GetOptionalContentGroupVisible in GetOptionalContentGroupPrintable, vrneta ena ali nič, tako da lahko ločeno preberete zaslonsko in tiskalno stanje plasti, namesto da bi eno sklepali iz drugega.

Izdelava dveh plasti na strani

Ustvarjanje plasti in njeno polnjenje poteka po določenem vrstnem redu. Na trenutno stran narišete vsebino za plast, nato pa pokličete SetContentStreamOptional z ID-jem skupine, kar ovije trenutni tok vsebine strani, tako da vse, kar je bilo izrisano do tistega trenutka, pripada tej skupini. Ker klic zajame vse, kar je v tistem trenutku v toku, je pravilo takšno, da najprej izrišete oznake ene plasti, jih dodelite in šele nato začnete z naslednjo plastjo. Spodnji primer postavi instalacije na prvo stran in ocenjevalčeve rdeče oznake na drugo stran, nastavi zaslonsko in tiskalno stanje vsake plasti ter shrani.

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;

Plast z rdečimi oznakami je primer, ki ga je vredno omeniti. Prikazana je na zaslonu, da ocenjevalec vidi opombo, njena zastavica za tiskanje pa je nič, tako da izpis iste datoteke ne vsebuje besedila ocene. Ta asimetrija je bistvo ohranjanja ločenih dveh stanj.

Branje konfiguracije nazaj

Branje plasti je drugačen sprehod skozi isto strukturo. Po nalaganju datoteke GetOptionalContentConfigCount poroča, koliko konfiguracijskih slovarjev vsebuje dokument; prva privzeta konfiguracija ima ID konfiguracije 1. Znotraj konfiguracije GetOptionalContentConfigOrderCount vrne število vnosov v drevesu vrstnega reda, ki jih indeksirate od 1 naprej. Za vsak vnos GetOptionalContentConfigOrderItemLabel vrne njegovo prikazano besedilo, GetOptionalContentConfigOrderItemLevel pa vrne njegovo globino gnezdenja, tako da je mogoče natančno rekonstruirati strukturo plošče s podplastmi, zamaknjenimi pod naslovi.

Vsak vnos ima tudi svojo vrsto. GetOptionalContentConfigOrderItemType razlikuje med dejansko izbirno skupino vsebin in navadno besedilno oznako, ki obstaja le kot naslov razdelka v drevesu. Ta razlika je pomembna, ker so poizvedbe o stanju posamezne skupine smiselne le za resnične skupine. Za vnos skupine GetOptionalContentConfigState poroča, ali jo konfiguracija začne kot vklopljeno, izklopljeno ali jo pusti nespremenjeno, GetOptionalContentConfigLocked pa poroča, ali je uporabniku preprečeno njeno preklapljanje. Spodnja zanka izriše drevo vrstnega reda s stanjem in zaklenjenim statusom vsake skupine ter zamikom glede na raven.

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;

Dve podrobnosti zagotavljajo pravilnost te zanke. Indeks vrstnega reda je eno-osnovan, od 1 do skupnega števila, kar ustreza notranjemu številčenju drevesa v knjižnici. Poleg tega se klici za posamezne skupine izvedejo le, ko je vrsta elementa skupina, saj je besedilna oznaka naslov z imenom in ravnijo, nima pa stanja vklopljenosti, izklopljenosti ali zaklenjenosti, po katerem bi lahko poizvedovali. Če to zaščito izpustite, boste od oznake zahtevali stanje, ki ga nima.

Kje se to umešča

Plasti so mehanizem predstavitve, zato jih mora pogon upoštevati na vsaki poti, ki izrisuje stran, izrisovalna stran pa je obravnavana v naših navodilih za večpogonsko izrisovanje v Delphi. Prepletajo se tudi s strukturo dokumenta, saj je ime plasti besedilo, namenjeno avtorju, bralcu pa koristi strukturiran oris plasti, kar se povezuje z delom v našem članku o označenem PDF in dostopni strukturi. Oboje se povezuje z vmesniki API za izbirno vsebino, ki so opisani tukaj in so na voljo kot del knjižnice Delphi PDF Library skupaj s funkcijami za strani, besedilo, pisave in skladnost, ki so obravnavane drugje na tem blogu.