Pred različico v3.539.24 je gradnja PDF Library for Delphi na Free Pascalu stiskala velike tokove v koščkih po 64 KB in vsakemu koščku dala svojo glavo in nadzorno vsoto zlib, tako da je priloga velikosti 1 MiB postala šestnajst členov zlib, zlepljenih od konca do konca. FlateDecode po ISO 32000-1 pričakuje natanko en tok zlib, standardni dekoder pa se ustavi pri prvem markerju konca toka, kar pomeni, da je vse za prvimi 65.536 bajti tiho izginilo. Popravek ohrani eno stanje zlib živo čez vse koščke, tako v DeflateStream kot v InflateStream
Napaka je obstajala le na strani enote za FPC in le na poti pretakanja po koščkih, kar je točno razlog, zakaj je preživela: veja za Delphi je bila vedno pravilna, pomožniki na nizih prav tako, majhni preskusni paketi pa sploh niso prišli do kode za koščke. Gre za tesno sorodnico napak iz petih napak prenašanja na FPC, ki jih je Delphi skrival, le da tukaj gradnja za Delphi ničesar ni zakrivala. Veja FPC je bila enostavno napisana ob napačni predstavi o tem, kaj je tok zlib
Zakaj tok zlib v koščkih drugi bralniki PDF odrežejo?
Ker je tok zlib, kot ga definira RFC 1950, ena posoda in ne zaporedje posod, izpolnjujoči inflater pa prvi zaključek Adler-32 obravnava kot konec podatkov. Format je dvobajtna glava, en neprekinjen bitni tok deflate po RFC 1951, katerega zadnji blok nosi zastavico zadnjega bloka, in štiribajtna nadzorna vsota Adler-32 nad vsemi nestisnjenimi bajti. ISO 32000-1 §7.4.4 definira /FlateDecode natanko v teh izrazih. Ko inflate pride do zaključka, vrne Z_STREAM_END in morebitni preostali vhod pusti prebran v avail_in. Z njegovega zornega kota ni nič narobe, zato ne sproži nobene napake, bajti za zaključkom pa se preprosto prezrejo. Zlepljeni členi so upravičena ideja v gzipu (RFC 1952 dopušča več členov v eni datoteki), verjetno od tam prihaja tudi intuicija, zlib pa takega pravila nima in PDF ga ni nikoli zahteval
Stari DeflateStream na FPC je branil vir po 64 KB naenkrat in vsak košček izročil ZFPCCompress, pomožniku, ki požene svoj deflateInit2, stiska z Z_FINISH in pokliče deflateEnd. Vsak košček je torej prišel ven kot popoln, veljaven, sam zaključen tok zlib, funkcija pa jih je pred zapisom zlepila v en AnsiString. Rezultat je izgledal kot podatki Flate, imel je pravilno glavo in se dekodiral brez napake, a samo do prvih 65.536 bajtov. Na bralni strani je imel InflateStream zrcalno sliko iste napake: ZFPCInflate je poklical enkrat na 64 KB stisnjenega vhoda, vsak klic pa je začel svež inflateInit2. Drugi košček se začne na sredini bitnega toka deflate brez glave zlib, zato ga novi inflater zavrne, in povsem običajen enočlenski tok od katerih koli drugega proizvajalca se je dekodiral le toliko, kolikor je nosil njegovih prvih 64 KB stisnjenih bajtov
// FPC DeflateStream pred v3.539.24 (poenostavljeno):
// ZFPCCompress izvede deflateInit2 / deflate(Z_FINISH) / deflateEnd,
// zato vsak košček 64 KB postane poseben člen zlib
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));
Kateri klici PDF Library for Delphi so prišli do poti po koščkih?
Na FPC je bila vsaka vdelana datoteka velikosti 1 MiB ali več zapisana narobe, vsak tok Flate, izvlečen skozi API za pretakanje, pa prebran narobe, takoj ko je njegova stisnjena velikost presegla en košček. TPDFStream.ReadFromStream odloča, kako kodirati dohodne podatke. Ko je Deflate resničen in Stream.Size >= 1048576, pretaka skozi DeflateStream; pod tem pragom prebere celoten vir v spomin in pokliče DeflateStr, enokratnega pomožnika, ki ni bil nikoli prizadet. Veriga filtrov ASCII85 plus Flate preskoči preizkus velikosti in vedno gre skozi DeflateStream, zato je bil na tej poti vsak paket, večji od 64 KB, že razdeljen na več členov. Javne vstopne točke, ki napajajo ReadFromStream z vklopljenim stiskanjem, so pisalniki vdelanih datotek:
TPDFlib.EmbedFileinTPDFlib.AddEmbeddedFile, ki datoteko z diska prebereta v tok/EmbeddedFileTPDFlib.AddAssociatedFileFromStreaminTPDFlib.AddAssociatedFileFromFile, pisalnika povezanih datotek PDF/A-3 za e-računski XML in druge izvorne podatke- Na bralni strani
TPDFlib.GetEmbeddedFileContentToStreaminGetEmbeddedFileContentToFile, ki dekodirata skoziTPDFStream.WriteDecodedToStreamin od tam skoziInflateStream
Odpoved je bila tiha na vsaki plasti. Pisalnik shrani /Params /Size in /CheckSum MD5, izračunan iz izvorne datoteke, zato je slovar priloge oglaševal polno velikost, medtem ko je tok nosil šestnajst členov. Gradnja iste knjižnice za Delphi se je pri branju te datoteke čisto ustavila pri prvem Z_STREAM_END in vrnila natanko 65.536 bajtov. GetEmbeddedFileContentToStream je vrnil 1, ker sporoča, ali je dekodiranje sprožilo napako, in ne, ali se izhod ujema z /Size. Vsakomur, ki je težavo z velikimi dokumenti že lovil skozi združevanje in razdvajanje gigabajtnih PDF-jev, je ta vzorec znan: datoteka se odpre, število strani je prav, škoda pa pride na dan šele, ko nekdo odpre prilogo
Eno stanje deflate čez vse koščke
Popravljeni DeflateStream v PDFlibZLib.pas inicializira en paszlib.TZStream, vsak košček dovaja deflate z Z_NO_FLUSH in šele na koncu izprazni kompresor z Z_FINISH, dokler ta ne vrne Z_STREAM_END. To da natanko eno glavo, en bitni tok deflate, katerega povratne reference lahko segajo čez meje koščkov, in en Adler-32 nad celotnim vhodom. Veja FPC ima zdaj enako zgradbo, kot jo je vselej imela veja Delphi. Izhod tudi zapisuje, ko nastaja, namesto da bi celoten stisnjeni rezultat najprej zlepila v AnsiString, zato pisalnik ne zgradi več druge polne kopije stisnjenih podatkov v spominu, preden jo prekopira na cilj
// FPC DeflateStream od v3.539.24 (napačne poti odrezane)
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); // isto stanje, brez preloma med členi
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 // en zaključek za celoten vhod
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 dobil simetričen prepis: en inflateInit2, notranjo zanko, ki kliče inflate, dokler ni trenutni košček porabljen in izhodni medpomnilnik ni več poln, in ustavitev ob Z_STREAM_END. Vredno vedenja je en stranski učinek. Stari pisalnik po koščkih je trdo kodiral stopnjo 6, novi pa upošteva PLDeflateLevel, zato stopnja, nastavljena prek TPDFlib.SetCompressionLevel(1..9), zdaj velja tudi za velike vdelane datoteke na FPC. To je pomembno, če stiskanje za arhivski izhod že uglašujete, kot je opisano v zmanjševanju velikosti datotek PDF v Delphiju
Kako preverite, da je tok Flate en sam člen zlib?
Napihnite ga z navadnim dekoderjem zlib in ob vrnitvi Z_STREAM_END preverite dvoje: dekodirana dolžina je enaka dolžini vira, avail_in pa je nič. Preostali vhod za markerjem konca je podpis zlepljenega toka. Popravek je bil preverjen takole: 200 KB preskusnih podatkov, ki segajo čez štiri koščke 64 KB, je šlo skozi novi DeflateStream in ven kot en sam tok zlib, dolg 534 bajtov, običajni dekoder zlib je vrnil vseh 200.000 bajtov brez preostalega vhoda, isti preizkus pa je uspel tudi na navzkrižno prevedenem cilju FPC za i386. Spodnja rutina je različica tega preizkusa za FPC, zgrajena neposredno na paszlib, zato ne zaupa kodi, ki jo preizkuša
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;
// En člen se konča točno pri zadnjem vhodnem bajtu
Result:= (Status = Z_STREAM_END) and (strm.avail_in = 0);
finally
inflateEnd(strm);
end;
end;
// Spustite 200.000 bajtov skozi DeflateStream s privzetim koščkom 64 KB
// in zahtevajte en člen, ki se dekodira nazaj do polne dolžine
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 ravni aplikacije je uporabna trditev tista, ki je knjižnica ne naredi za vas: primerjajte, kar pride iz priloge, z /Params /Size, zabeleženo ob vnosu. GetEmbeddedFileIntProperty z oznako 5 vrne to zabeleženo velikost, kazala vdelanih datotek tečejo od 1, paket pa mora imeti vsaj 1 MiB, da sploh pride do poti pretakanja. Isti preizkus poženite na vsakem prevajalniku, s katerim knjižnico dobavljate, saj je prvotna napaka na Delphiju šla skozi, odpovedala pa se je le 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 ali več gre po poti DeflateStream po koščkih 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;
Kaj popravek ne spremeni?
Veja FPC obdrži svoja popustljiva pravila dekodiranja in ne popravi datotek, ki so jih že zapisale starejše gradnje FPC. Ko je dosežen MaxOutput, FPC InflateStream odreže na meji in se vrne, medtem ko veja Delphi sproži ERangeError, FPC pa še vedno sprejme delno dekodiran izhod, kadar inflate sporoči napako podatkov, ker nekateri proizvajalci PDF oddajajo okrnjene tokove ali tokove s pokvarjeno nadzorno vsoto. PDF, zapisan z gradnjo FPC pred v3.539.24, še vedno vsebuje zlepljene člene, popravljeni bralnik pa se, kot vsak drug bralnik, ustavi pri prvem Z_STREAM_END. Ne poskušajte takšne datoteke zdraviti z dekodiranjem in ponovnim kodiranjem toka znotraj knjižnice, saj to okrnitev na 64 KB le naredi trajno. Namesto tega prilogo znova vdelajte iz njenega izvirnega vira. FPC zanka se še vedno konča pri prvem branju, ki vrne manj kot poln košček, kar je signal konca podatkov le za tokove, kot sta TFileStream in TMemoryStream, zato je po meri narejen TStream, podan AddAssociatedFileFromStream, najvarneje najprej prekopirati v TMemoryStream. Veja Delphi, pomožnika DeflateStr in InflateStr ter vsi tokovi, manjši od 1 MiB, na običajni poti Flate delujejo natanko kot prej
Popravljeni FPC DeflateStream in InflateStream prihajata z v3.539.24 PDF Library for Delphi, ki cilja na Delphi, C++Builder in Free Pascal iz enega izvornega drevesa, in kjer mora velika priloga zdaj iz gradnje FPC priti nazaj bajt za bajt, na enak način, kot je vedno prihajala iz Delphija