Teknik Makale

Bazı PDF Nesne Akışları Delphi'de Neden Çöpe Çözülüyor

Hatasız şişen ama yine de gürültü olarak okunan bir PDF nesne akışı, genellikle bir adımı eksik bırakır: ISO 32000-1 Predictor'ını geri almak. Bir akışın /DecodeParms sözlüğü /Predictor 2 veya üzerini taşıdığında, FlateDecode'un geri verdiği baytlar orijinal veri değildir -bunlar, herhangi bir sözlük araması anlamlı olmadan önce ikinci bir yeniden yapılandırma geçişine ihtiyaç duyan PNG tarzı satır-farklanmış veya TIFF tarzı yatay farklanmış değerlerdir. Delphi ve C++Builder için yerel VCL PDF bileşen kütüphanesi olan PDFiumPas, bu yeniden yapılandırma geçişini v2.16.0'da ekledi, özellikle PDF 1.5+ nesne akışları hiçbir sözlük ayrıştırıcının okuyamayacağı farklanmış baytlara genişlediği için

FlateDecode tek başına neden yeterli değil?

FlateDecode'un kendisi yalnızca DEFLATE kod çözmedir (ISO 32000-1 §7.4.4.1): kodlayıcının sıkıştırıcıya verdiği her ne bayt varsa onu yeniden üretir, başka bir şey değil. Predictor bir katman yukarıda, akışın /DecodeParms sözlüğünde yaşar ve kodlayıcının sıkıştırmadan önce uyguladığı bir dönüşümü tarif eder -farklama, bir çapraz referans akışı veya bir nesne akışı içindeki sıkıca paketlenmiş tam sayılar gibi benzer yapılandırılmış değerlerin uzun koşularını, DEFLATE'in çok daha iyi sıkıştırdığı küçük sayıların uzun koşularına dönüştürür. ISO 32000-1 §7.4.4.3 (Tablo 8), bu dönüşümü geri almanın, isteğe bağlı bir temizlik geçişi değil, filtrelenmiş bir akışı kod çözmenin bir parçası olduğu konusunda açıktır, ama yine de yalnızca inflate'i çağırıp orada duran bir FlateDecode yardımcısı yazmak kolaydır

Belirti, aramayı bildiğinizde ayırt edicidir. Predictor-farklanmış baytlar rastgele gürültü değildir -hâlâ sıkıştırılmış bir akışın şeklini taşırlar; bu yüzden naif bir ayrıştırıcı genellikle bir PDF adı, sayısı veya sınırlayıcısı olamayacak bir bayt dizisine çarpmadan önce birkaç geçerli görünen belirteci geçer ve altta yatan değerlerin komşularından ne kadar farklı olduğuna bağlı olarak farklı satırlar farklı konumlarda başarısız olur. Bu tutarsızlık, hatanın tek bir başarısız dosyadan tespit edilmesini zorlaştıran şeydir: aynı üreticiden iki PDF, yalnızca hangi değerlerin tekrarlandığı konusunda farklılık gösterebilir; bu yüzden biri neredeyse kazayla ayrıştırılırken diğeri tamamen başarısız olur

PDF Predictor parametresi gerçekte ne yapar?

/DecodeParms'taki /Predictor girdisi, uyumlu bir okuyucuya hangi tersine çevirmeyi çalıştıracağını söyler ve ISO 32000-1 Tablo 8, pratikte önemli olan değerleri tanımlar: 1 hiçbir tahminin uygulanmadığı anlamına gelir, 2 TIFF Predictor 2'yi (yatay farklama) seçer ve 10'dan 15'e kadar herhangi bir değer PNG tarzı tahmini seçer. Onunla birlikte üç anahtar daha seyahat eder -/Colors, /BitsPerComponent ve /Columns- ve birlikte, akış hiç görüntü verisi tutmasa bile farklamanın hesaplandığı satır geometrisini tarif ederler: bir nesne akışı bir resim değildir, ama PDF yazıcıları aynı satır tabanlı predictor makinesini onun için yeniden kullanır çünkü fark-sonra-deflate, sıkıca paketlenmiş tam sayıları ve nesne konumlarını ham deflate etmekten daha sıkı sıkıştırır

TIFF Predictor 2, iki şemadan daha basit olanıdır: her bileşen, aynı satırdaki önceki pikseldeki aynı bileşenden farkı olarak saklanır ve her satır, yukarıdaki satırdan bir fark taşımak yerine sol kenarında sıfırlanır. PNG tahmini daha özeldir, çünkü gerçek filtre satırdan satıra değişebilir: her satır tek bir etiket baytıyla başlar -None için 0, Sub için 1, Up için 2, Average için 3, Paeth için 4- ve bildirilen /Predictor değeri değil o etiket, o belirli satırın nasıl yeniden yapılandırılacağına karar verir. 12'lik bir /Predictor gerçekte yalnızca kodlayıcının Up filtresini tercih ettiğine dair bir ipucudur; burada her bayt, önceki satırda doğrudan üstündeki bayt eklenerek geri kazanılır, ama doğru bir kod çözücü yine de Up'ı baştan sona varsaymak yerine her satırda etiketi okumak zorundadır

Nesne akışları kaçırılan bir Predictor'ı neden görünmez kılar?

Nesne akışları, sorunu yalnızca tekrarlamak yerine bileşik hale getirir. ISO 32000-1 §7.5.7, bir PDF 1.5+ yazıcısının birden çok dolaylı nesneyi tek bir sıkıştırılmış kapsayıcıya, bir /ObjStm'ye paketlemesine izin verir ve bir doğrulayıcının en çok ihtiyaç duyduğu nesnelerin -katalog, /OutputIntents veya bir XMP /Metadata akışı- tam olarak o kapsayıcı içinde ekli /Predictor 12 ile seyahat etmesi yaygındır, çünkü bu nesneler satır farklamasından fayda sağlayacak kadar kısa ve tekrarlıdır. Predictor adımı eksik olduğunda, nesne akışını genişletmek bir hata fırlatmaz: yüzeysel olarak makul görünen ama beklenen nesnelere belirteçlenmeyen bir bayt dizisi üretir; bu yüzden içine paketlenen her neyse basitçe ortaya çıkmaz. Render etme nadiren fark eder, çünkü uyumlu bir render motoru, hiç düzene ulaşmadan önce predictor-farklanmış veriyi zaten yeniden yapılandırır; fark eden kod, tam olarak bu hatanın içinde saklandığı türdendir -yapısal bir soruyu yanıtlamak için ham PDF baytlarını kendisi dolaşan ve nesne akışının kendi görüşü yanlış geldiğinde hiçbir yedeği olmayan bir doğrulayıcı, imzalayıcı veya sürüm kontrolcüsü

PDFiumPas, v2.16.0'dan önce tam olarak bu başarısızlığa çarptı. /Predictor 12 ile kurulan nesne akışları -PDF 1.5+ yazıcıları için yaygın durum- yapısal tarayıcının ayrıştıramadığı farklanmış baytlara PdfExpandObjectStreams üzerinden genişledi; bu yüzden içine paketlenen katalog, /OutputIntents ve /Metadata nesneleri, uyumluluk taramaları için etkin bir şekilde görünmezdi -hiçbir istisna, hiçbir uyarı yok, yalnızca bu nesneler yokmuş gibi sessizce davranan bir tarama. PDFiumPas'ın bir nesne akışını, hibrit ve saf xref-akışı durumları dahil, aktif çapraz referans tablosuna karşı nasıl çözdüğüne dair daha derin mekanikler, PDFiumPas ile nesne ve xref akışlarını doğrulama üzerine makalede ayrıca ele alınmıştır; burada açıklanan predictor adımı, o çözümlemeden sonra, her sıkıştırılmış nesnenin gerçekte içerdiği baytlar üzerinde çalışır

Pascal'da PNG ve TIFF Predictor satırlarını tersine çevirmek

PDFiumPas, farklamayı tek bir rutinde, PdfApplyPredictor'da tersine çevirir ve onu çağırsanız da kendi Delphi kodunuzda fikri yeniden uygulasanız da geometri matematiği bilinmeye değer. Bayt cinsinden satır genişliği tavan(Columns × Colors × BitsPerComponent ÷ 8) ve her iki algoritmanın da kullandığı piksel başına bayt genişliği tavan(Colors × BitsPerComponent ÷ 8)'dir -ikisinden birini yanlış yuvarlayın ve yeniden yapılandırma bir satır sınırı boyunca değil, bir satır sınırının ötesine okur. 2'nin altındaki bir /Predictor dokunulmadan bırakılır, çünkü 1 kodlayıcının hiçbir dönüşüm uygulamadığı anlamına gelir; 2, aşağıda gösterilen TIFF dalını seçer ve 10'dan yukarısı, her satırın başındaki etiket baytının -bildirilen /Predictor değeri değil- o belirli satırın nasıl geri alınacağına karar verdiği PNG satır-filtresi yeniden yapılandırmasına düşer

function PdfApplyPredictor(const Src: TBytes;
  Predictor, Colors, Bpc, Columns: Integer): TBytes;
var
  RowLen, Bpp, R, I: Integer;
begin
  Result:= Src;
  if Predictor< 2 then
    Exit;                                   // 1 = no prediction, nothing to undo
  if Colors<= 0 then Colors:= 1;
  if Bpc<= 0 then Bpc:= 8;
  if Columns<= 0 then Columns:= 1;
  if (Colors> 64)or (Bpc> 32)or (Columns> 1 shl 24) then
    Exit;                                   // reject hostile row geometries
  RowLen:= (Columns* Colors* Bpc+ 7) div 8;  // ceil(), per ISO 32000-1 Table 8
  Bpp:= (Colors* Bpc+ 7) div 8;
  if Predictor= 2 then
  begin
    if Bpc<> 8 then
      Exit;                                 // only the 8-bit layout is reconstructed
    Result:= Copy(Src, 0, Length(Src));
    R:= 0;
    while R+ RowLen<= Length(Result) do
    begin
      for I:= R+ Bpp to R+ RowLen- 1 do
        Result[I]:= Byte(Result[I]+ Result[I- Bpp]);
      Inc(R, RowLen);
    end;
    Exit;
  end;
  // Predictor >= 10 falls through to PNG row-filter reconstruction below
end;
// Continues inside PdfApplyPredictor once Predictor>= 10 (PNG row filters).
// Rows:= Length(Src) div (RowLen+ 1); each row is a 1-byte filter tag
// followed by RowLen data bytes, decoded left to right.
for R:= 0 to Rows- 1 do
begin
  SrcOfs:= R* (RowLen+ 1);
  DstOfs:= R* RowLen;
  Tag:= Src[SrcOfs];
  Inc(SrcOfs);
  for I:= 0 to RowLen- 1 do
  begin
    if I>= Bpp then A:= Result[DstOfs+ I- Bpp] else A:= 0;   // byte to the left
    if R> 0 then B:= Result[DstOfs+ I- RowLen] else B:= 0;   // byte above
    case Tag of
    1: Result[DstOfs+ I]:= Byte(Src[SrcOfs+ I]+ A);            // Sub
    2: Result[DstOfs+ I]:= Byte(Src[SrcOfs+ I]+ B);            // Up
    3: Result[DstOfs+ I]:= Byte(Src[SrcOfs+ I]+ (A+ B) div 2); // Average
    // Paeth (tag 4) adds whichever of A, B or the byte above-left sits
    // closest to the linear predictor A+ B- C; tag 0 (None) copies the
    // filtered byte through unchanged
    else Result[DstOfs+ I]:= Src[SrcOfs+ I];
    end;
  end;
end;

PDFiumPas v2.16.0'da neyi değiştirdi?

PDFiumPas v2.16.0'da gönderilen düzeltme, bir akışın ham baytlarını okuyan ve nesne akışı genişletmesi dahil, PDF yapısını bayt düzeyinde incelemesi gereken her çağıran için onları kod çözen rutin olan PdfReadAndDecodeStream'in içinde oturur; yalnızca /Filter'ın çıplak bir FlateDecode olduğunu, hiçbir zaman bir zincirleme olmadığını doğruladıktan sonra yeniden yapılandırmayı dener, çünkü zincirlenmiş bir filtre bu katmanda güvenle predictor-düzeltilemez. Akış sözlüğünden /Predictor, /Colors, /BitsPerComponent ve /Columns'u geri okumak da genel bir sözlük ayrıştırıcısına ihtiyaç duymaz: PdfDictRefNum, o tek sözlüğün bayt aralığı içinde her anahtarı doğrudan ad-belirteç aramasıyla bulur ki bu, o dört anahtarın tek bir akış sözlüğü içinde tekrarlanamayacağı veya iç içe geçemeyeceği için burada güvenlidir tam olarak. Aynı ad-belirteç araması, bir PDF dosyasının daha büyük veya daha az sınırlı bir bölgesine yöneltildiğinde çok daha risklidir; bu, PDF sözlüklerini güvenle ayrıştırma üzerine tamamlayıcı makalenin konusudur

// Inside PdfReadAndDecodeStream, right after PdfInflate() has already run:
if PdfFilterIsPureFlate(DictTxt) then
begin
  Inflated:= PdfInflate(Raw);
  Predictor:= PdfDictRefNum(Data, DS, DE, 'Predictor');
  if Predictor>= 2 then
  begin
    PColors:= PdfDictRefNum(Data, DS, DE, 'Colors');
    PBpc:= PdfDictRefNum(Data, DS, DE, 'BitsPerComponent');
    PColumns:= PdfDictRefNum(Data, DS, DE, 'Columns');
    Result:= PdfApplyPredictor(Inflated, Predictor, PColors, PBpc, PColumns);
  end
  else
    Result:= Inflated;
end;

v2.16.0'dan önce, /Predictor 12 ile kurulan bir nesne akışı hiçbir hata fırlatılmadan farklanmış baytlara genişledi; bu yüzden içine paketlenen herhangi bir katalog, /OutputIntents veya /Metadata nesnesi, hiçbir uyarı olmadan PDFiumPas'ın yapısal taramalarından kayboldu. Düzeltmeden sonra, aynı nesne akışı şişer ve ardından doğru şekilde yeniden yapılandırılır ve içine paketlenen nesneler o taramalara yeniden görünür hale gelir. Savunmacı sınırlar düzeltmeyle birlikte seyahat etti: PdfApplyPredictor artık 64'ün üzerindeki /Colors'ı, 32'nin üzerindeki /BitsPerComponent'i ve 2^24'ün üzerindeki /Columns'u tamamen reddeder, çünkü bu kombinasyonlar hiçbir gerçek PDF üreticisinin ihtiyaç duymadığı satır geometrilerini tarif eder ve esas olarak bir kod çözücünün girdi baytlarının haklı çıkardığından çok daha fazla bellek ayırmasını sağlamak için var olurlar

var
  Pdf: TPdf;
  Report: TPdfAValidationResult;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.FileName := 'incoming.pdf';
    Pdf.Active := True;
    Report := Pdf.ValidatePdfA;
    if not Report.IsCompliant then
      LogNonCompliance(Report); // caller-supplied handler
  finally
    Pdf.Free;
  end;
end;

Bilinmeye değer sınırlar

PDFiumPas'ın predictor yeniden yapılandırmasının, ona güvenmeden önce bilinmeye değer iki kenarı vardır. TIFF Predictor 2 yeniden yapılandırması yalnızca bileşen başına 8 bit durumunu kapsar; PDF daha dar paketlemelere izin verir, ama bayt-altı TIFF-farklanmış veri tahmin edilmek yerine yeniden yapılandırılmadan geçer; bu yüzden /BitsPerComponent 1, 2 veya 4 ile /Predictor 2 bildiren bir akış bugün bu yol üzerinden doğru şekilde kod çözülmeyecektir. PNG tahmininin böyle bir kısıtlaması yoktur -her satır kendi filtre etiketini sağlar ve bildirilen /Predictor değerinin 10 ile 15 arasında ne olduğuna bakılmaksızın tanımlanan beş tür de yeniden yapılandırılır ki bu, PNG tarzı filtrelemenin gerçekte nasıl çalıştığıyla eşleşir: bildirilen değer, her satır hakkında bir vaatten çok kodlayıcının çoğunlukla neyi kullandığına dair bir ipucuna daha yakındır

PDFium'un yerel render motoru, predictor-farklanmış görüntü ve içerik akışı verisini zaten doğru şekilde yeniden yapılandırır ki bu, tam olarak bir dosyanın herhangi bir sıradan görüntüleyicide mükemmel şekilde render edilebilirken, onun üzerine kurulu bayt düzeyinde bir doğrulayıcı, imzalayıcı veya sürüm kontrolcüsünün aynı baytları yanlış okumasının nedenidir. Burada açıklanan predictor-farkında kod çözme, Delphi ve C++Builder için yerel VCL PDFium bileşeni olan PDFiumPas'ın PDF/A doğrulama, yapısal tarama ve imzalama özelliklerini destekler