Teknik Makale

Delphi ile PDFiumPas: Seyrek ve Yavaş PDF Nesne Dizini

2 GB bir PDFden tek bir sözlük istiyorsunuz ve araç önce tüm çapraz başvuru tablosunu römork /Size değerine göre boyutlandırılmış bir diziye açar. PDFiumPas bu adımı seyrek ve yavaş bir nesne diziniyle değiştirir: yalnızca xref bölüm tanımlayıcılarını tutar, tek bir nesne numarasını istek üzerine sınırlı pencerelerle çözer ve yalnızca gerçekte dokunduğunuz girdileri önbelleğe alır

FPdfCompress içindeki bu kodun eski biçimi dürüsttü ama pahalıydı. ApplyDefaultOpenAction tüm dosyayı tek bir TBytes içine okur, sonra /Size değerine kadar nesne numarası başına bir yuva içeren yoğun bir TPdfActiveXrefEntries dizisi ayırırdı. Ölçek büyüdüğünde iki şey ters gitti. Çağıran taraf dört sözlük istediğinde bile okuma maliyeti belge boyutuyla doğrusal büyüyordu ve yoğun dizi ayrıştırıcı bütçesiyle çakışıyordu: TPdfParserResourceBudget.Default MaxObjects değerini 4.000.000 yapar; bu yüzden en yüksek nesne numarası o tavanın üstünde olan tamamen geçerli bir dosya, doğruluk gerekçesiyle değil bellek gerekçesiyle reddediliyordu

Delphi içinde PDFiumPas seyrek yavaş nesne dizininin yoğun bir çapraz başvuru dizisiyle karşılaştırılması: yoğun yol tüm dosyayı okur ve römork boyutuna kadar nesne numarası başına bir yuva ayırırken, seyrek yol yalnızca bölüm tanımlayıcılarını tutar
Bellekte yalnızca tanımlayıcılar kalır, girdiler dosyada kalır ve her okuma sınırlı bir bir mebibaytlık pencereden geçer

PDF herkese açık API'si bu soruyu neden yanıtlamaz?

Çünkü bilgi PDFiumun içinde vardır ama hiçbir zaman C sınırını geçmez. CPDF_Parser çapraz başvuru tablosunu, nesne akışı üyeliğini ve düzeltim önceliğini içeride tutar; ancak yayımlanmış üst bilgiler, bir nesne numarası alıp ham konumunu, neslini, hangi düzeltimin kazandığını ya da hangi ObjStm içinde yaşadığını döndüren hiçbir giriş noktası sunmaz. Kaydetme tarafı da aynı ölçüde kapalıdır: FPDF_SaveAsCopy ve FPDF_SaveWithVersion size yalnızca sıralı bir yazma geri çağrımı verir. Yerel bir kayıttan sonra katalogda yapılacak her bayt düzeyli yama bu yüzden Pascal katmanında kurulmak zorundadır; PDFiumPasin bu yapıları DLL'i yeniden kullanmak yerine kendisi ayrıştırmasının nedeni budur

Seyrek dizin bellekte gerçekte ne tutar?

Girdileri değil, tanımlayıcıları. Klasik bir tablo için (ISO 32000-1 §7.5.4) bir TPdfSparseXrefSubsection ilk nesne numarasını, nesne sayısını, girdi satırlarının başladığı bayt konumunu ve ölçülen girdi genişliğini saklar. Girdilerin kendisi dosyada kalır. Genişlik 20 bayt varsayılmak yerine ilk satırdan ölçülür, çünkü üreticiler satır sonları konusunda anlaşmazlığa düşer; PDFiumPas 18 ile 64 aralığını kabul eder ve bu bandın dışındaki her şeyi, bildirilen sayısı akışın sonunu aşacak her alt bölümle birlikte reddeder. Bir çapraz başvuru akışı için (§7.5.8) bölüm, her biri 0 ile 8 arasıyla sınırlanmış üç /W alan genişliğini, düzleştirilmiş /Index çiftlerini ve tek bir bayt şişirilmeden önce beklenen uzunluğu /W ve /Index değerlerinden hesaplanan çözülmüş girdi baytlarını tutar

Tüm dizin Initialize tarafından en çok 1 MiB'lik bir kuyruk penceresinden kurulur; startxref işte orada bulunur ve sonraki her nesne okuması 1 MiB'lik bir nesne penceresi kullanır. Ham akış tavanı 64 MiB değeridir ve tek bir xref satırı 1024 baytı aşamaz. PDFiumPas ile nesne ve çapraz başvuru akışlarını doğrulama notumuzu okuduysanız, aynı alan genişliği disiplini burada da geçerlidir; yalnızca artık tüm bir tabloyu denetlemek yerine tek bir girdiyi adreslemek için kullanılır

uses
  FPdfCompress;

var
  Source: TFileStream;
  Revision: TPdfSparseRevisionInfo;
begin
  Source := TFileStream.Create(FileName, fmOpenRead or fmShareDenyWrite);
  try
    { yalnızca startxref, /Prev zinciri ve katalog boyunca yürür }
    if ReadPdfSparseRevisionInfo(Source, Revision) then
    begin
      Writeln('root      ', Revision.RootObjectNumber, ' ',
        Revision.RootGeneration);
      Writeln('max obj   ', Revision.MaximumObjectNumber);
      Writeln('xref str  ', Revision.UsesXrefStream);
      Writeln('encrypted ', Revision.HasEncrypt);
      Writeln(string(Revision.CatalogDictionary));
    end;
  finally
    Source.Free;
  end;
end;

Tek bir arama tek bir nesneye nasıl ulaşır?

Her iki düzende de aritmetikle. Klasik bir alt bölümün sabit genişlikli satırları vardır; bu yüzden bir girdinin adresi, alt bölüm başlangıcı artı nesne konumu çarpı ölçülen genişliktir; PDFiumPas bundan sonra o tek satırı okur, on basamaklı konumu ve beş basamaklı nesli ayrıştırır, nesli §7.5.4 ten gelen 65535 tavanına karşı denetler ve sondaki anahtar sözcüğü axkDirect ya da axkFree olarak sınıflandırır. Bir çapraz başvuru akışı bir adım daha gerektirir, çünkü /Index alt bölümleri çözülmüş bayt akışında art arda dizilidir; bu yüzden dizin, toplanan /W genişliğiyle çarpmadan önce önceki alt bölümlerin sayılarını biriktirir. Tür 1 bir konum verir, tür 2 bir nesne akışı numarası ve bir üye dizini verir ve başka her şey bir tahmin yerine axkUnknown olur

{ klasik tablo, ISO 32000-1 bölüm 7.5.4 }
  EntryOffset := Subsection.EntryOffset +
  Int64(ObjectNumber - Subsection.FirstObject) * Subsection.EntryWidth;

{ çapraz başvuru akışı, ISO 32000-1 bölüm 7.5.8 }
  EntryWidth := Section.Widths[0] + Section.Widths[1] + Section.Widths[2];
  EntryPosition := Integer((PriorCount + ObjectNumber -
  Section.IndexValues[I]) * EntryWidth);

İki yoldan hiçbiri /Size ile orantılı değildir. Yeniden yazımın tüm amacı budur: römork boyutu değeri üst veri olarak taşınır ve artımlı düzeltim yazılırken kullanılır, ancak hiçbir zaman bir ayırmayı yönlendirmez. Gerileme test takımı bunu, sayfa ağacı /Size 1000000002 bildiren bir römork altında 1.000.000.000 ve 1.000.000.001 numaralı nesnelerde yaşayan bir sınama düzeneğiyle sabitler. Eski yoğun uygulama o dosyayı reddediyordu; seyrek dizin her iki başvuruyu da çözer ve bildirilen boyutu çıktı römorkunda korur

PDFiumPasin Delphi içinde tek bir nesne numarasını nasıl çözdüğü: klasik bir çapraz başvuru tablosu ölçülen satır genişliğini çarpar; bir çapraz başvuru akışı ise /W dizisinden toplanan alan genişliklerini çarpmadan önce önceki alt bölümlerin sayılarını biriktirir
Her iki arama da saf aritmetiktir; bu yüzden ikisi de römorkta bildirilen nesne sayısıyla orantılı değildir

Karma düzeltimler, /Prev zincirleri ve onları çevreleyen koruyucular

Düzeltim önceliği, saf bir yavaş dizinin yanlış gittiği yerdir. PDFiumPas zinciri startxref noktasından en yeni öncelikli sırayla yürür ve bir aramayı yanıt veren ilk bölümde durdurur; bu da öncelik kuralını birleştirilmiş bir tabloyu somutlaştırmadan yeniden üretir. Karma başvurulu dosyalar (§7.5.8.4) klasik dalın içinde ele alınır: römork bir /XRefStm taşıdığında, tamamlayıcı akış bölümü onu başvuran klasik bölümden önce kaydedilir; böylece düz tablonun göremediği sıkıştırılmış nesneler yine de bulunurken klasik girdiler konumunu korur. Daha eski düzeltimler bundan sonra /Prev üzerinden izlenir

Bu yürüyüşü iki koruyucu sınırlar ve ikisi de hasarlı dosyalarda önemlidir. Ziyaret edilen her konum kaydedilir; böylece zincirin içine geri işaret eden bir /Prev dönmek yerine sonlanır ve geçiş derinliği varsayılanı 1024 olan MaxRecursionDepth ile sınırlanır. Şifreleme bayrağı yalnızca en yeni römorktan okunmak yerine tüm zincir boyunca biriktirilir; çünkü en son römorku /Encrypt öğesini atlamış bir belge daha geride hâlâ şifreli olabilir; düzeltim ekleyen çağıran taraflar, şifreli bir dosyaya düz metin nesneler yazmayı reddetmek için o bayrağa güvenir

PDFiumPasin Delphi içinde karma bir PDF düzeltim zincirini nasıl yürüdüğü: bölümler startxref noktasından en yeni öncelikli kaydedilir, tamamlayıcı bir XRefStm bölümü onu adlandıran klasik tablodan öne geçer ve /Prev yürüyüşü ziyaret edilen konumlarla ve bir derinlik tavanıyla sınırlanır
Bir arama yanıt veren ilk bölümde durur; bu da düzeltim önceliğini birleştirilmiş bir tabloyu hiç somutlaştırmadan yeniden üretir

Tür 2 girdiler: nesne akışı neden bekler?

Tür 2 bir girdi bir nesne akışını adlandırır ve PDFiumPas bir çağıran taraf onun bir üyesini isteyene kadar o akışa dokunmaz. Sonunda istediğinde /Type /ObjStm doğrulanır, /N nesne bütçesine ve /First çözülmüş bayt tavanına karşı denetlenir ve her başlık çifti en az dört bayt gerektirdiği için /N değeri /First değerine karşı sağlık kontrolünden geçirilir. Akış ancak bundan sonra şişirilir ve başlık taraması tam bir üye tablosu kurmak yerine istenen üyede ve onun ardılında durur. Aynı anda yalnızca bir çözülmüş nesne akışı tutulur; bir sayfa ağacı dalının tek bir ObjStm içinde kümelendiği durumda doğru ödünleşim budur; Delphi içinde nesne akışı ve yordayıcı çözme hakkındaki yazımız o şişirme adımının içinde ne olduğunu kapsar (§7.5.7)

var
  Reader: TPdfSparseDictionaryReader;
  Generation: Integer;
  Dict: AnsiString;
begin
  { tek tutulan dizin, nesil farkında çok sayıda okuma }
  Reader := TPdfSparseDictionaryReader.Create(Source);
  try
    if Reader.Valid and
       Reader.ReadLatestDictionary(PageObjectNumber, Generation, Dict) then
      HandlePage(PageObjectNumber, Generation, Dict);
  finally
    Reader.Free;  { Source sizde kalır }
  end;
end;

Önbelleğin söz vermeyi bıraktığı yer

Dizin bir anlık görüntüdür ve bu konusunda açık sözlü olmaya değer. Bölümler Initialize içinde bir kez ayrıştırılır; altındaki akış sonradan değiştirilirse önbelleğe alınan her girdi bayatlar ve sınıf bunu fark etmez. TPdfSparseDictionaryReader dizini, kaynağın çağıran tarafa ait ömrü boyunca tutar; bu, bir sayfa ağacı üzerinde özyinelemeli bir yürüyüşün tam olarak istediği ve bir yeniden yazım boyunca kesinlikle yapmamanız gereken şeydir. Girdi önbelleği doğrusal aranan düz bir dizidir ve negatif sonuçları da saklar; bu yüzden birkaç yüz arama ucuzdur ve birkaç yüz bin arama değildir. ReadDictionary kesin bir nesil eşleşmesi isterken ReadLatestDictionary etkin olanı çözer ve fark bilinçlidir: başvuru çözümü ilkini, katalog incelemesi ikincisini gerektirir. Bu sınırların onurlandırılamadığı yerde, çevreleyen birimler hâlâ çalışan dosya kümesini daraltmak yerine eski tüm dosya ayrıştırıcısına geri döner; bu kalıbı büyük PDFlerin istek üzerine akışla aktarılması için de kullanırız

Derleyiciler arası gerileme testleri aynı davranışı üç araç zincirinin hepsinde kapsar; buna 2 MiB'lik bir kaynağın asla 1 MiB'den büyük tek bir okuma görmediğine ilişkin bir doğrulama da dahildir. Doğrudan PDF yapısına dokunan Delphi, C++Builder ya da Lazarus kodu bakımını yapıyorsanız ve dört sözlük için tüm dosya ayrıştırma bedelini ödemekten bıktıysanız, seyrek dizin ve onu çevreleyen herkese açık dikiş PDFiumPas Delphi PDFium bileşeninde yer alır