Free Pascal üzerinde parçalı bir FlateDecode yazıcısı, tek zlib state'i tüm parçalar boyunca canlı tutmak zorundadır. Cazip olan kısayol, her 64 KB'lik parçayı ayrı ayrı sıkıştırmaktır; bu her parçaya kendi zlib başlığını ve sağlamasını verir ve 1 MiB'lik bir ek, uç uca yapıştırılmış on altı zlib üyesine dönüşür. 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 kaybolur. PDF Library for Delphi, hem DeflateStream hem de InflateStream içinde tek zlib state'i tüm parçalar boyunca canlı tutar
Tuzak, çapraz derleyicili bir birimin yalnızca FPC tarafında ve yalnızca parçalı akış yolunda var; kaçırılmasının nedeni de tam olarak bu: Delphi dalı ve dize tabanlı yardımcılar her biri tek bir zlib akışı üretir, küçük test verileri ise parçalı koda hiç ulaşmaz. Delphi'nin gizlemeye eğilimli olduğu beş FPC taşıma tuzağı yazısındakilerin yakın akrabasıdır; fark şu ki burada Delphi hiçbir şeyin üstünü örtmüyor. Kısayol, zlib akışının ne olduğuna dair yanlış bir zihinsel modele dayanıyor, hepsi bu
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
Naive FPC DeflateStream'i kaynağı 64 KB'lik dilimler hâlinde okuyor ve her parçayı ZFPCCompress'e veriyor; 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 çıkar ve fonksiyon bunları yazmadan önce tek bir AnsiString içinde birleştirir. Sonuç Flate verisine benzer, doğru bir başlığı vardır ve hatasız çözülür; ama yalnızca ilk 65.536 bayta kadar çözülür. Okuma tarafında aynadaki tuzak vardır: 64 KB sıkıştırılmış girdi başına bir kez ZFPCInflate çağırmak, her seferinde taze bir inflateInit2 başlatır. İ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ür
// Naive parçalı 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şır?
FPC'de parçalı yol, 1 MiB ve üzeri her gömülü dosyayı yazar ve akış API'si üzerinden çıkarılan her Flate akışını, sıkıştırılmış boyutu bir parçayı geçtiğinde okur. 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 ise kaynağın tamamını belleğe okuyup tek çağrıda her şeyi sıkıştıran tek seferlik yardımcı DeflateStr'i çağırır. 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ç parçaya yayılır. 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
Bunu yanlış yapmak her katmanda sessizce başarısız olur. Yazıcı, özgün dosyadan hesaplanan /Params /Size ve bir MD5 /CheckSum saklar; böylece akış on altı üye taşısa bile ek sözlüğü tam boyutu bildirir. Kurallara uyan her okuyucu, kütüphanenin Delphi derlemesi dâhil, ilk Z_STREAM_END'de temizce durur ve tam olarak 65.536 bayt döndürür. GetEmbeddedFileContentToStream 1 döndürür; çü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 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ı, Delphi dalıyla aynı yapıya sahiptir. Ayrıca sıkıştırılmış sonucun tamamını önce bir AnsiString içinde birleştirmek yerine çıktıyı üretildikçe yazar; böylece yazıcı, sıkıştırılmış verinin ikinci bir tam kopyasını bellekte hiç oluşturmadan hedefe kopyalar
// Tek zlib state'li 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 aynı deseni izler: 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. Yazıcı, sabit seviyeli bir yardımcıya gitmek yerine deflateInit2'yi kendisi çağırdığı için PLDeflateLevel'e saygı duyar; dolayısıyla TPDFlib.SetCompressionLevel(1..9) ile ayarlanan bir seviye FPC'de büyük gömülü dosyalara da uygulanır. Sıkıştırmayı arşiv çıktısı için 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ş bir akışın imzasıdır. Aşağıdaki rutin o kontrolün FPC sürümüdür ve doğrudan paszlib üzerine kurulur, böylece test edilen koda güvenmez; ardından 200.000 baytı, yani dört 64 KB'lik parçayı, DeflateStream'den geçiren bir sınama gelir
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. Bu sınıf hata Delphi'de geçip yalnızca FPC'de başarısız olduğundan aynı testi teslim ettiğiniz her derleyicide çalıştırın
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;
FPC dalındaki uç durumlar
FPC dalı hoşgörülü çözümleme kuralları kullanır. MaxOutput'a ulaşıldığında FPC InflateStream sınırda kırpar ve döner, Delphi dalı ise 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. Ne yazmış olursa olsun, birleştirilmiş üyeler içeren bir PDF her okuyucuda 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ü, tam bir parçadan az döndüren okumayı verinin sonu sayar; bu, TFileStream ve TMemoryStream gibi akışlar için geçerlidir, dolayısıyla AddAssociatedFileFromStream'e geçirilen özel bir TStream en güvenli önce bir TMemoryStream'e kopyalanır
Tek state'li FPC DeflateStream ve InflateStream, tek kaynak ağacından Delphi, C++Builder ve Free Pascal'ı hedefleyen PDF Library for Delphi ile gelir; böylece büyük bir ek, FPC derlemesinden de Delphi'den döndüğü gibi bayt bayt geri gelir