Un writer FlateDecode chunked su Free Pascal deve tenere vivo un solo stato zlib attraverso tutti i chunk. La scorciatoia allettante, comprimere ogni chunk da 64 KB per conto suo, dà a ogni chunk il suo header e il suo checksum zlib, così un allegato da 1 MiB diventa sedici membri zlib incollati uno dietro l'altro. FlateDecode secondo ISO 32000-1 si aspetta esattamente uno stream zlib, e un decoder standard si ferma al primo marker di fine stream, il che significa che tutto ciò che viene dopo i primi 65.536 byte sparisce in silenzio. PDF Library for Delphi tiene vivo un solo stato zlib attraverso tutti i chunk, sia in DeflateStream sia in InflateStream
La trappola esiste solo sul lato FPC di una unit cross-compilatore, e solo sul percorso streaming chunked, che è esattamente il motivo per cui è facile perderla: il ramo Delphi e gli helper a stringa producono ciascuno un singolo stream zlib, e i payload di test piccoli non arrivano mai al codice chunked. È un stretto parente delle trappole raccontate in cinque insidie di porting FPC che Delphi tende a nascondere, solo che qui Delphi non sta coprendo nulla. La scorciatoia poggia semplicemente su un modello mentale sbagliato di che cosa sia uno stream zlib
Perché gli altri viewer PDF troncano uno stream zlib chunked?
Perché uno stream zlib come definito da RFC 1950 è un contenitore unico, non una sequenza di contenitori, e un inflater conforme considera il primo trailer Adler-32 la fine dei dati. Il formato è un header di due byte, un bit stream deflate RFC 1951 continuo il cui ultimo blocco porta il flag final-block, e un checksum Adler-32 di quattro byte su tutti i byte non compressi. ISO 32000-1 §7.4.4 definisce /FlateDecode esattamente in questi termini. Quando inflate raggiunge il trailer restituisce Z_STREAM_END e lascia eventuale input residuo non letto in avail_in. Dal suo punto di vista non c'è nulla di sbagliato, quindi non alza alcun errore, e i byte dopo il trailer vengono semplicemente ignorati. I membri concatenati sono un'idea legittima in gzip (RFC 1952 consente più membri in un file), ed è probabile che l'intuizione venisse da lì, ma zlib non ha una regola del genere e il PDF non l'ha mai chiesta
Il DeflateStream FPC naive legge la sorgente 64 KB alla volta e passa ogni chunk a ZFPCCompress, un helper che esegue il suo deflateInit2, comprime con Z_FINISH e chiama deflateEnd. Ogni chunk esce quindi come stream zlib completo, valido e auto-terminato, e la funzione li concatena in un unico AnsiString prima di scriverlo. Il risultato sembra dati Flate, ha un header corretto e si decodifica senza errori, ma si decodifica solo fino ai primi 65.536 byte. Sul lato lettura c'è la trappola speculare: chiamare ZFPCInflate una volta per ogni 64 KB di input compresso fa partire un inflateInit2 nuovo ogni volta. Il secondo chunk inizia in mezzo a un bit stream deflate senza header zlib, quindi un inflater nuovo lo rifiuta, e uno stream perfettamente normale a membro singolo prodotto da qualunque altro producer viene decodificato solo fino a dove lo portano i primi 64 KB di byte compressi
// DeflateStream FPC chunked naive (semplificato):
// ZFPCCompress esegue deflateInit2 / deflate(Z_FINISH) / deflateEnd,
// quindi ogni chunk da 64 KB diventa un membro zlib separato
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));
Quali chiamate di PDF Library for Delphi arrivano al percorso chunked?
Su FPC, il percorso chunked scrive ogni file embedded da 1 MiB in su, e legge ogni stream Flate estratto attraverso la streaming API appena la sua dimensione compressa supera un chunk. TPDFStream.ReadFromStream decide come codificare i dati in entrata. Quando Deflate è true e Stream.Size >= 1048576, passa per DeflateStream in streaming; sotto quella soglia legge l'intera sorgente in memoria e chiama DeflateStr, l'helper one-shot che comprime tutto in una sola chiamata. La catena di filtri ASCII85 più Flate salta il test sulla dimensione e passa sempre per DeflateStream, quindi su quel percorso qualunque payload oltre 64 KB copre già più chunk. I punti d'ingresso pubblici che alimentano ReadFromStream con la compressione attiva sono i writer di file embedded:
TPDFlib.EmbedFileeTPDFlib.AddEmbeddedFile, che leggono un file dal disco dentro uno stream/EmbeddedFileTPDFlib.AddAssociatedFileFromStreameTPDFlib.AddAssociatedFileFromFile, i writer di associated-file per PDF/A-3 usati per l'XML delle fatture elettroniche e gli altri dati sorgente- Sul lato lettura,
TPDFlib.GetEmbeddedFileContentToStreameGetEmbeddedFileContentToFile, che decodificano attraversoTPDFStream.WriteDecodedToStreame da lì attraversoInflateStream
Sbagliare questo fallisce in silenzio a ogni layer. Il writer memorizza /Params /Size e un /CheckSum MD5 calcolato dal file originale, così il dizionario dell'allegato dichiara la dimensione completa anche quando lo stream contiene sedici membri. Qualunque reader conforme, build Delphi della libreria compresa, si ferma pulitamente al primo Z_STREAM_END e restituisce esattamente 65.536 byte. GetEmbeddedFileContentToStream restituisce 1, perché riporta se la decodifica ha alzato un errore, non se l'output corrisponde a /Size. Chi ha già rincorso un problema da documento grande attraverso merge e split di PDF da gigabyte riconosce lo schema: il file si apre, il conteggio pagine è giusto, e il danno si vede solo quando qualcuno apre l'allegato
Un solo stato deflate attraverso tutti i chunk
Il DeflateStream in PDFlibZLib.pas inizializza un solo paszlib.TZStream, alimenta ogni chunk a deflate con Z_NO_FLUSH, e solo alla fine svuota il compressore con Z_FINISH finché non restituisce Z_STREAM_END. Questo produce esattamente un header, un bit stream deflate i cui back-reference possono raggiungere oltre i confini dei chunk, e un Adler-32 sull'intero input. Il ramo FPC ha la stessa struttura del ramo Delphi. Scrive anche l'output mentre viene prodotto, invece di concatenare prima l'intero risultato compresso in un AnsiString, così il writer non costruisce mai una seconda copia completa dei dati compressi in memoria prima di copiarla sul target
// FPC DeflateStream con un solo stato zlib (percorsi di errore omessi)
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); // stesso stato, nessuna rottura di membro
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 solo trailer per l'intero input
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 segue lo stesso schema: un solo inflateInit2, un ciclo interno che continua a chiamare inflate finché il chunk corrente non è consumato e il buffer di output non è più pieno, e uno stop su Z_STREAM_END. C'è un effetto collaterale che vale la pena conoscere. Poiché il writer chiama deflateInit2 direttamente invece di passare per un helper a livello fisso, rispetta PLDeflateLevel, quindi un livello impostato con TPDFlib.SetCompressionLevel(1..9) vale anche per i file embedded grandi su FPC. Conta se già tarate la compressione per output di archivio come descritto in ridurre la dimensione dei PDF in Delphi
Come verificare che uno stream Flate sia un solo membro zlib?
Decodificalo con un decoder zlib di serie e controlla due cose quando restituisce Z_STREAM_END: che la lunghezza decodificata eguagli quella della sorgente, e che avail_in sia zero. Input residuo dopo il marker di fine è la firma di uno stream concatenato. La routine qui sotto è la versione FPC di quel controllo, costruita direttamente su paszlib così non si fida del codice sotto test, seguita da una prova che spinge 200.000 byte, quattro chunk da 64 KB, dentro DeflateStream
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 membro termina esattamente all'ultimo byte di input
Result:= (Status = Z_STREAM_END) and (strm.avail_in = 0);
finally
inflateEnd(strm);
end;
end;
// Passa 200.000 byte dentro DeflateStream con il chunk predefinito di 64 KB
// ed esigi un membro che si decodi di nuovo sulla lunghezza completa
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');
A livello di applicazione, l'asserzione utile è quella che la libreria non fa per te: confronta ciò che esce da un allegato con il /Params /Size registrato quando è entrato. GetEmbeddedFileIntProperty con tag 5 restituisce quella dimensione registrata, gli indici dei file embedded partono da 1, e il payload deve essere almeno 1 MiB per imboccare il percorso streaming. Esegui lo stesso test su ogni compilatore con cui spedisci, dato che questa classe di errori passa su Delphi e fallisce solo su 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 o più imbocca il percorso chunked di DeflateStream 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;
Casi limite sul ramo FPC
Il ramo FPC usa regole di decodifica permissive. Quando viene raggiunto MaxOutput, InflateStream su FPC tronca al limite e restituisce, mentre il ramo Delphi alza ERangeError, e FPC accetta ancora output parzialmente decodificato quando inflate segnala un errore sui dati, perché alcuni producer PDF emettono stream troncati o con checksum rotti. Un PDF che contiene già membri concatenati, qualunque cosa lo abbia scritto, si ferma al primo Z_STREAM_END in ogni reader. Non provare a guarire un file simile decodificando e ricodificando lo stream dentro la libreria, perché non fai che rendere permanente il troncamento a 64 KB. Ri-embedda l'allegato dalla sua sorgente originale. Il loop FPC tratta una lettura che restituisce meno di un chunk intero come fine dei dati, il che vale per stream come TFileStream e TMemoryStream, quindi uno TStream personalizzato passato a AddAssociatedFileFromStream è più sicuro se copiato prima in un TMemoryStream
Il DeflateStream e l'InflateStream FPC a stato unico sono in PDF Library for Delphi, che copre Delphi, C++Builder e Free Pascal da un unico albero sorgente, così un allegato grande torna da una build FPC byte per byte, esattamente come da Delphi