Teknik Makale

Delphi'de PDF Garbage Collection: Mark and Sweep

Bir PDF'ten bir sayfayı silmek, fontlarını, görüntülerini ya da içerik akışlarını silmez. losLab PDF Library, trailer köklerinden ileriye doğru nesne grafiğini gezen ve hiçbir şeyin ulaşmadığı her dolaylı nesneyi kaldıran bir mark-sweep toplayıcısıyla bunları geri kazanır. Tam bir kaydetmede çalışır, varsayılan olarak kapalıdır ve düşürdüğü nesne sayısını döndürür

PDF sayfalarını silmek dosyayı neden küçültmez?

Çünkü sayfa silme bir depolama işlemi değil bir referans düzenlemesidir. DeletePages(StartPage, PageCount), sayfa nesnelerinin sayfa ağacıyla bağlantısını keser ve onlara işaret eden outline girdilerini onarır. Yapamadığı şey, o sayfaların kullandığı font programının, içerik akışının ve görüntü XObject'inin artık ölü olduğuna karar vermektir, çünkü silme anında dosyada başka kimin hâlâ onlara işaret ediyor olabileceğini kaydeden hiçbir şey yoktur. O nesneler doküman nesne listesinde kalır ve tam bir kaydetme hepsini geri yazar. Sonuç, bu destek konularının çoğunu başlatan şikayettir: bir müşteri sayfaların yüzde doksanını siler, kaydeder ve dosya yüzde iki küçülür. Daha kötüsü, sızıntı bileşir. Yükle, sil, kaydet, tekrar yükle, tekrar sil, tekrar kaydet ve sayfa sayısı düşerken dosya monoton biçimde büyür. Bu, canlı nesneleri küçülten font subsetting ve görüntü downsampling tarafından çözülen sorundan farklı bir sorundur. Burada nesneler çok büyük değildir. Basitçe artık dokümanın bir parçası değildirler

Kök küme sayfa ağacı değil trailer'dır

PDF nesne grafiğinin ters-referans alanı yoktur. Biçim, hiçbir referans sayacı ve hiçbir geri-işaretçi listesi tanımlamaz ve var olan /Parent anahtarları, sayfa ağacı gibi belirli yapılara aittir, nesne grafiğinin bütününe değil. Dolaylı bir nesnede hiçbir şey size kimin ona işaret ettiğini söylemez, bu yüzden "hâlâ nesne 47'yi kullanan biri var mı" sorusunun tam olarak bir cevabı vardır: bilinen bir kökten ileriye doğru gezinin ve oraya varıp varmadığınızı görün. Bu, losLab PDF Library'deki toplayıcının bir refcount şeması değil bir mark-sweep toplayıcısı olmasının nedenidir

Kökler dosya trailer'ından gelir (ISO 32000-1 §7.5.5). Üç anahtar onları taşır: sayfa ağacının, isimlerin, outline'ların, AcroForm'un ve meta verinin hepsinin sarktığı §7.7.2'nin doküman kataloğu /Root; doküman bilgi sözlüğü /Info; ve şifreleme sözlüğü /Encrypt. Kalan iki trailer anahtarı yem'dir. /ID iki bayt dizesinden oluşan bir dizidir ve /Prev, önceki çapraz referans bölümüne tamsayı bayt ofsetidir. Hiçbiri dolaylı bir referans değildir, bu yüzden hiçbiri bir kök katkısında bulunmaz. losLab PDF Library, üç adlandırılmış anahtar yerine tüm trailer sözlüğünü kuyruğa alır; bu hiçbir şeye mal olmaz ve herhangi bir özel trailer uzantısını canlı tutar

Gezintinin kendisi özyinelemeli değil yinelemelidir. Gezinti dolaylı bir referansla karşılaştığında yalnızca nesne numarasını ve üretimi kaydeder, ilgili yuvayı işaretler ve hemen dereference etmek yerine bir FIFO kuyruğuna iter; bu, derin sayfa ağaçlarını ve uzun outline zincirlerini çağrı yığınının dışında tutar ve aynı nesnenin iki kez çözülmesini durdurur. Doğrudan sözlükler, diziler ve akış sözlükleri, ziyaret edilmiş bir küme tarafından korunan ikinci bir kuyruğa girer, çünkü gerçek dokümanlar gerçek döngüler içerir: bir sayfanın /Parent'ı kendi sayfa ağacı düğümüne geri işaret eder ve outline öğeleri /Prev ve /Next üzerinden her iki yönde zincirlenir. Üretim numaraları eşleşmenin parçasıdır, süs değil. Bir referans yalnızca nesne numarası ve üretim ikisi de uyuştuğunda çözülür; farklı bir üretimde var olan bir sayıya referans, spesifikasyonun gerektirdiği null nesne olarak ele alınır, hiçbir zaman canlı bir kenar olarak değil

Bir kaydetmede garbage collection'ı nasıl etkinleştirirsiniz?

Garbage collection opt-in'dir ve kaydetme seçenekleri kaydına aittir. Varsayılan olarak False'tur, çünkü toplayıcı nesne grafiği üzerinde yıkıcı bir geçiştir ve hiçbir kütüphane bir çağıranın hiç incelemesini istemediği nesneleri sessizce silmemelidir

var
  Pdf: TPDFlib;
  Opt: TPDFlibSaveOptions;
begin
  Pdf := TPDFlib.Create;
  try
    if Pdf.LoadFromFile('report-500pages.pdf', '') <> 1 then
      Exit;
    Pdf.DeletePages(11, 490);          // keep the first ten pages

    FillChar(Opt, SizeOf(Opt), 0);
    Opt.CompressContent := True;
    Opt.CompressFonts := True;
    Opt.OptimizeContentStreams := True;
    Opt.PackObjectStreams := True;
    Opt.GarbageCollect := True;        // drop everything the pages left behind
    Pdf.SaveToFileOptions('report-10pages.pdf', Opt);
  finally
    Pdf.Free;
  end;
end;

Aynı toplayıcıya ulaşan iki giriş noktası daha var. SetGarbageCollect(1), bayrağı seçili doküman üzerinde ayarlar, bu yüzden sıradan bir SaveToFile ona uyar ve GarbageCollectObjects, geçişi hemen çalıştırır ve kaldırılan sahipsiz dolaylı nesne sayısını döndürür. Anlık biçim, günlüğe kaydetmek ya da assert etmek istediğiniz bir sayı istediğinizde kullanılacak olandır ve kontrol edilmeye değer, çünkü negatif bir dönüş bir sayaç değildir

var
  Removed: Integer;
begin
  Pdf.DeletePages(11, 490);
  Removed := Pdf.GarbageCollectObjects;
  if Removed < 0 then
    // The graph could not be fully decoded. Nothing was swept and the
    // document is unchanged; save it without GC or reject the input.
    LogWarning('object graph incomplete, GC skipped')
  else
    LogInfo(Format('reclaimed %d orphaned objects', [Removed]));
end;

O başarısızlık yolu göründüğünden daha önemlidir. Nesneler tembel biçimde çözülür ve hiç çözülmemiş bir nesne hiçbir referans açığa çıkarmaz. Toplayıcı çözülemeyen bir nesneyi boş bir düğüm olarak ele alsaydı, yalnızca onun üzerinden erişilebilen her şeyi süpürürdü. Bu yüzden gezinti her nesneye dokunurken çözmeyi zorlar ve tek bir çözme hatası tüm geçişi negatif bir sonuçla iptal eder ve dokümanı bayt-özdeş bırakır. Yalnızca kısmen anladığınız bir grafiği süpürmek, bir toplayıcının bozuk bir dosyayı yok edilmiş bir dosyaya dönüştürmesinin yoludur

Saf bir PDF toplayıcısını ne bozar?

İki ayrıntı ve ikisi de yüksek sesle değil sessizce başarısız olur. Birincisi nesne akışlarıdır. PDF 1.5'ten beri, akış olmayan bir nesne bir /ObjStm konteynerinin içinde sıkıştırılmış olarak yaşayabilir (§7.5.7) ve çapraz referans girdisi, konteyneri artı içindeki bir indeksi adlandıran bir tür 2 girdisidir. Sıkıştırılmış bir nesne bu yüzden yalnızca konteyneri üzerinden erişilebilir. Üyeyi işaretleyin, konteyneri süpürün çünkü hiçbir şey ona bir doküman nesnesi olarak referans vermedi, işte artık var olmayan bir nesneye işaret eden bir xref'e sahip bir dosya yazmış oldunuz. Konteyner yapısal depolamadır, doküman verisi değildir, bu yüzden gezdiğiniz nesne grafiğinde asla bir kenar olarak görünmez. losLab PDF Library, konteynerler gitmeden önce hayatta kalan her sıkıştırılmış üyeyi kaynak konteynerinden ayırarak bunu ele alır, ardından kaydetme hayatta kalanları taze nesne akışlarına yeniden paketler. İkinci ayrıntı, bir akış nesnesinin gerçekte neye referans verdiğidir. Baytlar grafiğin parçası değildir. /F1 12 Tf ile metin çizen bir içerik akışı, bir fontu kaynak ismiyle adlandırır ve o isim sayfa /Resources sözlüğü üzerinden çözülür, bu yüzden erişilebilirlik kenarı sayfa → /Resources/Font → font nesnesi olarak çalışır, hiçbir zaman akış yükü üzerinden değil. Bir akışın katkıda bulunduğu tek referanslar sözlüğünden gelir; burada /Length, /Filter ve /DecodeParms'ın hepsi dolaylı olmasına izin verilir. Referans arayarak akış baytlarını ayrıştıran bir toplayıcı boş yere pahalı iş yapıyordur; akış sözlüklerini atlayan bir toplayıcı ise uzunluk nesnesini kaybeder ve dosyayı bozar

Serbest bıraktığınız nesne numaralarına ne olur

Boş girdiler haline gelirler ve aynı kaydetmede yeniden kullanılmazlar. Süpürme, silmelerin indeks-kararlı kalması için nesne listesini azalan sırada gezer, arama indeksini her kaldırmadan sonra değil sonunda bir kez yeniden oluşturur ve kaldırılan her nesne için sayıyı, üretimi bir artırılmış olarak boş listede kaydeder, tam olarak §7.5.4'ün daha sonra yeniden kullanılabilecek bir girdi için belirttiği gibi. Zaten 65535'te olan bir üretim orada kalır ve o sayıyı kalıcı olarak emekliye ayrılmış olarak işaretler. Nesne numaraları kasıtlı olarak sıkıştırılmaz. Bir toplamadan sonra dosya boşluklar tutar: nesne 12 boş olabilirken 13 ve 14 kullanımda olabilir ve trailer /Size'ı hâlâ hayatta kalan sayı yerine en yüksek sayı artı biri bildirir. Bu yasaldır ve normaldir. Yeniden numaralandırma, çapraz referans tablosunda birkaç bayt kazandırır ve dokümandaki her referansın yeniden yazılmasını gerektirir; bu, dışarıdan nesne numaraları tutan herhangi bir şeyi sessizce geçersiz kılan türden bir değişikliktir. Geri aldığınız boyut, xref tablosundan değil nesne gövdelerinden gelir

Toplayıcıyı çalıştırmamanız gereken zaman

Bir artımlı güncellemede asla. Toplayıcı tam kaydetmelere kilitlenmiştir ve doküman eklenmekte olduğunda bayrak basitçe okunmaz ve bu kilit, etrafından dolaşılacak bir sınırlama değildir. Bir artımlı güncelleme (§7.5.6), orijinal baytları dokunulmadan bırakır ve /Prev üzerinden öncekine zincirlenmiş yeni bir çapraz referans bölümü ekler. Her erken revizyon hâlâ her zaman işaret ettiği nesnelere işaret eder, bu yüzden geçerli revizyonda erişilemez bir nesne daha eski bir revizyonda pekâlâ erişilebilirdir. Onu silmek son revizyon dışında her revizyonu bozardı ve nedeninin mekaniği artımlı güncellemeler ve append-mode kaydetme üzerine yazıda ele alınmıştır. Aynı akıl yürütme, imzalanmış bir dokümanda garbage collection'ı dışlar, çünkü toplamayı mümkün kılan tam yeniden yazma, imzayı geçersiz kılan şeyin ta kendisidir

Toplamanın ne olmadığını da netleştirmeye değer. Bir temizleyici (sanitizer) değildir. Toplayıcı hiçbir şeyin referans vermediği nesneleri kaldırır; içeriklerinin hassas olup olmadığı hakkında hiçbir fikri yoktur ve hâlâ referans verilen bir nesne her neyse öyle kalır. Amaç dosyayı küçültmek değil bilgiyi geri kazanılamaz kılmaksa, nesne grafiği yanlış katmandır ve talimat-seviyesi redaction ve doküman temizleme doğru olandır. İkisi bu sırayla iyi bir araya gelir: önce redact edin ve temizleyin, sonra toplayın, böylece redaction'ın ayırdığı nesneler dosyayı gerçekten terk eder. Aynı eşleşme, garbage-collect seçeneğini geçirmenin temizlemeyi sonrasında bir toplama çalıştırmasını ve kaldırdığı sahipsizleri OrphanObjectsRemoved'da raporlamasını sağladığı kaynak temizleme API'sinde de vardır

Benimsenmeye değer son bir alışkanlık. Sayfa silmelerinizi yapan hangi toplu işse onda GarbageCollectObjects'in dönüş değerini günlüğe kaydedin ve birkaç hafta gerçek dokümanlar üzerinde izleyin. Az önce yarıya indirdiğiniz bir dosyada sıfır, akış yukarısında bir şeyin hâlâ beklemediğiniz bir referans tuttuğu anlamına gelir; genellikle bir isim ağacı girdisi, bir outline hedefi ya da bağlı olduğu sayfadan sağ çıkmış bir AcroForm alanı. Toplayıcı, sahip olacağınız en ucuz erişilebilirlik hata ayıklayıcısıdır, çünkü PDF biçiminin kendisinin yanıtlamayı reddettiği soruyu yanıtlar

Burada anlatılan garbage collector, kaydetme-seçenekleri kaydı ve kaynak temizleme API'si, Delphi ve C++Builder için losLab PDF Library'nin bir parçasıdır; ürün sayfası, toplama, nesne-akışı paketleme ve doğrusallaştırma arasındaki etkileşim dahil tam kaydetme-pipeline referansını içerir