Teknik Makale

Delphi'de Yeniden Üretilebilir PDF Çıktısı: Aynı Baytlar

HotPDF Delphi Component, ReproducibleOutput özelliği True iken kayıtlar arasında bayt bayt aynı PDF çıktısı üretir: Info /CreationDate ve /ModDate alanlarını sabit bir tarihe sabitler, duvar saati kaynaklı belge tanımlayıcısını tohumlanmış ya da içerikten türetilmiş bir hash ile değiştirir, AES şifreleme yollarının aksi hâlde çekeceği her rastgele baytın yerine sabitler koyar ve serileştirdiği her sözlüğü sıralar. Bu bayrak regresyon paketleri ve derleme çıktısı karşılaştırması için vardır, üretim belgeleri için değildir ve bu sınırın sebepleri işin ilginç kısmıdır. Özelliği tetikleyen senaryo bir altın dosya testidir. Bir fatura render edersiniz, PDF'i commit edersiniz ve yarının derlemesinin aynı baytları üretmesini beklersiniz. Asla aynı olmaz. Dosya her görüntüleyicide sorunsuz açılır, metin aynıdır, sayfa ağacı aynıdır ve fark yine dört beş yerde ışıldar. Bir PDF üreticisini bayt düzeyinde bir regresyon testine sokmayı deneyen herkes bu duvara çarpmıştır ve çözüm “zaman damgalarını sökmek” değil, yazıcının belgenin kendisinden başka neye başvurduğunun kesin bir dökümünü çıkarmaktır

Aynı PDF'in iki kaydı neden farklıdır?

Aynı belgenin iki kaydı farklıdır, çünkü HotPDF de dahil bir PDF yazıcısı sayfa içeriğiyle hiç ilgisi olmayan dört entropi kaynağına başvurur: duvar saati, belge tanımlayıcısı, kriptografik rastgele sayı üreteci ve sözlük girdilerinin bellek sırası. Her biri tek başına meşrudur. ISO 32000-1 onların orada olmasını ister. Yalnızca dosyayı, ne içerdiğinin değil ne zaman ve nerede yazıldığının bir fonksiyonu hâline getirirler

  • Saat. Info sözlüğü /CreationDate ve /ModDate alanlarını (ISO 32000-1 §14.3.3, Tablo 317) saat dilimi son ekli D:YYYYMMDDHHmmSS dizeleri olarak taşır (§7.9.4) ve XMP paketi aynı anı xmp:CreateDate ile xmp:ModifyDate olarak yineler. HotPDF ikisini de kurucunun Now ile başlattığı FCreationDate alanından damgalar, dolayısıyla iki kayıt yazıldıkları saniyede farklıdır
  • Tanımlayıcı. Trailer /ID dizisi (ISO 32000-1 §14.4) kalıcı bir tanımlayıcı ile bir değişiklik tanımlayıcısı tutar. HotPDF'in varsayılan tarifi birinci eleman için dosya adını geçerli saatle birlikte milisaniyeye kadar hash'ler, ikinci için de buna GetTickCount değerini ekleyip hash'ler. İki tanımlayıcı, her çalıştırmada iki taze değer
  • Rastgele baytlar. Standart güvenlik tanımlayıcıya ve gerçek rastgeleliğe bağlıdır. AES-256 için dosya şifreleme anahtarı, doğrulama ve anahtar tuzları ile her CBC başlatma vektörü sistemin rastgele kaynağından çekilir (ISO 32000-2 §7.6.4.4.7 rastgele tuzları şart koşar). /U, /UE, /O ve /OE alanlarının hepsi bu baytlardan hesaplandığı için şifreli bir belge, düz metni değişmese bile bütünüyle değişir. Eski algoritmalar birinci /ID elemanını anahtara katlar (ISO 32000-1 §7.6.3.3, §7.6.3.4), dolayısıyla tek başına taze bir tanımlayıcı dosyanın anahtarını yenilemeye yeter
  • Sıra. Bir PDF sözlüğü sırasız bir eşlemedir ve bellek içi listesini gezen bir yazıcı anahtarları ekleme sırasına göre yazar. Bir kaynak sözlüğünü farklı bir dizide kuran her kod yolu ya da farklı bir düzenden ayrıştırılmış yüklü bir belge, geçerli ama metinsel olarak farklı bir dosya üretir
Bir belgenin iki HotPDF kaydını farklı kılan dört entropi kaynağı: Now ile damgalanan FCreationDate, D: tarihlerini ve XMP paketini besler, trailer /ID dosya adını, saati ve GetTickCount değerini hash'ler, AES anahtar malzemesini sistem rastgele kaynağından çeker ve sözlükler bellekteki ekleme sırasına göre serileştirilir
Her kaynak tek başına meşrudur ve ISO 32000-1 onların orada olmasını ister, ama birlikte dosyayı ne içerdiğinin değil ne zaman ve nerede yazıldığının bir fonksiyonuna çevirirler

ReproducibleOutput neyi sabitler?

ReproducibleOutput := True ayarını BeginDoc ya da SaveLoadedDocument öncesinde yapmak dört kaynağın her birini sabit bir değerle değiştirir ve bunu, aksi hâlde saate ya da rastgele üretece uzanan aynı kod yollarında yapar, dolayısıyla ayrı bir temizlik geçişi gerekmez. Yukarıdaki listede neyin eksik olduğuna dikkat edin: içerik. Yazı tipleri, sayfa akışları, görüntü verisi ve çapraz başvuru tablosu aynı girdi için zaten belirlenimcidir; gürültü tümüyle metadatada ve güvenlik katmanında yaşar, bu yüzden tek bir hedefli özellik onu kaldırabilir. Özelliğin varsayılanı False'tur ve kütüphanede onu sizin yerinize açan hiçbir şey yoktur

var
  Pdf: THotPDF;
begin
  Pdf := THotPDF.Create(nil);
  try
    Pdf.AutoLaunch := False;
    Pdf.FileName := 'golden-invoice.pdf';
    Pdf.ReproducibleOutput := True;     // BeginDoc öncesinde
    Pdf.BeginDoc;
    Pdf.CurrentPage.SetFont('Arial', [], 12);
    Pdf.CurrentPage.TextOut(40, 40, 0, 'Invoice 2026-0042');
    Pdf.EndDoc;
  finally
    Pdf.Free;
  end;
end;

BeginDoc içinde yeniden üretilebilir dal FCreationDate := EncodeDate(2026, 1, 1) atamasını yapar ve belge tanımlayıcısını dosya adı artı saat özeti yerine MD5CalcString('HotPDF-reproducible-seed') ile tohumlar. Bu tek atama hem iki Info tarihini hem de iki XMP tarihini kapsar, çünkü dördü de aynı alandan render edilir. Dosya sonunda yazıldığında BuildDocumentIdentifiers, trailer tanımlayıcısı için ComputeCanonicalDocumentIdentifier fonksiyonuna başvurur: tüm nesne grafiğini kurallı sırayla dışa aktarır, bulduğu her D: tarih dizesinin basamaklarını sıfırlar ki zaman damgaları hash üzerinden geri sızamasın, ve sonucun MD5'ini alır. /ID dizisinin iki elemanı da bu değeri alır. Aynı içerikten türetilmiş tanımlayıcı, yüklü bir belge BeginDoc üzerinden hiç geçmeden şifrelendiğinde de kullanılır ki LoadFromFile ile açtığınız bir dosyada ActivateProtection durumu tam olarak budur

Rastgele baytlar en az bariz olan ikamedir. AES-256 anahtar rutini rastgele kaynağını, bayrak açıkken 32 baytlık dosya şifreleme anahtarı ve her 8 baytlık tuz için FillChar(P^, Count, $5A) çağıran yerel bir yardımcıya sarar; AES-128 ve AES-256 dize ve akış şifreleyicileri de AESGenerateRandomIV yerine, başlatma vektörünü I gözü için 14 * (1 + I) ile dolduran AESGenerateStaticIV fonksiyonuna geçer. Anahtar, tuzlar ve vektörler sabitlendiğinde /U, /UE, /O, /OE ve şifrelenmiş her akış ikinci çalıştırmada özdeş çıkar. Son olarak SaveToStream, yeniden üretilebilir bayrak set edildiğinde DeterministicDictionaryOrder özelliğini açar ve serileştirici her sözlüğü anahtar adlarının ham baytlarına göre ekleme sıralamasıyla sıralar; kısa önek önce gelir ve eşitlik bozucu olarak özgün indeks kullanılır. Bu, bir PDF'i elle düzenleyip sonra onarma yazısında anlatılan tanılama yazıcısının kullandığı sıralamanın aynısıdır; yeniden üretilebilir bayrak yalnızca sıralamayı ödünç alır, o yazıcının düz metin düzeninin geri kalanını almaz

ReproducibleOutput HotPDF'te neyi sabitler: oluşturma tarihi EncodeDate 2026, 1, 1 olur, trailer tanımlayıcısı D: basamakları sıfırlanmış kurallı grafik üzerinden ComputeCanonicalDocumentIdentifier'dan gelir, AES anahtarları ve tuzları $5A baytlarıyla dolar ve AESGenerateStaticIV her gözü doldurur, DeterministicDictionaryOrder ise her sözlüğü sıralar
İkameler, aksi hâlde saate ya da rastgele üretece uzanan aynı kod yollarında çalışır; bu yüzden ayrı bir temizlik geçişi gerekmez ve /ID dizisinin iki elemanı da aynı içerikten türetilmiş değeri alır

Sabit tarih neden yine de duvar saatini sızdırdı?

v2.752.2 düzeltmesi, sabit oluşturma tarihinin başlangıçta kurucuda belirlenmesi yüzünden var; kurucu ise çağıranın henüz set etmediği bir özelliği bilemez. Normal çağrı dizisi Create, ardından ReproducibleOutput := True, ardından BeginDoc biçimindedir. Kurulum anında FReproducibleOutput hâlâ False'tur, dolayısıyla FCreationDate Now değerini aldı ve onu korudu. Tanımlayıcı ve rastgele baytlar doğru sabitlenmişti, bu yüzden iki dosya neredeyse her yerde anlaşıyor ve tam olarak iki tarih dizesi ile iki XMP alanında anlaşamıyordu. Atamayı BeginDoc fonksiyonunun yeniden üretilebilir dalına, tohumlanmış tanımlayıcının yanına taşımak, kararı özelliğin son değerini aldığı noktaya koydu

Bunu kaçıran regresyon testi, düzeltmenin kendisinden daha değerlidir. İkisi de aynı duvar saati saniyesinde çalışan iki kayıt D: dizesini tesadüfen aynı yazar ve bayt karşılaştırması, daha yavaş her makinede başarısız olacak bir hata için geçer. Düzeltilmiş test iki kayıt arasında 1100 ms bekler ki PDF zaman damgası bir saniye sınırını kesin olarak geçsin, durumu düz, AES-128 ve AES-256 çıktı için ve şifreli iki varyantta gerçek parolalarla çalıştırır ve iki arabelleği CompareMem ile karşılaştırır; başarısızlıkta ilk farklı ofseti bildirir, böylece fark koca bir dosya yerine belirli bir nesneyi işaret eder. Bir bayt karşılaştırması belirlenimciliği kanıtlar, başka hiçbir şeyi değil; bu yüzden şifreli çıktıyı kullanıcı parolasıyla yeniden yükleyip sayfa sayısını okuyan ayrı bir doğrulama bulundurun. Dosyayı hem kararlı hem okunamaz kılan bir değişiklik, yeşil bir farkın gücüne sığınıp geçmemelidir

function SaveOnce(const Target: string): TBytes;
var
  Pdf: THotPDF;
  Stream: TFileStream;
begin
  Pdf := THotPDF.Create(nil);
  try
    Pdf.AutoLaunch := False;
    Pdf.FileName := Target;
    Pdf.ReproducibleOutput := True;
    Pdf.OwnerPassword := 'owner';
    Pdf.UserPassword := 'user';
    Pdf.CryptKeyLength := aes256;
    Pdf.ActivateProtection := True;
    Pdf.BeginDoc;
    Pdf.CurrentPage.SetFont('Arial', [], 12);
    Pdf.CurrentPage.TextOut(40, 40, 0, 'reproducible save');
    Pdf.EndDoc;
  finally
    Pdf.Free;
  end;
  Stream := TFileStream.Create(Target, fmOpenRead or fmShareDenyWrite);
  try
    SetLength(Result, Stream.Size);
    if Stream.Size > 0 then
      Stream.ReadBuffer(Result[0], Stream.Size);
  finally
    Stream.Free;
  end;
end;

// test gövdesinde
A := SaveOnce(PathA);
TThread.Sleep(1100);          // farklı bir PDF zaman damgası saniyesi zorla
B := SaveOnce(PathB);
Assert.AreEqual<Integer>(Length(A), Length(B));
Assert.IsTrue(CompareMem(@A[0], @B[0], Length(A)),
  'two saves under ReproducibleOutput must be byte-identical');

Yeniden üretilebilir şifreli bir PDF hâlâ güvenli mi?

Hayır. ReproducibleOutput altında şifrelenmiş bir belge anlamlı hiçbir biçimde korunmaz ve test dizininin dışına çıkan her şey için bayrak kapalı olmalıdır. AES-256 dosya şifreleme anahtarı otuz iki bayt $5A'dır, tuzlar sekiz bayt $5A'dır ve başlatma vektörleri yayımlanmış bir aritmetik deseni izler. Parola hâlâ /UE ve /OE sarmalayıcılarını kapılar, ama sarmalanan anahtar bir sabittir, dolayısıyla sabiti bilen herkes her içerik akışının şifresini parolasız çözebilir. Tuzların sabitlenmesi ayrıca ISO 32000-2 §7.6.4.4.7'nin, aynı parolaların dosyalar arasında aynı /U dizelerini vermemesini sağlamak için dayandığı belge başına benzersizliği ortadan kaldırır. Rastgele kaynak sağlamken şifreleme özelliklerinin neyi vaat ettiği için AES-256 kurulum yazısını okuyun; yeniden üretilebilir bayrak altında o vaatler askıya alınır

Tanımlayıcı ödünleşimi daha incedir. ISO 32000-1 §14.4 ikinci /ID elemanının her değişiklikte değişmesini ister ki araçlar güncellenmiş bir dosyayı atasından ayırt edebilsin; yeniden üretilebilir bir kayıt ise iki göze de aynı değeri yazar. Bu değer kurallı nesne grafiğinin hash'i olduğu için içeriği farklı iki belge yine farklı tanımlayıcılar alır ki bu bir sabitten iyidir. Ama BeginDoc fonksiyonunun anahtar türetmede kullandığı tohum her makinede her belge için aynı dizedir ve dosyaları ayırt etmek için /ID değerine bakan bir okuyucu, örneğin bir ek açıklama önbelleği ya da form verisi yan dosyası, aynı hash'i tutan her yeniden üretilebilir dosyayı birbirine karıştırır

Bayrak neyi kapsamaz?

ReproducibleOutput yazıcının kendi başına soktuğu entropiyi kaldırır; ortam üzerinden ya da denetlemediği kod yollarından giren entropiyi kaldıramaz ve bunlardan üçüne takılmak kolaydır

  • Saat dilimi son eki. _DateTimeToPdfDate yerel UTC ofsetini ekler, dolayısıyla bir derleme aracısında D:20260101000000+08'00' ile başka birinde D:20260101000000-05'00' aynı sabit tarih için farklı baytlardır. Yeniden üretilebilirlik tek makinedeki çalıştırmalar arasında ya da aynı saat dilimini paylaşan makineler arasında geçerlidir; altın dosyalarınız seyahat ediyorsa aracının saat dilimini sabitleyin
  • Artımlı güncellemeler. SaveIncrementalUpdate değişiklik tanımlayıcısını hedef yoldan, GetTickCount ve geçerli saatten hesaplar ve yeniden üretilebilir bir dalı yoktur, çünkü artımlı bir bölüm tanımı gereği yeni bir değişikliktir. Eklenen deltaları değil, tam yeniden yazımları karşılaştırın
  • Doğrudan geçirme kısayolu. SaveLoadedDocument normalde değiştirilmemiş ve şifresiz bir kaynak dosyayı yeniden serileştirmek yerine bayt bayt kopyalar. Yeniden üretilebilir bayrak bu kısayolu kapatır ve sıralama ile tanımlayıcı kuralları uygulansın diye tam bir yeniden yazımı zorlar; bu da yüklü bir dosyanın yeniden üretilebilir kaydının varsayılandan yavaş olduğu ve girdinin bir kopyası olmadığı anlamına gelir. Onu önceki bir yeniden üretilebilir kayıtla karşılaştırın, asla özgün girdiyle değil
HotPDF yeniden üretilebilir kayıtlarının nerede bittiği: _DateTimeToPdfDate yerel UTC ofsetini yine ekler, bu yüzden altın dosyalar saat dilimleri arasında farklılaşır; SaveIncrementalUpdate'in yeniden üretilebilir dalı yoktur çünkü delta yeni bir değişikliktir; doğrudan geçirme kısayolu kapatıldığı için yüklü bir dosya her zaman tamamen yeniden yazılır
Yeniden üretilebilirlik tek makinedeki ya da aynı saat dilimini paylaşan makineler arasındaki çalıştırmalarda geçerlidir ve yeniden üretilebilir bir kayıt önceki bir yeniden üretilebilir kayıtla karşılaştırılmalıdır, asla özgün girdiyle değil

Aynı sürümden, geçen bir kontrolün neyi kanıtlayıp neyi kanıtlamadığına dair bir ders daha. CharProcs.DeleteValue('A') çağıran, yani doğrudan tutulan bir glif akışını serbest bırakan, sonra aynı göstericiyi yeniden ekleyen ve ayrıca tek bir doğrudan ExtGState nesnesini hem bir kaynak sözlüğüne hem bir desene veren bir PDF/X-6 test fikstürü vardı. Uygunluk doğrulayıcısı o serbest bırakılmış bellek kullanımı ve çift sahiplik üzerinde ara sıra geçiyordu, çünkü serbest bırakılan belleğin o anda ne tuttuğunu okuyordu. Yapısal bir kontrol titriyorsa doğrulayıcıya bakmadan önce test girdisinin sahipliğine bakın. Yeniden üretilebilir çıktı bu disiplini ucuzlatır: iki kayıt bayt bayt aynı olduğunda titremenin kalan tek kaynağı nesne grafiğinin kendisidir ve katalogdan aşağı inen yapısal bir fark onu bulacaktır

Burada anlatılan ReproducibleOutput, DeterministicDictionaryOrder ve şifreleme özellikleri Delphi ve C++Builder için standart HotPDF Delphi Component ile gelir ve aynı bayrak kütüphanenin kendi regresyon korpusunu sürer; bu yüzden bir test paketinde gördüğünüz davranış, bileşenin test edildiği davranıştır