Een chunked FlateDecode-writer onder Free Pascal moet één zlib-state in leven houden over alle chunks heen. De verleidelijke shortcut, elke chunk van 64 KB afzonderlijk comprimeren, geeft elke chunk zijn eigen zlib-header en checksum, zodat een bijlage van 1 MiB uit zestien achter elkaar geplakte zlib-members bestaat. ISO 32000-1 verwacht bij FlateDecode precies één zlib-stream, en een standaarddecoder stopt bij de eerste end-of-stream-marker, wat betekent dat alles na de eerste 65.536 bytes geruisloos verdwijnt. PDF Library for Delphi houdt één zlib-state in leven over alle chunks heen, zowel in DeflateStream als in InflateStream
De valkuil bestaat alleen in het FPC-deel van een cross-compiler-unit, en alleen op het chunked streaming-pad, en dat is precies waarom hij zo makkelijk te missen is: de Delphi-tak en de stringgebaseerde helpers produceren elk een enkele zlib-stream, en kleine testpayloads bereiken de chunked code helemaal niet. Hij is een naaste verwant van de valkuilen uit vijf FPC-portagevalkuilen die Delphi vaak verborgen houdt, met dit verschil dat Delphi hier niets wegstopt. De shortcut berust simpelweg op een verkeerd beeld van wat een zlib-stream eigenlijk is
Waarom kappen andere PDF-readers een chunked zlib-stream af?
Omdat een zlib-stream zoals RFC 1950 die definieert één container is, geen reeks containers, en een conforme inflater de eerste Adler-32-trailer als het einde van de data behandelt. Het formaat bestaat uit een header van twee bytes, één doorlopende RFC 1951-deflate-bitstream waarvan het laatste blok de final-block-flag draagt, en een Adler-32-checksum van vier bytes over alle ongecomprimeerde bytes. ISO 32000-1 §7.4.4 definieert /FlateDecode precies in die termen. Zodra inflate de trailer bereikt geeft hij Z_STREAM_END terug en laat hij resterende invoer ongelezen in avail_in staan. Vanuit zijn perspectief is er niets aan de hand, dus hij geeft geen foutmelding, en de bytes na de trailer worden simpelweg genegeerd. Aaneengeregen members zijn een legitiem idee in gzip (RFC 1952 staat meerdere members in één bestand toe), en daar komt de intuïtie waarschijnlijk vandaan, maar zlib heeft zo'n regel niet en PDF heeft er nooit om gevraagd
De naïeve FPC-DeflateStream leest de bron per 64 KB en geeft elke chunk door aan ZFPCCompress, een helper die zijn eigen deflateInit2 draait, met Z_FINISH comprimeert en deflateEnd aanroept. Elke chunk komt er dus uit als een complete, geldige, zichzelf afsluitende zlib-stream, en de functie plakt ze aan elkaar tot één AnsiString voordat die weggeschreven wordt. Het resultaat lijkt op Flate-data, heeft een correcte header en decodeert zonder foutmelding, maar decodeert wel tot precies de eerste 65.536 bytes. Aan de leeszijkant wacht het spiegelbeeld van dezelfde valkuil: ZFPCInflate aanroepen per 64 KB gecomprimeerde invoer start telkens een verse inflateInit2. De tweede chunk begint midden in een deflate-bitstream zonder zlib-header, dus een nieuwe inflater wijst hem af, en een volstrekt normale single-member-stream van een willekeurige andere producent wordt alleen zo ver gedecodeerd als zijn eerste 64 KB gecomprimeerde bytes reiken
// Naïeve chunked FPC-DeflateStream (vereenvoudigd):
// ZFPCCompress doet deflateInit2 / deflate(Z_FINISH) / deflateEnd,
// dus elke chunk van 64 KB wordt een apart 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));
Welke aanroepen in PDF Library for Delphi bereiken het chunked pad?
Op FPC schrijft het chunked pad elk embedded file van 1 MiB of groter, en leest het elke via de streaming-API opgehaalde Flate-stream zodra zijn gecomprimeerde grootte één chunk passeert. TPDFStream.ReadFromStream bepaalt hoe inkomende data gecodeerd wordt. Is Deflate waar en geldt Stream.Size >= 1048576, dan streamt hij via DeflateStream; onder die drempel leest hij de hele bron in het geheugen en roept hij DeflateStr aan, de single-shot-helper die alles in één aanroep comprimeert. De filterketen ASCII85-plus-Flate slaat de sizetest over en gaat altijd via DeflateStream, dus op dat pad beslaat elke payload groter dan 64 KB al meerdere chunks. De publieke toegangspunten die ReadFromStream voeden met compressie ingeschakeld zijn de embedded-file-writers:
TPDFlib.EmbedFileenTPDFlib.AddEmbeddedFile, die een bestand van schijf inlezen in een/EmbeddedFile-streamTPDFlib.AddAssociatedFileFromStreamenTPDFlib.AddAssociatedFileFromFile, de PDF/A-3-associated-file-writers voor e-factuur-XML en andere brongegevens- Aan de leeszijkant
TPDFlib.GetEmbeddedFileContentToStreamenGetEmbeddedFileContentToFile, die decoderen viaTPDFStream.WriteDecodedToStreamen daarna viaInflateStream
Wie dit verkeerd doet, ziet op elke laag niets mis gaan. De writer bewaart /Params /Size en een MD5-/CheckSum berekend uit het originele bestand, dus de bijlage-dictionary adverteert de volledige grootte terwijl de stream zestien members bevat. Elke conformerende reader, ook een Delphi-build van de library, stopt netjes bij de eerste Z_STREAM_END en geeft precies 65.536 bytes terug. GetEmbeddedFileContentToStream geeft 1 terug, want hij meldt of het decoderen een fout opleverde, niet of de uitvoer met /Size overeenkomt. Wie ooit een groot-documentenprobleem najoeg langs het samenvoegen en splitsen van gigabyte-PDF’s kent dit patroon: het bestand opent, het paginanaantal klopt, en de schade komt pas boven water wanneer iemand de bijlage opent
Eén deflate-state over alle chunks heen
De DeflateStream in PDFlibZLib.pas initialiseert één paszlib.TZStream, voedt elke chunk aan deflate met Z_NO_FLUSH, en spoelt aan het einde de compressor pas leeg met Z_FINISH tot die Z_STREAM_END teruggeeft. Dat levert precies één header op, één deflate-bitstream waarvan de back-references over chunkgrenzen heen kunnen reiken, en één Adler-32 over de hele invoer. De FPC-tak heeft dezelfde structuur als de Delphi-tak. Hij schrijft de uitvoer bovendien weg naarmate die ontstaat, in plaats van eerst het hele gecomprimeerde resultaat in een AnsiString aan elkaar te plakken, dus de writer bouwt nooit een tweede volledige kopie van de gecomprimeerde data in het geheugen voordat die naar het doel gekopieerd wordt
// FPC-DeflateStream met één zlib-state (foutpaden weggelaten)
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); // zelfde state, geen member-breuk
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 voor de hele invoer
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 volgt hetzelfde patroon: één inflateInit2, een binnenlus die inflate blijft aanroepen tot de huidige chunk verbruikt is en de uitvoerbuffer niet langer vol zit, en een stop op Z_STREAM_END. Er is één bijwerking die u goed moet weten. Omdat de writer deflateInit2 zelf aanroept in plaats van via een helper met vast level, volgt hij PLDeflateLevel, dus een level dat via TPDFlib.SetCompressionLevel(1..9) gezet is geldt op FPC ook voor grote embedded files. Dat is relevant als u compressie al afstemt op archiefuitvoer, zoals beschreven in het verkleinen van PDF-bestanden in Delphi
Hoe verifieert u dat een Flate-stream één enkele zlib-member is?
Inflateer hem met een gewone zlib-decoder en controleer twee dingen op het moment dat die Z_STREAM_END teruggeeft: de gedecodeerde lengte is gelijk aan de bronnengte, en avail_in is nul. Invoer die na de eindmarker overblijft is de signatuur van een aaneengeregen stream. De routine hieronder is de FPC-versie van die controle, rechtstreeks op paszlib gebouwd zodat hij de code die getest wordt niet vertrouwt, gevolgd door een probe die 200.000 bytes, vier chunks van 64 KB, door DeflateStream drukt
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;
// Eén member eindigt precies op de laatste invoerbyte
Result:= (Status = Z_STREAM_END) and (strm.avail_in = 0);
finally
inflateEnd(strm);
end;
end;
// Duw 200.000 bytes door DeflateStream met de standaard chunk van 64 KB
// en eis één member die terug decodeert tot de volledige lengte
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');
Op applicatieniveau is de nuttige assertie degene die de library niet voor u maakt: vergelijk wat eruit komt van een bijlage met de /Params /Size die bij het erin stoppen is vastgelegd. GetEmbeddedFileIntProperty met tag 5 geeft die vastgelegde grootte terug, embedded-file-indexen zijn 1-based, en de payload moet minstens 1 MiB zijn om het streamingpad te raken. Draai dezelfde test op elke compiler waar u mee uitlevert, want deze klasse fouten gaat op Delphi goed en mislukt alleen op 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 of meer neemt het chunked DeflateStream-pad in 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;
Randgevallen in de FPC-tak
De FPC-tak hanteert soepele decoderingsregels. Als MaxOutput bereikt is, kapt FPC-InflateStream op de grens af en keert terug, terwijl de Delphi-tak een ERangeError gooit, en FPC accepteert nog steeds gedeeltelijk gedecodeerde uitvoer wanneer inflate een datafout meldt, omdat sommige PDF-producenten afgekapte of checksum-gebroken streams uitzenden. Een PDF die al aaneengeregen members bevat, van welke schrijver dan ook, stopt in elke reader bij de eerste Z_STREAM_END. Probeer zo’n bestand niet te genezen door de stream binnen de library te decoderen en opnieuw te encoderen, want dat maakt de afkapping van 64 KB alleen maar permanent. Embed de bijlage opnieuw vanaf zijn oorspronkelijke bron. De FPC-lus behandelt een leesactie die minder dan een volle chunk teruggeeft als het einde van de data, wat klopt voor streams als TFileStream en TMemoryStream, dus een eigen TStream die aan AddAssociatedFileFromStream wordt meegegeven kopieert u het veiligst eerst naar een TMemoryStream
De FPC-DeflateStream en InflateStream met één state zitten in PDF Library for Delphi, die Delphi, C++Builder en Free Pascal vanuit één source tree bedient, dus een grote bijlage komt uit een FPC-build byte voor byte terug, precies zoals uit Delphi