Ir al contenido

Flujos y filtros

Evidence: Standard-backed

La mayoría de los bytes de un PDF real reside dentro de flujos: el contenido de las páginas, las fuentes, las imágenes y el propio flujo de referencias cruzadas. Casi ninguno de esos bytes se almacena en bruto: primero pasa por uno o varios filtros. Esta página explica qué filtros suelen aparecer, para qué sirve cada uno, dónde causan problemas y por qué NextPDF fija la compresión para que la misma entrada produzca siempre los mismos bytes.

Un flujo y sus filtros son un contrato: «estos bytes están comprimidos con deflate y luego codificados en base-85; decodificarlos en ese orden permite obtener los datos reales». Si la entrada /Filter no coincide con lo que son realmente los bytes, o el valor de /Length es incorrecto, o dos filtros aparecen en el orden equivocado, el flujo no se puede decodificar y el objeto que contenía se pierde. Un lector no recurre a heurísticas para adivinarlo; hace lo que el diccionario le indica.

Hay un segundo coste, menos visible. Si el compresor de una biblioteca no es determinista —una compilación de zlib distinta, un nivel distinto, límites internos de bloque distintos—, dos ejecuciones que deberían producir un PDF idéntico generan dos archivos diferentes. Eso rompe la reproducibilidad a nivel de bytes. Una reproducibilidad rota, a su vez, rompe las pruebas contra archivos de referencia, la verificación de compilaciones firmadas y cualquier flujo de trabajo que compare salidas. Los filtros determinan si el PDF es correcto y también si el PDF es el mismo.

  • Un objeto de flujo es un diccionario más un bloque de bytes, envuelto en streamendstream, con un /Length y normalmente un /Filter.
  • La entrada /Filter indica el filtro de decodificación, o un arreglo de filtros aplicados como una canalización, en orden.
  • Los filtros se dividen en dos familias: compresión (FlateDecode, LZWDecode, RunLengthDecode, DCTDecode, JPXDecode, JBIG2Decode) y transporte ASCII (ASCIIHexDecode, ASCII85Decode), más el filtro especial Crypt para el cifrado.
  • El filtro que aparece con más frecuencia es FlateDecode: zlib/deflate. Es el predeterminado para el contenido, las fuentes y el flujo de referencias cruzadas.
  • NextPDF fija su salida Flate a un nivel y un formato específicos, de modo que los mismos bytes de entrada se comprimen siempre en los mismos bytes de salida.

NextPDF emite los objetos de flujo mediante una única utilidad de búfer y comprime mediante un único compresor fijado a propósito.

BinaryBuffer::writeStream() (src/Support/BinaryBuffer.php) envuelve el contenido del flujo en su diccionario, escribiendo siempre un /Length igual a la longitud real en bytes y fusionando cualquier entrada adicional que proporcione el llamador, como /Filter. No existe ninguna ruta en la que la longitud declarada pueda discrepar de los bytes escritos, porque la longitud se toma de la propia cadena de contenido.

La compresión pasa por PinnedZlibCompressor (src/Writer/PinnedZlibCompressor.php). Esta clase existe por una razón concreta. gzcompress, sin un nivel explícito, usa el valor predeterminado del entorno de ejecución de zlib, que históricamente ha variado entre compilaciones. La cabecera zlib de 2 bytes incluso codifica el nivel de forma indirecta, por lo que «el valor predeterminado» no es una salida estable. El compresor fija el nivel al máximo de RFC 1951 y emite siempre deflate envuelto en zlib (cabecera RFC 1950 + cola Adler-32), que es exactamente lo que /Filter /FlateDecode espera. Un fallo grave de zlib se convierte en una excepción tipada en lugar de en un repliegue silencioso a una salida sin comprimir: un flujo nunca se emite silenciosamente en bruto.

El propio flujo de referencias cruzadas es un ejemplo práctico de todo esto: CrossReferenceStream (src/Core/CrossReferenceStream.php) construye una tabla binaria, la comprime y la emite como un objeto de flujo con /Type /XRef, un arreglo /W de anchos de campo y /Filter /FlateDecode. El índice que permite a un lector encontrar cada objeto es, en sí mismo, un flujo filtrado.

FiltroFamiliaPara qué sirveDónde falla
FlateDecodeCompresiónzlib/deflate; el predeterminado para contenido, fuentes y flujos de referencias cruzadasUna compilación de zlib no determinista hace que PDF «idénticos» difieran byte a byte
LZWDecodeCompresiónCompresión Lempel–Ziv–Welch más antiguaHeredado; sustituido por Flate, aún visible de vez en cuando en archivos antiguos
DCTDecodeCompresiónImágenes JPEG en color/escala de grisesCon pérdida: volver a codificar una imagen que ya está en DCT la degrada de nuevo
JPXDecodeCompresiónDatos de imagen wavelet JPEG 2000No permitido por algunos perfiles de archivado; el soporte amplio es desigual
JBIG2DecodeCompresiónCompresión de imágenes binivel (1 bit)No debe usarse con imágenes en línea; los modos con pérdida pueden alterar los escaneados
RunLengthDecodeCompresiónCodificación de longitud de series orientada a bytesSolo ayuda con datos que tienen series largas de un mismo byte; puede aumentar el tamaño de otros datos
ASCIIHexDecodeTransporteBinario como dígitos hexadecimalesDuplica el tamaño; solo sirve para canales seguros de 7 bits, nunca para reducir tamaño
ASCII85DecodeTransporteBinario como ASCII en base-85~25 % de sobrecarga; una conveniencia de transporte, no compresión
CryptSeguridadAplica el gestor de seguridad del documentoUn flujo de referencias cruzadas no debe usar un filtro Crypt

El conjunto de filtros estándar de PDF, por familia, con el fallo típico que cada uno lleva asociado. NextPDF escribe FlateDecode para el contenido, las fuentes y el flujo de referencias cruzadas; los filtros de transporte ASCII son para canales de 7 bits, nunca para reducir el tamaño.

El mecanismo de filtros está definido por Spec: ISO 32000-2, §7.4 . Los filtros de un flujo se especifican mediante la entrada /Filter de su diccionario, y los filtros pueden encadenarse para formar una canalización que pasa el flujo por dos o más transformaciones de decodificación en secuencia. El ejemplo del propio estándar es LZW seguido de ASCII en base-85, decodificados en ese orden. Un escritor codifica un flujo para comprimirlo o para hacerlo seguro en 7 bits. Un lector invoca los filtros de decodificación correspondientes para recuperar los datos originales. Evidence: Standard-backed

La tabla de filtros del estándar clasifica cada filtro. FlateDecode descomprime datos codificados con zlib/deflate, reproduciendo el texto o los datos binarios originales. DCTDecode reproduce muestras de imagen que aproximan el original mediante JPEG; la palabra «aproximan» es la forma en que el estándar indica que es con pérdida. LZWDecode, RunLengthDecode, JBIG2Decode, JPXDecode y el filtro Crypt también se definen allí, y JBIG2 está explícitamente vetado para imágenes en línea.

El flujo de referencias cruzadas aplica a sí mismo la propia maquinaria del formato: es un objeto de flujo (/Type /XRef, Spec: ISO 32000-2, §7.5.8 ) cuyo arreglo /W indica el ancho en bytes de cada campo de entrada en el flujo decodificado. El estándar exige que no esté cifrado y que no use un filtro Crypt. El CrossReferenceStream de NextPDF lo sigue exactamente: FlateDecode, /W explícito, sin cifrado.

Un flujo de contenido de página, comprimido con Flate. Esta es, de largo, la forma más común: un diccionario con /Length y /Filter, y después los bytes comprimidos entre stream y 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

Un lector hace lo inverso: lee /Length bytes, los pasa por FlateDecode porque /Filter así lo indica, y recupera los operadores originales. Fijar el compresor hace que ese viaje de ida y vuelta no solo sea correcto, sino idéntico cada vez, que es de lo que dependen las comprobaciones de archivos de referencia y de compilación firmada.

La trampa es tratar los filtros ASCII como compresión. ASCIIHexDecode y ASCII85Decode hacen que un flujo sea más grande: aproximadamente el doble y aproximadamente un 25 %, respectivamente. Existen para mover datos binarios por un canal que solo admite con seguridad texto de 7 bits, no para ahorrar espacio. Elegir ASCII85 para «reducir» un PDF consigue lo contrario. La segunda mitad del mismo concepto erróneo es creer que FlateDecode es sin pérdida para las imágenes «gratis». Flate es sin pérdida, pero si la imagen ya estaba codificada con DCT (JPEG), volver a envolverla o transcodificarla a través de un filtro con pérdida la degrada con independencia de lo que haga Flate a su alrededor. La canalización de filtros preserva exactamente lo que se le entrega, incluido un artefacto de recompresión que se le entregó por accidente.

Esta página explica cómo se declaran y aplican los filtros, no el algoritmo a nivel de bits de cada uno. La garantía de determinismo se refiere específicamente a la salida Flate de NextPDF para los flujos que escribe. Se mantiene entre versiones menores de PHP y compilaciones de zlib conformes al estándar, pero el estándar permite explícitamente que un codificador deflate elija límites internos de bloque distintos, de modo que no se promete una salida byte a byte idéntica entre implementaciones de zlib genuinamente distintas (por ejemplo, un zlib estándar frente a zlib-ng). Por eso se fija el entorno de compilación.

NextPDF elige FlateDecode y los filtros de transporte ASCII para los datos que emite. No es un transcodificador de imágenes. No promete reempaquetar un flujo entrante arbitrario de JPEG2000 o JBIG2, y las limitaciones de las imágenes con pérdida son una propiedad de los datos de origen, no algo que un escritor pueda deshacer.

¿Por qué FlateDecode está en todas partes? Es sin pérdida, de propósito general, está bien soportado y encaja bien con el contenido de texto y operadores de la mayoría de los PDF. Es el valor predeterminado seguro para los flujos de contenido, las fuentes incrustadas y el flujo de referencias cruzadas.

¿Puedo desactivar la compresión? Puede omitirse /Filter y almacenar los bytes en bruto, y un lector lo aceptará. El archivo se hace más grande y no mejora nada más; rara vez hay un motivo fuera de la depuración.

¿Por qué fijar el nivel de compresión? Para que la salida sea reproducible. Un nivel sin fijar (o una compilación de zlib distinta) puede cambiar los bytes comprimidos sin cambiar el contenido descomprimido: la salida sigue siendo correcta, pero no idéntica, lo que invalida la verificación a nivel de bytes.

  • Objeto de flujo: un diccionario más un bloque de bytes entre stream y endstream, con un /Length y normalmente un /Filter.
  • Filtro: una transformación de decodificación con nombre que un lector aplica a los bytes de un flujo (por ejemplo, FlateDecode).
  • Canalización de filtros: un arreglo de filtros aplicados en secuencia; el orden del arreglo es el orden de decodificación.
  • FlateDecode: el filtro zlib/deflate; la compresión predeterminada para contenido, fuentes y flujos de referencias cruzadas.
  • DCTDecode: el filtro de imágenes JPEG; con pérdida, por lo que volver a codificar degrada la imagen de nuevo.
  • Filtro de transporte ASCII: ASCIIHexDecode / ASCII85Decode; hace que los datos sean seguros en 7 bits a costa del tamaño, no compresión.
  • Compresión determinista: producir una salida comprimida byte a byte idéntica para una entrada idéntica, lograda fijando el nivel y el formato del compresor.