Înainte de v3.539.24, build-ul Free Pascal al PDF Library for Delphi comprima stream-urile mari în chunk-uri de 64 KB și dădea fiecărui chunk propriul header și checksum zlib, astfel încât un atașament de 1 MiB devenea șaisprezece membri zlib lipiți cap la cap. FlateDecode din ISO 32000-1 așteaptă exact un singur stream zlib, iar un decoder standard se oprește la primul marker de sfârșit de stream, ceea ce înseamnă că tot ce venea după primii 65.536 de octeți dispărea în tăcere. Reparația ține o singură stare zlib vie peste toate chunk-urile, atât în DeflateStream, cât și în InflateStream
Bug-ul exista doar pe partea de FPC a unit-ului și doar pe calea de streaming chunked, ceea ce explică exact de ce a supraviețuit: ramura Delphi a fost mereu corectă, helper-ele pe bază de string au fost mereu corecte, iar payload-urile mici de test nu ajungeau niciodată până la codul chunked. Este o rudă apropiată a defectelor din cinci bug-uri de portare FPC pe care Delphi le-a ascuns, cu deosebirea că aici build-ul Delphi nu acoperea nimic. Ramura FPC fusese pur și simplu scrisă pe baza unui model mental greșit despre ce este un stream zlib
De ce trunchiază alte cititoare PDF un stream zlib chunked?
Pentru că un stream zlib așa cum îl definește RFC 1950 este un singur container, nu o secvență de containere, iar un inflater conformant tratează primul trailer Adler-32 drept sfârșitul datelor. Formatul este un header de doi octeți, un bit stream RFC 1951 deflate continuu al cărui ultim bloc poartă flag-ul final-block, și un checksum Adler-32 de patru octeți peste toți octeții necomprimați. §7.4.4 din ISO 32000-1 definește /FlateDecode exact în acești termeni. Când inflate ajunge la trailer, întoarce Z_STREAM_END și lasă orice input rămas necitit în avail_in. Din punctul ei de vedere nimic nu este în neregulă, deci nu ridică nicio eroare, iar octeții de după trailer sunt pur și simplu ignorați. Membrii concatenați sunt o idee legitimă în gzip (RFC 1952 permite mai mulți membri într-un fișier), de acolo vine probabil intuiția, dar zlib nu are o asemenea regulă și PDF nu a cerut niciodată una
Vechiul DeflateStream de pe FPC citea sursa câte 64 KB și împingea fiecare chunk spre ZFPCCompress, un helper care își rulează propriul deflateInit2, comprimă cu Z_FINISH și apelează deflateEnd. Fiecare chunk ieșea deci ca un stream zlib complet, valid, auto-terminat, iar funcția îi concatena într-un singur AnsiString înainte de a-l scrie. Rezultatul arăta a date Flate, avea un header corect și se decoda fără eroare, dar se decoda doar până la primii 65.536 de octeți. Pe partea de citire, InflateStream avea defectul în imagine inversă: apela ZFPCInflate o dată la fiecare 64 KB de input comprimat, iar fiecare apel pornea un inflateInit2 nou. Al doilea chunk începe în mijlocul unui bit stream deflate, fără header zlib, deci un inflater nou îl respinge, iar un stream perfect normal cu un singur membru, produs de oricine altcineva, era decodat doar până unde îl duceau primii săi 64 KB de octeți comprimați
// FPC DeflateStream înainte de v3.539.24 (simplificat):
// ZFPCCompress face deflateInit2 / deflate(Z_FINISH) / deflateEnd,
// deci fiecare chunk de 64 KB devine un membru zlib separat
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));
Ce apeluri din PDF Library for Delphi ajungeau pe calea chunked?
Pe FPC, orice fișier încorporat de 1 MiB sau mai mult era scris greșit, iar orice stream Flate extras prin API-ul de streaming era citit greșit imediat ce dimensiunea sa comprimată trecea de un chunk. TPDFStream.ReadFromStream decide cum să encodeze datele primite. Când Deflate este true și Stream.Size >= 1048576, le trece prin DeflateStream; sub acel prag citește întreaga sursă în memorie și apelează DeflateStr, helper-ul single-shot care n-a fost niciodată afectat. Lanțul de filtre ASCII85 plus Flate sare peste testul de dimensiune și trece mereu prin DeflateStream, deci pe calea aceea orice payload mai mare de 64 KB era deja împărțit în mai mulți membri. Punctele de intrare publice care hrănesc ReadFromStream cu compresia pornită sunt scriitorii de fișiere încorporate:
TPDFlib.EmbedFileșiTPDFlib.AddEmbeddedFile, care citesc un fișier de pe disc într-un stream/EmbeddedFileTPDFlib.AddAssociatedFileFromStreamșiTPDFlib.AddAssociatedFileFromFile, scriitorii de associated-file PDF/A-3 folosiți pentru XML de e-factură și alte date sursă- Pe partea de citire,
TPDFlib.GetEmbeddedFileContentToStreamșiGetEmbeddedFileContentToFile, care decod prinTPDFStream.WriteDecodedToStreamși de acolo prinInflateStream
Eșecul a fost tăcut la fiecare strat. Scriitorul stochează /Params /Size și un /CheckSum MD5 calculat din fișierul original, deci dicționarul atașamentului anunța dimensiunea completă în timp ce stream-ul ținea șaisprezece membri. Un build Delphi al aceleiași biblioteci care citea acel fișier se oprea curat la primul Z_STREAM_END și întorcea exact 65.536 de octeți. GetEmbeddedFileContentToStream întorcea 1, pentru că raportează dacă decodarea a ridicat o eroare, nu dacă rezultatul se potrivește cu /Size. Oricine a urmărit o problemă de document mare prin îmbinarea și divizarea de PDF-uri de ordinul gigabyte-ului cunoaște tiparul acesta: fișierul se deschide, numărul de pagini e corect, iar paguba se vede doar când cineva deschide atașamentul
O singură stare deflate peste toate chunk-urile
DeflateStream-ul reparat din PDFlibZLib.pas inițializează un singur paszlib.TZStream, îi dă fiecare chunk spre deflate cu Z_NO_FLUSH și abia la sfârșit golește compresorul cu Z_FINISH până când acesta întoarce Z_STREAM_END. Asta produce exact un header, un bit stream deflate ale cărui back-references pot sări peste granițele dintre chunk-uri și un singur Adler-32 peste tot input-ul. Ramura FPC are acum aceeași structură pe care ramura Delphi a avut-o dintotdeauna. Scrie și rezultatul pe măsură ce e produs, în loc să concateneze mai întâi tot rezultatul comprimat într-un AnsiString, deci scriitorul nu își mai construiește o a doua copie completă a datelor comprimate în memorie înainte de a o copia spre destinație
// FPC DeflateStream din v3.539.24 (căile de eroare tăiate)
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); // aceeași stare, fără rupere de membru
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 // un singur trailer pentru tot input-ul
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 a primit rescrierea simetrică: un singur inflateInit2, o buclă interioară care continuă să apeleze inflate până când chunk-ul curent e consumat și buffer-ul de rezultat nu mai e plin, și o oprire la Z_STREAM_END. Există un efect secundar pe care merită să îl știți. Vechiul scriitor chunked hardcoda nivelul 6, în timp ce cel nou respectă PLDeflateLevel, deci un nivel setat prin TPDFlib.SetCompressionLevel(1..9) se aplică acum și fișierelor încorporate mari pe FPC. Contează dacă vă reglați deja compresia pentru output de arhivă, așa cum descrie reducerea dimensiunii fișierelor PDF în Delphi
Cum verificați că un stream Flate este un singur membru zlib?
Îl inflați cu un decoder zlib obișnuit și verificați două lucruri când întoarce Z_STREAM_END: lungimea decodată este egală cu lungimea sursei și avail_in este zero. Input-ul rămas după markerul de sfârșit este semnătura unui stream concatenat. Reparația a fost verificată exact așa: 200 KB de date de test, care se întind pe patru chunk-uri de 64 KB, au trecut prin noul DeflateStream și au ieșit ca un stream zlib de 534 de octeți cu un singur membru, un decoder zlib de stoc a recuperat toți cei 200.000 de octeți fără input rămas, iar aceeași verificare a trecut și pe ținta FPC cross-compilată i386. Rutina de mai jos este versiunea FPC a acestei verificări, construită direct pe paszlib, ca să nu aibă încredere în codul testat
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;
// Un singur membru se termină exact la ultimul octet de input
Result:= (Status = Z_STREAM_END) and (strm.avail_in = 0);
finally
inflateEnd(strm);
end;
end;
// Trecem 200.000 de octeți prin DeflateStream cu chunk-ul implicit de 64 KB
// și cerem un singur membru care se decodează înapoi la lungimea completă
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');
La nivel de aplicație, aserțiunea utilă este cea pe care biblioteca nu o face pentru dumneavoastră: comparați ce iese dintr-un atașament cu /Params /Size înregistrat la intrare. GetEmbeddedFileIntProperty cu tag-ul 5 întoarce dimensiunea înregistrată, indicii fișierelor încorporate pornesc de la 1, iar payload-ul trebuie să aibă cel puțin 1 MiB ca să exercite calea de streaming. Rulați același test pe fiecare compilator cu care livrați, pentru că defectul original trecea pe Delphi și eșua doar pe 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 sau mai mult ia calea DeflateStream chunked din 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;
Ce nu schimbă reparația?
Ramura FPC își păstrează regulile de decodare îngăduitoare și nu repară fișierele pe care build-urile FPC anterioare le scrisese deja. Când se atinge MaxOutput, InflateStream de pe FPC trunchiază la limită și se întoarce, în timp ce ramura Delphi ridică ERangeError, iar FPC acceptă în continuare output decodat parțial când inflate raportează o eroare de date, pentru că unii producători PDF emit stream-uri trunchiate sau cu checksum stricat. Un PDF scris de un build FPC anterior lui v3.539.24 conține în continuare membri concatenați, iar cititorul corectat, ca orice alt cititor, se oprește la primul Z_STREAM_END. Nu încercați să vindecați un asemenea fișier decodând și recodând stream-ul în interiorul bibliotecii, pentru că așa faceți trunchierea de 64 KB permanentă. Re-încorporați atașamentul din sursa lui originală. Bucla FPC se mai termină și acum la prima citire care întoarce mai puțin decât un chunk întreg, ceea ce este semnal de sfârșit de date doar pentru stream-uri precum TFileStream și TMemoryStream, deci un TStream custom pasat spre AddAssociatedFileFromStream e cel mai sigur copiat întâi într-un TMemoryStream. Ramura Delphi, helper-ele DeflateStr și InflateStr și orice stream sub 1 MiB pe calea Flate simplă se comportă exact ca înainte
DeflateStream-ul și InflateStream-ul FPC corectate se livrează în v3.539.24 din PDF Library for Delphi, care țintește Delphi, C++Builder și Free Pascal dintr-un singur arbore de cod, iar un atașament mare ar trebui acum să revină dintr-un build FPC octet cu octet, exact cum a revenit dintotdeauna din Delphi