Teknik Makale

Delphi'de Word ve Excel'den Hibrit Referanslı PDF'leri Yükleme

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; iş bir EListError ile başarısız olur veya çıktı, hasarlı bir çapraz referans (cross-reference) uyarısıyla açılır. Dosya hiçbir zaman bozuk değildi. O bir hibrit referans (hybrid-reference) 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ş (free) olarak 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: Fragmanı, gerçek kurtarma verilerinin oturduğu /XRefStm'yi gösteren, sıklıkla xref ve ardından bir 0 0 alt bölüm başlığından başka bir şey olmayan kısa bir klasik bölüm

Doğ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');  // sayfa sayısı doğrudur
    // yüklü belgeyi burada inceleyin veya düzenleyin
    Pdf.SaveLoadedDocument('Invoice_secured.pdf');     // her nesneyi yürür
  finally
    Pdf.Free;
  end;
end;

Erken çıkış dedektörü yerindeyken, yükleme iyi görünür ve yeniden kaydetme, olmayan nesnelerin kendilerini duyurduğu yerdir. Düzeltme, başlangıçta daha fazla bayt okumak değil; dosyanın bittiğine karar vermeden önce hibrit fragmanı tanımak ve /XRefStm'yi takip etmektir

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

Bunun HotPDF'te anlamı nedir?

Motor, hibrit referans dosyalarını sizin için çözümler ve bunu çapraz referans verilerini ayrıştırması gereken her yolda yapar. LoadFromFile veya LoadFromStream ile bir belge yükleyin, değişikliklerinizi yapın ve SaveLoadedDocument çağrısı yapın; veya bir girdi okuyan ve bir çıktı yazan EncryptFile gibi tek seferlik bir işlem çalıştırın. Her iki durumda da kurtarma işlemi /XRefStm'yi okur, akış bölümünü klasik girişlerin önünde birleştirir ve yazma bunları listelemeden önce akışlarda yaşayan nesneleri çözümler. AES-256 şifreleme yolu, problemin kendini ilk gösterdiği yerdir, çünkü bir belgeyi şifrelemek her nesneyi yeniden yazar ve bu nedenle her nesnenin halihazırda konumlandırılmış 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 0                          % boş klasik alt bölüm: hiçbir satır yok
trailer
<< /Size 216                 % kullanımda olan 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 kayması (offset)
>>
startxref
88710                        % yukarıdaki klasik bölümü gösterir
%%EOF

0 0 alt bölümü ipucudur: Sıfır girişli klasik bir tablo yalnızca fragmanı taşımak için vardır ve fragman esas olarak /XRefStm 87325 demek için vardır. xref anahtar kelimesinde duran bir dedektör, bu noktada hiçbir şeyin dizinini görmemiştir. Kontrolü gözle yapmak yerine senaryolaştırmayı tercih ettiğinizde, işaret her zaman dosyanın son birkaç kilobaytı içinde yer alır, bu nedenle sınırlı bir geriye doğru okuma yeterlidir

// Dosyanın kuyruğundan /XRefStm ofsetini döndürür, veya işaret
// yoksa (dosya hibrit değilse veya hiç PDF değilse) -1 döndürür
function FindXRefStm(const FileName: string): Int64;
var
  FS: TFileStream;
  Tail: AnsiString;
  Len, P: Integer;
begin
  Result := -1;
  FS := TFileStream.Create(FileName, fmOpenRead or fmShareDenyWrite);
  try
    Len := 2048;                        // fragman kuyrukta yaşar
    if FS.Size < Len then
      Len := Integer(FS.Size);
    FS.Position := FS.Size - Len;       // sınırlı geriye doğru okuma: maks 2 KB
    SetLength(Tail, Len);
    FS.ReadBuffer(Tail[1], Len);
  finally
    FS.Free;
  end;
  P := Pos(AnsiString('/XRefStm'), Tail);
  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ı geç
  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 bir sonuç, akışın başladığı baytı adlandırır
if FindXRefStm('Invoice_XLS.pdf') >= 0 then
  Writeln('hibrit referanslı dosya: yeniden kaydetme /XRefStm bölümüne ihtiyaç duyacak');

Bu incelemeyi bir ayrıştırıcı olarak değil, bir önceliklendirme (triage) olarak ele alın: Size bir gruptaki hangi dosyaların yeniden kaydetme işi çalışmadan önce dikkat etmeye değer olduğunu söyler, daha fazlasını değil. Bir yükleyicinin bulduğu ofsetle ne yapması gerektiği, bölüm zincirini takip etmesi, akış girişlerini klasik olanların önünde birleştirmesi, boş giriş nöbetçilerine uyması, Ofis uygulamalarından hibrit referanslı PDF'leri ele alma hakkındaki eşlik eden makalemizde adım adım açıklanmıştır

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