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üğü
/CreationDateve/ModDatealanlarını (ISO 32000-1 §14.3.3, Tablo 317) saat dilimi son ekliD:YYYYMMDDHHmmSSdizeleri olarak taşır (§7.9.4) ve XMP paketi aynı anıxmp:CreateDateilexmp:ModifyDateolarak yineler. HotPDF ikisini de kurucununNowile başlattığıFCreationDatealanından damgalar, dolayısıyla iki kayıt yazıldıkları saniyede farklıdır - Tanımlayıcı. Trailer
/IDdizisi (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 bunaGetTickCountdeğ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,/Ove/OEalanları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/IDelemanı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
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
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.
_DateTimeToPdfDateyerel UTC ofsetini ekler, dolayısıyla bir derleme aracısındaD:20260101000000+08'00'ile başka birindeD: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.
SaveIncrementalUpdatedeğişiklik tanımlayıcısını hedef yoldan,GetTickCountve 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.
SaveLoadedDocumentnormalde 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
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