Technical Article

PDF sluoksniai Delphi: pasirenkamo turinio grupės (OCG)

Matininkas atidaro sklypo planą ir nori, kad kontūrai būtų paslėpti, o komunalinės paslaugos liktų matomos. Recenzentas nori, kad raudonos linijos anotacijos būtų matomos ekrane, bet dingtų iš spaudinio. Produkto lapas išsiunčiamas trimis kalbomis iš vieno failo, o skaitytojas pasirenka, kuri kalba rodoma. Visi trys yra ta pati PDF funkcija, o skydelis, kuris juos valdo Acrobat programoje, vadinamas Layers (Sluoksniai). Po šiuo skydeliu esanti funkcija yra pasirenkamas turinys (angl. optional content), ir būtent ji leidžia viename puslapyje turėti kelis nepriklausomus vaizdo sluoksnius, kuriuos peržiūros programa įjungia ir išjungia

Pasirenkamas turinys yra apibrėžtas ISO 32000-1 §8.11 skyriuje. Matomumo vienetas yra pasirenkamo turinio grupė, OCG, /OCG tipo žodynas, turintis pavadinimą. Pažymėtas turinys puslapyje yra susietas su grupe, o peržiūros programa nusprendžia, ar ta grupė šiuo metu rodoma. Susijusi konstrukcija, pasirenkamo turinio narystės žodynas arba OCMD, leidžia matomumui priklausyti nuo kelių grupių loginio derinio, tačiau kasdienis atvejis yra viena įvardyta grupė, atitinkanti vieną sluoksnį. Dokumentas susieja visą mechanizmą per vieną katalogo įrašą, /OCProperties, aprašytą toliau

Ką katalogas turi turėti

OCG pati savaime yra inertiška. Kad peržiūros programa galėtų pateikti sluoksnį ir prisiminti jo būseną, dokumentų katalogui reikia /OCProperties žodyno, o §8.11.4 skyriuje tiksliai išdėstyta, kas į jį įeina. Greta yra /OCGs masyvas, nurodantis kiekvieną failo grupę, ir yra /D įrašas, kuriame saugoma numatytoji konfigūracija. Numatytoji konfigūracija yra ta dalis, kurią skaitytuvas taiko pirmą kartą atidarius failą. Ji fiksuoja, kurios grupės pradedamos rodyti, o kurios ne, kurie įrašai yra užrakinti nuo vartotojo perjungimo, ir per /Order masyvą nurodo, kaip sluoksnių pavadinimai yra išdėstyti ir įterpti skydelyje

Praktinė pasekmė yra ta, kad sluoksnio kūrimas niekada nėra grynai vietinis veiksmas. Sluoksnio grupė turi būti nupiešta puslapyje, taip pat ji turi būti užregistruota katalogo lygio struktūroje, kurios anksčiau nebuvo. PDFlibPas atlieka abu veiksmus už jus. Pirmasis iškvietimas, sukuriantis grupę, prideda /OCProperties įrašą į katalogą ir užpildo numatytąją konfigūraciją, todėl sluoksnis yra ir nupiešiamas, ir įtraukiamas į sąrašą be atskiros apskaitos jūsų pusėje

Kodėl atitikties režimas gali neleisti naudoti šios funkcijos

Prieš paleidžiant bet kokį sluoksnio kodą, dokumento atitikties tikslas nusprendžia, ar pasirenkamas turinys išvis yra teisėtas. PDF/A-1, archyvavimo profilis, apibrėžtas ISO 19005-1, visiškai draudžia /OCProperties įrašą §6.1.13 skyriuje. Šis argumentas atitinka formato paskirtį. Archyvinis failas turi būti atvaizduojamas identiškai kiekvienam skaitytojui tolimoje ateityje, o turinys, kurio matomumą peržiūros programa gali pakeisti, yra turinys, kurio išvaizda nėra fiksuota, todėl profilis draudžia šią konstrukciją, užuot leidęs dviprasmišką archyvą. PDF/A-2 ir PDF/A-3, apibrėžti ISO 19005-2 ir ISO 19005-3, laikosi priešingos nuomonės savo §6.9 skyriuje ir leidžia pasirenkamą turinį su taisyklėmis dėl numatytojo matomumo

Šis skirtumas tiesiogiai atsispindi API. Kai dokumentas yra PDF/A-1 režime, NewOptionalContentGroup atsisako sukurti grupę ir grąžina nulį, nes patenkinus užklausą būtų sukurtas failas, kuris nepraeina savo paties deklaruotos atitikties. PDF/A-2 arba PDF/A-3 režimuose ir įprastame neribotame PDF tas pats iškvietimas pavyksta ir grąžina ne nulinį grupės ID. Todėl nulinis rezultatas nėra bendra klaida, kurią reikėtų tikrinti vėliau; tai yra biblioteka, pranešanti, kad aktyvus atitikties lygis neturi vietos šiai funkcijai

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;

Dvi būsenos vienam sluoksniui, o ne viena

Sluoksnis nėra tiesiog matomas arba nematomas. Numatytojoje konfigūracijoje fiksuojama jo būsena ekrane ir atskira spausdinimo būsena, nes §8.11.4 skyrius skiria tai, ką rodo peržiūros programa, nuo to, ką išveda spausdinimo konvejeris. Šie du dalykai yra nepriklausomi tyčia. Juodraščio vandens ženklas gali būti rodomas ekrane ir pašalintas iš popieriaus, o pjovimo linijos sluoksnis gali būti paslėptas ekrane, bet išsiųstas į braižytuvą. Sujungus šiuos du dalykus, vienas turėtų sekti kitą ir būtų prarasta būtent ta kontrolė, kuriai suteikti ši funkcija egzistuoja

PDFlibPas atskleidžia šią porą per du nustatymo metodus (angl. setters). SetOptionalContentGroupVisible priima grupės ID ir vėliavėlę, kur vienetas reiškia matomą, o nulis – paslėptą, ir valdo numatytąją būseną ekrane. SetOptionalContentGroupPrintable priima grupės ID ir vėliavėlę, nurodančią, ar sluoksnis yra išvedamas spausdinant dokumentą. Atitinkamos gavimo funkcijos (angl. getters) GetOptionalContentGroupVisible ir GetOptionalContentGroupPrintable grąžina vienetą arba nulį, todėl galite atskirai perskaityti sluoksnio ekrano ir spausdinimo nustatymus, užuot darę prielaidą apie vieną iš kito

Dviejų sluoksnių kūrimas puslapyje

Sluoksnio kūrimas ir jo užpildymas vyksta nustatyta tvarka. Jūs nupiešiate sluoksnio turinį dabartiniame puslapyje, tada iškviečiate SetContentStreamOptional su grupės ID, kuris apgaubia dabartinį puslapio turinio srautą, kad viskas, kas nupiešta iki šiol, priklausytų tai grupei. Kadangi iškvietimas užfiksuoja viską, kas tuo metu yra sraute, taisyklė yra nupiešti vieno sluoksnio žymes, jas priskirti ir tik tada pradėti kitą sluoksnį. Žemiau pateiktame pavyzdyje komunalinės paslaugos perkeliamos į pirmąjį puslapį, o recenzento raudonos linijos – į antrąjį puslapį, nustatoma kiekvieno sluoksnio ekrano ir spaudinio būsena bei išsaugoma

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;

Raudonos linijos sluoksnis yra tas atvejis, kurį verta pažymėti. Jis rodomas ekrane, kad recenzentas matytų pastabą, o jo spausdinimo vėliavėlė yra nulis, todėl to paties failo spaudinyje nėra jokio peržiūros teksto. Ši asimetrija yra visa esmė, kodėl šios dvi būsenos laikomos atskirai

Konfigūracijos skaitymas atgal

Sluoksnių skaitymas yra kitoks perėjimas per tą pačią struktūrą. Įkėlus failą, GetOptionalContentConfigCount praneša, kiek konfigūracijos žodynų yra dokumente; pirmoji numatytoji konfigūracija yra konfigūracijos ID 1. Konfigūracijoje GetOptionalContentConfigOrderCount nurodo įrašų skaičių užsakymų medyje, o jūs juos indeksuojate nuo 1. Kiekvienam įrašui GetOptionalContentConfigOrderItemLabel grąžina jo rodomą tekstą, o GetOptionalContentConfigOrderItemLevel – jo įdėjimo gylį, todėl skydelio struktūra su subsluoksniais, įtrauktais po antraštėmis, gali būti rekonstruota pažodžiui

Kiekvienas įrašas taip pat turi tipą. GetOptionalContentConfigOrderItemType skiria tikrąją pasirenkamo turinio grupę nuo paprasto teksto etiketės, kuri egzistuoja tik tam, kad pradėtų medžio sekciją. Šis skirtumas yra svarbus, nes užklausos apie grupės būseną turi prasmę tik tikroms grupėms. Grupės įrašui GetOptionalContentConfigState praneša, ar konfigūracija ją paleidžia įjungtą, išjungtą, ar palieka nepakeistą, o GetOptionalContentConfigLocked praneša, ar vartotojui draudžiama ją perjungti. Žemiau esantis ciklas atvaizduoja užsakymų medį su kiekvienos grupės būsena ir užrakinimo būsena, pritaikant įtrauką pagal lygį

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;

Dvi detalės užtikrina šio ciklo teisingumą. Užsakymo indeksas prasideda nuo vieneto, nuo 1 iki skaičiaus, atitinkant tai, kaip biblioteka numeruoja medį viduje. O iškvietimai atskiroms grupėms veikia tik tada, kai elemento tipas yra grupė, nes teksto etiketė yra antraštė su pavadinimu ir lygiu, bet neturinti įjungtos, išjungtos ar užrakintos būsenos, kurią būtų galima užklausti. Praleiskite šią apsaugą ir užklausite etiketės būsenos, kurios ji neturi

Kur tai tinka

Sluoksniai yra pateikimo mechanizmas, todėl variklis turi juos gerbti kiekviename kelyje, kuriuo atvaizduojamas puslapis, o atvaizdavimo pusė yra aprašyta mūsų kelių variklių atvaizdavimo apžvalgoje Delphi aplinkoje. Jie taip pat kertasi su dokumento struktūra, nes sluoksnio pavadinimas yra autoriaus tekstas, o skaitytojui naudinga struktūrizuota sluoksnių schema, kuri siejasi su darbu, aprašytu mūsų straipsnyje apie pažymėtą PDF ir prieinamumo struktūrą. Abu dera su čia aprašytomis pasirenkamo turinio API, kurios platinamos kaip Delphi PDF biblioteka šalia puslapių, teksto, šriftų ir atitikties priemonių, aptariamų kitur šiame tinklaraštyje