Teknik Makale

Delphi'de PDF Dosya Boyutu Denetimi: Kategoriye Göre Byte Dökümü

Bir PDF dosya boyutunun gerçekte nereye gittiğini bulmak için losLab PDF Library, AuditDocumentSpace'i sunar; bu fonksiyon her dolaylı nesneyi on iki kategoriye sınıflandırır — görüntüler, font programları, font sözlükleri, içerik akışları, form XObject'leri, nesne akışları, gömülü dosyalar, meta veri, yapı ağacı, açıklamalar, sayfa ağacı, diğer — ve her birinin nesne sayısını, saklanan bayt sayısını ve yüzde payını bildirir

Bunun var olma nedeni tanıdık bir durumdur. 40 sayfalık bir rapor üretecinizden 80 MB olarak çıkar, müşteri nedenini sorar ve sizin sunabileceğiniz tek şey bir tahmindir. Muhtemelen görüntüler. Belki fontlar. Bu yüzden downsampling'i açarsınız, gönderirsiniz ve dosya 74 MB'a iner, çünkü gerçek ağırlık tamamen başka bir yerdeydi. Font altkümeleme ve görüntü downsampling üzerine eşlik eden makalemiz, bir PDF'in nasıl küçültüleceğini ele alır; bu makale ise önce gelmesi gereken adımı, yani küçültmek üzere olduğunuz şeyi ölçmeyi ele alır

Sıkıştırmadan önce neden ölçmeli?

Çünkü üç standart optimizasyon geçişi belirli bir dosyada çılgınca farklı getiriler sağlar ve dosya hakkında hiçbir şey, siz saymadan hangisinin geçerli olduğunu söylemez. Fontları zaten baytlarının %2'si olan bir belgede altkümelemek, bir yuvarlama hatasını taşımakla geçirilen bir öğleden sonradır. Ağırlığı sıkıştırılmamış içerik akışları olan bir dosyada görüntüleri downsample etmek de aynı hayal kırıklığını üretir. Optimize edici zor kısım değildir — her kütüphanenin bir tanesi vardır. Hangi optimize ediciyi bu dosyaya yönlendireceğinizi bilmek zor kısımdır ve bu bir sıkıştırma sorusu değil, bir muhasebe sorusudur. Bir denetim ayrıca hiçbir optimize edicinin yanıt olmadığı durumları da yakalar: %60'ının gömülü ekler olduğu ortaya çıkan bir dosyanın daha iyi sıkıştırmaya değil, o eklerin belgede olması gerekip gerekmediği konusunda bir konuşmaya ihtiyacı vardır ve %30'u yapı ağacı olan bir dosya, erişilebilirlik etiketlemesinin bedelini öder, ki bu genellikle sessizce çıkarmamanız gereken kasıtlı bir maliyettir. Baytlar bir kez atfedildiğinde, en yakındaki hangi anahtara uzanırsanız uzanın yerine arkasında sayılar olan bir ürün kararı veriyorsunuzdur

On iki kategorili rapor neler içerir?

AuditDocumentSpace, bir kayıt yerine bir dize listesi tutamacı döndürür, böylece rapor düz DLL ve COM cephelerinden değişmeden kurtulur. Liste, bir Total,Objects,Bytes,100.0 özet satırı ve ardından sözleşmenin bir parçası olan sabit bir sırayla tam olarak on iki Category,Objects,Bytes,Percent satırı tutar: Images, Font programs, Font dictionaries, Content streams, Form XObjects, Object streams, Embedded files, Metadata, Structure tree, Annotations, Page tree, Other. Bir kategori boş olsa bile her zaman on üç satır

var
  Lib: TPDFlib;
  ListID, I: Integer;
begin
  Lib := TPDFlib.Create;
  try
    if Lib.LoadFromFile('report.pdf', '') <> 1 then
      Exit;
    ListID := Lib.AuditDocumentSpace;   // 0 when no document is selected
    if ListID = 0 then
      Exit;
    try
      // GetStringListItem is 1-based: items run 1..GetStringListCount
      for I := 1 to Lib.GetStringListCount(ListID) do
        Memo1.Lines.Add(Lib.GetStringListItem(ListID, I));
    finally
      Lib.ReleaseStringList(ListID);
    end;
  finally
    Lib.Free;
  end;
end;

Bu döngüdeki bir Delphi ayrıntısı sizi tam olarak bir kez ısırır. GetStringListItem, GetStringListCount ile eşleşen bire dayalı öğe indeksleri kullanır ve aralık dışı bir indeks, hata fırlatmak yerine boş bir dize döndürür. Döngüyü alışkanlıkla for I := 0 to Count - 1 yazarsanız, boş bir ilk satır, sessizce düşürülmüş bir son satır ve indekslemenin yanlış olduğunu söyleyecek hiçbir istisna elde edersiniz. Raporun kendisi neredeyse doğru görünür, ki bu bir tanı aracının sahip olabileceği en kötü hata modudur

Denetim neden çözülmüş boyut yerine saklanan uzunluğu kullanır?

Çünkü saklanan uzunluk hem istediğiniz sayı hem de elde edilmesi ucuz olan sayıdır. Her dolaylı nesne, ayrıştırıldığı haliyle nesnenin dosyada işgal ettiği ham byte uzunluğunu taşıyan TPDFIndObj.FLength'i taşır. Bunu kullanmak, 900 KB'lık bir DCTDecode görüntüsünün, çözüldüğünde ürettiği 40 MB'lık RGB örnekleri yerine 900 KB — diskte size mal olan baytlar — olarak raporlanması anlamına gelir. Ayrıca denetimin hiçbir şeyi çözmesi gerekmediği anlamına gelir: tembel yüklenen nesneler tembel kalır, filtreler çalıştırılmaz ve 500 MB'lık bir dosyayı denetlemek tam bir sıkıştırma açma döngüsü yerine nesne başlıkları üzerinden bir geçiştir

İkinci kural bir çift sayım savunmasıdır. Bir nesne, sıfır olmayan bir FObjStrNum ile belirtildiği üzere sıkıştırılmış bir nesne akışının içinde yaşadığında, byte sayısı sıfır olarak kaydedilir. Depolaması, ISO 32000-1 §7.5.7'nin bir Flate ile sıkıştırılmış tek bir yükte birçok nesne tutan bir /Type /ObjStm akışı olarak tanımladığı konteyner akışı tarafından zaten bir kez ödenmiştir. Her üyeyi kendi payından ve sonra konteyneri tekrar ücretlendirmek, toplamı gerçek dosya boyutunun ötesine şişirir. Bunun, aşağıda ve nesne akışları ve çapraz referans akışları makalemizde daha derinlemesine ele alınan, çıktıyı nasıl okuduğunuz üzerinde doğrudan bir sonucu vardır

Bir font programı kendini neden sınıflandıramaz?

Çünkü bir PDF'e gömülü bir TrueType font dosyasının bunu söyleyen bir işareti yoktur. ISO 32000-1 §9.8.1, gömülü font programını bir font tanımlayıcısındaki /FontFile, /FontFile2 veya /FontFile3'ün değeri olarak tanımlar ve bu referansın diğer ucundaki akış sözlüğü /Length1 ve filtre anahtarları taşır ama onu font olarak tanımlayan hiçbir /Type veya /Subtype taşımaz. Tek başına bakıldığında anonim bir ikili akıştır. Yalnızca ona işaret eden tanımlayıcı ne olduğunu bilir. Aynı asimetri açıklamalar için de görünür: §12.5.2, bir açıklama sözlüğünde /Type /Annot'u isteğe bağlı yapar, dolayısıyla güvenilir sinyal sözlüğün kendisi değil, bir sayfa /Annots dizisine üyeliktir

Bu yüzden sınıflandırma iki kez çalışır. İlk geçiş her nesnenin kendi /Type ve /Subtype'ını okur ve kolay kazanımları alır: /ObjStm, /Subtype /Image, /Subtype /Form, /Type /Font ve /Type /FontDescriptor, /Metadata, /EmbeddedFile ve /Filespec, /StructTreeRoot ve /StructElem, /Annot, /Page ve /Pages. Geri kalan her şey geçici olarak Other'a düşer. İkinci geçiş sonra referans veren tarafı dolaşır ve geçersiz kılar: her sayfa sözlüğü /Contents'ini içerik akışlarına, /Annots girdilerini açıklamalara ve /Thumb'ını görüntülere yeniden atarken, her font sözlüğü kendi tanımlayıcı zincirini dolaşır

// Shape of the second pass: the referrer names the object
Descriptor := DictOf(FontDict.FindValueByKeyName('FontDescriptor'));
if Assigned(Descriptor) then
begin
  MarkRef(FontDict.FindValueByKeyName('FontDescriptor'), catFontDicts);
  MarkRef(Descriptor.FindValueByKeyName('FontFile'),  catFontPrograms);
  MarkRef(Descriptor.FindValueByKeyName('FontFile2'), catFontPrograms);
  MarkRef(Descriptor.FindValueByKeyName('FontFile3'), catFontPrograms);
end;
// Type0 fonts keep the descriptor one level down
Descendants := FontDict.FindValueByKeyName('DescendantFonts', True);
if (Descendants is TPDFArray) and (TPDFArray(Descendants).Count > 0) then
  MarkFontProgramRefs(DictOf(TPDFArray(Descendants).Item[0]));

Raporu okumak ve bir sonraki adımı seçmek

Önce payları okuyun, sonra nesne sayılarını ve aralarındaki büyük bir farkı bir sinyal olarak ele alın. Modern bir PDF, küçük sözlüklerinin çoğunu nesne akışlarının içine koyar, bu yüzden Page tree ve Structure tree rutin olarak neredeyse sıfır bayta karşı düzinelerce nesne gösterir — gerçek maliyetleri Object streams satırına katlanmıştır. Object streams kendisi büyükse, dosya içerik yerine meta veri benzeri yapıyla yoğundur ve kaldıraç sıkıştırmak değil, nesneleri budamaktır. Açıklama görünüm akışları benzer şekilde davranır: /Subtype /Form taşırlar, dolayısıyla ağır bir şekilde damgalanmış bir belge ağırlığını Form XObjects altında gösterirken Annotations satırı küçük kalır

function CategoryShare(Lib: TPDFlib; ListID: Integer;
  const Category: string): Double;
var
  I: Integer;
  Parts: TArray<string>;
  Inv: TFormatSettings;
begin
  Result := 0;
  Inv := FormatSettings;
  Inv.DecimalSeparator := '.';   // the report is locale-independent
  for I := 2 to Lib.GetStringListCount(ListID) do   // line 1 is Total
  begin
    Parts := string(Lib.GetStringListItem(ListID, I)).Split([',']);
    if (Length(Parts) = 4) and SameText(Parts[0], Category) then
      Exit(StrToFloatDef(Parts[3], 0, Inv));
  end;
end;

Yüzdeleri görüntülemek yerine ayrıştırıyorsanız iki biçimlendirme gerçeği önemlidir. Ondalık ayırıcı, makinenin bölgesi ne olursa olsun her zaman gerçek bir noktadır, dolayısıyla bir Alman veya Fransız iş istasyonunda ortam FormatSettings ile ayrıştırmak başarısız olur veya daha kötüsü, yanlış okur. Ve sondaki sıfırlar kırpılır, dolayısıyla baytların tam olarak %40'ını tutan bir kategori 40.0 değil 40 olarak yazdırılır — asla sabit bir ondalık basamak varsaymayın. Pay elinizde olduğunda yönlendirme mekaniktir: baskın bir Images payı DownsampleImages'e, baskın bir Font programs payı SubsetEmbeddedFonts'a ve hacimli Content streams CompressContent'a işaret eder

Denetimin kasıtlı olarak size söylemediği şey

Toplam, dolaylı nesneler üzerinden bir toplamdır ve bir PDF dosyası nesnelerinden biraz daha fazlasıdır. Dosya başlığı, trailer, nesneler arası boşluk ve klasik bir çapraz referans tablosu dolaylı nesneler değildir, dolayısıyla bu baytlar hiçbir şeye atfedilmez ve denetim toplamı disk üzerindeki boyutun biraz altında kalır. Bir çapraz referans akışı farklıdır — o, /Type /XRef ile gerçek bir nesnedir, dolayısıyla modern bir dosyada bu baytlar Other kategorisinde görünür. Her iki davranış da bir kusur değildir, ama denetimi dosya sisteminden gelen bir byte sayımıyla mutabık kılıyorsanız, farkın geldiği yer burasıdır

Açıkça belirtmeye değer iki sınır daha vardır. İlk olarak, sayılar yazılmakta olan bir dosyayı değil, yüklenmiş bir dosyayı tanımlar: henüz saklanan bir uzunluğu olmayan bellekte inşa edilmiş nesneler için boyut, akış sözlüğü için nominal bir tolerans ile serileştirilmiş çıktıya geri döner, ki bu bir ölçüm değil nihai yazımın bir tahminidir. Kesin rakamlar istiyorsanız kaydet-ve-yeniden-yükle sonrası denetleyin. İkincisi, şişman bir Other satırı bir bulgudur, bir hata raporu değil — genellikle artık hiçbir şeyin referans vermediği yetim nesneler anlamına gelir, ki bu herhangi bir sıkıştırma geçişi için değil mark-and-sweep çöp toplama için bir iştir

Bu şekilde kullanıldığında denetim konuşmanın şeklini değiştirir. 80 MB'lık raporu tahmin etmek yerine onu açar, bir çağrı çalıştırır ve görüntülerin %8, font programlarının %61 olduğunu ve belgenin üç yüz kullanan bir kurumsal stil için dokuz tam font programı gömdüğünü okursunuz. Bu, üzerine bir sayı iliştirilmiş, düzeltilebilir bir cevaptır. AuditDocumentSpace, sizi yönlendirdiği optimizasyon geçişleriyle birlikte, referans sayfalarının tam kategori listesini ve etrafındaki dize listesi API'sini belgelediği Delphi ve C++Builder için losLab PDF Library'de gönderilir