Prije v3.539.24, Free Pascal build PDF Library for Delphi komprimirao je velike streamove u chunkove od 64 KB i svakom chunku dodavao vlastito zlib zaglavlje i checksum, pa je prilog od 1 MiB postajao šesnaest zlib članova zalijepljenih kraj na kraj. ISO 32000-1 FlateDecode očekuje točno jedan zlib stream, a standardni dekoder staje na prvom end-of-stream markeru, što znači da je sve nakon prvih 65.536 bajtova tiho nestajalo. Popravak drži jedan zlib state živim preko svih chunkova, i u DeflateStream i u InflateStream
Bug je postojao samo na FPC strani unita, i to samo na chunked streaming putanji, i upravo zato je preživio: Delphi grana bila je uvijek ispravna, helperi nad stringovima također, a mali test payloadi nikad uopće nisu stigli do chunked koda. Bliski je rođak grešaka iz članka o pet FPC porting bugova koje je Delphi godinama krio, s tom razlikom što Delphi build ovdje nije ništa krpio. FPC grana jednostavno je bila napisana na pogrešnoj predodžbi o tome što zapravo jest zlib stream
Zašto drugi PDF čitači skraćuju chunked zlib stream?
Zato što je zlib stream kako ga definira RFC 1950 jedan kontejner, a ne niz kontejnera, i sukladni inflater prvi Adler-32 trailer tretira kao kraj podataka. Format je dvobajtno zaglavlje, jedan neprekidni RFC 1951 deflate bit stream čiji zadnji blok nosi final-block flag, i četverobajtni Adler-32 checksum nad svim nekompresiranim bajtovima. ISO 32000-1 §7.4.4 definira /FlateDecode upravo tim riječima. Kad inflate dođe 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 baca grešku, a bajtovi iza trailera jednostavno se ignoriraju. Ulančani članovi legitimna su ideja u gzipu (RFC 1952 dopušta više članova u jednoj datoteci), odakle je vjerojatno došla i ta intuicija, ali zlib nema takvo pravilo, a PDF ga nikad nije ni tražio
Stari FPC DeflateStream čitao je izvor u koracima od 64 KB i svaki chunk predavao ZFPCCompressu, helperu koji radi vlastiti deflateInit2, komprimira s Z_FINISH i zove deflateEnd. Svaki je chunk stoga izlazio kao potpun, valjan, samozatvorivi zlib stream, a funkcija ih je spojila u jedan AnsiString prije zapisivanja. Rezultat je izgledao kao Flate podaci, imao je ispravno zaglavlje i dekodirao se bez greške, ali se dekodirao samo prvih 65.536 bajtova. Na strani čitanja, InflateStream imao je zrcalnu manu: zvao je ZFPCInflate jednom po 64 KB kompresiranog ulaza, i svaki je poziv pokretao svježi inflateInit2. Drugi chunk počinje usred deflate bit streama bez zlib zaglavlja, pa ga novi inflater odbija, i sasvim normalni jednočlani stream od bilo kojeg drugog producenta dekodirao se samo dok su ga nosili prva 64 KB kompresiranih bajtova
// FPC DeflateStream prije v3.539.24 (pojednostavljeno):
// ZFPCCompress radi deflateInit2 / deflate(Z_FINISH) / deflateEnd,
// pa svaki chunk od 64 KB postaje zaseban 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 ulaze u chunked putanju?
Na FPC-u svaka ugrađena datoteka od 1 MiB ili više zapisivala se pogrešno, a svaki Flate stream izvučen kroz streaming API čitao se pogrešno čim mu je komprimirana veličina prešla jedan chunk. TPDFStream.ReadFromStream odlučuje kako će kodirati dolazne podatke. Kad je Deflate true i Stream.Size >= 1048576, streama kroz DeflateStream; ispod tog praga učita cijeli izvor u memoriju i zove DeflateStr, jednokratni helper koji nikad nije bio pogođen. ASCII85 plus Flate lanac filtera preskače test veličine i uvijek ide kroz DeflateStream, pa je na toj putanji svaki payload veći od 64 KB već bio podijeljen na više članova. Javni ulazni punktovi koji hrane ReadFromStream s uključenom kompresijom pisci su ugrađenih datoteka:
TPDFlib.EmbedFileiTPDFlib.AddEmbeddedFile, koji datoteku s diska učitavaju u stream/EmbeddedFileTPDFlib.AddAssociatedFileFromStreamiTPDFlib.AddAssociatedFileFromFile, pisci PDF/A-3 pridruženih datoteka koje se koriste za XML e-računa i ostale izvorne podatke- Na strani čitanja,
TPDFlib.GetEmbeddedFileContentToStreamiGetEmbeddedFileContentToFile, koji dekodiraju krozTPDFStream.WriteDecodedToStream, a odatle krozInflateStream
Kvar je bio tih na svakom sloju. Pisac zapisuje /Params /Size i MD5 /CheckSum izračunat iz izvorne datoteke, pa je rječnik priloga najavljivao punu veličinu dok je stream držao šesnaest članova. Delphi build iste biblioteke, čitajući tu datoteku, uredno je stao na prvom Z_STREAM_END i vratio točno 65.536 bajtova. GetEmbeddedFileContentToStream vratio je 1, jer javlja je li dekodiranje bacilo grešku, a ne podudara li izlaz s /Size. Tko je god tragao za problemom velikih dokumenata kroz spajanje i rezanje gigabajtnih PDF-ova, taj zna ovaj obrazac: datoteka se otvori, broj stranica je točan, a šteta se pokaže tek kad netko otvori prilog
Jedan deflate state preko svih chunkova
Popravljeni DeflateStream u PDFlibZLib.pas inicijalizira jedan paszlib.TZStream, svaki chunk prepušta pozivu deflate sa Z_NO_FLUSH, i tek na kraju ispušta kompresor sa Z_FINISH dok ne vrati Z_STREAM_END. Time nastaje točno jedno zaglavlje, jedan deflate bit stream čiji back-reference dosežu preko granica chunkova, i jedan Adler-32 nad cijelim ulazom. FPC grana sada ima istu strukturu koju je Delphi grana oduvijek imala. Ispisuje i izlaz čim se proizvede, umjesto da prvo spoji cijeli komprimirani rezultat u AnsiString, pa pisac više ne gradi drugu punu kopiju komprimiranih podataka u memoriji prije nego je kopira u cilj
// FPC DeflateStream od v3.539.24 (grane s greškama izbačene)
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 lomljenja č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 cijeli 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 dobio je simetričan prepravak: jedan inflateInit2, unutarnju petlju koja zove inflate dok se trenutačni chunk ne potroši i izlazni buffer više nije pun, i zaustavljanje na Z_STREAM_END. Jedan je nusprodukt vrijedan pažnje. Stari chunked pisac hardkodirao je razinu 6, a novi poštuje PLDeflateLevel, pa razina postavljena kroz TPDFlib.SetCompressionLevel(1..9) sada vrijedi i za velike ugrađene datoteke na FPC-u. To je bitno ako kompresiju već podešavate za arhivski izlaz, kako je opisano u članku o smanjenju veličine PDF datoteke u Delphiju
Kako provjeriti je li Flate stream jedan jedini zlib član?
Progurajte ga kroz običan zlib dekoder i kad vrati Z_STREAM_END provjerite dvije stvari: je li dekodirana duljina jednaka duljini izvora i je li avail_in nula. Preostali ulaz nakon end markera potpis je ulančanog streama. Popravak je ovako provjeren: 200 KB testnih podataka, koji obuhvaćaju četiri chunka od 64 KB, prošlo je kroz novi DeflateStream i izašlo kao jedan zlib stream od 534 bajta, generički zlib dekoder vratio je svih 200.000 bajtova bez preostalog ulaza, a ista je provjera prošla i na i386 cross-compiled FPC targetu. Rutina ispod FPC je verzija te provjere, izgrađena izravno na paszlibu, pa ne vjeruje kodu koji se 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 završava točno na zadnjem ulaznom bajtu
Result:= (Status = Z_STREAM_END) and (strm.avail_in = 0);
finally
inflateEnd(strm);
end;
end;
// Propustite 200.000 bajtova kroz DeflateStream sa zadanim chunkom od 64 KB
// i zahtijevajte jedan član koji se dekodira natrag do pune duljine
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 razini aplikacije korisna je tvrdnja koju biblioteka ne čini za vas: usporedite ono što izađe iz priloga s /Params /Size zabilježenim pri ulasku. GetEmbeddedFileIntProperty s tagom 5 vraća tu zabilježenu veličinu, indeksi ugrađenih datoteka kreću od 1, a payload mora imati barem 1 MiB da bi se uopće aktivirala streaming putanja. Isti test pokrenite na svakom kompajleru s kojim isporučujete, jer je izvorna greška na Delphiju prolazila, a padala 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 chunked DeflateStream putanjom u ReadFromStream
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;
Što popravak ne mijenja?
FPC grana zadržava svoja popustljiva pravila dekodiranja, i ne popravlja datoteke koje su raniji FPC buildovi već zapisali. Kad se dosegne MaxOutput, FPC InflateStream skraćuje na granici i vraća se, dok Delphi grana baca ERangeError, a FPC i dalje prima djelomično dekodiran izlaz kad inflate javi grešku u podacima, jer neki PDF producenti emitiraju skraćene ili checksum-pokvarene streamove. PDF zapisan FPC buildom starijim od v3.539.24 i dalje sadrži ulančane članove, i popravljeni čitač, kao i svaki drugi, staje na prvom Z_STREAM_END. Ne pokušavajte takvu datoteku izliječiti dekodiranjem i ponovnim kodiranjem streama unutar biblioteke, jer time samo učvršćujete skraćenje na 64 KB. Prilog umjesto toga ponovno ugradite iz njegova izvornog izvora. FPC petlja i dalje završava na prvom čitanju koje vrati manje od punog chunka, a to je signal kraja podataka samo za streamove poput TFileStream i TMemoryStream, pa je vlastiti TStream predan AddAssociatedFileFromStream najsigurnije prvo iskopirati u TMemoryStream. Delphi grana, helperi DeflateStr i InflateStr i svaki stream manji od 1 MiB na običnoj Flate putanji ponašaju se točno kao i prije
Popravljeni FPC DeflateStream i InflateStream isporučuju se u v3.539.24 PDF Library for Delphi, koja iz jednog izvornog stabla cilja Delphi, C++Builder i Free Pascal, a veliki bi se prilog sada iz FPC builda trebao vraćati bajt za bajt, onako kako se iz Delphija oduvijek vraćao