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
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
// 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.EmbedFileveTPDFlib.AddEmbeddedFile: diskten bir dosyayı okuyup bir/EmbeddedFileakışına yazanlarTPDFlib.AddAssociatedFileFromStreamveTPDFlib.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.GetEmbeddedFileContentToStreamveGetEmbeddedFileContentToFile: önceTPDFStream.WriteDecodedToStreamüzerinden, oradan daInflateStreamü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
// 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