Tehnički članak

PDF slojevi u Delphi-ju: Opcione grupe sadržaja (OCG)

Geometar otvara situacioni plan i želi da konture budu skrivene dok instalacije ostaju prikazane. Recenzent želi da crvene oznake za korekciju budu vidljive na ekranu, ali uklonjene sa odštampanog lista. List sa podacima o proizvodu se isporučuje na tri jezika u jednoj datoteci, a čitalac bira koji će jezik biti prikazan. Sva tri primera koriste istu PDF funkciju, a panel koji ih kontroliše u Acrobat-u zove se Layers. Funkcija koja leži iza tog panela je opcioni sadržaj, i to je ono što omogućava da jedna stranica nosi nekoliko nezavisnih vizuelnih slojeva koje čitač uključuje i isključuje

Opcioni sadržaj je specifikovan u standardu ISO 32000-1 §8.11. Jedinica vidljivosti je opciona grupa sadržaja, OCG, rečnik tipa /OCG koji nosi ime. Označeni sadržaj na stranici je povezan sa grupom, a čitač odlučuje da li je ta grupa trenutno prikazana. Srodna struktura, rečnik članstva opcionog sadržaja ili OCMD, omogućava da vidljivost zavisi od logičke kombinacije nekoliko grupa, ali svakodnevni slučaj je jedna imenovana grupa koja predstavlja jedan sloj. Dokument povezuje ceo mehanizam kroz jedan unos kataloga, /OCProperties, koji je opisan u nastavku

Šta katalog mora da sadrži

OCG je sam po sebi inertan. Da bi čitač izlistao sloj i zapamtio njegovo stanje, katalogu dokumenta je potreban rečnik /OCProperties, a §8.11.4 tačno propisuje šta ide u njega. Postoji niz /OCGs koji imenuje svaku grupu u datoteci, kao i unos /D koji drži podrazumevanu konfiguraciju. Podrazumevana konfiguracija je deo koji čitač primenjuje kada se datoteka prvi put otvori. Ona beleži koje grupe počinju kao uključene a koje kao isključene, koji unosi su zaključani protiv korisničkog menjanja i, kroz niz /Order, kako su nazivi slojeva raspoređeni i ugnežđeni u panelu

Struktura PDF optional content grupa iza PDF Library for Delphi: OCProperties unos kataloga izlistava svaki OCG sloj i njegovu podrazumevanu konfiguraciju sa početnim stanjima, zaključavanjima i redosledom panela, dok marked content strane vezuje crteže za grupe
Slojevi postaju adresabilni samo kroz katalog: OCProperties izlistava svaki OCG i podrazumevana konfiguracija beleži početna stanja, brave i red panela

Praktična posledica je da kreiranje sloja nikada nije čisto lokalni čin. Grupa se mora iscrtati na stranici, a takođe mora biti registrovana u strukturi na nivou kataloga koja ranije nije postojala. PDF Library for Delphi radi oba koraka umesto vas. Prvi poziv koji kreira grupu dodaje unos /OCProperties u katalog i postavlja podrazumevanu konfiguraciju, tako da je sloj i nacrtan i izlistan bez posebnog vođenja evidencije sa vaše strane

Zašto režim usklađenosti može uskratiti ovu funkciju

Pre nego što se pokrene bilo koji kod za slojeve, cilj usklađenosti dokumenta odlučuje da li je opcioni sadržaj uopšte dozvoljen. PDF/A-1, arhivski profil definisan u ISO 19005-1, u potpunosti zabranjuje unos /OCProperties u §6.1.13. Razlog odgovara svrsi formata. Arhivska datoteka mora da se renderuje identično za svakog čitaoca daleko u budućnosti, a sadržaj čiju vidljivost čitalac može da promeni je sadržaj čiji izgled nije fiksiran, pa profil zabranjuje ovu strukturu umesto da dozvoli dvosmislenu arhivu. PDF/A-2 i PDF/A-3, definisani u ISO 19005-2 i ISO 19005-3, zauzimaju suprotan stav u svom §6.9 i dozvoljavaju opcioni sadržaj, uz pravila o podrazumevanoj vidljivosti

Ta razlika se direktno odražava u API-ju. Kada je dokument u PDF/A-1 režimu, NewOptionalContentGroup odbija da kreira grupu i vraća nulu, jer bi ispunjavanje zahteva proizvelo datoteku koja ne prolazi sopstvenu deklarisanu usklađenost. U režimu PDF/A-2 ili PDF/A-3, kao i u običnom neograničenom PDF-u, isti poziv uspeva i vraća ne-nulti ID grupe. Nulti rezultat stoga nije generička greška koju treba kasnije ispitati; to vam biblioteka govori da aktivni nivo usklađenosti nema mesta za tu funkciju

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
      // odbijeno pod PDF/A-1; nije prolazna greška, režim zabranjuje slojeve
      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. Podrazumevana konfiguracija beleži njegovo stanje na ekranu i posebno stanje štampanja, jer §8.11.4 razlikuje ono što čitač prikazuje od onoga što sistem za štampanje emituje. Ta dva stanja su namerno nezavisna. Vodeni žig nacrta može biti prikazan na ekranu i izostavljen sa papira, a sloj sa linijama sečenja može biti skriven na ekranu, ali poslat na ploter. Spajanje ova dva stanja primoralo bi jedno da prati drugo i izgubila bi se upravo ona kontrola zbog koje funkcija i postoji

PDF Library for Delphi izlaže ovaj par kroz dva setera. SetOptionalContentGroupVisible uzima ID grupe i zastavicu, gde jedan znači vidljivo a nula skriveno, i upravlja podrazumevanim stanjem na ekranu. SetOptionalContentGroupPrintable uzima ID grupe i zastavicu za to da li se sloj emituje kada se dokument štampa. Odgovarajući geteri, GetOptionalContentGroupVisible i GetOptionalContentGroupPrintable, vraćaju jedan ili nulu, tako da možete zasebno pročitati raspored sloja na ekranu i pri štampanju, umesto da zaključujete jedno iz drugog

Matrica nezavisnih stanja vidljivosti na ekranu i štampi za PDF slojeve stvorene Delphi PDF Library: uslužni slojevi ostaju vidljivi svuda, oznake recenzenta se prikazuju na ekranu ali se nikad ne štampaju, a plotter rezna linija se krije na ekranu ali se ipak štampa
Vidljivost na ekranu i emitovanje štampe postavljaju se po grupi, pa utilitarni slojevi mogu da se pojave svuda dok oznake recenzenta ostaju napomena samo za ekran

Izgradnja dva sloja na stranici

Kreiranje sloja i njegovo popunjavanje prate fiksni redosled. Nacrtate sadržaj za sloj na trenutnoj stranici, a zatim pozovete SetContentStreamOptional sa ID-om grupe, što obavija trenutni tok sadržaja stranice tako da sve što je do sada nacrtano pripada toj grupi. Pošto poziv snima sve što je na toku u tom trenutku, pravilo je da postavite oznake jednog sloja, dodelite ih, pa tek onda započnete sledeći sloj. Primer ispod postavlja instalacije na prvu stranicu, a recenzentske ispravke na drugu stranicu, podešava stanje ekrana i štampanja za svaki sloj i čuva datoteku

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);

    // Sloj 1: pomoćni elementi, iscrtan pa dodeljen sopstvenoj grupi
    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);   // prikazano na ekranu
    Pdf.SetOptionalContentGroupPrintable(UtilLayer, 1); // i na papiru

    // Sloj 2: recenzentske oznake na novoj stranici
    Pdf.InsertPages(2, 1);                     // dodaj jednu stranicu posle stranice 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);    // vidljivo tokom pregleda
    Pdf.SetOptionalContentGroupPrintable(RedlineLayer, 0);  // never printed

    Pdf.SaveToFile('SitePlan_Layers.pdf');
  finally
    Pdf.Free;
  end;
end;

Sloj sa recenzentskim ispravkama je primer vredan pažnje. Prikazuje se na ekranu kako bi recenzent video belešku, a njegova zastavica za štampanje je nula, tako da odštampana verzija iste datoteke ne sadrži tekst recenzije. Ta asimetrija je cela poenta držanja ova dva stanja odvojeno

Čitanje konfiguracije nazad

Čitanje slojeva je drugačiji prolazak kroz istu strukturu. Nakon što se datoteka učita, GetOptionalContentConfigCount izveštava o tome koliko rečnika konfiguracije dokument sadrži; prva podrazumevana konfiguracija je ID konfiguracije 1. Unutar konfiguracije, GetOptionalContentConfigOrderCount daje broj unosa u stablu redosleda, a vi ih indeksirate počevši od 1. Za svaki unos, GetOptionalContentConfigOrderItemLabel vraća njegov tekst za prikaz, a GetOptionalContentConfigOrderItemLevel vraća njegovu dubinu ugnežđivanja, tako da se kontura panela sa pod-slojevima uvučenim ispod naslova može rekonstruisati doslovno

Svaki unos takođe ima tip. GetOptionalContentConfigOrderItemType razlikuje stvarnu opcionu grupu sadržaja od obične tekstualne oznake koja postoji samo da bi vodila odeljak stabla. Ta razlika je važna jer upiti o stanju po grupi imaju smisla samo za stvarne grupe. Za unos grupe, GetOptionalContentConfigState izveštava da li je konfiguracija pokreće kao uključenu, isključenu ili je ostavlja nepromenjenom, a GetOptionalContentConfigLocked izveštava da li je korisniku zabranjeno da je menja. Petlja ispod prikazuje stablo redosleda sa stanjem i statusom zaključavanja svake grupe, uvlačeći ih po nivoima

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;                                  // podrazumevana konfiguracija
    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 = opciona grupa sadržaja
      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 je naslov tekstualne oznake; nema stanje po grupi

      Writeln(Line);
    end;
  finally
    Pdf.Free;
  end;
end;

Dva detalja održavaju ovu petlju ispravnom. Indeks redosleda počinje od jedan, od 1 do ukupnog broja, što odgovara načinu na koji biblioteka interno numeriše stablo. Takođe, pozivi po grupi se pokreću samo kada je tip stavke grupa, jer je tekstualna oznaka zapravo naslov sa imenom i nivoom, ali bez stanja uključenosti, isključenosti ili zaključanosti koje bi se moglo ispitati. Preskočite tu zaštitu i pitaćete oznaku za stanje koje ona nema

Petlja čitanja konfiguracija optional content-a u PDF Library for Delphi: izlistaj konfiguracije, prođi kroz order unose od jedan, pa se granaj po ItemType tako da se samo prave grupe ispituju za on, off ili locked stanje dok se tekstualne oznake preskaču
Čitanje slojeva nazad hodi unosima reda baziranim na jedinici i pita stanje samo za prave grupe, jer naslovna etiketa nema stanje uključeno, isključeno ili zaključano za javljanje

Gde se ovo uklapa

Slojevi su mehanizam prezentacije, tako da mehanizam mora da ih poštuje na svakoj putanji koja renderuje stranicu, a strana renderovanja je pokrivena u našem vodiču kroz renderovanje sa više mehanizama u Delphi-ju. Oni se takođe presecaju sa strukturom dokumenta, jer je ime sloja tekst okrenut autoru, a čitalac ima koristi od strukturirane konture slojeva, što se povezuje sa radom u našem članku o označenom PDF-u i strukturi pristupačnosti. Oba se uparuju sa API-jima za opcioni sadržaj koji su ovde opisani, a koji se isporučuju kao deo Delphi PDF biblioteke zajedno sa funkcijama za stranice, tekst, fontove i usklađenost o kojima se govori na drugim mestima na ovom blogu