Teknik Makale

FPC'de parçalı zlib: FlateDecode neden tek akış ister?

v3.539.24 öncesinde PDF Library for Delphi'nin Free Pascal derlemesi büyük akışları 64 KB'lik parçalar hâlinde sıkıştırıyor ve her parçaya kendi zlib başlığı ile sağlamasını veriyordu; sonuçta 1 MiB'lik bir ek, uç uca yapıştırılmış on altı zlib üyesine dönüşüyordu. ISO 32000-1 FlateDecode tam olarak tek zlib akışı bekler ve standart bir decoder ilk akış sonu işaretinde durur; yani ilk 65.536 bayttan sonraki her şey sessizce kayboluyordu. Düzeltme, DeflateStream ve InflateStream içinde tek zlib state'i tüm parçalar boyunca canlı tutuyor

Hata biriminin yalnızca FPC tarafında ve yalnızca parçalı akış yolunda vardı; hayatta kalmasının sebebi de tam olarak buydu: Delphi dalı her zaman doğruydu, dize tabanlı yardımcılar her zaman doğruydu ve küçük test verileri parçalı koda hiç ulaşmıyordu. Delphi'nin örtbas ettiği beş FPC taşıma hatası yazısındaki kusurların yakın akrabasıdır; fark şu ki burada Delphi derlemesi hiçbir şeyi örtemiyordu. FPC dalı basitçe zlib akışının ne olduğuna dair yanlış bir zihinsel modelle yazılmıştı

Parçalı bir zlib akışı diğer PDF okuyucularında neden kırpılır?

Çünkü RFC 1950'nin tanımladığı zlib akışı tek bir kapsayıcıdır, kapsayıcı dizisi değil ve kurallara uyan bir inflater ilk Adler-32 trailer'ını verinin sonu sayar. Biçim; iki baytlık bir başlık, son bloğu final-block bayrağı taşıyan kesintisiz bir RFC 1951 deflate bit akışı ve sıkıştırılmamış tüm baytlar üzerinde dört baytlık bir Adler-32 sağlamasından oluşur. ISO 32000-1 §7.4.4 /FlateDecode'i tam da bu terimlerle tanımlar. inflate trailer'a ulaştığında Z_STREAM_END döndürür ve kalan girdiyi avail_in içinde okunmamış bırakır. Onun bakış açısından hiçbir şey yanlış değildir, bu yüzden hata fırlatmaz ve trailer'dan sonraki baytlar görmezden gelinir. Uç uca eklenmiş üyeler gzip'te meşru bir fikirdir (RFC 1952 tek dosyada birden çok üyeye izin verir); sezgi muhtemelen oradan gelir ama zlib'de böyle bir kural yoktur ve PDF de hiç istememiştir

PDFlibPas FlateDecode akışlarında RFC 1950 zlib üyesi anatomisi: iki baytlık bir başlık, son bloğu final-block bayrağı taşıyan kesintisiz bir deflate bit akışı ve inflate'ın Z_STREAM_END döndürdüğü dört baytlık Adler-32 trailer'ı; kalan girdi avail_in içinde okunmamış kalır, hata fırlatılmaz
Kurallara uyan bir inflater ilk Adler-32 trailer'ını verinin sonu sayar; üye sınırı sert bir duraktır ve yazanın ardından yapıştırdığı her şey hiçbir okuyucunun çözmeyeceği ölü ağırlıktır

Eski FPC DeflateStream kaynağı 64 KB'lik dilimlerle okuyor ve her parçayı ZFPCCompress'e veriyordu; bu yardımcı kendi deflateInit2'sini çalıştırır, Z_FINISH ile sıkıştırır ve deflateEnd çağırır. Böylece her parça tam, geçerli, kendi kendini sonlandıran bir zlib akışı olarak çıkıyor ve fonksiyon bunları yazmadan önce tek bir AnsiString içinde birleştiriyordu. Sonuç Flate verisine benziyor, doğru bir başlığı vardı ve hatasız çözülüyordu ama yalnızca ilk 65.536 bayta kadar çözülüyordu. Okuma tarafında InflateStream'de aynadaki gibi bir kusur vardı: 64 KB sıkıştırılmış girdi başına bir kez ZFPCInflate çağırıyordu ve her çağrı taze bir inflateInit2 başlatıyordu. İkinci parça, zlib başlığı olmayan bir deflate bit akışının ortasında başlar; yeni bir inflater onu reddeder ve başka bir üreticiden gelen tümüyle normal tek üyeli bir akış, yalnızca ilk 64 KB sıkıştırılmış baytının taşıdığı yere kadar çözülüyordu

PDFlibPas'ın FPC DeflateStream'inde parçalı yazıcı kusuru: her 64 KB'lik parça ZFPCCompress'ten tam bir zlib üyesi olarak geçer, böylece 1 MiB'lik bir ek on altı yapıştırılmış üye taşır, okuyucu 65.536 bayttan sonra ilk trailer'da durur ve GetEmbeddedFileContentToStream yine de başarı bildirir
Hasar görünmez kaldı çünkü her katman başarılıydı: ek sözlüğü tam /Params /Size bildiriyordu, çözümleme hata fırlatmıyordu ve kırpılmayı yalnızca eki açan biri fark ediyordu
// v3.539.24 öncesi FPC DeflateStream (sadeleştirilmiş):
// ZFPCCompress deflateInit2 / deflate(Z_FINISH) / deflateEnd çalıştırır,
// böylece her 64 KB'lik parça ayrı bir zlib üyesine dönüşür
Repeat
  ReadCount:= Source.Read(Input[1], ChunkSize);
  If (ReadCount> 0) Then
    Compressed:= Compressed+ ZFPCCompress(Copy(Input, 1, ReadCount), 6, False);
Until (ReadCount< ChunkSize);
If (Compressed<> '') Then
  Target.WriteBuffer(PAnsiChar(Compressed)^, Length(Compressed));

Hangi PDF Library for Delphi çağrıları parçalı yola ulaşıyordu?

FPC'de 1 MiB ve üzeri her gömülü dosya yanlış yazılıyordu ve akış API'si üzerinden çıkarılan her Flate akışı, sıkıştırılmış boyutu bir parçayı geçtiği anda yanlış okunuyordu. TPDFStream.ReadFromStream gelen verinin nasıl kodlanacağına karar verir. Deflate true ve Stream.Size >= 1048576 olduğunda veriyi DeflateStream üzerinden akıtır; eşiğin altında kaynağın tamamını belleğe okuyup DeflateStr'i çağırır ki bu tek seferlik yardımcı hiç etkilenmedi. ASCII85 artı Flate filtre zinciri boyut testini atlar ve daima DeflateStream'den geçer, dolayısıyla o yolda 64 KB'tan büyük her veri çoktan birkaç üyeye bölünmüştü. ReadFromStream'i sıkıştırma açık hâlde besleyen herkese açık giriş noktaları, gömülü dosya yazıcılarıdır:

  • TPDFlib.EmbedFile ve TPDFlib.AddEmbeddedFile: diskten bir dosyayı okuyup bir /EmbeddedFile akışına yazanlar
  • TPDFlib.AddAssociatedFileFromStream ve TPDFlib.AddAssociatedFileFromFile: e-fatura XML'i ve diğer kaynak veriler için kullanılan PDF/A-3 associated-file yazıcıları
  • Okuma tarafında ise TPDFlib.GetEmbeddedFileContentToStream ve GetEmbeddedFileContentToFile: önce TPDFStream.WriteDecodedToStream üzerinden, oradan da InflateStream üzerinden çözerler

Başarısızlık her katmanda sessizdi. Yazıcı, özgün dosyadan hesaplanan /Params /Size ve bir MD5 /CheckSum saklar; böylece ek sözlüğü tam boyutu bildirirken akış on altı üye taşıyordu. Aynı kütüphanenin Delphi derlemesi o dosyayı okurken ilk Z_STREAM_END'de temizce durdu ve tam olarak 65.536 bayt döndürdü. GetEmbeddedFileContentToStream 1 döndürdü, çünkü çözümlemenin hata fırlatıp fırlatmadığını bildirir, çıktının /Size ile eşleşip eşleşmediğini değil. Büyük belge sorununu gigabaytlık PDF'leri birleştirme ve bölme üzerinden kovalamış herkes bu deseni tanır: dosya açılır, sayfa sayısı doğrudur ve hasar yalnızca biri eki açtığında ortaya çıkar

Her parçada tek deflate state

PDFlibZLib.pas içindeki düzeltilmiş DeflateStream tek bir paszlib.TZStream başlatır, her parçayı Z_NO_FLUSH ile deflate'e verir ve yalnızca sonda Z_STREAM_END döndürene kadar Z_FINISH ile kompresörü boşaltır. Bu tam olarak bir başlık, geri referansları parça sınırlarını aşabilen tek bir deflate bit akışı ve tüm girdi üzerinde tek bir Adler-32 üretir. FPC dalı artık Delphi dalının her zaman sahip olduğu yapıya sahip. Ayrıca çıktıyı, sıkıştırılmış sonucun tamamını önce bir AnsiString içinde birleştirmek yerine üretildikçe yazar; böylece yazıcı, sıkıştırılmış verinin ikinci bir tam kopyasını hedefe kopyalamadan önce bellekte artık oluşturmuyor

PDFlibPas'ta düzeltilmiş DeflateStream: bir kez başlatılan tek paszlib.TZStream, her 64 KB'lik parça Z_NO_FLUSH ile beslenir ve sonunda Z_FINISH boşaltması gelir; tam olarak bir başlık, geri referansları parça sınırlarını aşan kesintisiz bir deflate bit akışı ve tüm girdi üzerinde tek Adler-32
Tek state çıktının yazılışını da değiştirir: sıkıştırılmış baytlar ikinci bir tam kopyada birikmek yerine her tampon dolduğunda çıkar ve PLDeflateLevel artık FPC'de büyük gömülü dosyalara da ulaşır
// v3.539.24'ten itibaren FPC DeflateStream (hata yolları kırpıldı)
If (deflateInit2(strm, Level, Z_DEFLATED, 15, 8, Z_DEFAULT_STRATEGY)<> Z_OK) Then
  Exit;
Try
  Repeat
    ReadCount:= Source.Read(Input[0], ChunkSize);
    If (ReadCount> 0) Then
    Begin
      strm.next_in:= Pointer(Input);
      strm.avail_in:= ReadCount;
      While (strm.avail_in> 0) Do
      Begin
        strm.next_out:= Pointer(Output);
        strm.avail_out:= ChunkSize;
        Status:= deflate(strm, Z_NO_FLUSH);   // aynı state, üye sınırında kopma yok
        Produced:= ChunkSize- strm.avail_out;
        If (Produced> 0) Then
          Target.WriteBuffer(Output[0], Produced);
        If (Status<> Z_OK) Then
          Break;
      End;
    End;
  Until (ReadCount< ChunkSize);
  Repeat                                    // tüm girdi için tek trailer
    strm.next_out:= Pointer(Output);
    strm.avail_out:= ChunkSize;
    Status:= deflate(strm, Z_FINISH);
    Produced:= ChunkSize- strm.avail_out;
    If (Produced> 0) Then
      Target.WriteBuffer(Output[0], Produced);
  Until (Status= Z_STREAM_END);
Finally
  deflateEnd(strm);
End;

InflateStream da simetrik yeniden yazımı aldı: tek bir inflateInit2, mevcut parça tüketilene ve çıkış tamponu dolu olmaktan çıkana kadar inflate çağıran bir iç döngü ve Z_STREAM_END'de durma. Bilinmesinde fayda olan bir yan etki var. Eski parçalı yazıcı seviye 6'yı kod içine gömmüştü; yenisi PLDeflateLevel'e saygı duyar, dolayısıyla TPDFlib.SetCompressionLevel(1..9) ile ayarlanan bir seviye artık FPC'de büyük gömülü dosyalara da uygulanır. Arşiv çıktısı için sıkıştırmayı Delphi'de PDF dosya boyutunu küçültme yazısında anlatıldığı gibi zaten ayarlıyorsanız bu sizin için önemli

Bir Flate akışının tek zlib üyesi olduğunu nasıl doğrularsınız?

Akışı sıradan bir zlib decoder ile inflate edin ve Z_STREAM_END döndürdüğünde iki şeye bakın: çözülen uzunluk kaynak uzunluğuna eşittir ve avail_in sıfırdır. Son işaretinden sonra kalan girdi, birleştirilmiş akışın imzasıdır. Düzeltme bu şekilde doğrulandı: dört 64 KB'lik parçaya yayılan 200 KB test verisi yeni DeflateStream'den geçti ve 534 baytlık tek bir zlib akışı olarak çıktı; hazır bir zlib decoder 200.000 baytın tamamını kalan girdisiz geri kazandı ve aynı kontrol i386 çapraz derlenmiş FPC hedefinde de geçti. Aşağıdaki rutin o kontrolün FPC sürümüdür ve doğrudan paszlib üzerine kuruludur, böylece test edilen koda güvenmez

uses Classes, SysUtils, paszlib, PDFlibZLib;

function IsSingleZlibMember(Packed: TMemoryStream; out Decoded: Int64): Boolean;
var
  strm: TZStream;
  Buf: array[0..65535] of Byte;
  Status: Integer;
begin
  Result:= False;
  Decoded:= 0;
  FillChar(strm, SizeOf(strm), 0);
  if inflateInit2(strm, 15) <> Z_OK then
    Exit;
  try
    strm.next_in:= Packed.Memory;
    strm.avail_in:= Cardinal(Packed.Size);
    repeat
      strm.next_out:= @Buf;
      strm.avail_out:= SizeOf(Buf);
      Status:= inflate(strm, Z_NO_FLUSH);
    until Status <> Z_OK;
    Decoded:= strm.total_out;
    // Tek üye tam olarak son girdi baytında biter
    Result:= (Status = Z_STREAM_END) and (strm.avail_in = 0);
  finally
    inflateEnd(strm);
  end;
end;

// 200.000 baytı varsayılan 64 KB'lik parçayla DeflateStream'den geçirin
// ve tam uzunluğa geri çözülen tek bir üye isteyin
Source.Position:= 0;
DeflateStream(Source, Packed);
if not (IsSingleZlibMember(Packed, Decoded) and (Decoded = Source.Size)) then
  raise Exception.Create('DeflateStream produced more than one zlib member');

Uygulama düzeyinde faydalı sav, kütüphanenin sizin için yapmadığı savdır: ekin çıkardığınız veriyi, girerken kaydedilen /Params /Size ile karşılaştırın. Tag 5 ile GetEmbeddedFileIntProperty kaydedilen o boyutu döndürür, gömülü dosya indeksleri 1 tabanlıdır ve akış yolunu tetiklemek için verinin en az 1 MiB olması gerekir. Aynı testi teslim ettiğiniz her derleyicide çalıştırın; çünkü özgün kusur Delphi'de geçiyor, yalnızca FPC'de başarısız oluyordu

procedure CheckLargeAttachmentRoundTrip(const PayloadFile, OutFile: string);
var
  PDF: TPDFlib;
  Extracted: TMemoryStream;
  I, Declared: Integer;
begin
  PDF:= TPDFlib.Create;
  try
    PDF.NewDocument;
    PDF.AddStandardFont(4);
    PDF.DrawText(80, 100, 'Large attachment round trip');
    // 1 MiB ve üzeri, ReadFromStream içindeki parçalı DeflateStream yolunu alır
    if PDF.EmbedFile('Payload', PayloadFile, 'application/octet-stream') <> 1 then
      raise Exception.Create('EmbedFile failed');
    if PDF.SaveToFile(OutFile) <> 1 then
      raise Exception.Create('SaveToFile failed');
  finally
    PDF.Free;
  end;

  PDF:= TPDFlib.Create;
  Extracted:= TMemoryStream.Create;
  try
    if PDF.LoadFromFile(OutFile, '') = 0 then
      raise Exception.Create('LoadFromFile failed');
    for I:= 1 to PDF.EmbeddedFileCount do
    begin
      Extracted.Clear;
      if PDF.GetEmbeddedFileContentToStream(I, Extracted) <> 1 then
        raise Exception.CreateFmt('Attachment %d could not be decoded', [I]);
      Declared:= PDF.GetEmbeddedFileIntProperty(I, 5);   // /Params /Size
      if Extracted.Size <> Declared then
        raise Exception.CreateFmt('Attachment %d truncated: %d of %d bytes',
          [I, Extracted.Size, Declared]);
    end;
  finally
    Extracted.Free;
    PDF.Free;
  end;
end;

Düzeltme neyi değiştirmiyor?

FPC dalı kendi hoşgörülü çözümleme kurallarını korur ve önceki FPC derlemelerinin yazdığı dosyaları onarmaz. MaxOutput'a ulaşıldığında FPC InflateStream sınırda kırpar ve dönerken Delphi dalı ERangeError fırlatır; FPC ayrıca inflate veri hatası bildirdiğinde kısmen çözülmüş çıktıyı kabul etmeye devam eder, çünkü bazı PDF üreticileri kırpılmış ya da sağlaması bozuk akışlar çıkarır. v3.539.24 öncesi bir FPC derlemesinin yazdığı PDF hâlâ birleştirilmiş üyeler içerir ve düzeltilmiş okuyucu, diğer her okuyucu gibi, ilk Z_STREAM_END'de durur. Böyle bir dosyayı akışı kütüphane içinde çözüp yeniden kodlayarak iyileştirmeye çalışmayın; bu yalnızca 64 KB'lik kırpılmayı kalıcı kılar. Bunun yerine eki özgün kaynağından yeniden gömün. FPC döngüsü ayrıca hâlâ tam parçadan az döndüren ilk okumada biter; bu, TFileStream ve TMemoryStream gibi akışlar için bir veri sonu sinyalidir, dolayısıyla AddAssociatedFileFromStream'e geçirilen özel bir TStream en güvenli önce bir TMemoryStream'e kopyalanır. Delphi dalı, DeflateStr ve InflateStr yardımcıları ve düz Flate yolundaki 1 MiB'den küçük her akış tam olarak eskisi gibi davranır

Düzeltilmiş FPC DeflateStream ve InflateStream, tek kaynak ağacından Delphi, C++Builder ve Free Pascal'ı hedefleyen PDF Library for Delphi'nin v3.539.24'ünde geliyor; büyük bir ekin artık FPC derlemesinden de bayt bayt, her zaman Delphi'den döndüğü gibi geri gelmesi gerekiyor