Teknik Makale

Delphi'de /ParentTree ile /StructParents'i Senkron Tutmak

PDFium Component, mevcut PDF içeriğini Delphi tarafından FPdfDocumentStructure.pas içindeki EditPdfDocumentStructure ile etiketler. psekAddMarkedContent ile psekAddObjectReference işlemleri, bir yapı öğesinin /K'sine bir MCR ya da OBJR eklemekten fazlasını yapar: /StructParents ya da /StructParent anahtarını da ayırır ve ISO 32000-1 §14.7.4.4'ün gerektirdiği ters indeks olan eşleşen /ParentTree girdisini yazar

İkinci yarı, sonradan etiketlemenin genellikle yanıldığı yerdir. Bir BDC sarmalayıcısı ile bir MCR eklemek kolaydır ve yapı ağacını tepeden gezinen bir görüntüleyici yeni içeriği memnuniyetle bulur. Yardımcı teknoloji ise çoğu zaman öteki yönden gider: sayfadaki bir içerik parçasından başlar ve onu hangi yapı öğesinin sahiplendiğini sorar. Sayfada bir /StructParents anahtarı yoksa ya da /ParentTree'de yeni MCID için bir girdi yoksa o arama başarısız olur ve denetleyiciler bunu eksik ya da tutarsız bir ebeveyn ağacı olarak raporlar. Dosya etiketli görünür ve hâlâ çalışmaz

/ParentTree gerçekte neyi eşler?

/ParentTree, tamsayı anahtarları yapı öğelerine eşleyen bir sayı ağacıdır ve anahtarlar ağaçtan değil içerikten gelir. ISO 32000-1 §14.7.4.4, işaretli içerik barındıran her sayfaya ya da Form XObject'e bir /StructParents tamsayısı verir; o anahtarın altındaki ebeveyn ağacı değeri MCID ile indekslenen bir dizidir: dizinin 3. girdisi <</MCID 3>>'e sahip olan yapı öğesidir. Bütün bir içerik öğesi olarak kullanılan bir açıklama ya da XObject ise tek bir /StructParent anahtarı alır ve onun ebeveyn ağacı değeri bir dizi değil yapı öğesinin kendisidir

Delphi'de PDFium Component'in yazdığı PDF/UA ebeveyn ağacı ters indeksi: MCID 3 taşıyan bir içerik işareti, sayfanın /StructParents anahtarı 17 üzerinden /ParentTree sayı ağacına çözülür; ağacın 3. dizi girdisi, işaretli içeriği sahiplenen yapı öğesidir
Etiketli PDF için okuma düzeni önemlidir: yardımcı teknoloji sayfadaki MCID'den /StructParents üzerinden /ParentTree dizisine yürür; eksik bir anahtar ya da girdi, yapı ağacının kendisi sağlam görünse de o aramayı bozar

Kapsayıcı biçim, §7.9.7'deki genel sayı ağacıdır. Bir yaprak, anahtar sırasına göre artan sırada dizilmiş anahtar-değer çiftlerinden oluşan düz bir dizi olan /Nums'u tutar. Kök altındaki ara ve yaprak düğümler, kapsadıkları en küçük ile en büyük anahtarı taşıyan /Limits taşır; kökün kendisinde ise /Limits olmamalıdır. Üstüne bir de, yapı ağacı kökü bir yazıcının vereceği sonraki anahtar olan /ParentTreeNextKey'i (§14.7.2) taşıyabilir. Yeni içerik etiketleyen her düzenleme bu yüzden uyuşması gereken dört nesneye dokunur: içerik akışı, sayfa ya da form sözlüğü, ebeveyn ağacı yaprağı ve yapı ağacı kökü

İleri yönün, §14.7.4.2 ile §14.7.4.3'te konan kendi kuralları vardır. Sayfanın kendi içerik akışındaki içerik için bir işaretli içerik referansı (MCR) yalnızca /Pg ile /MCID ister. İşaretli içerik bir Form XObject'in içindeyse MCR ayrıca o akışı adlandıran bir /Stm ister; çünkü MCID'ler içerik akışı başına numaralanır. Bir nesne referansı (OBJR), açıklamayı ya da XObject'i /Obj ile, sayfasını /Pg ile adlandırır

Bir içerik akışını EditPdfDocumentStructure ile etiketlemek

EditPdfDocumentStructure, anlık görüntü korumalı bir toplu düzenleyicidir. Önce bir TPdfDocumentStructureSnapshot almak için ReadPdfDocumentStructure çağırır, sonra onun SourceSha256 değerini bir TPdfStructureEditOperation kaydı dizisiyle birlikte geri verirsiniz. Kaynak baytları arada değiştiyse düzenleme, çıktısız reddedilir. Mevcut öğelere NodeID içindeki nesne numaralarıyla hitap edilir ve 0, yapı ağacı kökünü gösterir

uses
  System.SysUtils, System.Classes, System.IOUtils, FPdfDocumentStructure;

function TagContentStream(const FileName, OutName: string;
  ElementID, StreamObj, StreamGen: Integer): Boolean;
var
  Source: TMemoryStream;
  Options: TPdfStructureEditOptions;
  Snapshot: TPdfDocumentStructureSnapshot;
  Ops: TPdfStructureEditOperations;
  Report: TPdfStructureEditReport;
  Output: TBytes;
begin
  Source := TMemoryStream.Create;
  try
    Source.LoadFromFile(FileName);
    Options := TPdfStructureEditOptions.Default;
    if not ReadPdfDocumentStructure(Source, Options, Snapshot, Report) then
      raise Exception.Create(Report.ErrorMessage);

    SetLength(Ops, 1);
    Ops[0] := Default(TPdfStructureEditOperation);
    Ops[0].Kind := psekAddMarkedContent;
    Ops[0].NodeID := ElementID;            // mevcut öğe; nesne numarasıyla
    Ops[0].TargetObjectNumber := StreamObj; // sayfa içerik akışı ya da sayfa Form XObject'i
    Ops[0].TargetGeneration := StreamGen;

    Result := EditPdfDocumentStructure(Source, Snapshot.SourceSha256,
      Ops, Options, Output, Report);
    if not Result then
      raise Exception.Create(Report.ErrorMessage);
    TFile.WriteAllBytes(OutName, Output);
  finally
    Source.Free;
  end;
end;

O tek işlem için EditPdfDocumentStructure, hedef akışı /P <</MCID n>> BDC ... EMC ile sarar, MCR'ı öğenin /K'sine ekler, sayfaya (ya da forma) /StructParents anahtarı yoksa bir tane verir ve o anahtarın ebeveyn ağacı dizisine öğe n'i yazar. Sonuç, özgün baytlardan sonra artımlı güncelleme olarak yazılır ve Report.CreatedMarkedContents kaç MCR kurulduğunu söyler. Akış hâlihazırda dengeli BDC/EMC çiftleri içeriyorsa yalnızca son üst düzey EMC'den sonraki etiketsiz kuyruk sarılır; aksi hâlde bütün akış sarılır

Yeni MCID'ler ve ebeveyn ağacı anahtarları nasıl seçilir?

Yeni bir MCID, geçerli maksimumun bir fazlasıdır; yeni bir ebeveyn ağacı anahtarı ise Max(MaxKey + 1, ParentTreeNextKey)'dir. MCID için EditPdfDocumentStructure, o sayfa (ya da o Form XObject) için hâlihazırda kaydedilmiş MCR'lar arasında ve akışın kendi özellik listelerinde bulduğu her /MCID'de maksimumu alır. İkisini de saymak önemlidir: bir yapı ağacı, içerik işareti hâlâ dururken bir MCR kaybetmiş olabilir ve o numarayı yeniden kullanmak iki öğenin aynı içeriği sahiplenmesine yol açar

Anahtar mantığı aynı savunmacı fikri izler. Sahibinde bir /StructParents değeri zaten varsa mevcut dizi MCID indeksinde uzatılır; boşlukları null doldurur ve yeni anahtar ayrılmaz. Yoksa bileşen her /Nums yaprağını en yüksek anahtar için tarar, onu /ParentTreeNextKey'le karşılaştırır ve büyüğü alır. Kullanımdaki anahtarların altında kalan bayat bir /ParentTreeNextKey ile son girdinin ötesindeki anahtarları rezerve eden bayat, ikisi de işlenir; iki değerin büyüğü daima boştadır. Ayırmadan sonra /ParentTreeNextKey, yeni anahtarın bir fazlasına ilerletilir

Delphi için PDFium Component yapı düzenleyicisinde MCID ile ebeveyn ağacı anahtarı ayrımı: yeni MCID, MCR'larda ve akış özellik listelerinde hâlihazırda kayıtlı maksimumun bir üzeridir; anahtar ise en yüksek /Nums anahtarının bir fazlası ile /ParentTreeNextKey'in büyüğüdür ve dizi boşluklarını null doldurur
EditPdfDocumentStructure, yeni MCID'yi hem MCR'lardan hem akış içi işaretlerden türetir ve ebeveyn ağacı anahtarını savunmacı biçimde ayırır; bayat bir /ParentTreeNextKey ya da yeniden kullanılan bir MCID, iki öğenin aynı içeriği sahiplenmesini sağlayamaz

Yeni çift, /Nums'e eklenmek yerine artan sırada yerleştirilir; çünkü sayı ağacı aramaları o düzene dayanır. Yaprak /Limits taşıyorsa sınırlar yeniden yazılan diziden hesaplanır. Yapı ağacı kökünde hiçbir /ParentTree yoksa — yalnızca ileri ağacı yazan araçların etiketlediği dosyalarda yaygındır — EditPdfDocumentStructure, ilk girdiyi eklemeden önce yeni bir dolaylı << /Nums [] >> nesnesi kurar ve onu /StructTreeRoot'dan bağlar

Form XObject'ler, /StmOwn ve nesne referansları

Bir Form XObject'in içindeki işaretli içerik yalnızca o form tam olarak bir sayfa tarafından kullanılıyorken desteklenir. EditPdfDocumentStructure'in bir form için yazdığı MCR, form akışını adlandıran /Stm ile sayfayı adlandıran /StmOwn'e ek olarak /Pg taşır ve /StructParents anahtarı sayfanın yerine form sözlüğüne gider. Üç sayfa tarafından çizilen bir formun üç olası sahibi vardır; tek bir /StmOwn ikisi için yanlış olurdu. Bileşen, tahmin etmek yerine düzenlemeyi bir hatayla reddeder

Delphi'de PDFium Component ile Form XObject içeriğini etiketlemede tek-sahip kuralı: tek sayfa tarafından kullanılan bir form üzerindeki MCR, /Pg, /Stm ve /StmOwn taşır ve /StructParents form sözlüğüne gider; üç sayfa tarafından çizilen bir form ise /StmOwn'u belirsizleştirir ve düzenleme reddedilir
/StmOwn anahtarı yalnızca bir sayfa forma sahipken anlam taşır; paylaşılan formlar ile paylaşılan açıklamalar bu yüzden tahmin edilebilir bir sahibiyle etiketlenmek yerine reddedilir

Aynı eşsizlik kuralı psekAddObjectReference için de geçerlidir. Hedef, tam olarak bir sayfanın /Annots'unda görünen bir açıklama ya da tam olarak bir sayfanın kaynaklarında görünen bir Form ya da Image XObject olmalıdır; başka her şey reddedilir. OBJR /Obj ile /Pg'yi alır; hedef bir /StructParent anahtarı alır (varsa yeniden kullanılır) ve ebeveyn ağacı değeri öğeye düz bir referanstır. Görsel olarak etiketlenip ağaca hiç bağlanmamış bir bağlantı açıklaması ya da şekil görseli için olağan düzeltme budur

// Etiketsiz bir görseli yeni bir Figure öğesine sar, sonra iki yolla bağla:
// görsel XObject'i için bir OBJR olarak ve sayfada işaretli içerik olarak
SetLength(Ops, 3);
Ops[0] := Default(TPdfStructureEditOperation);
Ops[0].Kind := psekInsertContainer;
Ops[0].NodeID := -1;              // geçici kimlik; negatif ve eşsiz olmalı
Ops[0].ParentNodeID := SectID;    // mevcut Sect öğesi
Ops[0].Value := 'Figure';         // standart rol ya da bir RoleMap girdisi

Ops[1] := Default(TPdfStructureEditOperation);
Ops[1].Kind := psekAddObjectReference;
Ops[1].NodeID := -1;              // yukarıda oluşturulan kapsayıcıya atıfta bulunur
Ops[1].TargetObjectNumber := ImageObj;
Ops[1].TargetGeneration := 0;

Ops[2] := Default(TPdfStructureEditOperation);
Ops[2].Kind := psekAddMarkedContent;
Ops[2].NodeID := -1;
Ops[2].TargetObjectNumber := ContentObj;
Ops[2].TargetGeneration := 0;

if EditPdfDocumentStructure(Source, Snapshot.SourceSha256, Ops, Options,
  Output, Report) then
  Writeln(Format('Figure is object %d, %d OBJR, %d MCR', [
    Report.InsertedNodes[0].ObjectNumber,
    Report.CreatedObjectReferences, Report.CreatedMarkedContents]));

Negatif NodeID değerleri aynı toplu işte daha önce oluşturulan kapsayıcıları adlandırır ve Report.InsertedNodes, istenen her kimliği aldığı nesne numarasına eşler. psekInsertContainer, P, H1'den H6'ya, Figure, Table ya da LI gibi standart PDF 1.7, PDF 2.0 ve PDF/UA rollerini ve /RoleMap'te hâlihazırda eşlenmiş özel rolleri kabul eder. İki içerik işleminde BeforeNodeID ayarlamak, yeni MCR'ı ya da OBJR'ı sona değil mevcut bir çocuğun önüne koyar

Aynı akışı bir toplu işte iki kez düzenlemek neden özen ister?

Bir toplu iş aynı nesneye defalarca çarpabilir ve sonraki her işlem öncekilerin sonucunu görmek zorundadır. Aynı sayfaya karşı yapılan iki psekAddMarkedContent işlemi de onun içerik akışını yeniden yazar ve ikisi de aynı ebeveyn ağacı yaprağını günceller. Her düzenlemeyi ayrı eklenmiş bir nesne olarak sahneye koyan bir uygulama, güncellemede 12 numaralı nesnenin iki sürümünü yazar. Okuyucu hangisini tutarsa ilk BDC sarmalayıcısını ya da ilk /Nums çiftini kaybedersiniz ve ikinci MCID, bayat baytlardan hesaplandığı için birincisiyle çakışabilir

EditPdfDocumentStructure, sahnelenmiş güncellemeleri (ObjectNumber, Generation) ile anahtarlar, mevcut girdiyi yerinde ezer ve aynı anda nesnenin bellek içi kopyasını tazeler. İkinci işlem bu yüzden hâlihazırda sarılmış akışı ayrıştırır, MCID 0'ı görür, MCID 1 ayırır ve aynı ebeveyn ağacı dizisini uzatır. Tek nesne, tek nihai sürüm. Herhangi bir şey döndürülmeden önce çıktı geri okunur ve bellek içi modelle karşılaştırılır: öğe sayısı, içerik öğesi sayıları ve korunan her MCR ya da OBJR eşleşmelidir; aksi hâlde çağrı çıktısız başarısız olur

Sıkıştırılmış akışlar aynı yolu izler. /Filter /FlateDecode'li (ya da tek öğeli [/FlateDecode] dizili) ve /DecodeParms tahmincisi olmayan bir içerik akışı, 32 MiB ya da hangisi küçükse MaxOutputBytes ile sınırlı bir sınırlı çözücüyle açılır, sarılır ve yeni bir /Length ile /FlateDecode'e geri sıkıştırılır. Tavanın ötesine açılacak bir akış kırpılmak yerine reddedilir; güvenilmeyen kaynaklardan gelen dosyalar için önemli olan budur

Bir PDF/UA onarım turundan önce bilinmeye değer sınırlar

EditPdfDocumentStructure tasarımı gereği tutucudur ve sınırlarının çoğu sessizce bozuk bir dosya değil net bir Report.ErrorMessage olarak ortaya çıkar. Toplu bir onarım planlarken bunları akılda tutun:

  • Yalnızca süzülmemiş ve düz FlateDecode'li akışlar sarılabilir; öteki filtreler ya da tahmincili Flate reddedilir
  • Bir içerik akışı tam olarak bir sayfanın /Contents'ine ait olmalıdır ve Form XObject'ler, açıklamalar ile XObject'ler tam olarak bir sayfa tarafından kullanılmalıdır
  • Sarmalayıcı daima /P etiketini kullanır; anlamsal rol, yapı öğesinin /S'inden gelir, BDC etiket adından değil
  • Ebeveyn ağacı eşitlemesi düz bir /Nums kökünü ya da bir düzey /Kids yaprağını okur; daha derin ara düğümlere inilmez, böyle dosyaları düzenledikten sonra yeniden doğrulayın
  • İmza ya da imza alanı içeren dosyalar tümüyle reddedilir; o yüzden yapı onarımından sonra imzalayın, önce değil

Doğrulama adımı bir onarım hattı için isteğe bağlı değildir. Toplu işten önce ve sonra PDFium Component'te PDF/UA yapı ağacı doğrulamasını çalıştırın ve hangi sayfa nesnelerinin yeni işaretlerin içine düştüğünü teyit etmeniz gerektiğinde BDC etiketleri ile MCID'leri okumak için sayfa nesnesi işaretli içerik API'sini kullanın. Temiz bir ebeveyn ağacı, aramaların çözüldüğünü söyler; okuma düzeninin ya da alternatif metnin doğru olduğunu söylemez

SSS

EditPdfDocumentStructure bütün /ParentTree'yi yeniden kurar mı?

Hayır. EditPdfDocumentStructure yalnızca geçerli toplu işte etiketlediği içeriğin girdilerini günceller; mevcut /StructParents anahtarlarını yeniden kullanır ve gerektiğinde yenilerini ayırır, böylece mevcut ebeveyn ağacı girdileri dokunulmamış kalır

PDF'te yapı ağacı var ama /ParentTree yoksa ne olur?

Bileşen, dolaylı bir << /Nums [] >> sayı ağacı kurar, onu /StructTreeRoot'dan bağlar ve etiketlenen içerik için ilk anahtarı ekler

Birkaç sayfa tarafından paylaşılan bir Form XObject'teki içeriği etiketleyebilir miyim?

Bu işlemle hayır. Paylaşılan bir form /StmOwn'u belirsizleştirir; psekAddMarkedContent formun tam olarak bir sayfa tarafından referans verilmesini ister ve aksi hâlde hata raporlar

Yapı ağacı okuma ve düzenleme, PDF/UA doğrulaması ile işaretli içerik erişimi Delphi ve C++Builder için PDFium Component'in parçasıdır; bileşen Delphi, C++Builder ve Lazarus projeleri için PDFium'u sarmalar