Før v3.539.24 komprimerede Free Pascal-bygningen af PDF Library for Delphi store streams i 64 KB-chunks og gav hver chunk sit eget zlib-header og checksum, så en vedhæftet fil på 1 MiB blev til seksten zlib-members sat sammen ende mod ende. ISO 32000-1 FlateDecode forventer præcis én zlib-stream, og en standard-decoder stopper ved den første end-of-stream-marker, hvilket betyder, at alt efter de første 65.536 bytes lydløst forsvandt. Fixet holder én zlib-tilstand i live på tværs af alle chunks i både DeflateStream og InflateStream
Fejlen fandtes kun på unitens FPC-side, og kun på den chunkede streamingsti, hvilket præcis er grunden til, at den overlevede: Delphi-grenen var altid korrekt, de strengbaserede helpers var altid korrekte, og små test-payloads nåede aldrig frem til den chunkede kode overhovedet. Den er tæt beslægtet med fejlene i fem FPC-porteringsfejl, som Delphi havde skjult, blot var Delphi-bygningen her ikke med til at dække over noget. FPC-grenen var simpelthen skrevet ud fra en forkert mental model af, hvad en zlib-stream er
Hvorfor bliver en chunked zlib-stream afkortet af andre PDF-readers?
For en zlib-stream som defineret i RFC 1950 er én container, ikke en sekvens af dem, og en korrekt inflater behandler den første Adler-32-trailer som enden på dataene. Formatet er en header på to bytes, én sammenhængende RFC 1951 deflate-bitstream, hvis sidste blok bærer final-block-flaget, og en Adler-32-checksum på fire bytes over alle de ukomprimerede bytes. ISO 32000-1 §7.4.4 definerer /FlateDecode i netop de termer. Når inflate når traileren, returnerer den Z_STREAM_END og efterlader eventuelt resterende input ulæst i avail_in. Set fra dens synsvinkel er der intet galt, så den rejser ingen fejl, og bytesene efter traileren ignoreres simpelthen. Members sat sammen i kæde er en legitim idé i gzip (RFC 1952 tillader flere members i én fil), og dér kommer nok intuitionen fra, men zlib har ingen sådan regel, og PDF har aldrig bedt om en
Den gamle FPC-DeflateStream læste kilden 64 KB ad gangen og gav hver chunk videre til ZFPCCompress, en helper, der kører sin egen deflateInit2, komprimerer med Z_FINISH og kalder deflateEnd. Hver chunk kom derfor ud som en komplet, gyldig, selvterminerende zlib-stream, og funktionen satte dem sammen til én AnsiString, før den skrev den ud. Resultatet lignede Flate-data, havde en korrekt header og dekodede uden fejl, men det dekodede til de første 65.536 bytes og ikke mere. På læsesiden havde InflateStream spejlbilledet af fejlen: den kaldte ZFPCInflate én gang pr. 64 KB komprimeret input, og hvert kald startede en frisk inflateInit2. Den anden chunk begynder midt i en deflate-bitstream uden zlib-header, så en ny inflater afviser den, og en helt normal single-member-stream fra enhver anden producent blev kun dekodet, så langt dens første 64 KB komprimerede bytes rakte
// FPC DeflateStream før v3.539.24 (forenklet):
// ZFPCCompress kører deflateInit2 / deflate(Z_FINISH) / deflateEnd,
// så hver 64 KB-chunk bliver et separat zlib-member
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));
Hvilke PDF Library for Delphi-kald ramte den chunkede sti?
På FPC blev enhver embedded fil på 1 MiB eller mere skrevet forkert, og enhver Flate-stream, der blev pakket ud gennem streaming-API'en, blev læst forkert, så snart dens komprimerede størrelse overskred én chunk. TPDFStream.ReadFromStream beslutter, hvordan indkommende data encodes. Når Deflate er true og Stream.Size >= 1048576, streamer den gennem DeflateStream; under den tærskel læser den hele kilden ind i hukommelsen og kalder DeflateStr, single-shot-helpers, der aldrig blev berørt. ASCII85-plus-Flate-filterkæden springer størrelsestjekket over og går altid gennem DeflateStream, så på den sti var enhver payload større end 64 KB allerede delt op i flere members. De offentlige entry points, der føder ReadFromStream med komprimering slået til, er embedded-file-writerne:
TPDFlib.EmbedFileogTPDFlib.AddEmbeddedFile, som læser en fil fra disk ind i en/EmbeddedFile-streamTPDFlib.AddAssociatedFileFromStreamogTPDFlib.AddAssociatedFileFromFile, PDF/A-3 associated-file-writerne, der bruges til e-faktura-XML og andre kildedata- På læsesiden
TPDFlib.GetEmbeddedFileContentToStreamogGetEmbeddedFileContentToFile, som decoder gennemTPDFStream.WriteDecodedToStreamog derfra gennemInflateStream
Fejlen var stille i hvert lag. Writeren gemmer /Params /Size og en MD5-/CheckSum beregnet ud fra den oprindelige fil, så attachment-dictionaryen annoncerede den fulde størrelse, mens streamen indeholdt seksten members. En Delphi-bygning af samme bibliotek, der læste filen, stoppede rent ved den første Z_STREAM_END og returnerede præcis 65.536 bytes. GetEmbeddedFileContentToStream returnerede 1, fordi den rapporterer, om dekodningen rejste en fejl, ikke om outputtet matcher /Size. Enhver, der har jaget et stor-dokument-problem gennem merging og splitting af gigabyte-PDF'er, kender mønstret: filen åbner, sideantallet stemmer, og skaden viser sig først, når nogen åbner den vedhæftede fil
Én deflate-tilstand på tværs af alle chunks
Den rettede DeflateStream i PDFlibZLib.pas initialiserer én paszlib.TZStream, føder hver chunk til deflate med Z_NO_FLUSH og tapper først til sidst kompressoren med Z_FINISH, indtil den returnerer Z_STREAM_END. Det giver præcis én header, én deflate-bitstream, hvis back-references kan nå på tværs af chunk-grænser, og én Adler-32 over hele inputtet. FPC-grenen har nu samme struktur, som Delphi-grenen altid har haft. Den skriver også output ud, efterhånden som det produceres, i stedet for først at samle hele det komprimerede resultat i en AnsiString, så writeren ikke længere bygger en fuld kopi af de komprimerede data i hukommelsen, før den kopierer den til target
// FPC DeflateStream fra v3.539.24 (fejlstier trimmet)
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); // samme tilstand, intet member-skift
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 // én trailer for hele inputtet
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 fik den symmetriske omskrivning: én inflateInit2, en indre løkke, der bliver ved med at kalde inflate, til den aktuelle chunk er konsumeret og output-bufferen ikke længere er fuld, og et stop ved Z_STREAM_END. Der er én bivirkning, du skal kende. Den gamle chunkede writer hardcodede level 6, mens den nye respekterer PLDeflateLevel, så et level sat gennem TPDFlib.SetCompressionLevel(1..9) gælder nu også for store embeddede filer på FPC. Det betyder noget, hvis du allerede tuner komprimering til arkival-output som beskrevet i at reducere PDF-filstørrelsen i Delphi
Hvordan verificerer du, at en Flate-stream er ét enkelt zlib-member?
Inflat den med en almindelig zlib-decoder og tjek to ting, når den returnerer Z_STREAM_END: den dekodede længde svarer til kildens længde, og avail_in er nul. Input tilbage efter end-markeren er signaturen på en stream sat sammen af flere members. Fixet blev verificeret sådan: 200 KB testdata, der spænder over fire 64 KB-chunks, gik gennem den nye DeflateStream og kom ud som én enkelt zlib-stream på 534 bytes, en standard zlib-decoder gengav alle 200.000 bytes uden resterende input, og samme tjek bestod på FPC-targetet cross-kompileret til i386. Rutinen nedenfor er FPC-versionen af det tjek, bygget direkte på paszlib, så den ikke stoler på koden under test
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;
// Ét member slutter præcis ved den sidste input-byte
Result:= (Status = Z_STREAM_END) and (strm.avail_in = 0);
finally
inflateEnd(strm);
end;
end;
// Push 200.000 bytes gennem DeflateStream med standard 64 KB-chunken
// og kræv ét member, der dekoder tilbage til den fulde længde
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');
På applikationsniveau er den nyttige assertion den, biblioteket ikke laver for dig: sammenlign, hvad der kommer ud af en vedhæftet fil, med den /Params /Size, der blev registreret, da den kom ind. GetEmbeddedFileIntProperty med tag 5 returnerer den registrerede størrelse, embedded-file-indekser er 1-baserede, og payloaden skal være mindst 1 MiB for at ramme streamingstien. Kør den samme test på hver compiler, du sender med, da den oprindelige fejl bestod på Delphi og kun fejlede på 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 eller mere tager den chunkede DeflateStream-sti i 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;
Hvad ændrer fixet ikke?
FPC-grenen beholder sine tolerante dekoderingsregler, og den reparerer ikke filer, som tidligere FPC-builds allerede har skrevet. Når MaxOutput nås, afkorter FPC-InflateStream ved grænsen og returnerer, mens Delphi-grenen rejser ERangeError, og FPC accepterer stadig delvist dekodet output, når inflate rapporterer en datafejl, fordi nogle PDF-producenter udsender afkortede eller checksum-brudte streams. En PDF skrevet af en FPC-build før v3.539.24 indeholder stadig members sat sammen i kæde, og den rettede reader stopper, som enhver anden reader, ved den første Z_STREAM_END. Forsøg ikke at hele sådan en fil ved at dekode og re-encode streamen inde i biblioteket, for det gør kun 64 KB-afkortningen permanent. Embed i stedet den vedhæftede fil igen fra dens oprindelige kilde. FPC-løkken slutter stadig ved den første læsning, der returnerer mindre end en fuld chunk, hvilket kun er et end-of-data-signal for streams som TFileStream og TMemoryStream, så en custom TStream, der sendes til AddAssociatedFileFromStream, er sikrest kopieret ind i en TMemoryStream først. Delphi-grenen, DeflateStr- og InflateStr-helpersne og enhver stream under 1 MiB på den rene Flate-sti opfører sig præcis som før
Den rettede FPC-DeflateStream og InflateStream følger med i v3.539.24 af PDF Library for Delphi, som targeter Delphi, C++Builder og Free Pascal fra ét source tree, og hvor en stor vedhæftet fil nu skulle komme tilbage fra en FPC-build byte for byte, på samme måde som den altid har gjort fra Delphi