HotPDF Delphi Component, bir PDF 1.5 dosyasını LoadFromFile ile yüklerken /Type /ObjStm kaplarının içine paketlenmiş nesneleri ayrıştırmaz. Her sıkıştırılmış üyenin nerede yaşadığını kaydeder ve onu ancak bir şey istediğinde ayrıştırır. Yükleme süresini gerçekte dokunduğunuz şeyle orantılı tutan şey bu tembel değişmezdir ve tam bir yeniden yazımın, hiçbir bayt dışarı çıkmadan önce fazladan bir iş yapmasının sebebi de budur: henüz ayrıştırılmamış her üyeyi açmak, çünkü yeniden yazım o üyelerin yaşadığı kapları atmak üzeredir
Bu notu yazdıran belirti tarif etmesi kolay, ayıklaması nahoş bir şeydir. Yazı tipleri, renk uzayları ve yapı ağacı nesne akışlarında duran bir dosyayı yükleyin, BeginDoc ve EndDoc üretim çiftinden geçirin; çıktı hiçbir itiraz olmadan açılır. Sayfa sayısı doğrudur, göz attığınız sayfalarda metin görünür. Sonra bir iş arkadaşı 40. sayfayı açar ve gövde metni ikame edilmiş bir yazı tipiyle render edilir ya da Extract Text komutu eskiden bir ActualText yerine geçişinin bulunduğu yerde çöp döndürür. Hiçbir şey çökmedi. Yazıcı yalnızca hiç yüklenmemiş bir nesneyi serileştirdi ve yüklenmemiş bir nesne hiçbir şey olarak serileştirilir
LoadFromFile sıkıştırılmış bir nesne için aslında neyi saklar?
Her tip-2 çapraz başvuru girdisi için LoadFromFile, FCompactObjects içinde küçük bir kayıt tutar: nesne numarası, kapsayıcı akışın kap tablosundaki indeksi, üyenin o akış içindeki konumu ve nil ile başlayan bir ParsedObject göstericisi. Kabın kendisi bulunur, belge şifreliyse şifresi çözülür ve açılır, ama üye gövdeleri bayt olarak bırakılır. ISO 32000-1 §7.5.7 bunu mümkün kılan kap düzenini tanımlar: nesne numarası ve ofset çiftlerinden oluşan bir başlık, ardından /First sonrasında birbirine eklenmiş üye gövdeleri; böylece herhangi bir üye, komşularına dokunulmadan kesilip çıkarılabilir
EnsureCompressedObjectLoaded bir kaydı nesneye çeviren tek yoldur. Kaydı nesne numarasıyla bulur ve ParsedObject zaten atanmışsa o önbelleklenmiş nesneyi döndürüp bir önbellek isabeti sayar. Aksi hâlde, kap dışarı atılmışsa onu yeniden yükler, üyenin bayt aralığını ofset tablosundan hesaplar, ayrıştırıcıya o dilimin kopyasız bir görünümünü verir ve sonucu kayda geri yazar. O andan sonra nesne dolaylıdır, gerçek nesne numarasını taşır ve dosya gövdesinden ayrıştırılmış her nesne gibi belgenin nesne indeksine kaydedilir. Katalog, bilgi sözlüğü, sayfa ağacı kökü ve sayfa nesneleri, gezinme onlara ihtiyaç duyduğu için yükleme anında bu yoldan geçer. Yazı tipleri, renk uzayları, ExtGState sözlükleri ve yapı elemanları geçmez ve bir sayfa render işlemi ya da bir yeniden yazım onlara dokunana kadar kayıt olarak kalır
Bunu dışarıdan izleyebilirsiniz. GetLoadedObjectStreamCacheInfo kaç kap bulunduğunu, kaç üyenin indekslendiğini ve bunlardan kaçının şu ana kadar ayrıştırıldığını bildirir:
var
Pdf: THotPDF;
Info: THPDFObjectStreamCacheInfo;
begin
Pdf := THotPDF.Create(nil);
try
Pdf.LoadFromFile('tagged-report.pdf');
if Pdf.GetLoadedObjectStreamCacheInfo(Info) then
Writeln(Format('%d containers, %d members indexed, %d parsed so far',
[Info.ContainerCount, Info.IndexedObjectCount,
Info.MaterializedObjectCount]));
finally
Pdf.Free;
end;
end;
Yapı yoğun bir dosyada üçüncü sayı, yüklemeden hemen sonra ikincinin küçük bir kesridir. Bu boşluk tembel yüklemenin bütün amacıdır ve tam bir yeniden yazımın geri dönüp alması gereken nesne kümesi de tam olarak budur
Tam yeniden yazım, artımlı kaydın koruduğu yazı tiplerini neden düşürür?
Tam bir yeniden yazım, kaynak dosyanın /ObjStm ve /XRef kaplarını atar ve nesne grafiğini sıfırdan yeniden serileştirir; dolayısıyla ParsedObject alanı hâlâ nil olan hiçbir üyenin çıktıda bir karşılığı kalmaz. Artımlı bir güncellemede bu sorun hiç yaşanmaz, çünkü özgün baytların ardına yeni nesneler ekler ve eski kapları önceki çapraz başvuru bölümünün adresleyebilmesi için yerinde bırakır. Fark, iki kipin yazı tiplerine davranışında değildir. Fark, özgün kapların bir sonraki görüntüleyici tarafından okunmak üzere ayakta kalıp kalmamasındadır
Düzeltme, FileName ya da OutputStream ayarlamış olun, EndDoc fonksiyonunun sürdüğü serileştirici SaveToStream içindedir. Herhangi bir yazıcı dalına dağıtım yapmadan önce FCompactObjects listesini gezer ve her girdi üzerinde EnsureCompressedObjectLoaded çağırır. Bir üye yüklenemiyorsa kayıt devam etmek yerine hata verir, çünkü bir yazı tipi sözlüğünü sessizce düşüren bir yeniden yazım, duran bir yeniden yazımdan kötüdür. Açma işlemi o düzeyde, klasik, paketlenmiş ve doğrusallaştırılmış dalların üstünde ve doğrusallaştırılmış yolun yeniden yüklenen yapısal akışları budamasının üstünde oturmalıdır. Önceki bir sürüm üyeleri yalnızca SaveLoadedDocument içinde açıyordu; bu, yüklü belge sözlüğünü kapsıyor ve üretim sözlüğünü tümüyle kaçırıyordu. LoadFromFile ardından BeginDoc, sayfa düzenlemeleri ve EndDoc, her el değmemiş üye hâlâ ayrıştırılmamış hâlde doğrudan yazıcıya gidiyordu
// Artık iki yeniden yazım sözlüğü de her yazıcı çalışmadan önce kompakt üyeleri açar.
// Yüklü belge yolu:
Pdf.LoadFromFile('quarterly.pdf');
Pdf.SaveLoadedDocument('quarterly-rewritten.pdf');
// Yüklü bir dosya üzerinden üretim yolu:
Pdf.LoadFromFile('quarterly.pdf');
Pdf.FileName := 'quarterly-stamped.pdf';
Pdf.BeginDoc;
Pdf.CurrentPage.SetFont('Arial', [], 9);
Pdf.CurrentPage.TextOut(40, 20, 0, 'Reviewed 2026-09-11');
Pdf.EndDoc; // SaveToStream önce her FCompactObjects girdisini somutlaştırır
Önbelleklenmiş üyeler onlara yaptığınız her şeyi korur. Ayrıştırılmış, düzenlenmiş ve kayıttan önce kirli işaretlenmiş bir nesne, düzenlemeleriyle birlikte önbellekten döner; sildiğiniz bir üye de silinme durumunu tekrarlanan kayıtlar boyunca korur. Açma geçişi yapısı gereği idempotenttir: yalnızca nil gözleri doldurur
Üç sayfada piksel kontrolü ActualText durumunu neden kaçırır
Yapı elemanları, bu hatanın en uzun saklandığı yerdir. ISO 32000-1 §14.9.4'te tanımlanan, işaretli içerik dizisi üzerindeki bir ActualText girdisi, çıkarma ve erişilebilirlik için gliflerin yerine geçer ama render etmeyi etkilemez. Yapı elemanı bir nesne akışında yaşıyorsa ve yeniden yazım onu kaybediyorsa sayfa yine doğru çizilir, ilk, orta ve son sayfalar kaynakla piksel piksel karşılaştırılır ve regresyon ancak birileri metin çıkarma ya da bir ekran okuyucu çalıştırdığında ortaya çıkar. Yalnızca sayfa render eden bir yeniden yazım testi, etiketli PDF için bir yeniden yazım testi değildir. Çıkarılan metni ve yapı ağacını da karşılaştırın
Boş bir kullanıcı parolası yüklemeyi nasıl değiştirir?
Boş bir kullanıcı parolası yine de dosyanın şifreli olduğu anlamına gelir ve böyle bir dosyadaki nesne akışları, dosya anahtarı geri kazanılana kadar şifreli metindir. ISO 32000-1 §7.6.3.4 Algoritma 2 bu anahtarı paroladan, /O girdisinden, /P değerinden ve ilk belge tanımlayıcısından türetir ve HotPDF'in tip-2 geçişi tek bir kabı açabilmeden önce onu boş dizeye karşı çalıştırması gerekir. Bu yüzden yüklü ve şifreli bir belgede BeginDoc, her şeyden önce DecryptLoadedDocument fonksiyonunu boş parolayla çağırır: çağıran çıktıyı korumayı düşünüyor olsun olmasın, bir yeniden yazım başlayabilmeden önce nesne grafiğinin doğrulanması ve şifresinin çözülmesi gerekir. Çıktı şifrelemesi ayrı bir karardır, çağıranın koruma ayarlarıyla yönetilir ve BeginDoc şifre çözme geçişinden sonra o ayarları geri yükler, böylece şifreli bir girdi sessizce şifreli bir çıktıya dönüşmez
Kap politikası herhangi bir parola denenmeden önce /Encrypt sözlüğünden okunur. /V 1 ve 2 için her akış dosya anahtarıyla şifrelenir. Kript filtrelerinde HotPDF /StmF alanını /CF üzerinden çözer: Identity filtresi ya da None değerinde bir /CFM, düz metin kaplar demektir; V2 ve AESV2 ise şifreli olanları. Yanıt FReloadObjectStreamsEncrypted alanına düşer ve belirli bir durum için önemlidir. Kaplar düz metin ama dizeler değilken üyeler tek tek şifresi çözülmesi gereken şifreli dizeler taşır, bu yüzden MaterializeMembersOfPlaintextObjectStreams nesne başına şifre çözme geçişinden önce her kompakt üyeyi açar. Politika henüz bilinmiyorken hiçbir şey yapmaz ve kapların kendisi şifreliyken de hiçbir şey yapmaz, çünkü şifreli bir kabın üyeleri onunla birlikte zaten şifresi çözülmüştür ve asla iki kez çözülmemelidir
Bir kabın şifresi çözülemediğinde ne olur?
Şifre çözmede başarısız olan bir kap karantinaya alınır, ölümcül sayılmaz. Tip-2 geçişi FObjStmQuarantine içine, kabın nesne numarasıyla, bir THPDFObjStmQuarantineReason ile, tanı amaçlı bir dizeyle ve çapraz başvurunun o kaba yönlendirdiği üye nesne numaraları listesiyle bir THPDFObjStmQuarantineInfo girdisi kaydeder. osqrDecryptFailed dört ayrı durum için ortaya çıkar: hiçbir kript filtresi çözülemedi, AES-256 ya da AES-GCM şifre çözme hata verdi, eski usul RC4 ya da AES-128 şifre çözme hata verdi ya da hiç kullanılabilir dosya anahtarı yok. Bağımsız kaplar yüklenmeye devam eder, böylece hasarlı tek kabı olan bir belge yine açılır ve ona bağlı olmayan her sayfayı yine render eder
Karantina listesi ayrıştırıcı geri düşüşünden sağ çıkar. Birincil çapraz başvuru yüklemesi başarısız olur ve HotPDF nesne tablosunu dosyayı tarayarak yeniden kurarsa, ilk denemenin şifreli bayrağı bu yeniden kurulumda ayakta kalmayabilir, ama karantina kayıtları kalır. BeginDoc fonksiyonunun şifreli bayrağı değil karantina listesini denetlemesinin sebebi budur: yüklü bir belgede FObjStmQuarantine listesini gezer ve ilk osqrDecryptFailed girdisinde, kabı adıyla anarak ve geçerli bir parolayla yeniden yükleme isteyerek hata verir. Bu noktanın ötesine geçen bir yeniden yazım, kabın tutması gereken üyeleri boş nesneler olarak yazar ve başarı bildirirdi. Aynı denetimi kendiniz, daha erken ve kendi politikanızla, genel erişimciler üzerinden çalıştırabilirsiniz:
var
Info: THPDFObjStmQuarantineInfo;
I: Integer;
begin
Pdf.LoadFromFile('vendor-form.pdf'); // boş kullanıcı parolası
for I := 0 to Pdf.GetLoadedQuarantinedObjStmCount - 1 do
if Pdf.GetLoadedQuarantinedObjStmInfo(I, Info) and
(Info.Reason = osqrDecryptFailed) then
raise Exception.CreateFmt(
'Object stream %d is unreadable (%s); %d members unresolved',
[Info.ContainerObjNum, String(Info.Diagnostic),
Length(Info.MemberObjNums)]);
// buradan itibaren yeniden yazmak güvenli
end;
Diğer karantina nedenleri kriptografik olmayan arızaları kapsar: akış olmayan bir kap, eksik bir sözlük, geçersiz bir /N ya da /First, kabul edilen aralığın dışında bir akış boyutu, bir açma hatası, verinin ötesini işaret eden bir /First ya da çözülüp de ayrıştırılamayan bir üye gövdesi. Bunlar alım sırasında günlüğe kaydedilmeye değer, çünkü her biri aşağı yönde eksik kalacak üyeleri tam olarak adlandırır
Bir yeniden yazım neden özgün sayısal token'a ihtiyaç duyar?
HotPDF her sayısal nesneyi bir Single olarak saklar ve bir Single, bir gerçek sayının kaynak metnini yeniden üretemez. ISO 32000-1 §7.3.3 bir yazıcının aynı değer için 0.750000, .75 ya da 0.75 yazmasına izin verir ve bunların hiçbiri 24 bitlik ikili gösterim ile genel bir biçimlendiriciden geçen bir gidiş dönüşten değişmeden çıkmaz. Daha kötüsü, 0.7 gibi bir değer bir Single içinde hiç temsil edilemez; en yakın kayan noktalı sayıya ayrıştırılır ve o sayıyı yeniden biçimlendirmek, basamak döngüsüne bağlı olarak 0.69999999 ya da yuvarlanmış bir komşusunu üretebilir. Bir dolgu renginde ya da bir /CA saydamlık sabitinde bu, 8 bitlik bir kanalda bir birimlik fark demektir; kaynağa karşı bir piksel karşılaştırmasını düşürmeye yeter ve gradyan sınırlarında göze çarpmaya da yeter
THPDFNumericObject.RememberSourceToken bunu değiştirilmemiş durum için çözer. Ayrıştırıcı, Value alanını atadıktan hemen sonra onu ham token ile çağırır; yöntem yalnızca rakamlardan, en fazla bir ondalık noktadan ve isteğe bağlı bir baştaki işaretten oluşan token'ları kabul eder ve token'ı karşılık geldiği değerle birlikte FSourceValue içinde saklar. SourceToken özelliği, saklanan metni yalnızca Value hâlâ FSourceValue ile eşitken döndürür. Sayıyı değiştirin, token buharlaşır; böylece değiştirilmiş bir değer her zaman mevcut biçimlendirme yolundan geçer ve asla bayat metin yazmaz. SaveNumericObject önce SourceToken alanına bakar ve varsa onu aynen yazar; tamsayı, renk uzayı başvurusu ve kesirli dallara ise yalnızca bellekte oluşturulmuş ya da düzenlenmiş sayılar için düşer
Değişmez küçüktür ve açıkça söylenmeye değer: dokunmadığınız bir sayı okunduğu baytlarla yazılır, dokunduğunuz bir sayı ise HotPDF'in kendi biçimlendiricisiyle yazılır. Kompakt üyeler bundan gövde nesneleriyle aynı şekilde yararlanır, çünkü EnsureCompressedObjectLoaded aynı ayrıştırıcıyı üye dilimi üzerinde çalıştırır. Sayı biçimlendirmesinin kendisi ve süreç yerel ayarından bağımsızlığı HotPDF'te yerel ayardan bağımsız PDF sayı biçimlendirme yazısında ele alınır
Bir yeniden yazım yolunu nesne akışlarına karşı test etmek
Üç kontrol yukarıda anlatılan her arızayı yakalar ve hiçbiri Acrobat gerektirmez. Birincisi, kayıttan sonra IndexedObjectCount ile MaterializedObjectCount değerlerini karşılaştırın; tam bir yeniden yazımda eşit olmalıdırlar ve her boşluk düşürülmüş bir üyedir. İkincisi, metni çıkarın ve yapı ağacını her iki dosyada numaralandırın, yalnızca render etmekle kalmayın; böylece kayıp bir ActualText ya da kayıp bir yapı elemanı bir fark olarak görünür. Üçüncüsü, çıktıyı taze bir örnekle yükleyin ve GetLoadedQuarantinedObjStmCount değerinin sıfır olduğunu doğrulayın; bu ayrıca yazıcının okuyucunun açamayacağı bir kap üretmediğini kanıtlar. FReloadObjectStreamsEncrypted alanına karar veren kript filtre bileşimleri StmF, StrF ve EFF politikası yazısında ortaya konur. Bu hikâyenin yazıcı tarafı, yani nesne akışlarını nasıl üreteceğiniz ve ne zaman yeniden yazım yerine artımlı güncellemeyi tercih edeceğiniz, nesne akışları ve artımlı güncellemeler kılavuzundadır
Tembel üye yükleme, yazıcı öncesi açma geçişi, şifre çözme karantinası ve kaynak token koruması, Delphi ve C++Builder için HotPDF Delphi Component ile birlikte gelir. GetLoadedObjectStreamCacheInfo ve karantina erişimcilerini kendi alım hattınıza karşı izlemek isterseniz ürün sayfası API referansına bağlantı verir