Microsoft Word veya Excel'in ürettiği bir PDF'i açın, sayfalarını çevirin; olağandışı hiçbir şey görünmez. Bunu bir Delphi programına yükleyin, sayfa sayısını geri okuyun; sayı doğrudur. Ardından şifreleme açıkken yeniden kaydedin; kayıt adımı hata fırlatır ya da çıktı, hasarlı bir çapraz referans uyarısıyla açılır. Dosya hiçbir zaman bozuk değildi. O bir hibrit referans dosyasıdır ve on beş yıllık bir görüntüleyicinin onu açmasına izin veren yapının kendisi, okumayı çok erken durduran bir yükleyiciyi mağlup eden yapıdır
Bu, her dahili testi geçen bir PDF süreç hattının gidiş-dönüş yaptıramayacağı bir dosyayla karşılaşmasının en yaygın yollarından biridir. Girdilerin tümü dahili olarak üretilmişti, bu nedenle hiçbir zaman hibrit değillerdi. İlk hibrit dosya, bir müşterinin elektronik tablodan dışa aktarılmış bir faturayı ilettiği gün gelir
Word ve Excel gerçekte ne yazar?
ISO 32000-1, hibrit referans düzenini §7.5.8.4'te açıklar. Belge akışları (object streams) gibi PDF 1.5 özelliklerini isteyen ancak yine de bir PDF 1.4 okuyucusunun dosyayı açmasına izin veren bir uygulama, çapraz referans bilgilerini iki kez yazar. Sürüm 1.4'e kadar her PDF'i sonlandıran sabit genişlikteki ASCII satırları olan klasik bir çapraz referans tablosu vardır ve geri kalanını dizinleyen bir çapraz referans akışı vardır. Klasik bölümün fragmanı (trailer), değeri o akışın bayt kayması (offset) olan bir /XRefStm girişini taşır
İş bölümü kasıtlıdır. Eski bir okuyucunun ulaşması gereken nesneler, aralarında katalog ve sayfa ağacı da olmak üzere, klasik tablodan adreslenebilir. Sıkıştırılmış nesne akışlarına katlanan nesneler, klasik tabloda bir f tipi girişiyle boş işaretlenir; böylece 1.4 okuyucusu doğrudan bunların yanından geçer ve asla ayrıştıramayacağı bir yapıya takılmaz. Gerçek konumları yalnızca çapraz referans akışında yaşar. Böyle bir dosyanın imzası kuyruğudur: yalnızca eski bir okuyucunun ihtiyaç duyduğu nesneleri kapsayan kısa bir klasik bölüm; onun trailer'ı, gerçek kurtarma verisinin oturduğu /XRefStm'yi gösterir
/XRefStm ofseti ise nesne indeksinin geri kalanını tutan akışa götürürDoğru sayfa sayısı neden hiçbir şeyi kanıtlamaz?
Katalog ve sayfa ağacı kasıtlı olarak klasik tablodan erişilebilir olduğundan, yalnızca o tabloyu okuyan bir yükleyici /Root'u bulur, sayfa ağacını yürür ve doğru sayfa sayısını bildirir. Eski bir okuyucunun ihtiyaç duyduğu her şey mevcuttur, bu nedenle dosya sağlıklı görünür. Kaybolan nesneler, nesne akışlarına paketlenmiş olanlardır: AcroForm alan sözlükleri, etiketli-PDF (tagged-PDF) yapı öğeleri, eski bir görüntüleyiciye asla görünmesi gerekmeyen küçük sözlüklerin uzun kuyruğu
Bir şey o nesnelere dokunana kadar boşluğu fark etmezsiniz ve tam bir yeniden kaydetme hepsine dokunur. Belgeyi yeniden şifrelemek veya yeniden yazmak için yürümek, tam olarak sırayla her nesne numarasını isteyen işlemdir; bu nedenle semptom, nedeninden çok uzakta, yükleme zamanı yerine kaydetme zamanında yüzeye çıkar
Tuzak, xref'i görüp duran bir dedektördür
Bir dosyanın nasıl dizinlendiğine karar vermenin ucuz yolu, startxref'i takip etmek ve işaret ettiği ilk baytları incelemektir. xref anahtar kelimesi klasik bir tablo anlamına gelir; bir akış nesnesi ise çapraz referans akışı anlamına gelir. Bu test, tek bir şemaya bağlı kalan her dosya için doğrudur. Yalnızca eski okuyucuları tatmin etmek amacıyla klasik bir bölümü hedefleyen startxref'e sahip hibrit bir dosya için yanlıştır; oysa o bölümün fragmanındaki /XRefStm belgenin çoğunun gerçekte dizinlendiği yerdir. Karşılaştığı ilk xref üzerinde "klasik" döndüren bir dedektör asla /XRefStm'yi okumaz ve yalnızca akışta yaşayan her nesne görünmez hale gelir
var
Pdf: THotPDF;
PageCount: Integer;
begin
Pdf := THotPDF.Create(nil);
try
PageCount := Pdf.LoadFromFile('Invoice_XLS.pdf'); // sayfalar klasik tablodan erişilebilir
// yüklü belgeyi burada inceleyin veya düzenleyin
Pdf.SaveLoadedDocument('Invoice_secured.pdf'); // her nesneyi ister, yalnızca akışta olanları da
finally
Pdf.Free;
end;
end;
O diziyi erken çıkışlı dedektör etrafında kurulmuş bir yükleyicide çalıştırın; yükleme iyi görünür, yeniden kaydetme ise olmayan nesnelerin kendilerini duyurduğu yerdir. Çözüm başta daha fazla bayt okumak değildir; dosyanın bittiğine karar vermeden önce hibrit trailer'ı tanımak ve /XRefStm'yi izlemektir
Birleştirme sırası müzakere edilemez
Her iki dizin de okunduktan sonra, yalnızca tek bir yönde birleştirilebilirler. Klasik girişlerin etrafını dolduracak şekilde, önce çapraz referans akışının birleştirilmesi gerekir. Bunun nedeni, biçimin merkezindeki küçük aldatmacadır. Hibrit bir dosya, sıkıştırılmış nesnelerini klasik tabloda boş (free) olarak işaretler, böylece eski okuyucular bunları göz ardı eder. İlk görülen kazanır politikasını onurlandıran ve önce klasik tabloyu okuyan bir yükleyici, o nesne numaralarını boş olarak kaydedecek, ardından yuvalar zaten alınmış olduğu için onları gerçekten konumlandıran akış girişlerini atacaktır. Sırayı tersine çevirin ve akıştaki tip 2 girişleri (her biri bir nesne-akış numarası artı bir dizin) sahip olmaları gereken yuvaları kazanır ve klasik girişler onların etrafına yerleşir
Aynı disiplin, daha eski bir revizyonun silinmiş bir nesneyi diriltmesine karşı da koruma sağlar. Artımlı güncellemeler /Prev aracılığıyla geriye doğru zincirlenir ve tip 0 boş bir giriş, daha yeni bir bölümün bir nesne numarasını emekliye ayırdığının nöbetçisidir. Zincirdeki daha sonraki, daha eski bir bölümün bu nöbetçinin üzerine eski bir konumla yazmasına izin verilmemelidir. Boş işaretçiler için ilk görüleni yetkili olarak kabul ederseniz silinen nesne silinmiş olarak kalır; dikkatsizce davranırsanız dosyanın kendi geçmişi, en son revizyonun kaldırdığı içeriği yeniden canlandırır
İki görünüm nasıl ayrışır
Bunların hepsi iki indeksin hemfikir olduğunu varsayar. Word'den çıktığı hâlde hemfikirdirler: her görünüm, bildirdiği kapsam içinde aynı belgeyi tarif eder. Sorun, dosya ikisinden yalnızca birini anlayan bir aractan geçtiğinde başlar. Damga vuran araçlar tipik şüphelilerdir. Biri, birkaç yeni nesneyle klasik tarzda artımlı bir güncelleme ekler: yeni bir xref bölümü, önceki bölüme giden bir /Prev bağı ve taze bir trailer. O trailer /XRefStm'yi düşürürse akış görünümü yetim kalır. Eski değeri ileri kopyalarsa akış görünümü, damga basılmadan önceki belgeyi tarif etmeye devam eder. Her iki durumda da indeksler artık anlaşmaz
İmza, bir kez tanıdığınızda kolayca tanınır. Bir görünümde görünen nesneler diğerinde eksiktir ya da bayattır. Akış üzerinden çözen bir okuyucu, güncellenmiş bir nesnenin düzenleme öncesi sürümünü bulur ya da eklenmiş olan için hiç girdi bulamaz. Yalnızca tabloya güvenen bir okuyucu düzenlemeyi görür ama yalnızca akışın konumlayabildiği her sıkıştırılmış nesneyi kaybeder. Kullanıcılar bunu, bir görüntüleyicide görünüp başka birinde kaybolan form alanları, damga adımı silmiş gibi görünen açıklamalar ya da bütünüyle yanlış nesneye düşen aramalar olarak tarif eder
Bu dosyaları kovalamayı bu kadar pahalı yapan Adobe Acrobat'tır. Bildirilen indeks baytlarla anlaşmadığında Acrobat çapraz referans verisini çoğu zaman nesne başlıklarını tarayarak sessizce yeniden kurar; böylece bozuk dosyayı üreten kişi onu açar, yanlış bir şey görmez ve yollar. Hata daha sonra ve başka bir yerde ortaya çıkar — bir preflight doğrulayıcısında, bir imza servisinde, bir arşiv alım işinde; bildirilen yapıya güvenen ve eksik nesne ya da çapraz referans uyuşmazlığı bildiren herhangi bir tüketicide. "Acrobat'ta sorunsuz açılıyor" cümlesi, neredeyse her hibrit ayrışma bildiriminin başlangıcıdır
Bunun HotPDF'te anlamı nedir?
Motor hibrit referans dosyalarını sizin için çözümler ve bunu çapraz referans verisini ayrıştırması gereken her yolda yapar. Bir belgeyi LoadFromFile ya da LoadFromStream ile yükleyin, değişikliklerinizi yapın ve SaveLoadedDocument çağırın; ya da bir girdi okuyup bir çıktı yazan EncryptFile gibi tek seferlik bir işlem çalıştırın. Her iki durumda da kurtarma /XRefStm'yi okur, akış bölümünü klasik girişlerin önünde birleştirir ve yazma onları listelemeden önce akışlarda yaşayan nesneleri çözer. AES-256 şifreleme yolu, bunun en görünür şekilde önem taşıdığı yerdir; çünkü bir belgeyi şifrelemek her nesneyi yeniden yazar ve her nesnenin çoktan konumlanmış olmasını gerektirir
// Tek seferlik: hibrit girdiyi okuyun, AES-256 şifreli bir kopya yazın
Pdf.EncryptFile('Letter_DOC.pdf', 'Letter_secured.pdf',
'owner-secret', '', aes256, [prPrint, prFillAnnotations]);
Aklınızda tutmaya değer ayrıntı, API'nin yukarısında yer alır. Word, Excel, PowerPoint ve uzun bir "PDF olarak kaydet" süreç hattı listesinden gelen dosyalar rutin olarak hibrittir, bu nedenle yalnızca kendi oluşturucu çıktınıza karşı çalıştırdığınız bir yükleyici testlerde bunlarla hiç karşılaşmayabilir. Test verilerinizi (fixtures) yalnızca kendi kodunuzun ürettiği dosyalarla değil, gerçek Ofis uygulamalarından dışa aktarılan belgelerle besleyin
Şüphelendiğiniz bir dosyayı kontrol etme
İki inceleme soruyu hızla çözer. Dosyayı bir onaltılık (hex) görünümde açın ve son startxref'ten sonraki baytları okuyun; hibrit bir dosya, fragman sözlüğü /XRefStm içeren kısa bir klasik bölüm gösterir. Veya tam bir ayrıştırmanın bildirdiği nesne sayısını, fragmandaki /Size'ın bildirdiği en yüksek nesne numarasıyla karşılaştırın. Büyük bir fark, nesnelerin yükleyicinin açmadığı akışlarda gizlendiği anlamına gelir; bu da daha sonra kaydetme zamanı hatasına dönüşen yetersizliğin aynısıdır
Tipik bir Excel dışa aktarımının kuyruğu ilk kontrolü somutlaştırır. Son xref anahtar kelimesinden sonraki her şey düz ASCII'dir, bu nedenle imza doğrudan onaltılık görünümden okunabilir (ofsetler açıklama amaçlıdır, ek açıklamalar eklenmiştir)
xref
0 16 % nesneler 0-15: eski bir okuyucunun ihtiyaç duyduğu
0000000000 65535 f
0000000017 00000 n
... % 14 satır daha; gerisi nesne akışlarında yaşar
trailer
<< /Size 216 % kullanımdaki en yüksek nesne numarasından bir fazlası
/Root 1 0 R
/Info 15 0 R
/ID [<5C9A...> <5C9A...>]
/XRefStm 87325 % çapraz referans akışının bayt ofseti
>>
startxref
88710 % yukarıdaki klasik bölümü gösterir
%%EOF
Trailer'daki /XRefStm anahtarı ele verir. Yukarındaki klasik bölüm, dosyanın kullandığı 216 nesne numarasından 16'sını listeler; nesne akışlarına paketlenen her şey ya onda yoktur ya da boş işaretlenmiştir. xref anahtar kelimesinde duran bir dedektör bu noktada yalnızca o kısmi indeksi görmüş olur. Kontrolü gözle yapmak yerine senaryolaştırmayı tercih ederseniz, işaret dosyanın son birkaç kilobaytı içindedir; sınırlı bir geriye doğru okuma yeter. Dosya sonradan artımlı güncellendiyse kuyrukta birden fazla trailer bulunur ve sayılan olan sondur
// Dosyanın kuyruğundaki en yeni trailer'dan /XRefStm ofsetini döndürür,
// ya da işaret yoksa -1 (dosya hibrit değil ya da hiç PDF değil)
function FindXRefStm(const FileName: string): Int64;
var
FS: TFileStream;
Tail: AnsiString;
Len, P, Q: Integer;
begin
Result := -1;
FS := TFileStream.Create(FileName, fmOpenRead or fmShareDenyWrite);
try
Len := 2048; // trailer kuyrukta yaşar
if FS.Size < Len then
Len := Integer(FS.Size);
FS.Position := FS.Size - Len; // sınırlı geriye doğru okuma: en fazla 2 KB
SetLength(Tail, Len);
FS.ReadBuffer(Tail[1], Len);
finally
FS.Free;
end;
P := 0; // son işareti tut: artımlı
Q := Pos(AnsiString('/XRefStm'), Tail); // güncellemeler daha yeni trailer'lar ekler
while Q > 0 do
begin
P := Q;
Q := Pos(AnsiString('/XRefStm'), Tail, P + 1);
end;
if P = 0 then
Exit; // kuyrukta hibrit işareti yok
Inc(P, Length('/XRefStm'));
while (P <= Len) and (Tail[P] in [' ', #9, #13, #10]) do
Inc(P); // anahtardan sonraki boşlukları atla
Result := 0;
while (P <= Len) and (Tail[P] in ['0'..'9']) do
begin
Result := Result * 10 + Ord(Tail[P]) - Ord('0');
Inc(P);
end;
end;
// Kullanım: negatif olmayan sonuç, akışın başladığı baytı adlandırır
if FindXRefStm('Invoice_XLS.pdf') >= 0 then
Writeln('hybrid-reference file: resave will need the /XRefStm section');
Sınayı bir ayrıştırıcı olarak değil önceliklendirme olarak ele alın: bir yeniden kayıt işi çalışmadan önce bir yığındaki hangi dosyaların ilgiyi hak ettiğini söyler, başka hiçbir şey
Onu gerçek girdilerden oluşan bir klasöre yöneltin ve dosyalar üç gruba ayrılır. Yalnızca klasik bir dosyanın kuyruğunda trailer anahtar kelimesi vardır ama /XRefStm yoktur; sınay -1 döndürür. Tamamen çapraz referans akışlarına bağlı bir dosya da -1 döndürür, doğru şekilde. Hiç trailer anahtar kelimesi yoktur; çünkü trailer anahtarları akış sözlüğünde yaşar. Yalnızca çift dizinli düzen bir ofset verir. Önceliklendirme raporunuzun ilk iki grubu ayırt etmesi gerekiyorsa, atmadan önce aynı kuyrukta trailer arayın; bir Pos çağrısı daha yeter
Sınay başıboş çalışmadan önce iki yerde sağlamlaştırın. Bir ofset yalnızca bir iddiadır. Ona gidin ve sözlüğü /Type /XRef diyen bir nesnenin gerçekten orada oturduğunu doğrulayın; çünkü hasarlı ya da kesilmiş bir dosya, adlandırdığı akış gitmiş olsa da anahtarı taşıyabilir. O dosya, sağlıklı bir hibritten farklı kovaya girer. Kontrol, aynı dosya işlemesinin üstüne birkaç satırdır
// Offset, FindXRefStm'in döndürdüğü değerdir; FS, aynı şekilde açılmış
// bir TFileStream, Head bir AnsiString'tir (Min, System.Math'ten gelir)
if Offset >= FS.Size then
Exit(False); // anahtar hayatta, akış değil
FS.Position := Offset;
SetLength(Head, Integer(Min(Int64(512), FS.Size - Offset)));
FS.ReadBuffer(Head[1], Length(Head));
Result := (Pos(AnsiString('/Type /XRef'), Head) > 0) or
(Pos(AnsiString('/Type/XRef'), Head) > 0); // PDF sözdizimi her iki yazıma izin verir
Sonra 2 KB'lik pencereyi bir parametre yapın. İki kilobayt sıradan Office çıktısını kapsar ama alışılmadık biçimde büyük bir trailer sözlüğü işareti menzil dışına itebilir; pencereyi genişletmek, dosyanın kazayla klasik ilan edilmesinden iyidir
Hibrit dosyaları bir süreç hattından yönlendirmek
Algılama, bir dosyanın bir sonraki nereye gideceğini değiştirdiğinde karşılığını verir. O yer, süreç hattının dosyayla ne yapmak üzere olduğuna bağlıdır
Yalnızca okunan, render edilen ya da doğrulanan dosyalar, her iki görünümü de çözen bir yükleyiciye gitmeli. PDFium Component, /XRefStm zincirini yükleme sırasında ayrıştırır; böylece kodunuzun gördüğü nesne tablosu hâlihazırda birleşik olandır ve nesne ve çapraz referans akışlarını doğrulama yazımızdaki kontroller değişmeden geçerlidir. Ayrışmış bir hibrit, yüklemeyi reddedecek kadar hasarlıysa motor bunu yükleme hata koduyla bildirir ve yapısal hasarın ürettiği kod FPDF_ERR_FORMAT'tır. Temiz bir yükleme daha zayıf bir kanıttır. PDFium tasarımı gereği hoşgörülüdür ve tutarsız dosyaların çoğunu söz etmeden yeniden kurar; başarı, dosyanın kurtarılabilir olduğunu kanıtlar, iki görünümünün anlaşıp anlaşmadığı hakkında hiçbir şey söylemez. O soru için, tam bir nesne yürüyüşünün bulduklarını trailer'ın /Size'ı ile karşılaştırmaya geri dönün
Süreç hattının değiştirdiği dosyalar başka bir hikâyedir. Buradaki tercihimiz nettir: dosyaların hibrit olmasını tamamen önlemek. Belgeyi HotPDF ile yükleyip tam kaydedin; çıktı, tek biçimde tek ve kendi içinde tutarlı bir çapraz referans taşır. /XRefStm yoktur, ayrışacak ikinci bir görünüm kalmamıştır, her nesne tam olarak bir indeks girdisine aittir. O normalleştirme, arşiv alımından önce, katı bir akış aşağısı RIP ya da imza servisinden önce ve hibrit bir girdiye uygulanan her düzenlemeden sonra istediğiniz şeydir. Tamamen önceki akış-önce birleşmesine dayanır; çünkü tam bir kayıt yalnızca yüklemenin konumlamayı başardığı nesneleri yazabilir
İmzalı belgeler istisnadır. Tam bir yeniden yazım her baytı taşır; bu, özgün bayt aralıkları üzerinden hesaplanmış her imzayı geçersiz kılar. İmzalı bir hibrite yapılacak değişiklik, her iki görünümü de eş tutan gerçek bir artımlı güncelleme olarak girmelidir ve yalnızca okunması gereken imzalı bir dosya süreç hattından dokunulmadan geçmelidir
Bu hikayenin yazma tarafı, nesne akışlarının ve sıkıştırılmış çapraz referansların ilk etapta nasıl üretildiği, nesne akışları ve artımlı güncellemeler hakkındaki makalemizde ele alınmıştır. Söz konusu hibrit dosya aynı zamanda çok büyük olduğunda, büyük PDF iş akışları için Doğrudan Dosya API'si (Direct File API) incelemesindeki yükleme teknikleri, her şeyi belleğe okumadan onu incelemenize olanak tanır. Her ikisi de, bu blogun başka yerlerinde ele alınan yükleme, düzenleme, şifreleme ve imzalama API'lerinin yanı sıra Delphi ve C++Builder için HotPDF Bileşeni'nin bir parçası olarak gönderilen ve burada açıklanan kurtarma işlemiyle doğal olarak eşleşir