Перейти к содержимому

Потоки и фильтры

Evidence: Standard-backed

В реальном PDF большая часть байтов находится внутри потоков: содержимое страниц, шрифты, изображения и сам поток таблицы перекрёстных ссылок. Почти все эти байты хранятся не в сыром виде: сначала они проходят через один или несколько фильтров. На этой странице рассматривается, какие фильтры вам встретятся, для чего нужен каждый из них, где они создают проблемы и почему NextPDF фиксирует сжатие, чтобы один и тот же ввод всегда давал одни и те же байты.

Поток и его фильтр — это контракт: эти байты сжаты с помощью deflate, затем закодированы в base-85; декодируйте их в этом порядке, чтобы получить реальные данные. Если запись /Filter не соответствует тому, чем байты являются на самом деле, либо значение /Length неверно, либо два фильтра перечислены в неправильном порядке, поток невозможно декодировать, и содержащийся в нём объект теряется. Читатель не подбирает эвристики; он делает то, что предписывает словарь.

Есть и вторая, менее очевидная цена. Если компрессор библиотеки недетерминирован — другая сборка zlib, другой уровень, другие внутренние границы блоков, — то два запуска, которые должны были бы дать идентичный PDF, дают два разных файла. Это нарушает побайтовую воспроизводимость. Потеря воспроизводимости затем ломает эталонные (golden-file) тесты, проверку подписанных сборок и любой конвейер, который сравнивает вывод. Фильтры определяют и то, корректен ли PDF, и то, тот ли это самый PDF.

  • Любой объект-поток — это словарь плюс блок байтов, заключённый между streamendstream, с записью /Length и обычно /Filter.
  • Запись /Filter указывает фильтр декодирования — или массив фильтров, применяемых как конвейер, по порядку.
  • Фильтры делятся на два семейства: сжатие (FlateDecode, LZWDecode, RunLengthDecode, DCTDecode, JPXDecode, JBIG2Decode) и ASCII-транспорт (ASCIIHexDecode, ASCII85Decode), плюс особый фильтр Crypt для шифрования.
  • Чаще всего встречается FlateDecode — zlib/deflate. Это значение по умолчанию для содержимого, шрифтов и потока перекрёстных ссылок.
  • NextPDF фиксирует вывод Flate на определённом уровне и формате, чтобы одни и те же входные байты всегда сжимались в одни и те же выходные байты.

NextPDF намеренно записывает объекты-потоки через единый буферный helper и сжимает их через один зафиксированный компрессор.

BinaryBuffer::writeStream() (src/Support/BinaryBuffer.php) оборачивает содержимое потока в его словарь, всегда записывает /Length, равный фактической длине в байтах, и добавляет любые дополнительные записи, которые передаёт вызывающий код, например /Filter. Нет сценария, в котором объявленная длина могла бы разойтись с записанными байтами, поскольку длина берётся из самой строки содержимого.

Сжатие выполняет PinnedZlibCompressor (src/Writer/PinnedZlibCompressor.php). Этот класс нужен по одной причине. gzcompress без явного уровня полагается на значение zlib по умолчанию в текущей среде, а оно исторически менялось от сборки к сборке. 2-байтовый заголовок zlib к тому же косвенно кодирует уровень, поэтому “значение по умолчанию” не является стабильным выводом. Компрессор фиксирует уровень на максимуме RFC 1951 и всегда выдаёт deflate в обёртке zlib (заголовок RFC 1950 + завершитель Adler-32), а это именно то, чего ожидает /Filter /FlateDecode. Сбой zlib становится типизированным исключением, а не молчаливым откатом к несжатому выводу — поток никогда не выдаётся тихо в сыром виде.

Сам поток перекрёстных ссылок хорошо показывает весь механизм: CrossReferenceStream (src/Core/CrossReferenceStream.php) строит двоичную таблицу, сжимает её и выдаёт как объект-поток с /Type /XRef, массивом ширин полей /W и /Filter /FlateDecode. Индекс, который позволяет читателю найти каждый объект, сам по себе является отфильтрованным потоком.

ФильтрСемействоДля чего нуженГде даёт сбой
FlateDecodeСжатиеzlib/deflate; значение по умолчанию для содержимого, шрифтов, потоков xrefНедетерминированная сборка zlib заставляет “идентичные” PDF различаться побайтово
LZWDecodeСжатиеБолее старое сжатие Лемпеля–Зива–ВелчаУстаревший; вытеснен Flate, изредка всё ещё встречается в старых файлах
DCTDecodeСжатиеИзображения в кодировке JPEG, colour/grayscaleС потерями — повторное кодирование изображения, уже закодированного DCT, снова ухудшает его
JPXDecodeСжатиеДанные изображений JPEG 2000 (вейвлет)Не допускается некоторыми архивными профилями; широта поддержки неравномерна
JBIG2DecodeСжатиеСжатие двухуровневых (1-битных) изображенийНе должен использоваться для встроенных изображений; режимы с потерями могут изменять сканы
RunLengthDecodeСжатиеПобайтовое кодирование длин серийПомогает только данным с длинными сериями одинаковых байтов; для других данных может увеличивать объём
ASCIIHexDecodeТранспортДвоичные данные в виде шестнадцатеричных цифрУдваивает размер; только для 7-битных каналов, никогда ради размера
ASCII85DecodeТранспортДвоичные данные в виде ASCII base-85Накладные расходы ~25%; удобство транспорта, а не сжатие
CryptБезопасностьПрименяет обработчик безопасности документаПоток перекрёстных ссылок не должен использовать фильтр Crypt

Стандартный набор фильтров PDF по семействам и типичный сбой для каждого из них. NextPDF записывает FlateDecode для содержимого, шрифтов и потока перекрёстных ссылок; ASCII-транспортные фильтры предназначены для 7-битных каналов, а не для уменьшения размера.

Механизм фильтров определён в Spec: ISO 32000-2, §7.4 . Словарь потока указывает свои фильтры через /Filter. Когда запись перечисляет более одного фильтра, эти фильтры образуют конвейер декодирования и применяются последовательно. Записывающая сторона кодирует поток, чтобы сжать его или сделать 7-битно-безопасным. Читающая сторона вызывает соответствующие фильтры декодирования, чтобы восстановить исходные данные. Evidence: Standard-backed

Таблица фильтров стандарта классифицирует каждый фильтр. FlateDecode распаковывает данные, закодированные zlib/deflate, воспроизводя исходный текст или двоичные данные. DCTDecode воспроизводит отсчёты изображения, которые приближают оригинал через JPEG: слово “приближают” в стандарте означает, что фильтр работает с потерями. LZWDecode, RunLengthDecode, JBIG2Decode, JPXDecode и фильтр Crypt также определены там, причём для встроенных изображений JBIG2 явно запрещён.

Поток перекрёстных ссылок применяет собственный механизм формата к самому себе: это объект-поток (/Type /XRef, Spec: ISO 32000-2, §7.5.8 ), чей массив /W указывает ширину в байтах каждого поля записи в декодированном потоке. Стандарт требует, чтобы он не был зашифрован и не использовал фильтр Crypt. NextPDF в CrossReferenceStream следует этому в точности — FlateDecode, явный /W, без шифрования.

Поток содержимого страницы, сжатый с помощью Flate. Это самая распространённая форма: словарь с /Length и /Filter, затем сжатые байты между stream и 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

Читатель делает обратное: считывает /Length байт, пропускает их через FlateDecode, потому что так предписывает /Filter, и получает обратно исходные операторы. Зафиксируйте компрессор — и этот полный цикл не просто корректен: он идентичен каждый раз, на что и опираются проверки эталонных файлов и подписанных сборок.

Типичная ошибка — считать ASCII-фильтры сжатием. ASCIIHexDecode и ASCII85Decode делают поток больше — примерно вдвое и примерно на 25% соответственно. Они существуют для того, чтобы перемещать двоичные данные через канал, безопасный только для 7-битного текста, а не для экономии места. Выбор ASCII85, чтобы “уменьшить” PDF, даёт противоположный эффект. Вторая часть того же заблуждения — вера в то, что FlateDecode сжимает изображения без потерь “бесплатно”. Flate действительно работает без потерь, но если изображение уже было закодировано в DCT (JPEG), повторное оборачивание или перекодирование через фильтр с потерями ухудшает его независимо от того, что делает вокруг Flate. Конвейер фильтров сохраняет ровно то, что вы в него подаёте, — включая артефакт повторного сжатия, который вы подали по ошибке.

Эта страница рассказывает, как фильтры объявляются и применяются, а не о побитовых алгоритмах внутри каждого из них. Гарантия детерминизма касается именно вывода Flate у NextPDF для потоков, которые он записывает. Она действует на разных минорных версиях PHP и на сборках zlib, соответствующих стандарту, но стандарт явно разрешает кодировщику deflate выбирать разные внутренние границы блоков, поэтому вывод, идентичный байт в байт, между действительно разными реализациями zlib (например, стоковый zlib против zlib-ng) не гарантируется. Именно по этой причине среда сборки зафиксирована.

NextPDF выбирает FlateDecode и ASCII-транспортные фильтры для данных, которые он выдаёт. Это не перекодировщик изображений. Он не обещает переупаковать произвольный входящий поток JPEG2000 или JBIG2, а компромиссы сжатия изображений с потерями — это свойство исходных данных, а не то, что записывающая сторона может отменить.

Почему FlateDecode повсюду? Он работает без потерь, универсален, хорошо поддерживается и хорошо подходит для содержимого большинства PDF, состоящего из текста и операторов. Это безопасное значение по умолчанию для потоков содержимого, встроенных шрифтов и потока перекрёстных ссылок.

Можно ли отключить сжатие? Да, можно опустить /Filter и хранить сырые байты, и читатель их примет. Файл становится больше, и больше ничего не улучшается; вне отладки редко есть смысл делать это.

Зачем вообще фиксировать уровень сжатия? Чтобы вывод был воспроизводимым. Незафиксированный уровень (или сборка zlib) может изменить сжатые байты, не меняя распакованного содержимого, — корректно, но не идентично, что сводит на нет побайтовую проверку.

  • Объект-поток — словарь плюс блок байтов между stream и endstream; содержит /Length и обычно /Filter.
  • Фильтр — именованное преобразование декодирования, которое читатель применяет к байтам потока (например, FlateDecode).
  • Конвейер фильтров — массив фильтров, применяемых последовательно; порядок массива — это порядок декодирования.
  • FlateDecode — фильтр zlib/deflate; сжатие по умолчанию для содержимого, шрифтов и потоков перекрёстных ссылок.
  • DCTDecode — фильтр изображений JPEG; с потерями, поэтому повторное кодирование снова ухудшает изображение.
  • ASCII-транспортный фильтр — ASCIIHexDecode / ASCII85Decode; делает данные безопасными для 7-битного канала ценой увеличения размера — это не сжатие.
  • Детерминированное сжатие — получение сжатого результата, идентичного байт в байт, для идентичного ввода, достигаемое фиксацией уровня и формата компрессора.