Technical Article

PDF slojevi u Delphiu: Grupe neobaveznog sadržaja (OCG)

Geodet otvara situacijski nacrt i želi da konture budu skrivene dok komunalne instalacije ostaju prikazane. Recenzent želi da crvene bilješke (redline) budu vidljive na zaslonu, a uklonjene s ispisa. Tehnički list proizvoda isporučuje se na tri jezika iz jedne datoteke, a čitatelj bira koji će jezik biti prikazan. Sva tri slučaja su ista PDF značajka, a ploča koja njima upravlja u Acrobat-u zove se Slojevi (Layers). Značajka ispod te ploče je neobavezni sadržaj (optional content), i to je ono što omogućuje da jedna stranica nosi nekoliko neovisnih vizualnih slojeva koje preglednik uključuje i isključuje.

Neobavezni sadržaj specificiran je u ISO 32000-1 §8.11. Jedinica vidljivosti je grupa neobaveznog sadržaja, OCG, rječnik tipa /OCG koji nosi naziv. Označeni sadržaj na stranici povezan je s grupom, a preglednik odlučuje hoće li ta grupa trenutno biti prikazana. Srodni koncept, rječnik članstva neobaveznog sadržaja ili OCMD, omogućuje da vidljivost ovisi o booleovoj kombinaciji nekoliko grupa, no svakodnevni slučaj je jedna imenovana grupa koja predstavlja jedan sloj. Dokument povezuje cijeli mehanizam putem jednog unosa u katalogu, /OCProperties, opisanog u nastavku.

Što katalog mora sadržavati

OCG sam po sebi je inertan. Da bi preglednik izlistao sloj i zapamtio njegovo stanje, katalog dokumenta treba rječnik /OCProperties, a §8.11.4 točno propisuje što ide u njega. Postoji niz (array) /OCGs koji imenuje svaku grupu u datoteci, te unos /D koji sadrži zadanu konfiguraciju. Zadana konfiguracija je dio koji čitatelj primjenjuje kada se datoteka prvi put otvori. Ona bilježi koje grupe počinju kao uključene, a koje kao isključene, koji su unosi zaključani protiv korisničkog mijenjanja te, putem niza /Order, kako su nazivi slojeva raspoređeni i ugniježđeni na ploči.

Praktična posljedica je da stvaranje sloja nikada nije čisto lokalni čin. Grupa se mora iscrtati na stranici, a također se mora registrirati u strukturi na razini kataloga koja prije nije postojala. PDFlibPas radi oboje za vas. Prvi poziv koji stvara grupu dodaje unos /OCProperties u katalog i postavlja zadanu konfiguraciju, tako da je sloj i nacrtan i izlistan bez posebnog vođenja evidencije s vaše strane.

Zašto način usklađenosti može uskratiti ovu značajku

Prije nego što se pokrene bilo koji kod za slojeve, cilj usklađenosti dokumenta odlučuje je li neobavezni sadržaj uopće dopušten. PDF/A-1, arhivski profil definiran u ISO 19005-1, u potpunosti zabranjuje unos /OCProperties u §6.1.13. Razlog odgovara svrsi formata. Arhivska datoteka mora se prikazivati identično za svakog čitatelja daleko u budućnost, a sadržaj čiju vidljivost preglednik može promijeniti je sadržaj čiji izgled nije fiksiran, pa profil zabranjuje taj koncept umjesto da dopusti dvosmislenu arhivu. PDF/A-2 i PDF/A-3, definirani u ISO 19005-2 i ISO 19005-3, zauzimaju suprotno stajalište u svom §6.9 i dopuštaju neobavezni sadržaj, uz pravila o zadanoj vidljivosti.

Ta se razlika izravno odražava u API-ju. Kada je dokument u načinu rada PDF/A-1, NewOptionalContentGroup odbija stvoriti grupu i vraća nulu, jer bi udovoljavanje tom zahtjevu proizvelo datoteku koja ne zadovoljava vlastitu deklariranu usklađenost. U načinu rada PDF/A-2 ili PDF/A-3, kao i u običnom neograničenom PDF-u, isti poziv uspijeva i vraća nenulti ID grupe. Nulti rezultat stoga nije općenita pogreška koju treba kasnije istraživati; to vam knjižnica govori da aktivna razina usklađenosti nema mjesta za ovu značajku.

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;

Dva stanja po sloju, a ne jedno

Sloj nije jednostavno vidljiv ili nevidljiv. Zadana konfiguracija bilježi njegovo stanje na zaslonu i zasebno stanje ispisa, jer §8.11.4 razlikuje ono što preglednik prikazuje od onoga što sustav za ispis emitira. Ta dva stanja su namjerno neovisna. Vodeni žig skice (draft) može biti prikazan na zaslonu i izostavljen s papira, a sloj s linijama rezanja može biti skriven na zaslonu, a ipak poslan na crtač (plotter). Spajanje ta dva stanja prisililo bi jedno da prati drugo, čime bi se izgubila upravo ona kontrola zbog koje ova značajka i postoji.

PDFlibPas izlaže ovaj par putem dva postavljača. SetOptionalContentGroupVisible uzima ID grupe i zastavicu, pri čemu jedan znači vidljivo, a nula skriveno, te upravlja zadanim stanjem na zaslonu. SetOptionalContentGroupPrintable uzima ID grupe i zastavicu za to hoće li se sloj emitirati prilikom ispisa dokumenta. Odgovarajući dohvaćatelji, GetOptionalContentGroupVisible i GetOptionalContentGroupPrintable, vraćaju jedan ili nulu, tako da možete zasebno pročitati izgled zaslona i ispisa sloja umjesto da jedno zaključujete iz drugoga.

Izgradnja dvaju slojeva na stranici

Stvaranje sloja i njegovo popunjavanje slijedi fiksni redoslijed. Nacrtate sadržaj za sloj na trenutnoj stranici, a zatim pozovete SetContentStreamOptional s ID-om grupe, što omotava trenutni tok sadržaja stranice tako da sve do sada nacrtano pripada toj grupi. Budući da poziv bilježi sve što se u tom trenutku nalazi u toku, pravilo je postaviti oznake jednog sloja, dodijeliti ih, pa tek onda započeti sljedeći sloj. Primjer u nastavku postavlja instalacije na prvu stranicu, a recenzentov crveni marker na drugu stranicu, postavlja stanje zaslona i ispisa svakog sloja te sprema.

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;

Sloj s crvenim markerom je slučaj vrijedan pažnje. Prikazuje se na zaslonu kako bi recenzent vidio bilješku, a njegova zastavica za ispis je nula tako da ispis iste datoteke ne nosi tekst recenzije. Ta je asimetrija cijela poanta držanja ova dva stanja odvojenima.

Čitanje konfiguracije natrag

Čitanje slojeva je drugačiji prolaz kroz istu strukturu. Nakon učitavanja datoteke, GetOptionalContentConfigCount javlja koliko rječnika konfiguracije dokument sadrži; prva zadana konfiguracija je config ID 1. Unutar konfiguracije, GetOptionalContentConfigOrderCount daje broj unosa u stablu redoslijeda, a vi ih indeksirate od 1. Za svaki unos, GetOptionalContentConfigOrderItemLabel vraća njegov tekst za prikaz, a GetOptionalContentConfigOrderItemLevel vraća njegovu dubinu ugniježđenja, tako da se izgled ploče s pod-slojevima uvučenim ispod zaglavlja može rekonstruirati doslovno.

Svaki unos također ima tip. GetOptionalContentConfigOrderItemType razlikuje stvarnu grupu neobaveznog sadržaja od obične tekstualne oznake koja postoji samo kao zaglavlje odjeljka stabla. Ta je razlika važna jer upiti o stanju pojedine grupe imaju smisla samo za stvarne grupe. Za unos grupe, GetOptionalContentConfigState izvještava pokreće li je konfiguracija kao uključenu, isključenu ili je ostavlja nepromijenjenom, a GetOptionalContentConfigLocked izvještava je li korisniku zabranjeno mijenjati njezino stanje. Petlja u nastavku prikazuje stablo redoslijeda sa stanjem svake grupe i statusom zaključavanja, uvlačeći ih po razini.

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;

Dva detalja osiguravaju točnost ove petlje. Indeks redoslijeda počinje od jedan, od 1 do ukupnog broja, što odgovara načinu na koji knjižnica interno numerira stablo. Pozivi po grupama pokreću se samo kada je tip stavke grupa, jer je tekstualna oznaka zapravo zaglavlje s nazivom i razinom, ali bez stanja uključenosti, isključenosti ili zaključanosti za ispitivanje. Preskočite tu zaštitu i zatražit ćete od oznake stanje koje ona nema.

Gdje se ovo uklapa

Slojevi su mehanizam prezentacije, tako da ih motor mora poštovati na svakoj stazi koja iscrtava stranicu, a strana iscrtavanja pokrivena je u našem vodiču kroz iscrtavanje s više motora u Delphiu. Oni se također sijeku sa strukturom dokumenta, jer je naziv sloja tekst okrenut autoru, a čitatelj ima koristi od strukturiranog obrisa sloja, što se povezuje s radom u našem članku o označenom PDF-u i strukturi pristupačnosti. Oboje se uparuje s API-jima za neobavezni sadržaj koji su ovdje opisani, a koji se isporučuju kao dio Delphi PDF knjižnice uz funkcije za stranice, tekst, fontove i usklađenost o kojima se raspravlja drugdje na ovom blogu.