Pre verzije v3.539.24, Free Pascal build PDF Library for Delphi komprimovao je velike streamove u blokovima od 64 KB i svakom bloku davao sopstveni zlib header i checksum, pa je prilog od 1 MiB postajao šesnaest zlib članova zalepljenih kraj na kraj. FlateDecode iz ISO 32000-1 očekuje tačno jedan zlib stream, a standardni dekoder staje na prvom end-of-stream markeru, što znači da je sve posle prvih 65.536 bajtova tiho nestajalo. Popravka drži jedan zlib state živim kroz svaki blok, i u DeflateStream-u i u InflateStream-u
Bug je postojao samo na FPC strani jedinice, i to samo na chunked streaming putanji, i upravo zato je i preživeo: Delphi grana je uvek bila ispravna, helper-i zasnovani na stringovima su uvek bili ispravni, a mali test payload-ovi nikada nisu dolazili do chunked koda. Bliski je rođak defekata iz teksta o pet FPC bugova pri portovanju koje je Delphi krivao, s tim što ovde Delphi build nije pokrivao ništa. FPC grana je jednostavno bila napisana po pogrešnoj predstavi o tome šta zlib stream uopšte jeste
Zašto drugi PDF čitači skraćuju chunked zlib stream?
Zato što je zlib stream po definiciji iz RFC 1950 jedan kontejner, a ne niz njih, i korektan inflater prvi Adler-32 trailer tretira kao kraj podataka. Format je dvobajtni header, jedan neprekidan RFC 1951 deflate bit stream čiji poslednji blok nosi final-block flag, i četvorobajtna Adler-32 kontrolna suma preko svih nekompresovanih bajtova. ISO 32000-1 §7.4.4 definiše /FlateDecode baš u tim pojmovima. Kada inflate stigne do trailera, vraća Z_STREAM_END i sav preostali ulaz ostavlja nepročitan u avail_in. Iz njegove perspektive sve je u redu, pa ne podiže nikakvu grešku, a bajtovi iza trailera se jednostavno ignorišu. Ulančani članovi su legitimna ideja u gzip-u (RFC 1952 dozvoljava više članova u jednom fajlu), odakle je verovatno došla i ta intuicija, ali zlib nema takvo pravilo, a PDF ga nikada nije ni tražio
Stari FPC DeflateStream čitao je izvor po 64 KB i svaki blok predavao ZFPCCompress-u, helper-u koji pokreće svoj deflateInit2, komprimuje sa Z_FINISH i poziva deflateEnd. Svaki blok je zato izlazio kao kompletan, validan, sam po sebi završen zlib stream, a funkcija ih je spajala u jedan AnsiString pre upisa. Rezultat je ličio na Flate podatke, imao je ispravan header i dekodovao se bez greške, ali samo do prvih 65.536 bajtova. Na strani čitanja, InflateStream imao je ogledalo-defekt: pozivao je ZFPCInflate po jednom na svakih 64 KB kompresovanog ulaza, i svaki poziv startovao svež inflateInit2. Drugi blok počinje usred deflate bit streama bez zlib headera, pa ga novi inflater odbija, i sasvim normalan single-member stream od bilo kog drugog proizvođača dekodovan je samo onoliko koliko su ga prva 64 KB kompresovanih bajtova odnela
// FPC DeflateStream pre v3.539.24 (pojednostavljeno):
// ZFPCCompress radi deflateInit2 / deflate(Z_FINISH) / deflateEnd,
// pa svaki blok od 64 KB postaje poseban zlib član
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));
Koji pozivi PDF Library for Delphi dolaze do chunked putanje?
Na FPC-u je svaki embedded file od 1 MiB ili više bio loše upisan, a svaki Flate stream izvučen kroz streaming API bio je loše pročitan čim je njegova kompresovana veličina prešla jedan blok. TPDFStream.ReadFromStream odlučuje kako će enkodovati dolazeće podatke. Kada je Deflate uključen i Stream.Size >= 1048576, podaci idu kroz DeflateStream; ispod tog praga čita ceo izvor u memoriju i poziva DeflateStr, single-shot helper koji nikada nije bio pogođen. ASCII85-plus-Flate filter lanac preskače proveru veličine i uvek ide kroz DeflateStream, pa je na toj putanji svaki payload veći od 64 KB već bio podeljen na više članova. Javni ulazni punktovi koji hrane ReadFromStream sa uključenom kompresijom su pisci embedded fajlova:
TPDFlib.EmbedFileiTPDFlib.AddEmbeddedFile, koji čitaju fajl sa diska u/EmbeddedFilestreamTPDFlib.AddAssociatedFileFromStreamiTPDFlib.AddAssociatedFileFromFile, pisci PDF/A-3 associated fajlova koje koristite za e-fakturu u XML-u i druge izvorne podatke- Na strani čitanja,
TPDFlib.GetEmbeddedFileContentToStreamiGetEmbeddedFileContentToFile, koji dekoduju krozTPDFStream.WriteDecodedToStreama odatle krozInflateStream
Kvar je bio tih na svakom sloju. Pisac upisuje /Params /Size i MD5 /CheckSum izračunat iz originalnog fajla, pa je rečnik prloga reklamirao punu veličinu dok je stream nosio šesnaest članova. Delphi build iste biblioteke koji je čitao taj fajl uredno je stao na prvom Z_STREAM_END i vratio tačno 65.536 bajtova. GetEmbeddedFileContentToStream je vratio 1, jer javlja da li je dekodovanje podiglo grešku, a ne da li izlaz odgovara /Size. Svako ko je već jurio problem velikih dokumenata kroz spajanje i deljenje gigabajtnih PDF-ova poznaje ovaj obrazac: fajl se otvori, broj stranica je tačan, a šteta se vidi tek kad neko otvori prilog
Jedan deflate state kroz sve blokove
Popravljeni DeflateStream u PDFlibZLib.pas inicijalizuje jedan paszlib.TZStream, svaki blok hrani deflate-u sa Z_NO_FLUSH, i tek na kraju isprazni kompresor sa Z_FINISH dok ne vrati Z_STREAM_END. To daje tačno jedan header, jedan deflate bit stream čiji back-reference mogu da prekoče granice blokova, i jedan Adler-32 preko celog ulaza. FPC grana sada ima istu strukturu koju je Delphi grana oduvek imala. Izlaz se i ispisuje čim se proizvede, umesto da se ceo kompresovani rezultat prvo nagomila u AnsiString, pa pisac više ne pravi drugu punu kopiju kompresovanih podataka u memoriji pre nego što je kopira u cilj
// FPC DeflateStream od v3.539.24 (grane za greške izostavljene)
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); // isti state, bez prekida člana
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 // jedan trailer za ceo ulaz
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 je dobio simetričan prepis: jedan inflateInit2, unutrašnju petlju koja zove inflate dok se tekući blok ne potroši i izlazni bafer prestane da bude pun, i stop na Z_STREAM_END. Postoji jedna sporedna posledica koju vredi znati. Stari chunked pisac je hard-kodirao nivo 6, a novi poštuje PLDeflateLevel, pa se nivo postavljen kroz TPDFlib.SetCompressionLevel(1..9) sada odnosi i na velike embedded fajlove na FPC-u. To je bitno ako kompresiju već štimujete za arhivski izlaz, kako je opisano u tekstu o smanjenju veličine PDF fajla u Delphi-ju
Kako da proverite da li je Flate stream jedan zlib član?
Dekompresujte ga običnim zlib dekoderom i kada vrati Z_STREAM_END proverite dve stvari: dekodovana dužina jednaka dužini izvora, i avail_in jednak nuli. Preostali ulaz iza end markera je potpis ulančanog streama. Popravka je ovako i verifikovana: 200 KB test podataka, koji prelaze četiri bloka od 64 KB, prošlo je kroz novi DeflateStream i izašlo kao jedan zlib stream od 534 bajta, standardni zlib dekoder povratio je svih 200.000 bajtova bez preostalog ulaza, a ista provera prošla je i na i386 cross-compiled FPC targetu. Rutina ispod je FPC verzija te provere, izgrađena direktno na paszlib-u da ne bi verovala kodu koji testira
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;
// Jedan član se završava tačno na poslednjem ulaznom bajtu
Result:= (Status = Z_STREAM_END) and (strm.avail_in = 0);
finally
inflateEnd(strm);
end;
end;
// Prognite 200.000 bajtova kroz DeflateStream sa podrazumevanim blokom
// od 64 KB i zahtevajte jedan član koji se dekoduje nazad na punu dužinu
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');
Na nivou aplikacije, korisna asercija je ona koju biblioteka ne radi za vas: uporedite ono što izađe iz prloga sa /Params /Size zabeleženim pri ubacivanju. GetEmbeddedFileIntProperty sa tagom 5 vraća tu zabeleženu veličinu, indeksi embedded fajlova kreću od 1, a payload mora imati bar 1 MiB da bi uopšte ušao u streaming putanju. Isti test provucite kroz svaki kompajler uz koji isporučujete, jer je originalni defekt prolazio na Delphi-ju a padalo je samo na FPC-u
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 ili više ide kroz chunked DeflateStream putanju u ReadFromStream-u
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;
Šta popravka ne menja?
FPC grana zadržava svoje blage pravile dekodovanja, i ne popravljaje fajlove koje su raniji FPC buildovi već upisali. Kada se dostigne MaxOutput, FPC InflateStream skrati na granici i vrati se, dok Delphi grana diže ERangeError, a FPC i dalje prihvata delimično dekodovan izlaz kada inflate javi grešku u podacima, jer neki PDF proizvođači emituju skraćene ili checksum-pokvarene streamove. PDF upisan pre-v3.539.24 FPC builda i dalje sadrži ulančane članove, a popravljeni čitač, kao i svaki drugi, staje na prvom Z_STREAM_END. Ne pokušavajte da izlečite takav fajl dekodovanjem i ponovnim enkodovanjem streama unutar biblioteke, jer time samo učvršćujete skraćenje na 64 KB. Umesto toga ponovo ubacite prilog iz njegovog originalnog izvora. FPC petlja takođe i dalje staje na prvom čitanju koje vrati manje od punog bloka, a to je signal kraja podataka samo za streamove poput TFileStream i TMemoryStream, pa je sopstveni TStream prosleđen AddAssociatedFileFromStream-u najsigurnije prvo prekopirati u TMemoryStream. Delphi grana, helper-i DeflateStr i InflateStr, i svaki stream manji od 1 MiB na običnoj Flate putanji ponašaju se tačno kao do sada
Popravljeni FPC DeflateStream i InflateStream isporučuju se u v3.539.24 PDF Library for Delphi biblioteke, koja iz jednog source tree-a cilja Delphi, C++Builder i Free Pascal, i u kojoj veliki prilog sada treba da se iz FPC builda vrati bajt po bajt, isto kao što se iz Delphi-ja oduvek vraćao