Um writer FlateDecode em blocos no Free Pascal precisa manter um único estado zlib vivo ao longo de todos os blocos. O atalho tentador, comprimir cada bloco de 64 KB por conta própria, dá a cada bloco seu próprio header e checksum de zlib, então um anexo de 1 MiB vira dezesseis membros zlib colados de ponta a ponta. O FlateDecode da ISO 32000-1 espera exatamente um stream zlib, e um decoder padrão para no primeiro marcador de fim de stream, o que significa que tudo depois dos primeiros 65.536 bytes desaparece em silêncio. A PDF Library for Delphi mantém um único estado zlib vivo ao longo de todos os blocos, tanto no DeflateStream quanto no InflateStream
A armadilha só existe do lado FPC de uma unit cross-compiler, e só no caminho de streaming em blocos, e é exatamente por isso que ela é fácil de deixar passar: o branch Delphi e os helpers baseados em string produzem cada um um stream zlib único, e payloads de teste pequenos nunca chegam ao código em blocos. É primo próximo das armadilhas de cinco armadilhas de port para FPC que o Delphi costuma esconder, com a diferença de que aqui o Delphi não está cobrindo nada. O atalho simplesmente se apoia num modelo mental errado do que um stream zlib é
Por que um stream zlib em blocos é truncado por outros leitores de PDF?
Porque um stream zlib como definido na RFC 1950 é um container só, não uma sequência deles, e um inflater em conformidade trata o primeiro trailer Adler-32 como o fim dos dados. O formato é um header de dois bytes, um bit stream deflate contínuo da RFC 1951 cujo último bloco carrega a flag de bloco final, e um checksum Adler-32 de quatro bytes sobre todos os bytes descomprimidos. A ISO 32000-1 §7.4.4 define o /FlateDecode exatamente nesses termos. Quando o inflate chega ao trailer ele retorna Z_STREAM_END e deixa qualquer input restante sem ler em avail_in. Do ponto de vista dele nada está errado, então ele não levanta erro nenhum, e os bytes depois do trailer são simplesmente ignorados. Membros concatenados são uma ideia legítima no gzip (a RFC 1952 permite vários membros num arquivo), e provavelmente foi daí que veio a intuição, mas o zlib não tem regra assim e o PDF nunca pediu uma
O DeflateStream ingênuo do FPC lê a fonte 64 KB por vez e entrega cada bloco ao ZFPCCompress, um helper que roda o próprio deflateInit2, comprime com Z_FINISH e chama deflateEnd. Cada bloco sai então como um stream zlib completo, válido e auto-terminado, e a função concatena tudo num único AnsiString antes de escrever. O resultado parece dado Flate, tem header correto e decodifica sem erro, mas decodifica apenas os primeiros 65.536 bytes. Do lado da leitura há a armadilha espelhada: chamar o ZFPCInflate uma vez por 64 KB de input comprimido inicia um inflateInit2 novo a cada chamada. O segundo bloco começa no meio de um bit stream deflate, sem header de zlib, então um inflater novo o rejeita, e um stream de membro único perfeitamente normal vindo de qualquer outro produtor é decodificado só até onde os primeiros 64 KB de bytes comprimidos o levam
// DeflateStream do FPC ingênuo em blocos (simplificado):
// o ZFPCCompress faz deflateInit2 / deflate(Z_FINISH) / deflateEnd,
// então cada bloco de 64 KB vira um membro zlib separado
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));
Quais chamadas da PDF Library for Delphi chegam ao caminho em blocos?
No FPC, o caminho em blocos escreve todo embedded file de 1 MiB ou mais, e lê todo stream Flate extraído pela API de streaming assim que o tamanho comprimido passa de um bloco. O TPDFStream.ReadFromStream decide como codificar os dados que chegam. Quando Deflate é true e Stream.Size >= 1048576, ele faz streaming pelo DeflateStream; abaixo desse limiar ele lê a fonte inteira na memória e chama o DeflateStr, o helper single-shot que comprime tudo numa chamada só. A cadeia de filtros ASCII85 mais Flate pula o teste de tamanho e sempre passa pelo DeflateStream, então nesse caminho qualquer payload maior que 64 KB já se estende por vários blocos. Os pontos de entrada públicos que alimentam o ReadFromStream com compressão ligada são os writers de embedded file:
TPDFlib.EmbedFileeTPDFlib.AddEmbeddedFile, que leem um arquivo do disco para um stream/EmbeddedFileTPDFlib.AddAssociatedFileFromStreameTPDFlib.AddAssociatedFileFromFile, os writers de associated file do PDF/A-3 usados para XML de e-invoice e outros dados-fonte- Do lado da leitura,
TPDFlib.GetEmbeddedFileContentToStreameGetEmbeddedFileContentToFile, que decodificam viaTPDFStream.WriteDecodedToStreame daí viaInflateStream
Errar isso falha em silêncio em todas as camadas. O writer armazena o /Params /Size e um /CheckSum MD5 calculado a partir do arquivo original, então o dicionário do anexo anuncia o tamanho completo mesmo quando o stream guarda dezesseis membros. Qualquer leitor em conformidade, inclusive uma build Delphi da biblioteca, para limpatamente no primeiro Z_STREAM_END e devolve exatamente 65.536 bytes. O GetEmbeddedFileContentToStream retorna 1, porque ele reporta se a decodificação levantou erro, não se o output casa com o /Size. Quem já perseguiu um problema de documento grande em merge e split de PDFs gigabyte conhece esse padrão: o arquivo abre, a contagem de páginas está certa, e o dano só aparece quando alguém abre o anexo
Um estado deflate ao longo de todos os blocos
O DeflateStream em PDFlibZLib.pas inicializa um único paszlib.TZStream, alimenta cada bloco no deflate com Z_NO_FLUSH e só no final drena o compressor com Z_FINISH até ele retornar Z_STREAM_END. Isso produz exatamente um header, um bit stream deflate cujas back-references podem atravessar fronteiras de bloco, e um Adler-32 sobre o input inteiro. O branch FPC tem a mesma estrutura que o branch Delphi. Ele também escreve o output à medida que é produzido, em vez de concatenar primeiro o resultado comprimido inteiro num AnsiString, então o writer nunca monta uma segunda cópia completa dos dados comprimidos na memória antes de copiá-los para o destino
// DeflateStream do FPC com um único estado zlib (caminhos de erro removidos)
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); // mesmo estado, sem quebra de 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 // um trailer para o input inteiro
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;
O InflateStream segue o mesmo padrão: um inflateInit2, um loop interno que segue chamando inflate até o bloco atual ser consumido e o buffer de output não estar mais cheio, e uma parada em Z_STREAM_END. Há um efeito colateral que vale conhecer. Como o writer chama o deflateInit2 ele mesmo, em vez de passar por um helper de level fixo, ele honra o PLDeflateLevel, então um level definido via TPDFlib.SetCompressionLevel(1..9) vale também para embedded files grandes no FPC. Isso importa se você já ajusta a compressão para output de arquivamento, como descrito em reduzir o tamanho de arquivo PDF no Delphi
Como verificar que um stream Flate é um único membro zlib?
Infla com um decoder zlib comum e confere duas coisas quando ele retorna Z_STREAM_END: o tamanho decodificado é igual ao tamanho da fonte, e o avail_in está zerado. Input sobrando depois do marcador de fim é a assinatura de um stream concatenado. A rotina abaixo é a versão FPC dessa checagem, construída direto sobre o paszlib para não confiar no código sob teste, seguida de uma sonda que empurra 200.000 bytes, quatro blocos de 64 KB, pelo 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;
// Um membro termina exatamente no último byte de input
Result:= (Status = Z_STREAM_END) and (strm.avail_in = 0);
finally
inflateEnd(strm);
end;
end;
// Empurra 200.000 bytes pelo DeflateStream com o chunk padrão de 64 KB
// e exige um único membro que decodifique de volta ao tamanho completo
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');
No nível da aplicação, a asserção útil é justamente aquela que a biblioteca não faz por você: compare o que sai de um anexo com o /Params /Size registrado na entrada. O GetEmbeddedFileIntProperty com tag 5 devolve esse tamanho registrado, os índices de embedded file são 1-based, e o payload precisa ter pelo menos 1 MiB para exercitar o caminho de streaming. Rode o mesmo teste em cada compilador com que você distribui, já que essa classe de erro passa no Delphi e falha só no 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 ou mais segue o caminho do DeflateStream em blocos no 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;
Casos de borda no branch FPC
O branch FPC usa regras de decodificação tolerantes. Quando o MaxOutput é atingido, o InflateStream do FPC trunca no limite e retorna, enquanto o branch Delphi levanta ERangeError, e o FPC ainda aceita output parcialmente decodificado quando o inflate reporta erro de dados, porque alguns produtores de PDF emitem streams truncados ou com checksum quebrado. Um PDF que já contém membros concatenados, não importa quem o escreveu, para no primeiro Z_STREAM_END em qualquer leitor. Não tente curar um arquivo desses decodificando e recodificando o stream dentro da biblioteca, porque isso só torna o truncamento de 64 KB permanente. Re-embeba o anexo a partir da fonte original. O loop do FPC trata uma leitura que devolve menos que um bloco cheio como fim dos dados, o que vale para streams como TFileStream e TMemoryStream, então um TStream customizado passado ao AddAssociatedFileFromStream fica mais seguro copiado para um TMemoryStream primeiro
O DeflateStream e o InflateStream do FPC com estado único entram na PDF Library for Delphi, que atende Delphi, C++Builder e Free Pascal a partir de uma árvore de código única, então um anexo grande volta de uma build FPC byte a byte, do mesmo jeito que volta do Delphi