Un encodeur FlateDecode par blocs sur Free Pascal doit conserver un seul état zlib d'un bloc au suivant. Le raccourci tentant, compresser chaque bloc de 64 Ko indépendamment, donne à chaque bloc son propre en-tête zlib et son propre checksum, si bien qu'une pièce jointe de 1 Mio devient seize membres zlib collés bout à bout. ISO 32000-1 attend exactement un seul flux zlib derrière FlateDecode, et un décodeur standard s'arrête au premier marqueur de fin de flux, ce qui fait disparaître silencieusement tout ce qui suit les premiers 65 536 octets. PDF Library for Delphi conserve un seul état zlib d'un bloc au suivant, dans DeflateStream comme dans InflateStream
Le piège n'existe que du côté FPC d'une unité multi-compilateurs, et seulement sur le chemin du streaming par blocs, c'est précisément pourquoi il passe inaperçu : la branche Delphi et les helpers basés sur chaînes produisent chacun un flux zlib unique, et de petits payloads de test n'atteignent jamais le code par blocs. C'est un proche cousin des pièges des cinq bugs de portage FPC que Delphi cache souvent, à cette différence que Delphi ne masque rien ici. Le raccourci repose simplement sur un modèle mental erroné de ce qu'est un flux zlib
Pourquoi les autres lecteurs PDF tronquent-ils un flux zlib par blocs ?
Parce qu'un flux zlib au sens du RFC 1950 est un conteneur unique, pas une séquence de conteneurs, et qu'un inflater conforme traite le premier trailer Adler-32 comme la fin des données. Le format, c'est un en-tête de deux octets, un flux de bits deflate RFC 1951 continu dont le dernier bloc porte le drapeau final-block, et une somme de contrôle Adler-32 de quatre octets calculée sur tous les octets décompressés. ISO 32000-1 §7.4.4 définit /FlateDecode exactement en ces termes. Quand inflate atteint le trailer, il renvoie Z_STREAM_END et laisse dans avail_in toute entrée restante non lue. De son point de vue rien ne va pas, donc il ne lève pas d'erreur, et les octets après le trailer sont simplement ignorés. Les membres concaténés sont une idée légitime en gzip (le RFC 1952 autorise plusieurs membres dans un fichier), et c'est probablement de là que venait l'intuition, mais zlib n'a pas de telle règle et le PDF ne l'a jamais demandée
Le DeflateStream FPC naïf lit la source par blocs de 64 Ko et confie chaque bloc à ZFPCCompress, un helper qui appelle son propre deflateInit2, compresse avec Z_FINISH, puis appelle deflateEnd. Chaque bloc produit donc un flux zlib complet, valide et auto-terminé, et la fonction concatène le tout dans une seule AnsiString avant d'écrire. Le résultat ressemble à des données Flate, a un en-tête correct et se décode sans erreur, mais il ne décode que les premiers 65 536 octets. La lecture cache le piège symétrique : appeler ZFPCInflate une fois par bloc de 64 Ko d'entrée compressée relance un inflateInit2 neuf à chaque fois. Le second bloc commence au milieu d'un bit stream deflate sans en-tête zlib, un nouvel inflater le refuse donc, et un flux parfaitement normal à membre unique produit par n'importe quel autre producteur n'est décodé que jusqu'où portent ses premiers 64 Ko d'octets compressés
// FPC DeflateStream naïf par blocs (simplifié) :
// ZFPCCompress enchaîne deflateInit2 / deflate(Z_FINISH) / deflateEnd,
// donc chaque bloc de 64 Ko devient un membre zlib séparé
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));
Quels appels de PDF Library for Delphi empruntent le chemin par blocs ?
Sur FPC, le chemin par blocs écrit toute pièce jointe de 1 Mio ou plus, et lit tout flux Flate extrait via l'API de streaming dès que sa taille compressée dépasse un bloc. TPDFStream.ReadFromStream décide comment encoder les données entrantes. Quand Deflate est à true et que Stream.Size >= 1048576, il passe par DeflateStream en streaming ; en dessous de ce seuil il charge toute la source en mémoire et appelle DeflateStr, le helper one-shot qui compresse tout en un seul appel. La chaîne de filtres ASCII85 plus Flate saute le test de taille et passe toujours par DeflateStream, si bien que sur ce chemin tout payload de plus de 64 Ko chevauche déjà plusieurs blocs. Les points d'entrée publics qui alimentent ReadFromStream avec compression activée sont les écrivains de pièces jointes :
TPDFlib.EmbedFileetTPDFlib.AddEmbeddedFile, qui lisent un fichier depuis le disque vers un flux/EmbeddedFileTPDFlib.AddAssociatedFileFromStreametTPDFlib.AddAssociatedFileFromFile, les écrivains de fichiers associés PDF/A-3 utilisés pour le XML de facture électronique et d'autres données sources- Côté lecture,
TPDFlib.GetEmbeddedFileContentToStreametGetEmbeddedFileContentToFile, qui décodent viaTPDFStream.WriteDecodedToStreampuis viaInflateStream
Se tromper ici échoue silencieusement à chaque couche. L'écriture stocke /Params /Size et un /CheckSum MD5 calculé sur le fichier d'origine, si bien que le dictionnaire de pièce jointe annonce la taille complète même quand le flux contient seize membres. Tout lecteur conforme, build Delphi de la bibliothèque compris, s'arrête proprement au premier Z_STREAM_END et rend exactement 65 536 octets. GetEmbeddedFileContentToStream renvoie 1, parce qu'il signale si le décodage a levé une erreur, pas si la sortie correspond à /Size. Quiconque a traqué un problème de gros document en passant par la fusion et la découpe de PDF de plusieurs gigaoctets connaît ce schéma : le fichier s'ouvre, le nombre de pages est bon, et le dégât n'apparaît que lorsque quelqu'un ouvre la pièce jointe
Un seul état deflate à travers tous les blocs
Le DeflateStream de PDFlibZLib.pas initialise un seul paszlib.TZStream, alimente chaque bloc vers deflate avec Z_NO_FLUSH, et ne draine le compresseur avec Z_FINISH, jusqu'à obtenir Z_STREAM_END, qu'à la fin. Résultat : exactement un en-tête, un bit stream deflate dont les références arrière peuvent traverser les frontières de blocs, et un Adler-32 sur toute l'entrée. La branche FPC a la même structure que la branche Delphi. Elle écrit aussi la sortie au fil de sa production, au lieu de concaténer d'abord tout le résultat compressé dans une AnsiString, si bien que le writer ne construit jamais une seconde copie complète des données compressées en mémoire avant de la copier vers la cible
// FPC DeflateStream avec un seul état zlib (chemins d'erreur retirés)
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); // même état, aucune rupture de membre
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 seul trailer pour toute l'entrée
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 suit le même schéma : un seul inflateInit2, une boucle interne qui rappelle inflate jusqu'à ce que le bloc courant soit consommé et que le tampon de sortie ne soit plus plein, et un arrêt sur Z_STREAM_END. Un effet de bord mérite d'être connu. Comme l'écriture appelle elle-même deflateInit2 au lieu de passer par un helper à niveau figé, elle honore PLDeflateLevel, donc un niveau fixé via TPDFlib.SetCompressionLevel(1..9) vaut aussi pour les grosses pièces jointes sur FPC. Cela compte si vous réglez déjà la compression pour de l'archivage, comme décrit dans la réduction de la taille des PDF en Delphi
Comment vérifier qu'un flux Flate est un seul membre zlib ?
Décompressez-le avec un décodeur zlib ordinaire et vérifiez deux choses au retour de Z_STREAM_END : la longueur décodée égale la longueur source, et avail_in vaut zéro. De l'entrée restante après le marqueur de fin, c'est la signature d'un flux concaténé. La routine ci-dessous est la version FPC de ce contrôle, construite directement sur paszlib pour ne pas faire confiance au code testé, suivie d'une sonde qui pousse 200 000 octets, quatre blocs de 64 Ko, dans 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 membre se termine exactement au dernier octet d'entrée
Result:= (Status = Z_STREAM_END) and (strm.avail_in = 0);
finally
inflateEnd(strm);
end;
end;
// Poussez 200 000 octets dans DeflateStream avec le bloc de 64 Ko par défaut
// et exigez un seul membre qui redécode à la longueur complète
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');
Au niveau applicatif, l'assertion utile est celle que la bibliothèque ne fait pas pour vous : comparez ce qui sort d'une pièce jointe avec le /Params /Size enregistré à l'entrée. GetEmbeddedFileIntProperty avec le tag 5 rend cette taille enregistrée, les index de pièces jointes sont à base 1, et le payload doit faire au moins 1 Mio pour emprunter le chemin streaming. Exécutez le même test sur chaque compilateur que vous livrez, puisque cette classe d'erreur passe sur Delphi et n'échoue que sur 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 Mio ou plus emprunte le chemin DeflateStream par blocs dans 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;
Cas particuliers de la branche FPC
La branche FPC applique des règles de décodage tolérantes. Quand MaxOutput est atteint, InflateStream FPC tronque à la limite et rend la main, tandis que la branche Delphi lève ERangeError, et FPC accepte encore une sortie partiellement décodée quand inflate signale une erreur de données, car certains producteurs de PDF émettent des flux tronqués ou au checksum cassé. Un PDF qui contient déjà des membres concaténés, quel qu'en soit l'auteur, s'arrête au premier Z_STREAM_END dans tous les lecteurs. N'essayez pas de réparer un tel fichier en décodant puis en réencodant le flux dans la bibliothèque : cela ne rend que la troncature à 64 Ko définitive. Réintégrez plutôt la pièce jointe depuis sa source d'origine. La boucle FPC traite une lecture qui rend moins qu'un bloc complet comme la fin des données, ce qui vaut pour des flux comme TFileStream et TMemoryStream, aussi un TStream personnalisé passé à AddAssociatedFileFromStream a tout intérêt à être copié d'abord dans un TMemoryStream
Le DeflateStream et le InflateStream FPC à état unique sont livrés dans PDF Library for Delphi, qui cible Delphi, C++Builder et Free Pascal depuis un seul arbre source, si bien qu'une grosse pièce jointe revient d'un build FPC octet pour octet, exactement comme depuis Delphi