Delphi için PDFium Component (PDFiumPas), PDF dosyalarını TPdf üzerindeki üç yeniden yazım API'siyle küçültür: SaveAsReachable, hiçbir şeyin referans vermediği nesneleri atar; SaveAsDeduplicated, bayt düzeyinde özdeş font, görüntü ve Form XObject'leri birleştirir; SaveAsFlateRecompressed, mevcut Flate stream'lerini azami sıkıştırma düzeyinde yeniden kodlar. Her biri bir rapor kaydı döndürür; böylece neyin atıldığını ve kaç bayt kazandırdığını görürsünüz
Alışılmış tetikleyici, "yirmi sayfa sildik ve dosya büyüdü" diyen bir destek kaydıdır. Biri bir sözleşme açmış, bir eki çıkarmış, bir kapak sayfası eklemiş ve kaydetmiş. Her adım bir artımlı güncellemeydi; eski nesneler hâlâ dosyanın içinde ve o belge sonradan yüklenip FPDF_SaveAsCopy ile yazıldığında olduğu gibi geri çıkar. PDFium, bellek içi nesne tablosunda duran her neyse onu serileştirir; dolaylı nesneleri listelemek, birini silmek ya da referansları yeniden yazmak için genel bir çağrısı yoktur. Bu yüzden bu yazıdaki her şey, PDFium'un ürettiği baytların üstündeki PDFiumPas sarmalayıcı katmanında çalışır
FPDF_SaveAsCopy silinmiş sayfaları dosyada neden tutar?
FPDF_SaveAsCopy, yetim nesneleri tutar; çünkü PDFium, cross-reference tablosundaki her canlı girişi belgesine yükler, ona hâlâ bir şeyin işaret edip etmediğine bakmaksızın. ISO 32000-1 §7.5.6, artımlı güncellemeleri dosyanın sonuna yeni nesne sürümleri ve yeni bir xref bölümü eklemek olarak betimler. Bir sayfayı kaldırmak yalnızca sayfa ağacını düzenler; page dictionary, onun content stream'i ve yalnızca o sayfanın kullandığı font ya da görüntüler hâlâ kullanımdaki nesneler olarak listelenir. Eksiksiz bir artımsız kayıt bunu değiştirmez; çünkü nesne tablosunun gözünden o nesneler kusursuz geçerlidir
Aynı artıklar, sayfa birleştirmelerinden, başka araçların yaptığı form düzleştirmelerinden ve hasarlı dosyaların onarım kayıtlarından sonra da ortaya çıkar. Her görüntüleyicide görünmezdirler ve bayttan başka bir şey eklemezler. Taranmış belgelerde tek bir yetim kalmış sayfa görüntüsü birkaç megabayt olabilir
SaveAsReachable ile mark-and-sweep
TPdf.SaveAsReachable, yetimleri klasik bir mark-and-sweep turuyla kaldırır: PDFium'un geçerli belgenin artımsız bir kopyasını yazmasına izin verir, o kopyayı bir nesne grafiğine ayrıştırır, köklerden ulaşılabilen her şeyi işaretler ve yalnızca işaretlenmiş nesneleri içeren tek bir klasik revizyon yazar. Object stream'lerin içindeki nesneler yol boyunca sıradan dolaylı nesneler olarak maddileştirilir; böylece çıktı daima düz bir xref tablosu kullanır
Kökleri seçmek, naif bir uygulamanın yanıldığı yerdir. Bariz tohum, trailer'ın /Root adlandırdığı Catalog'tur. Oradan gezinme, sayfa ağacına, page resource'lara, fontlara, görüntülere, Form XObject'lere, AcroForm'a, yapı ağacına, outline'lara, name tree'lere, optional content'e, XMP meta verisine, output intent'lere ve gömülü dosyalara ulaşır. Ama belge bilgi sözlüğüne trailer'ın /Info girişi referans verir (ISO 32000-1 §7.5.5), Catalog değil. Yalnızca /Rootdan tohumlarsanız işlediğiniz her dosyanın Title, Author ve Producer'ını sessizce silersiniz. PDFiumPas gezinmeyi hem /Root hem /Info ile tohumlar ve yeniden kurulan trailer, /Infoyu yalnızca o nesne gerçekten hayatta kaldığında ileri taşır
Gezgin, açık bir yığın ve bir ziyaret kümesiyle derinlik öncelikli bir gezinmedir; böylece referans döngüleri (sayfa ağacındaki parent bağlantıları, annotation /P geri işaretçileri) onu asılı tutamaz. Zaten boş olan bir nesne numarasına yapılan referans, hata sayılmak yerine hiç izlenmez; bu da turu hafif hasarlı girdiye karşı affedici tutar
Nesne numaraları korunur, yeniden numaralanmaz
TPdf.SaveAsReachable, hayatta kalan her nesneyi özgün nesne numarası ve generation'ında tutar. Süpürülen nesneler xref tablosunda boş girişler olarak yazılır (ISO 32000-1 §7.5.4), canlı numaralar arasındaki boşluklar boş girişlerle doldurulur ve nesne 0 boş listenin başı olarak kalır. Hiçbir şey yeniden numaralanmadığı için hayatta kalan her referans, tek bir bayt yeniden yazım olmadan hâlâ doğrudur ve yanlış yapılacak eski-yeni bir eşleme yoktur. Bedeli, her biri 20 bayt olan birkaç kullanılmayan xref satırıdır; yapılmaya değer bir takas
uses
PDFium, FPdfCompress;
procedure PruneOrphans(const SourcePdf, TargetPdf: string);
var
Pdf: TPdf;
Report: TPdfPruneReport;
begin
Pdf := TPdf.Create(nil);
try
Pdf.FileName := SourcePdf;
Pdf.Active := True;
if not Pdf.SaveAsReachable(TargetPdf, Report) then
raise Exception.Create(Report.ErrorMessage);
Writeln(Format('%d bytes -> %d bytes, kept %d object(s), pruned %d',
[Report.SourceByteCount, Report.OutputByteCount,
Report.KeptObjectCount, Report.PrunedObjectCount]));
finally
Pdf.Free;
end;
end;
TPdfPruneReport, SourceByteCounti (PDFium'un ürettiği artımsız kopyanın boyutu), OutputByteCountu, KeptObjectCountu, PrunedObjectCountu, Succeeded ile ErrorMessagei taşır. Dosya adı overload'u hedefin yanındaki bir geçici dosyaya yazar ve hedefi yalnızca yeniden yazım başarılı olduğunda değiştirir; böylece başarısız bir çalıştırma asla kesilmiş bir PDF bırakmaz
Bu API'ler hangi dosyaları reddeder?
Üç yeniden yazım API'si de şifreli belgeleri, varsayılan olarak da imzalı olanları reddeder. Şifreli bir girdi, istisna yerine Succeeded = False ve bir hata mesajıyla başarısız olur: tek revizyonlu bir yeniden yazım, şifreleme sözlüğünün dayandığı nesne numaralarını, bayt aralıklarını ve nesne başına anahtarları koruyamaz; çıktı okunamaz olurdu. Dijital imzalı bir belge, AllowSignedDocument = True geçirmediğiniz sürece EPdfError yükseltir; çünkü eksiksiz bir yeniden yazım imzalı bayt aralığını her zaman geçersizleştirir. O bayrağı ayarlamak, imzaları bozuk rapor edecek bir dosyayı kabul ettiğiniz demektir; arşiv kopyası için doğru karar, geri göndereceğiniz bir sözleşme için asla değil
SaveAsDeduplicated iki görüntünün aynı olduğuna nasıl karar verir?
TPdf.SaveAsDeduplicated, iki kaynağı yalnızca sözlükleri, bütün ulaşılabilir alt grafikleri ve ham stream baytları eşleştiğinde özdeş sayar. Önce aynı ulaşılabilirlik gezinmesini çalıştırır; sonra ulaşılabilir her fontu, image XObject'i ve Form XObject'i parmak izler, eşleşen her grubu en düşük nesne numaralı üyenin üzerine birleştirir ve her referansı o hayatta kalana işaret edecek şekilde yeniden yazar. Birleştirilen kopyalar, yetimler gibi boş xref girişlerine dönüşür. Bu, her sayfada aynı resource'ları yeniden gömen mail-merge araçlarının ürettiği klasik "logo 400 kez gömülü" raporunun çözümüdür
Parmak izi, özyinelemeli hesaplanan 64 bitlik bir FNV-1a hash'idir. Her aday için gezgin, nesne gövdesini alır; her N G R referansını işaret ettiği nesnenin parmak iziyle değiştirir ve sonucu hash'ler. Bu, hash'i nesne numaralandırmasından bağımsız kılar: descriptor'ları tesadüfen 12 ve 87 nesneleri olan aynı fontun iki kopyası hâlâ aynı değeri üretir. Sonuçlar memoize edilir; böylece paylaşılan alt nesneler bir kez hash'lenir ve bir visiting kümesi, geri kenarlar için sabit bir sentinel döndürür; böylece döngüsel grafikler sonlanır
İnce hata stream yükünde oturur. Referansları güvenle bulmak için PDFiumPas, stream verisi, metinler ve yorumların boşluklarla silinmiş olduğu her nesnenin bir kopyasını tarar; yoksa tesadüfen 12 0 R gibi görünen bir ikili piksel verisi dizisi referans sanılırdı. Yalnızca o arındırılmış kopyayı hash'lerseniz, özdeş sözlüklü (aynı /Width, /Height, /BitsPerComponent, /Filter, /Length) ama pikselleri farklı iki görüntü tek parmak izine çöker ve sayfadaki ikinci fotoğraf sessizce birincisiyle değiştirilir. SaveAsDeduplicated, stream ile endstream arasındaki ham baytları hash'lenen veriye ekler; böylece pikseller kimliğin parçasıdır. Adaylar ayrıca karşılaştırmadan önce kategoriye göre gruplanır; böylece font ile görüntü arasındaki nadir bir hash çarpışması onları asla birleştiremez
Referans yeniden yazımı aynı arındırılmış tarama hilesini ters yönde yeniden kullanır: referanslar silinmiş kopyada bulunur ama baytlar özgün gövdeden kopyalanır; böylece yalnızca dictionary ve dizi yapısı değiştirilir ve ikili yükler dokunulmadan geçer
procedure MergeDuplicateResources(Pdf: TPdf; const TargetPdf: string);
var
Report: TPdfDedupReport;
begin
if not Pdf.SaveAsDeduplicated(TargetPdf, Report) then
raise Exception.Create(Report.ErrorMessage);
Writeln(Format('fonts %d, images %d, forms %d merged; %d -> %d bytes',
[Report.MergedFontCount, Report.MergedImageCount,
Report.MergedFormXObjectCount,
Report.SourceByteCount, Report.OutputByteCount]));
end;
TPdfDedupReportu dikkatle okuyun. Üç Merged*Count alanının yanında KeptObjectCount ile PrunedObjectCount vardır ve bu raporda PrunedObjectCount yalnızca birleştirilmiş kopyaları sayar. Ulaşılamayan nesneler yine de atılır ama o sayıya dâhil edilmez. Günlükleme için yetim sayısına ihtiyacınız varsa onu ayrı bir SaveAsReachable çalıştırmasından alın. Öbür sınır, eşleşmenin kodlanmış stream üzerinde bayt düzeyinde tam olmasıdır: iki farklı üretici tarafından iki farklı Flate düzeyinde sıkıştırılmış aynı logo birleştirilmez
Flate stream'lerini azami düzeyde yeniden sıkıştırmak
TPdf.SaveAsFlateRecompressed, ulaşılabilir her FlateDecode stream'ini açar (inflate) ve en yüksek sıkıştırma düzeyinde yeniden sıkıştırır (deflate); sonucu yalnızca daha küçük olduğunda ve eşiklerinizi geçtiğinde tutar. Birçok üretici Flate'i hızlı bir varsayılan düzeyde yazar; bu yüzden content stream'leri ve gömülü font programları çoğu zaman daha da küçülür. Nesne numaraları ve generation'lar yerinde kalır; yalnızca yük ve onun doğrudan /Lengthi değişir
TPdfFlateRecompressOptionsun iki alanı vardır ve varsayılan yapıcısı yoktur; ikisini de ayarlayın. MinSavingBytes, stream başına asgari mutlak kazançtır ve MinSavingRatio, özgün uzunluğun bir kesridir (0,05, %5 demektir); 0 olan bir oran oran testini kapatır. Yeniden kurulan stream, stream, CRLF, yeni yük, CRLF ve endstream ile yazılır; böylece sonuç özgün dosyanın satır sonlarına bağlı değildir
procedure RecompressFlate(Pdf: TPdf; const TargetPdf: string);
var
Options: TPdfFlateRecompressOptions;
Report: TPdfFlateRecompressReport;
begin
Options.MinSavingBytes := 256; // minik kazançları yok say
Options.MinSavingRatio := 0.05; // ve %5 altındaki her şeyi
if not Pdf.SaveAsFlateRecompressed(TargetPdf, Options, Report) then
raise Exception.Create(Report.ErrorMessage);
Writeln(Format('recompressed %d, skipped %d, failed %d; streams %d -> %d bytes',
[Report.RecompressedCount, Report.SkippedCount, Report.FailedCount,
Report.OriginalStreamBytes, Report.RecompressedStreamBytes]));
end;
Uç durumlar tasarımdan tutucudur. /Lengthi dolaylı bir referans olan bir stream atlanır; çünkü yeniden yazım doğrudan bir tamsayı saklamak zorundadır. Açılamayan bir yük, FailedCounte sayılır ve değiştirilmeden kopyalanır. Eşikleri karşılamayan bir Flate stream'i SkippedCounte sayılır. DCT, JPX, JBIG2 ve CCITT görüntüleri asla aday listesine girmez; bu tur JPEG kalitesine asla dokunmaz. Kayıplı görüntü kazançları için son kayıttan önce PDFiumPas'ta uyarlanır görüntü yeniden örnekleme kullanın
O eşik testini yazmaktan gelen bir taşınabilirlik tuzağı: ilk taslak, Savingi Int64 olarak bildirip Single(Saving)/Single(OrigLen) karşılaştırması yapıyordu. Delphi bunu kabul eder ama FPC 3.2.2, Illegal type conversion: "Int64" to "Single" ile durur; çünkü FPC'de o sözdizimi sayısal bir dönüşüm değil bir typecast'tır. Düz Saving / OrigLen, iki derleyicide de gerçek bölmedir ve bir Single ile doğrudan karşılaştırılabilir; dönüştürmeler baştan gereksizdi
Üç API hangi sırayla çalışmalı?
Önce SaveAsDeduplicatedi çalıştırın, çıktıyı yeniden yükleyin, sonra SaveAsFlateRecompressedi çalıştırın. Tekilleştirme ulaşılabilirlik süpürmesini zaten kapsar; onun önünde ayrı bir SaveAsReachable turu fazlalıktır. SaveAsReachablei, yalnızca yetim temizliği ve kesin bir budama sayısı istediğiniz işler için saklayın. İkinci adımda yeniden sıkıştırmak, pahalı inflate-deflate işinin kopya başına bir kez değil hayatta kalan stream başına bir kez çalışması demektir. Yeniden sıkıştırma turu ayrıca grafiği /Root ve /Infodan yeniden gezer; bu da yalnızca birleştirilmiş bir kopya fontun hâlâ işaret ettiği font descriptor'larını ya da font dosyalarını temizler
procedure ShrinkPdf(const SourcePdf, TargetPdf: string);
var
Pdf: TPdf;
TempPdf: string;
begin
TempPdf := ChangeFileExt(TargetPdf, '.dedup.pdf');
Pdf := TPdf.Create(nil);
try
Pdf.FileName := SourcePdf;
Pdf.Active := True;
MergeDuplicateResources(Pdf, TempPdf); // buda + birleştir
Pdf.Active := False;
Pdf.FileName := TempPdf;
Pdf.Active := True;
RecompressFlate(Pdf, TargetPdf); // hayatta kalanları yeniden deflate et
finally
Pdf.Free;
DeleteFile(TempPdf);
end;
end;
Sansürleme (redaction) de uyguluyorsanız bunu, bütün bunlardan önce yapın; içerigi fiziksel olarak kaldırmak, en başta yeni yetimler yaratan şeydir. Operatör seviyesi sansürleme ve resource budama yazısı sansürleme tarafını ele alır. Ve hattınızda son dosyayı daima bir akıl sağlığı denetimi olarak yeniden yükleyin: kaynağa uyan bir sayfa sayısı, müşterinin umursadığı herhangi bir şeyin üzerine yazmadan önceki ucuz sigortadır
FAQ
SaveAsReachable nesneleri yeniden numalar mı ya da referansları değiştirir mi?
Hayır. TPdf.SaveAsReachable, hayatta kalan her nesneyi özgün numarası ve generation'ında tutar ve kaldırılan nesneleri boş xref girişleri olarak işaretler; böylece hiçbir referansın yeniden yazılması gerekmez
SaveAsDeduplicated iki farklı görüntüyü yanılgıyla birleştirebilir mi?
Yalnızca eşleşen sözlüklerle hayır. Parmak izi ham stream baytlarını ve bütün referanslı alt grafiği kapsar ve adaylar yalnızca aynı kategori içinde (font, görüntü, Form XObject) karşılaştırılır
Bu API'ler dijital imzalarımı geçerli tutar mı?
Hayır. Her API, imzalı bayt aralığını bozan eksiksiz bir yeniden yazım yapar; AllowSignedDocument = True geçirip çıktıdaki geçersiz imzaları kabul etmedikçe imzalı dosyalar reddedilir
Üç API de bu blogda başka yerlerde ele alınan sansürleme ve görüntü yeniden örnekleme özellikleriyle birlikte Delphi ve C++Builder için PDFium Component ile gelir