Iki v3.539.24 Free Pascal versija PDF Library for Delphi didelius srautus spaudė 64 KB gabalais ir kiekvienam gabalui dėdavo savo zlib antraštę bei kontrolinę sumą, tad 1 MiB priedas virsdavo šešiolika galo su galu suklijuotų zlib narių. ISO 32000-1 FlateDecode tikisi lygiai vieno zlib srauto, o standartinis dekoderis sustoja prie pirmosios srauto pabaigos žymos – tai reiškia, kad visa, kas buvo po pirmųjų 65 536 baitų, tyliai dingo. Pataisa DeflateStream ir InflateStream viduje palaiko vieną zlib būseną per visus gabalus
Klaida gyveno tik unito FPC pusėje ir tik gabalų srautinio perdavimo kelyje – būtent todėl ji ir išgyveno: Delphi šaka visuomet buvo teisinga, eilučių pagalbinės funkcijos visuomet buvo teisingos, o maži testiniai duomenų paketai iki gabalų kodo apskritai nepasiekdavo. Ji yra artima giminaitė defektams iš straipsnio apie penkias FPC perkėlimo klaidas, kurias slapstė Delphi, tik čia Delphi versija nieko nedengė. FPC šaka buvo tiesiog parašyta remiantis klaidingu įsivaizdavimu, kas iš tikrųjų yra zlib srautas
Kodėl kitos PDF peržiūros programos apkarpo gabalais rašytą zlib srautą?
Nes RFC 1950 apibrėžtas zlib srautas yra vienas konteineris, o ne jų seka, ir standartą geriantis inflater pirmąjį Adler-32 užraktą laiko duomenų pabaiga. Formatas yra dviejų baitų antraštė, viena nenutrūkstama RFC 1951 deflate bitų srautas, kurio paskutinis blokas neša final-block vėliavėlę, ir keturių baitų Adler-32 kontrolinė suma visiems nesuspaustiems baitams. ISO 32000-1 §7.4.4 /FlateDecode apibrėžia būtent tais žodžiais. Kai inflate pasiekia užraktą, jis grąžina Z_STREAM_END, o likusį neiskaitytą įvedimą palieka avail_in. Jo požiūriu nieko bloga neįvyko, todėl klaidos jis nekelia, o baitus po užrakto tiesiog ignoruoja. Sujungti nariai gzip formatuote yra teisėta idėja (RFC 1952 leidžia kelis narius viename faile) – greičiausiai iš ten ir atėjo ta intuicija, bet zlib tokios taisyklės neturi, o PDF jos niekada neprašė
Sena FPC DeflateStream skaitė šaltinį po 64 KB ir kiekvieną gabalą perdavinėjo ZFPCCompress – pagalbinei funkcijai, kuri pati paleidžia deflateInit2, spaudžia su Z_FINISH ir iškviečia deflateEnd. Kiekvienas gabalas taip išeidavo kaip pilnas, teisingas, pats save uždarantis zlib srautas, o funkcija juos sujungdavo į vieną AnsiString prieš rašydama į išvedimą. Rezultatas atrodė kaip Flate duomenys, turėjo teisingą antraštę ir dekodavosi be klaidų, bet dekoduodavosi tik iki pirmųjų 65 536 baitų. Skaitymo pusėje InflateStream turėjo veidrodišką defektą: jis kviesdavo ZFPCInflate po kartą kiekvieniems 64 KB suspaustų duomenų, ir kiekvienas iškvietimas startuodavo su nauja inflateInit2. Antras gabalas prasideda deflate bitų srauto viduryje be zlib antraštės, tad naujas inflater jį atmeta, ir visiškai įprastas vieno nario srautas iš bet kurio kito generatoriaus būdavo dekoduotas tik tiek, kiek nešė pirmieji jo 64 KB suspaustų baitų
// FPC DeflateStream iki v3.539.24 (supaprastinta):
// ZFPCCompress atlieka deflateInit2 / deflate(Z_FINISH) / deflateEnd,
// tad kiekvienas 64 KB gabalas tampa atskiru zlib nariu
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));
Kurie PDF Library for Delphi iškvietimai patekdavo į gabalų kelią?
FPC aplinkoje bet koks įmontuotas failas nuo 1 MiB ir daugiau būdavo rašomas neteisingai, o bet koks Flate srautas, ištrauktas pro srautinę API, būdavo skaitomas neteisingai, tik jo suspaustas dydis peržengdavo vieną gabalą. TPDFStream.ReadFromStream nusprendžia, kaip koduoti gaunamus duomenis. Kai Deflate yra true ir Stream.Size >= 1048576, duomenys keliauja pro DeflateStream; žemiau tos ribos visas šaltinis skaitomas į atmintį ir kviečiamas DeflateStr – vienkartė pagalbinė funkcija, kurios šis defektas niekada nepalietė. ASCII85 plus Flate filtrų grandinė dydžio testo nepaiso ir visuomet eina pro DeflateStream, tad tame kelyje bet kokie duomenys, didesni nei 64 KB, jau būdavo suskaidyti į kelis narius. Viešieji įėjimo taškai, kurie maitina ReadFromStream įjungtu suspaudimu, yra įmontuotų failų rašytojai:
TPDFlib.EmbedFileirTPDFlib.AddEmbeddedFile, kurie iš disko skaito failą į/EmbeddedFilesrautąTPDFlib.AddAssociatedFileFromStreamirTPDFlib.AddAssociatedFileFromFile– PDF/A-3 susietųjų failų rašytojai, naudojami el. sąskaitų XML ir kitiems šaltinio duomenims- Skaitymo pusėje –
TPDFlib.GetEmbeddedFileContentToStreamirGetEmbeddedFileContentToFile, kurie dekoduoja proTPDFStream.WriteDecodedToStream, o iš ten proInflateStream
Gedimas buvo tylus kiekviename sluoksnyje. Rašytojas saugo /Params /Size ir MD5 /CheckSum, apskaičiuotą iš originalaus failo, tad priedo žodynas reklamavo pilną dydį, kol sraute gulėjo šešiolika narių. Tos pačios bibliotekos Delphi versija, skaičiusi tą failą, švariai sustodavo prie pirmojo Z_STREAM_END ir grąžindavo lygiai 65 536 baitų. GetEmbeddedFileContentToStream grąžindavo 1, nes jis praneša, ar dekodavimas kėlė klaidą, o ne ar išvedimas sutampa su /Size. Kas benarštėtų didelio dokumento problemą pro gigabaitinių PDF sujungimą ir skaidymą, tas pažįsta šį raštą: failas atsiverčia, puslapių skaičius teisingas, o žala išryškėja tik tada, kai kas nors atveria priedą
Viena deflate būsena per visus gabalus
Pataisytas DeflateStream faile PDFlibZLib.pas inicializuoja vieną paszlib.TZStream, kiekvieną gabalą duoda deflate su Z_NO_FLUSH, o tik pačioje pabaigoje ištuština kompresorių su Z_FINISH, kol šis grąžina Z_STREAM_END. Taip gaunama lygiai viena antraštė, vienas deflate bitų srautas, kurio atgalinės nuorodos gali persitiesti per gabalų ribas, ir vienas Adler-32 visam įvedimui. FPC šaka dabar turi tą pačią struktūrą, kurią Delphi šaka turėjo visada. Be to, išvedimas rašomas jau jį gaminant, o ne iš anksto sujungiant visą suspaustą rezultatą į AnsiString, tad rašytojas daugiau nebekuria antros pilnos suspaustų duomenų kopijos atmintyje prieš nukopijuodamas ją į taikinį
// FPC DeflateStream nuo v3.539.24 (klaidų atšakos iškirptos)
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); // ta pati būsena, jokio nario lūžio
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 // vienas užraktas visam įvedimui
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 sulaukė simetriško perrašymo: vienas inflateInit2, vidinė kilpa, kuri nenustoja kvietusi inflate, kol dabartinis gabalas išvartojamas ir išvedimo buferis nebeprisipildo, ir sustojimas ties Z_STREAM_END. Yra vienas šalutinis efektas, vertas žinoti. Senas gabalų rašytojas lygį 6 turėjo įkoduotą, o naujasis gerbia PLDeflateLevel, tad lygis, nustatytas pro TPDFlib.SetCompressionLevel(1..9), dabar galioja ir dideliems įmontuotiems failams FPC aplinkoje. Tai svarbu, jei suspaudimą jau derinate archyviniams išvedimams, kaip aprašyta straipsnyje apie PDF failo dydžio mažinimą Delphi aplinkoje
Kaip patikrinti, ar Flate srautas yra vienas zlib narys?
Išpūskite jį paprastu zlib dekoderiumi ir kai jis grąžina Z_STREAM_END, pasitikrinkite du dalykus: dekoduotas ilgis lygus šaltinio ilgiui, o avail_in lygus nuliui. Likutis įvedimo po pabaigos žymos yra sujungto srauto pirštų atspaudas. Pataisa buvo patikrinta būtent taip: 200 KB testinių duomenų, dengiančių keturis 64 KB gabalus, praėjo pro naująjį DeflateStream ir išėjo kaip 534 baitų vienas zlib srautas, standartinis zlib dekoderis atkūrė visus 200 000 baitų be likusio įvedimo, o tas pats testas praeito ir i386 cross-compile FPC taikinyje. Žemiau esanti procedūra yra tos patikros FPC versija, paremta tiesiogiai paszlib, kad netektų pasitikėti patikrinamu kodu
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;
// Vienas naris baigiasi lygiai paskutiniame įvedimo baite
Result:= (Status = Z_STREAM_END) and (strm.avail_in = 0);
finally
inflateEnd(strm);
end;
end;
// Perkiškite 200 000 baitų pro DeflateStream su numatytuoju 64 KB gabalu
// ir reikalaukite vieno nario, kuris dekoduojasi atgal į pilną ilgį
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');
Programos lygiu naudingiausia patikra yra ta, kurios biblioteka už jus nepadaro: palyginkite, kas išeina iš priedo, su /Params /Size, įrašytu jį įdedant. GetEmbeddedFileIntProperty su žyme 5 grąžina tą įrašytą dydį, įmontuotų failų indeksai skaičiuojami nuo 1, o duomenų paketas turi būti bent 1 MiB, kad pasiektų srautinį kelią. Tą patį testą paleiskite su kiekvienu kompiliatoriumi, su kuriuo pristatote produktą, nes originalus defektas Delphi aplinkoje praėjo sėkmingai, o žlugdavo tik 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 ir daugiau eina gabalų DeflateStream keliu ReadFromStream viduje
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;
Ką pataisa nekeičia?
FPC šaka lieka prie savo atlaisvintų dekodavimo taisyklių ir negydo failų, kuriuos jau buvo parašę ankstesni FPC variantai. Pasiekus MaxOutput, FPC InflateStream nukerpa ties riba ir grįžta, kol Delphi šaka kelia ERangeError, o FPC vis tiek priima iš dalies dekoduotą išvedimą, kai inflate praneša duomenų klaidą, nes kai kurie PDF generatoriai išveda apkerptus arba su sugadinta kontroline suma srautus. PDF, parašytas FPC versijos iki v3.539.24, vis dar turi sujungtus narius, ir pataisytas skaitytuvas, kaip ir bet kuris kitas, sustoja prie pirmojo Z_STREAM_END. Negydykite tokio failo dekoduodami ir pakartotinai koduodami srautą bibliotekos viduje – tai tik įamžins 64 KB apkarpytimą. Geriau vėl įmontuokite priedą iš jo pirminio šaltinio. FPC kilpa taip pat vis dar baigiasi pirmuoju skaitymu, grąžinusiu mažiau nei pilną gabalą, o tai duomenų pabaigos signalas tik srautams, tokiems kaip TFileStream ir TMemoryStream, tad nuosavą TStream, perduodamą AddAssociatedFileFromStream, saugiausia pirmiausia nukopijuoti į TMemoryStream. Delphi šaka, DeflateStr ir InflateStr pagalbinės funkcijos ir kiekvienas mažesnis nei 1 MiB srautas paprastame Flate kelyje elgiasi lygiai taip, kaip anksčiau
Pataisytos FPC DeflateStream ir InflateStream atkeliauja su PDF Library for Delphi v3.539.24, kuri iš vieno šaltinio medžio palaiko Delphi, C++Builder ir Free Pascal, ir kurioje didelis priedas iš FPC versijos dabar turėtų grįžti baitas po baito – taip, kaip visada grįždavo iš Delphi