Teknik Makale

Tekrarlanabilir derlemeler için deterministik PDF kimliği

Aynı şablon ve veriden bir faturayı iki kez üretin, sonra iki PDF'yi diff'leyin. İki yer farklıdır: trailer /ID dizisinin hex dizgeleri ve belge bilgi sözlüğündeki ModDate dizgesi — ikisi de saatten alınır. losLab PDF Library'de SetDeterministicDocumentID(1) artı sabit bir ModDate, çalıştırma başına XMP UUID'leri ya da ekleri olmayan bir belge için dosyaları bayt özdeş yapar

Bariz ilk deneme tarihleri sabitlemek ve yeniden koşmaktır. CreationDate ile ModDate artık uyuşur ve hash'ler yine farklıdır; bu kez yalnızca dosyanın en sonuna yakın, /ID [<…> <…>] içindeki iki 32 haneli hex dizgesinde. O tanımlayıcı, üst veriden bağımsız hesaplanır ve kaydetme zamanına bağlılığı kalmadıkça, dosya hash'ine bağlı bir derleme önbelleği hiç tutmaz ve içerik adresli bir depo belge başına değil çalıştırma başına bir blob tutar

Trailer ID dizisi ne işe yarar?

Trailer /ID dizisi bir dosyayı tanımlar; içeriğinin sağlamasını yapmaz. ISO 32000-1 §14.4 onu iki bayt dizgesinden oluşan bir dizi olarak tanımlar. Birincisi kalıcı tanımlayıcıdır: belge üretilirken atanır ve sonraki her düzenlemeyi hayatta kalması beklenir. İkincisi değişen tanımlayıcıdır; yazıcı dosyayı değiştirdiğinde onu tazeler. Bir belge yönetim sistemi çifti karşılaştırır ve iki dosyanın bir belgenin revizyonları mı yoksa ilgisiz iki belge mi olduğuna karar verir. §7.5.5'in Tablo 15'i, trailer /Encrypt taşıdığında girdiyi zorunlu kılar, aksi halde isteğe bağlıdır; §14.4 ise onu atmanın dosya kimliğine dayanan iş akışlarının yolunu kesebileceğini not eder

Şartname değerin nasıl hesaplanacağını hiç söylemez. O anki zaman, dosya yolu, dosya boyutu ve bilgi sözlüğündeki değerler gibi şeyler üzerinde bir MD5 özeti önerir ve saat, sonucu benzersiz kılan bileşendir. Benzersizlik, bir kimlik işaretinden istediğiniz şeydir ve tekrarlanabilirliği mahveden de tam olarak odur. losLab PDF Library varsayılan olarak o öneriyi en yalın biçimiyle izler: o anki zaman damgasının MD5'i; böylece bir saniye arayla üretilen iki belge, öteki her bayt uyuşsa bile farklı kalıcı tanımlayıcılar alır

Varsayılan duvar saati temelli PDF trailer kimlikleriyle farklılaşan özdeş Delphi derleme çalıştırmalarının, bayt özdeş deterministik çıktıyla karşılaştırılmasının şeması
Varsayılan trailer kimliği duvar saatini dosya kimliğine katarken deterministik mod onu kararlı bir tohumdan türetir

Tekrarlanabilir bir tanımlayıcıyı açmak

Deterministik mod belge başına bir anahtardır ve varsayılan olarak kapalıdır; böylece var olan çıktı, siz istemeden değişmez. SetDeterministicDocumentID 0 ya da 1 alır, değer kabul edildiğinde 1, başkaları için 0 döndürür; GetDeterministicDocumentID o anki ayarı bildirir. SetDocumentIDSeed açık bir tohum dizgesi verir ve tohum yerindeyken tanımlayıcı o dizgenin MD5'idir ve dizinin iki elemanına da yazılır. GetDocumentFileID, kaydetmeden sonra /ID[0] değerini geri okur; günlük yazmak ya da bir test iddiası için kullanışlıdır

var
  Lib: TPDFlib;
  FileID: WideString;
begin
  Lib := TPDFlib.Create;
  try
    Lib.SetDeterministicDocumentID(1);        // Create'den hemen sonra
    Lib.SetDocumentIDSeed('invoice-4471-rev3');
    Lib.SetInformation(7, 'D:20260101000000Z');   // CreationDate
    Lib.SetInformation(8, 'D:20260101000000Z');   // ModDate
    Lib.SetOrigin(1);
    Lib.DrawText(100, 700, 'Invoice 4471');
    Lib.SaveToFile('invoice.pdf');
    FileID := Lib.GetDocumentFileID;          // her çalıştırmada aynı değer
  finally
    Lib.Free;
  end;
end;

Tanımlayıcı kaydetme anında hesaplanır. Anahtarı belge kurulumunun geç bir aşamasında çevirmek yine çalışır ve değişen bir tohum bir sonraki tam kayıtta dosyaya ulaşır: A tohumuyla kaydedin, B tohumuyla yeniden kaydedin, iki çıktı farklı tanımlayıcılar taşır; A'ya dönmek özgün değeri yeniden verir

Ama anahtarı nesneyi oluşturduktan hemen sonra açın. Yazı tipleri, görüntüler, grafik durumları ve öteki sayfa kaynakları, sayfanın kaynak sözlüğünde ad alır ve deterministik mod açıkken o adlar bir sayacaçından gelir. Anahtar açılmadan eklenen her şey rastgele üretilmiş bir ad tutar; bu, tanımlayıcıyı kararlı bırakır ama içerik akışlarında her çalıştırmada birkaç bayt değiştirir

Tohum ayarlanmadığında tanımlayıcı neden hâlâ değişiyor?

Çünkü bilgi sözlüğü hash'in parçasıdır. Tohumu boş bırakın ya da birini temizlemek için SetDocumentIDSeed çağrısına boş bir dizge geçirin; losLab PDF Library tanımlayıcıyı dosyanın kendisinden türetir: serileştirilmiş gövdeyi yazarken SHA-256'dan geçirir ve /IDnin iki elemanı için özetin ilk 16 baytını kullanır. Sayfa içeriğindeki her değişiklik tanımlayıcıyı değiştirir — bütün mesele de budur. Püf noktası: CreationDate ile ModDate de o gövdede yaşar ve save yordamları, çağıran ayarlamadıkça ModDate alanını o anki zamanla damgalar

PDF Library for Delphi karar akışı: SetDocumentIDSeed'in deterministik PDF kimlikleri için kullanılan türetilmiş belge durumu tohumuna üstünlüğünü gösteriyor
Açık bir tohum her şeyi ezer; türetilmiş yol, CreationDate ve ModDate dâhil bilgi sözlüğü girdilerini hashler
Lib.SetDeterministicDocumentID(1);
Lib.SetDocumentIDSeed('');   // /ID'yi kaydedilen baytlardan türet
// Gövde hash'i Info sözlüğünü kapsar; tarihler derleme girdisi olmalı.
// Anahtar 8 ayrıca kaydın ModDate damgalamasını durdurur.
Lib.SetInformation(2, 'Quarterly Report');        // Title
Lib.SetInformation(5, 'reporting-service 4.2');   // Creator
Lib.SetInformation(7, 'D:20260101000000Z');       // CreationDate
Lib.SetInformation(8, 'D:20260101000000Z');       // ModDate
Lib.SaveToFile('report.pdf');

Anahtar 8 burada çifte görev görür ve insanların en sık atladığı satır odur. ModDate alanını SetInformation üzerinden ayarlamak tarihi çağıran sağlamış olarak işaretler ve o andan sonra SaveToFile ile SaveToString ona dokunmaz. Onsuz her kayıt taze bir zaman damgası yazar, hashlenen gövde değişir ve içerikten türeyen tanımlayıcı da onunla değişir. Tarihleri kaynak kayıttan ya da sabit bir devreden alın. Tekrarlanabilir bir derlemede Nowın işi yoktur

Birkaç şey bilinçli olarak hash'in dışında tutulur. Rastgele üretilmiş kaynak adları ve bir XMP xmpMM:InstanceID girdisindeki UUID, hash'ten önce normalize edilir; yalnızca o belirteçlerde farklılaşan iki çalıştırma aynı /IDyi alır. Dosya baytları o yerlerde yine farklılaşır; böylece her çalıştırmada taze bir örnek UUID'si taşıyan PDF/A tarzı belge size tekrarlanabilir bir tanımlayıcı verir ama bayt özdeş bir dosya vermez. Gömülü ekler bir kayma kaynağı dahadır: AddAssociatedFileFromString, ekin kendi /ModDate alanını o anki zamanla damgalar ve o tarih, öteki her gövde baytı gibi hashlenir

Açık tohum ya da içerik hash'i

Derleme artefaktları için tohumu boş bırakırdık. İçerikten türeyen tanımlayıcı, baytlar değiştiğinde tam o anda değişir; ona bağlı bir önbellek asla bayat dosya sunamaz ve veriyle senkron tutulacak ikinci bir anahtarınız olmaz. Açık tohum başka bir işe uyar: /ID[0]u belge kimliği sayan ve aynı faturanın yeniden üretilmelerinde — şablon değişse bile — aynı tanımlayıcı bekleyen bir aşağı akış arşivi. §14.4'ün niyetine daha iyi uyar; bedeli, görsel olarak farklı iki dosyanın bir tanımlayıcı paylaşabilmesidir

İçerik yolunun karar verilmesi gereken bir sonucu var. Var olan bir PDF yükleyip deterministik mod açıkken tam kaydederseniz, gelen /ID çöpe gider ve yenisi yalnızca yazılanlara bağlıdır. Yalnızca trailer tanımlayıcılarında ayrışan iki kaynak dosya aynı /IDyle çıkar. Normalleştiren bir derleme adımının istediği tam olarak budur. Bir müşterinin belgesini düzenliyorsanız ve kimliği hayatta kalmalıysa o iş için deterministik modu kapalı bırakın ya da artımlı kaydedin

ID'yi yeniden yazmak neden şifreli bir PDF'yi bozuyor?

Çünkü şifreli bir belgede /ID[0] anahtar malzemesidir. ISO 32000-1 §7.6.3.3 Algoritma 2, dosya tanımlayıcısının ilk elemanını, revizyon 2 ile 4 arasında standart güvenlik işleyicisinin şifreleme anahtarı hesabına besler; dolgulu parola, /O değeri ve izin bitleriyle birlikte. O anahtar, bir okuyucunun açılışta denetlediği /U dizgesini üretir. Anahtar, Encrypt çağırdığınızda ya da şifreli bir dosya yüklediğinizde — herhangi bir kayıttan önce — türetilir; kaydetme sırasında tanımlayıcıyı değiştirmek, kimsenin — sizin dâhil — parola denetimini geçemediği biçimsel olarak sağlam bir dosya üretirdi

losLab PDF Library bu yüzden deterministik tanımlayıcıyı yalnızca şifresiz tam kaydetmelerde tazeler. Şifreli bir belge zaten taşıdığı /IDyi korur ve anahtar o yolda etkisizdir. Böyle bir dosyayı kullanıcı parolasıyla yeniden açmak alışıldığı gibi çalışır. Şifreli çıktıyı bayt karşılaştırmasına da sokmayın: §7.6.2 altındaki AES şifrelemesi her şifreli dizge ve akışı rastgele 16 baytlık bir başlatma vektörüyle başlatır; aynı belgenin iki şifrelemesi asla bayt bayt uyuşmaz. Orada bir regresyon denetimi gerekiyorsa ikisini de çözüp döneni karşılaştırın. Revizyon ele alma ve izin bitleri, PDF şifrelemesi ve izin denetimi gezinmesinde daha derin işlenir

PDF Library for Delphi: deterministik PDF kimliğinin tam kaydetmeler, şifreli belgeler, artımlı güncellemeler ve şifreleme geri yükleme yolunu kapsayan emniyet korkulukları haritası
Yalnızca şifresiz tam kaydetmeler kimliği tazeleyebilir; şifreli dosyalar /ID[0] alanını anahtar malzemesi sayar ve revizyon eklemeleri bağlı kalmalıdır

Artımlı kaydetmeler özgün tanımlayıcıyı korur

Artımlı bir güncelleme, dosyanın önceki her baytını yerinde bırakır ve arkasına yeni bir revizyon ekler. /ID[0] alanının kalıcılığı, tüketiciye eklenen revizyonun öncekilerle aynı belgeye ait olduğunu söyler. Onu yeniden yazmak, dosyada duran revizyonlarla çelişir ve imza iş akışlarını sarsar; çünkü bir imza, tek bir belirli revizyonun bayt aralığını kapsar. Ek modu, deterministik moddayken tanımlayıcıya hiç dokunmaz; böylece PDF artımlı güncellemeleri ve akışa ekleme başlığında anlatılan davranış olduğu gibi kalır

Çıktıyı zaman içinde tekrarlanabilir tutmak

Üreticideki sonraki her değişiklik, bir saat değerini dosyaya geri sokma fırsatıdır: üretim zamanını basan bir filigran, dosya ekleyen bir kod yolu, başka bir kaydetme seçeneği. Kaydetme seçenekleri içerik kadar önemlidir: nesneleri sıkıştırılmış nesne akışlarına paketlemek, klasik çapraz başvuru tablosundan tamamen farklı bir dosya düzeni üretir; nesne akışları ve çapraz başvuru akışları notlarında açıklandığı gibi, o ayarları hattan hatta sabit tutun. En ucuz savunma, aynı belgeyi bellekte iki kez kurup baytları karşılaştıran bir testtir

function BuildQuote(const Seed: WideString): AnsiString;
var
  Lib: TPDFlib;
begin
  Lib := TPDFlib.Create;
  try
    Lib.SetDeterministicDocumentID(1);
    Lib.SetDocumentIDSeed(Seed);
    Lib.SetInformation(7, 'D:20260101000000Z');
    Lib.SetInformation(8, 'D:20260101000000Z');
    Lib.SetOrigin(1);
    Lib.DrawText(100, 700, 'Quote 8812');
    Result := Lib.SaveToString;
  finally
    Lib.Free;
  end;
end;

// İki bağımsız derleme tek bir bayt dizisi vermeli
if BuildQuote('quote-8812') = BuildQuote('quote-8812') then
  WriteLn('reproducible')
else
  WriteLn('nondeterminism leaked into the output');

Aynı düzeneği, aynı kaynak üzerinde bir serial ve bir paralel RecompressStreams çalışmasını karşılaştırmak için de kullanabilirsiniz: kaynağı iki TPDFlib örneğine yükleyin, her ikisinde deterministik kimlikleri açıp ModDate alanını sabitleyin, birine SetOptimizationWorkers(1) verip ötekine daha yüksek bir işçi sayısı verin ve iki SaveToString sonucunu diff'leyin. Saat iki dosyadan da çıkarılmışken geriye kalan fark, sıkıştırmayı yeniden yapmanın kendisinden gelir

Tekrarlanabilir trailer tanımlayıcıları, üst veri denetimi ve artımlı kaydetmenin hepsi, Delphi ve C++Builder için losLab PDF Library ürününün parçasıdır; başvurusu burada sözü edilen şifreleme seçeneklerini de listeler