Pred verziou v3.539.24 komprimovalo Free Pascal zostavenie PDF Library for Delphi veľké streamy v 64 KB blokoch a každý blok dostal vlastnú zlib hlavičku a kontrolný súčet, takže príloha s veľkosťou 1 MiB sa premenila na šestnásť zlib členov nalepených jeden za druhým. FlateDecode podľa ISO 32000-1 očakáva presne jeden zlib stream a štandardný dekodér sa zastaví na prvej značke konca streamu, takže všetko za prvými 65 536 bajtmi potichu zmizlo. Oprava drží jeden zlib stav nažive cez každý blok v oboch DeflateStream aj InflateStream
Chyba existovala len na strane FPC tejto jednotky a len na chunked streaming ceste, a presne preto prežila: Delphi vetva bola vždy správna, helpery postavené na reťazcoch boli vždy správne a malé testovacie dátové sady sa ku chunked kódu nikdy nedostali. Je to blízky príbuzný defektov opísaných v článku o piatich FPC portovacích chybách, ktoré Delphi roky krylo, s tým rozdielom, že tu Delphi zostavenie nič nepokrývalo. FPC vetva bola jednoducho napísaná podľa nesprávnej predstavy o tom, čo zlib stream vlastne je
Prečo iné PDF prehliadače skrátia chunked zlib stream?
Pretože zlib stream podľa definície v RFC 1950 je jeden kontajner, nie postupnosť kontajnerov, a korektný inflater považuje prvý trailér Adler-32 za koniec dát. Formát je dvojbajtová hlavička, jeden súvislý deflate bitový stream podľa RFC 1951, ktorého posledný blok nesie príznak final-block, a štyrbajtový kontrolný súčet Adler-32 cez všetky nekomprimované bajty. ISO 32000-1 §7.4.4 definuje /FlateDecode presne v týchto termínoch. Keď inflate dorazí k trailéru, vráti Z_STREAM_END a prípadný zostávajúci vstup nechá neprečítaný v avail_in. Z jeho pohľadu nie je nič pokazené, takže nevyhodí žiadnu chybu a bajty za trailérom jednoducho ignoruje. Spájané členy sú v gzipu legitimná idea, RFC 1952 dovoľuje viacero členov v jednom súbore, a odtiaľ tá intuícia asi prišla, ale zlib žiadne takéto pravidlo nemá a PDF oň nikdy nežiadalo
Starý FPC DeflateStream čítal zdroj po 64 KB a každý blok odovzdával helperu ZFPCCompress, ktorý si spustí vlastný deflateInit2, komprimuje s Z_FINISH a zavolá deflateEnd. Každý blok teda vyšiel ako kompletný, platný, sebaukončujúci zlib stream a funkcia ich spájala do jedného AnsiString ešte pred zápisom. Výsledok vyzeral ako Flate dáta, mal správnu hlavičku a dekódoval sa bez chyby, ale dekódoval sa len do prvých 65 536 bajtov. Na čítacej strane mal InflateStream zrkadlový defekt: volal ZFPCInflate raz na každých 64 KB komprimovaného vstupu a každé volanie štartovalo čerstvý inflateInit2. Druhý blok začína v strede deflate bitového streamu bez zlib hlavičky, takže nový inflater ho odmietne, a úplne bežný jednoclenný stream od ktoréhokoľvek iného producenta sa dekódoval len po prvých 64 KB komprimovaných bajtov
// FPC DeflateStream pred v3.539.24 (zjednodušené):
// ZFPCCompress robí deflateInit2 / deflate(Z_FINISH) / deflateEnd,
// takže každý 64 KB blok sa stane samostatným zlib členom
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));
Ktoré volania PDF Library for Delphi sa dostali na chunked cestu?
Na FPC sa každý vložený súbor od 1 MiB nahor zapisoval zle a každý Flate stream ťahaný cez streaming API sa čítal zle, akonáhle jeho komprimovaná veľkosť prekročila jeden blok. TPDFStream.ReadFromStream rozhoduje, ako kódovať prichádzajúce dáta. Keď je Deflate true a Stream.Size >= 1048576, tečie cez DeflateStream; pod touto hranicou načíta celý zdroj do pamäte a zavolá DeflateStr, jednorazový helper, ktorého sa chyba nikdy nedotkla. Reťazec filtrov ASCII85 plus Flate test veľkosti preskočí a ide vždy cez DeflateStream, takže na tejto ceste sa každá dátová sada väčšia než 64 KB už delila na viacero členov. Verejné vstupné body, ktoré kŕmia ReadFromStream so zapnutou kompresiou, sú pisateľia vložených súborov:
TPDFlib.EmbedFileaTPDFlib.AddEmbeddedFile, ktoré načítajú súbor z disku do streamu/EmbeddedFileTPDFlib.AddAssociatedFileFromStreamaTPDFlib.AddAssociatedFileFromFile, pisateľia asociovaných súborov PDF/A-3, ktorí sa používajú pre XML e-faktúr a iné zdrojové dáta- Na čítacej strane
TPDFlib.GetEmbeddedFileContentToStreamaGetEmbeddedFileContentToFile, ktoré dekódujú cezTPDFStream.WriteDecodedToStreama odtiaľ cezInflateStream
Zlyhanie bolo v každej vrstve tiché. Pisateľ ukladá /Params /Size a MD5 /CheckSum počítaný z pôvodného súboru, takže slovník prílohy deklaroval plnú veľkosť, kým stream držal šestnásť členov. Delphi zostavenie tej istej knižnice sa pri čítaní takéhoto súboru poriadne zastavilo na prvom Z_STREAM_END a vrátilo presne 65 536 bajtov. GetEmbeddedFileContentToStream vrátilo 1, lebo hlási, či dekódovanie vyhodilo chybu, nie či výstup sedí s /Size. Ktokoľvek, kto už prenasledoval problém veľkých dokumentov cez spájanie a delenie gigabajtových PDF, pozná tento vzor: súbor sa otvorí, počet strán sedí a poškodenie sa ukáže, až keď si niekto otvorí prílohu
Jeden deflate stav cez všetky bloky
Opravený DeflateStream v PDFlibZLib.pas inicializuje jeden paszlib.TZStream, kŕmi každý blok do deflate s Z_NO_FLUSH a až na konci vypúšťa kompresor s Z_FINISH, kým nevráti Z_STREAM_END. To dáva presne jednu hlavičku, jeden deflate bitový stream, ktorého back-references môžu siahnuť cez hranice blokov, a jedno Adler-32 cez celý vstup. FPC vetva má teraz tú istú štruktúru, ktorú mala Delphi vetva vždy. Výstup navyše zapisuje hneď, ako vzniká, namiesto toho, aby najprv spojil celý komprimovaný výsledok do AnsiString, takže pisateľ si už pred kopírovaním do cieľa nebuduje druhú plnú kópiu komprimovaných dát v pamäti
// FPC DeflateStream od v3.539.24 (chybové cesty vystrihnuté)
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); // ten istý stav, žiadne lomenie člena
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 // jeden trailér pre celý vstup
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 dostal symetrický prepis: jedno inflateInit2, vnútornú slučku, ktorá volá inflate donekonečna, kým sa nespotrebuje aktuálny blok a výstupný buffer už nie je plný, a zastavenie na Z_STREAM_END. Jeden vedľajší efekt stojí za poznanie. Starý chunked pisateľ mal natvrdo level 6, nový rešpektuje PLDeflateLevel, takže level nastavený cez TPDFlib.SetCompressionLevel(1..9) sa teraz na FPC uplatní aj pri veľkých vložených súboroch. To má význam, keď už ladíte kompresiu pre archívny výstup, ako je popísané v článku o znižovaní veľkosti PDF súborov v Delphi
Ako overiť, že Flate stream je jeden zlib člen?
Nafúkajte ho obyčajným zlib dekodérom a po vrátení Z_STREAM_END skontrolujte dve veci: dekódovaná dĺžka sa rovná dĺžke zdroja a avail_in je nula. Zostávajúci vstup za značkou konca je podpis spájaného streamu. Oprava sa overila presne takto: 200 KB testovacích dát, čo sú štyri 64 KB bloky, prešlo novým DeflateStream a vyšiel z nich jediný zlib stream o 534 bajtoch, štandardný zlib dekodér vrátil všetkých 200 000 bajtov bez zostatku na vstupe a tá istá kontrola prešla aj na i386 cross-kompilovanom FPC cieli. Nižšia rutina je FPC verzia tejto kontroly, postavená priamo na paszlib, aby neverila testovanému kódu
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;
// Jeden člen končí presne na poslednom vstupnom bajte
Result:= (Status = Z_STREAM_END) and (strm.avail_in = 0);
finally
inflateEnd(strm);
end;
end;
// Preženite 200 000 bajtov cez DeflateStream s predvoleným 64 KB blokom
// a vyžadujte jeden člen, ktorý sa dekóduje späť na celú dĺžku
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 úrovni aplikácie je užitočný ten assertion, ktorý za vás knižnica nerobí: porovnajte, čo z prílohy vychádza, s /Params /Size zaznamenaným pri vkladaní. GetEmbeddedFileIntProperty s tagom 5 vráti túto zaznamenanú veľkosť, indexy vložených súborov sa číslujú od 1 a dátová sada musí mať aspoň 1 MiB, aby sa streaming cesta vôbec spustila. Rovnaký test spustite na každom kompilátore, s ktorým dodávate, keďže pôvodný defekt na Delphi prešiel a zlyhal len na FPC
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 a viac berie chunked cestu DeflateStream v 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;
Čo oprava nemení?
FPC vetva si necháva zhovievavé dekódovacie pravidlá a neremontuje súbory, ktoré už napísali staršie FPC zostavenia. Keď sa dosiahne MaxOutput, FPC InflateStream skráti výstup na hranici a vráti sa, zatiaľ čo Delphi vetva vyhodí ERangeError, a FPC stále prijíma čiastočne dekódovaný výstup, keď inflate hlási chybu dát, lebo niektorí PDF producenti vypúšťajú skrátené alebo kontrolným súčtom pokazené streamy. PDF napísané FPC zostavením starším než v3.539.24 stále obsahuje spájané členy a opravený čítač sa, ako každý iný, zastaví na prvom Z_STREAM_END. Takýto súbor sa nepokúšajte vyliečiť dekódovaním a opätovným zakódovaním streamu vnútri knižnice, tým sa len 64 KB skrátenie zabetónuje. Namiesto toho vložte prílohu znovu z jej pôvodného zdroja. FPC slučka tiež stále končí na prvom čítaní, ktoré vráti menej než plný blok, čo je signál konca dát len pre streamy typu TFileStream a TMemoryStream, takže vlastný TStream odovzdaný do AddAssociatedFileFromStream je najbezpečnejšie najprv skopírovať do TMemoryStream. Delphi vetva, helpery DeflateStr a InflateStr a každý stream menší než 1 MiB na čistej Flate ceste sa správajú presne ako doteraz
Opravený FPC DeflateStream a InflateStream prichádza vo v3.539.24 PDF Library for Delphi, ktorá cieli na Delphi, C++Builder aj Free Pascal z jedného zdrojového stromu, a kde by veľká príloha mala z FPC zostavenia vyjsť teraz bajt za bajtom, rovnako ako z Delphi vždy vychádzala