Потоки и фильтры
ISO 32000-2 §7.4 Evidence: Standard-backed
В реальном PDF большая часть байтов находится внутри потоков: содержимое страниц, шрифты, изображения и сам поток таблицы перекрёстных ссылок. Почти все эти байты хранятся не в сыром виде: сначала они проходят через один или несколько фильтров. На этой странице рассматривается, какие фильтры вам встретятся, для чего нужен каждый из них, где они создают проблемы и почему NextPDF фиксирует сжатие, чтобы один и тот же ввод всегда давал одни и те же байты.
Почему это важно
Заголовок раздела «Почему это важно»Поток и его фильтр — это контракт: эти байты сжаты с помощью deflate, затем закодированы в base-85; декодируйте их в этом порядке, чтобы получить реальные данные. Если запись /Filter не соответствует тому, чем байты являются на самом деле, либо значение /Length неверно, либо два фильтра перечислены в неправильном порядке, поток невозможно декодировать, и содержащийся в нём объект теряется. Читатель не подбирает эвристики; он делает то, что предписывает словарь.
Есть и вторая, менее очевидная цена. Если компрессор библиотеки недетерминирован — другая сборка zlib, другой уровень, другие внутренние границы блоков, — то два запуска, которые должны были бы дать идентичный PDF, дают два разных файла. Это нарушает побайтовую воспроизводимость. Потеря воспроизводимости затем ломает эталонные (golden-file) тесты, проверку подписанных сборок и любой конвейер, который сравнивает вывод. Фильтры определяют и то, корректен ли PDF, и то, тот ли это самый PDF.
Если коротко
Заголовок раздела «Если коротко»- Любой объект-поток — это словарь плюс блок байтов, заключённый между
stream…endstream, с записью/Lengthи обычно/Filter. - Запись
/Filterуказывает фильтр декодирования — или массив фильтров, применяемых как конвейер, по порядку. - Фильтры делятся на два семейства: сжатие (FlateDecode, LZWDecode, RunLengthDecode, DCTDecode, JPXDecode, JBIG2Decode) и ASCII-транспорт (ASCIIHexDecode, ASCII85Decode), плюс особый фильтр Crypt для шифрования.
- Чаще всего встречается FlateDecode — zlib/deflate. Это значение по умолчанию для содержимого, шрифтов и потока перекрёстных ссылок.
- NextPDF фиксирует вывод Flate на определённом уровне и формате, чтобы одни и те же входные байты всегда сжимались в одни и те же выходные байты.
Как NextPDF подходит к этому
Заголовок раздела «Как NextPDF подходит к этому»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 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 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, а компромиссы сжатия изображений с потерями — это свойство исходных данных, а не то, что записывающая сторона может отменить.
Мини-FAQ
Заголовок раздела «Мини-FAQ»Почему FlateDecode повсюду? Он работает без потерь, универсален, хорошо поддерживается и хорошо подходит для содержимого большинства PDF, состоящего из текста и операторов. Это безопасное значение по умолчанию для потоков содержимого, встроенных шрифтов и потока перекрёстных ссылок.
Можно ли отключить сжатие? Да, можно опустить /Filter и хранить сырые байты, и читатель их примет. Файл становится больше, и больше ничего не улучшается; вне отладки редко есть смысл делать это.
Зачем вообще фиксировать уровень сжатия? Чтобы вывод был воспроизводимым. Незафиксированный уровень (или сборка zlib) может изменить сжатые байты, не меняя распакованного содержимого, — корректно, но не идентично, что сводит на нет побайтовую проверку.
Связанная документация
Заголовок раздела «Связанная документация»- Что на самом деле представляет собой PDF — объектная модель, в которой находятся описанные здесь потоки.
- Шрифты: самое сложное — встроенные программы шрифтов представляют собой отфильтрованные потоки со своими режимами сбоя.
- PDF 2.0: что изменилось — как базовый уровень 2.0 обрабатывает потоки и поток перекрёстных ссылок, который NextPDF использует по умолчанию.
Глоссарий
Заголовок раздела «Глоссарий»- Объект-поток — словарь плюс блок байтов между
streamиendstream; содержит/Lengthи обычно/Filter. - Фильтр — именованное преобразование декодирования, которое читатель применяет к байтам потока (например,
FlateDecode). - Конвейер фильтров — массив фильтров, применяемых последовательно; порядок массива — это порядок декодирования.
- FlateDecode — фильтр zlib/deflate; сжатие по умолчанию для содержимого, шрифтов и потоков перекрёстных ссылок.
- DCTDecode — фильтр изображений JPEG; с потерями, поэтому повторное кодирование снова ухудшает изображение.
- ASCII-транспортный фильтр — ASCIIHexDecode / ASCII85Decode; делает данные безопасными для 7-битного канала ценой увеличения размера — это не сжатие.
- Детерминированное сжатие — получение сжатого результата, идентичного байт в байт, для идентичного ввода, достигаемое фиксацией уровня и формата компрессора.