Teknik Makale

Delphi'de PDF Ad Ağacı ve Numara Ağacı Düzenleme

Bir yaprak dizisini yamalayıp tek bir ad ekle, girdi yine de eksik okunabilir. PDFium Component buna TPdfTreeEditor ile yanıt verir; ISO 32000 ad ağaçları ve numara ağaçları için genel bir editör: ağacı sıkı doğrulamayla yükler, girdileri bellek içinde düzenler, doğru /Limits ile dengeli bir düğüm kümesini yeniden kurar ve doğrulanmış bir artımlı revizyon yazar

Bu unit'i motive eden hata, yalnızca bayt okuyarak teşhis edebileceğin türden. Bir araç, bir yaprak /Names dizisine anahtar ve değer ekler, yeni bir revizyon yazar ve her hex dump anahtarın dosyada olduğunu doğrular. Acrobat yine de ek dosyanın var olmadığını söyler. Naif anlamda hiçbir şey bozuk değildir. Ağaç, arama algoritmasının inebileceği bir ağaç olmaktan çıkmıştır sadece

Yamalı bir yaprak dizisi aramayı neden bozar?

Çünkü arama yaprakların taranması değil, /Limits üzerinden ikili bir iniştir. ISO 32000-1 §7.9.6 ad ağacını şöyle tanımlar: /Kids değerleri /Names tutan yapraklara inen bir kök; bu dizi dönüşümlü anahtar ve değer listesidir, anahtarlar tüm ağaç boyunca bayt karşılaştırmasıyla sıralıdır ve kök olmayan her düğüm kendi en küçük ve en büyük anahtarı olarak /Limits bildirir. Uyumlu bir okuyucu istenen anahtarı her kid aralığıyla karşılaştırır ve tam olarak tek bir çocuk açar. Bir yaprağa anahtar ekleyip eski /Limits değerine dokunmazsan iniş, onu gerçekten tutan düğümün yanından dümdüz geçer

PDFium Component'te PDF ad ağacı aramasının şeması: okuyucu kid /Limits aralıklarında bayt karşılaştırmasıyla ikili iner; bayat /Limits değeri hâlâ (f) ile biten bir yaprağa eklenen invoice-2026-09 gibi bir anahtar asla açılmaz, iniş bunun yerine (g) (z) kid'ini seçer ve ek dosya dosyada olmasına rağmen eksik okunur
Arama, istenen anahtarı her kid aralığıyla karşılaştırarak tam olarak tek bir çocuk açar, bu yüzden /Limits tazelemeden anahtar kazanan bir yaprağın yanından dümdüz inilir

Numara ağaçları (§7.9.7) /Nums ve tamsayı anahtarlarla aynı biçime sahiptir ve gerçek trafik taşırlar: §12.4.2'deki /PageLabels bir numara ağacıdır ve §7.7.4'ün katalog /Names kategorileri sana /Dests, /EmbeddedFiles ve /JavaScript değerlerini ad ağacı olarak verir. Bu yüzden dört değişmez bir arada tutmak zorundadır, teker teker değil. Anahtarlar ağaç sırasında kesin artan olmalı, her seviye kendi gerçek anahtar aralığıyla eşleşen limitler yayımlamalı, kardeş aralıklar çakışmamalı ve kök nesne numarası ile generasyon tam olarak oldukları yerde kalmalıdır; çünkü katalog ve diğer her referans veren, dolaylı referansla o kökü gösterir. Delphi'de PDF ekleriyle çalışmak üzerine notumuz aynı yapının tüketici tarafını gösterir

TPdfTreeEditor neyi yüklemeyi reddeder?

Beklediğinden fazlasını ve bilerek. TPdfTreeEditor.TryLoad düğüm grafiğini TPdfSparseDictionaryReader üzerinden yürür, her çocuğu referansın adlandırdığı tam generasyonda ReadDictionary ile çözer; hangi revizyonun aktif olduğunu kabul etmek yerine. İyi biçimli olduğu kanıtlanamayan bir ağaç, tek bir düzenlemeye izin verilmeden önce reddedilir; bu da bozuk bir girdinin makul görünümlü bozuk bir çıktıya dönüşmesini engeller

  • Döngüler ve tekrarlanan referanslar, ikisi de ptviCycle olarak raporlanır; böylece zinciri yukarı doğru gösteren bir /Kids girdisi özyinelemek yerine sonlanır
  • Karışık düğüm biçimleri: /Kids ile birlikte /Names ya da /Nums taşıyan bir sözlük ptviInvalidNodeShape olarak raporlanır; boş bir kök olmayan yaprak da öyle
  • Sıralanmamış ya da yinelenen anahtarlar, ptviUnsortedKey ve ptviDuplicateKey olarak raporlanır; tek bir yaprak içinde değil tüm gezinme boyunca kontrol edilir
  • Gerçekte mevcut anahtarlarla çelişen limitler, ptviInvalidLimits olarak raporlanır
  • Tam olarak tek bir eksiksiz PDF nesnesi olmayan ham bir değer, ptviInvalidValue olarak raporlanır
  • Bütçe aşımları: ptviDepthLimit, ptviNodeLimit, ptviEntryLimit ve ptviDeadlineExceeded; hem TPdfTreeEditOptions hem merkezi parser bütçesiyle sınırlanır

TPdfTreeEditOptions.Default bu ağaç-yerel sınırları 1.000.000 girdi, 100.000 düğüm, derinlik 64, yaprak başına 64 girdi ve düğüm başına 32 kid olarak ayarlar. Yaprak ve kid rakamları yalnızca koruma değildir; bunlar ağaç yeniden kurulurken daha sonra kullanılan fan-out değerleridir, bu yüzden kayıt ikisinin de en az iki olmasını doğrular

uses
  FPdfCompress;

var
  Options: TPdfTreeEditOptions;
  RootRef: TPdfTreeObjectReference;
  Editor: TPdfTreeEditor;
  Report: TPdfTreeValidationReport;
begin
  Options := TPdfTreeEditOptions.Default;
  RootRef.ObjectNumber := EmbeddedFilesRootObject;   { katalog /Names /EmbeddedFiles }
  RootRef.Generation := EmbeddedFilesRootGeneration;
  if not TPdfTreeEditor.TryLoad(Source, ptkName, RootRef, Options,
    Editor, Report) then
  begin
    Writeln('tree rejected: ', Report.ErrorMessage);
    Exit;
  end;
  try
    { değer, tek bir eksiksiz PDF nesnesinin ham baytlarıdır }
    Editor.UpsertName('invoice-2026-09',
      '<< /Type /Filespec /F (invoice.pdf) /EF << /F 42 0 R >> >>');
    Editor.RemoveName('draft-2025-11');
  finally
    { Editor.Free, aşağıdaki kayıttan sonra }
  end;
end;

Girdi değerleri ham PDF nesne baytları olarak kalır

Bir girdi değeri olduğu gibi saklanır ve bu, editörü senin üretmediğin dosyalara doğrultmayı güvenli kılan tek tasarım kararıdır. TPdfTreeEntry, NameKey, NumberKey ve RawValue tutar; RawValue tek bir eksiksiz PDF nesnesinin ham bayt dizisidir: bir sözlük, bir dizi, bir string, bir stream referansı ya da çıplak bir dolaylı referans. Hiçbir şey bir Delphi variant'ına, TStrings ya da türlenmiş bir kayda çözülmez. Kazanç, unit'in hiçbir şey bilmediği değerlerin kayıpsız round-trip edilmesidir. Satıcı extension'ı taşıyan bir /Filespec, alışılmadık bir fit türü olan bir hedef dizisi, özel anahtarlı bir /JavaScript eylem sözlüğü — hepsi yeniden kurulumdan bayt bayt sağ çıkar, çünkü editörün içerikleri hakkında hiçbir zaman bir fikri olmadı. Ayrıca değişiklik API'sini kanıtlanabilir bir şeye küçültür: UpsertName ve UpsertNumber yalnızca verilen değerin tam olarak tek bir eksiksiz nesne olarak ayrıştığını doğrulamak zorundadır; RemoveName, RemoveNumber ve Clear yalnızca girdi listesine dokunur

Kök yeniden numaralandırılmadan dengeli bir ağaç nasıl yeniden kurulur?

Tek bir sabit noktayla iki geçişte. BuildBalancedObjects önce sıralı girdi listesini en fazla MaxLeafEntries içeren yapraklara böler, sonra her seviyeyi en fazla MaxKidsPerNode içeren ebeveynlere katlar ve tek bir düğüm kalana kadar tekrarlar. Limitler aşağıdan yukarı hesaplanır; bir yaprak ilk ve son anahtarını alır, bir iç düğüm ilk çocuğunun alt limitini ve son çocuğunun üst limitini alır, böylece çakışan kardeş aralıkları yapım gereği oluşamaz

Sabit nokta kimliktir. Nihai kök her zaman ağacın yüklendiği nesne numarasını ve generasyonu alır; böylece katalog referansı geçerli kalır ve belgedeki başka hiçbir şeyin yeniden yazılması gerekmez. Diğer her düğüm, yalnızca düğüm türü eşleştiğinde ve tam anahtar aralığı eşleştiğinde önceden yüklenmiş bir düğümün referansını yeniden kullanır; aksi halde aktif maksimum nesne numarası artı birden tahsis edilen taze bir numara alır. Bu kombinasyon değişmemiş alt ağaçların mevcut nesnelerini göstermesini sağlarken, içeriği taşınan herhangi bir düğümün sessizce yeniden yorumlanmak yerine yeni bir nesne olarak yazılmasını garanti eder. Aynı referans-yeniden kullanım disiplini, kardeş unit içindeki outline editörünü ve sayfa yeniden eşlemesini de yönetir

PDFium Component'teki TPdfTreeEditor yeniden kurulumunun şeması: BuildBalancedObjects sıralı girdi listesini en fazla MaxLeafEntries anahtar içeren yapraklara böler, her seviyeyi en fazla MaxKidsPerNode içeren ebeveynlere katlar, /Limits değerlerini ilk ve son anahtarlardan aşağıdan yukarı hesaplar ve bitmiş köke ağacın yüklendiği tam nesne numarasını ve generasyonu verir
Yapraklar limitlerini ilk ve son anahtarlarından alır, iç düğümler en dıştaki çocuklarından alır ve nihai kök, ağacın yüklendiği nesne numarasını ve generasyonu korur
var
  Serialized: TPdfTreeSerializedObjects;
  I: Integer;
begin
  { FirstNewObjectNumber kaynak revizyonundan gelir }
  if Editor.BuildBalancedObjects(MaximumObjectNumber + 1,
    Serialized, Report) then
  begin
    Writeln('nodes ', Report.NodeCount, ' depth ', Report.MaximumDepth);
    for I := 0 to High(Serialized) do
      Writeln(Serialized[I].Reference.ObjectNumber, ' ',
        Serialized[I].Reference.Generation, ' obj');
  end
  else
    for I := 0 to High(Report.Issues) do
      Writeln('issue ', Ord(Report.Issues[I].Kind), ' at obj ',
        Report.Issues[I].ObjectNumber, ': ', Report.Issues[I].Message);
end;

Revizyonu yaz, sonra sonucu geri oku

Bir kayıt, çıktı yeniden açılıp karşılaştırılmadan tamamlanmış sayılmaz. SaveIncremental kaynak revizyon bilgilerini okur, belge kataloğunun kendisi olan bir kökü reddeder, dengeli düğüm kümesini artı katalog nesnesini serileştirir ve her şeyi AppendPdfSparseUpdate'a devreder; bu da §7.5.6'ya göre yeni bir revizyon eklerken mevcut /Info referansını ve belge /ID dizisini korur

Sonra kendi yazarlarına kopyalamaya değer kısım gelir. Editör TPdfTreeEditor.TryLoad'ı bu kez hedef stream'e karşı, aynı kök referansı ve aynı seçeneklerle yeniden çağırır ve yeniden yüklenen girdi sayısını, her anahtarı ve RawValue değerini yazmayı amaçladığı şeyle karşılaştırır. Yeniden yükleme başarısız olursa ya da herhangi bir karşılaştırma farklıysa, hedef stream geri verilmek yerine sıfırlanır; böylece kısmen yazılmış bir revizyon asla başarı olarak elden çıkarılmaz. Doğrulama, çıktıyı sparse lazy PDF nesne indeksi üzerine yazımızda anlatılan aynı sınırlı sparse yol üzerinden okur; büyük dosyalarda kontrolü karşılanabilir tutan da budur

PDFium Component'teki doğrulanmış artımlı kaydın şeması: SaveIncremental dengeli düğüm kümesini serileştirir, AppendPdfSparseUpdate /Info ve belge /ID dizisini koruyarak yeni bir revizyon ekler, sonra TPdfTreeEditor.TryLoad hedef stream'i yeniden okur ve girdi sayısını, her anahtarı ve RawValue değerini karşılaştırır; yarı yazılmış bir revizyonu geri vermek yerine stream'i sıfırlar
Kayıt, çıktı aynı sınırlı yol üzerinden yeniden açılıp girdi girdi karşılaştırılana kadar tamamlanmış sayılmaz; başarısız bir karşılaştırma hedef stream'i sıfırlar
var
  Dest: TFileStream;
begin
  Dest := TFileStream.Create(OutputName, fmCreate);
  try
    if Editor.SaveIncremental(Source, Dest, Report) then
      Writeln('wrote ', Report.NodeCount, ' nodes, ',
        Report.EntryCount, ' entries')
    else
      { hedef zaten sıfırlandı, yarı yazılmış hiçbir şey tutulmadı }
      Writeln('save refused: ', Report.ErrorMessage);
  finally
    Dest.Free;
    Editor.Free;
  end;
end;

Şifreli kaynaklar reddedilir ve bu dürüstlüktür

Güncel uygulama şifreli bir kaynağı doğrudan reddeder. Neden savunmacı değil spesifiktir: bir ağaç yeniden kurulumu yeni string nesneleri getirir ve yeni stream referansları getirebilir; §7.6 bunların belge anahtarı ve sahip nesne numarası ile generasyonuyla nesne başına şifrelenmesini gerektirir. /Encrypt trailer'ını kopyalayıp düz metin stringler eklemek, açılan ve yeni anahtarlarının tam olduğu yerde çöp olarak okunan bir dosya üretir; açık bir reddedişten daha kötüdür. Açıkça söylemeye değer iki sınır daha var. Editör kaynak stream'in bir anlık görüntüsü üzerinde çalışır, bu yüzden editör onun üzerinde canlıyken kaynak değiştirilmemelidir; ayrıca belge kataloğunu bir ağaç kökü olarak ele almayı reddeder, çünkü katalog ağaç düğümünün kendisi değil, içinde ağaçlar bulunan bir sözlüktür. /EmbeddedFiles, /Dests, /JavaScript ya da /PageLabels yazan Delphi, C++Builder ya da Lazarus kodu bakıyorsan ve yaprak dizilerini elle yamalıyorsan, ağaç editörü ve doğrulanmış artımlı kaydı PDFiumPas Delphi PDFium component ile birlikte gelir