Pular para o conteúdo

Fluxos e filtros

Evidence: Standard-backed

A maior parte dos bytes de um PDF de verdade fica dentro de fluxos: conteúdo de página, fontes, imagens e o próprio fluxo de referências cruzadas. Praticamente nenhum desses bytes é armazenado em bruto; antes disso, eles passam por um ou mais filtros. Esta página explica quais filtros você encontra, para que cada um serve, onde eles causam problemas e por que o NextPDF fixa a compressão para que a mesma entrada produza sempre os mesmos bytes.

Um fluxo e seu filtro são um contrato: “estes bytes estão comprimidos com deflate e depois codificados em base-85 — decodifique nessa ordem para obter os dados reais.” Se a entrada /Filter discordar do que os bytes realmente são, se o /Length estiver errado ou se dois filtros forem listados na ordem errada, o fluxo fica indecodificável e o objeto que ele carregava é perdido. Um leitor não adivinha por heurística; ele faz o que o dicionário manda.

Há um segundo custo, mais discreto. Se o compressor de uma biblioteca for não determinístico — build de zlib diferente, nível diferente, fronteiras de bloco internas diferentes — duas execuções que deveriam produzir um PDF idêntico acabam gerando dois arquivos diferentes. Isso quebra a reprodutibilidade no nível dos bytes. Quando a reprodutibilidade quebra, também quebram os testes de golden file, a verificação de builds assinados e qualquer pipeline que compare a saída. Os filtros determinam tanto se o PDF está correto quanto se o PDF é o mesmo.

  • Um objeto de fluxo é um dicionário mais um bloco de bytes, envolvido em streamendstream, com um /Length e geralmente um /Filter.
  • A entrada /Filter nomeia o filtro de decodificação — ou um array de filtros aplicados como um pipeline, em ordem.
  • Os filtros se dividem em duas famílias: compressão (FlateDecode, LZWDecode, RunLengthDecode, DCTDecode, JPXDecode, JBIG2Decode) e transporte ASCII (ASCIIHexDecode, ASCII85Decode), mais o filtro especial Crypt para criptografia.
  • O que você verá com mais frequência é o FlateDecode — zlib/deflate. É o padrão para conteúdo, fontes e o fluxo de referências cruzadas.
  • O NextPDF fixa a saída Flate em um nível e formato definidos para que os mesmos bytes de entrada comprimam sempre nos mesmos bytes de saída.

O NextPDF emite objetos de fluxo por um único auxiliar de buffer e comprime por um único compressor fixo — de propósito.

BinaryBuffer::writeStream() (src/Support/BinaryBuffer.php) envolve o conteúdo do fluxo no dicionário correspondente, sempre escrevendo um /Length igual ao comprimento real em bytes e mesclando quaisquer entradas extras que o chamador forneça, como /Filter. Não há caminho em que o comprimento declarado possa divergir dos bytes escritos, porque o comprimento é obtido da própria string de conteúdo.

A compressão passa pelo PinnedZlibCompressor (src/Writer/PinnedZlibCompressor.php). Esta classe existe por um único motivo. O gzcompress sem um nível explícito recorre ao padrão do runtime do zlib, que historicamente variou entre builds. O cabeçalho zlib de 2 bytes inclusive codifica o nível de forma indireta, então “o padrão” não é uma saída estável. O compressor fixa o nível no máximo da RFC 1951 e sempre emite deflate envolto em zlib (cabeçalho RFC 1950 + trailer Adler-32), exatamente o que o /Filter /FlateDecode espera. Uma falha grave do zlib vira uma exceção tipada, em vez de cair silenciosamente para uma saída não comprimida — um fluxo nunca é emitido em bruto sem aviso.

O próprio fluxo de referências cruzadas é um exemplo prático de tudo isso: o CrossReferenceStream (src/Core/CrossReferenceStream.php) constrói uma tabela binária, comprime-a e emite-a como um objeto de fluxo com /Type /XRef, um array de larguras de campo /W e /Filter /FlateDecode. O índice que permite a um leitor encontrar cada objeto é, ele próprio, um fluxo filtrado.

FiltroFamíliaPara que serveOnde falha
FlateDecodeCompressãozlib/deflate; o padrão para conteúdo, fontes e fluxos xrefUm build de zlib não determinístico faz PDFs “idênticos” diferirem byte a byte
LZWDecodeCompressãoCompressão Lempel–Ziv–Welch mais antigaLegado; substituída pelo Flate, mas ocasionalmente ainda vista em arquivos antigos
DCTDecodeCompressãoImagens JPEG colour/grayscaleCom perdas — recodificar uma imagem que já é DCT a degrada de novo
JPXDecodeCompressãoDados de imagem wavelet JPEG 2000Não permitido por alguns perfis de arquivamento; o suporte amplo é irregular
JBIG2DecodeCompressãoCompressão de imagens bilevel (1 bit)Não deve ser usado com imagens inline; modos com perdas podem alterar digitalizações
RunLengthDecodeCompressãoRun-length orientado a bytesSó ajuda com dados que têm longas sequências de um único byte; pode aumentar outros dados
ASCIIHexDecodeTransporteBinário como dígitos hexadecimaisDobra o tamanho; só para canais seguros de 7 bits, nunca para tamanho
ASCII85DecodeTransporteBinário como ASCII base-85~25% de sobrecarga; uma conveniência de transporte, não compressão
CryptSegurançaAplica o manipulador de segurança do documentoUm fluxo de referências cruzadas não deve usar um filtro Crypt

O conjunto de filtros padrão do PDF, por família, com a falha associada a cada um. O NextPDF escreve FlateDecode para conteúdo, fontes e o fluxo de referências cruzadas; os filtros de transporte ASCII servem para canais de 7 bits, nunca para reduzir tamanho.

O mecanismo de filtros é definido pela Spec: ISO 32000-2, §7.4 . Um dicionário de fluxo nomeia seus filtros por meio do /Filter. Quando a entrada lista mais de um filtro, esses filtros formam um pipeline de decodificação e são aplicados em sequência. Um escritor codifica um fluxo para comprimi-lo ou para torná-lo seguro em 7 bits. Um leitor invoca os filtros de decodificação correspondentes para recuperar os dados originais. Evidence: Standard-backed

A tabela de filtros do padrão classifica cada filtro. O FlateDecode descomprime dados codificados em zlib/deflate-encoded, reproduzindo o texto ou os dados binários originais. O DCTDecode reproduz amostras de imagem que aproximam o original via JPEG — a palavra “aproximam” é o padrão indicando que o processo é com perdas. LZWDecode, RunLengthDecode, JBIG2Decode, JPXDecode e o filtro Crypt também estão definidos ali, com o JBIG2 explicitamente proibido em imagens inline.

O fluxo de referências cruzadas aplica a própria maquinaria do formato a si mesmo: é um objeto de fluxo (/Type /XRef, Spec: ISO 32000-2, §7.5.8 ) cujo array /W indica a largura em bytes de cada campo de entrada no fluxo decodificado. O padrão exige que ele não seja criptografado e não use um filtro Crypt. O NextPDF mantém seu CrossReferenceStream seguindo isso exatamente — FlateDecode, /W explícito, sem criptografia.

Um fluxo de conteúdo de página, comprimido com Flate. Esta é a forma mais comum de longe: um dicionário com /Length e /Filter e, em seguida, os bytes comprimidos entre stream e endstream.

<?php
declare(strict_types=1);
use NextPDF\Writer\PinnedZlibCompressor;
// The marking operators a page content stream carries, uncompressed.
$content = "BT /F1 12 Tf 72 712 Td (Hello) Tj ET\n";
// NextPDF compresses through the pinned compressor: fixed level,
// fixed zlib-wrapped format. The same $content always yields the
// same $compressed bytes, on any supported PHP/zlib build.
$compressed = PinnedZlibCompressor::compress($content);
// Emitted as a stream object. /Length is the real byte length of
// $compressed; /Filter names the decode the reader must apply.
// N 0 obj
// << /Length <strlen($compressed)> /Filter /FlateDecode >>
// stream
// <$compressed bytes>
// endstream
// endobj

Um leitor faz o inverso: lê /Length bytes, passa-os pelo FlateDecode porque o /Filter assim manda e recupera os operadores originais. Fixe o compressor, e essa ida e volta não é apenas correta. Ela é idêntica todas as vezes, que é o que sustenta as verificações de golden file e de builds assinados.

A armadilha é tratar os filtros ASCII como compressão. O ASCIIHexDecode e o ASCII85Decode tornam um fluxo maior — aproximadamente o dobro e aproximadamente 25%, respectivamente. Eles existem para mover dados binários por um canal que só é seguro para texto de 7 bits, não para economizar espaço. Escolher o ASCII85 para “encolher” um PDF faz o oposto. A segunda metade do mesmo equívoco é acreditar que o FlateDecode torna imagens sem perdas “de graça”. O Flate é sem perdas, mas, se a imagem já estava codificada em DCT (JPEG), envolvê-la de novo ou transcodificá-la por um filtro com perdas a degrada, independentemente do que o Flate faça ao redor. O pipeline de filtros preserva exatamente o que você lhe dá — incluindo um artefato de recompressão que você entregou por acidente.

Esta página aborda como os filtros são declarados e aplicados, não o algoritmo em nível de bits dentro de cada um. A garantia de determinismo diz respeito especificamente à saída Flate do NextPDF para os fluxos que ele escreve. Ela vale em versões menores do PHP e em builds de zlib conformes ao padrão, mas o padrão permite explicitamente que um codificador deflate escolha fronteiras de bloco internas diferentes; portanto, uma saída byte a byte idêntica entre implementações de zlib genuinamente diferentes (por exemplo, um zlib comum versus zlib-ng) não é prometida. O ambiente de build é fixado por esse motivo.

O NextPDF escolhe o FlateDecode e os filtros de transporte ASCII para os dados que emite. Ele não é um transcodificador de imagens. Ele não promete reempacotar um fluxo JPEG2000 ou JBIG2 de entrada arbitrário, e os compromissos de imagens com perdas são uma propriedade dos dados de origem, não algo que um escritor consiga desfazer.

Por que o FlateDecode está em toda parte? Ele é sem perdas, de propósito geral, bem suportado e um bom ajuste para o conteúdo de texto e operadores da maioria dos PDFs. É o padrão seguro para fluxos de conteúdo, fontes embutidas e o fluxo de referências cruzadas.

Posso desligar a compressão? Você pode omitir o /Filter e armazenar bytes em bruto, e um leitor vai aceitar. O arquivo fica maior e nada mais melhora; raramente há motivo para isso fora de depuração.

Por que fixar o nível de compressão, afinal? Para que a saída seja reprodutível. Um nível não fixado (ou um build de zlib) pode mudar os bytes comprimidos sem mudar o conteúdo descomprimido — correto, mas não idêntico, o que anula a verificação em nível de bytes.

  • Objeto de fluxo — um dicionário mais um bloco de bytes entre stream e endstream, carregando um /Length e geralmente um /Filter.
  • Filtro — uma transformação de decodificação nomeada que um leitor aplica aos bytes de um fluxo (por exemplo, FlateDecode).
  • Pipeline de filtros — um array de filtros aplicados em sequência; a ordem do array é a ordem de decodificação.
  • FlateDecode — o filtro zlib/deflate; a compressão padrão para conteúdo, fontes e fluxos de referências cruzadas.
  • DCTDecode — o filtro de imagem JPEG; com perdas, portanto recodificar degrada a imagem de novo.
  • Filtro de transporte ASCII — ASCIIHexDecode / ASCII85Decode; torna os dados seguros em 7 bits ao custo do tamanho — não compressão.
  • Compressão determinística — produzir uma saída comprimida byte a byte idêntica para uma entrada idêntica, conseguida fixando o nível e o formato do compressor.