Technical Article

Lapisan PDF di Delphi: Optional Content Groups (OCG)

Seorang surveyor membuka rencana tapak (site plan) dan ingin garis konturnya disembunyikan sementara utilitasnya tetap menyala. Seorang peninjau ingin anotasi garis merah (redline) terlihat di layar dan hilang dari hasil cetak. Lembar produk dikirim dalam tiga bahasa dari satu file, dan pembaca memilih bahasa mana yang ditampilkan. Ketiganya merupakan fitur PDF yang sama, dan panel yang mengaturnya di Acrobat disebut Layers. Fitur di bawah panel tersebut adalah konten opsional (optional content), dan inilah yang memungkinkan satu halaman membawa beberapa strata visual independen yang dapat dinyalakan dan dimatikan oleh pembaca.

Konten opsional ditentukan dalam ISO 32000-1 §8.11. Unit visibilitasnya adalah grup konten opsional (optional content group), yaitu OCG, kamus tipe /OCG yang memiliki nama. Konten bertanda (marked content) pada halaman dikaitkan dengan sebuah grup, dan pembaca memutuskan apakah grup tersebut saat ini ditampilkan. Konstruksi terkait, kamus keanggotaan konten opsional (optional content membership dictionary) atau OCMD, memungkinkan visibilitas bergantung pada kombinasi boolean dari beberapa grup, tetapi kasus sehari-hari adalah grup bernama tunggal yang mewakili satu lapisan. Dokumen mengikat seluruh mekanisme ini bersama-sama melalui satu entri katalog, /OCProperties, yang dijelaskan berikutnya.

Apa yang harus dibawa oleh katalog

Sebuah OCG saja tidak aktif. Agar pembaca dapat mencantumkan lapisan dan mengingat keadaannya, katalog dokumen memerlukan kamus /OCProperties, dan §8.11.4 menjabarkan dengan tepat apa yang ada di dalamnya. Ada array /OCGs yang menamai setiap grup di dalam berkas, dan ada entri /D yang menyimpan konfigurasi default. Konfigurasi default adalah bagian yang diterapkan pembaca saat berkas pertama kali dibuka. Ini mencatat grup mana yang mulai menyala dan mana yang mati, entri mana yang dikunci agar pengguna tidak dapat mengalihkannya, dan, melalui array /Order, bagaimana nama-nama lapisan diatur dan disarangkan di panel.

Konsekuensi praktisnya adalah bahwa membuat lapisan tidak pernah menjadi tindakan yang murni lokal. Grup harus digambar di halaman, dan juga harus didaftarkan dalam struktur tingkat katalog yang sebelumnya tidak ada. PDFlibPas melakukan keduanya untuk Anda. Panggilan pertama yang membuat grup akan menambahkan entri /OCProperties ke katalog dan menyemai konfigurasi default, sehingga lapisan tersebut digambar dan terdaftar tanpa pembukuan terpisah di pihak Anda.

Mengapa mode kepatuhan dapat menolak fitur ini

Sebelum kode lapisan apa pun dijalankan, target kesesuaian dokumen menentukan apakah konten opsional diperbolehkan secara hukum. PDF/A-1, profil pengarsipan yang ditentukan dalam ISO 19005-1, melarang entri /OCProperties secara langsung di §6.1.13. Alasan tersebut sesuai dengan tujuan formatnya. Berkas arsip harus dirender secara identik untuk setiap pembaca di masa depan, dan konten yang visibilitasnya dapat diubah oleh pembaca adalah konten yang tampilannya tidak tetap, sehingga profil tersebut melarang konstruksi ini alih-alih mengizinkan arsip yang ambigu. PDF/A-2 dan PDF/A-3, yang didefinisikan dalam ISO 19005-2 and ISO 19005-3, mengambil pandangan sebaliknya dalam §6.9 mereka dan mengizinkan konten opsional, dengan aturan tentang visibilitas default.

Perbedaan itu muncul secara langsung di API. Ketika dokumen berada dalam mode PDF/A-1, NewOptionalContentGroup menolak untuk membuat grup dan mengembalikan nilai nol, karena memenuhi permintaan tersebut akan menghasilkan berkas yang gagal dalam kepatuhan yang dinyatakannya sendiri. Dalam mode PDF/A-2 atau PDF/A-3, dan dalam PDF biasa tanpa batasan, panggilan yang sama berhasil dan mengembalikan ID grup non-nol. Oleh karena itu, hasil nol bukanlah kegagalan umum untuk diperiksa nanti; itu adalah pustaka yang memberi tahu Anda bahwa tingkat kepatuhan aktif tidak memiliki ruang untuk fitur tersebut.

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;

Dua status per lapisan, bukan satu

Lapisan tidak sekadar terlihat atau tidak terlihat. Konfigurasi default mencatat keadaan di layar dan keadaan cetak terpisah, karena §8.11.4 membedakan apa yang ditampilkan pembaca dari apa yang dikeluarkan oleh alur kerja pencetakan. Keduanya sengaja dibuat independen. Tanda air draf dapat ditampilkan di layar dan dihilangkan dari kertas, dan lapisan garis potong (cut-line) dapat disembunyikan di layar namun tetap dikirim ke plotter. Menyatukan keduanya akan memaksa satu untuk mengikuti yang lain dan kehilangan kontrol yang justru menjadi alasan fitur ini ada.

PDFlibPas mengekspos pasangan tersebut melalui dua setter. SetOptionalContentGroupVisible mengambil ID grup dan sebuah bendera (flag), di mana satu berarti terlihat dan nol berarti tersembunyi, dan mengatur keadaan default di layar. SetOptionalContentGroupPrintable mengambil ID grup dan bendera untuk menentukan apakah lapisan tersebut dikeluarkan saat dokumen dicetak. Getter yang cocok, GetOptionalContentGroupVisible dan GetOptionalContentGroupPrintable, masing-masing mengembalikan nilai satu atau nol, sehingga Anda dapat membaca kembali disposisi layar dan cetak lapisan secara terpisah alih-alih menyimpulkan satu dari yang lain.

Membangun dua lapisan di halaman

Membuat lapisan dan mengisinya mengikuti urutan yang tetap. Anda menggambar konten untuk lapisan tersebut pada halaman saat ini, lalu memanggil SetContentStreamOptional dengan ID grup, yang membungkus aliran konten (content stream) halaman saat ini sehingga semua yang digambar sejauh ini termasuk dalam grup tersebut. Karena panggilan tersebut menangkap apa pun yang ada di aliran konten pada saat itu, disiplinnya adalah meletakkan tanda untuk satu lapisan, menetapkannya, dan baru kemudian memulai lapisan berikutnya. Contoh di bawah ini menempatkan utilitas di halaman pertama dan garis merah peninjau (reviewer redline) di halaman kedua, menetapkan status layar dan cetak masing-masing lapisan, lalu menyimpannya.

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;

Lapisan garis merah (redline) adalah kasus yang perlu diperhatikan. Lapisan ini ditampilkan di layar agar peninjau dapat melihat catatan tersebut, dan bendera dapat dicetaknya (printable flag) bernilai nol sehingga hasil cetak dari berkas yang sama tidak membawa teks peninjauan tersebut. Asimetri itu adalah seluruh poin dari pemisahan kedua status tersebut.

Membaca kembali konfigurasi

Membaca lapisan adalah cara penelusuran yang berbeda melalui struktur yang sama. Setelah berkas dimuat, GetOptionalContentConfigCount melaporkan berapa banyak kamus konfigurasi yang dimiliki dokumen; konfigurasi default pertama adalah ID konfigurasi 1. Di dalam konfigurasi, GetOptionalContentConfigOrderCount memberikan jumlah entri di pohon urutan (order tree), dan Anda mengindeksnya mulai dari 1. Untuk setiap entri, GetOptionalContentConfigOrderItemLabel mengembalikan teks tampilannya dan GetOptionalContentConfigOrderItemLevel mengembalikan kedalaman sarangnya, sehingga kerangka panel dengan sub-lapisan yang menjorok di bawah judul dapat direkonstruksi secara verbatim.

Setiap entri juga memiliki tipe. GetOptionalContentConfigOrderItemType membedakan grup konten opsional yang sebenarnya dari label teks biasa yang ada hanya untuk memimpin bagian pohon. Perbedaan itu penting karena kueri status per grup hanya masuk akal untuk grup nyata. Untuk entri grup, GetOptionalContentConfigState melaporkan apakah konfigurasi memulainya dalam keadaan hidup, mati, atau membiarkannya tidak berubah, dan GetOptionalContentConfigLocked melaporkan apakah pengguna dilarang mengalihkannya. Perulangan di bawah ini merender pohon urutan dengan status masing-masing grup dan status kunci, menjorokkan berdasarkan level.

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;

Dua detail membuat perulangan ini tetap benar. Indeks urutan berbasis satu (one-based), dari 1 hingga jumlahnya, cocok dengan cara pustaka menomori pohon secara internal. Dan panggilan per grup hanya berjalan ketika tipe item adalah grup, karena label teks adalah judul dengan nama dan level tetapi tidak memiliki status hidup, mati, atau terkunci untuk ditanyakan. Lewati pelindung (guard) tersebut dan Anda akan menanyakan label untuk status yang tidak dimilikinya.

Di mana ini cocok

Lapisan adalah mekanisme presentasi, sehingga mesin harus menghormatinya di setiap jalur yang merender halaman, dan sisi rendering tersebut dibahas dalam panduan kami tentang perenderan multi-mesin di Delphi. Lapisan ini juga beririsan dengan struktur dokumen, karena nama lapisan adalah teks yang menghadap ke penulis dan pembaca mendapat manfaat dari kerangka lapisan terstruktur, yang terhubung dengan pekerjaan dalam artikel kami tentang struktur aksesibilitas dan PDF bertag. Keduanya berpasangan dengan API konten opsional yang dijelaskan di sini, yang dikirim sebagai bagian dari Delphi PDF Library bersama dengan fasilitas halaman, teks, font, dan kepatuhan yang dibahas di bagian lain di blog ini.