Microsoft Word veya Excel'den Farklı Kaydet ile bir belgeyi PDF olarak dışa aktardığınızda diskteki dosya, çoğu zaman hibrit referanslı (hybrid-reference) bir dosyadır. Çapraz referans (cross-reference) bilgisini iki kez taşır: bir kez 1.4 sürümüne kadar her PDF'nin sonunu oluşturan klasik sabit genişlikli tablo olarak ve bir kez de belgenin büyük kısmının aslında bağlı olduğu sıkıştırılmış çapraz referans akışı olarak. Tek bir trailer anahtarı olan /XRefStm, bu iki görünümü birbirine diker ve bir aracın tüm belgeyi görüp göremeyeceği bu anahtarı takip edip etmediğine bağlıdır
Bu makale, hibrit dosyalara tüketici (okuyucu) tarafından bakmaktadır: dosyanın sonundaki baytların nasıl göründüğüne, düzenleme aşamasında iki görünümün nasıl birbirinden uzaklaştığına ve bir Delphi boru hattının (pipeline) hibrit girdileri nasıl tespit edip yönlendirebileceğine odaklanmaktadır. Bir yükleyicinin bu görünümleri nasıl birleştirdiği ve sıranın neden tartışılamaz olduğu, hibrit referanslı dosyaların yüklenmesine dair HotPDF makalemizin konusudur; bu makale ise öncelikle bu yapıyı (layout) tanımakla ilgilidir
Office dışa aktarımları dizini neden iki kez yazar?
PDF 1.5, dosyanın şeklini değiştiren iki özellik sundu: nesne dizinini düz metin tablosu yerine sıkıştırılmış ikili (binary) veri olarak depolayan çapraz referans akışları (cross-reference streams) ve birçok küçük nesneyi Flate ile sıkıştırılmış tek bir kapsayıcıya (container) paketleyen nesne akışları (object streams). Bunları kullanan bir yazıcı daha küçük dosyalar üretir, ancak yapıların dayandığı xref anahtar kelimesi ve trailer sözlüğü (dictionary) artık olmadığı için bir PDF 1.4 okuyucusu bu sonucu açamaz
ISO 32000-1 §7.5.8.4 bu uzlaşmayı tanımlar. Hibrit referanslı bir dosya her ikisini de yazar: katalog (catalog) ve sayfa ağacı (page tree) da dahil olmak üzere eski bir okuyucunun ulaşması gereken nesneleri adresleyen klasik bir çapraz referans tablosu ve geri kalan her şeyi dizinleyen bir çapraz referans akışı. Nesne akışlarına katlanmış (folded) nesneler, klasik tabloda boş (free) olarak işaretlenir, böylece bir 1.4 okuyucusu şikayet etmeden bunları atlar; bu nesnelerin gerçek konumları yalnızca akışta bulunur. Ardından klasik trailer, o akışın bayt ofsetini (byte offset) tutan bir /XRefStm anahtarı taşır. Eski bir görüntüleyici bu anahtarı asla okumaz ve dosyayı tablo görünümünden oluşturur (render). Modern bir görüntüleyici ise onu takip eder ve belgenin tamamını görür. Word ve Excel yıllardır tam olarak bu yapıyı dışa aktarmaktadır; bu yüzden hibrit dosyalar nadir karşılaşılan tuhaf vakalar değil, iş boru hatlarının aldığı dosyaların büyük bir bölümüdür
Hibrit bir dosyanın kuyruğu (tail) nasıl görünür?
Bu yapıyı anlamanın en kolay yolu baytlara bakmaktır. Aşağıda ofsetleri kısaltılmış küçük bir hibrit dosyanın kuyruğu yer almaktadır; gerçek bir Office dışa aktarımında /XRefStm değeri genellikle dosyanın sonuna yakın büyük bir ofsettir. Okuma sırası, PDF dosya yapısına genel bakış makalemizde açıklanan kuyruktan başa doğru (tail-first) yürüme işlemidir: %%EOF bul, startxref oku, tabloya atla
% ... body objects, including object streams and, at byte 116,
% the cross-reference stream (a stream object with /Type /XRef) ...
xref % classic section: what startxref points at
0 4
0000000000 65535 f % slot 0: head of the free list, always present
0000000017 00000 n % object 1: the catalog, visible to any reader
0000000000 65535 f % object 2: marked free -- lives in an object stream
0000000000 65535 f % object 3: same; only the stream view locates it
trailer
<<
/Size 4
/Root 1 0 R
/XRefStm 116 % byte offset of the cross-reference stream
>>
startxref
7164 % byte offset of the 'xref' keyword above
%%EOF
Bu dökümdeki iki ayrıntı tüm mekanizmayı taşır. İlk olarak, startxref kasıtlı olarak klasik bölümü işaret eder: bu, eski bir okuyucunun inmesi gereken adrestir. Çapraz referans akışına yalnızca trailer sözlüğü içindeki /XRefStm anahtarı üzerinden ulaşılabilir; bu yüzden bu anahtarı asla aramayan bir ayrıştırıcı (parser), akışın var olduğunu da asla öğrenemez. İkincisi, 2. ve 3. nesneler iyi huylu birer yalandır. Klasik tablo onları boş (free) ilan eder, ancak onlar sıkıştırılmış bir kapsayıcı (container) içinde oturan gerçek nesnelerdir; bu boş işareti, bir 1.4 okuyucusunun kullanamayacağı girdilere takılıp tökezlemesini önler. Sadece klasik görünüme güvenen bir tüketici, belgenin büyük kısmının var olmadığı sonucuna varır
İki görünüm birbirinden nasıl uzaklaşır?
Word'den yeni çıkmış bir hibrit dosya kendi içinde tutarlıdır: her iki görünüm de kendi beyan ettiği kapsam içinde aynı belgeyi tanımlar. Sorun, dosyanın yalnızca bir görünümü anlayan bir araç tarafından düzenlenmesiyle başlar. Klasik tarzda (classic-style) artımlı bir güncelleme (incremental update) ekleyen bir mühürleme (stamping) aracını düşünün: yeni nesneler, yeni bir xref bölümü, önceki bölüme bir /Prev zinciri ve yeni bir trailer. Eğer bu trailer /XRefStm anahtarını düşürürse, akış görünümü öksüz kalır; eğer eski değeri ileriye kopyalarsa, akış görünümü belgeyi hala düzenlemeden önceki haliyle tanımlamaya devam eder. Her iki durumda da, iki dizin artık dosyanın ne içerdiği konusunda anlaşmazlığa düşmüştür
Ortaya çıkan dosyanın belirgin bir hata işareti vardır: bir görünümde görünen nesneler diğerinde eksiktir veya bayattır (stale). Akış görünümü üzerinden çözen (resolve) bir okuyucu, güncellenmiş bir nesnenin düzenleme öncesi sürümünü bulur veya eklenmiş bir nesne için hiçbir girdi bulamaz. Tablo görünümündeki bir okuyucu ise düzenlemeyi görür, ancak sadece akışın konumlandırdığı sıkıştırılmış nesnelerin izini kaybeder. Uygulamada bu durum; bir görüntüleyicide sağlam kalıp diğerinde yok olan form alanları, bir mühürleme işleminin silmiş gibi göründüğü notlar (annotations) veya tamamen yanlış bir nesneye inen aramalar (lookups) olarak yüzeye çıkar
Bu dosyalardaki hataları ayıklamayı maliyetli kılan şey, Adobe Acrobat'ın genellikle bunları şikayet etmeden açmasıdır: dizin baytlarla uyuşmadığında, nesne başlıklarını tarayarak çapraz referans verilerini sessizce yeniden oluşturur, böylece bozuk dosyayı üreten kimse bir sorun görmez. Hata ancak daha sonra, dosya sıkı bir tüketiciye (strict consumer), baskı öncesi (preflight) doğrulayıcıya, bir imzalama hizmetine veya beyan edilen yapıya güvenen ve eksik nesneler ya da çapraz referans uyuşmazlığı bildiren bir arşiv alma (archival ingest) işine ulaştığında gün yüzüne çıkar. "Acrobat'ta sorunsuz açılıyor" ifadesi, neredeyse her hibrit senkronizasyon bozukluğu bildiriminin başlangıç cümlesidir
Saf Delphi'de hibrit bir dosyayı algılama
Girdileri sınıflandırmak için bir PDF kütüphanesine gerek yoktur. /XRefStm anahtarı yalnızca klasik bir trailer sözlüğü içinde bulunabilir ve aktif trailer dosyanın son birkaç kilobaytlık kısmı içinde oturur, çünkü şartname (specification) %%EOF işaretinin fiziksel sonun yakınında görünmesini zorunlu kılar. Sınırlandırılmış bir kuyruk penceresini (tail window) okumak ve içinde arama yapmak, önceliklendirme (triage) için yeterlidir:
uses
System.SysUtils, System.Classes, System.StrUtils, System.Math;
function IsHybridReferencePdf(const FileName: string): Boolean;
const
TailWindow = 2048;
var
Stream: TFileStream;
Buf: TBytes;
Tail: string;
Len, TrailerPos, NextPos, KeyPos, StartXrefPos: Integer;
begin
Result := False;
Stream := TFileStream.Create(FileName, fmOpenRead or fmShareDenyWrite);
try
if Stream.Size < 48 then
Exit;
Len := Min(TailWindow, Integer(Stream.Size));
SetLength(Buf, Len);
Stream.Position := Stream.Size - Len;
Stream.ReadBuffer(Buf[0], Len);
finally
Stream.Free;
end;
// Every keyword involved is 7-bit ASCII, so a byte-wise decode is safe
Tail := TEncoding.ANSI.GetString(Buf);
// Find the LAST 'trailer' keyword: with incremental updates,
// the newest trailer is the one that governs the file
TrailerPos := 0;
NextPos := Pos('trailer', Tail);
while NextPos > 0 do
begin
TrailerPos := NextPos;
NextPos := PosEx('trailer', Tail, NextPos + 1);
end;
if TrailerPos = 0 then
Exit; // no classic trailer: a pure xref-stream file, not hybrid
// A hybrid trailer carries /XRefStm between 'trailer' and 'startxref'
KeyPos := PosEx('/XRefStm', Tail, TrailerPos);
StartXrefPos := PosEx('startxref', Tail, TrailerPos);
Result := (KeyPos > 0) and
((StartXrefPos = 0) or (KeyPos < StartXrefPos));
end;
Bu üç sonuç, üç farklı yapıyla örtüşür. Sadece klasik yapıda olan bir dosyanın trailer'ı vardır ama /XRefStm'i yoktur: False. Kendini tamamen çapraz referans akışlarına adamış bir dosyanın trailer anahtar kelimesi hiç yoktur, trailer anahtarları akış sözlüğünde yaşar: bu dosya da doğru bir şekilde False döndürür, çünkü böyle bir dosya hibrit değil, sıkıştırılmıştır. Sadece çift dizinli yapı True döndürür
Üretim (production) kullanımı için, iki sertleştirme (hardening) fazladan kod yazmaya değerdir. /XRefStm'den sonraki tam sayıyı ayrıştırın, o ofsete gidin (seek) ve orada /Type /XRef olan bir akış nesnesinin gerçekten bulunduğunu doğrulayın; kesilmiş (truncated) bir dosya akış yokken anahtarı taşıyabilir ve bu, sağlıklı bir hibrit dosyadan farklı bir sepete aittir. Ayrıca pencere boyutunu bir parametre olarak ele alın: 2 KB sıradan Office çıktılarını kapsar, ancak alışılmadık derecede büyük bir trailer sözlüğü anahtar kelimeyi menzil dışına itebilir ve pencereyi genişletmek dosyayı kazara klasik ilan etmekten daha iyidir
Hibrit dosyaları bir Delphi boru hattı üzerinden yönlendirme
Algılama size bir yönlendirme kararı kazandırır. Yalnızca okunan, oluşturulan veya doğrulanan dosyalar için her iki görünümü de çözen bir yükleyici kullanın ve ardından baytlar yerine davranışı doğrulayın. PDFium Bileşeni, yükleme sırasında /XRefStm zincirini ayrıştırır, böylece kodunuzun gördüğü nesne tablosu birleştirilmiş (merged) olandır ve nesne ve çapraz referans akışlarını doğrulama konulu makalemizde açıklanan kontroller değişiklik olmadan uygulanır. Eğer senkronizasyonu bozulmuş bir hibrit dosya yüklemeyi reddedecek kadar hasar görmüşse, motor bunu hata seti üzerinden raporlar: FPDF_ERR_SUCCESS, FPDF_ERR_UNKNOWN, FPDF_ERR_FILE, FPDF_ERR_FORMAT, FPDF_ERR_PASSWORD, FPDF_ERR_SECURITY ve FPDF_ERR_PAGE, bunlardan FPDF_ERR_FORMAT yapısal hasarın ürettiği hatadır. Yine de o sinyale güvenmeyin: PDFium tasarımı gereği hoşgörülüdür ve tutarsız dosyaların çoğunu sessizce yeniden oluşturur; bu nedenle başarılı bir yükleme dosyanın iki görünümünün anlaştığını değil, kurtarılabilir olduğunu kanıtlar. Anlamlı tutarlılık kontrolü, tam bir nesne yürüyüşünün bulduklarını trailer'ın /Size değerinin beyan ettiğiyle karşılaştırmaktır
Boru hattınızın değiştirdiği dosyalar için en güvenli politika, onların hibrit olmalarını tamamen durdurmaktır. HotPDF aracılığıyla bir yüklemenin ardından tam bir kaydetme, belgeyi tek biçimde, tek ve kendi içinde tutarlı bir çapraz referansla yeniden yazar: /XRefStm yoktur, senkronizasyondan çıkacak ikinci bir görünüm yoktur, her nesne tam olarak tek bir dizin girdisine aittir. Bu normalleştirme; arşiv alımından önce, aşağı akış (downstream) yönündeki katı bir RIP veya imzalama hizmetinden önce ve hibrit bir girdiye uygulanan herhangi bir düzenlemeden sonra isteyeceğiniz şeydir. Bu işe yarar çünkü yükleyici girişte görünümleri doğru bir şekilde birleştirmiştir; HotPDF hibrit referans makalesinin ayrıntılı olarak ele aldığı mekanizma budur
Kendi haline bırakılması gereken tek dosya sınıfı, dijital olarak imzalanmış belgelerdir. Tam bir yeniden yazma işlemi her baytı taşır ve bu, orijinal aralıklar üzerinden hesaplanan her türlü imzayı geçersiz kılar. İmzalanmış bir hibrit dosyadaki değişiklik, her iki görünümü de koruyan uygun bir artımlı güncelleme olarak içeri girmelidir; yalnızca okunması gereken bir dosya ise dokunulmadan geçmelidir. Normalleştirme sizin sahip olduğunuz dosyalar içindir; imzalı dosyalara ise sadece ekleme (append) yaparsınız
Hibrit referanslı PDF'ler bozuk formda değildir; formatın kendi uyumluluk köprüsüdürler ve PDF 1.4 okuyucuları kurulu tabanda (install base) hayatta kaldığı sürece Office uygulamaları bunları üretmeye devam edecektir. /XRefStm anahtarını fark edebilen, PDFium Bileşeni ile birleştirilmiş belgeyi doğrulayabilen ve HotPDF Bileşeni ile temiz, tek dizinli bir çıktı yeniden üretebilen bir boru hattı, onlara asıl oldukları gibi davranır: trailer'ında fazladan bir yön tabelası olan sıradan girdiler