Teknik Makale

Delphi'de PDF Katmanları (OCG ve OCMD) PDFium ile Oluşturma

PDFium Component, Delphi tarafından yeni PDF katmanlarını TPdf.SaveAsOptionalContentCreated ile oluşturur. Tek bir doğrulanmış kaydetmede metot, isteğe bağlı içerik gruplarını (OCG), isteğe bağlı içerik üyelik sözlüklerini (OCMD) ve /OC girdisi yeni bir gruba işaret eden Form XObject'lere sarılmış, çağıran tarafından verilen çizimleri ekler; katman anahtarlandığında PDFium ve diğer görüntüleyiciler o çizimi gösterir ya da gizler

İstek nadiren soyut olur. Bir tesis mühendisliği ekibi, aynı çizim sayfası üzerinde metrik ve emperyal ölçüleri, tam olarak bir kümesi görünür olacak biçimde ister. Bir matbaa, ekranda görünen ama prova öncesi kapatılabilen bir kesme çizgisi katmanı ister. İki dosyada da henüz katman yoktur; anahtarlanacak bir şey yoktur. Mevcut katmanları anahtarlamak farklı bir iştir ve SaveAsOptionalContentConfigured ile mevcut PDF isteğe bağlı içerik katmanlarını anahtarlamak makalesinde ele alınır; bu makale katmanları baştan yaratmaktan söz eder

PDFium ile bir sayfa nesnesine neden doğrudan /OC ayarlayamazsınız?

PDFium, mevcut bir sayfa nesnesine isteğe bağlı içerik ekleyen genel bir fonksiyona sahip değildir. Genel başlıklarda FPDFPageObj_SetOC ya da eşdeğeri bir şey yoktur; o yüzden alışılmış düzenle-sonra-FPDFPage_GenerateContent yolu geçitli içerik üretemez. Geçit dosya yapısına yazılmak zorundadır ve tek soru, PDFium'un render ederken hangi yapıya riayet edeceğidir

ISO 32000-1 §8.11.3, grafikleri isteğe bağlı yapmanın üç yolunu verir ve PDFium onlara farklı davranır:

  • /OC /Name BDC ... EMC işaretli içeriğine sarılmış sayfa içerik akışı bölümleri; render sırasında sayfa nesnesi başına kontrol edilir
  • Kendi sözlüğünde bir /OC girdisi taşıyan Form XObject'ler ile görsel XObject'leri; XObject boyandığında kontrol edilir
  • Açıklama sözlüğünde bir /OC girdisi olan açıklamalar; PDFium render yolu bunlara hiç başvurmaz, geçitli bir açıklama katman durumundan bağımsız görünür kalır

İşaretli içeriğe sarmak, mevcut içerik akışlarını operatör operatör yeniden yazmak demektir ve açıklamaları renderer eler. Geriye Form XObject yolu kalır ve PDFium Component'in kullandığı da odur: çiziminiz, /OC'si gruba işaret eden yeni bir Form XObject'e girer; form, sayfa /Resources /XObject sözlüğüne kaydedilir ve tek bir /Name Do tutan yeni bir içerik akışı sayfa /Contents'ine eklenir. Mevcut sayfa içeriği asla ayrıştırılmaz ya da yeniden yazılmaz

PDFium altında ISO 32000-1 isteğe bağlı içerik geçit yolları: OC BDC ile EMC arasındaki işaretli içerik sayfa nesnesi başına kontrol edilir; /OC girdisi taşıyan bir Form XObject boyandığında kontrol edilir ve PDFium Component'in Delphi'den yazdığı yoldur; açıklama /OC'sine ise hiç başvurulmaz
PDFium, isteğe bağlı içeriği işaretli içerik bölümleri ile XObject sözlükleri için kontrol eder ama açıklamalar için etmez; SaveAsOptionalContentCreated bu yüzden verilen çizimi bir Form XObject ile bir Do operatörü üzerinden geçite alır

Grupları ve geçitli formları Delphi'de tanımlamak

TPdf.SaveAsOptionalContentCreated üç dizi alır ve bir rapor döndürür. Bir TStream'e, öteki bir dosya adına yazan her iki aşırı yükleme aynı parametre listesini paylaşır: Groups: TPdfOptionalContentCreateGroups, Memberships: TPdfOptionalContentCreateMemberships, Forms: TPdfOptionalContentCreateForms, out Report: TPdfOptionalContentCreateReport ve isteğe bağlı AllowSignedDocument: Boolean = False. Metot PDFium Component v3.154.0 ile geldi

Her TPdfOptionalContentCreateGroup bir Name, isteğe bağlı bir Creator, pociView ya da pociDesign olan bir Intent, bir InitialStateVisible bayrağı, bir Locked bayrağı ve bir RadioTag taşır. Aynı RadioTag'i paylaşan gruplar tek bir /RBGroups dizisine düşer; görüntüleyicinin birini açmanın diğerlerini kapattığını bilmesi budur. Her TPdfOptionalContentCreateForm, geçit grubunu GroupIndex (sizin Groups dizinize sıfır tabanlı bir indeks), 1 tabanlı bir PageNumber, ham içerik akışı baytları, bir sınırlayıcı kutu, isteğe bağlı bir matris ve isteğe bağlı bir ResourceName ile adlandırır

var
  Pdf: TPdf;
  Groups: TPdfOptionalContentCreateGroups;
  Forms: TPdfOptionalContentCreateForms;
  Report: TPdfOptionalContentCreateReport;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.FileName := 'sheet-a1.pdf';
    Pdf.Active := True;

    SetLength(Groups, 2);            // SetLength kayıtları sıfırla doldurur
    Groups[0].Name := 'Dimensions (mm)';
    Groups[0].Intent := pociView;
    Groups[0].InitialStateVisible := True;
    Groups[0].RadioTag := 'Units';
    Groups[1].Name := 'Dimensions (in)';
    Groups[1].Intent := pociView;
    Groups[1].InitialStateVisible := False;
    Groups[1].RadioTag := 'Units';

    SetLength(Forms, 2);
    Forms[0].GroupIndex := 0;
    Forms[0].PageNumber := 1;
    Forms[0].Content := TEncoding.ASCII.GetBytes(
      '0 0 1 RG 1.5 w 72 120 m 540 120 l S');
    Forms[0].BBoxRight := 612;
    Forms[0].BBoxTop := 792;
    Forms[0].ResourceName := 'DimMetric';
    Forms[1] := Forms[0];
    Forms[1].GroupIndex := 1;
    Forms[1].Content := TEncoding.ASCII.GetBytes(
      '1 0 0 RG 1.5 w 72 140 m 540 140 l S');
    Forms[1].ResourceName := 'DimImperial';

    if not Pdf.SaveAsOptionalContentCreated('sheet-a1-layers.pdf',
      Groups, nil, Forms, Report) then
      raise Exception.Create(Report.ErrorMessage);
  finally
    Pdf.Free;
  end;
end;

Form içeriğinin saf vektör çizimi olduğuna dikkat edin. Bileşen her forma yalnızca boş bir /Resources sözlüğü yazar; içerik akışı bu yüzden kendi kendine yetmelidir: aygıt renkleri, yollar, kırpma ve dönüşümler çalışır ama adlandırılmış bir kaynak gerektiren her şey (Tf için bir font, Do için bir görsel, gs için bir ExtGState) çözümleyecek hiçbir şey bulamaz. ResourceName'i boş bırakırsanız formlar FmOc1, FmOc2 diye adlandırılır; özel bir ad yalnızca harf, rakam, alt çizgi, eksi ve nokta içerebilir

Bir OCMD, düz bir grubun üstüne ne katar?

Bir isteğe bağlı içerik üyelik sözlüğü, görünürlüğü aynı anda birkaç gruba bağlar. ISO 32000-1 §8.11.2.2 iki biçim tanımlar: /AnyOn ya da /AllOn gibi bir /P politikasıyla bir /OCs listesi ve spesifikasyonda /And, /Or ile /Not'tan kurulan bir /VE görünürlük ifadesi. PDFium Component, MemberGroupIndices, bir Policy ile bir Expression taşıyan bir TPdfOptionalContentCreateMembership alır; Expression.GroupIndices boş olmadığında ifade kazanır ve /VE, /OCs'in yanına yazılır

procedure AddUnitMemberships(var Memberships: TPdfOptionalContentCreateMemberships);
begin
  SetLength(Memberships, 2);

  // Birim katmanlarından biri açıkken görünür: /OCs [...] /P /AnyOn
  SetLength(Memberships[0].MemberGroupIndices, 2);
  Memberships[0].MemberGroupIndices[0] := 0;
  Memberships[0].MemberGroupIndices[1] := 1;
  Memberships[0].Policy := ocmpAnyOn;

  // Yalnızca emperyal katman kapalıyken görünür: /VE [ /Not ref ]
  SetLength(Memberships[1].MemberGroupIndices, 1);
  Memberships[1].MemberGroupIndices[0] := 1;
  Memberships[1].Expression.Kind := poceNot;
  SetLength(Memberships[1].Expression.GroupIndices, 1);
  Memberships[1].Expression.GroupIndices[0] := 1;
end;

Desteklenen alt küme bilinçli olarak dardır. Politikalar ocmpAllOn ile ocmpAnyOn'a sınırlıdır; ocmpAnyOff ile ocmpAllOff reddedilir. İfadeler bir düzey derindir; oluşturulan gruplar üzerinde poceAll, poceAny ya da poceNot ve /Not tam olarak bir grup indeksi ister. poceAll /And olarak, poceAny /Or olarak ve poceNot /Not olarak serileşir; §8.11.2.2'nin tanımladığı üç operatör budur. 3.181.2 öncesi sürümler /All ile /Any yazıyordu; PDFium bunları gizli olarak değerlendirir, eski bir derlemeyle OCMD oluşturduysanız ifadeye güvenmek yerine onları yeniden üretin. Her üyelik ayrıca, bir ifade varken bile, MemberGroupIndices içinde en az bir girdi ister; çünkü o liste /OCs dizisi olur. Yeni sözlüklerin nesne numaraları Report.CreatedMembershipObjectNumbers'ta geri döner

Delphi'de PDFium Component'in oluşturduğu isteğe bağlı içerik üyelik sözlükleri: /AnyOn ya da /AllOn politikalı bir /OCs listesi grup durumlarını birlikte değerlendirir; bir grup üzerinde /Not gibi bir /VE görünürlük ifadesi ise içeriği And, Or ve Not mantığıyla bir düzey derinlikte geçite alır
Politikalar /OCs listesini değerlendirirken /VE ifadeleri grupları /And, /Or ve /Not ile birleştirir — her iki durumda da üyelik, referans verecek en az bir üye grup ister

OCMD'lerin üzerine inşa etmeden önce bilinmesi gereken iki davranış var. Birincisi, aynı çağrıdaki geçitli formlar yalnızca gruplara referans verir: TPdfOptionalContentCreateForm.GroupIndex Memberships'i değil Groups'u indeksler; bir form bu API üzerinden yeni oluşturulmuş bir OCMD ile geçite alınamaz. İkincisi, PDFium Component her yeni OCMD'yi katalog /OCProperties /OCGs dizisine ekler; /ON, /OFF ile /Order yalnızca gruba özgü kalır. ISO 32000-1 §8.11.4.2, /OCGs'yi grupların listesi olarak anlatır ama o kayıt olmadan TPdf.InspectOptionalContent üyeliği hiç saymaz. Yan etki, çıktının envanterinin her OCMD'yi Inventory.Groups içinde adsız bir ek girdi olarak listelemesidir; grupları konuma göre değil Name'e göre eşleştirin

Bileşen mevcut bir /OCProperties'i nasıl yamalar?

TPdf.SaveAsOptionalContentCreated, /OCProperties'i ya sıfırdan kurar ya da katalogdaki mevcudu genişletir ve genişletmeyi ekleyerek yapar; asla yeniden kurarak değil. Katmansız bir belge, ISO 32000-1 §8.11.4.3'ün dizdiği gibi /OCGs ile, /ON, /OFF ve /Order taşıyan bir varsayılan yapılandırma /D, herhangi bir grup isterse /Locked ile /RBGroups içeren yeni bir sözlük alır

Hâlihazırda katmanı olan bir belge için her hedef dizi, yeni referansları kapanış köşeli parantezinin hemen önüne eklenerek yamalanır. Bu titiz görünür ve bir nedeni vardır. Bir /Order dizisi, bir görüntüleyicinin gösterdiği katman ağacını şekillendiren iç içe diziler ile metin etiketleri tutabilir; diziyi düzleştirip yeniden kuran önceki bir yaklaşım o etiketleri kaybetti. Sonda ekleme, mevcut ağacı sağlam tutar ve yeni grupları arkasına yalnızca listeler. Kendi dolaylı nesnelerinde yaşayan diziler çözülür ve yerinde yamalanır

Delphi'de PDFium Component ile mevcut bir /OCProperties sözlüğünü genişletmek: yeni isteğe bağlı içerik grubu referansları, iç içe /Order dizileri ve metin etiketleri hayatta kalsın diye kapanış köşeli parantezinin hemen önüne eklenir; dolaylı bir /D varsayılan yapılandırması izlenir ve yerinde yamalanır
Referansları kapanış köşeli parantezinden önce eklemek mevcut /Order ağacını sağlam tutar; iç içe dizileri ve etiketleri düzleştiren bir yeniden kurulum bunu garantileyemez

v3.175.0'dan bu yana varsayılan yapılandırmanın kendisi de dolaylı olabilir. Bazı üreticiler /OCProperties << /OCGs [...] /D 12 0 R >> yazar; PDFium Component artık o referansı izler ve bozuk girdi hatasıyla başarısız olmak yerine hedef sözlüğü yamar. Dolaylı bir /XObject alt sözlüğü dahil, doğrudan ile dolaylı sayfa /Resources sözlükleri aynı biçimde işlenir

Yeni katmanların gerçekten anahtarlandığını kanıtlamak

/OCProperties'te var olan ama hiçbir şeyi geçite almayan bir katman üretmesi kolay, fark etmesi zor bir şeydir. Geliştirme sırasında form yazıcısının ilk derlemesi, kendi N 0 obj başlığı ile endobj kuyruğunu hâlihazırda taşıyan bir akış nesnesini ikinci bir başlığın içine sarmıştı. PDFium ikiye katlanmış nesneyi ayrıştıramadı ve hata bildirmek yerine Do operatörünü sessizce attı. Dosya açılıyordu, katman paneli doğru görünüyordu ve geçitli çizim hiç görünmüyordu. Onu ortaya çıkaran, katmanı anahtarlayıp render edilmiş pikselleri karşılaştıran bir sondaydı: fark yoktu, yani bir şeyler yanlıştı

Metot bu yüzden teslim etmeden önce doğrular. Çıktı, çapraz referans tablosunu kontrol eden katı bir yüklemeyle yeniden açılır ve istenen her grup, adını, ilk durumunu ve kilitli kaydını teyit etmek için InspectOptionalContent üzerinden geri okunur. Ancak ondan sonra çıktı akışınıza kopyalanır; dosya aşırı yüklemesi geçici bir dosya yazar ve hedefi atomik olarak değiştirir. Aynı anahtar kontrolünü kendiniz de çalıştırabilirsiniz: çıktıyı yeniden açın, grubu adıyla bulun ve ters durumla bir kopya kaydedin

function SaveImperialPreset(const SourceName, TargetName: string): Boolean;
var
  Pdf: TPdf;
  Inventory: TPdfOptionalContentInventory;
  Changes: TPdfOptionalContentStateChanges;
  Options: TPdfOptionalContentConfigureOptions;
  CfgReport: TPdfOptionalContentConfigureReport;
  I: Integer;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.FileName := SourceName;
    Pdf.Active := True;
    if not Pdf.InspectOptionalContent(Inventory) then
      raise Exception.Create(Inventory.ErrorMessage);

    for I := 0 to High(Inventory.Groups) do
      if (Inventory.Groups[I].Name = 'Dimensions (mm)') or
         (Inventory.Groups[I].Name = 'Dimensions (in)') then
      begin
        SetLength(Changes, Length(Changes) + 1);
        Changes[High(Changes)].GroupObjectNumber := Inventory.Groups[I].ObjectNumber;
        Changes[High(Changes)].Visible := Inventory.Groups[I].Name = 'Dimensions (in)';
      end;

    Options := TPdfOptionalContentConfigureOptions.Default;
    Result := Pdf.SaveAsOptionalContentConfigured(TargetName, Changes,
      Options, CfgReport);
  finally
    Pdf.Free;
  end;
end;

İki dosyanın da birinci sayfasını render edin; mavi ile kırmızı çizgiler yer değiştirmeli. Değişmiyorsa önce envanterde grubun EffectiveVisible değerine bakın; yapılandırma dizileri ile kullanım kuralları uygulandıktan sonra PDFium'un gerçekte render edeceği durumu yansıtır

Planında göze almanız gereken sınırlar ve tuzaklar

TPdf.SaveAsOptionalContentCreated sınırlı bir profildir ve sürprizlerin çoğu onu genel bir katman editörü saymaktan gelir. Aşağıdaki kısıtlar baştan uygulanır; ihlal eden bir çağrı Report.ErrorMessage ayarlı olarak başarısız olur ve hiçbir çıktı yazmaz:

  • En az bir grup gerekir; gruplar, üyelikler ve formların her biri 4096 girdiyle sınırlıdır; grup adları eşsiz ve 1 ile 255 karakter uzunluğunda olmalıdır ve her form en fazla 16 MiB içerik baytı kabul eder
  • Şifreli belgeler reddedilir ve imzalı bir belge AllowSignedDocument = True ister; çıktı eksiksiz bir yeniden yazımdır, mevcut imzalar artık doğrulanmaz
  • Her Forms girdisi kendi Form XObject'i olur; aynı katmanı 200 sayfaya koymak içeriği bir kez değil 200 kez saklar
  • Yeni Do akışı mevcut sayfa içeriğinden sonra eklenir; önceki akışların bıraktığı hangi grafik durumdaysa öyle çalışır. Düzgün kurulmuş sayfalar durumlarını geri yükler ama dengesiz q/Q'lu bir sayfa katmanınızı kaydırabilir
  • Mevcut sayfa içeriği asla bir katmana taşınmaz; Forms'ta verdiğiniz çizim geçite alınır

Tekrar noktası, büyük belgeler için tartılacak olandır. Katman ağır gidiyor ve her sayfada yineliyorsa Form XObject'lere dayalı yeniden kullanılabilir sayfa damgalarındaki paylaşımlı nesne yaklaşımı dosyada tek kopya tutar; bedeli, katman geçidini kendinizin yönetmesidir

SSS

PDFium Component, mevcut sayfa içeriğini yeni bir katmana koyabilir mi?

Hayır. TPdf.SaveAsOptionalContentCreated yalnızca Forms'ta verdiğiniz içeriği geçite alır; mevcut operatörleri işaretli içeriğe sarmaz ya da bir XObject'e taşımaz. Mevcut grafiği katmanlamak için onu kendi kendine yeten form içeriği olarak yeniden üretin ve özgün çizimi ayrı bir adımda kaldırın

Katman kapalıyken geçitli açıklamalarım neden hâlâ görünür?

PDFium, render yolunda açıklama sözlüklerinin /OC girdisini kontrol etmez; açıklama düzeyi geçit PDFium tabanlı görüntüleyicilerde etkisizdir. PDFium Component'in katman içeriğini Form XObject'ler üzerinden eklemesinin nedeni budur; PDFium onları geçite alır

Yeni katmanları sonra anahtarlarken hangi nesne numaralarını kullanmalıyım?

Report.CreatedGroupObjectNumbers, az önce yazılan dosyadaki numaraları tutar. O dosyayı yeniden açıp yapılandırdığınızda ObjectNumber'ı TPdf.InspectOptionalContent'ten alın ve Name'e göre eşleştirin; sonraki bir kaydetme nesneleri yeniden numaralandırsa bile bu doğru kalır

PDFium Component, SaveAsOptionalContentCreated, SaveAsOptionalContentConfigured ile InspectOptionalContent'i, PDFium renderer'ı üzerine kurulu tek bir isteğe bağlı içerik araç seti olarak Delphi ve C++Builder için sunar. Ayrıntılar ve indirmeler PDFium Component ürün sayfasında